mirror of
https://github.com/moodle/moodle.git
synced 2025-04-13 20:42:22 +02:00
MDL-53363 tags: helper to search for tagged items; search course modules
This commit is contained in:
parent
b611ade3ab
commit
4218ad9371
106
course/lib.php
106
course/lib.php
@ -3917,3 +3917,109 @@ function core_course_inplace_editable($itemtype, $itemid, $newvalue) {
|
||||
return \core_course\output\course_module_name::update($itemid, $newvalue);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns course modules tagged with a specified tag ready for output on tag/index.php page
|
||||
*
|
||||
* This is a callback used by the tag area core/course_modules to search for course modules
|
||||
* tagged with a specific tag.
|
||||
*
|
||||
* @param core_tag_tag $tag
|
||||
* @param bool $exclusivemode if set to true it means that no other entities tagged with this tag
|
||||
* are displayed on the page and the per-page limit may be bigger
|
||||
* @param int $fromcontextid context id where the link was displayed, may be used by callbacks
|
||||
* to display items in the same context first
|
||||
* @param int $contextid context id where to search for records
|
||||
* @param bool $recursivecontext search in subcontexts as well
|
||||
* @param int $page 0-based number of page being displayed
|
||||
* @return \core_tag\output\tagindex
|
||||
*/
|
||||
function course_get_tagged_course_modules($tag, $exclusivemode = false, $fromcontextid = 0, $contextid = 0,
|
||||
$recursivecontext = 1, $page = 0) {
|
||||
global $OUTPUT;
|
||||
$perpage = $exclusivemode ? 20 : 5;
|
||||
|
||||
// Build select query.
|
||||
$ctxselect = context_helper::get_preload_record_columns_sql('ctx');
|
||||
$query = "SELECT cm.id AS cmid, c.id AS courseid, $ctxselect
|
||||
FROM {course_modules} cm
|
||||
JOIN {tag_instance} tt ON cm.id = tt.itemid
|
||||
JOIN {course} c ON cm.course = c.id
|
||||
JOIN {context} ctx ON ctx.instanceid = cm.id AND ctx.contextlevel = :coursemodulecontextlevel
|
||||
WHERE tt.itemtype = :itemtype AND tt.tagid = :tagid AND tt.component = :component
|
||||
AND c.id %COURSEFILTER% AND cm.id %ITEMFILTER%";
|
||||
|
||||
$params = array('itemtype' => 'course_modules', 'tagid' => $tag->id, 'component' => 'core',
|
||||
'coursemodulecontextlevel' => CONTEXT_MODULE);
|
||||
if ($contextid) {
|
||||
$context = context::instance_by_id($contextid);
|
||||
$query .= $recursivecontext ? ' AND (ctx.id = :contextid OR ctx.path LIKE :path)' : ' AND ctx.id = :contextid';
|
||||
$params['contextid'] = $context->id;
|
||||
$params['path'] = $context->path.'/%';
|
||||
}
|
||||
|
||||
$query .= ' ORDER BY';
|
||||
if ($fromcontextid) {
|
||||
// In order-clause specify that modules from inside "fromctx" context should be returned first.
|
||||
$fromcontext = context::instance_by_id($fromcontextid);
|
||||
$query .= ' (CASE WHEN ctx.id = :fromcontextid OR ctx.path LIKE :frompath THEN 0 ELSE 1 END),';
|
||||
$params['fromcontextid'] = $fromcontext->id;
|
||||
$params['frompath'] = $fromcontext->path.'/%';
|
||||
}
|
||||
$query .= ' c.sortorder, cm.id';
|
||||
$totalpages = $page + 1;
|
||||
|
||||
// Use core_tag_index_builder to build and filter the list of items.
|
||||
// Request one item more than we need so we know if next page exists.
|
||||
$builder = new core_tag_index_builder('core', 'course_modules', $query, $params, $page * $perpage, $perpage + 1);
|
||||
while ($item = $builder->has_item_that_needs_access_check()) {
|
||||
context_helper::preload_from_record($item);
|
||||
$courseid = $item->courseid;
|
||||
if (!$builder->can_access_course($courseid)) {
|
||||
$builder->set_accessible($item, false);
|
||||
continue;
|
||||
}
|
||||
$modinfo = get_fast_modinfo($builder->get_course($courseid));
|
||||
// Set accessibility of this item and all other items in the same course.
|
||||
$builder->walk(function ($taggeditem) use ($courseid, $modinfo, $builder) {
|
||||
if ($taggeditem->courseid == $courseid) {
|
||||
$cm = $modinfo->get_cm($taggeditem->cmid);
|
||||
$builder->set_accessible($taggeditem, $cm->uservisible);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
$items = $builder->get_items();
|
||||
if (count($items) > $perpage) {
|
||||
$totalpages = $page + 2; // We don't need exact page count, just indicate that the next page exists.
|
||||
array_pop($items);
|
||||
}
|
||||
|
||||
// Build the display contents.
|
||||
if ($items) {
|
||||
$tagfeed = new core_tag\output\tagfeed();
|
||||
foreach ($items as $item) {
|
||||
context_helper::preload_from_record($item);
|
||||
$course = $builder->get_course($item->courseid);
|
||||
$modinfo = get_fast_modinfo($course);
|
||||
$cm = $modinfo->get_cm($item->cmid);
|
||||
$courseurl = course_get_url($item->courseid, $cm->sectionnum);
|
||||
$cmname = $cm->get_formatted_name();
|
||||
if (!$exclusivemode) {
|
||||
$cmname = shorten_text($cmname, 100);
|
||||
}
|
||||
$cmname = html_writer::link($cm->url?:$courseurl, $cmname);
|
||||
$coursename = format_string($course->fullname, true,
|
||||
array('context' => context_course::instance($item->courseid)));
|
||||
$coursename = html_writer::link($courseurl, $coursename);
|
||||
$icon = html_writer::empty_tag('img', array('src' => $cm->get_icon_url()));
|
||||
$tagfeed->add($icon, $cmname, $coursename);
|
||||
}
|
||||
|
||||
$content = $OUTPUT->render_from_template('core_tag/tagfeed',
|
||||
$tagfeed->export_for_template($OUTPUT));
|
||||
|
||||
return new core_tag\output\tagindex($tag, 'core', 'course_modules', $content,
|
||||
$exclusivemode, $fromcontextid, $contextid, $recursivecontext, $page, $totalpages);
|
||||
}
|
||||
}
|
||||
|
@ -2809,4 +2809,117 @@ class core_course_courselib_testcase extends advanced_testcase {
|
||||
$this->assertEquals('New forum name', $res['value']);
|
||||
$this->assertEquals('New forum name', $DB->get_field('forum', 'name', array('id' => $forum->id)));
|
||||
}
|
||||
|
||||
/**
|
||||
* Testing function course_get_tagged_course_modules - search tagged course modules
|
||||
*/
|
||||
public function test_course_get_tagged_course_modules() {
|
||||
global $DB;
|
||||
$this->resetAfterTest();
|
||||
$course3 = $this->getDataGenerator()->create_course();
|
||||
$course2 = $this->getDataGenerator()->create_course();
|
||||
$course1 = $this->getDataGenerator()->create_course();
|
||||
$cm11 = $this->getDataGenerator()->create_module('assign', array('course' => $course1->id,
|
||||
'tags' => 'Cat, Dog'));
|
||||
$cm12 = $this->getDataGenerator()->create_module('page', array('course' => $course1->id,
|
||||
'tags' => 'Cat, Mouse', 'visible' => 0));
|
||||
$cm13 = $this->getDataGenerator()->create_module('page', array('course' => $course1->id,
|
||||
'tags' => 'Cat, Mouse, Dog'));
|
||||
$cm21 = $this->getDataGenerator()->create_module('forum', array('course' => $course2->id,
|
||||
'tags' => 'Cat, Mouse'));
|
||||
$cm31 = $this->getDataGenerator()->create_module('forum', array('course' => $course3->id,
|
||||
'tags' => 'Cat, Mouse'));
|
||||
|
||||
// Admin is able to view everything.
|
||||
$this->setAdminUser();
|
||||
$res = course_get_tagged_course_modules(core_tag_tag::get_by_name(0, 'Cat'),
|
||||
/*$exclusivemode = */false, /*$fromctx = */0, /*$ctx = */0, /*$rec = */1, /*$page = */0);
|
||||
$this->assertRegExp('/'.$cm11->name.'/', $res->content);
|
||||
$this->assertRegExp('/'.$cm12->name.'/', $res->content);
|
||||
$this->assertRegExp('/'.$cm13->name.'/', $res->content);
|
||||
$this->assertRegExp('/'.$cm21->name.'/', $res->content);
|
||||
$this->assertRegExp('/'.$cm31->name.'/', $res->content);
|
||||
// Results from course1 are returned before results from course2.
|
||||
$this->assertTrue(strpos($res->content, $cm11->name) < strpos($res->content, $cm21->name));
|
||||
|
||||
// Ordinary user is not able to see anything.
|
||||
$user = $this->getDataGenerator()->create_user();
|
||||
$this->setUser($user);
|
||||
|
||||
$res = course_get_tagged_course_modules(core_tag_tag::get_by_name(0, 'Cat'),
|
||||
/*$exclusivemode = */false, /*$fromctx = */0, /*$ctx = */0, /*$rec = */1, /*$page = */0);
|
||||
$this->assertNull($res);
|
||||
|
||||
// Enrol user as student in course1 and course2.
|
||||
$roleids = $DB->get_records_menu('role', null, '', 'shortname, id');
|
||||
$this->getDataGenerator()->enrol_user($user->id, $course1->id, $roleids['student']);
|
||||
$this->getDataGenerator()->enrol_user($user->id, $course2->id, $roleids['student']);
|
||||
core_tag_index_builder::reset_caches();
|
||||
|
||||
// Searching in the course context returns visible modules in this course.
|
||||
$context = context_course::instance($course1->id);
|
||||
$res = course_get_tagged_course_modules(core_tag_tag::get_by_name(0, 'Cat'),
|
||||
/*$exclusivemode = */false, /*$fromctx = */0, /*$ctx = */$context->id, /*$rec = */1, /*$page = */0);
|
||||
$this->assertRegExp('/'.$cm11->name.'/', $res->content);
|
||||
$this->assertNotRegExp('/'.$cm12->name.'/', $res->content);
|
||||
$this->assertRegExp('/'.$cm13->name.'/', $res->content);
|
||||
$this->assertNotRegExp('/'.$cm21->name.'/', $res->content);
|
||||
$this->assertNotRegExp('/'.$cm31->name.'/', $res->content);
|
||||
|
||||
// Searching FROM the course context returns visible modules in all courses.
|
||||
$context = context_course::instance($course2->id);
|
||||
$res = course_get_tagged_course_modules(core_tag_tag::get_by_name(0, 'Cat'),
|
||||
/*$exclusivemode = */false, /*$fromctx = */$context->id, /*$ctx = */0, /*$rec = */1, /*$page = */0);
|
||||
$this->assertRegExp('/'.$cm11->name.'/', $res->content);
|
||||
$this->assertNotRegExp('/'.$cm12->name.'/', $res->content);
|
||||
$this->assertRegExp('/'.$cm13->name.'/', $res->content);
|
||||
$this->assertRegExp('/'.$cm21->name.'/', $res->content);
|
||||
$this->assertNotRegExp('/'.$cm31->name.'/', $res->content); // No access to course3.
|
||||
// Results from course2 are returned before results from course1.
|
||||
$this->assertTrue(strpos($res->content, $cm21->name) < strpos($res->content, $cm11->name));
|
||||
|
||||
// Enrol user in course1 as a teacher - now he should be able to see hidden module.
|
||||
$this->getDataGenerator()->enrol_user($user->id, $course1->id, $roleids['editingteacher']);
|
||||
get_fast_modinfo(0,0,true);
|
||||
|
||||
$context = context_course::instance($course1->id);
|
||||
$res = course_get_tagged_course_modules(core_tag_tag::get_by_name(0, 'Cat'),
|
||||
/*$exclusivemode = */false, /*$fromctx = */$context->id, /*$ctx = */0, /*$rec = */1, /*$page = */0);
|
||||
$this->assertRegExp('/'.$cm12->name.'/', $res->content);
|
||||
|
||||
// Create more modules and try pagination.
|
||||
$cm14 = $this->getDataGenerator()->create_module('assign', array('course' => $course1->id,
|
||||
'tags' => 'Cat, Dog'));
|
||||
$cm15 = $this->getDataGenerator()->create_module('page', array('course' => $course1->id,
|
||||
'tags' => 'Cat, Mouse', 'visible' => 0));
|
||||
$cm16 = $this->getDataGenerator()->create_module('page', array('course' => $course1->id,
|
||||
'tags' => 'Cat, Mouse, Dog'));
|
||||
|
||||
$context = context_course::instance($course1->id);
|
||||
$res = course_get_tagged_course_modules(core_tag_tag::get_by_name(0, 'Cat'),
|
||||
/*$exclusivemode = */false, /*$fromctx = */0, /*$ctx = */$context->id, /*$rec = */1, /*$page = */0);
|
||||
$this->assertRegExp('/'.$cm11->name.'/', $res->content);
|
||||
$this->assertRegExp('/'.$cm12->name.'/', $res->content);
|
||||
$this->assertRegExp('/'.$cm13->name.'/', $res->content);
|
||||
$this->assertNotRegExp('/'.$cm21->name.'/', $res->content);
|
||||
$this->assertRegExp('/'.$cm14->name.'/', $res->content);
|
||||
$this->assertRegExp('/'.$cm15->name.'/', $res->content);
|
||||
$this->assertNotRegExp('/'.$cm16->name.'/', $res->content);
|
||||
$this->assertNotRegExp('/'.$cm31->name.'/', $res->content); // No access to course3.
|
||||
$this->assertEmpty($res->prevpageurl);
|
||||
$this->assertNotEmpty($res->nextpageurl);
|
||||
|
||||
$res = course_get_tagged_course_modules(core_tag_tag::get_by_name(0, 'Cat'),
|
||||
/*$exclusivemode = */false, /*$fromctx = */0, /*$ctx = */$context->id, /*$rec = */1, /*$page = */1);
|
||||
$this->assertNotRegExp('/'.$cm11->name.'/', $res->content);
|
||||
$this->assertNotRegExp('/'.$cm12->name.'/', $res->content);
|
||||
$this->assertNotRegExp('/'.$cm13->name.'/', $res->content);
|
||||
$this->assertNotRegExp('/'.$cm21->name.'/', $res->content);
|
||||
$this->assertNotRegExp('/'.$cm14->name.'/', $res->content);
|
||||
$this->assertNotRegExp('/'.$cm15->name.'/', $res->content);
|
||||
$this->assertRegExp('/'.$cm16->name.'/', $res->content);
|
||||
$this->assertNotRegExp('/'.$cm31->name.'/', $res->content); // No access to course3.
|
||||
$this->assertNotEmpty($res->prevpageurl);
|
||||
$this->assertEmpty($res->nextpageurl);
|
||||
}
|
||||
}
|
||||
|
@ -57,6 +57,7 @@ $string['cachedef_navigation_expandcourse'] = 'Navigation expandable courses';
|
||||
$string['cachedef_observers'] = 'Event observers';
|
||||
$string['cachedef_plugin_functions'] = 'Plugins available callbacks';
|
||||
$string['cachedef_plugin_manager'] = 'Plugin info manager';
|
||||
$string['cachedef_tagindexbuilder'] = 'Search results for tagged items';
|
||||
$string['cachedef_questiondata'] = 'Question definitions';
|
||||
$string['cachedef_repositories'] = 'Repositories instances data';
|
||||
$string['cachedef_grade_categories'] = 'Grade category queries';
|
||||
|
@ -277,5 +277,18 @@ $definitions = array(
|
||||
'mode' => cache_store::MODE_REQUEST,
|
||||
'simplekeys' => true,
|
||||
'simpledata' => true
|
||||
)
|
||||
),
|
||||
|
||||
// Caches tag index builder results.
|
||||
'tagindexbuilder' => array(
|
||||
'mode' => cache_store::MODE_SESSION,
|
||||
'simplekeys' => true,
|
||||
'simplevalues' => true,
|
||||
'staticacceleration' => true,
|
||||
'staticaccelerationsize' => 10,
|
||||
'ttl' => 900, // 15 minutes.
|
||||
'invalidationevents' => array(
|
||||
'resettagindexbuilder',
|
||||
),
|
||||
),
|
||||
);
|
||||
|
@ -83,5 +83,7 @@ $tagareas = array(
|
||||
array(
|
||||
'itemtype' => 'course_modules', // Course modules.
|
||||
'component' => 'core',
|
||||
'callback' => 'course_get_tagged_course_modules',
|
||||
'callbackfile' => '/course/lib.php',
|
||||
),
|
||||
);
|
||||
|
373
tag/classes/index_builder.php
Normal file
373
tag/classes/index_builder.php
Normal file
@ -0,0 +1,373 @@
|
||||
<?php
|
||||
// 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/>.
|
||||
|
||||
/**
|
||||
* Class core_tag_index_builder
|
||||
*
|
||||
* @package core_tag
|
||||
* @copyright 2016 Marina Glancy
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
|
||||
defined('MOODLE_INTERNAL') || die();
|
||||
|
||||
/**
|
||||
* Helper to build tag index
|
||||
*
|
||||
* This can be used by components to implement tag area callbacks. This is especially
|
||||
* useful for in-course content when we need to check and cache user's access to
|
||||
* multiple courses. Course access and accessible items are stored in session cache
|
||||
* with 15 minutes expiry time.
|
||||
*
|
||||
* Example of usage:
|
||||
*
|
||||
* $builder = new core_tag_index_builder($component, $itemtype, $sql, $params, $from, $limit);
|
||||
* while ($item = $builder->has_item_that_needs_access_check()) {
|
||||
* if (!$builder->can_access_course($item->courseid)) {
|
||||
* $builder->set_accessible($item, false);
|
||||
* } else {
|
||||
* $accessible = true; // Check access and set $accessible respectively.
|
||||
* $builder->set_accessible($item, $accessible);
|
||||
* }
|
||||
* }
|
||||
* $items = $builder->get_items();
|
||||
*
|
||||
* @package core_tag
|
||||
* @copyright 2016 Marina Glancy
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
class core_tag_index_builder {
|
||||
|
||||
/** @var string component specified in the constructor */
|
||||
protected $component;
|
||||
|
||||
/** @var string itemtype specified in the constructor */
|
||||
protected $itemtype;
|
||||
|
||||
/** @var string SQL statement */
|
||||
protected $sql;
|
||||
|
||||
/** @var array parameters for SQL statement */
|
||||
protected $params;
|
||||
|
||||
/** @var int index from which to return records */
|
||||
protected $from;
|
||||
|
||||
/** @var int maximum number of records to return */
|
||||
protected $limit;
|
||||
|
||||
/** @var array result of SQL query */
|
||||
protected $items;
|
||||
|
||||
/** @var array list of item ids ( array_keys($this->items) ) */
|
||||
protected $itemkeys;
|
||||
|
||||
/** @var string alias of the item id in the SQL result */
|
||||
protected $idfield = 'id';
|
||||
|
||||
/** @var array cache of items accessibility (id => bool) */
|
||||
protected $accessibleitems;
|
||||
|
||||
/** @var array cache of courses accessibility (courseid => bool) */
|
||||
protected $courseaccess;
|
||||
|
||||
/** @var bool indicates that items cache was changed in this class and needs pushing to MUC */
|
||||
protected $cachechangedaccessible = false;
|
||||
|
||||
/** @var bool indicates that course accessibiity cache was changed in this class and needs pushing to MUC */
|
||||
protected $cachechangedcourse = false;
|
||||
|
||||
/** @var array cached courses (not pushed to MUC) */
|
||||
protected $courses;
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* Specify the SQL query for retrieving the tagged items, SQL query must:
|
||||
* - return the item id as the first field and make sure that it is unique in the result
|
||||
* - provide ORDER BY that exclude any possibility of random results, if $fromctx was specified when searching
|
||||
* for tagged items it is the best practice to make sure that items from this context are returned first.
|
||||
*
|
||||
* This query may also contain placeholders %COURSEFILTER% or %ITEMFILTER% that will be substituted with
|
||||
* expressions excluding courses and/or filters that are already known as inaccessible.
|
||||
*
|
||||
* Example: "WHERE c.id %COURSEFILTER% AND cm.id %ITEMFILTER%"
|
||||
*
|
||||
* This query may contain fields to preload context if context is needed for formatting values.
|
||||
*
|
||||
* It is recommended to sort by course sortorder first, this way the items from the same course will be next to
|
||||
* each other and the sequence of courses will the same in different tag areas.
|
||||
*
|
||||
* @param string $component component responsible for tagging
|
||||
* @param string $itemtype type of item that is being tagged
|
||||
* @param string $sql SQL query that would retrieve all relevant items without permission check
|
||||
* @param array $params parameters for the query (must be named)
|
||||
* @param int $from return a subset of records, starting at this point
|
||||
* @param int $limit return a subset comprising this many records in total (this field is NOT optional)
|
||||
*/
|
||||
public function __construct($component, $itemtype, $sql, $params, $from, $limit) {
|
||||
$this->component = preg_replace('/[^A-Za-z0-9_]/i', '', $component);
|
||||
$this->itemtype = preg_replace('/[^A-Za-z0-9_]/i', '', $itemtype);
|
||||
$this->sql = $sql;
|
||||
$this->params = $params;
|
||||
$this->from = $from;
|
||||
$this->limit = $limit;
|
||||
$this->courses = array();
|
||||
}
|
||||
|
||||
/**
|
||||
* Substitute %COURSEFILTER% with an expression filtering out courses where current user does not have access
|
||||
*/
|
||||
protected function prepare_sql_courses() {
|
||||
global $DB;
|
||||
if (!preg_match('/\\%COURSEFILTER\\%/', $this->sql)) {
|
||||
return;
|
||||
}
|
||||
$this->init_course_access();
|
||||
$unaccessiblecourses = array_filter($this->courseaccess, function($item) {
|
||||
return !$item;
|
||||
});
|
||||
$idx = 0;
|
||||
while (preg_match('/^([^\\0]*?)\\%COURSEFILTER\\%([^\\0]*)$/', $this->sql, $matches)) {
|
||||
list($sql, $params) = $DB->get_in_or_equal(array_keys($unaccessiblecourses),
|
||||
SQL_PARAMS_NAMED, 'ca_'.($idx++).'_', false, 0);
|
||||
$this->sql = $matches[1].' '.$sql.' '.$matches[2];
|
||||
$this->params += $params;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Substitute %ITEMFILTER% with an expression filtering out items where current user does not have access
|
||||
*/
|
||||
protected function prepare_sql_items() {
|
||||
global $DB;
|
||||
if (!preg_match('/\\%ITEMFILTER\\%/', $this->sql)) {
|
||||
return;
|
||||
}
|
||||
$this->init_items_access();
|
||||
$unaccessibleitems = array_filter($this->accessibleitems, function($item) {
|
||||
return !$item;
|
||||
});
|
||||
$idx = 0;
|
||||
while (preg_match('/^([^\\0]*?)\\%ITEMFILTER\\%([^\\0]*)$/', $this->sql, $matches)) {
|
||||
list($sql, $params) = $DB->get_in_or_equal(array_keys($unaccessibleitems),
|
||||
SQL_PARAMS_NAMED, 'ia_'.($idx++).'_', false, 0);
|
||||
$this->sql = $matches[1].' '.$sql.' '.$matches[2];
|
||||
$this->params += $params;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensures that SQL query was executed and $this->items is filled
|
||||
*/
|
||||
protected function retrieve_items() {
|
||||
global $DB;
|
||||
if ($this->items !== null) {
|
||||
return;
|
||||
}
|
||||
$this->prepare_sql_courses();
|
||||
$this->prepare_sql_items();
|
||||
$this->items = $DB->get_records_sql($this->sql, $this->params);
|
||||
$this->itemkeys = array_keys($this->items);
|
||||
if ($this->items) {
|
||||
// Find the name of the first key of the item - usually 'id' but can be something different.
|
||||
// This must be a unique identifier of the item.
|
||||
$firstitem = reset($this->items);
|
||||
$firstitemarray = (array)$firstitem;
|
||||
$this->idfield = key($firstitemarray);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the filtered records from SQL query result.
|
||||
*
|
||||
* This function can only be executed after $builder->has_item_that_needs_access_check() returns null
|
||||
*
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function get_items() {
|
||||
global $DB, $CFG;
|
||||
if (is_siteadmin()) {
|
||||
$this->sql = preg_replace('/\\%COURSEFILTER\\%/', '<>0', $this->sql);
|
||||
$this->sql = preg_replace('/\\%ITEMFILTER\\%/', '<>0', $this->sql);
|
||||
return $DB->get_records_sql($this->sql, $this->params, $this->from, $this->limit);
|
||||
}
|
||||
if ($CFG->debugdeveloper && $this->has_item_that_needs_access_check()) {
|
||||
debugging('Caller must ensure that has_item_that_needs_access_check() does not return anything '
|
||||
. 'before calling get_items(). The item list may be incomplete', DEBUG_DEVELOPER);
|
||||
}
|
||||
$this->retrieve_items();
|
||||
$this->save_caches();
|
||||
$idx = 0;
|
||||
$items = array();
|
||||
foreach ($this->itemkeys as $id) {
|
||||
if (!array_key_exists($id, $this->accessibleitems) || !$this->accessibleitems[$id]) {
|
||||
continue;
|
||||
}
|
||||
if ($idx >= $this->from) {
|
||||
$items[$id] = $this->items[$id];
|
||||
}
|
||||
$idx++;
|
||||
if ($idx >= $this->from + $this->limit) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
return $items;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the first row from the SQL result that we don't know whether it is accessible by user or not.
|
||||
*
|
||||
* This will return null when we have necessary number of accessible items to return in {@link get_items()}
|
||||
*
|
||||
* After analyzing you may decide to mark not only this record but all similar as accessible or not accessible.
|
||||
* For example, if you already call get_fast_modinfo() to check this item's accessibility, why not mark all
|
||||
* items in the same course as accessible or not accessible.
|
||||
*
|
||||
* Helpful methods: {@link set_accessible()} and {@link walk()}
|
||||
*
|
||||
* @return null|object
|
||||
*/
|
||||
public function has_item_that_needs_access_check() {
|
||||
if (is_siteadmin()) {
|
||||
return null;
|
||||
}
|
||||
$this->retrieve_items();
|
||||
$counter = 0; // Counter for accessible items.
|
||||
foreach ($this->itemkeys as $id) {
|
||||
if (!array_key_exists($id, $this->accessibleitems)) {
|
||||
return (object)(array)$this->items[$id];
|
||||
}
|
||||
$counter += $this->accessibleitems[$id] ? 1 : 0;
|
||||
if ($counter >= $this->from + $this->limit) {
|
||||
// We found enough accessible items fot get_items() method, do not look any further.
|
||||
return null;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Walk through the array of items and call $callable for each of them
|
||||
* @param callable $callable
|
||||
*/
|
||||
public function walk($callable) {
|
||||
$this->retrieve_items();
|
||||
array_walk($this->items, $callable);
|
||||
}
|
||||
|
||||
/**
|
||||
* Marks record or group of records as accessible (or not accessible)
|
||||
*
|
||||
* @param int|std_Class $identifier either record id of the item that needs to be set accessible
|
||||
* @param bool $accessible whether to mark as accessible or not accessible (default true)
|
||||
*/
|
||||
public function set_accessible($identifier, $accessible = true) {
|
||||
if (is_object($identifier)) {
|
||||
$identifier = (int)($identifier->{$this->idfield});
|
||||
}
|
||||
$this->init_items_access();
|
||||
if (is_int($identifier)) {
|
||||
$accessible = (int)(bool)$accessible;
|
||||
if (!array_key_exists($identifier, $this->accessibleitems) ||
|
||||
$this->accessibleitems[$identifier] != $accessible) {
|
||||
$this->accessibleitems[$identifier] = $accessible;
|
||||
$this->cachechangedaccessible;
|
||||
}
|
||||
} else {
|
||||
throw new coding_exception('Argument $identifier must be either int or object');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves a course record (only fields id,visible,fullname,shortname,cacherev).
|
||||
*
|
||||
* This method is useful because it also caches results and preloads course context.
|
||||
*
|
||||
* @param int $courseid
|
||||
*/
|
||||
public function get_course($courseid) {
|
||||
global $DB;
|
||||
if (!array_key_exists($courseid, $this->courses)) {
|
||||
$ctxquery = context_helper::get_preload_record_columns_sql('ctx');
|
||||
$sql = "SELECT c.id,c.visible,c.fullname,c.shortname,c.cacherev, $ctxquery
|
||||
FROM {course} c JOIN {context} ctx ON ctx.contextlevel = ? AND ctx.instanceid=c.id
|
||||
WHERE c.id = ?";
|
||||
$params = array(CONTEXT_COURSE, $courseid);
|
||||
$this->courses[$courseid] = $DB->get_record_sql($sql, $params);
|
||||
context_helper::preload_from_record($this->courses[$courseid]);
|
||||
}
|
||||
return $this->courses[$courseid];
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensures that we read the course access from the cache.
|
||||
*/
|
||||
protected function init_course_access() {
|
||||
if ($this->courseaccess === null) {
|
||||
$this->courseaccess = cache::make('core', 'tagindexbuilder')->get('courseaccess') ?: [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensures that we read the items access from the cache.
|
||||
*/
|
||||
protected function init_items_access() {
|
||||
if ($this->accessibleitems === null) {
|
||||
$this->accessibleitems = cache::make('core', 'tagindexbuilder')->get($this->component.'__'.$this->itemtype) ?: [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if current user has access to the course
|
||||
*
|
||||
* This method calls global function {@link can_access_course} and caches results
|
||||
*
|
||||
* @param int $courseid
|
||||
* @return bool
|
||||
*/
|
||||
public function can_access_course($courseid) {
|
||||
$this->init_course_access();
|
||||
if (!array_key_exists($courseid, $this->courseaccess)) {
|
||||
$this->courseaccess[$courseid] = can_access_course($this->get_course($courseid)) ? 1 : 0;
|
||||
$this->cachechangedcourse = true;
|
||||
}
|
||||
return $this->courseaccess[$courseid];
|
||||
}
|
||||
|
||||
/**
|
||||
* Saves course/items caches if needed
|
||||
*/
|
||||
protected function save_caches() {
|
||||
if ($this->cachechangedcourse) {
|
||||
cache::make('core', 'tagindexbuilder')->set('courseaccess', $this->courseaccess);
|
||||
$this->cachechangedcourse = false;
|
||||
}
|
||||
if ($this->cachechangedaccessible) {
|
||||
cache::make('core', 'tagindexbuilder')->set($this->component.'__'.$this->itemtype,
|
||||
$this->accessibleitems);
|
||||
$this->cachechangedaccessible = false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Resets all course/items session caches - useful in unittests when we change users and enrolments.
|
||||
*/
|
||||
public static function reset_caches() {
|
||||
cache_helper::purge_by_event('resettagindexbuilder');
|
||||
}
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user