MDL-53167 search: Add ability to limit courses searched

This commit is contained in:
Eric Merrill 2016-04-07 14:41:59 -04:00
parent b611ade3ab
commit 427e3cbcd6
17 changed files with 145 additions and 30 deletions

View File

@ -2139,7 +2139,8 @@ class core_course_external extends external_api {
'requiredcapabilities' => new external_multiple_structure(
new external_value(PARAM_CAPABILITY, 'Capability string used to filter courses by permission'),
VALUE_OPTIONAL
)
),
'limittoenrolled' => new external_value(PARAM_BOOL, 'limit to enrolled courses', VALUE_DEFAULT, 0),
)
);
}
@ -2152,6 +2153,7 @@ class core_course_external extends external_api {
* @param int $page Page number (for pagination)
* @param int $perpage Items per page
* @param array $requiredcapabilities Optional list of required capabilities (used to filter the list).
* @param int $limittoenrolled Limit to only enrolled courses
* @return array of course objects and warnings
* @since Moodle 3.0
* @throws moodle_exception
@ -2160,7 +2162,8 @@ class core_course_external extends external_api {
$criteriavalue,
$page=0,
$perpage=0,
$requiredcapabilities=array()) {
$requiredcapabilities=array(),
$limittoenrolled=0) {
global $CFG;
require_once($CFG->libdir . '/coursecatlib.php');
@ -2207,10 +2210,22 @@ class core_course_external extends external_api {
$courses = coursecat::search_courses($searchcriteria, $options, $params['requiredcapabilities']);
$totalcount = coursecat::search_courses_count($searchcriteria, $options, $params['requiredcapabilities']);
if (!empty($limittoenrolled)) {
// Get the courses where the current user has access.
$enrolled = enrol_get_my_courses(array('id', 'cacherev'));
}
$finalcourses = array();
$categoriescache = array();
foreach ($courses as $course) {
if (!empty($limittoenrolled)) {
// Filter out not enrolled courses.
if (empty($enrolled[$course->id])) {
$totalcount--;
continue;
}
}
$coursecontext = context_course::instance($course->id);

View File

@ -25,6 +25,7 @@
$string['advancedsearch'] = 'Advanced search';
$string['all'] = 'All';
$string['allareas'] = 'All areas';
$string['allcourses'] = 'All courses';
$string['author'] = 'Author';
$string['authorname'] = 'Author name';
$string['back'] = 'Back';

File diff suppressed because one or more lines are too long

View File

@ -1 +1 @@
define(["core/ajax","jquery"],function(a,b){return{processResults:function(a,c){var d=[],e=0,f=String(b(a).data("exclude")).split(",");for(e=0;e<c.courses.length;e++)-1===f.indexOf(String(c.courses[e].id))&&d.push({value:c.courses[e].id,label:c.courses[e].displayname});return d},transport:function(c,d,e,f){var g=b(c).data("requiredcapabilities");g=""!==g.trim()?g.split(","):[];var h=null;"undefined"==typeof d&&(d="");var i={criterianame:"search",criteriavalue:d,page:0,perpage:100,requiredcapabilities:g};return h=a.call([{methodname:"core_course_search_courses",args:i}]),h[0].done(e),h[0].fail(f),h}}});
define(["core/ajax","jquery"],function(a,b){return{processResults:function(a,c){var d=[],e=0,f=String(b(a).data("exclude")).split(",");for(e=0;e<c.courses.length;e++)-1===f.indexOf(String(c.courses[e].id))&&d.push({value:c.courses[e].id,label:c.courses[e].displayname});return d},transport:function(c,d,e,f){var g=b(c).data("requiredcapabilities");g=""!==g.trim()?g.split(","):[];var h=b(c).data("limittoenrolled"),i=null;"undefined"==typeof d&&(d="");var j={criterianame:"search",criteriavalue:d,page:0,perpage:100,requiredcapabilities:g,limittoenrolled:h};return i=a.call([{methodname:"core_course_search_courses",args:j}]),i[0].done(e),i[0].fail(f),i}}});

View File

@ -686,8 +686,9 @@ define(['jquery', 'core/log', 'core/str', 'core/templates', 'core/notification']
* These are modeled on Select2 see: https://select2.github.io/options.html#ajax
* @param {String} placeholder - The text to display before a selection is made.
* @param {Boolean} caseSensitive - If search has to be made case sensitive.
* @param {String} noSelectionString - Text to display when there is no selection
*/
enhance: function(selector, tags, ajax, placeholder, caseSensitive, showSuggestions) {
enhance: function(selector, tags, ajax, placeholder, caseSensitive, showSuggestions, noSelectionString) {
// Set some default values.
var options = {
selector: selector,
@ -695,7 +696,8 @@ define(['jquery', 'core/log', 'core/str', 'core/templates', 'core/notification']
ajax: false,
placeholder: placeholder,
caseSensitive: false,
showSuggestions: true
showSuggestions: true,
noSelectionString: noSelectionString
};
if (typeof tags !== "undefined") {
options.tags = tags;
@ -709,6 +711,9 @@ define(['jquery', 'core/log', 'core/str', 'core/templates', 'core/notification']
if (typeof showSuggestions !== "undefined") {
options.showSuggestions = showSuggestions;
}
if (typeof noSelectionString === "undefined") {
options.noSelectionString = str.get_string('noselection', 'form');
}
// Look for the select element.
var originalSelect = $(selector);

View File

@ -48,6 +48,9 @@ define(['core/ajax', 'jquery'], function(ajax, $) {
} else {
requiredcapabilities = [];
}
var limittoenrolled = $(selector).data('limittoenrolled');
// Build the query.
var promise = null;
@ -60,7 +63,8 @@ define(['core/ajax', 'jquery'], function(ajax, $) {
criteriavalue: query,
page: 0,
perpage: 100,
requiredcapabilities: requiredcapabilities
requiredcapabilities: requiredcapabilities,
limittoenrolled: limittoenrolled
};
// Go go go!
promise = ajax.call([{

View File

@ -50,6 +50,8 @@ class MoodleQuickForm_autocomplete extends MoodleQuickForm_select {
protected $casesensitive = false;
/** @var bool $showsuggestions Show suggestions by default - but this can be turned off. */
protected $showsuggestions = true;
/** @var string $noselectionstring String that is shown when there are no selections. */
protected $noselectionstring = '';
/**
* constructor
@ -79,6 +81,12 @@ class MoodleQuickForm_autocomplete extends MoodleQuickForm_select {
$this->placeholder = $attributes['placeholder'];
unset($attributes['placeholder']);
}
$this->noselectionstring = get_string('noselection', 'form');
if (isset($attributes['noselectionstring'])) {
$this->noselectionstring = $attributes['noselectionstring'];
unset($attributes['noselectionstring']);
}
if (isset($attributes['ajax'])) {
$this->ajax = $attributes['ajax'];
unset($attributes['ajax']);
@ -114,7 +122,7 @@ class MoodleQuickForm_autocomplete extends MoodleQuickForm_select {
$this->_generateId();
$id = $this->getAttribute('id');
$PAGE->requires->js_call_amd('core/form-autocomplete', 'enhance', $params = array('#' . $id, $this->tags, $this->ajax,
$this->placeholder, $this->casesensitive, $this->showsuggestions));
$this->placeholder, $this->casesensitive, $this->showsuggestions, $this->noselectionstring));
return parent::toHTML();
}

View File

@ -54,6 +54,11 @@ class MoodleQuickForm_course extends MoodleQuickForm_autocomplete {
*/
protected $requiredcapabilities = array();
/**
* @var bool $limittoenrolled Only allow enrolled courses.
*/
protected $limittoenrolled = false;
/**
* Constructor
*
@ -78,15 +83,25 @@ class MoodleQuickForm_course extends MoodleQuickForm_autocomplete {
if (isset($options['requiredcapabilities'])) {
$this->requiredcapabilities = $options['requiredcapabilities'];
}
if (isset($options['limittoenrolled'])) {
$this->limittoenrolled = $options['limittoenrolled'];
}
$validattributes = array(
'ajax' => 'core/form-course-selector',
'data-requiredcapabilities' => implode(',', $this->requiredcapabilities),
'data-exclude' => implode(',', $this->exclude)
'data-exclude' => implode(',', $this->exclude),
'data-limittoenrolled' => (int)$this->limittoenrolled
);
if ($this->multiple) {
$validattributes['multiple'] = 'multiple';
}
if (isset($options['noselectionstring'])) {
$validattributes['noselectionstring'] = $options['noselectionstring'];
}
if (isset($options['placeholder'])) {
$validattributes['placeholder'] = $options['placeholder'];
}
parent::__construct($elementname, $elementlabel, array(), $validattributes);
}

View File

@ -29,12 +29,13 @@
* multiple True if this field allows multiple selections
* selectionId The dom id of the current selection list.
* items List of items with label and value fields.
* noSelectionString String to use when no items are selected
Example context (json):
{ "multiple": true, "selectionId": 1, "items": [
{ "label": "Item label with <strong>tags</strong>", "value": "5" },
{ "label": "Another item label with <strong>tags</strong>", "value": "4" }
]}
], "noSelectionString": "No selection" }
}}
<div class="form-autocomplete-selection {{#multiple}}form-autocomplete-multiple{{/multiple}}" id="{{selectionId}}" role="list" aria-atomic="true" {{#multiple}}tabindex="0" aria-multiselectable="true"{{/multiple}}>
<span class="accesshide">{{#str}}selecteditems, form{{/str}}</span>
@ -44,7 +45,7 @@
</span>
{{/items}}
{{^items}}
<span>{{#str}}noselection,form{{/str}}</span>
<span>{{noSelectionString}}</span>
{{/items}}
</div>
</div>

View File

@ -3,6 +3,8 @@ information provided here is intended especially for developers.
=== 3.1 ===
* Webservice function core_course_search_courses accepts a new parameter 'limittoenrolled' to filter the results
only to courses the user is enrolled in, and are visible to them.
* The moodle/blog:associatecourse and moodle/blog:associatemodule capabilities has been removed.
* The following functions has been finally deprecated and can not be used any more:
- profile_display_badges()

View File

@ -311,9 +311,10 @@ class manager {
* information and there will be a performance benefit on passing only some contexts
* instead of the whole context array set.
*
* @param array|false $limitcourseids An array of course ids to limit the search to. False for no limiting.
* @return bool|array Indexed by area identifier (component + area name). Returns true if the user can see everything.
*/
protected function get_areas_user_accesses() {
protected function get_areas_user_accesses($limitcourseids = false) {
global $CFG, $USER;
// All results for admins. Eventually we could add a new capability for managers.
@ -336,7 +337,7 @@ class manager {
// This will store area - allowed contexts relations.
$areascontexts = array();
if (!empty($areasbylevel[CONTEXT_SYSTEM])) {
if (empty($limitcourseids) && !empty($areasbylevel[CONTEXT_SYSTEM])) {
// We add system context to all search areas working at this level. Here each area is fully responsible of
// the access control as we can not automate much, we can not even check guest access as some areas might
// want to allow guests to retrieve data from them.
@ -349,9 +350,16 @@ class manager {
// Get the courses where the current user has access.
$courses = enrol_get_my_courses(array('id', 'cacherev'));
$courses[SITEID] = get_course(SITEID);
$site = \course_modinfo::instance(SITEID);
if (empty($limitcourseids) || in_array(SITEID, $limitcourseids)) {
$courses[SITEID] = get_course(SITEID);
}
foreach ($courses as $course) {
if (!empty($limitcourseids) && !in_array($course->id, $limitcourseids)) {
// Skip non-included courses.
continue;
}
// Info about the course modules.
$modinfo = get_fast_modinfo($course);
@ -402,10 +410,15 @@ class manager {
public function search(\stdClass $formdata) {
global $USER;
$limitcourseids = false;
if (!empty($formdata->courseids)) {
$limitcourseids = $formdata->courseids;
}
// Clears previous query errors.
$this->engine->clear_query_error();
$areascontexts = $this->get_areas_user_accesses();
$areascontexts = $this->get_areas_user_accesses($limitcourseids);
if (!$areascontexts) {
// User can not access any context.
$docs = array();

View File

@ -62,6 +62,14 @@ class search extends \moodleform {
}
$mform->addElement('select', 'areaid', get_string('searcharea', 'search'), $areanames);
$options = array(
'multiple' => true,
'limittoenrolled' => !is_siteadmin(),
'noselectionstring' => get_string('allcourses', 'search'),
);
$mform->addElement('course', 'courseids', get_string('courses', 'core'), $options);
$mform->setType('courseids', PARAM_INT);
$mform->addElement('date_time_selector', 'timestart', get_string('fromtime', 'search'), array('optional' => true));
$mform->setDefault('timestart', 0);

View File

@ -135,6 +135,9 @@ class engine extends \core_search\engine {
// Even if it is only supposed to contain PARAM_ALPHANUMEXT, better to prevent.
$query->addFilterQuery('{!field cache=false f=areaid}' . $data->areaid);
}
if (!empty($data->courseids)) {
$query->addFilterQuery('{!cache=false}courseid:(' . implode(' OR ', $data->courseids) . ')');
}
if (!empty($data->timestart) or !empty($data->timeend)) {
if (empty($data->timestart)) {
@ -159,19 +162,23 @@ class engine extends \core_search\engine {
// If the user can access all contexts $usercontexts value is just true, we don't need to filter
// in that case.
if ($usercontexts && is_array($usercontexts)) {
if (!empty($data->areaid)) {
$query->addFilterQuery('contextid:(' . implode(' OR ', $usercontexts[$data->areaid]) . ')');
} else {
// Join all area contexts into a single array and implode.
$allcontexts = array();
foreach ($usercontexts as $areacontexts) {
foreach ($areacontexts as $contextid) {
// Ensure they are unique.
$allcontexts[$contextid] = $contextid;
}
// Join all area contexts into a single array and implode.
$allcontexts = array();
foreach ($usercontexts as $areaid => $areacontexts) {
if (!empty($data->areaid) && ($areaid !== $data->areaid)) {
// Skip unused areas.
continue;
}
foreach ($areacontexts as $contextid) {
// Ensure they are unique.
$allcontexts[$contextid] = $contextid;
}
$query->addFilterQuery('contextid:(' . implode(' OR ', $allcontexts) . ')');
}
if (empty($allcontexts)) {
// This means there are no valid contexts for them, so they get no results.
return array();
}
$query->addFilterQuery('contextid:(' . implode(' OR ', $allcontexts) . ')');
}
try {

View File

@ -191,6 +191,14 @@ class search_solr_engine_testcase extends advanced_testcase {
$querydata->title = 'moodle/course:renameroles roleid 1';
$this->assertCount(1, $this->search->search($querydata));
// Course IDs.
unset($querydata->title);
$querydata->courseids = array(SITEID + 1);
$this->assertCount(0, $this->search->search($querydata));
$querydata->courseids = array(SITEID);
$this->assertCount(3, $this->search->search($querydata));
// Check that index contents get updated.
$DB->delete_records('role_capabilities', array('capability' => 'moodle/course:renameroles'));
$this->search->index(true);

View File

@ -28,7 +28,7 @@ $page = optional_param('page', 0, PARAM_INT);
$q = optional_param('q', '', PARAM_NOTAGS);
$title = optional_param('title', '', PARAM_NOTAGS);
$areaid = optional_param('areaid', false, PARAM_ALPHANUMEXT);
// Moving timestart and timeend further down as they might come as an array if they come from the form.
// Moving courseids, timestart, and timeend further down as they might come as an array if they come from the form.
$context = context_system::instance();
$pagetitle = get_string('globalsearch', 'search');
@ -67,6 +67,11 @@ if (!$data && $q) {
$data->q = $q;
$data->title = $title;
$data->areaid = $areaid;
$courseids = optional_param('courseids', '', PARAM_RAW);
if (!empty($courseids)) {
$courseids = explode(',', $courseids);
$data->courseids = clean_param_array($courseids, PARAM_INT);
}
$data->timestart = optional_param('timestart', 0, PARAM_INT);
$data->timeend = optional_param('timeend', 0, PARAM_INT);
$mform->set_data($data);
@ -78,6 +83,9 @@ if ($data) {
$urlparams['q'] = $data->q;
$urlparams['title'] = $data->title;
$urlparams['areaid'] = $data->areaid;
if (!empty($data->courseids)) {
$urlparams['courseids'] = implode(',', $data->courseids);
}
$urlparams['timestart'] = $data->timestart;
$urlparams['timeend'] = $data->timeend;
}

View File

@ -67,8 +67,8 @@ class testable_core_search extends \core_search\manager {
*
* @return array
*/
public function get_areas_user_accesses() {
return parent::get_areas_user_accesses();
public function get_areas_user_accesses($limitcourseids = false) {
return parent::get_areas_user_accesses($limitcourseids);
}
/**

View File

@ -200,5 +200,25 @@ class search_manager_testcase extends advanced_testcase {
$contexts = $search->get_areas_user_accesses();
$this->assertEquals(array($frontpageforumcontext->id => $frontpageforumcontext->id, $context1->id => $context1->id),
$contexts[$this->forumpostareaid]);
// Now test course limited searches.
set_coursemodule_visible($forum2->cmid, 1);
$this->getDataGenerator()->enrol_user($student->id, $course2->id, 'student');
$contexts = $search->get_areas_user_accesses();
$allcontexts = array($frontpageforumcontext->id => $frontpageforumcontext->id, $context1->id => $context1->id,
$context2->id => $context2->id, $context3->id => $context3->id);
$this->assertEquals($allcontexts, $contexts[$this->forumpostareaid]);
$contexts = $search->get_areas_user_accesses(array($course1->id, $course2->id));
$allcontexts = array($context1->id => $context1->id, $context2->id => $context2->id, $context3->id => $context3->id);
$this->assertEquals($allcontexts, $contexts[$this->forumpostareaid]);
$contexts = $search->get_areas_user_accesses(array($course2->id));
$allcontexts = array($context3->id => $context3->id);
$this->assertEquals($allcontexts, $contexts[$this->forumpostareaid]);
$contexts = $search->get_areas_user_accesses(array($course1->id));
$allcontexts = array($context1->id => $context1->id, $context2->id => $context2->id);
$this->assertEquals($allcontexts, $contexts[$this->forumpostareaid]);
}
}