diff --git a/.upgradenotes/MDL-83900-2025052719455356.yml b/.upgradenotes/MDL-83900-2025052719455356.yml new file mode 100644 index 00000000000..3bbe080973e --- /dev/null +++ b/.upgradenotes/MDL-83900-2025052719455356.yml @@ -0,0 +1,7 @@ +issueNumber: MDL-83900 +notes: + mod_wiki: + - message: >- + Create a manager class to regroup common functionalities and a wiki_mode + enum related to the two different modes + type: improved diff --git a/mod/wiki/classes/manager.php b/mod/wiki/classes/manager.php new file mode 100644 index 00000000000..4225deda3cc --- /dev/null +++ b/mod/wiki/classes/manager.php @@ -0,0 +1,258 @@ +. + +namespace mod_wiki; + +use cm_info; +use context_module; +use stdClass; + +/** + * Class manager for wiki activity + * + * @package mod_wiki + * @copyright 2025 Laurent David + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class manager { + /** Module name. */ + public const MODULE = 'wiki'; + + /** The plugin name. */ + public const PLUGINNAME = 'mod_wiki'; + + /** @var context_module the current context. */ + private context_module $context; + + /** @var stdClass $course record. */ + private stdClass $course; + + /** + * @var int $groupmode as defined in SEPARATEGROUPS, VISIBLEGROUPS, or NOGROUPS. + */ + private int $groupmode; + + /** + * Class constructor. + * + * @param cm_info $cm course module info object + * @param stdClass $instance activity instance object. + * @param \moodle_database $db the database instance. + */ + public function __construct( + /** @var cm_info $cm course_modules record. */ + private cm_info $cm, + /** @var stdClass $instance course_module record. */ + private stdClass $instance, + /** @var \moodle_database $db the database instance. */ + private readonly \moodle_database $db + ) { + $this->context = context_module::instance($cm->id); + $this->course = $cm->get_course(); + $this->groupmode = groups_get_activity_groupmode($cm, $this->course); + } + + /** + * Create a manager instance from an instance record. + * + * @param stdClass $instance an activity record + * @return manager + */ + public static function create_from_instance(stdClass $instance): self { + $cm = get_coursemodule_from_instance(self::MODULE, $instance->id); + // Ensure that $this->cm is a cm_info object. + $cm = cm_info::create($cm); + $db = \core\di::get(\moodle_database::class); + return new self($cm, $instance, $db); + } + + /** + * Create a manager instance from a course_modules record. + * + * @param stdClass|cm_info $cm an activity record + * @return manager + */ + public static function create_from_coursemodule(stdClass|cm_info $cm): self { + // Ensure that $this->cm is a cm_info object. + $cm = cm_info::create($cm); + $db = \core\di::get(\moodle_database::class); + $instance = $db->get_record(self::MODULE, ['id' => $cm->instance], '*', MUST_EXIST); + return new self($cm, $instance, $db); + } + + /** + * Return the current context. + * + * @return context_module + */ + public function get_context(): context_module { + return $this->context; + } + + /** + * Return the current instance. + * + * @return stdClass the instance record + */ + public function get_instance(): stdClass { + return $this->instance; + } + + /** + * Return the current cm_info. + * + * @return cm_info the course module + */ + public function get_coursemodule(): cm_info { + return $this->cm; + } + + /** + * Return the current entries count for this wiki module, that the provided user. + * + * @param int $userid the current user id (for grouping purposes) + * @return int the number of entries + */ + public function get_all_entries_count(int $userid): int { + $where = ' WHERE wsp.wikiid = :wikiid '; + $params = ['wikiid' => $this->instance->id]; + $groupmemberjoin = ''; + // Individual wikis acts like a personal notebook, so we only count the pages of the current user. + // However, for teachers, or in visible groups, the user also sees pages from other users. + if ( + $this->get_wiki_mode() == wiki_mode::INDIVIDUAL + && !has_capability('mod/wiki:managewiki', $this->context, $userid) + && $this->cm->groupmode != VISIBLEGROUPS + ) { + $where .= 'AND wp.userid = :authoruserid'; + $params['authoruserid'] = $userid; + } else { + [ + 'join' => $groupmemberjoin, + 'params' => $params, + 'where' => $where, + ] = $this->get_group_member_join($userid, $this->instance->id); + } + + return $this->db->count_records_sql( + 'SELECT COUNT(*) FROM {wiki_pages} wp + LEFT JOIN {wiki_subwikis} wsp ON wsp.id=wp.subwikiid' + . $groupmemberjoin . $where, + $params + ); + } + + /** + * Get the SQL join for group members based on the provided user's group. + * + * @param int $userid the current user id + * @param int $wikiid the wiki id + * @return array an array containing the SQL join string and parameters + */ + private function get_group_member_join(int $userid, int $wikiid): array { + $where = ' WHERE wsp.wikiid = :wikiid'; + $params = ['wikiid' => $wikiid]; + if ( + $this->groupmode == SEPARATEGROUPS + && !has_capability('moodle/site:accessallgroups', $this->context, $userid) + ) { + $groups = groups_get_all_groups($this->course->id, $userid, 0, 'g.id'); + if (empty($groups)) { + // No groups found for this user, return empty join but we show only records belonging to this user. + $where .= ' AND wp.userid = :userid'; + $params['userid'] = $userid; + return [ + 'join' => '', + 'params' => $params, + 'where' => $where, + ]; + } + // If not we will check both group from the subwiki and wiki pages user's. + $groupids = array_column($groups, 'id'); + [$groupmembersql, $groupmemberparams] = groups_get_members_ids_sql($groupids, $this->context); + $params = array_merge($params, $groupmemberparams); + $groupmemberjoin = " JOIN ({$groupmembersql}) jg ON jg.id = wp.userid"; + [$wheregroup, $paramgroup] = $this->db->get_in_or_equal($groupids, SQL_PARAMS_NAMED, 'groupid'); + $where .= ' AND wsp.groupid ' . $wheregroup; + $params = array_merge($params, $paramgroup); + } else { + $groupmemberjoin = ''; + } + return ['join' => $groupmemberjoin, 'params' => $params, 'where' => $where]; + } + + /** + * Return the number of entries for a given user. + * + * @param int $userid the user id. We will ignore subwikis and groups. + * @return int the number of entries + */ + public function get_user_entries_count(int $userid): int { + $where = ' WHERE wsp.wikiid = :wikiid AND wp.userid = :userid'; + $params = [ + 'wikiid' => $this->instance->id, + 'userid' => $userid, + ]; + return $this->db->count_records_sql( + 'SELECT COUNT(*) FROM {wiki_pages} wp + LEFT JOIN {wiki_subwikis} wsp ON wsp.id=wp.subwikiid' . $where, + $params + ); + } + + /** + * Get Wiki mode (Individual or Collaborative) + * + * @return wiki_mode the wiki current mode + */ + public function get_wiki_mode(): wiki_mode { + return wiki_mode::tryFrom($this->instance->wikimode) ?? wiki_mode::UNDEFINED; + } + + /** + * Get the main wiki page id for the current user, group and wiki. + * This follow the routine in the view.php file taking info from the course module + * id and the current group.. + * + * @return int|null the wiki page id or null if not found + */ + public function get_main_wiki_pageid(): ?int { + global $USER, $CFG; + require_once($CFG->dirroot . '/mod/wiki/locallib.php'); + + if (!$wiki = wiki_get_wiki($this->cm->instance)) { + return null; + } + $currentgroup = groups_get_activity_group($this->cm); + + if (wiki_mode::tryFrom($wiki->wikimode) === wiki_mode::INDIVIDUAL) { + $userid = $USER->id; + } else { + $userid = 0; + } + + // Getting subwiki. If it does not exists, return null. + if (!$subwiki = wiki_get_subwiki_by_group($wiki->id, $currentgroup, $userid)) { + return null; + } + + // Getting first page of the wiki. + if (!$page = wiki_get_first_page($subwiki->id)) { + return null; + } + return $page->id; + } +} diff --git a/mod/wiki/classes/wiki_mode.php b/mod/wiki/classes/wiki_mode.php new file mode 100644 index 00000000000..c60e5585dbe --- /dev/null +++ b/mod/wiki/classes/wiki_mode.php @@ -0,0 +1,43 @@ +. + +namespace mod_wiki; + +/** + * Wiki modes enum. + * + * @package mod_wiki + * @copyright 2025 Laurent David + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +enum wiki_mode: string { + case UNDEFINED = ''; + case COLLABORATIVE = 'collaborative'; + case INDIVIDUAL = 'individual'; + + /** + * Returns the user friendly string representation of the wiki mode. + * + * @return string user friendly representation. + */ + public function to_string(): string { + $stringmanager = \core\di::get(\core_string_manager::class); + if ($this === self::UNDEFINED) { + return $stringmanager->get_string('wikimodeundefined', 'mod_wiki'); + } + return $stringmanager->get_string('wikimode' . $this->value, 'mod_wiki'); + } +} diff --git a/mod/wiki/tests/manager_test.php b/mod/wiki/tests/manager_test.php new file mode 100644 index 00000000000..2f452aa6236 --- /dev/null +++ b/mod/wiki/tests/manager_test.php @@ -0,0 +1,465 @@ +. + +namespace mod_wiki; +/** + * Generator tests class. + * + * @package mod_wiki + * @copyright 2025 Laurent David + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @covers \mod_wiki\manager + */ +final class manager_test extends \advanced_testcase { + + /** + * Data provider for wiki modes. + * + * @return array + */ + public static function get_wiki_mode_provider(): array { + return [ + 'collaborative' => ['mode' => 'collaborative', 'expected' => wiki_mode::COLLABORATIVE], + 'individual' => ['mode' => 'individual', 'expected' => wiki_mode::INDIVIDUAL], + ]; + } + + /** + * Data provider for test_get_all_entries_count. + * + * @return array + */ + public static function get_all_entries_count_provider(): array { + return [ + 'teacher 1 (no group mode, collaborative)' => [ + 'username' => 't1', + 'coursegroupmode' => NOGROUPS, + 'wikimode' => wiki_mode::COLLABORATIVE, + 'expectedcount' => 2, + ], + 'teacher 1 (no group mode, undefined)' => [ + 'username' => 't1', + 'coursegroupmode' => NOGROUPS, + 'wikimode' => wiki_mode::UNDEFINED, + 'expectedcount' => 2, + ], + 'teacher 1 (separate group mode, collaborative)' => [ + 'username' => 't1', + 'coursegroupmode' => SEPARATEGROUPS, + 'wikimode' => wiki_mode::COLLABORATIVE, + 'expectedcount' => 1, + ], + // Teacher 1 belongs to group 1, so should see s1. + 'teacher 1 (visible group mode, collaborative)' => [ + 'username' => 't1', + 'coursegroupmode' => VISIBLEGROUPS, + 'wikimode' => wiki_mode::COLLABORATIVE, + 'expectedcount' => 2, + ], + // Teacher 2 does not belong to any group. + 'teacher 2 (no group mode, collaborative)' => [ + 'username' => 't2', + 'coursegroupmode' => NOGROUPS, + 'wikimode' => wiki_mode::COLLABORATIVE, + 'expectedcount' => 2, + ], + 'teacher 2 (separate group mode, collaborative)' => [ + 'username' => 't2', + 'coursegroupmode' => SEPARATEGROUPS, + 'wikimode' => wiki_mode::COLLABORATIVE, + 'expectedcount' => 0, + ], + // Teacher 2 does not belong to any group. + 'teacher 2 (visible group mode, collaborative)' => [ + 'username' => 't2', + 'coursegroupmode' => VISIBLEGROUPS, + 'wikimode' => wiki_mode::COLLABORATIVE, + 'expectedcount' => 2, + ], + // Teacher Individual mode. + 'teacher 1 (no group mode, individual)' => [ + 'username' => 't1', + 'coursegroupmode' => NOGROUPS, + 'wikimode' => wiki_mode::INDIVIDUAL, + 'expectedcount' => 2, + ], + 'teacher 1 (separate group mode, individual)' => [ + 'username' => 't1', + 'coursegroupmode' => SEPARATEGROUPS, + 'wikimode' => wiki_mode::INDIVIDUAL, + 'expectedcount' => 1, + ], + 'teacher 1 (visible group mode, individual)' => [ + 'username' => 't1', + 'coursegroupmode' => VISIBLEGROUPS, + 'wikimode' => wiki_mode::INDIVIDUAL, + 'expectedcount' => 2, + ], + // Student collaborative mode. + 'student 1 (no group mode, collaborative)' => [ + 'username' => 's1', + 'coursegroupmode' => NOGROUPS, + 'wikimode' => wiki_mode::COLLABORATIVE, + 'expectedcount' => 2, + ], + 'student 1 (separate group mode, collaborative)' => [ + 'username' => 's1', + 'coursegroupmode' => SEPARATEGROUPS, + 'wikimode' => wiki_mode::COLLABORATIVE, + 'expectedcount' => 1, + ], + 'student 1 (visible group mode, collaborative)' => [ + 'username' => 's1', + 'coursegroupmode' => VISIBLEGROUPS, + 'wikimode' => wiki_mode::COLLABORATIVE, + 'expectedcount' => 2, + ], + // Student individual mode. + 'student 1 (no group mode, individual)' => [ + 'username' => 's1', + 'coursegroupmode' => NOGROUPS, + 'wikimode' => wiki_mode::INDIVIDUAL, + 'expectedcount' => 1, + ], + 'student 1 (separate group mode, individual)' => [ + 'username' => 's1', + 'coursegroupmode' => SEPARATEGROUPS, + 'wikimode' => wiki_mode::INDIVIDUAL, + 'expectedcount' => 1, + ], + 'student 1 (visible group mode, individual)' => [ + 'username' => 's1', + 'coursegroupmode' => VISIBLEGROUPS, + 'wikimode' => wiki_mode::INDIVIDUAL, + 'expectedcount' => 2, + ], + ]; + } + + /** + * Data provider for test_get_all_entries_count. + * + * @return array + */ + public static function get_user_entries_count_provider(): array { + return [ + 'student 1' => ['s1', 1], + 'student 2' => ['s2', 1], + 'teacher 1' => ['t1', 0], + ]; + } + + /** + * Set up the test environment. + */ + public function setUp(): void { + parent::setUp(); + $this->resetAfterTest(); + } + + /** + * Test creating a manager instance from an instance record. + * + * @covers \mod_wiki\manager::create_from_instance + */ + public function test_create_manager_instance_from_instance_record(): void { + $this->resetAfterTest(); + ['instance' => $instance] = $this->setup_users_and_activity(); + $manager = manager::create_from_instance($instance); + $this->assertNotNull($manager); + } + + /** + * Setup users and activity for testing answers retrieval. + * + * @param int $groupmode the group mode to use for the course. + * @param wiki_mode $mode the wiki mode to use for the instance. + * @return array indexed array with 'users', 'course' and 'instance'. + */ + private function setup_users_and_activity(int $groupmode = NOGROUPS, wiki_mode $mode = wiki_mode::COLLABORATIVE): array { + global $CFG; + require_once($CFG->dirroot . '/mod/wiki/locallib.php'); + $users = []; + $generator = $this->getDataGenerator(); + $courseparams = []; + if ($groupmode !== NOGROUPS) { + // Set the group mode for the course. + $courseparams['groupmode'] = $groupmode; + } + $course = $generator->create_course($courseparams); + foreach (['s1' => 'student', 's2' => 'student', 't1' => 'teacher', 't2' => 'teacher'] as $username => $role) { + $users[$username] = $generator->create_and_enrol($course, $role, ['username' => $username]); + } + + $groups = []; + if ($groupmode !== NOGROUPS) { + // Create a group if the group mode is not NOGROUPS. + $groups[] = $generator->create_group(['courseid' => $course->id]); + $groups[] = $generator->create_group(['courseid' => $course->id]); + groups_add_member($groups[0], $users['s1']->id); + groups_add_member($groups[1], $users['s2']->id); + groups_add_member($groups[0], $users['t1']->id); + } + $instance = $generator->create_module( + 'wiki', + [ + 'course' => $course, + 'wikimode' => $mode->value, + 'groupmode' => $groupmode, + ], + ); + + $wikigenerator = $generator->get_plugin_generator('mod_wiki'); + + $pages = []; + foreach (['s1', 's2'] as $username) { + $user = $users[$username]; + // Create a first page for each user. + $this->setUser($user->id); + $groups = groups_get_my_groups(); + $userid = $mode === wiki_mode::INDIVIDUAL ? $user->id : 0; // Use user id for individual mode, 0 for collaborative + // This is similar to the view.php logic, where the first page is created either for group or users. + foreach ($groups as $group) { + // Ensure the user is in the group. + $pages["{$username}{$group->name}"] = $wikigenerator->create_first_page($instance, [ + 'wikiid' => $instance->id, + 'userid' => $userid, + 'group' => $group->id, + 'content' => "Wiki first page content for $username", + 'title' => $instance->firstpagetitle, // We need to use the first page title from the instance to make sure + // that this page is considered as first page. {@see wiki_get_first_page()}. + ]); + } + if (empty($groups)) { + $pages["{$username}"] = $wikigenerator->create_page($instance, [ + 'wikiid' => $instance->id, + 'userid' => $userid, + 'content' => "Wiki first page content for $username", + 'title' => "Wiki first page title for $username", + ]); + } + } + return [ + 'users' => $users, + 'course' => $course, + 'instance' => $instance, + 'pages' => $pages, + + ]; + } + + /** + * Test creating a manager instance from a course module. + * + * @covers \mod_wiki\manager::create_from_coursemodule + */ + public function test_create_manager_instance_from_coursemodule(): void { + $this->resetAfterTest(); + ['instance' => $instance, 'course' => $course] = $this->setup_users_and_activity(); + $cm = get_fast_modinfo($course)->get_cm($instance->cmid); + $manager = manager::create_from_coursemodule($cm); + $this->assertNotNull($manager); + } + + /** + * Test the wiki mode of the wiki instance. + * + * @param string $mode the mode of the wiki instance. + * @param wiki_mode $expected the expected wiki mode. + * + * @covers \mod_wiki\manager::get_wiki_mode + * @dataProvider get_wiki_mode_provider + */ + public function test_wiki_mode(string $mode, wiki_mode $expected): void { + $this->resetAfterTest(); + $course = $this->getDataGenerator()->create_course(); + $wiki = $this->getDataGenerator()->create_module('wiki', ['course' => $course, 'wikimode' => $mode]); + $manager = manager::create_from_instance($wiki); + $this->assertEquals($expected, $manager->get_wiki_mode()); + } + + /** + * Test retrieving entries count for all users. + * + * @param string $username the username of the user to retrieve entries count for. + * @param int $coursegroupmode the group mode of the course. + * @param wiki_mode $wikimode the wiki mode of the instance. + * @param int $expectedcount the expected count of answers for the user. + * + * @covers \mod_wiki\manager::get_all_entries_count + * @dataProvider get_all_entries_count_provider + */ + public function test_get_all_entries_count( + string $username, + int $coursegroupmode, + wiki_mode $wikimode, + int $expectedcount + ): void { + [ + 'users' => $users, + 'instance' => $instance + ] = $this->setup_users_and_activity($coursegroupmode, $wikimode); + + $manager = manager::create_from_instance($instance); + $count = $manager->get_all_entries_count($users[$username]->id); + $this->assertEquals($expectedcount, $count); + } + + /** + * Test retrieving entries my count for a given user. + * + * @param string $username the username of the user to retrieve entries count for. + * @param int $expectedcount the expected count of answers for the user. + * + * @covers \mod_wiki\manager::get_user_entries_count + * @dataProvider get_user_entries_count_provider + */ + public function test_get_user_entries_count(string $username, int $expectedcount): void { + ['users' => $users, 'instance' => $instance] = $this->setup_users_and_activity(); + $manager = manager::create_from_instance($instance); + $count = $manager->get_user_entries_count($users[$username]->id); + $this->assertEquals($expectedcount, $count); + } + + /** + * Test the get_wiki_pageid method. + * + * @param string $username the username of the user to retrieve the page id for. + * @param int $groupmode + * @param wiki_mode $wikimode the wiki mode of the instance. + * @param string|null $expectedpage the expected page id for the user. + * + * @covers \mod_wiki\manager::get_main_wiki_pageid + * @dataProvider get_main_wiki_pageid_data_provider + */ + public function test_get_main_wiki_pageid(string $username, int $groupmode, wiki_mode $wikimode, ?string $expectedpage): void { + $this->resetAfterTest(); + ['users' => $users, 'instance' => $instance, 'pages' => $pages] = $this->setup_users_and_activity($groupmode, $wikimode); + $manager = manager::create_from_instance($instance); + $this->setUser($users[$username]); // Set the user to the one who created the wiki. + $pageid = $manager->get_main_wiki_pageid(); + $pagestoid = array_map(function($page) { + return $page->id; + }, $pages); + $idtopage = array_flip($pagestoid); + $this->assertEquals( + $expectedpage, + $idtopage[$pageid] ?? null, + "Page id for user $username does not match expected page id." + ); + + } + + /** + * Data provider for test_get_wiki_pageid. + * + * @return array + */ + public static function get_main_wiki_pageid_data_provider(): array { + return [ + 'teacher 1 (no group mode)' => [ + 'username' => 't1', + 'groupmode' => NOGROUPS, + 'wikimode' => wiki_mode::COLLABORATIVE, + 'expectedpage' => null, + ], + 'teacher 2 (no group mode)' => [ + 'username' => 't2', + 'groupmode' => NOGROUPS, + 'wikimode' => wiki_mode::COLLABORATIVE, + 'expectedpage' => null, + ], + 'student1 (no group mode, collaborative)' => [ + 'username' => 's1', + 'groupmode' => NOGROUPS, + 'wikimode' => wiki_mode::COLLABORATIVE, + 'expectedpage' => null, + ], + 'student1 (no group mode, no wiki mode)' => [ + 'username' => 's1', + 'groupmode' => SEPARATEGROUPS, + 'wikimode' => wiki_mode::UNDEFINED, + 'expectedpage' => 's1group-0001', // This is the first page created for all users (userid = 0). + ], + 'teacher 1 (separate group mode)' => [ + 'username' => 't1', + 'groupmode' => SEPARATEGROUPS, + 'wikimode' => wiki_mode::COLLABORATIVE, + 'expectedpage' => "s1group-0001", + ], + 'teacher 2 (separate group mode)' => [ + 'username' => 't2', + 'groupmode' => SEPARATEGROUPS, + 'wikimode' => wiki_mode::COLLABORATIVE, + 'expectedpage' => null, + ], + 'student 1 (separate group mode)' => [ + 'username' => 's1', + 'groupmode' => SEPARATEGROUPS, + 'wikimode' => wiki_mode::COLLABORATIVE, + 'expectedpage' => "s1group-0001", + ], + 'student 1 (separate group mode, no wiki mode)' => [ + 'username' => 's1', + 'groupmode' => SEPARATEGROUPS, + 'wikimode' => wiki_mode::UNDEFINED, + 'expectedpage' => "s1group-0001", + ], + 'teacher 1 (visible group mode)' => [ + 'username' => 't1', + 'groupmode' => VISIBLEGROUPS, + 'wikimode' => wiki_mode::COLLABORATIVE, + 'expectedpage' => "s1group-0001", + ], + 'teacher 2 (visible group mode)' => [ + 'username' => 't2', + 'groupmode' => VISIBLEGROUPS, + 'wikimode' => wiki_mode::COLLABORATIVE, + 'expectedpage' => "s1group-0001", + ], + 'student 1 (visible group mode)' => [ + 'username' => 's1', + 'groupmode' => VISIBLEGROUPS, + 'wikimode' => wiki_mode::COLLABORATIVE, + 'expectedpage' => "s1group-0001", + ], + 'student 1 (visible group mode, no wiki mode)' => [ + 'username' => 's1', + 'groupmode' => VISIBLEGROUPS, + 'wikimode' => wiki_mode::UNDEFINED, + 'expectedpage' => "s1group-0001", + ], + 'teacher 1 (no group mode) - individual' => [ + 'username' => 't1', + 'groupmode' => VISIBLEGROUPS, + 'wikimode' => wiki_mode::INDIVIDUAL, + 'expectedpage' => null, + ], + 'teacher 1 (separate group mode) - individual' => [ + 'username' => 't1', + 'groupmode' => VISIBLEGROUPS, + 'wikimode' => wiki_mode::INDIVIDUAL, + 'expectedpage' => null, + ], + 'teacher 1 (visible group mode) - individual' => [ + 'username' => 't1', + 'groupmode' => VISIBLEGROUPS, + 'wikimode' => wiki_mode::INDIVIDUAL, + 'expectedpage' => null, + ], + ]; + } +}