From 1d4edcb57b9680926f36b2e013bf29319dbb0c68 Mon Sep 17 00:00:00 2001 From: Amaia Anabitarte Date: Mon, 27 Sep 2021 08:19:38 +0200 Subject: [PATCH] MDL-72099 core_contenbank: Add context navigation --- contentbank/classes/contentbank.php | 31 +++ contentbank/classes/output/bankcontent.php | 53 ++++- contentbank/index.php | 2 +- contentbank/templates/bankcontent.mustache | 32 ++- .../templates/bankcontent/navigation.mustache | 27 +++ .../tests/behat/navigate_content.feature | 104 +++++++++ lang/en/cache.php | 2 + lang/en/contentbank.php | 1 + lib/accesslib.php | 202 +++++++++++------- .../get_user_capability_course_helper.php | 35 +++ lib/db/caches.php | 23 ++ lib/tests/accesslib_test.php | 83 +++++++ version.php | 2 +- 13 files changed, 511 insertions(+), 86 deletions(-) create mode 100644 contentbank/templates/bankcontent/navigation.mustache create mode 100644 contentbank/tests/behat/navigate_content.feature diff --git a/contentbank/classes/contentbank.php b/contentbank/classes/contentbank.php index 637405534b2..dc277f72c35 100644 --- a/contentbank/classes/contentbank.php +++ b/contentbank/classes/contentbank.php @@ -224,6 +224,37 @@ class contentbank { return $contents; } + + /** + * Return all the context where a user has all the given capabilities. + * + * @param string $capability The capability the user needs to have. + * @param int|null $userid Optional userid. $USER by default. + * @return array Array of the courses and course categories where the user has the given capability. + */ + public function get_contexts_with_capabilities_by_user($capability = 'moodle/contentbank:access', $userid = null): array { + global $USER; + + if (!$userid) { + $userid = $USER->id; + } + + $categoriescache = \cache::make('core', 'contentbank_allowed_categories'); + $coursescache = \cache::make('core', 'contentbank_allowed_courses'); + + $categories = $categoriescache->get($userid); + $courses = $coursescache->get($userid); + + if ($categories === false || $courses === false) { + list($categories, $courses) = get_user_capability_contexts($capability, true, $userid, true, + 'shortname, ctxlevel, ctxinstance, ctxid', 'name, ctxlevel, ctxinstance, ctxid', 'shortname', 'name'); + $categoriescache->set($userid, $categories); + $coursescache->set($userid, $courses); + } + + return [$categories, $courses]; + } + /** * Create content from a file information. * diff --git a/contentbank/classes/output/bankcontent.php b/contentbank/classes/output/bankcontent.php index 2678eaf41e9..a595f195c16 100644 --- a/contentbank/classes/output/bankcontent.php +++ b/contentbank/classes/output/bankcontent.php @@ -24,6 +24,7 @@ namespace core_contentbank\output; +use core_contentbank\contentbank; use renderable; use templatable; use renderer_base; @@ -53,17 +54,29 @@ class bankcontent implements renderable, templatable { */ private $context; + /** + * @var array Course categories that the user has access to. + */ + private $allowedcategories; + + /** + * @var array Courses that the user has access to. + */ + private $allowedcourses; + /** * Construct this renderable. * * @param \core_contentbank\content[] $contents Array of content bank contents. - * @param array $toolbar List of content bank toolbar options. + * @param array $toolbar List of content bank toolbar options. * @param \context $context Optional context to check (default null) + * @param contentbank $cb Contenbank object. */ - public function __construct(array $contents, array $toolbar, \context $context = null) { + public function __construct(array $contents, array $toolbar, \context $context = null, contentbank $cb) { $this->contents = $contents; $this->toolbar = $toolbar; $this->context = $context; + list($this->allowedcategories, $this->allowedcourses) = $cb->get_contexts_with_capabilities_by_user(); } /** @@ -73,7 +86,7 @@ class bankcontent implements renderable, templatable { * @return stdClass */ public function export_for_template(renderer_base $output): stdClass { - global $PAGE; + global $PAGE, $SITE; $PAGE->requires->js_call_amd('core_contentbank/search', 'init'); $PAGE->requires->js_call_amd('core_contentbank/sort', 'init'); @@ -118,6 +131,40 @@ class bankcontent implements renderable, templatable { $data->tools[] = $tool; } + $allowedcontexts = []; + $systemcontext = \context_system::instance(); + if (has_capability('moodle/contentbank:access', $systemcontext)) { + $allowedcontexts[$systemcontext->id] = get_string('coresystem'); + } + $options = []; + foreach ($this->allowedcategories as $allowedcategory) { + $options[$allowedcategory->ctxid] = $allowedcategory->name; + } + if (!empty($options)) { + $allowedcontexts['categories'] = [get_string('coursecategories') => $options]; + } + $options = []; + foreach ($this->allowedcourses as $allowedcourse) { + // Don't add the frontpage course to the list. + if ($allowedcourse->id != $SITE->id) { + $options[$allowedcourse->ctxid] = $allowedcourse->shortname; + } + } + if (!empty($options)) { + $allowedcontexts['courses'] = [get_string('courses') => $options]; + } + if (!empty($allowedcontexts)) { + $url = new \moodle_url('/contentbank/index.php'); + $singleselect = new \single_select( + $url, + 'contextid', + $allowedcontexts, + $this->context->id, + get_string('choosecontext', 'core_contentbank') + ); + $data->allowedcontexts = $singleselect->export_for_template($output); + } + return $data; } diff --git a/contentbank/index.php b/contentbank/index.php index 7845f58ecd4..121117a3a22 100644 --- a/contentbank/index.php +++ b/contentbank/index.php @@ -116,7 +116,7 @@ if ($errormsg !== '' && get_string_manager()->string_exists($errormsg, 'core_con } // Render the contentbank contents. -$folder = new \core_contentbank\output\bankcontent($foldercontents, $toolbar, $context); +$folder = new \core_contentbank\output\bankcontent($foldercontents, $toolbar, $context, $cb); echo $OUTPUT->render($folder); echo $OUTPUT->box_end(); diff --git a/contentbank/templates/bankcontent.mustache b/contentbank/templates/bankcontent.mustache index 6b500e1898e..c2a229fd150 100644 --- a/contentbank/templates/bankcontent.mustache +++ b/contentbank/templates/bankcontent.mustache @@ -75,15 +75,41 @@ { "icon": "i/export" } + ], + "allowedcontexts": [ + { + "name": "contextid", + "method": "get", + "action": "http://localhost/stable_master/contentbank/index.php", + "options": [ + { + "value": "1", + "name": "System", + "selected": true, + "optgroup": false + }, + { + "value": "32", + "name": "Category 1", + "selected": false, + "optgroup": false + } + ] + } ] } - }} +
-
- {{>core_contentbank/bankcontent/search}} +
+
+ {{>core_contentbank/bankcontent/navigation}} +
+
+ {{>core_contentbank/bankcontent/search}} +
{{>core_contentbank/bankcontent/toolbar}} diff --git a/contentbank/templates/bankcontent/navigation.mustache b/contentbank/templates/bankcontent/navigation.mustache new file mode 100644 index 00000000000..dbbb201b91e --- /dev/null +++ b/contentbank/templates/bankcontent/navigation.mustache @@ -0,0 +1,27 @@ +{{! + This file is part of Moodle - http://moodle.org/ + + Moodle is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Moodle is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Moodle. If not, see . +}} +{{! + @template core_contentbank/bankcontent/navigation + + Example context (json): + { + } +}} + +{{#allowedcontexts}} + {{> core/single_select }} +{{/allowedcontexts}} diff --git a/contentbank/tests/behat/navigate_content.feature b/contentbank/tests/behat/navigate_content.feature new file mode 100644 index 00000000000..99459896eb6 --- /dev/null +++ b/contentbank/tests/behat/navigate_content.feature @@ -0,0 +1,104 @@ +@core @core_contentbank @core_h5p @contentbank_h5p @_file_upload @javascript +Feature: Navigate to different contexts in the content bank + In order to navigate easily in the content bank + I need to be able to view dropdown with all allowed contexts in the content bank + + Background: + Given I log in as "admin" + And the following "categories" exist: + | name | category | idnumber | + | Cat 1 | 0 | CAT1 | + | Cat 2 | 0 | CAT2 | + And the following "courses" exist: + | fullname | shortname | category | + | Course 0 | C0 | | + | Course 1 | C1 | CAT1 | + | Course 2 | C2 | CAT2 | + And I navigate to "H5P > Manage H5P content types" in site administration + And I upload "h5p/tests/fixtures/filltheblanks.h5p" file to "H5P content type" filemanager + And I click on "Upload H5P content types" "button" in the "#fitem_id_uploadlibraries" "css_element" + And the following "contentbank content" exist: + | contextlevel | reference | contenttype | user | contentname | filepath | + | System | | contenttype_h5p | admin | santjordi.h5p | /h5p/tests/fixtures/filltheblanks.h5p | + | Category | CAT1 | contenttype_h5p | admin | santjordi_rose.h5p | /h5p/tests/fixtures/filltheblanks.h5p | + | Category | CAT2 | contenttype_h5p | admin | SantJordi_book | /h5p/tests/fixtures/filltheblanks.h5p | + | Course | C0 | contenttype_h5p | admin | Dragon.h5p | /h5p/tests/fixtures/filltheblanks.h5p | + | Course | C1 | contenttype_h5p | admin | princess.h5p | /h5p/tests/fixtures/filltheblanks.h5p | + | Course | C2 | contenttype_h5p | admin | mathsbook.h5p | /h5p/tests/fixtures/filltheblanks.h5p | + + Scenario: Admins can view and navigate to all the contexts in the content bank + Given I am on site homepage + And I turn editing mode on + And I add the "Navigation" block if not present + And I expand "Site pages" node + When I click on "Content bank" "link" + And "contextid" "select" should exist + And the "contextid" select box should contain "System" + And the "contextid" select box should contain "Cat 1" + And the "contextid" select box should contain "Cat 2" + And the "contextid" select box should contain "C0" + And the "contextid" select box should contain "C1" + And the "contextid" select box should contain "C2" + And I should see "santjordi.h5p" + And I should not see "santjordi_rose.h5p" + And I should not see "Dragon.h5p" + And I click on "contextid" "select" + And I click on "Cat 1" "option" + Then I should not see "santjordi.h5p" + And I should see "santjordi_rose.h5p" + And I should not see "Dragon.h5p" + And I click on "contextid" "select" + And I click on "C0" "option" + And I should not see "santjordi.h5p" + And I should not see "santjordi_rose.h5p" + And I should see "Dragon.h5p" + + Scenario: Teachers can view and navigate to contexts in the content bank based on their permissions + Given the following "users" exist: + | username | firstname | lastname | + | teacher | Joseba | Cilarte | + And the following "course enrolments" exist: + | user | course | role | + | teacher | C0 | editingteacher | + | teacher | C1 | editingteacher | + And I log out + And I am on the "C0" "Course" page logged in as "teacher" + And I turn editing mode on + And I add the "Navigation" block if not present + And I expand "Site pages" node + When I click on "Content bank" "link" + And "contextid" "select" should exist + And the "contextid" select box should contain "C0" + And the "contextid" select box should contain "C1" + And the "contextid" select box should not contain "System" + And the "contextid" select box should not contain "Cat 1" + And the "contextid" select box should not contain "Cat 2" + And the "contextid" select box should not contain "C2" + And I should see "Dragon.h5p" + And I should not see "princess.h5p" + And I should not see "santjordi.h5p" + And I should not see "santjordi_rose.h5p" + And I click on "contextid" "select" + And I click on "C1" "option" + Then I should not see "Dragon.h5p" + And I should see "princess.h5p" + And I should not see "santjordi.h5p" + And I should not see "santjordi_rose.h5p" + And the following "role assigns" exist: + | user | role | contextlevel | reference | + | teacher | manager | Category | CAT1 | + And I am on the "C0" "Course" page logged in as "teacher" + And I expand "Site pages" node + When I click on "Content bank" "link" + And "contextid" "select" should exist + And the "contextid" select box should contain "C0" + And the "contextid" select box should contain "C1" + And the "contextid" select box should contain "Cat 1" + And the "contextid" select box should not contain "System" + And the "contextid" select box should not contain "Cat 2" + And the "contextid" select box should not contain "C2" + And I should see "Dragon.h5p" + And I click on "contextid" "select" + And I click on "Cat 1" "option" + And I should not see "Dragon.h5p" + And I should see "santjordi_rose.h5p" diff --git a/lang/en/cache.php b/lang/en/cache.php index d4eac728c9e..4f24cdbf2f2 100644 --- a/lang/en/cache.php +++ b/lang/en/cache.php @@ -41,6 +41,8 @@ $string['cachedef_calendar_subscriptions'] = 'Calendar subscriptions'; $string['cachedef_calendar_categories'] = 'Calendar course categories that a user can access'; $string['cachedef_capabilities'] = 'System capabilities list'; $string['cachedef_config'] = 'Config settings'; +$string['cachedef_contentbank_allowed_categories'] = 'Allowed content bank course categories for current user'; +$string['cachedef_contentbank_allowed_courses'] = 'Allowed content bank courses for current user'; $string['cachedef_contentbank_enabled_extensions'] = 'Allowed extensions and its supporter plugins in content bank'; $string['cachedef_contentbank_context_extensions'] = 'Allowed extensions and its supporter plugins in a content bank context'; $string['cachedef_coursecat'] = 'Course categories lists for particular user'; diff --git a/lang/en/contentbank.php b/lang/en/contentbank.php index 69c3c32a2f4..c127217dc7e 100644 --- a/lang/en/contentbank.php +++ b/lang/en/contentbank.php @@ -25,6 +25,7 @@ $string['author'] = 'Author'; $string['contentbank'] = 'Content bank'; $string['close'] = 'Close'; +$string['choosecontext'] = 'Choose course or category...'; $string['contentbankpreferences'] = 'Content bank preferences'; $string['contentdeleted'] = 'The content has been deleted.'; $string['contentname'] = 'Content name'; diff --git a/lib/accesslib.php b/lib/accesslib.php index a79f7bb005a..11669e1dd6b 100644 --- a/lib/accesslib.php +++ b/lib/accesslib.php @@ -4101,6 +4101,116 @@ function count_role_users($roleid, context $context, $parent = false) { return $DB->count_records_sql($sql, $params); } +/** + * This function gets the list of course and course category contexts that this user has a particular capability in. + * + * It is now reasonably efficient, but bear in mind that if there are users who have the capability + * everywhere, it may return an array of all contexts. + * + * @param string $capability Capability in question + * @param int $userid User ID or null for current user + * @param bool $getcategories Wether to return also course_categories + * @param bool $doanything True if 'doanything' is permitted (default) + * @param string $coursefieldsexceptid Leave blank if you only need 'id' in the course records; + * otherwise use a comma-separated list of the fields you require, not including id. + * Add ctxid, ctxpath, ctxdepth etc to return course context information for preloading. + * @param string $categoryfieldsexceptid Leave blank if you only need 'id' in the course records; + * otherwise use a comma-separated list of the fields you require, not including id. + * Add ctxid, ctxpath, ctxdepth etc to return course context information for preloading. + * @param string $courseorderby If set, use a comma-separated list of fields from course + * table with sql modifiers (DESC) if needed + * @param string $categoryorderby If set, use a comma-separated list of fields from course_category + * table with sql modifiers (DESC) if needed + * @param int $limit Limit the number of courses to return on success. Zero equals all entries. + * @return array Array of categories and courses. + */ +function get_user_capability_contexts(string $capability, bool $getcategories, $userid = null, $doanything = true, + $coursefieldsexceptid = '', $categoryfieldsexceptid = '', $courseorderby = '', + $categoryorderby = '', $limit = 0): array { + global $DB, $USER; + + // Default to current user. + if (!$userid) { + $userid = $USER->id; + } + + if ($doanything && is_siteadmin($userid)) { + // If the user is a site admin and $doanything is enabled then there is no need to restrict + // the list of courses. + $contextlimitsql = ''; + $contextlimitparams = []; + } else { + // Gets SQL to limit contexts ('x' table) to those where the user has this capability. + list ($contextlimitsql, $contextlimitparams) = \core\access\get_user_capability_course_helper::get_sql( + $userid, $capability); + if (!$contextlimitsql) { + // If the does not have this capability in any context, return false without querying. + return [false, false]; + } + + $contextlimitsql = 'WHERE' . $contextlimitsql; + } + + $categories = []; + if ($getcategories) { + $fieldlist = \core\access\get_user_capability_course_helper::map_fieldnames($categoryfieldsexceptid); + if ($categoryorderby) { + $fields = explode(',', $categoryorderby); + $orderby = ''; + foreach ($fields as $field) { + if ($orderby) { + $orderby .= ','; + } + $orderby .= 'c.'.$field; + } + $orderby = 'ORDER BY '.$orderby; + } + $rs = $DB->get_recordset_sql(" + SELECT c.id $fieldlist + FROM {course_categories} c + JOIN {context} x ON c.id = x.instanceid AND x.contextlevel = ? + $contextlimitsql + $orderby", array_merge([CONTEXT_COURSECAT], $contextlimitparams)); + $basedlimit = $limit; + foreach ($rs as $category) { + $categories[] = $category; + $basedlimit--; + if ($basedlimit == 0) { + break; + } + } + } + + $courses = []; + $fieldlist = \core\access\get_user_capability_course_helper::map_fieldnames($coursefieldsexceptid); + if ($courseorderby) { + $fields = explode(',', $courseorderby); + $courseorderby = ''; + foreach ($fields as $field) { + if ($courseorderby) { + $courseorderby .= ','; + } + $courseorderby .= 'c.'.$field; + } + $courseorderby = 'ORDER BY '.$courseorderby; + } + $rs = $DB->get_recordset_sql(" + SELECT c.id $fieldlist + FROM {course} c + JOIN {context} x ON c.id = x.instanceid AND x.contextlevel = ? + $contextlimitsql + $courseorderby", array_merge([CONTEXT_COURSE], $contextlimitparams)); + foreach ($rs as $course) { + $courses[] = $course; + $limit--; + if ($limit == 0) { + break; + } + } + $rs->close(); + return [$categories, $courses]; +} + /** * This function gets the list of courses that this user has a particular capability in. * @@ -4118,84 +4228,20 @@ function count_role_users($roleid, context $context, $parent = false) { * @param int $limit Limit the number of courses to return on success. Zero equals all entries. * @return array|bool Array of courses, if none found false is returned. */ -function get_user_capability_course($capability, $userid = null, $doanything = true, $fieldsexceptid = '', $orderby = '', - $limit = 0) { - global $DB, $USER; - - // Default to current user. - if (!$userid) { - $userid = $USER->id; - } - - if ($doanything && is_siteadmin($userid)) { - // If the user is a site admin and $doanything is enabled then there is no need to restrict - // the list of courses. - $contextlimitsql = ''; - $contextlimitparams = []; - } else { - // Gets SQL to limit contexts ('x' table) to those where the user has this capability. - list ($contextlimitsql, $contextlimitparams) = \core\access\get_user_capability_course_helper::get_sql( - $userid, $capability); - if (!$contextlimitsql) { - // If the does not have this capability in any context, return false without querying. - return false; - } - - $contextlimitsql = 'WHERE' . $contextlimitsql; - } - - // Convert fields list and ordering - $fieldlist = ''; - if ($fieldsexceptid) { - $fields = array_map('trim', explode(',', $fieldsexceptid)); - foreach ($fields as $field) { - // Context fields have a different alias. - if (strpos($field, 'ctx') === 0) { - switch($field) { - case 'ctxlevel' : - $realfield = 'contextlevel'; - break; - case 'ctxinstance' : - $realfield = 'instanceid'; - break; - default: - $realfield = substr($field, 3); - break; - } - $fieldlist .= ',x.' . $realfield . ' AS ' . $field; - } else { - $fieldlist .= ',c.'.$field; - } - } - } - if ($orderby) { - $fields = explode(',', $orderby); - $orderby = ''; - foreach ($fields as $field) { - if ($orderby) { - $orderby .= ','; - } - $orderby .= 'c.'.$field; - } - $orderby = 'ORDER BY '.$orderby; - } - - $courses = array(); - $rs = $DB->get_recordset_sql(" - SELECT c.id $fieldlist - FROM {course} c - JOIN {context} x ON c.id = x.instanceid AND x.contextlevel = ? - $contextlimitsql - $orderby", array_merge([CONTEXT_COURSE], $contextlimitparams)); - foreach ($rs as $course) { - $courses[] = $course; - $limit--; - if ($limit == 0) { - break; - } - } - $rs->close(); - return empty($courses) ? false : $courses; +function get_user_capability_course($capability, $userid = null, $doanything = true, $fieldsexceptid = '', + $orderby = '', $limit = 0) { + list($categories, $courses) = get_user_capability_contexts( + $capability, + false, + $userid, + $doanything, + $fieldsexceptid, + '', + $orderby, + '', + $limit + ); + return $courses; } /** diff --git a/lib/classes/access/get_user_capability_course_helper.php b/lib/classes/access/get_user_capability_course_helper.php index 93dccfa5a99..194a427bd66 100644 --- a/lib/classes/access/get_user_capability_course_helper.php +++ b/lib/classes/access/get_user_capability_course_helper.php @@ -429,4 +429,39 @@ class get_user_capability_course_helper { return self::create_sql($root); } + + /** + * Map fieldnames to get ready for the SQL query. + * + * @param string $fieldsexceptid A comma-separated list of the fields you require, not including id. + * Add ctxid, ctxpath, ctxdepth etc to return course context information for preloading. + * @return string Mapped field list for the SQL query. + */ + public static function map_fieldnames(string $fieldsexceptid = ''): string { + // Convert fields list and ordering. + $fieldlist = ''; + if ($fieldsexceptid) { + $fields = array_map('trim', explode(',', $fieldsexceptid)); + foreach ($fields as $field) { + // Context fields have a different alias. + if (strpos($field, 'ctx') === 0) { + switch($field) { + case 'ctxlevel' : + $realfield = 'contextlevel'; + break; + case 'ctxinstance' : + $realfield = 'instanceid'; + break; + default: + $realfield = substr($field, 3); + break; + } + $fieldlist .= ',x.' . $realfield . ' AS ' . $field; + } else { + $fieldlist .= ',c.'.$field; + } + } + } + return $fieldlist; + } } diff --git a/lib/db/caches.php b/lib/db/caches.php index fac98b7aa63..3be3ae37072 100644 --- a/lib/db/caches.php +++ b/lib/db/caches.php @@ -493,4 +493,27 @@ $definitions = array( 'staticacceleration' => true, 'datasource' => '\core_course\cache\course_image', ], + + // Cache the course categories where the user has access the content bank. + 'contentbank_allowed_categories' => [ + 'mode' => cache_store::MODE_SESSION, + 'simplekeys' => true, + 'simpledata' => true, + 'invalidationevents' => [ + 'changesincoursecat', + 'changesincategoryenrolment', + ], + ], + + // Cache the courses where the user has access the content bank. + 'contentbank_allowed_courses' => [ + 'mode' => cache_store::MODE_SESSION, + 'simplekeys' => true, + 'simpledata' => true, + 'invalidationevents' => [ + 'changesincoursecat', + 'changesincategoryenrolment', + 'changesincourse', + ], + ], ); diff --git a/lib/tests/accesslib_test.php b/lib/tests/accesslib_test.php index 116db957fc4..509e47e0daa 100644 --- a/lib/tests/accesslib_test.php +++ b/lib/tests/accesslib_test.php @@ -2230,6 +2230,89 @@ class core_accesslib_testcase extends advanced_testcase { $this->assert_course_ids([SITEID, $c1->id, $c2->id], $courses); } + /** + * Tests get_user_capability_contexts() which checks a capability across all courses and categories. + * Testing for categories only because courses results are covered by test_get_user_capability_course. + */ + public function test_get_user_capability_contexts() { + $this->resetAfterTest(); + + $generator = $this->getDataGenerator(); + $cap = 'moodle/contentbank:access'; + $defaultcategoryid = 1; + +// The structure being created here is this: +// +// All tests work with the single capability 'moodle/contentbank:access'. +// ROLE DEF/OVERRIDE . +// Role: Allow Prohibit Empty . +// System ALLOW PROHIBIT . +// cat1 PREVENT ALLOW ALLOW . +// cat3 ALLOW PROHIBIT . +// cat2 PROHIBIT PROHIBIT PROHIBIT . + + // Create a role which allows contentbank:access and one that prohibits it, and one neither. + $allowroleid = $generator->create_role(); + $prohibitroleid = $generator->create_role(); + $emptyroleid = $generator->create_role(); + $systemcontext = context_system::instance(); + assign_capability($cap, CAP_ALLOW, $allowroleid, $systemcontext->id); + assign_capability($cap, CAP_PROHIBIT, $prohibitroleid, $systemcontext->id); + + // Create three categories (two of them nested). + $cat1 = $generator->create_category(); + $cat2 = $generator->create_category(); + $cat3 = $generator->create_category(['parent' => $cat1->id]); + + // Category overrides: in cat 1, empty role is allowed; in cat 2, empty role is prevented. + assign_capability($cap, CAP_ALLOW, $emptyroleid, + context_coursecat::instance($cat1->id)->id); + assign_capability($cap, CAP_PREVENT, $emptyroleid, + context_coursecat::instance($cat2->id)->id); + + // Course category overrides: in cat1, allow role is prevented and prohibit role is allowed; + // in Cat2, allow role is prohibited. + assign_capability($cap, CAP_PREVENT, $allowroleid, + context_coursecat::instance($cat1->id)->id); + assign_capability($cap, CAP_ALLOW, $prohibitroleid, + context_coursecat::instance($cat1->id)->id); + assign_capability($cap, CAP_PROHIBIT, $allowroleid, + context_coursecat::instance($cat2->id)->id); + + // User 1 has no roles except default user role. + $u1 = $generator->create_user(); + + // It returns false (annoyingly) if there are no course categories. + list($categories, $courses) = get_user_capability_contexts($cap, true, $u1->id, true, '', '', '', 'id'); + $this->assertFalse($categories); + + // User 2 has allow role (system wide). + $u2 = $generator->create_user(); + role_assign($allowroleid, $u2->id, $systemcontext->id); + + // Should get $defaultcategory only. cat2 is prohibited; cat1 is prevented, so cat3 is not allowed. + list($categories, $courses) = get_user_capability_contexts($cap, true, $u2->id, true, '', '', '', 'id'); + // Using same assert_course_ids helper even when we are checking course category ids. + $this->assert_course_ids([$defaultcategoryid], $categories); + + // User 3 has empty role (system wide). + $u3 = $generator->create_user(); + role_assign($emptyroleid, $u3->id, $systemcontext->id); + + // Should get cat1 and cat3. cat2 is prohibited; no access to system level. + list($categories, $courses) = get_user_capability_contexts($cap, true, $u3->id, true, '', '', '', 'id'); + $this->assert_course_ids([$cat1->id, $cat3->id], $categories); + + // User 4 has prohibit role (system wide). + $u4 = $generator->create_user(); + role_assign($prohibitroleid, $u4->id, $systemcontext->id); + + // Should not get any, because all of them are prohibited at system level. + // Even if we try to allow an specific category. + list($categories, $courses) = get_user_capability_contexts($cap, true, $u4->id, true, '', '', '', 'id'); + $this->assertFalse($categories); + } + /** * Extracts an array of course ids to make the above test script shorter. * diff --git a/version.php b/version.php index 01dd2a6f7a0..f96f1d9c8d1 100644 --- a/version.php +++ b/version.php @@ -29,7 +29,7 @@ defined('MOODLE_INTERNAL') || die(); -$version = 2021101300.00; // YYYYMMDD = weekly release date of this DEV branch. +$version = 2021101300.01; // YYYYMMDD = weekly release date of this DEV branch. // RR = release increments - 00 in DEV branches. // .XX = incremental changes. $release = '4.0dev+ (Build: 20211013)'; // Human-friendly version name