MDL-86858 core_courseformat: Implement duplicate module in cmactions
This commit is contained in:
@@ -0,0 +1,7 @@
|
||||
issueNumber: MDL-86858
|
||||
notes:
|
||||
core_courseformat:
|
||||
- message: >-
|
||||
Create a new core_courseformat\cmactions::duplicate aiming to replace
|
||||
the course/lib.php version
|
||||
type: improved
|
||||
@@ -0,0 +1,7 @@
|
||||
issueNumber: MDL-86858
|
||||
notes:
|
||||
core_course:
|
||||
- message: >-
|
||||
The duplicate_module has been deprecated in favor of
|
||||
core_courseformat\cmactions::duplicate
|
||||
type: deprecated
|
||||
@@ -396,6 +396,172 @@ class cmactions extends baseactions {
|
||||
rebuild_course_cache($cm->course, false, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Duplicate a course module.
|
||||
*
|
||||
* @param int $cmid Course module id.
|
||||
* @param int|null $targetsectionid Target section id. If null, the original section is used.
|
||||
* @param string|null $newname If provided, the duplicated module will be renamed to this name, if not we
|
||||
* use the default ' (copy)' postfix.
|
||||
* @return \core_course\cm_info |null The duplicated course module info object, or null if duplication failed.
|
||||
* @throws \core\exception\coding_exception
|
||||
* @throws moodle_exception When course module does not exist in the course
|
||||
*/
|
||||
public function duplicate(
|
||||
int $cmid,
|
||||
?int $targetsectionid = null,
|
||||
?string $newname = null
|
||||
): ?\core_course\cm_info {
|
||||
global $CFG, $DB, $USER;
|
||||
require_once($CFG->dirroot . '/backup/util/includes/backup_includes.php');
|
||||
require_once($CFG->dirroot . '/backup/util/includes/restore_includes.php');
|
||||
require_once($CFG->libdir . '/filelib.php');
|
||||
|
||||
$modinfo = get_fast_modinfo($this->course);
|
||||
$cm = $modinfo->get_cm($cmid);
|
||||
$targetsection = $modinfo->get_section_info_by_id($targetsectionid ?? $cm->get_section_info()->id, MUST_EXIST);
|
||||
// Plugins with this feature flag set to false must ALWAYS be in section 0.
|
||||
if (!course_modinfo::is_mod_type_visible_on_course($cm->modname)) {
|
||||
if ($modinfo->get_section_info(0, MUST_EXIST)->id != $targetsectionid) {
|
||||
throw new \core\exception\coding_exception(
|
||||
'Modules with FEATURE_CAN_DISPLAY set to false can not be moved from section 0'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
$a = new stdClass();
|
||||
$a->modtype = get_string('modulename', $cm->modname);
|
||||
$a->modname = format_string($cm->name);
|
||||
|
||||
if (!plugin_supports('mod', $cm->modname, FEATURE_BACKUP_MOODLE2)) {
|
||||
throw new moodle_exception('duplicatenosupport', 'error', '', $a);
|
||||
}
|
||||
|
||||
// Backup the activity.
|
||||
|
||||
$bc = new \backup_controller(
|
||||
\backup::TYPE_1ACTIVITY,
|
||||
$cm->id,
|
||||
\backup::FORMAT_MOODLE,
|
||||
\backup::INTERACTIVE_NO,
|
||||
\backup::MODE_IMPORT,
|
||||
$USER->id
|
||||
);
|
||||
|
||||
$backupid = $bc->get_backupid();
|
||||
$backupbasepath = $bc->get_plan()->get_basepath();
|
||||
|
||||
$bc->execute_plan();
|
||||
|
||||
$bc->destroy();
|
||||
|
||||
// Restore the backup immediately.
|
||||
|
||||
$rc = new \restore_controller(
|
||||
$backupid,
|
||||
$this->course->id,
|
||||
\backup::INTERACTIVE_NO,
|
||||
\backup::MODE_IMPORT,
|
||||
$USER->id,
|
||||
\backup::TARGET_CURRENT_ADDING
|
||||
);
|
||||
|
||||
// Make sure that the restore_general_groups setting is always enabled when duplicating an activity.
|
||||
$plan = $rc->get_plan();
|
||||
$groupsetting = $plan->get_setting('groups');
|
||||
if (empty($groupsetting->get_value())) {
|
||||
$groupsetting->set_value(true);
|
||||
}
|
||||
|
||||
$cmcontext = \context_module::instance($cm->id);
|
||||
if (!$rc->execute_precheck()) {
|
||||
$precheckresults = $rc->get_precheck_results();
|
||||
if (is_array($precheckresults) && !empty($precheckresults['errors'])) {
|
||||
if (empty($CFG->keeptempdirectoriesonbackup)) {
|
||||
fulldelete($backupbasepath);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$rc->execute_plan();
|
||||
|
||||
// Now a bit hacky part follows - we try to get the cmid of the newly
|
||||
// restored copy of the module.
|
||||
$newcmid = null;
|
||||
$tasks = $rc->get_plan()->get_tasks();
|
||||
foreach ($tasks as $task) {
|
||||
if (is_subclass_of($task, 'restore_activity_task')) {
|
||||
if ($task->get_old_contextid() == $cmcontext->id) {
|
||||
$newcmid = $task->get_moduleid();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$rc->destroy();
|
||||
|
||||
if (empty($CFG->keeptempdirectoriesonbackup)) {
|
||||
fulldelete($backupbasepath);
|
||||
}
|
||||
|
||||
// If we know the cmid of the new course module, let us move it
|
||||
// right below the original one. otherwise it will stay at the
|
||||
// end of the section.
|
||||
if ($newcmid) {
|
||||
// Proceed with activity renaming before everything else. We don't use APIs here to avoid
|
||||
// triggering a lot of create/update duplicated events.
|
||||
if (!$newname) {
|
||||
// Add ' (copy)' language string postfix to duplicated module.
|
||||
$newname = get_string('duplicatedmodule', 'moodle', $cm->name);
|
||||
}
|
||||
if ($newname !== $cm->name) {
|
||||
$this->rename($newcmid, $newname);
|
||||
}
|
||||
// Move the new module to the target section.
|
||||
if (isset($targetsectionid) && $targetsectionid != $cm->section) {
|
||||
$this->move_end_section($newcmid, $targetsection->id);
|
||||
} else {
|
||||
// Move the new module right after the original one, so it means before the next one.
|
||||
$sectioninfo = $cm->get_section_info();
|
||||
$cmsequence = $sectioninfo->get_sequence_cm_infos();
|
||||
$cmidsequence = array_map(fn($cm) => $cm->id, $cmsequence); // We get the cmid => sequence key map.
|
||||
$aftercmposition = array_search($cm->id, $cmidsequence, true);
|
||||
$nextcm = null;
|
||||
if (array_key_exists($aftercmposition + 1, $cmsequence)) {
|
||||
$nextcm = $cmsequence[$aftercmposition + 1];
|
||||
}
|
||||
if ($nextcm) {
|
||||
$this->move_before($newcmid, $nextcm->id);
|
||||
} else {
|
||||
$this->move_end_section($newcmid, $sectioninfo->id);
|
||||
}
|
||||
}
|
||||
// Copy permission overrides to new course module.
|
||||
$newcmcontext = \context_module::instance($newcmid);
|
||||
$overrides = $DB->get_records('role_capabilities', ['contextid' => $cmcontext->id]);
|
||||
foreach ($overrides as $override) {
|
||||
$override->contextid = $newcmcontext->id;
|
||||
unset($override->id);
|
||||
$DB->insert_record('role_capabilities', $override);
|
||||
}
|
||||
|
||||
// Copy locally assigned roles to new course module.
|
||||
$overrides = $DB->get_records('role_assignments', ['contextid' => $cmcontext->id]);
|
||||
foreach ($overrides as $override) {
|
||||
$override->contextid = $newcmcontext->id;
|
||||
unset($override->id);
|
||||
$DB->insert_record('role_assignments', $override);
|
||||
}
|
||||
|
||||
// Trigger course module created event. We can trigger the event only if we know the newcmid.
|
||||
$newcm = get_fast_modinfo($cm->course)->get_cm($newcmid);
|
||||
$event = \core\event\course_module_created::create_from_cm($newcm);
|
||||
$event->trigger();
|
||||
}
|
||||
|
||||
return $newcm ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Schedule a course module for deletion in the background using an adhoc task.
|
||||
*
|
||||
|
||||
@@ -207,7 +207,7 @@ final class cmactions_test extends \advanced_testcase {
|
||||
|
||||
$executedhook = null;
|
||||
|
||||
$testcallback = function(after_cm_name_edited $hook) use (&$executedhook): void {
|
||||
$testcallback = function (after_cm_name_edited $hook) use (&$executedhook): void {
|
||||
$executedhook = $hook;
|
||||
};
|
||||
$this->redirectHook(after_cm_name_edited::class, $testcallback);
|
||||
@@ -738,4 +738,408 @@ final class cmactions_test extends \advanced_testcase {
|
||||
$cmactions = new cmactions($course);
|
||||
$cmactions->set_groupmode(10000, VISIBLEGROUPS);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test duplicating a course module.
|
||||
*
|
||||
* @param array $coursedata Array defining the course structure. Keys are section names, values are arrays of cm names.
|
||||
* @param string $cmname Name of the course module to duplicate.
|
||||
* @param string|null $sectionname Name of the section to duplicate into, or null to duplicate into the same section.
|
||||
* @param string|null $newname New name for the duplicated course module, or null to use default naming
|
||||
* (original name + ' (copy)').
|
||||
* @param array $expected Expected result array with keys: 'section' (int), 'position' (int), 'name' (string).
|
||||
* @return void
|
||||
*
|
||||
*/
|
||||
#[\PHPUnit\Framework\Attributes\DataProvider('duplicate_provider')]
|
||||
public function test_duplicate(
|
||||
array $coursedata,
|
||||
string $cmname,
|
||||
?string $sectionname,
|
||||
?string $newname,
|
||||
array $expected
|
||||
): void {
|
||||
$this->resetAfterTest();
|
||||
|
||||
$course = $this->create_course_from_data($coursedata);
|
||||
// Lookup cmid and sectionid based on names.
|
||||
$cmactions = new cmactions($course);
|
||||
$modinfo = get_fast_modinfo($course);
|
||||
$targetsectionid = null;
|
||||
$allcms = $modinfo->get_cms();
|
||||
$allcmsbyname = array_combine(
|
||||
array_map(fn($cminfo) => $cminfo->get_name(), $allcms),
|
||||
$allcms
|
||||
);
|
||||
$cmid = $allcmsbyname[$cmname]->id;
|
||||
|
||||
$allsectionsbyname = $this->get_sections_by_name($course);
|
||||
if ($sectionname !== null) {
|
||||
$targetsectionid = $allsectionsbyname[$sectionname]->id;
|
||||
}
|
||||
// For backup/restore operations, we need to be logged in.
|
||||
$this->setAdminUser();
|
||||
$newcm = $cmactions->duplicate(
|
||||
cmid: $cmid,
|
||||
targetsectionid: $targetsectionid,
|
||||
newname: $newname,
|
||||
);
|
||||
// Verify expected result.
|
||||
$mappedcourse = [];
|
||||
$modinfo = get_fast_modinfo($course); // Refresh modinfo.
|
||||
foreach ($modinfo->get_section_info_all() as $sectioninfo) {
|
||||
if (empty($sectioninfo->name)) {
|
||||
continue; // Ignore sections without a name.
|
||||
}
|
||||
$mappedcourse[$sectioninfo->name] = [];
|
||||
foreach ($sectioninfo->get_sequence_cm_infos() as $cminfo) {
|
||||
$mappedcourse[$sectioninfo->name][] = $cminfo->name;
|
||||
}
|
||||
}
|
||||
$this->assertEquals($expected, $mappedcourse);
|
||||
|
||||
// We ignore obvious differences and also sections information as it is already tested above (and
|
||||
// can differ due to section movements).
|
||||
$ignoredproperties = ['id', 'url', 'instance', 'added', 'context', 'section', 'sectionid', 'sectionnum'];
|
||||
// Make sure they are the same, except obvious id changes.
|
||||
foreach ($modinfo->get_cm($cmid) as $prop => $value) {
|
||||
if (in_array($prop, $ignoredproperties, true)) {
|
||||
// Ignore obviously different properties.
|
||||
continue;
|
||||
}
|
||||
if ($prop == 'name') {
|
||||
if (empty($newname)) {
|
||||
$value = get_string('duplicatedmodule', 'moodle', $value);
|
||||
} else {
|
||||
$value = $newname;
|
||||
}
|
||||
}
|
||||
$this->assertEquals($value, $newcm->$prop);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Data provider for test_duplicate.
|
||||
*
|
||||
* @return \Generator
|
||||
*/
|
||||
public static function duplicate_provider(): \Generator {
|
||||
yield 'duplicate after current module, no name provided' => [
|
||||
'coursedata' => [
|
||||
'Section 1' => [
|
||||
'cm1',
|
||||
'cm2',
|
||||
'cm3',
|
||||
],
|
||||
],
|
||||
'cmname' => 'cm1',
|
||||
'sectionname' => null,
|
||||
'newname' => null,
|
||||
'expected' => [
|
||||
'Section 1' => [
|
||||
'cm1',
|
||||
'cm1 (copy)',
|
||||
'cm2',
|
||||
'cm3',
|
||||
],
|
||||
],
|
||||
];
|
||||
|
||||
yield 'duplicate after current module, name provided' => [
|
||||
'coursedata' => [
|
||||
'Section 1' => [
|
||||
'cm1',
|
||||
'cm2',
|
||||
'cm3',
|
||||
],
|
||||
],
|
||||
'cmname' => 'cm1',
|
||||
'sectionname' => null,
|
||||
'newname' => 'New name',
|
||||
'expected' => [
|
||||
'Section 1' => [
|
||||
'cm1',
|
||||
'New name',
|
||||
'cm2',
|
||||
'cm3',
|
||||
],
|
||||
],
|
||||
];
|
||||
yield 'duplicate at the end of a section, name not provided' => [
|
||||
'coursedata' => [
|
||||
'Section 1' => [
|
||||
'cm1',
|
||||
],
|
||||
'Section 2' => [
|
||||
'cm2',
|
||||
],
|
||||
],
|
||||
'cmname' => 'cm1',
|
||||
'sectionname' => 'Section 2',
|
||||
'newname' => null,
|
||||
'expected' => [
|
||||
'Section 1' => [
|
||||
'cm1',
|
||||
],
|
||||
'Section 2' => [
|
||||
'cm2',
|
||||
'cm1 (copy)',
|
||||
],
|
||||
],
|
||||
];
|
||||
yield 'duplicate at the end of a section, name provided' => [
|
||||
'coursedata' => [
|
||||
'Section 1' => [
|
||||
'cm1',
|
||||
],
|
||||
'Section 2' => [
|
||||
'cm2',
|
||||
],
|
||||
],
|
||||
'cmname' => 'cm1',
|
||||
'sectionname' => 'Section 2',
|
||||
'newname' => 'New name',
|
||||
'expected' => [
|
||||
'Section 1' => [
|
||||
'cm1',
|
||||
],
|
||||
'Section 2' => [
|
||||
'cm2',
|
||||
'New name',
|
||||
],
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Test duplicating a course with wrong cmid (from another course).
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function test_duplicate_wrong_cm(): void {
|
||||
$this->resetAfterTest();
|
||||
$generator = $this->getDataGenerator();
|
||||
|
||||
$course1 = $generator->create_course();
|
||||
$course2 = $generator->create_course();
|
||||
$cm5 = $generator->create_module(
|
||||
'assign',
|
||||
['course' => $course2->id, 'name' => 'cm5', 'section' => 1],
|
||||
);
|
||||
|
||||
// Lookup cmid and sectionid based on names.
|
||||
$cmactions = new cmactions($course1);
|
||||
$this->expectException(\moodle_exception::class);
|
||||
$this->expectExceptionMessage('Invalid course module ID: ' . $cm5->cmid);
|
||||
// For backup/restore operations, we need to be logged in.
|
||||
$this->setAdminUser();
|
||||
$this->assertFalse(
|
||||
$cmactions->duplicate(
|
||||
cmid: $cm5->cmid,
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test duplicating a course with wrong targetsectionid.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function test_duplicate_wrong_targetsectionid(): void {
|
||||
$this->resetAfterTest();
|
||||
$course = $this->getDataGenerator()->create_course(['numsections' => 2]);
|
||||
$cm = $this->getDataGenerator()->create_module(
|
||||
'assign',
|
||||
['course' => $course->id, 'name' => 'cm1', 'section' => 1],
|
||||
);
|
||||
$cmactions = new cmactions($course);
|
||||
$this->expectException(\moodle_exception::class);
|
||||
$this->expectExceptionMessage('This section does not exist');
|
||||
// For backup/restore operations, we need to be logged in.
|
||||
$this->setAdminUser();
|
||||
$this->assertFalse(
|
||||
$cmactions->duplicate(
|
||||
cmid: $cm->cmid,
|
||||
targetsectionid: 99999,
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test that duplicating a module triggers the expected event.
|
||||
*/
|
||||
public function test_duplicate_module_created_event(): void {
|
||||
global $USER;
|
||||
$this->resetAfterTest();
|
||||
$this->setAdminUser();
|
||||
|
||||
// Create an assign module.
|
||||
$sink = $this->redirectEvents();
|
||||
$course = $this->getDataGenerator()->create_course();
|
||||
$module = $this->getDataGenerator()->create_module('assign', ['course' => $course]);
|
||||
$sink->clear(); // Make sure we only capture events from duplication.
|
||||
// Lookup cmid and sectionid based on names.
|
||||
$cmactions = new cmactions($course);
|
||||
$newcm = $cmactions->duplicate($module->cmid);
|
||||
$events = $sink->get_events();
|
||||
$eventscount = 0;
|
||||
$sink->close();
|
||||
|
||||
foreach ($events as $event) {
|
||||
if ($event instanceof \core\event\course_module_created) {
|
||||
$eventscount++;
|
||||
// Validate event data.
|
||||
$this->assertInstanceOf('\core\event\course_module_created', $event);
|
||||
$this->assertEquals($newcm->id, $event->objectid);
|
||||
$this->assertEquals($USER->id, $event->userid);
|
||||
$this->assertEquals($course->id, $event->courseid);
|
||||
$url = new \core\url('/mod/assign/view.php', ['id' => $newcm->id]);
|
||||
$this->assertEquals($url, $event->get_url());
|
||||
}
|
||||
}
|
||||
// Only one \core\event\course_module_created event should be triggered.
|
||||
$this->assertEquals(1, $eventscount);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test that permissions are correctly duplicated when duplicating a module.
|
||||
*/
|
||||
public function test_duplicate_module_permissions(): void {
|
||||
global $DB;
|
||||
$this->resetAfterTest();
|
||||
$this->setAdminUser();
|
||||
|
||||
// Create course and course module.
|
||||
$course = self::getDataGenerator()->create_course();
|
||||
$res = self::getDataGenerator()->create_module('assign', ['course' => $course]);
|
||||
$cm = get_coursemodule_from_id('assign', $res->cmid, 0, false, MUST_EXIST);
|
||||
$cmcontext = \context_module::instance($cm->id);
|
||||
|
||||
// Enrol student user.
|
||||
$user = self::getDataGenerator()->create_user();
|
||||
$roleid = $DB->get_field('role', 'id', ['shortname' => 'student'], MUST_EXIST);
|
||||
self::getDataGenerator()->enrol_user($user->id, $course->id, $roleid);
|
||||
|
||||
// Add capability to original course module.
|
||||
assign_capability('gradereport/grader:view', CAP_ALLOW, $roleid, $cmcontext->id);
|
||||
|
||||
// Duplicate module.
|
||||
$cmactions = new cmactions($course);
|
||||
$newcm = $cmactions->duplicate($res->cmid);
|
||||
$newcmcontext = \context_module::instance($newcm->id);
|
||||
|
||||
// Assert that user still has capability.
|
||||
$this->assertTrue(has_capability('gradereport/grader:view', $newcmcontext, $user));
|
||||
|
||||
// Assert that both modules contain the same count of overrides.
|
||||
$overrides = $DB->get_records('role_capabilities', ['contextid' => $cmcontext->id]);
|
||||
$newoverrides = $DB->get_records('role_capabilities', ['contextid' => $newcmcontext->id]);
|
||||
$this->assertEquals(count($overrides), count($newoverrides));
|
||||
}
|
||||
|
||||
/**
|
||||
* Test that calendar events are correctly duplicated when duplicating a module with a due date.
|
||||
*/
|
||||
public function test_duplicate_calendar_event(): void {
|
||||
global $DB;
|
||||
$this->resetAfterTest();
|
||||
$this->setAdminUser();
|
||||
|
||||
$course = self::getDataGenerator()->create_course();
|
||||
$duedate = time() + 3600;
|
||||
$module = self::getDataGenerator()->create_module('assign', ['course' => $course, 'duedate' => $duedate]);
|
||||
|
||||
$event = $DB->get_record('event', [
|
||||
'modulename' => 'assign',
|
||||
'instance' => $module->id,
|
||||
'eventtype' => 'due',
|
||||
], '*', MUST_EXIST);
|
||||
$this->assertEquals($duedate, $event->timestart);
|
||||
|
||||
$cmactions = new cmactions($course);
|
||||
$newcm = $cmactions->duplicate($module->cmid);
|
||||
|
||||
$newevent = $DB->get_record('event', [
|
||||
'modulename' => 'assign',
|
||||
'instance' => $newcm->instance,
|
||||
'eventtype' => 'due',
|
||||
], '*', MUST_EXIST);
|
||||
$this->assertEquals($duedate, $newevent->timestart);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test that local permissions are correctly duplicated when duplicating a module.
|
||||
*/
|
||||
public function test_duplicate_module_role_assignments(): void {
|
||||
global $DB;
|
||||
$this->resetAfterTest();
|
||||
$this->setAdminUser();
|
||||
|
||||
// Create course and course module.
|
||||
$course = self::getDataGenerator()->create_course();
|
||||
$res = self::getDataGenerator()->create_module('assign', ['course' => $course]);
|
||||
$cm = get_coursemodule_from_id('assign', $res->cmid, 0, false, MUST_EXIST);
|
||||
$cmcontext = \context_module::instance($cm->id);
|
||||
|
||||
// Enrol student user.
|
||||
$user = self::getDataGenerator()->create_user();
|
||||
$roleid = $DB->get_field('role', 'id', ['shortname' => 'student'], MUST_EXIST);
|
||||
self::getDataGenerator()->enrol_user($user->id, $course->id, $roleid);
|
||||
|
||||
// Assign user a new local role.
|
||||
$newroleid = $DB->get_field('role', 'id', ['shortname' => 'editingteacher'], MUST_EXIST);
|
||||
role_assign($newroleid, $user->id, $cmcontext->id);
|
||||
|
||||
// Duplicate module.
|
||||
$cmactions = new cmactions($course);
|
||||
$newcm = $cmactions->duplicate($res->cmid);
|
||||
$newcmcontext = \context_module::instance($newcm->id);
|
||||
|
||||
// Assert that user still has role assigned.
|
||||
$this->assertTrue(user_has_role_assignment($user->id, $newroleid, $newcmcontext->id));
|
||||
|
||||
// Assert that both modules contain the same count of overrides.
|
||||
$overrides = $DB->get_records('role_assignments', ['contextid' => $cmcontext->id]);
|
||||
$newoverrides = $DB->get_records('role_assignments', ['contextid' => $newcmcontext->id]);
|
||||
$this->assertEquals(count($overrides), count($newoverrides));
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function to create a course from given data.
|
||||
*
|
||||
* @param array $coursedata Array defining the course structure.
|
||||
* @return \stdClass The created course object.
|
||||
*/
|
||||
private function create_course_from_data(array $coursedata): \stdClass {
|
||||
$generator = $this->getDataGenerator();
|
||||
$course = $generator->create_course(['numsections' => count($coursedata), 'initsections' => 1]);
|
||||
$allsections = $this->get_sections_by_name($course);
|
||||
$allsectionsbyname = array_filter($allsections, fn($section) => !empty($section->name));
|
||||
// Create course modules as per $coursedata.
|
||||
foreach ($coursedata as $sectionname => $cmlist) {
|
||||
$section = $allsectionsbyname[$sectionname];
|
||||
foreach ($cmlist as $cm) {
|
||||
$generator->create_module(
|
||||
'assign',
|
||||
['course' => $course->id, 'name' => $cm, 'section' => $section->section],
|
||||
);
|
||||
}
|
||||
}
|
||||
return $course;
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function to get sections by name.
|
||||
*
|
||||
* @param \stdClass $course The course object.
|
||||
* @return array Array of sections indexed by their names.
|
||||
*/
|
||||
private function get_sections_by_name(\stdClass $course): array {
|
||||
$modinfo = get_fast_modinfo($course);
|
||||
$allsections = $modinfo->get_section_info_all();
|
||||
return array_combine(
|
||||
array_map(fn($section) => $section->name, $allsections),
|
||||
$allsections
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
+14
-129
@@ -2543,136 +2543,21 @@ function mod_duplicate_activity($course, $cm, $sr = null) {
|
||||
*
|
||||
* @return cm_info|null cminfo object if we sucessfully duplicated the mod and found the new cm.
|
||||
*/
|
||||
#[\core\attribute\deprecated(
|
||||
replacement: 'core_courseformat\local\cmactions',
|
||||
since: '5.2',
|
||||
mdl: 'MDL-86858',
|
||||
reason: 'Replaced by an cmactions::duplicate.',
|
||||
)]
|
||||
function duplicate_module($course, $cm, ?int $sectionid = null, bool $changename = true): ?cm_info {
|
||||
global $CFG, $DB, $USER;
|
||||
require_once($CFG->dirroot . '/backup/util/includes/backup_includes.php');
|
||||
require_once($CFG->dirroot . '/backup/util/includes/restore_includes.php');
|
||||
require_once($CFG->libdir . '/filelib.php');
|
||||
|
||||
// Plugins with this feature flag set to false must ALWAYS be in section 0.
|
||||
if (!course_modinfo::is_mod_type_visible_on_course($cm->modname)) {
|
||||
if (get_fast_modinfo($course)->get_section_info(0, MUST_EXIST)->id != $sectionid) {
|
||||
throw new coding_exception('Modules with FEATURE_CAN_DISPLAY set to false can not be moved from section 0');
|
||||
}
|
||||
}
|
||||
|
||||
$a = new stdClass();
|
||||
$a->modtype = get_string('modulename', $cm->modname);
|
||||
$a->modname = format_string($cm->name);
|
||||
|
||||
if (!plugin_supports('mod', $cm->modname, FEATURE_BACKUP_MOODLE2)) {
|
||||
throw new moodle_exception('duplicatenosupport', 'error', '', $a);
|
||||
}
|
||||
|
||||
// Backup the activity.
|
||||
|
||||
$bc = new backup_controller(backup::TYPE_1ACTIVITY, $cm->id, backup::FORMAT_MOODLE,
|
||||
backup::INTERACTIVE_NO, backup::MODE_IMPORT, $USER->id);
|
||||
|
||||
$backupid = $bc->get_backupid();
|
||||
$backupbasepath = $bc->get_plan()->get_basepath();
|
||||
|
||||
$bc->execute_plan();
|
||||
|
||||
$bc->destroy();
|
||||
|
||||
// Restore the backup immediately.
|
||||
|
||||
$rc = new restore_controller($backupid, $course->id,
|
||||
backup::INTERACTIVE_NO, backup::MODE_IMPORT, $USER->id, backup::TARGET_CURRENT_ADDING);
|
||||
|
||||
// Make sure that the restore_general_groups setting is always enabled when duplicating an activity.
|
||||
$plan = $rc->get_plan();
|
||||
$groupsetting = $plan->get_setting('groups');
|
||||
if (empty($groupsetting->get_value())) {
|
||||
$groupsetting->set_value(true);
|
||||
}
|
||||
|
||||
$cmcontext = context_module::instance($cm->id);
|
||||
if (!$rc->execute_precheck()) {
|
||||
$precheckresults = $rc->get_precheck_results();
|
||||
if (is_array($precheckresults) && !empty($precheckresults['errors'])) {
|
||||
if (empty($CFG->keeptempdirectoriesonbackup)) {
|
||||
fulldelete($backupbasepath);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$rc->execute_plan();
|
||||
|
||||
// Now a bit hacky part follows - we try to get the cmid of the newly
|
||||
// restored copy of the module.
|
||||
$newcmid = null;
|
||||
$tasks = $rc->get_plan()->get_tasks();
|
||||
foreach ($tasks as $task) {
|
||||
if (is_subclass_of($task, 'restore_activity_task')) {
|
||||
if ($task->get_old_contextid() == $cmcontext->id) {
|
||||
$newcmid = $task->get_moduleid();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$rc->destroy();
|
||||
|
||||
if (empty($CFG->keeptempdirectoriesonbackup)) {
|
||||
fulldelete($backupbasepath);
|
||||
}
|
||||
|
||||
// If we know the cmid of the new course module, let us move it
|
||||
// right below the original one. otherwise it will stay at the
|
||||
// end of the section.
|
||||
if ($newcmid) {
|
||||
// Proceed with activity renaming before everything else. We don't use APIs here to avoid
|
||||
// triggering a lot of create/update duplicated events.
|
||||
$newcm = get_coursemodule_from_id($cm->modname, $newcmid, $cm->course);
|
||||
if ($changename) {
|
||||
// Add ' (copy)' language string postfix to duplicated module.
|
||||
$newname = get_string('duplicatedmodule', 'moodle', $newcm->name);
|
||||
set_coursemodule_name($newcm->id, $newname);
|
||||
}
|
||||
|
||||
$section = get_fast_modinfo($course)->get_section_info_by_id($sectionid ?? $cm->section);
|
||||
$action = formatactions::cm($course);
|
||||
if (isset($sectionid)) {
|
||||
$action->move_end_section($newcm->id, $section->id);
|
||||
} else {
|
||||
$modarray = explode(",", trim($section->sequence));
|
||||
$cmindex = array_search($cm->id, $modarray);
|
||||
if ($cmindex !== false && $cmindex < count($modarray) - 1) {
|
||||
$beforecmid = $modarray[$cmindex + 1];
|
||||
$action->move_before($newcm->id, $beforecmid);
|
||||
}
|
||||
}
|
||||
|
||||
// Update calendar events with the duplicated module.
|
||||
// The following line is to be removed in MDL-58906.
|
||||
course_module_update_calendar_events($newcm->modname, null, $newcm);
|
||||
|
||||
// Copy permission overrides to new course module.
|
||||
$newcmcontext = context_module::instance($newcm->id);
|
||||
$overrides = $DB->get_records('role_capabilities', ['contextid' => $cmcontext->id]);
|
||||
foreach ($overrides as $override) {
|
||||
$override->contextid = $newcmcontext->id;
|
||||
unset($override->id);
|
||||
$DB->insert_record('role_capabilities', $override);
|
||||
}
|
||||
|
||||
// Copy locally assigned roles to new course module.
|
||||
$overrides = $DB->get_records('role_assignments', ['contextid' => $cmcontext->id]);
|
||||
foreach ($overrides as $override) {
|
||||
$override->contextid = $newcmcontext->id;
|
||||
unset($override->id);
|
||||
$DB->insert_record('role_assignments', $override);
|
||||
}
|
||||
|
||||
// Trigger course module created event. We can trigger the event only if we know the newcmid.
|
||||
$newcm = get_fast_modinfo($cm->course)->get_cm($newcmid);
|
||||
$event = \core\event\course_module_created::create_from_cm($newcm);
|
||||
$event->trigger();
|
||||
}
|
||||
|
||||
return isset($newcm) ? $newcm : null;
|
||||
\core\deprecation::emit_deprecation(__FUNCTION__);
|
||||
$modinfo = get_fast_modinfo($course);
|
||||
$cm = $modinfo->get_cm($cm->id);
|
||||
return formatactions::cm($course->id)->duplicate(
|
||||
cmid: $cm->id,
|
||||
targetsectionid: $sectionid,
|
||||
newname: $changename ? null : $cm->name, // This is the opposite of changename, if we provide null the name is changed.
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user