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

This commit is contained in:
Sara Arjona 2023-01-04 16:57:31 +01:00
commit 908f3a4c78
25 changed files with 326 additions and 35 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

@ -59,7 +59,7 @@ define(
// component compatible formats and the default actions.js won't be necessary anymore. // component compatible formats and the default actions.js won't be necessary anymore.
// Meanwhile, we filter the migrated actions. // Meanwhile, we filter the migrated actions.
const componentActions = [ const componentActions = [
'moveSection', 'moveCm', 'addSection', 'deleteSection', 'cmDelete', 'sectionHide', 'sectionShow', 'moveSection', 'moveCm', 'addSection', 'deleteSection', 'cmDelete', 'cmDuplicate', 'sectionHide', 'sectionShow',
'cmHide', 'cmShow', 'cmStealth', 'sectionHighlight', 'sectionUnhighlight', 'cmHide', 'cmShow', 'cmStealth', 'sectionHighlight', 'sectionUnhighlight',
]; ];

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

View file

@ -10,6 +10,6 @@ define("core_courseformat/local/courseeditor/dndcmitem",["exports","core/reactiv
* @copyright 2021 Ferran Recio <ferran@moodle.com> * @copyright 2021 Ferran Recio <ferran@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @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})); 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,event){if(dropdata.id!=this.id&&dropdata.nextcmid!=this.id){const mutation=event.altKey?"cmDuplicate":"cmMove";this.reactive.dispatch(mutation,[dropdata.id],null,this.id)}}}return _exports.default=_default,_exports.default}));
//# sourceMappingURL=dndcmitem.min.js.map //# sourceMappingURL=dndcmitem.min.js.map

File diff suppressed because one or more lines are too long

View file

@ -10,6 +10,6 @@ define("core_courseformat/local/courseeditor/dndsection",["exports","core/reacti
* @copyright 2021 Ferran Recio <ferran@moodle.com> * @copyright 2021 Ferran Recio <ferran@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @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})); 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,event){if("cm"==dropdata.type){const mutation=event.altKey?"cmDuplicate":"cmMove";this.reactive.dispatch(mutation,[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 //# sourceMappingURL=dndsection.min.js.map

File diff suppressed because one or more lines are too long

View file

@ -10,6 +10,6 @@ define("core_courseformat/local/courseeditor/dndsectionitem",["exports","core/re
* @copyright 2021 Ferran Recio <ferran@moodle.com> * @copyright 2021 Ferran Recio <ferran@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @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})); 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,event){if("cm"==dropdata.type){var _this$section2;const mutation=event.altKey?"cmDuplicate":"cmMove";this.reactive.dispatch(mutation,[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 //# sourceMappingURL=dndsectionitem.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

View file

@ -33,6 +33,7 @@ import DispatchActions from 'core_courseformat/local/content/actions';
import * as CourseEvents from 'core_course/events'; import * as CourseEvents from 'core_course/events';
// The jQuery module is only used for interacting with Boostrap 4. It can we removed when MDL-71979 is integrated. // The jQuery module is only used for interacting with Boostrap 4. It can we removed when MDL-71979 is integrated.
import jQuery from 'jquery'; import jQuery from 'jquery';
import Pending from 'core/pending';
export default class Component extends BaseComponent { export default class Component extends BaseComponent {
@ -502,11 +503,13 @@ export default class Component extends BaseComponent {
* @param {object} param0.element the state object * @param {object} param0.element the state object
*/ */
_reloadCm({element}) { _reloadCm({element}) {
const pendingReload = new Pending(`courseformat/content:reloadCm_${element.id}`);
const cmitem = this.getElement(this.selectors.CM, element.id); const cmitem = this.getElement(this.selectors.CM, element.id);
if (cmitem) { if (cmitem) {
const promise = courseActions.refreshModule(cmitem, element.id); const promise = courseActions.refreshModule(cmitem, element.id);
promise.then(() => { promise.then(() => {
this._indexContents(); this._indexContents();
pendingReload.resolve();
return; return;
}).catch(); }).catch();
} }
@ -522,11 +525,13 @@ export default class Component extends BaseComponent {
* @param {object} param0.element the state object * @param {object} param0.element the state object
*/ */
_reloadSection({element}) { _reloadSection({element}) {
const pendingReload = new Pending(`courseformat/content:reloadSection_${element.id}`);
const sectionitem = this.getElement(this.selectors.SECTION, element.id); const sectionitem = this.getElement(this.selectors.SECTION, element.id);
if (sectionitem) { if (sectionitem) {
const promise = courseActions.refreshSection(sectionitem, element.id); const promise = courseActions.refreshSection(sectionitem, element.id);
promise.then(() => { promise.then(() => {
this._indexContents(); this._indexContents();
pendingReload.resolve();
return; return;
}).catch(); }).catch();
} }

View file

@ -408,6 +408,22 @@ export default class extends BaseComponent {
this.reactive.dispatch(mutationName, [target.dataset.id]); this.reactive.dispatch(mutationName, [target.dataset.id]);
} }
/**
* Handle a course module duplicate request.
*
* @param {Element} target the dispatch action element
* @param {Event} event the triggered event
*/
async _requestCmDuplicate(target, event) {
const cmId = target.dataset.id;
if (!cmId) {
return;
}
const sectionId = target.dataset.sectionid ?? null;
event.preventDefault();
this.reactive.dispatch('cmDuplicate', [cmId], sectionId);
}
/** /**
* Handle a delete cm request. * Handle a delete cm request.
* *

View file

@ -120,11 +120,13 @@ export default class extends BaseComponent {
* Drop event handler. * Drop event handler.
* *
* @param {Object} dropdata the accepted drop data * @param {Object} dropdata the accepted drop data
* @param {Event} event the drop event
*/ */
drop(dropdata) { drop(dropdata, event) {
// Call the move mutation if necessary. // Call the move mutation if necessary.
if (dropdata.id != this.id && dropdata.nextcmid != this.id) { if (dropdata.id != this.id && dropdata.nextcmid != this.id) {
this.reactive.dispatch('cmMove', [dropdata.id], null, this.id); const mutation = (event.altKey) ? 'cmDuplicate' : 'cmMove';
this.reactive.dispatch(mutation, [dropdata.id], null, this.id);
} }
} }

View file

@ -151,11 +151,13 @@ export default class extends BaseComponent {
* Drop event handler. * Drop event handler.
* *
* @param {Object} dropdata the accepted drop data * @param {Object} dropdata the accepted drop data
* @param {Event} event the drop event
*/ */
drop(dropdata) { drop(dropdata, event) {
// Call the move mutation. // Call the move mutation.
if (dropdata.type == 'cm') { if (dropdata.type == 'cm') {
this.reactive.dispatch('cmMove', [dropdata.id], this.id); const mutation = (event.altKey) ? 'cmDuplicate' : 'cmMove';
this.reactive.dispatch(mutation, [dropdata.id], this.id);
} }
if (dropdata.type == 'section') { if (dropdata.type == 'section') {
this.reactive.dispatch('sectionMove', [dropdata.id], this.id); this.reactive.dispatch('sectionMove', [dropdata.id], this.id);

View file

@ -135,11 +135,13 @@ export default class extends BaseComponent {
* Drop event handler. * Drop event handler.
* *
* @param {Object} dropdata the accepted drop data * @param {Object} dropdata the accepted drop data
* @param {Event} event the drop event
*/ */
drop(dropdata) { drop(dropdata, event) {
// Call the move mutation. // Call the move mutation.
if (dropdata.type == 'cm') { if (dropdata.type == 'cm') {
this.reactive.dispatch('cmMove', [dropdata.id], this.id, this.section?.cmlist[0]); const mutation = (event.altKey) ? 'cmDuplicate' : 'cmMove';
this.reactive.dispatch(mutation, [dropdata.id], this.id, this.section?.cmlist[0]);
} }
} }
} }

View file

@ -175,6 +175,33 @@ export default class {
await this._cmBasicAction(stateManager, 'cm_stealth', cmIds); await this._cmBasicAction(stateManager, 'cm_stealth', cmIds);
} }
/**
* Duplicate course modules
* @param {StateManager} stateManager the current state manager
* @param {array} cmIds the list of course modules ids
* @param {number|undefined} targetSectionId the optional target sectionId
* @param {number|undefined} targetCmId the target course module id
*/
async cmDuplicate(stateManager, cmIds, targetSectionId, targetCmId) {
const course = stateManager.get('course');
// Lock all target sections.
const sectionIds = new Set();
if (targetSectionId) {
sectionIds.add(targetSectionId);
} else {
cmIds.forEach((cmId) => {
const cm = stateManager.get('cm', cmId);
sectionIds.add(cm.sectionid);
});
}
this.sectionLock(stateManager, Array.from(sectionIds), true);
const updates = await this._callEditWebservice('cm_duplicate', course.id, cmIds, targetSectionId, targetCmId);
stateManager.processUpdates(updates);
this.sectionLock(stateManager, Array.from(sectionIds), false);
}
/** /**
* Move course modules to specific course location. * Move course modules to specific course location.
* *

View file

@ -422,6 +422,71 @@ class stateactions {
} }
} }
/**
* Duplicate a course modules instances into the same course.
*
* @param stateupdates $updates the affected course elements track
* @param stdClass $course the course object
* @param int[] $ids course modules ids to duplicate
* @param int|null $targetsectionid optional target section id destination
* @param int|null $targetcmid optional target before cm id destination
*/
public function cm_duplicate(
stateupdates $updates,
stdClass $course,
array $ids = [],
?int $targetsectionid = null,
?int $targetcmid = null
): void {
$this->validate_cms($course, $ids, __FUNCTION__);
$modinfo = get_fast_modinfo($course);
$cms = $this->get_cm_info($modinfo, $ids);
// Check capabilities on every activity context.
foreach ($cms as $cmid => $cm) {
$modcontext = context_module::instance($cmid);
require_all_capabilities(
['moodle/course:manageactivities', 'moodle/backup:backuptargetimport', 'moodle/restore:restoretargetimport'],
$modcontext
);
if (!course_allowed_module($course, $cm->modname)) {
throw new moodle_exception('No permission to create that activity');
}
}
$targetsection = null;
if (!empty($targetsectionid)) {
$this->validate_sections($course, [$targetsectionid], __FUNCTION__);
$targetsection = $modinfo->get_section_info_by_id($targetsectionid, MUST_EXIST);
}
$beforecm = null;
if (!empty($targetcmid)) {
$this->validate_cms($course, [$targetcmid], __FUNCTION__);
$beforecm = $modinfo->get_cm($targetcmid);
$targetsection = $modinfo->get_section_info_by_id($beforecm->section, MUST_EXIST);
}
// Duplicate course modules.
$affectedcmids = [];
foreach ($cms as $cm) {
if ($newcm = duplicate_module($course, $cm)) {
if ($targetsection) {
moveto_module($newcm, $targetsection, $beforecm);
} else {
$affectedcmids[] = $newcm->id;
}
}
}
if ($targetsection) {
$this->section_state($updates, $course, [$targetsection->id]);
} else {
$this->cm_state($updates, $course, $affectedcmids);
}
}
/** /**
* Delete course cms. * Delete course cms.
* *

View file

@ -16,6 +16,7 @@
namespace core_courseformat; namespace core_courseformat;
use course_modinfo;
use moodle_exception; use moodle_exception;
use stdClass; use stdClass;
@ -35,6 +36,7 @@ class stateactions_test extends \advanced_testcase {
*/ */
public static function setupBeforeClass(): void { public static function setupBeforeClass(): void {
global $CFG; global $CFG;
// State data uses external_format_string.
require_once($CFG->dirroot . '/lib/externallib.php'); require_once($CFG->dirroot . '/lib/externallib.php');
} }
@ -174,6 +176,24 @@ class stateactions_test extends \advanced_testcase {
return $result; return $result;
} }
/**
* Enrol, set and create the test user depending on the role name.
*
* @param stdClass $course the course data
* @param string $rolename the testing role name
*/
private function set_test_user_by_role(stdClass $course, string $rolename) {
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);
}
}
/** /**
* Test the behaviour course_state. * Test the behaviour course_state.
* *
@ -207,15 +227,7 @@ class stateactions_test extends \advanced_testcase {
$references = $this->course_references($course); $references = $this->course_references($course);
// Create and enrol user using given role. // Create and enrol user using given role.
if ($role == 'admin') { $this->set_test_user_by_role($course, $role);
$this->setAdminUser();
} else {
$user = $this->getDataGenerator()->create_user();
if ($role != 'unenroled') {
$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. // 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["cm0"] = $this->create_activity($course->id, 'assign', 1, true);
@ -874,6 +886,155 @@ class stateactions_test extends \advanced_testcase {
]; ];
} }
/**
* Duplicate course module method.
*
* @covers ::cm_duplicate
* @dataProvider cm_duplicate_provider
* @param string $targetsection the target section (empty for none)
* @param bool $validcms if uses valid cms
* @param string $role the current user role name
* @param bool $expectedexception if the test will raise an exception
*/
public function test_cm_duplicate(
string $targetsection = '',
bool $validcms = true,
string $role = 'admin',
bool $expectedexception = false
) {
$this->resetAfterTest();
// Create a course with 3 sections.
$course = $this->create_course('topics', 3, []);
$references = $this->course_references($course);
// Create and enrol user using given role.
$this->set_test_user_by_role($course, $role);
// 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, 'page', 2, false);
if ($expectedexception) {
$this->expectException(moodle_exception::class);
}
// Initialise stateupdates.
$courseformat = course_get_format($course->id);
$updates = new stateupdates($courseformat);
// Execute method.
$targetsectionid = (!empty($targetsection)) ? $references[$targetsection] : null;
$cmrefs = ($validcms) ? ['cm0', 'cm1'] : ['invalidcm'];
$actions = new stateactions();
$actions->cm_duplicate(
$updates,
$course,
$this->translate_references($references, $cmrefs),
$targetsectionid,
);
// Check the new elements in the course structure.
$originalsections = [
'assign' => $references['section1'],
'page' => $references['section2'],
];
$modinfo = course_modinfo::instance($course);
$cms = $modinfo->get_cms();
$i = 0;
foreach ($cms as $cmid => $cminfo) {
if ($cmid == $references['cm0'] || $cmid == $references['cm1']) {
continue;
}
$references["newcm$i"] = $cmid;
if ($targetsectionid) {
$this->assertEquals($targetsectionid, $cminfo->section);
} else {
$this->assertEquals($originalsections[$cminfo->modname], $cminfo->section);
}
$i++;
}
// Check the resulting updates.
$results = $this->summarize_updates($updates);
if ($targetsectionid) {
$this->assertArrayHasKey($references[$targetsection], $results['put']['section']);
} else {
$this->assertArrayHasKey($references['section1'], $results['put']['section']);
$this->assertArrayHasKey($references['section2'], $results['put']['section']);
}
$countcms = ($targetsection == 'section3' || $targetsection === '') ? 2 : 3;
$this->assertCount($countcms, $results['put']['cm']);
$this->assertArrayHasKey($references['newcm0'], $results['put']['cm']);
$this->assertArrayHasKey($references['newcm1'], $results['put']['cm']);
}
/**
* Duplicate course module data provider.
*
* @return array the testing scenarios
*/
public function cm_duplicate_provider(): array {
return [
'valid cms without target section' => [
'targetsection' => '',
'validcms' => true,
'role' => 'admin',
'expectedexception' => false,
],
'valid cms targeting an empty section' => [
'targetsection' => 'section3',
'validcms' => true,
'role' => 'admin',
'expectedexception' => false,
],
'valid cms targeting a section with activities' => [
'targetsection' => 'section2',
'validcms' => true,
'role' => 'admin',
'expectedexception' => false,
],
'invalid cms without target section' => [
'targetsection' => '',
'validcms' => false,
'role' => 'admin',
'expectedexception' => true,
],
'invalid cms with target section' => [
'targetsection' => 'section3',
'validcms' => false,
'role' => 'admin',
'expectedexception' => true,
],
'student role with target section' => [
'targetsection' => 'section3',
'validcms' => true,
'role' => 'student',
'expectedexception' => true,
],
'student role without target section' => [
'targetsection' => '',
'validcms' => true,
'role' => 'student',
'expectedexception' => true,
],
'unrenolled user with target section' => [
'targetsection' => 'section3',
'validcms' => true,
'role' => 'unenroled',
'expectedexception' => true,
],
'unrenolled user without target section' => [
'targetsection' => '',
'validcms' => true,
'role' => 'unenroled',
'expectedexception' => true,
],
];
}
/** /**
* Test for cm_delete * Test for cm_delete
* *

View file

@ -1816,10 +1816,15 @@ function course_get_cm_edit_actions(cm_info $mod, $indent = -1, $sr = null) {
plugin_supports('mod', $mod->modname, FEATURE_BACKUP_MOODLE2) && plugin_supports('mod', $mod->modname, FEATURE_BACKUP_MOODLE2) &&
course_allowed_module($mod->get_course(), $mod->modname)) { course_allowed_module($mod->get_course(), $mod->modname)) {
$actions['duplicate'] = new action_menu_link_secondary( $actions['duplicate'] = new action_menu_link_secondary(
new moodle_url($baseurl, array('duplicate' => $mod->id)), new moodle_url($baseurl, ['duplicate' => $mod->id]),
new pix_icon('t/copy', '', 'moodle', array('class' => 'iconsmall')), new pix_icon('t/copy', '', 'moodle', array('class' => 'iconsmall')),
$str->duplicate, $str->duplicate,
array('class' => 'editing_duplicate', 'data-action' => 'duplicate', 'data-sectionreturn' => $sr) [
'class' => 'editing_duplicate',
'data-action' => ($courseformat->supports_components()) ? 'cmDuplicate' : 'duplicate',
'data-sectionreturn' => $sr,
'data-id' => $mod->id,
]
); );
} }

View file

@ -1092,8 +1092,14 @@ class behat_course extends behat_base {
"/ancestor::li[contains(concat(' ', normalize-space(@class), ' '), ' section ')]" . "/ancestor::li[contains(concat(' ', normalize-space(@class), ' '), ' section ')]" .
"/descendant::div[contains(concat(' ', @class, ' '), ' lightbox ')][contains(@style, 'display: none')]"; "/descendant::div[contains(concat(' ', @class, ' '), ' lightbox ')][contains(@style, 'display: none')]";
$this->execute("behat_general::wait_until_exists", // Component based courses do not use lightboxes anymore but js depending.
array($this->escape($hiddenlightboxxpath), "xpath_element") $sectionreadyxpath = "//*[contains(@id,'page-content')]" .
"/descendant::*[contains(concat(' ', normalize-space(@class), ' '), ' stateready ')]";
$duplicationreadyxpath = "$hiddenlightboxxpath | $sectionreadyxpath";
$this->execute(
"behat_general::wait_until_exists",
[$this->escape($duplicationreadyxpath), "xpath_element"]
); );
// Close the original activity actions menu. // Close the original activity actions menu.