From a4cacd5c3358f6ba0ab8d8ffe86a32bbbc13f2c2 Mon Sep 17 00:00:00 2001 From: Markus Heck Date: Wed, 3 Apr 2024 16:52:25 +0200 Subject: [PATCH] add cli script to assign a user to a role on a given course category (or create a new course cat), code and documentation improvements. --- classes/local/course_category_manager.php | 19 +- classes/local/course_category_path.php | 38 ++-- classes/local/db/moodle_core_repository.php | 8 + classes/local/exceptions/exit_exception.php | 16 ++ classes/moodle_core.php | 4 + ...create_course_cat_and_assign_user_role.php | 73 +++++++ ...e_course_cat_and_assign_user_role_test.php | 179 ++++++++++++++++++ tests/local/course_category_path_test.php | 3 +- 8 files changed, 320 insertions(+), 20 deletions(-) create mode 100644 classes/local/exceptions/exit_exception.php create mode 100644 cli/create_course_cat_and_assign_user_role.php create mode 100644 tests/cli/create_course_cat_and_assign_user_role_test.php diff --git a/classes/local/course_category_manager.php b/classes/local/course_category_manager.php index 1970131..465c556 100644 --- a/classes/local/course_category_manager.php +++ b/classes/local/course_category_manager.php @@ -2,11 +2,22 @@ namespace local_adler\local; +use coding_exception; +use dml_exception; +use invalid_parameter_exception; use local_adler\local\db\moodle_core_repository; use local_adler\moodle_core; use moodle_exception; class course_category_manager { + /** + * @param string $username The username of the existing user. + * @param string $role shortname of the role to assign to the user. + * @param string|null $category_path The path of the category. If null or an empty string is passed, it initializes to "adler/{$username}". + * @throws dml_exception + * @throws moodle_exception + * @throws invalid_parameter_exception + */ public static function create_category_user_can_create_courses_in(string $username, string $role, string|null $category_path = Null) { $moodle_core_repository = new moodle_core_repository(); @@ -36,11 +47,15 @@ public static function create_category_user_can_create_courses_in(string $userna return $category_id; } - private static function assign_user_to_role_in_category(string $username, string $role, int $category_id) { + /** + * @throws coding_exception + * @throws dml_exception + */ + private static function assign_user_to_role_in_category(string $username, string $role_shortname, int $category_id): void { $moodle_core_repository = new moodle_core_repository(); $user_id = $moodle_core_repository->get_user_id_by_username($username); - $role_id = $moodle_core_repository->get_role_id_by_shortname($role); + $role_id = $moodle_core_repository->get_role_id_by_shortname($role_shortname); $context = moodle_core::context_coursecat_instance($category_id); moodle_core::role_assign($role_id, $user_id, $context->id); } diff --git a/classes/local/course_category_path.php b/classes/local/course_category_path.php index 52e8f55..6f1f5bd 100644 --- a/classes/local/course_category_path.php +++ b/classes/local/course_category_path.php @@ -11,7 +11,8 @@ class course_category_path implements Countable { private array $path; /** - * @param string|null $path the path in moodle (with spaces around the /) or UNIX format (without spaces), can be empty string or null + * @param string|null $path the path in moodle (with spaces around the /) or UNIX format (without spaces), + * can be empty string or null to initialize an empty path */ public function __construct(string|null $path) { if ($path === null || strlen($path) === 0) { @@ -28,6 +29,9 @@ public function __toString(): string { return implode(' / ', $this->path); } + /** + * @return array Returns the path as an array of strings. + */ public function get_path(): array { return $this->path; } @@ -36,25 +40,28 @@ public function count(): int { return count($this->path); } + /** + * @return bool Returns true if the category path exists in moodle, false otherwise. + */ public function exists(): bool { - try { - $this->get_category_id(); - return true; - } catch (moodle_exception $e) { + if ($this->get_category_id() === false) { return false; + } else { + return true; } } /** - * @throws moodle_exception if the category already exists + * @return int Returns the ID of the created category (the last category in the path). * @throws invalid_parameter_exception if the path is empty + * @throws moodle_exception if the category already exists */ public function create(): int { - if(count($this) === 0) { + if (count($this) === 0) { throw new invalid_parameter_exception('path must not be empty'); } - if($this->exists()) { + if ($this->exists()) { throw new moodle_exception('category_already_exists', 'local_adler'); } @@ -82,15 +89,11 @@ public function create(): int { /** - * @throws moodle_exception if the category does not exist + * @return int|bool Returns the ID of the category, or false if the category does not exist. */ - public function get_category_id(): int { + public function get_category_id(): int|bool { $categories = core_course_category::make_categories_list(); - $key = array_search((string)$this, $categories); - if ($key === false) { - throw new moodle_exception('category_not_found', 'local_adler'); - } - return $key; + return array_search((string)$this, $categories); } /** @@ -104,6 +107,10 @@ public function append_to_path(string $path_part): void { $this->path = array_merge($this->path, $this->split_and_trim_path($path_part)); } + /** + * @param string $path The path to split and trim. + * @return array Returns the path as an array of strings after splitting by '/' and trimming whitespace. + */ private function split_and_trim_path(string $path): array { // remove preceding and trailing / $path = trim($path, ' /'); @@ -111,5 +118,4 @@ private function split_and_trim_path(string $path): array { $path_parts = explode('/', $path); return array_map('trim', $path_parts); } - } diff --git a/classes/local/db/moodle_core_repository.php b/classes/local/db/moodle_core_repository.php index ca26080..f1a7044 100644 --- a/classes/local/db/moodle_core_repository.php +++ b/classes/local/db/moodle_core_repository.php @@ -3,12 +3,20 @@ namespace local_adler\local\db; +use dml_exception; + class moodle_core_repository extends base_repository { + /** + * @throws dml_exception + */ public function get_role_id_by_shortname(string $shortname): int { global $DB; return (int)$DB->get_field('role', 'id', array('shortname' => $shortname)); } + /** + * @throws dml_exception + */ public function get_user_id_by_username(string $username): int { global $DB; return (int)$DB->get_field('user', 'id', array('username' => $username)); diff --git a/classes/local/exceptions/exit_exception.php b/classes/local/exceptions/exit_exception.php new file mode 100644 index 0000000..b5ca66d --- /dev/null +++ b/classes/local/exceptions/exit_exception.php @@ -0,0 +1,16 @@ +libdir}/clilib.php"); + +$help = + "Create a new course category and grant the user permission to create adler courses in it. + +Options: +--username=STRING User name +--role=STRING Role name +--category_path=STRING Category path (optional) + +-h, --help Print out this help +"; + +// Parse command line arguments +list($options, $unrecognized) = cli_get_params( + array( + 'username' => false, + 'role' => false, + 'category_path' => false, + 'help' => false + ), + array( + 'h' => 'help' + ) +); + +if ($unrecognized) { + $unrecognized = implode("\n ", $unrecognized); + cli_write(get_string('cliunknowoption', 'admin', $unrecognized) . "\n"); + echo $help; + throw new exit_exception(1); +} + +if (!empty($options['help'])) { + echo $help; +} else { + if (empty(trim($options['username']))) { + cli_writeln('--username is required'); + throw new exit_exception(1); + } + + if (empty(trim($options['role']))) { + cli_writeln('--role is required'); + throw new exit_exception(1); + } + + $username = trim($options['username']); + $role = trim($options['role']); + $category_path = trim($options['category_path']); + + + try { + $category_id = course_category_manager::create_category_user_can_create_courses_in($username, $role, $category_path); + } catch (moodle_exception $e) { + cli_writeln($e->getMessage()); + throw new exit_exception(1); + } + + cli_writeln("Created category with ID $category_id and assigned user $username the role $role in it."); +} + + diff --git a/tests/cli/create_course_cat_and_assign_user_role_test.php b/tests/cli/create_course_cat_and_assign_user_role_test.php new file mode 100644 index 0000000..e9be6e6 --- /dev/null +++ b/tests/cli/create_course_cat_and_assign_user_role_test.php @@ -0,0 +1,179 @@ +dirroot . '/local/adler/tests/lib/adler_testcase.php'); + +class create_course_cat_and_assign_user_role_test extends adler_testcase { + /** + * @dataProvider provide_test_create_course_cat_and_assign_user_role_data + * @runInSeparateProcess + */ + public function test_create_course_cat_and_assign_user_role($username, $role, $category_path, $expected_parameters, $expected_exit_code) { + global $CFG; + + // Arrange + $mock = Mockery::mock('overload:' . course_category_manager::class); + $mock->shouldReceive('create_category_user_can_create_courses_in') + ->once() + ->with(...$expected_parameters) + ->andReturn(42); + + $_SERVER['argv'] = [ + 'create_course_cat_and_assign_user_role.php', + '--username=' . $username, + '--role=' . $role, + '--category_path=' . $category_path, + ]; + + + if ($expected_exit_code !== 0) { + $this->expectExceptionCode($expected_exit_code); + } + + // Act + require $CFG->dirroot . '/local/adler/cli/create_course_cat_and_assign_user_role.php'; + } + + public function provide_test_create_course_cat_and_assign_user_role_data() { + return [ + [ + 'username' => 'valid_username', + 'role' => 'valid_role', + 'category_path' => 'valid_category_path', + 'expected_parameters' => ['valid_username', 'valid_role', 'valid_category_path'], + 'expected_exit_code' => 0, + ], [ + 'username' => ' valid_username ', + 'role' => ' valid_role ', + 'category_path' => ' valid_category_path ', + 'expected_parameters' => ['valid_username', 'valid_role', 'valid_category_path'], + 'expected_exit_code' => 0, + ], [ + 'username' => 'valid_username', + 'role' => 'valid_role', + 'category_path' => null, + 'expected_parameters' => ['valid_username', 'valid_role', null], + 'expected_exit_code' => 0, + ], [ + 'username' => null, + 'role' => 'valid_role', + 'category_path' => 'valid_category_path', + 'expected_parameters' => [], + 'expected_exit_code' => 1, + ], [ + 'username' => '', + 'role' => 'valid_role', + 'category_path' => 'valid_category_path', + 'expected_parameters' => [], + 'expected_exit_code' => 1, + ], [ + 'username' => 'valid_username', + 'role' => null, + 'category_path' => 'valid_category_path', + 'expected_parameters' => [], + 'expected_exit_code' => 1, + ], + + ]; + } + + public function test_script_with_unknown_parameter() { + global $CFG; + + // Arrange + $_SERVER['argv'] = [ + 'create_course_cat_and_assign_user_role.php', + '--unknown_parameter=value', + ]; + + $this->expectException(exit_exception::class); + $this->expectExceptionCode(1); + + // Act + require $CFG->dirroot . '/local/adler/cli/create_course_cat_and_assign_user_role.php'; + } + + public function test_script_with_help_parameter() { + global $CFG; + + // Arrange + $_SERVER['argv'] = [ + 'create_course_cat_and_assign_user_role.php', + '--help', + ]; + + // Act + ob_start(); + require $CFG->dirroot . '/local/adler/cli/create_course_cat_and_assign_user_role.php'; + $output = ob_get_clean(); + + // Assert + $this->assertStringContainsString('username=STRING', $output); + } + + /** + * integration test + */ + public function test_script_with_valid_parameters() { + $category_path = 'test / valid_category_path'; + + global $CFG, $DB; + + // Arrange + $user = $this->getDataGenerator()->create_user(); + $role_id = $this->getDataGenerator()->create_role(); + $role = $DB->get_record('role', ['id' => $role_id]); + + $_SERVER['argv'] = [ + 'create_course_cat_and_assign_user_role.php', + '--username=' . $user->username, + '--role=' . $role->shortname, + '--category_path=' . $category_path, + ]; + + // Act + require $CFG->dirroot . '/local/adler/cli/create_course_cat_and_assign_user_role.php'; + + // Assert + // category exists + $category_id = array_search($category_path, core_course_category::make_categories_list()); + $this->assertNotFalse($category_id); + + // user has the role + // Check if user has role in category context + $users_with_role = get_role_users($role_id, context_coursecat::instance($category_id)); + $this->assertArrayHasKey($user->id, $users_with_role); + } + + /** + * integration test + */ + public function test_script_without_category_path() { + global $CFG, $DB; + + // Arrange + $user = $this->getDataGenerator()->create_user(); + $role_id = $this->getDataGenerator()->create_role(); + $role = $DB->get_record('role', ['id' => $role_id]); + + $_SERVER['argv'] = [ + 'create_course_cat_and_assign_user_role.php', + '--username=' . $user->username, + '--role=' . $role->shortname, + ]; + + $category_count_before = count(core_course_category::make_categories_list()); + + // Act + require $CFG->dirroot . '/local/adler/cli/create_course_cat_and_assign_user_role.php'; + + // Assert + // default creates two categories, "adler" and "adler / " + $this->assertEquals($category_count_before + 2, count(core_course_category::make_categories_list())); + } +} \ No newline at end of file diff --git a/tests/local/course_category_path_test.php b/tests/local/course_category_path_test.php index 798620f..e0dc250 100644 --- a/tests/local/course_category_path_test.php +++ b/tests/local/course_category_path_test.php @@ -87,8 +87,7 @@ public function test_get_category_id_method_category_does_not_exists() { $this->setup_make_categories_list_mock(); $path = new course_category_path('category5/category6'); - $this->expectException(moodle_exception::class); - $path->get_category_id(); + $this->assertEquals(false, $path->get_category_id()); } /**