diff --git a/course/editsection.php b/course/editsection.php index c379b08a775..9cae5dfad2a 100644 --- a/course/editsection.php +++ b/course/editsection.php @@ -104,7 +104,12 @@ $editoroptions = array( ); $courseformat = course_get_format($course); -$defaultsectionname = $courseformat->get_default_section_name($section); + +if ($sectioninfo->is_delegated()) { + $defaultsectionname = $sectioninfo->name; +} else { + $defaultsectionname = $courseformat->get_default_section_name($section); +} $customdata = [ 'cs' => $sectioninfo, diff --git a/course/format/classes/hook/after_cm_name_edited.php b/course/format/classes/hook/after_cm_name_edited.php new file mode 100644 index 00000000000..50330908392 --- /dev/null +++ b/course/format/classes/hook/after_cm_name_edited.php @@ -0,0 +1,77 @@ +. + +namespace core_courseformat\hook; + +use core\hook\described_hook; +use cm_info; + +/** + * Hook for course-module name edited. + * + * @package core_courseformat + * @copyright 2024 Ferran Recio + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class after_cm_name_edited implements described_hook { + /** + * Constructor. + * + * @param cm_info $cm the course module + * @param string $newname the new name + */ + public function __construct( + protected cm_info $cm, + protected string $newname, + ) { + } + + + /** + * Describes the hook purpose. + * + * @return string + */ + public static function get_hook_description(): string { + return 'This hook is triggered when a course module name is edited.'; + } + + /** + * List of tags that describe this hook. + * + * @return string[] + */ + public static function get_hook_tags(): array { + return ['cm_name_edited']; + } + + /** + * Get course module instance. + * + * @return cm_info + */ + public function get_cm(): cm_info { + return $this->cm; + } + + /** + * Get new name. + * @return string + */ + public function get_newname(): string { + return $this->newname; + } +} diff --git a/course/format/classes/local/cmactions.php b/course/format/classes/local/cmactions.php index c7f8f7636a7..347b2a31a5e 100644 --- a/course/format/classes/local/cmactions.php +++ b/course/format/classes/local/cmactions.php @@ -70,6 +70,11 @@ class cmactions extends baseactions { course_modinfo::purge_course_module_cache($cm->course, $cm->id); rebuild_course_cache($cm->course, false, true); + // Modules may add some logic to renaming. + $modinfo = get_fast_modinfo($cm->course); + $hook = new \core_courseformat\hook\after_cm_name_edited($modinfo->get_cm($cm->id), $name); + \core\hook\manager::get_instance()->dispatch($hook); + // Attempt to update the grade item if relevant. $grademodule = $DB->get_record($cm->modname, ['id' => $cm->instance]); $grademodule->cmidnumber = $cm->idnumber; diff --git a/course/format/classes/local/sectionactions.php b/course/format/classes/local/sectionactions.php index 3a73bc03b15..f0e5657a9e1 100644 --- a/course/format/classes/local/sectionactions.php +++ b/course/format/classes/local/sectionactions.php @@ -356,6 +356,9 @@ class sectionactions extends baseactions { throw new \moodle_exception('maximumchars', 'moodle', '', 255); } + // If the section is delegated to a component, it may control some section values. + $fields = $this->preprocess_delegated_section_fields($sectioninfo, $fields); + if (empty($fields)) { return false; } @@ -428,4 +431,22 @@ class sectionactions extends baseactions { \course_modinfo::purge_course_modules_cache($this->course->id, $cmids); rebuild_course_cache($this->course->id, false, true); } + + /** + * Preprocess the section fields before updating a delegated section. + * + * @param section_info $sectioninfo the section info or database record to update. + * @param array $fields the fields to update. + * @return array the updated fields + */ + protected function preprocess_delegated_section_fields(section_info $sectioninfo, array $fields): array { + $delegated = $sectioninfo->get_component_instance(); + if (!$delegated) { + return $fields; + } + if (array_key_exists('name', $fields)) { + $fields['name'] = $delegated->preprocess_section_name($sectioninfo, $fields['name']); + } + return $fields; + } } diff --git a/course/format/classes/sectiondelegate.php b/course/format/classes/sectiondelegate.php index 44faea6328d..ef7772af38e 100644 --- a/course/format/classes/sectiondelegate.php +++ b/course/format/classes/sectiondelegate.php @@ -17,6 +17,7 @@ namespace core_courseformat; use section_info; +use core_courseformat\stateupdates; /** * Section delegate base class. @@ -77,4 +78,30 @@ abstract class sectiondelegate { public static function has_delegate_class(string $pluginname): bool { return self::get_delegate_class_name($pluginname) !== null; } + + /** + * Define the section final name. + * + * This method can process the section name and return the validated new name. + * + * @param section_info $section + * @param string|null $newname the new name value to store in the database + * @return string|null the name value to store in the database + */ + public function preprocess_section_name(section_info $section, ?string $newname): ?string { + return $newname; + } + + /** + * Add extra state updates when put or create a section. + * + * This method is called every time the backend sends a delegated section + * state update to the UI. + * + * @param section_info $section the affected section. + * @param stateupdates $updates the state updates object to notify the UI. + */ + public function put_section_state_extra_updates(section_info $section, stateupdates $updates): void { + // By default, do nothing. + } } diff --git a/course/format/classes/stateupdates.php b/course/format/classes/stateupdates.php index eb2bfe2acb9..feed784ed52 100644 --- a/course/format/classes/stateupdates.php +++ b/course/format/classes/stateupdates.php @@ -123,6 +123,12 @@ class stateupdates implements JsonSerializable { $currentstate = new $sectionclass($this->format, $section); $this->add_update('section', $action, $currentstate->export_for_template($this->output)); + + // If the section is delegated to a component, give the component oportunity to add updates. + $delegated = $section->get_component_instance(); + if ($delegated) { + $delegated->put_section_state_extra_updates($section, $this); + } } /** diff --git a/course/format/tests/local/cmactions_test.php b/course/format/tests/local/cmactions_test.php index e6c2215decb..53057795240 100644 --- a/course/format/tests/local/cmactions_test.php +++ b/course/format/tests/local/cmactions_test.php @@ -16,6 +16,8 @@ namespace core_courseformat\local; +use core_courseformat\hook\after_cm_name_edited; + /** * Course module format actions class tests. * @@ -191,4 +193,32 @@ final class cmactions_test extends \advanced_testcase { $this->assertInstanceOf('\core\event\course_module_updated', $event); $this->assertEquals(\context_module::instance($activity->cmid), $event->get_context()); } + + /** + * Test renaming an activity triggers the after_cm_name_edited hook. + * @covers ::rename + */ + public function test_rename_after_cm_name_edited_hook(): void { + $this->resetAfterTest(); + + $course = $this->getDataGenerator()->create_course(); + $activity = $this->getDataGenerator()->create_module( + 'assign', + ['course' => $course->id, 'name' => 'Old name'] + ); + + $executedhook = null; + + $testcallback = function(after_cm_name_edited $hook) use (&$executedhook): void { + $executedhook = $hook; + }; + $this->redirectHook(after_cm_name_edited::class, $testcallback); + + $cmactions = new cmactions($course); + $result = $cmactions->rename($activity->cmid, 'New name'); + $this->assertTrue($result); + + $this->assertEquals($activity->cmid, $executedhook->get_cm()->id); + $this->assertEquals('New name', $executedhook->get_newname()); + } } diff --git a/course/format/tests/local/sectionactions_test.php b/course/format/tests/local/sectionactions_test.php index df61302767b..5c6cee52143 100644 --- a/course/format/tests/local/sectionactions_test.php +++ b/course/format/tests/local/sectionactions_test.php @@ -862,4 +862,43 @@ class sectionactions_test extends \advanced_testcase { $this->assertEquals(0, $cm3->visible); $this->assertEquals(1, $cm4->visible); } + + /** + * Test that the preprocess_section_name method can alter the section rename value. + * + * @covers ::update + * @covers ::preprocess_delegated_section_fields + */ + public function test_preprocess_section_name(): void { + global $DB, $CFG; + $this->resetAfterTest(); + + require_once($CFG->libdir . '/tests/fixtures/sectiondelegatetest.php'); + + $course = $this->getDataGenerator()->create_course(); + + $sectionactions = new sectionactions($course); + $section = $sectionactions->create_delegated('test_component', 1); + + $result = $sectionactions->update($section, ['name' => 'new_name']); + $this->assertTrue($result); + + $section = $DB->get_record('course_sections', ['id' => $section->id]); + $this->assertEquals('new_name_suffix', $section->name); + + $sectioninfo = get_fast_modinfo($course->id)->get_section_info_by_id($section->id); + $this->assertEquals('new_name_suffix', $sectioninfo->name); + + // Validate null name. + $section = $sectionactions->create_delegated('test_component', 1, (object)['name' => 'sample']); + + $result = $sectionactions->update($section, ['name' => null]); + $this->assertTrue($result); + + $section = $DB->get_record('course_sections', ['id' => $section->id]); + $this->assertEquals('null_name', $section->name); + + $sectioninfo = get_fast_modinfo($course->id)->get_section_info_by_id($section->id); + $this->assertEquals('null_name', $sectioninfo->name); + } } diff --git a/course/format/tests/stateupdates_test.php b/course/format/tests/stateupdates_test.php index 714d123fadb..a5206b3ff7c 100644 --- a/course/format/tests/stateupdates_test.php +++ b/course/format/tests/stateupdates_test.php @@ -383,4 +383,43 @@ class stateupdates_test extends \advanced_testcase { ], ]; } + + /** + * Test components can add data to delegated section state updates. + * @covers ::add_section_put + */ + public function test_put_section_state_extra_updates(): void { + global $DB, $CFG; + $this->resetAfterTest(); + + require_once($CFG->libdir . '/tests/fixtures/sectiondelegatetest.php'); + + $course = $this->getDataGenerator()->create_course(); + $activity = $this->getDataGenerator()->create_module( + 'assign', + ['course' => $course->id] + ); + + // The test component section delegate will add the activity cm info into the state. + $section = formatactions::section($course)->create_delegated('test_component', $activity->cmid); + + $format = course_get_format($course); + $updates = new \core_courseformat\stateupdates($format); + + $updates->add_section_put($section->id); + + $data = $updates->jsonSerialize(); + + $this->assertCount(2, $data); + + $sectiondata = $data[0]; + $this->assertEquals('section', $sectiondata->name); + $this->assertEquals('put', $sectiondata->action); + $this->assertEquals($section->id, $sectiondata->fields->id); + + $cmdata = $data[1]; + $this->assertEquals('cm', $cmdata->name); + $this->assertEquals('put', $cmdata->action); + $this->assertEquals($activity->cmid, $cmdata->fields->id); + } } diff --git a/course/modlib.php b/course/modlib.php index 5ad339cbe10..8b99058b60e 100644 --- a/course/modlib.php +++ b/course/modlib.php @@ -723,6 +723,15 @@ function update_moduleinfo($cm, $moduleinfo, $course, $mform = null) { $cminfo = cm_info::create($cm); $completion->reset_all_state($cminfo); } + + if ($cm->name != $moduleinfo->name) { + $hook = new \core_courseformat\hook\after_cm_name_edited( + get_fast_modinfo($course)->get_cm($cm->id), + $moduleinfo->name + ); + \core\hook\manager::get_instance()->dispatch($hook); + } + $cm->name = $moduleinfo->name; \core\event\course_module_updated::create_from_cm($cm, $modcontext)->trigger(); diff --git a/lib/tests/fixtures/sectiondelegatetest.php b/lib/tests/fixtures/sectiondelegatetest.php index 0e9959ddf9d..9d5f706633f 100644 --- a/lib/tests/fixtures/sectiondelegatetest.php +++ b/lib/tests/fixtures/sectiondelegatetest.php @@ -17,6 +17,8 @@ namespace test_component\courseformat; use core_courseformat\sectiondelegate as sectiondelegatebase; +use core_courseformat\stateupdates; +use section_info; /** * Test class for section delegate. @@ -26,4 +28,28 @@ use core_courseformat\sectiondelegate as sectiondelegatebase; * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class sectiondelegate extends sectiondelegatebase { + /** + * Test method to fake preprocesses the section name by appending a suffix to it. + * + * @param section_info $section The section information. + * @param string|null $newname The new name for the section. + * @return string|null The preprocessed section name with the suffix appended. + */ + public function preprocess_section_name(section_info $section, ?string $newname): ?string { + if (empty($newname)) { + return 'null_name'; + } + return $newname . '_suffix'; + } + + /** + * Test method to add state updates of a section with additional information. + * + * @param section_info $section The section to update. + * @param stateupdates $updates The state updates to apply. + * @return void + */ + public function put_section_state_extra_updates(section_info $section, stateupdates $updates): void { + $updates->add_cm_put($section->itemid); + } }