Commit 1d4edcb5 authored by Amaia's avatar Amaia
Browse files

MDL-72099 core_contenbank: Add context navigation

parent 9145d80b
......@@ -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.
*
......
......@@ -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;
}
......
......@@ -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();
......
......@@ -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
}
]
}
]
}
}}
<div class="content-bank-container {{#viewlist}}view-list{{/viewlist}} {{^viewlist}}view-grid{{/viewlist}}"
data-region="contentbank">
<div class="d-flex justify-content-between flex-column flex-sm-row">
<div class="cb-search-container mb-2">
{{>core_contentbank/bankcontent/search}}
<div class="d-flex">
<div class="cb-navigation-container mb-2 mr-2">
{{>core_contentbank/bankcontent/navigation}}
</div>
<div class="cb-search-container mb-2">
{{>core_contentbank/bankcontent/search}}
</div>
</div>
<div class="cb-toolbar-container mb-2 d-flex">
{{>core_contentbank/bankcontent/toolbar}}
......
{{!
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 <http://www.gnu.org/licenses/>.
}}
{{!
@template core_contentbank/bankcontent/navigation
Example context (json):
{
}
}}
{{#allowedcontexts}}
{{> core/single_select }}
{{/allowedcontexts}}
@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"
......@@ -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';
......
......@@ -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';
......
......@@ -4102,24 +4102,31 @@ function count_role_users($roleid, context $context, $parent = false) {
}
/**
* This function gets the list of courses that this user has a particular capability in.
* 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 courses.
* 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 $fieldsexceptid Leave blank if you only need 'id' in the course records;
* @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 $orderby If set, use a comma-separated list of fields from course
* @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|bool Array of courses, if none found false is returned.
* @return array Array of categories and courses.
*/
function get_user_capability_course($capability, $userid = null, $doanything = true, $fieldsexceptid = '', $orderby = '',
$limit = 0) {
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.
......@@ -4135,58 +4142,64 @@ function get_user_capability_course($capability, $userid = null, $doanything = t
} 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);
$userid, $capability);
if (!$contextlimitsql) {
// If the does not have this capability in any context, return false without querying.
return false;
return [false, 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;
$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 .= ',';
}
$fieldlist .= ',x.' . $realfield . ' AS ' . $field;
} else {
$fieldlist .= ',c.'.$field;
$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;
}
}
}
if ($orderby) {
$fields = explode(',', $orderby);
$orderby = '';
$courses = [];
$fieldlist = \core\access\get_user_capability_course_helper::map_fieldnames($coursefieldsexceptid);
if ($courseorderby) {
$fields = explode(',', $courseorderby);
$courseorderby = '';
foreach ($fields as $field) {
if ($orderby) {
$orderby .= ',';
if ($courseorderby) {
$courseorderby .= ',';
}
$orderby .= 'c.'.$field;
$courseorderby .= 'c.'.$field;
}
$orderby = 'ORDER BY '.$orderby;
$courseorderby = 'ORDER BY '.$courseorderby;
}
$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 = ?
JOIN {context} x ON c.id = x.instanceid AND x.contextlevel = ?
$contextlimitsql
$orderby", array_merge([CONTEXT_COURSE], $contextlimitparams));
$courseorderby", array_merge([CONTEXT_COURSE], $contextlimitparams));
foreach ($rs as $course) {
$courses[] = $course;
$limit--;
......@@ -4195,7 +4208,40 @@ function get_user_capability_course($capability, $userid = null, $doanything = t
}
}
$rs->close();
return empty($courses) ? false : $courses;
return [$categories, $courses];
}
/**
* This function gets the list of courses 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 courses.
*
* @param string $capability Capability in question
* @param int $userid User ID or null for current user
* @param bool $doanything True if 'doanything' is permitted (default)
* @param string $fieldsexceptid 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 $orderby If set, use a comma-separated list of fields from course
* 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|bool Array of courses, if none found false is returned.
*/
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;
}
/**
......
......@@ -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;
}
}
......@@ -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',
],
],
);
......@@ -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 .