diff --git a/course/format/classes/stateactions.php b/course/format/classes/stateactions.php index 28da968c87f..d8dc11a97d8 100644 --- a/course/format/classes/stateactions.php +++ b/course/format/classes/stateactions.php @@ -48,6 +48,7 @@ class stateactions { * @param int[] $ids the list of affected course module ids * @param int $targetsectionid optional target section id * @param int $targetcmid optional target cm id + * @throws moodle_exception */ public function cm_move( stateupdates $updates, @@ -85,6 +86,9 @@ class stateactions { $cm = $modinfo->get_cm($cmid); $currentsectionid = $cm->section; $targetsection = $modinfo->get_section_info_by_id($targetsectionid, MUST_EXIST); + if ($targetsection->is_delegated() && $cm->get_delegated_section_info()) { + throw new moodle_exception('subsectionmoveerror', 'core'); + } $beforecm = (!empty($beforecmdid)) ? $modinfo->get_cm($beforecmdid) : null; if ($beforecm === null || $beforecm->id != $cmid) { moveto_module($cm, $targetsection, $beforecm); diff --git a/course/format/tests/stateactions_test.php b/course/format/tests/stateactions_test.php index 1d9bacebc2c..069ad9f8c6a 100644 --- a/course/format/tests/stateactions_test.php +++ b/course/format/tests/stateactions_test.php @@ -1389,6 +1389,170 @@ class stateactions_test extends \advanced_testcase { ]; } + /** + * Test course module move and subsection move. + * + * @covers ::cm_move + * @dataProvider cm_move_provider + * @param string[] $cmtomove the sections to move + * @param string $targetsection + * @param string[] $expectedcoursetree expected course tree + * @param string|null $expectedexception if it will expect an exception. + */ + public function test_cm_move( + array $cmtomove, + string $targetsection, + array $expectedcoursetree, + ?string $expectedexception = null + ): void { + $this->resetAfterTest(); + $course = $this->create_course('topics', 4, []); + + $manager = \core_plugin_manager::resolve_plugininfo_class('mod'); + $manager::enable_plugin('subsection', 1); + $subsection1 = $this->getDataGenerator()->create_module( + 'subsection', ['course' => $course, 'section' => 1, 'name' => 'subsection1'] + ); + $subsection2 = $this->getDataGenerator()->create_module( + 'subsection', ['course' => $course, 'section' => 1, 'name' => 'subsection2'] + ); + $modinfo = get_fast_modinfo($course); + $subsection1info = $modinfo->get_section_info_by_component('mod_subsection', $subsection1->id); + $subsection2info = $modinfo->get_section_info_by_component('mod_subsection', $subsection2->id); + + $references = $this->course_references($course); + // 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', 0); + $references["cm1"] = $this->create_activity($course->id, 'page', 2); + $references["cm2"] = $this->create_activity($course->id, 'forum', $subsection1info->sectionnum); + $references["subsection1"] = intval($subsection1->cmid); + $references["subsection2"] = intval($subsection2->cmid); + $references["subsection1sectionid"] = $subsection1info->id; + $references["subsection2sectionid"] = $subsection2info->id; + $user = $this->getDataGenerator()->create_user(); + $this->getDataGenerator()->enrol_user($user->id, $course->id, 'editingteacher'); + $this->setUser($user); + + // Initialise stateupdates. + $courseformat = course_get_format($course->id); + $updates = new stateupdates($courseformat); + + if ($expectedexception) { + $this->expectExceptionMessage($expectedexception); + } + // Execute the method. + $actions = new stateactions(); + // We do this to make sure we can reference subsection1 in the course tree (and not just section5 as subsection1 is + // both a module and a subsection). + if (str_starts_with($targetsection, 'subsection')) { + $targetsection = $targetsection . 'sectionid'; + } + $actions->cm_move( + $updates, + $course, + $this->translate_references($references, $cmtomove), + $references[$targetsection] + ); + + $coursetree = $this->get_course_tree($course, $references); + $this->assertEquals($expectedcoursetree, $coursetree); + } + + /** + * Get Course tree for later comparison. + * + * @param stdClass $course + * @param array $references + * @return array + */ + private function get_course_tree(stdClass $course, array $references): array { + $coursetree = []; + $modinfo = get_fast_modinfo($course); // Get refreshed version. + + $allsections = $modinfo->get_listed_section_info_all(); + $cmidstoref = array_flip($references); + foreach ($allsections as $sectioninfo) { + $sectionkey = 'section' . $sectioninfo->sectionnum; + $coursetree[$sectionkey] = []; + if (empty(trim($sectioninfo->sequence))) { + continue; + } + $cmids = explode(",", $sectioninfo->sequence); + foreach ($cmids as $cmid) { + $cm = $modinfo->get_cm($cmid); + $delegatedsection = $cm->get_delegated_section_info(); + + // Course modules without a delegated section are included as activities. + if (!$delegatedsection) { + $coursetree[$sectionkey][] = $cmidstoref[$cmid]; + continue; + } + + // Course modules with a delegated are included as a section, not as an activity. + $delegatedsectionkey = $delegatedsection->name; // We gave it a name, so let's use it as key. + $coursetree[$sectionkey][$delegatedsectionkey] = []; + + if (empty(trim($delegatedsection->sequence))) { + continue; + } + $delegatedcmids = explode(",", $delegatedsection->sequence); + foreach ($delegatedcmids as $dcmid) { + $coursetree[$sectionkey][$delegatedsectionkey][] = $cmidstoref[$dcmid]; + } + + } + } + return $coursetree; + } + + /** + * Provider for test_section_move. + * + * + * The original coursetree looks like this: + * 'coursetree' => [ + * 'section0' => ['cm0'], + * 'section1' => ['subsection1' => ['cm2'],'subsection2' => []], + * 'section2' => ['cm1'], + * 'section3' => [], + * 'section4' => [], + * ], + * + * @return array the testing scenarios + */ + public static function cm_move_provider(): array { + return [ + 'Move module into section2' => [ + 'cmtomove' => ['cm0'], + 'targetsection' => 'section2', + 'expectedcoursetree' => [ + 'section0' => [], + 'section1' => ['subsection1' => ['cm2'], 'subsection2' => []], + 'section2' => ['cm1', 'cm0'], + 'section3' => [], + 'section4' => [], + ], + ], + 'Move subsection into another subsection' => [ + 'cmtomove' => ['subsection1'], // When moving a subsection we actually move the delegated module. + 'targetsection' => 'subsection2', + 'expectedcoursetree' => [], + 'exception' => 'error/subsectionmoveerror', + ], + 'Move module into subsection' => [ + 'cmtomove' => ['cm1'], + 'targetsection' => 'subsection1', + 'expectedcoursetree' => [ + 'section0' => ['cm0'], + 'section1' => ['subsection1' => ['cm2', 'cm1'], 'subsection2' => []], + 'section2' => [], + 'section3' => [], + 'section4' => [], + ], + ], + ]; + } + /** * Test for section_move_after capability checks. * diff --git a/lang/en/moodle.php b/lang/en/moodle.php index 7708e72e45f..f9eff549b5d 100644 --- a/lang/en/moodle.php +++ b/lang/en/moodle.php @@ -1960,6 +1960,7 @@ $string['secretalreadyused'] = 'Change password confirmation link was already us $string['secs'] = 'secs'; $string['section'] = 'Section'; $string['sectionactionnotsupported'] = 'Section action "{$a}" is not supported here'; +$string['subsectionmoveerror'] = 'Cannot move this subsection on another subsection'; $string['sectionname'] = 'Section name'; $string['sectionoutline'] = 'Section outline'; $string['sections'] = 'Sections';