Skip to content

Commit

Permalink
MDL-53167 search: Add ability to limit courses searched
Browse files Browse the repository at this point in the history
  • Loading branch information
ericmerrill committed Apr 7, 2016
1 parent b611ade commit 427e3cb
Show file tree
Hide file tree
Showing 17 changed files with 145 additions and 30 deletions.
19 changes: 17 additions & 2 deletions course/externallib.php
Original file line number Diff line number Diff line change
Expand Up @@ -2139,7 +2139,8 @@ public static function search_courses_parameters() {
'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),
)
);
}
Expand All @@ -2152,6 +2153,7 @@ public static function search_courses_parameters() {
* @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
Expand All @@ -2160,7 +2162,8 @@ public static function search_courses($criterianame,
$criteriavalue,
$page=0,
$perpage=0,
$requiredcapabilities=array()) {
$requiredcapabilities=array(),
$limittoenrolled=0) {
global $CFG;
require_once($CFG->libdir . '/coursecatlib.php');

Expand Down Expand Up @@ -2207,10 +2210,22 @@ public static function search_courses($criterianame,
$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);

Expand Down
1 change: 1 addition & 0 deletions lang/en/search.php
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down
2 changes: 1 addition & 1 deletion lib/amd/build/form-autocomplete.min.js

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion lib/amd/build/form-course-selector.min.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

9 changes: 7 additions & 2 deletions lib/amd/src/form-autocomplete.js
Original file line number Diff line number Diff line change
Expand Up @@ -686,16 +686,18 @@ 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,
tags: false,
ajax: false,
placeholder: placeholder,
caseSensitive: false,
showSuggestions: true
showSuggestions: true,
noSelectionString: noSelectionString
};
if (typeof tags !== "undefined") {
options.tags = tags;
Expand All @@ -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);
Expand Down
6 changes: 5 additions & 1 deletion lib/amd/src/form-course-selector.js
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,9 @@ define(['core/ajax', 'jquery'], function(ajax, $) {
} else {
requiredcapabilities = [];
}

var limittoenrolled = $(selector).data('limittoenrolled');

// Build the query.
var promise = null;

Expand All @@ -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([{
Expand Down
10 changes: 9 additions & 1 deletion lib/form/autocomplete.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -79,6 +81,12 @@ public function __construct($elementName=null, $elementLabel=null, $options=null
$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']);
Expand Down Expand Up @@ -114,7 +122,7 @@ function toHtml(){
$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();
}
Expand Down
17 changes: 16 additions & 1 deletion lib/form/course.php
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,11 @@ class MoodleQuickForm_course extends MoodleQuickForm_autocomplete {
*/
protected $requiredcapabilities = array();

/**
* @var bool $limittoenrolled Only allow enrolled courses.
*/
protected $limittoenrolled = false;

/**
* Constructor
*
Expand All @@ -78,15 +83,25 @@ public function __construct($elementname = null, $elementlabel = null, $options
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);
}
Expand Down
5 changes: 3 additions & 2 deletions lib/templates/form_autocomplete_selection.mustache
Original file line number Diff line number Diff line change
Expand Up @@ -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>
Expand All @@ -44,7 +45,7 @@
</span>
{{/items}}
{{^items}}
<span>{{#str}}noselection,form{{/str}}</span>
<span>{{noSelectionString}}</span>
{{/items}}
</div>
</div>
2 changes: 2 additions & 0 deletions lib/upgrade.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
23 changes: 18 additions & 5 deletions search/classes/manager.php
Original file line number Diff line number Diff line change
Expand Up @@ -311,9 +311,10 @@ public static function extract_areaid_parts($areaid) {
* 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.
Expand All @@ -336,7 +337,7 @@ protected function get_areas_user_accesses() {
// 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.
Expand All @@ -349,9 +350,16 @@ protected function get_areas_user_accesses() {

// 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);
Expand Down Expand Up @@ -402,10 +410,15 @@ protected function get_areas_user_accesses() {
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();
Expand Down
8 changes: 8 additions & 0 deletions search/classes/output/form/search.php
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,14 @@ function definition() {
}
$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);

Expand Down
29 changes: 18 additions & 11 deletions search/engine/solr/classes/engine.php
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,9 @@ public function execute_query($filters, $usercontexts) {
// 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)) {
Expand All @@ -159,19 +162,23 @@ public function execute_query($filters, $usercontexts) {
// 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 {
Expand Down
8 changes: 8 additions & 0 deletions search/engine/solr/tests/engine_test.php
Original file line number Diff line number Diff line change
Expand Up @@ -191,6 +191,14 @@ public function test_search() {
$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);
Expand Down
10 changes: 9 additions & 1 deletion search/index.php
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@
$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');
Expand Down Expand Up @@ -67,6 +67,11 @@
$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);
Expand All @@ -78,6 +83,9 @@
$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;
}
Expand Down
4 changes: 2 additions & 2 deletions search/tests/fixtures/testable_core_search.php
Original file line number Diff line number Diff line change
Expand Up @@ -67,8 +67,8 @@ public static function instance($searchengine = false) {
*
* @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);
}

/**
Expand Down
Loading

0 comments on commit 427e3cb

Please sign in to comment.