MDL-53515 search: Extend search API to allow file indexing

This commit is contained in:
Eric Merrill 2016-03-24 15:44:27 -04:00
parent fb75b07c9e
commit 091973dbd7
26 changed files with 903 additions and 45 deletions

View File

@ -64,6 +64,8 @@ $string['incourse'] = 'in course {$a}';
$string['index'] = 'Index';
$string['invalidindexerror'] = 'Index directory either contains an invalid index, or nothing at all.';
$string['ittook'] = 'It took';
$string['matchingfile'] = 'Matched from file <span class="filename">{$a}</span>';
$string['matchingfiles'] = 'Matched from files:';
$string['next'] = 'Next';
$string['noindexmessage'] = 'Admin: There appears to be no search index. Please';
$string['noresults'] = 'No results';

View File

@ -26,6 +26,8 @@ namespace mod_assign\search;
defined('MOODLE_INTERNAL') || die();
require_once($CFG->dirroot . '/mod/assign/locallib.php');
/**
* Search area for mod_assign activities.
*
@ -34,4 +36,32 @@ defined('MOODLE_INTERNAL') || die();
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class activity extends \core_search\area\base_activity {
/**
* Returns true if this area uses file indexing.
*
* @return bool
*/
public function uses_file_indexing() {
return true;
}
/**
* Add the attached description files.
*
* @param document $document The current document
* @return null
*/
public function attach_files($document) {
$fs = get_file_storage();
$cm = $this->get_cm($this->get_module_name(), $document->get('itemid'), $document->get('courseid'));
$context = \context_module::instance($cm->id);
$files = $fs->get_area_files($context->id, 'mod_assign', ASSIGN_INTROATTACHMENT_FILEAREA, 0,
'sortorder DESC, id ASC', false);
foreach ($files as $file) {
$document->add_stored_file($file);
}
}
}

View File

@ -0,0 +1,131 @@
<?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/>.
/**
* Assign search unit tests.
*
* @package mod_assign
* @category test
* @copyright 2016 Eric Merrill {@link http://www.merrilldigital.com}
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
defined('MOODLE_INTERNAL') || die();
global $CFG;
require_once($CFG->dirroot . '/search/tests/fixtures/testable_core_search.php');
require_once($CFG->dirroot . '/mod/assign/locallib.php');
/**
* Provides the unit tests for forum search.
*
* @package mod_assign
* @category test
* @copyright 2016 Eric Merrill {@link http://www.merrilldigital.com}
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class mod_assign_search_testcase extends advanced_testcase {
/**
* @var string Area id
*/
protected $assignareaid = null;
public function setUp() {
$this->resetAfterTest(true);
set_config('enableglobalsearch', true);
$this->assignareaid = \core_search\manager::generate_areaid('mod_assign', 'activity');
// Set \core_search::instance to the mock_search_engine as we don't require the search engine to be working to test this.
$search = testable_core_search::instance();
}
/**
* Test for assign file attachments.
*
* @return void
*/
public function test_attach_files() {
global $USER;
$this->setAdminUser();
// Setup test data.
$course = $this->getDataGenerator()->create_course();
$fs = get_file_storage();
$usercontext = context_user::instance($USER->id);
$record = new stdClass();
$record->course = $course->id;
$assign = $this->getDataGenerator()->create_module('assign', $record);
$context = context_module::instance($assign->cmid);
// Attach the main file. We put them in the draft area, create_module will move them.
$filerecord = array(
'contextid' => $context->id,
'component' => 'mod_assign',
'filearea' => ASSIGN_INTROATTACHMENT_FILEAREA,
'itemid' => 0,
'filepath' => '/'
);
// Attach 4 files.
for ($i = 1; $i <= 4; $i++) {
$filerecord['filename'] = 'myfile'.$i;
$fs->create_file_from_string($filerecord, 'Test assign file '.$i);
}
// And a fifth in a sub-folder.
$filerecord['filename'] = 'myfile5';
$filerecord['filepath'] = '/subfolder/';
$fs->create_file_from_string($filerecord, 'Test assign file 5');
// Returns the instance as long as the area is supported.
$searcharea = \core_search\manager::get_search_area($this->assignareaid);
$this->assertInstanceOf('\mod_assign\search\activity', $searcharea);
$recordset = $searcharea->get_recordset_by_timestamp(0);
$nrecords = 0;
foreach ($recordset as $record) {
$doc = $searcharea->get_document($record);
$searcharea->attach_files($doc);
$files = $doc->get_files();
// Assign should return all files attached.
$this->assertCount(5, $files);
// We don't know the order, so get all the names, then sort, then check.
$filenames = array();
foreach ($files as $file) {
$filenames[] = $file->get_filename();
}
sort($filenames);
for ($i = 1; $i <= 5; $i++) {
$this->assertEquals('myfile'.$i, $filenames[($i - 1)]);
}
$nrecords++;
}
// If there would be an error/failure in the foreach above the recordset would be closed on shutdown.
$recordset->close();
$this->assertEquals(1, $nrecords);
}
}

View File

@ -34,4 +34,31 @@ defined('MOODLE_INTERNAL') || die();
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class activity extends \core_search\area\base_activity {
/**
* Returns true if this area uses file indexing.
*
* @return bool
*/
public function uses_file_indexing() {
return true;
}
/**
* Add all the folder files to the index.
*
* @param document $document The current document
* @return null
*/
public function attach_files($document) {
$fs = get_file_storage();
$cm = $this->get_cm($this->get_module_name(), $document->get('itemid'), $document->get('courseid'));
$context = \context_module::instance($cm->id);
$files = $fs->get_area_files($context->id, 'mod_folder', 'content', 0, 'sortorder DESC, id ASC', false);
foreach ($files as $file) {
$document->add_stored_file($file);
}
}
}

View File

@ -0,0 +1,130 @@
<?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/>.
/**
* Folder search unit tests.
*
* @package mod_folder
* @category test
* @copyright 2016 Eric Merrill {@link http://www.merrilldigital.com}
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
defined('MOODLE_INTERNAL') || die();
global $CFG;
require_once($CFG->dirroot . '/search/tests/fixtures/testable_core_search.php');
/**
* Provides the unit tests for forum search.
*
* @package mod_folder
* @category test
* @copyright 2016 Eric Merrill {@link http://www.merrilldigital.com}
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class mod_folder_search_testcase extends advanced_testcase {
/**
* @var string Area id
*/
protected $folderareaid = null;
public function setUp() {
$this->resetAfterTest(true);
set_config('enableglobalsearch', true);
$this->folderareaid = \core_search\manager::generate_areaid('mod_folder', 'activity');
// Set \core_search::instance to the mock_search_engine as we don't require the search engine to be working to test this.
$search = testable_core_search::instance();
}
/**
* Test for folder file attachments.
*
* @return void
*/
public function test_attach_files() {
global $USER;
$this->setAdminUser();
// Setup test data.
$course = $this->getDataGenerator()->create_course();
$fs = get_file_storage();
$usercontext = context_user::instance($USER->id);
$record = new stdClass();
$record->course = $course->id;
$record->files = file_get_unused_draft_itemid();
// Attach the main file. We put them in the draft area, create_module will move them.
$filerecord = array(
'contextid' => $usercontext->id,
'component' => 'user',
'filearea' => 'draft',
'itemid' => $record->files,
'filepath' => '/'
);
// Attach 4 files.
for ($i = 1; $i <= 4; $i++) {
$filerecord['filename'] = 'myfile'.$i;
$fs->create_file_from_string($filerecord, 'Test folder file '.$i);
}
// And a fifth in a sub-folder.
$filerecord['filename'] = 'myfile5';
$filerecord['filepath'] = '/subfolder/';
$fs->create_file_from_string($filerecord, 'Test folder file 5');
$this->getDataGenerator()->create_module('folder', $record);
// Returns the instance as long as the area is supported.
$searcharea = \core_search\manager::get_search_area($this->folderareaid);
$this->assertInstanceOf('\mod_folder\search\activity', $searcharea);
$recordset = $searcharea->get_recordset_by_timestamp(0);
$nrecords = 0;
foreach ($recordset as $record) {
$doc = $searcharea->get_document($record);
$searcharea->attach_files($doc);
$files = $doc->get_files();
// Folder should return all files attached.
$this->assertCount(5, $files);
// We don't know the order, so get all the names, then sort, then check.
$filenames = array();
foreach ($files as $file) {
$filenames[] = $file->get_filename();
}
sort($filenames);
for ($i = 1; $i <= 5; $i++) {
$this->assertEquals('myfile'.$i, $filenames[($i - 1)]);
}
$nrecords++;
}
// If there would be an error/failure in the foreach above the recordset would be closed on shutdown.
$recordset->close();
$this->assertEquals(1, $nrecords);
}
}

View File

@ -65,7 +65,7 @@ class post extends \core_search\area\base_mod {
FROM {forum_posts} fp
JOIN {forum_discussions} fd ON fd.id = fp.discussion
JOIN {forum} f ON f.id = fd.forum
WHERE fp.modified >= ? ORDER BY fp.modified ASC';
WHERE fp.modified >= ? ORDER BY fp.modified ASC';
return $DB->get_recordset_sql($sql, array($modifiedfrom));
}
@ -73,9 +73,10 @@ class post extends \core_search\area\base_mod {
* Returns the document associated with this post id.
*
* @param stdClass $record Post info.
* @param array $options
* @return \core_search\document
*/
public function get_document($record) {
public function get_document($record, $options = array()) {
try {
$cm = $this->get_cm('forum', $record->forumid, $record->courseid);
@ -96,15 +97,62 @@ class post extends \core_search\area\base_mod {
$doc->set('title', $record->subject);
$doc->set('content', content_to_text($record->message, $record->messageformat));
$doc->set('contextid', $context->id);
$doc->set('type', \core_search\manager::TYPE_TEXT);
$doc->set('courseid', $record->courseid);
$doc->set('userid', $record->userid);
$doc->set('owneruserid', \core_search\manager::NO_OWNER_ID);
$doc->set('modified', $record->modified);
// Check if this document should be considered new.
if (isset($options['lastindexedtime']) && ($options['lastindexedtime'] < $record->created)) {
// If the document was created after the last index time, it must be new.
$doc->set_is_new(true);
}
return $doc;
}
/**
* Returns true if this area uses file indexing.
*
* @return bool
*/
public function uses_file_indexing() {
return true;
}
/**
* Add the forum post attachments.
*
* @param document $document The current document
* @return null
*/
public function attach_files($document) {
global $DB;
$postid = $document->get('itemid');
try {
$post = $this->get_post($postid);
} catch (\dml_missing_record_exception $e) {
unset($this->postsdata[$postid]);
debugging('Could not get record to attach files to '.$document->get('id'), DEBUG_DEVELOPER);
return;
}
// Because this is used during indexing, we don't want to cache posts. Would result in memory leak.
unset($this->postsdata[$postid]);
$cm = $this->get_cm('forum', $post->forum, $document->get('courseid'));
$context = \context_module::instance($cm->id);
// Get the files and attach them.
$fs = get_file_storage();
$files = $fs->get_area_files($context->id, 'mod_forum', 'attachment', $postid, "filename", false);
foreach ($files as $file) {
$document->add_stored_file($file);
}
}
/**
* Whether the user can access the document or not.
*

View File

@ -267,4 +267,107 @@ class mod_forum_search_testcase extends advanced_testcase {
$this->assertEquals(\core_search\manager::ACCESS_GRANTED, $searcharea->check_access($discussion1reply1->id));
$this->assertEquals(\core_search\manager::ACCESS_DENIED, $searcharea->check_access($discussion2reply1->id));
}
/**
* Test for post attachments.
*
* @return void
*/
public function test_attach_files() {
global $DB;
$fs = get_file_storage();
// Returns the instance as long as the area is supported.
$searcharea = \core_search\manager::get_search_area($this->forumpostareaid);
$this->assertInstanceOf('\mod_forum\search\post', $searcharea);
$user1 = self::getDataGenerator()->create_user();
$user2 = self::getDataGenerator()->create_user();
$course1 = self::getDataGenerator()->create_course();
$this->getDataGenerator()->enrol_user($user1->id, $course1->id, 'student');
$this->getDataGenerator()->enrol_user($user2->id, $course1->id, 'student');
$record = new stdClass();
$record->course = $course1->id;
$forum1 = self::getDataGenerator()->create_module('forum', $record);
// Create discussion1.
$record = new stdClass();
$record->course = $course1->id;
$record->userid = $user1->id;
$record->forum = $forum1->id;
$record->message = 'discussion';
$record->attachemt = 1;
$discussion1 = self::getDataGenerator()->get_plugin_generator('mod_forum')->create_discussion($record);
// Attach 2 file to the discussion post.
$post = $DB->get_record('forum_posts', array('discussion' => $discussion1->id));
$filerecord = array(
'contextid' => context_module::instance($forum1->cmid)->id,
'component' => 'mod_forum',
'filearea' => 'attachment',
'itemid' => $post->id,
'filepath' => '/',
'filename' => 'myfile1'
);
$file1 = $fs->create_file_from_string($filerecord, 'Some contents 1');
$filerecord['filename'] = 'myfile2';
$file2 = $fs->create_file_from_string($filerecord, 'Some contents 2');
// Create post1 in discussion1.
$record = new stdClass();
$record->discussion = $discussion1->id;
$record->parent = $discussion1->firstpost;
$record->userid = $user2->id;
$record->message = 'post2';
$record->attachemt = 1;
$discussion1reply1 = self::getDataGenerator()->get_plugin_generator('mod_forum')->create_post($record);
$filerecord['itemid'] = $discussion1reply1->id;
$filerecord['filename'] = 'myfile3';
$file3 = $fs->create_file_from_string($filerecord, 'Some contents 3');
// Create post2 in discussion1.
$record = new stdClass();
$record->discussion = $discussion1->id;
$record->parent = $discussion1->firstpost;
$record->userid = $user2->id;
$record->message = 'post3';
$discussion1reply2 = self::getDataGenerator()->get_plugin_generator('mod_forum')->create_post($record);
// Now get all the posts and see if they have the right files attached.
$searcharea = \core_search\manager::get_search_area($this->forumpostareaid);
$recordset = $searcharea->get_recordset_by_timestamp(0);
$nrecords = 0;
foreach ($recordset as $record) {
$doc = $searcharea->get_document($record);
$searcharea->attach_files($doc);
$files = $doc->get_files();
// Now check that each doc has the right files on it.
switch ($doc->get('itemid')) {
case ($post->id):
$this->assertCount(2, $files);
$this->assertEquals($file1->get_id(), $files[$file1->get_id()]->get_id());
$this->assertEquals($file2->get_id(), $files[$file2->get_id()]->get_id());
break;
case ($discussion1reply1->id):
$this->assertCount(1, $files);
$this->assertEquals($file3->get_id(), $files[$file3->get_id()]->get_id());
break;
case ($discussion1reply2->id):
$this->assertCount(0, $files);
break;
default:
$this->fail('Unexpected post returned');
break;
}
$nrecords++;
}
$recordset->close();
$this->assertEquals(3, $nrecords);
}
}

View File

@ -61,9 +61,10 @@ class entry extends \core_search\area\base_mod {
* Returns the documents associated with this glossary entry id.
*
* @param stdClass $entry glossary entry.
* @param array $options
* @return \core_search\document
*/
public function get_document($entry) {
public function get_document($entry, $options = array()) {
global $DB;
$keywords = array();
@ -92,12 +93,17 @@ class entry extends \core_search\area\base_mod {
$doc->set('title', $entry->concept);
$doc->set('content', content_to_text($entry->definition, $entry->definitionformat));
$doc->set('contextid', $context->id);
$doc->set('type', \core_search\manager::TYPE_TEXT);
$doc->set('courseid', $entry->course);
$doc->set('userid', $entry->userid);
$doc->set('owneruserid', \core_search\manager::NO_OWNER_ID);
$doc->set('modified', $entry->timemodified);
// Check if this document should be considered new.
if (isset($options['lastindexedtime']) && ($options['lastindexedtime'] < $entry->timecreated)) {
// If the document was created after the last index time, it must be new.
$doc->set_is_new(true);
}
// Adding keywords as extra info.
if ($keywords) {
$doc->set('description1', implode(' ' , $keywords));

View File

@ -43,9 +43,10 @@ class activity extends \core_search\area\base_activity {
* description field is not.
*
* @param stdClass $record
* @param array $options
* @return \core_search\document
*/
public function get_document($record) {
public function get_document($record, $options = array()) {
try {
$cm = $this->get_cm($this->get_module_name(), $record->id, $record->course);
@ -66,7 +67,6 @@ class activity extends \core_search\area\base_activity {
$doc->set('title', $record->name);
$doc->set('content', content_to_text($record->content, $record->contentformat));
$doc->set('contextid', $context->id);
$doc->set('type', \core_search\manager::TYPE_TEXT);
$doc->set('courseid', $record->course);
$doc->set('owneruserid', \core_search\manager::NO_OWNER_ID);
$doc->set('modified', $record->timemodified);

View File

@ -34,4 +34,34 @@ defined('MOODLE_INTERNAL') || die();
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class activity extends \core_search\area\base_activity {
/**
* Returns true if this area uses file indexing.
*
* @return bool
*/
public function uses_file_indexing() {
return true;
}
/**
* Add the main file to the index.
*
* @param document $document The current document
* @return null
*/
public function attach_files($document) {
$fs = get_file_storage();
$cm = $this->get_cm($this->get_module_name(), $document->get('itemid'), $document->get('courseid'));
$context = \context_module::instance($cm->id);
// Order by sortorder desc, the first is consided the main file.
$files = $fs->get_area_files($context->id, 'mod_resource', 'content', 0, 'sortorder DESC, id ASC', false);
$mainfile = $files ? reset($files) : null;
if ($mainfile && $mainfile->get_sortorder() > 0) {
$document->add_stored_file($mainfile);
}
}
}

View File

@ -0,0 +1,116 @@
<?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/>.
/**
* Resource search unit tests.
*
* @package mod_resource
* @category test
* @copyright 2016 Eric Merrill {@link http://www.merrilldigital.com}
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
defined('MOODLE_INTERNAL') || die();
global $CFG;
require_once($CFG->dirroot . '/search/tests/fixtures/testable_core_search.php');
/**
* Provides the unit tests for forum search.
*
* @package mod_resource
* @category test
* @copyright 2016 Eric Merrill {@link http://www.merrilldigital.com}
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class mod_resource_search_testcase extends advanced_testcase {
/**
* @var string Area id
*/
protected $resourceareaid = null;
public function setUp() {
$this->resetAfterTest(true);
set_config('enableglobalsearch', true);
$this->resourceareaid = \core_search\manager::generate_areaid('mod_resource', 'activity');
// Set \core_search::instance to the mock_search_engine as we don't require the search engine to be working to test this.
$search = testable_core_search::instance();
}
/**
* Test for resource file attachments.
*
* @return void
*/
public function test_attach_files() {
global $USER;
$this->setAdminUser();
// Setup test data.
$course = $this->getDataGenerator()->create_course();
$fs = get_file_storage();
$usercontext = context_user::instance($USER->id);
$record = new stdClass();
$record->course = $course->id;
$record->files = file_get_unused_draft_itemid();
// Attach the main file. We put them in the draft area, create_module will move them.
$filerecord = array(
'contextid' => $usercontext->id,
'component' => 'user',
'filearea' => 'draft',
'itemid' => $record->files,
'filepath' => '/',
'filename' => 'mainfile',
'sortorder' => 1
);
$fs->create_file_from_string($filerecord, 'Test resource file');
// Attach a second file that shouldn't be returned with the search doc.
$filerecord['filename'] = 'extrafile';
$filerecord['sortorder'] = 0;
$fs->create_file_from_string($filerecord, 'Test resource file 2');
$resource = $this->getDataGenerator()->create_module('resource', $record);
$searcharea = \core_search\manager::get_search_area($this->resourceareaid);
$this->assertInstanceOf('\mod_resource\search\activity', $searcharea);
$recordset = $searcharea->get_recordset_by_timestamp(0);
$nrecords = 0;
foreach ($recordset as $record) {
$doc = $searcharea->get_document($record);
$searcharea->attach_files($doc);
$files = $doc->get_files();
// Resources should only return their main file.
$this->assertCount(1, $files);
$file = reset($files);
$this->assertEquals('mainfile', $file->get_filename());
$nrecords++;
}
$recordset->close();
$this->assertEquals(1, $nrecords);
}
}

View File

@ -41,10 +41,11 @@ class activity extends \core_search\area\base_activity {
* Overwrites base_activity to add the provided URL as description.
*
* @param stdClass $record
* @param array $options
* @return \core_search\document
*/
public function get_document($record) {
$doc = parent::get_document($record);
public function get_document($record, $options = array()) {
$doc = parent::get_document($record, $options);
if (!$doc) {
return false;
}

View File

@ -185,6 +185,15 @@ abstract class base {
return (bool)get_config($componentname, $varname . '_enabled');
}
/**
* Returns true if this area uses file indexing.
*
* @return bool
*/
public function uses_file_indexing() {
return false;
}
/**
* Returns a recordset ordered by modification date ASC.
*
@ -213,10 +222,25 @@ abstract class base {
* Search areas should send plain text to the search engine, use the following function to convert any user
* input data to plain text: {@link content_to_text}
*
* Valid keys for the options array are:
* indexfiles => File indexing is enabled if true.
* lastindexedtime => The last time this area was indexed. 0 if never indexed.
*
* @param \stdClass $record A record containing, at least, the indexed document id and a modified timestamp
* @param array $options Options for document creation
* @return \core_search\document
*/
abstract public function get_document($record);
abstract public function get_document($record, $options = array());
/**
* Add any files to the document that should be indexed.
*
* @param document $document The current document
* @return void
*/
public function attach_files($document) {
return;
}
/**
* Can the current user see the document.

View File

@ -43,6 +43,11 @@ abstract class base_activity extends base_mod {
*/
const MODIFIED_FIELD_NAME = 'timemodified';
/**
* Activities with a time created field can overwrite this constant.
*/
const CREATED_FIELD_NAME = '';
/**
* The context levels the search area is working on.
* @var array
@ -68,9 +73,10 @@ abstract class base_activity extends base_mod {
* default ones, or to fill description optional fields with extra stuff.
*
* @param stdClass $record
* @param array $options
* @return \core_search\document
*/
public function get_document($record) {
public function get_document($record, $options = array()) {
try {
$cm = $this->get_cm($this->get_module_name(), $record->id, $record->course);
@ -91,11 +97,19 @@ abstract class base_activity extends base_mod {
$doc->set('title', $record->name);
$doc->set('content', content_to_text($record->intro, $record->introformat));
$doc->set('contextid', $context->id);
$doc->set('type', \core_search\manager::TYPE_TEXT);
$doc->set('courseid', $record->course);
$doc->set('owneruserid', \core_search\manager::NO_OWNER_ID);
$doc->set('modified', $record->{static::MODIFIED_FIELD_NAME});
// Check if this document should be considered new.
if (isset($options['lastindexedtime'])) {
$createdfield = static::CREATED_FIELD_NAME;
if (!empty($createdfield) && ($options['lastindexedtime'] < $record->{$createdfield})) {
// If the document was created after the last index time, it must be new.
$doc->set_is_new(true);
}
}
return $doc;
}

View File

@ -63,13 +63,12 @@ abstract class base_mod extends base {
* @return \cm_info
*/
protected function get_cm($modulename, $instanceid, $courseid) {
$modinfo = get_fast_modinfo($courseid);
// Hopefully not many, they are indexed by cmid.
$instances = $modinfo->get_instances_of($modulename);
foreach ($instances as $cminfo) {
if ($cminfo->instance === $instanceid) {
if ($cminfo->instance == $instanceid) {
return $cminfo;
}
}

View File

@ -68,6 +68,16 @@ class document implements \renderable, \templatable {
*/
protected $contentitemid = null;
/**
* @var bool Should be set to true if document hasn't been indexed before. False if unknown.
*/
protected $isnew = false;
/**
* @var \stored_file[] An array of stored files to attach to the document.
*/
protected $files = array();
/**
* All required fields any doc should contain.
*
@ -156,9 +166,21 @@ class document implements \renderable, \templatable {
'type' => 'string',
'stored' => true,
'indexed' => true
),
)
);
/**
* Any fields that are engine specifc. These are fields that are solely used by a search engine plugin
* for internal purposes.
*
* Field names should be prefixed with engine name to avoid potential conflict with core fields.
*
* Uses same format as fields above.
*
* @var array
*/
protected static $enginefields = array();
/**
* We ensure that the document has a unique id across search areas.
*
@ -178,6 +200,40 @@ class document implements \renderable, \templatable {
$this->data['itemid'] = intval($itemid);
}
/**
* Add a stored file to the document.
*
* @param \stored_file|int $file The file to add, or file id.
* @return void
*/
public function add_stored_file($file) {
if (is_numeric($file)) {
$this->files[$file] = $file;
} else {
$this->files[$file->get_id()] = $file;
}
}
/**
* Returns the array of attached files.
*
* @return \stored_file[]
*/
public function get_files() {
// The files array can contain stored file ids, so we need to get instances if asked.
foreach ($this->files as $id => $listfile) {
if (is_numeric($listfile)) {
$fs = get_file_storage();
if ($file = $fs->get_file_by_id($id)) {
$this->files[$id] = $file;
}
}
}
return $this->files;
}
/**
* Setter.
*
@ -197,6 +253,8 @@ class document implements \renderable, \templatable {
$fielddata = static::$requiredfields[$fieldname];
} else if (!empty(static::$optionalfields[$fieldname])) {
$fielddata = static::$optionalfields[$fieldname];
} else if (!empty(static::$enginefields[$fieldname])) {
$fielddata = static::$enginefields[$fieldname];
}
if (empty($fielddata)) {
@ -268,13 +326,31 @@ class document implements \renderable, \templatable {
return (isset($this->data[$field]) || isset($this->extradata[$field]));
}
/**
* Set if this is a new document. False if unknown.
*
* @param bool $new
*/
public function set_is_new($new) {
$this->isnew = (bool)$new;
}
/**
* Returns if the document is new. False if unknown.
*
* @return bool
*/
public function get_is_new() {
return $this->isnew;
}
/**
* Returns all default fields definitions.
*
* @return array
*/
public static function get_default_fields_definition() {
return static::$requiredfields + static::$optionalfields;
return static::$requiredfields + static::$optionalfields + static::$enginefields;
}
/**
@ -337,7 +413,7 @@ class document implements \renderable, \templatable {
* @return void
*/
public function set_data_from_engine($docdata) {
$fields = static::$requiredfields + static::$optionalfields;
$fields = static::$requiredfields + static::$optionalfields + static::$enginefields;
foreach ($fields as $fieldname => $field) {
// Optional params might not be there.
@ -395,6 +471,8 @@ class document implements \renderable, \templatable {
* @return array
*/
public function export_for_engine() {
// Set any unset defaults.
$this->apply_defaults();
// We don't want to affect the document instance.
$data = $this->data;
@ -416,7 +494,8 @@ class document implements \renderable, \templatable {
}
}
foreach (static::$optionalfields as $fieldname => $field) {
$fields = static::$optionalfields + static::$enginefields;
foreach ($fields as $fieldname => $field) {
if (!isset($data[$fieldname])) {
continue;
}
@ -432,6 +511,18 @@ class document implements \renderable, \templatable {
return $data;
}
/**
* Apply any defaults to unset fields before export. Called after document building, but before export.
*
* Sub-classes of this should make sure to call parent::apply_defaults().
*/
protected function apply_defaults() {
// Set the default type, TYPE_TEXT.
if (!isset($this->data['type'])) {
$this->data['type'] = manager::TYPE_TEXT;
}
}
/**
* Export the document data to be used as a template context.
*
@ -459,6 +550,22 @@ class document implements \renderable, \templatable {
'description2' => $this->is_set('description2') ? $this->format_text($this->get('description2')) : null,
];
// Now take any attached any files.
$files = $this->get_files();
if (!empty($files)) {
if (count($files) > 1) {
$filenames = array();
foreach ($files as $file) {
$filenames[] = $file->get_filename();
}
$data['multiplefiles'] = true;
$data['filenames'] = $filenames;
} else {
$file = reset($files);
$data['filename'] = $file->get_filename();
}
}
if ($this->is_set('userid')) {
$data['userurl'] = new \moodle_url('/user/view.php', array('id' => $this->get('userid'), 'course' => $this->get('courseid')));
$data['userfullname'] = format_string($this->get('userfullname'), true, array('context' => $this->get('contextid')));

View File

@ -313,6 +313,15 @@ abstract class engine {
return $this->queryerror;
}
/**
* Return true if file indexing is supported and enabled. False otherwise.
*
* @return bool
*/
public function file_indexing_enabled() {
return false;
}
/**
* Clears the current query error value.
*
@ -334,10 +343,11 @@ abstract class engine {
/**
* Adds a document to the search engine.
*
* @param array $doc
* @return void
* @param document $document
* @param bool $fileindexing True if file indexing is to be used
* @return bool False if the file was skipped or failed, true on success
*/
abstract function add_document($doc);
abstract function add_document($document, $fileindexing = false);
/**
* Executes the query on the engine.

View File

@ -42,6 +42,11 @@ class manager {
*/
const TYPE_TEXT = 1;
/**
* @var int File contents.
*/
const TYPE_FILE = 2;
/**
* @var int User can not access the document.
*/
@ -498,33 +503,40 @@ class manager {
$numdocsignored = 0;
$lastindexeddoc = 0;
$prevtimestart = intval(get_config($componentconfigname, $varname . '_indexingstart'));
if ($fullindex === true) {
$prevtimestart = 0;
$referencestarttime = 0;
} else {
$prevtimestart = intval(get_config($componentconfigname, $varname . '_indexingstart'));
$referencestarttime = $prevtimestart;
}
// Getting the recordset from the area.
$recordset = $searcharea->get_recordset_by_timestamp($prevtimestart);
$recordset = $searcharea->get_recordset_by_timestamp($referencestarttime);
// Pass get_document as callback.
$iterator = new \core\dml\recordset_walk($recordset, array($searcharea, 'get_document'));
$fileindexing = $this->engine->file_indexing_enabled() && $searcharea->uses_file_indexing();
$options = array('indexfiles' => $fileindexing, 'lastindexedtime' => $prevtimestart);
$iterator = new \core\dml\recordset_walk($recordset, array($searcharea, 'get_document'), $options);
foreach ($iterator as $document) {
if (!$document instanceof \core_search\document) {
continue;
}
$docdata = $document->export_for_engine();
switch ($docdata['type']) {
case static::TYPE_TEXT:
$this->engine->add_document($docdata);
$numdocs++;
break;
default:
$numdocsignored++;
$iterator->close();
throw new \moodle_exception('doctypenotsupported', 'search');
if ($prevtimestart == 0) {
// If we have never indexed this area before, it must be new.
$document->set_is_new(true);
}
if ($fileindexing) {
// Attach files if we are indexing.
$searcharea->attach_files($document);
}
if ($this->engine->add_document($document, $fileindexing)) {
$numdocs++;
} else {
$numdocsignored++;
}
$lastindexeddoc = $document->get('modified');

View File

@ -314,11 +314,27 @@ class engine extends \core_search\engine {
*
* This does not commit to the search engine.
*
* @param array $doc
* @return void
* @param document $document
* @param bool $fileindexing True if file indexing is to be used
* @return bool
*/
public function add_document($doc) {
public function add_document($document, $fileindexing = false) {
$docdata = $document->export_for_engine();
if (!$this->add_text_document($docdata)) {
return false;
}
return true;
}
/**
* Adds a text document to the search engine.
*
* @param array $filedoc
* @return bool
*/
protected function add_text_document($doc) {
$solrdoc = new \SolrInputDocument();
foreach ($doc as $field => $value) {
$solrdoc->addField($field, $value);
@ -326,6 +342,7 @@ class engine extends \core_search\engine {
try {
$result = $this->get_search_client()->addDocument($solrdoc, true, static::AUTOCOMMIT_WITHIN);
return true;
} catch (\SolrClientException $e) {
debugging('Solr client error adding document with id ' . $doc['id'] . ': ' . $e->getMessage(), DEBUG_DEVELOPER);
} catch (\SolrServerException $e) {
@ -333,6 +350,8 @@ class engine extends \core_search\engine {
$msg = strtok($e->getMessage(), "\n");
debugging('Solr server error adding document with id ' . $doc['id'] . ': ' . $msg, DEBUG_DEVELOPER);
}
return false;
}
/**

View File

@ -213,7 +213,7 @@ class search_solr_engine_testcase extends advanced_testcase {
// Get the doc and insert the default doc.
$doc = $area->get_document($record);
$engine->add_document($doc->export_for_engine());
$engine->add_document($doc);
$users = array();
$users[] = $this->getDataGenerator()->create_user();
@ -225,9 +225,10 @@ class search_solr_engine_testcase extends advanced_testcase {
// Now add a custom doc for each user.
foreach ($users as $user) {
$doc = $area->get_document($record);
$doc->set('id', $originalid.'-'.$user->id);
$doc->set('owneruserid', $user->id);
$engine->add_document($doc->export_for_engine());
$engine->add_document($doc);
}
$engine->area_index_complete($area->get_area_id());

View File

@ -38,6 +38,9 @@
* userfullname
* description1
* description2
* filename
* multiplefiles
* filenames
Example context (json):
{
@ -64,6 +67,21 @@
{{#description2}}
<div class="result-content">{{{description2}}}</div>
{{/description2}}
{{#filename}}
<div class="result-content-filename">
{{#str}}matchingfile, search, {{filename}}{{/str}}
</div>
{{/filename}}
{{#multiplefiles}}
<div class="result-content-filenames">
{{#str}}matchingfiles, search{{/str}}<br>
<ul class="list">
{{#filenames}}
<li><span class="filename">{{.}}</span></li>
{{/filenames}}
</ul>
</div>
{{/multiplefiles}}
<div class="result-context-info">
<a href="{{{contexturl}}}">{{#str}}viewresultincontext, search{{/str}}</a> -
<a href="{{{courseurl}}}">{{#str}}incourse, search, {{coursefullname}}{{/str}}</a>

View File

@ -44,7 +44,7 @@ class role_capabilities extends \core_search\area\base {
return $DB->get_recordset_sql("SELECT id, contextid, roleid, capability FROM {role_capabilities} where timemodified >= ? and capability = ?", array($modifiedfrom, 'moodle/course:renameroles'));
}
public function get_document($record) {
public function get_document($record, $options = array()) {
global $USER;
// Prepare associative array with data from DB.
@ -52,7 +52,6 @@ class role_capabilities extends \core_search\area\base {
$doc->set('title', $record->capability . ' roleid ' . $record->roleid);
$doc->set('content', $record->capability . ' roleid ' . $record->roleid . ' message');
$doc->set('contextid', $record->contextid);
$doc->set('type', \core_search\manager::TYPE_TEXT);
$doc->set('courseid', SITEID);
$doc->set('userid', $USER->id);
$doc->set('owneruserid', \core_search\manager::NO_OWNER_ID);
@ -61,6 +60,30 @@ class role_capabilities extends \core_search\area\base {
return $doc;
}
public function attach_files($document) {
global $CFG;
// Add the searchable file fixture.
$syscontext = \context_system::instance();
$filerecord = array(
'contextid' => $syscontext->id,
'component' => 'core',
'filearea' => 'unittest',
'itemid' => 0,
'filepath' => '/',
'filename' => 'searchfile'.$document->get('itemid').'.txt',
);
$fs = get_file_storage();
$file = $fs->create_file_from_string($filerecord, 'File contents');
$document->add_stored_file($file);
}
public function uses_file_indexing() {
return true;
}
public function check_access($id) {
return \core_search\manager::ACCESS_GRANTED;
}

View File

@ -37,7 +37,7 @@ class engine extends \core_search\engine {
return true;
}
public function add_document($doc) {
public function add_document($document, $fileindexing = false) {
// No need to implement.
}

View File

@ -12,3 +12,6 @@
margin: 7px 0;
}
.search-results .result .filename {
font-style: italic;
}

View File

@ -12,6 +12,10 @@
margin: 7px 0;
}
.search-results .result .filename {
font-style: italic;
}
.search-input-wrapper {
margin: 0 5px 0 2px;
overflow: hidden;

File diff suppressed because one or more lines are too long