mirror of
https://github.com/moodle/moodle.git
synced 2025-04-16 14:02:32 +02:00
MDL-53167 search: Add ability to limit courses searched
This commit is contained in:
parent
b611ade3ab
commit
427e3cbcd6
@ -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);
|
||||
|
||||
|
@ -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';
|
||||
|
2
lib/amd/build/form-autocomplete.min.js
vendored
2
lib/amd/build/form-autocomplete.min.js
vendored
File diff suppressed because one or more lines are too long
2
lib/amd/build/form-course-selector.min.js
vendored
2
lib/amd/build/form-course-selector.min.js
vendored
@ -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}}});
|
@ -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);
|
||||
|
@ -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([{
|
||||
|
@ -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();
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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>
|
||||
|
@ -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()
|
||||
|
@ -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();
|
||||
|
@ -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);
|
||||
|
||||
|
@ -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 {
|
||||
|
@ -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);
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -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]);
|
||||
}
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user