diff --git a/admin/settings/plugins.php b/admin/settings/plugins.php index 43909fba34690..f225b0a901fe5 100644 --- a/admin/settings/plugins.php +++ b/admin/settings/plugins.php @@ -751,6 +751,12 @@ $ADMIN->add('searchplugins', new admin_externalpage('searchareas', new lang_string('searchareas', 'admin'), new moodle_url('/admin/searchareas.php'))); + // Only add reindex page if search indexing is enabled. + if (\core_search\manager::is_indexing_enabled()) { + $ADMIN->add('searchplugins', new admin_externalpage('searchreindex', new lang_string('reindexcourse', 'search'), + new moodle_url('/search/reindex.php'))); + } + core_collator::asort_objects_by_property($pages, 'visiblename'); foreach ($pages as $page) { $ADMIN->add('searchplugins', $page); diff --git a/lang/en/search.php b/lang/en/search.php index 540b6184b05d3..ef4c6ff2544de 100644 --- a/lang/en/search.php +++ b/lang/en/search.php @@ -105,6 +105,8 @@ $string['progress'] = 'Progress'; $string['queryerror'] = 'The query you provided could not be parsed by the search engine: {$a}'; $string['queueheading'] = 'Additional indexing queue ({$a} items)'; +$string['reindexcourse'] = 'Reindex course or activity'; +$string['reindexcourse_info'] = 'If a course or activity gives incomplete search results, you can use this form to reindex it. This is not necessary as part of normal operation.'; $string['resultsreturnedfor'] = 'results returned for'; $string['runindexer'] = 'Run indexer (real)'; $string['runindexertest'] = 'Run indexer test'; diff --git a/lib/db/services.php b/lib/db/services.php index 1886f7de5976a..751a2a9a3177a 100644 --- a/lib/db/services.php +++ b/lib/db/services.php @@ -1752,6 +1752,12 @@ 'type' => 'read', 'ajax' => true ), + 'core_search_get_course_activities' => [ + 'classname' => '\core_search\external\get_course_activities', + 'description' => 'Gets activities on a particular course', + 'type' => 'read', + 'ajax' => true + ], 'core_search_get_results' => [ 'classname' => '\core_search\external\get_results', 'description' => 'Get search results.', diff --git a/search/amd/build/activityselector.min.js b/search/amd/build/activityselector.min.js new file mode 100644 index 0000000000000..61c2b960da607 --- /dev/null +++ b/search/amd/build/activityselector.min.js @@ -0,0 +1,10 @@ +define("core_search/activityselector",["exports","core/ajax"],(function(_exports,_ajax){var obj; +/** + * Activity selector for the reindex form. + * + * @module core_search/activityselector + * @copyright 2024 The Open University + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.processResults=function(selector,results){const output=[];for(let result of results)output.push({value:result.cmid,label:result.name});return output},_exports.transport=async function(selector,query,success,failure){const courseSelection=document.querySelector(selector).closest("form").querySelector("select[name=courseid] ~ .form-autocomplete-selection"),courseId=parseInt(courseSelection.dataset.activeValue);try{const response=await _ajax.default.call([{methodname:"core_search_get_course_activities",args:{courseid:courseId,query:query}}]);success(response)}catch(e){failure(e)}},_ajax=(obj=_ajax)&&obj.__esModule?obj:{default:obj}})); + +//# sourceMappingURL=activityselector.min.js.map \ No newline at end of file diff --git a/search/amd/build/activityselector.min.js.map b/search/amd/build/activityselector.min.js.map new file mode 100644 index 0000000000000..6d5e9ae478965 --- /dev/null +++ b/search/amd/build/activityselector.min.js.map @@ -0,0 +1 @@ +{"version":3,"file":"activityselector.min.js","sources":["../src/activityselector.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see .\n\n/**\n * Activity selector for the reindex form.\n *\n * @module core_search/activityselector\n * @copyright 2024 The Open University\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport Ajax from 'core/ajax';\n\n/**\n * Obtains list of activities for Ajax autocomplete element.\n *\n * @param {String} selector Selector of the element\n * @param {String} query The query string\n * @param {Function} success Callback function to be called with an array of results\n * @param {Function} failure Callback to be called in case of failure, with error message\n */\nexport async function transport(selector, query, success, failure) {\n // Get course id.\n const element = document.querySelector(selector);\n const courseSelection = element.closest('form').querySelector(\n 'select[name=courseid] ~ .form-autocomplete-selection');\n const courseId = parseInt(courseSelection.dataset.activeValue);\n\n // Do AJAX request to list activities on course matching query.\n try {\n const response = await Ajax.call([{methodname: 'core_search_get_course_activities', args: {\n courseid: courseId,\n query: query\n }}]);\n success(response);\n } catch (e) {\n failure(e);\n }\n}\n\n/**\n * Processes results for Ajax autocomplete element.\n *\n * @param {String} selector Selector of the element\n * @param {Array} results Array of results\n * @return {Object[]} Array of results with 'value' and 'label' fields\n */\nexport function processResults(selector, results) {\n const output = [];\n for (let result of results) {\n output.push({value: result.cmid, label: result.name});\n }\n return output;\n}\n"],"names":["selector","results","output","result","push","value","cmid","label","name","query","success","failure","courseSelection","document","querySelector","closest","courseId","parseInt","dataset","activeValue","response","Ajax","call","methodname","args","courseid","e"],"mappings":";;;;;;;8FA2D+BA,SAAUC,eAC/BC,OAAS,OACV,IAAIC,UAAUF,QACfC,OAAOE,KAAK,CAACC,MAAOF,OAAOG,KAAMC,MAAOJ,OAAOK,cAE5CN,0CA/BqBF,SAAUS,MAAOC,QAASC,eAGhDC,gBADUC,SAASC,cAAcd,UACPe,QAAQ,QAAQD,cAC5C,wDACEE,SAAWC,SAASL,gBAAgBM,QAAQC,uBAIxCC,eAAiBC,cAAKC,KAAK,CAAC,CAACC,WAAY,oCAAqCC,KAAM,CACtFC,SAAUT,SACVP,MAAOA,UAEXC,QAAQU,UACV,MAAOM,GACLf,QAAQe"} \ No newline at end of file diff --git a/search/amd/src/activityselector.js b/search/amd/src/activityselector.js new file mode 100644 index 0000000000000..00ee286158c6d --- /dev/null +++ b/search/amd/src/activityselector.js @@ -0,0 +1,66 @@ +// 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 . + +/** + * Activity selector for the reindex form. + * + * @module core_search/activityselector + * @copyright 2024 The Open University + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +import Ajax from 'core/ajax'; + +/** + * Obtains list of activities for Ajax autocomplete element. + * + * @param {String} selector Selector of the element + * @param {String} query The query string + * @param {Function} success Callback function to be called with an array of results + * @param {Function} failure Callback to be called in case of failure, with error message + */ +export async function transport(selector, query, success, failure) { + // Get course id. + const element = document.querySelector(selector); + const courseSelection = element.closest('form').querySelector( + 'select[name=courseid] ~ .form-autocomplete-selection'); + const courseId = parseInt(courseSelection.dataset.activeValue); + + // Do AJAX request to list activities on course matching query. + try { + const response = await Ajax.call([{methodname: 'core_search_get_course_activities', args: { + courseid: courseId, + query: query + }}]); + success(response); + } catch (e) { + failure(e); + } +} + +/** + * Processes results for Ajax autocomplete element. + * + * @param {String} selector Selector of the element + * @param {Array} results Array of results + * @return {Object[]} Array of results with 'value' and 'label' fields + */ +export function processResults(selector, results) { + const output = []; + for (let result of results) { + output.push({value: result.cmid, label: result.name}); + } + return output; +} diff --git a/search/classes/external/get_course_activities.php b/search/classes/external/get_course_activities.php new file mode 100644 index 0000000000000..d87f7e6e1191c --- /dev/null +++ b/search/classes/external/get_course_activities.php @@ -0,0 +1,104 @@ +. + +namespace core_search\external; + +use core_external\external_api; +use core_external\external_function_parameters; +use core_external\external_single_structure; +use core_external\external_multiple_structure; +use core_external\external_value; + +/** + * External function for listing activities on a course. + * + * @package core_search + * @copyright 2024 The Open University + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class get_course_activities extends external_api { + + /** + * Webservice parameters. + * + * @return external_function_parameters + */ + public static function execute_parameters(): external_function_parameters { + return new external_function_parameters( + [ + 'courseid' => new external_value(PARAM_INT, 'Course id'), + 'query' => new external_value(PARAM_NOTAGS, 'Optional query (blank = all activities)'), + ] + ); + } + + /** + * Webservice returns. + * + * @return external_multiple_structure + */ + public static function execute_returns(): external_multiple_structure { + return new external_multiple_structure(new external_single_structure([ + 'cmid' => new external_value(PARAM_INT, 'Course-module id'), + 'name' => new external_value(PARAM_INT, 'Activity name e.g. "General discussion forum"'), + 'modname' => new external_value(PARAM_ALPHANUMEXT, 'Module name e.g. "forum"'), + ])); + } + + /** + * Gets activities on a course. + * + * @param int $courseid Course id + * @param string $query Optional search query ('' = all) + * @return array List of activities + */ + public static function execute(int $courseid, string $query): array { + global $PAGE; + + ['courseid' => $courseid, 'query' => $query] = self::validate_parameters( + self::execute_parameters(), + [ + 'courseid' => $courseid, + 'query' => $query, + ] + ); + + // Get the context. This should ensure that user is allowed to access the course. + $context = \context_course::instance($courseid); + external_api::validate_context($context); + + // Get details for all activities in course. + $modinfo = get_fast_modinfo($courseid); + $results = []; + $lowerquery = \core_text::strtolower($query); + foreach ($modinfo->get_cms() as $cm) { + // When there is a query, skip activities that don't match it. + if ($lowerquery !== '') { + $lowername = \core_text::strtolower($cm->name); + if (strpos($lowername, $lowerquery) === false) { + continue; + } + } + $results[] = (object)[ + 'cmid' => $cm->id, + 'name' => $cm->name, + 'modname' => $cm->modname, + ]; + } + + return $results; + } +} diff --git a/search/classes/form/reindex.php b/search/classes/form/reindex.php new file mode 100644 index 0000000000000..21d0bd4a205a0 --- /dev/null +++ b/search/classes/form/reindex.php @@ -0,0 +1,56 @@ +. + +/** + * Form to reindex a course or activity. + * + * @package core_search + * @copyright 2024 The Open University + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace core_search\form; + +defined('MOODLE_INTERNAL') || die(); + +require_once($CFG->libdir . '/formslib.php'); + +/** + * Form to reindex a course or activity. + * + * @package core_search + */ +class reindex extends \moodleform { + /** + * Defines form contents. + */ + public function definition() { + $mform = $this->_form; + + $mform->addElement('static', 'info', '', get_string('reindexcourse_info', 'search')); + + $mform->addElement('course', 'courseid', get_string('course')); + $mform->addRule('courseid', null, 'required', null, 'client'); + +// $options = [ +// 'ajax' => 'core_search/activityselector' +// ]; +// $mform->addElement('autocomplete', 'cmid', get_string('activity'), [], $options); +// $mform->disabledIf('cmid', 'courseid', 'eq', 0); + + $mform->addElement('submit', 'reindex', get_string('reindexcourse', 'search')); + } +} diff --git a/search/reindex.php b/search/reindex.php new file mode 100644 index 0000000000000..e40de02ef607b --- /dev/null +++ b/search/reindex.php @@ -0,0 +1,46 @@ +. + +/** + * Manually request a reindex of a particular context for cases when the index is incorrect. + * + * @package core_search + * @copyright 2024 The Open University + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +require_once(__DIR__ . '/../config.php'); +require_once($CFG->libdir . '/adminlib.php'); + +admin_externalpage_setup('searchreindex'); + +$PAGE->set_primary_active_tab('siteadminnode'); + +$mform = new core_search\form\reindex(); + +if ($mform->get_data()) { + +} + +echo $OUTPUT->header(); + +// Throw an error if search indexing is off - we don't show the page link in that case. +if (!\core_search\manager::is_indexing_enabled()) { + throw new \moodle_exception('notavailable'); +} + +echo $mform->render(); + +echo $OUTPUT->footer();