Commit 66e37026 authored by sam marshall's avatar sam marshall
Browse files

MDL-55356 core_search: Change existing search areas to new API

This change considers all existing search areas in Moodle and makes
necessary changes.

Custom change to course search, supported by helper in base.php:

* course/classes/search/mycourse.php

Custom change to message search:

* message/classes/search/message_received.php
* message/classes/search/message_sent.php

Custom change to user search:

* user/classes/search/user.php

Custom changes to module areas, supported by helper in base_mod.php:

* mod/book/classes/search/chapter.php
* mod/data/classes/search/entry.php
* mod/forum/classes/search/post.php
* mod/glossary/classes/search/entry.php
* mod/survey/classes/search/activity.php
* mod/wiki/classes/search/collaborative_page.php

(Note: the unit tests do not exhaustively check every context type
for these, given that's mainly handled by the helper function
which was already tested in the base_activity test.)

Handled by block base class (no change):

* blocks/html/classes/search/content.php

Handled by activity base class (no change):

* mod/assign/classes/search/activity.php
* mod/book/classes/search/activity.php
* mod/chat/classes/search/activity.php
* mod/choice/classes/search/activity.php
* mod/data/classes/search/activity.php
* mod/feedback/classes/search/activity.php
* mod/folder/classes/search/activity.php
* mod/forum/classes/search/activity.php
* mod/glossary/classes/search/activity.php
* mod/imscp/classes/search/activity.php
* mod/label/classes/search/activity.php
* mod/lesson/classes/search/activity.php
* mod/lti/classes/search/activity.php
* mod/page/classes/search/activity.php
* mod/quiz/classes/search/activity.php
* mod/resource/classes/search/activity.php
* mod/scorm/classes/search/activity.php
* mod/url/classes/search/activity.php
* mod/wiki/classes/search/activity.php
* mod/workshop/classes/search/activity.php
parent 81a98883
......@@ -45,11 +45,24 @@ class mycourse extends \core_search\base {
* Returns recordset containing required data for indexing courses.
*
* @param int $modifiedfrom timestamp
* @return \moodle_recordset
* @param \context|null $context Restriction context
* @return \moodle_recordset|null Recordset or null if no change possible
*/
public function get_recordset_by_timestamp($modifiedfrom = 0) {
public function get_document_recordset($modifiedfrom = 0, \context $context = null) {
global $DB;
return $DB->get_recordset_select('course', 'timemodified >= ?', array($modifiedfrom), 'timemodified ASC');
list ($contextjoin, $contextparams) = $this->get_course_level_context_restriction_sql(
$context, 'c');
if ($contextjoin === null) {
return null;
}
return $DB->get_recordset_sql("
SELECT c.*
FROM {course} c
$contextjoin
WHERE c.timemodified >= ?
ORDER BY c.timemodified ASC", array_merge($contextparams, [$modifiedfrom]));
}
/**
......
......@@ -98,6 +98,85 @@ class course_search_testcase extends advanced_testcase {
$recordset->close();
}
/**
* Tests course indexing support for contexts.
*/
public function test_mycourses_indexing_contexts() {
global $DB, $USER, $SITE;
$searcharea = \core_search\manager::get_search_area($this->mycoursesareaid);
// Create some courses in categories, and a forum.
$generator = $this->getDataGenerator();
$cat1 = $generator->create_category();
$course1 = $generator->create_course(['category' => $cat1->id]);
$cat2 = $generator->create_category(['parent' => $cat1->id]);
$course2 = $generator->create_course(['category' => $cat2->id]);
$cat3 = $generator->create_category();
$course3 = $generator->create_course(['category' => $cat3->id]);
$forum = $generator->create_module('forum', ['course' => $course1->id]);
$DB->set_field('course', 'timemodified', 0, ['id' => $SITE->id]);
$DB->set_field('course', 'timemodified', 1, ['id' => $course1->id]);
$DB->set_field('course', 'timemodified', 2, ['id' => $course2->id]);
$DB->set_field('course', 'timemodified', 3, ['id' => $course3->id]);
// Find the first block to use for a block context.
$blockid = array_values($DB->get_records('block_instances', null, 'id', 'id', 0, 1))[0]->id;
$blockcontext = context_block::instance($blockid);
// Check with block context - should be null.
$this->assertNull($searcharea->get_document_recordset(0, $blockcontext));
// Check with user context - should be null.
$this->setAdminUser();
$usercontext = context_user::instance($USER->id);
$this->assertNull($searcharea->get_document_recordset(0, $usercontext));
// Check with module context - should be null.
$modcontext = context_module::instance($forum->cmid);
$this->assertNull($searcharea->get_document_recordset(0, $modcontext));
// Check with course context - should return specified course if timestamp allows.
$coursecontext = context_course::instance($course3->id);
$results = self::recordset_to_ids($searcharea->get_document_recordset(3, $coursecontext));
$this->assertEquals([$course3->id], $results);
$results = self::recordset_to_ids($searcharea->get_document_recordset(4, $coursecontext));
$this->assertEquals([], $results);
// Check with category context - should return course in categories and subcategories.
$catcontext = context_coursecat::instance($cat1->id);
$results = self::recordset_to_ids($searcharea->get_document_recordset(0, $catcontext));
$this->assertEquals([$course1->id, $course2->id], $results);
$results = self::recordset_to_ids($searcharea->get_document_recordset(2, $catcontext));
$this->assertEquals([$course2->id], $results);
// Check with system context and null - should return all these courses + site course.
$systemcontext = context_system::instance();
$results = self::recordset_to_ids($searcharea->get_document_recordset(0, $systemcontext));
$this->assertEquals([$SITE->id, $course1->id, $course2->id, $course3->id], $results);
$results = self::recordset_to_ids($searcharea->get_document_recordset(0, null));
$this->assertEquals([$SITE->id, $course1->id, $course2->id, $course3->id], $results);
$results = self::recordset_to_ids($searcharea->get_document_recordset(3, $systemcontext));
$this->assertEquals([$course3->id], $results);
$results = self::recordset_to_ids($searcharea->get_document_recordset(3, null));
$this->assertEquals([$course3->id], $results);
}
/**
* Utility function to convert recordset to array of IDs for testing.
*
* @param moodle_recordset $rs Recordset to convert (and close)
* @return array Array of IDs from records indexed by number (0, 1, 2, ...)
*/
protected static function recordset_to_ids(moodle_recordset $rs) {
$results = [];
foreach ($rs as $rec) {
$results[] = $rec->id;
}
$rs->close();
return $results;
}
/**
* Document contents.
*
......
......@@ -132,4 +132,53 @@ abstract class base_message extends \core_search\base {
return $users;
}
/**
* Helper function to implement get_document_recordset for subclasses.
*
* @param int $modifiedfrom Modified from date
* @param \context|null $context Context or null
* @param string $userfield Name of user field (from or to) being considered
* @return \moodle_recordset|null Recordset or null if no results possible
* @throws \coding_exception If context invalid
*/
protected function get_document_recordset_helper($modifiedfrom, \context $context = null,
$userfield) {
global $DB;
// Set up basic query.
$where = $userfield . ' != :noreplyuser AND ' . $userfield .
' != :supportuser AND timecreated >= :modifiedfrom';
$params = [
'noreplyuser' => \core_user::NOREPLY_USER,
'supportuser' => \core_user::SUPPORT_USER,
'modifiedfrom' => $modifiedfrom
];
// Check context to see whether to add other restrictions.
if ($context === null) {
$context = \context_system::instance();
}
switch ($context->contextlevel) {
case CONTEXT_COURSECAT:
case CONTEXT_COURSE:
case CONTEXT_MODULE:
case CONTEXT_BLOCK:
// There are no messages in any of these contexts so nothing can be found.
return null;
case CONTEXT_USER:
// Add extra restriction to specific user context.
$where .= ' AND ' . $userfield . ' = :userid';
$params['userid'] = $context->instanceid;
break;
case CONTEXT_SYSTEM:
break;
default:
throw new \coding_exception('Unexpected contextlevel: ' . $context->contextlevel);
}
return $DB->get_recordset_select('message_read', $where, $params, 'timeread ASC');
}
}
......@@ -36,19 +36,14 @@ defined('MOODLE_INTERNAL') || die();
class message_received extends base_message {
/**
* Returns recordset containing message records.
* Returns a recordset with the messages for indexing.
*
* @param int $modifiedfrom timestamp
* @return \moodle_recordset
* @param int $modifiedfrom
* @param \context|null $context Optional context to restrict scope of returned results
* @return moodle_recordset|null Recordset (or null if no results)
*/
public function get_recordset_by_timestamp($modifiedfrom = 0) {
global $DB;
// We don't want to index messages received from noreply and support users.
$params = array('modifiedfrom' => $modifiedfrom, 'noreplyuser' => \core_user::NOREPLY_USER,
'supportuser' => \core_user::SUPPORT_USER);
return $DB->get_recordset_select('message_read', 'timeread >= :modifiedfrom AND
useridto != :noreplyuser AND useridto != :supportuser', $params, 'timeread ASC');
public function get_document_recordset($modifiedfrom = 0, \context $context = null) {
return $this->get_document_recordset_helper($modifiedfrom, $context, 'useridto');
}
/**
......
......@@ -35,19 +35,14 @@ defined('MOODLE_INTERNAL') || die();
class message_sent extends base_message {
/**
* Returns recordset containing message records.
* Returns a recordset with the messages for indexing.
*
* @param int $modifiedfrom timestamp
* @return \moodle_recordset
* @param int $modifiedfrom
* @param \context|null $context Optional context to restrict scope of returned results
* @return moodle_recordset|null Recordset (or null if no results)
*/
public function get_recordset_by_timestamp($modifiedfrom = 0) {
global $DB;
// We don't want to index messages sent by noreply and support users.
$params = array('modifiedfrom' => $modifiedfrom, 'noreplyuser' => \core_user::NOREPLY_USER,
'supportuser' => \core_user::SUPPORT_USER);
return $DB->get_recordset_select('message_read', 'timeread >= :modifiedfrom AND
useridfrom != :noreplyuser AND useridfrom != :supportuser', $params, 'timeread ASC');
public function get_document_recordset($modifiedfrom = 0, \context $context = null) {
return $this->get_document_recordset_helper($modifiedfrom, $context, 'useridfrom');
}
/**
......
......@@ -113,6 +113,71 @@ class message_received_search_testcase extends advanced_testcase {
$recordset->close();
}
/**
* Indexing messages, with restricted contexts.
*/
public function test_message_received_indexing_contexts() {
global $SITE;
require_once(__DIR__ . '/search_sent_test.php');
$searcharea = \core_search\manager::get_search_area($this->messagereceivedareaid);
$user1 = self::getDataGenerator()->create_user();
$user2 = self::getDataGenerator()->create_user();
$this->preventResetByRollback();
$sink = $this->redirectMessages();
// Send first message.
$message = new \core\message\message();
$message->courseid = SITEID;
$message->userfrom = $user1;
$message->userto = $user2;
$message->subject = 'Test1';
$message->smallmessage = 'Test small messsage';
$message->fullmessage = 'Test full messsage';
$message->fullmessageformat = 0;
$message->fullmessagehtml = null;
$message->notification = 0;
$message->component = 'moodle';
$message->name = 'instantmessage';
message_send($message);
// Ensure that ordering by timestamp will return in consistent order.
$this->waitForSecond();
// Send second message in opposite direction.
$message = new \core\message\message();
$message->courseid = SITEID;
$message->userfrom = $user2;
$message->userto = $user1;
$message->subject = 'Test2';
$message->smallmessage = 'Test small messsage';
$message->fullmessage = 'Test full messsage';
$message->fullmessageformat = 0;
$message->fullmessagehtml = null;
$message->notification = 0;
$message->component = 'moodle';
$message->name = 'instantmessage';
message_send($message);
// Test function with null context and system context (same).
$rs = $searcharea->get_document_recordset(0, null);
$this->assertEquals(['Test1', 'Test2'], message_sent_search_testcase::recordset_to_subjects($rs));
$rs = $searcharea->get_document_recordset(0, context_system::instance());
$this->assertEquals(['Test1', 'Test2'], message_sent_search_testcase::recordset_to_subjects($rs));
// Test with user context for each user.
$rs = $searcharea->get_document_recordset(0, \context_user::instance($user1->id));
$this->assertEquals(['Test2'], message_sent_search_testcase::recordset_to_subjects($rs));
$rs = $searcharea->get_document_recordset(0, \context_user::instance($user2->id));
$this->assertEquals(['Test1'], message_sent_search_testcase::recordset_to_subjects($rs));
// Test with a course context (should return null).
$this->assertNull($searcharea->get_document_recordset(0,
context_course::instance($SITE->id)));
}
/**
* Document contents.
*
......
......@@ -113,6 +113,85 @@ class message_sent_search_testcase extends advanced_testcase {
$recordset->close();
}
/**
* Indexing messages, with restricted contexts.
*/
public function test_message_sent_indexing_contexts() {
global $SITE;
$searcharea = \core_search\manager::get_search_area($this->messagesentareaid);
$user1 = self::getDataGenerator()->create_user();
$user2 = self::getDataGenerator()->create_user();
$this->preventResetByRollback();
$sink = $this->redirectMessages();
// Send first message.
$message = new \core\message\message();
$message->courseid = SITEID;
$message->userfrom = $user1;
$message->userto = $user2;
$message->subject = 'Test1';
$message->smallmessage = 'Test small messsage';
$message->fullmessage = 'Test full messsage';
$message->fullmessageformat = 0;
$message->fullmessagehtml = null;
$message->notification = 0;
$message->component = 'moodle';
$message->name = 'instantmessage';
message_send($message);
// Ensure that ordering by timestamp will return in consistent order.
$this->waitForSecond();
// Send second message in opposite direction.
$message = new \core\message\message();
$message->courseid = SITEID;
$message->userfrom = $user2;
$message->userto = $user1;
$message->subject = 'Test2';
$message->smallmessage = 'Test small messsage';
$message->fullmessage = 'Test full messsage';
$message->fullmessageformat = 0;
$message->fullmessagehtml = null;
$message->notification = 0;
$message->component = 'moodle';
$message->name = 'instantmessage';
message_send($message);
// Test function with null context and system context (same).
$rs = $searcharea->get_document_recordset(0, null);
$this->assertEquals(['Test1', 'Test2'], self::recordset_to_subjects($rs));
$rs = $searcharea->get_document_recordset(0, context_system::instance());
$this->assertEquals(['Test1', 'Test2'], self::recordset_to_subjects($rs));
// Test with user context for each user.
$rs = $searcharea->get_document_recordset(0, \context_user::instance($user1->id));
$this->assertEquals(['Test1'], self::recordset_to_subjects($rs));
$rs = $searcharea->get_document_recordset(0, \context_user::instance($user2->id));
$this->assertEquals(['Test2'], self::recordset_to_subjects($rs));
// Test with a course context (should return null).
$this->assertNull($searcharea->get_document_recordset(0,
context_course::instance($SITE->id)));
}
/**
* Utility function to convert recordset to array of message subjects for testing.
*
* @param moodle_recordset $rs Recordset to convert (and close)
* @return array Array of IDs from records indexed by number (0, 1, 2, ...)
*/
public static function recordset_to_subjects(moodle_recordset $rs) {
$results = [];
foreach ($rs as $rec) {
$results[] = $rec->subject;
}
$rs->close();
return $results;
}
/**
* Document contents.
*
......@@ -272,4 +351,4 @@ class message_sent_search_testcase extends advanced_testcase {
$this->assertFalse($doc);
}
}
\ No newline at end of file
}
......@@ -43,16 +43,24 @@ class chapter extends \core_search\base_mod {
* Returns a recordset with all required chapter information.
*
* @param int $modifiedfrom
* @return moodle_recordset
* @param \context|null $context Optional context to restrict scope of returned results
* @return moodle_recordset|null Recordset (or null if no results)
*/
public function get_recordset_by_timestamp($modifiedfrom = 0) {
public function get_document_recordset($modifiedfrom = 0, \context $context = null) {
global $DB;
$sql = 'SELECT c.*, b.id AS bookid, b.course AS courseid
list ($contextjoin, $contextparams) = $this->get_context_restriction_sql(
$context, 'book', 'b');
if ($contextjoin === null) {
return null;
}
$sql = "SELECT c.*, b.id AS bookid, b.course AS courseid
FROM {book_chapters} c
JOIN {book} b ON b.id = c.bookid
WHERE c.timemodified >= ? ORDER BY c.timemodified ASC';
return $DB->get_recordset_sql($sql, array($modifiedfrom));
$contextjoin
WHERE c.timemodified >= ? ORDER BY c.timemodified ASC";
return $DB->get_recordset_sql($sql, array_merge($contextparams, [$modifiedfrom]));
}
/**
......
......@@ -118,6 +118,26 @@ class mod_book_search_testcase extends advanced_testcase {
// No new records.
$this->assertFalse($recordset->valid());
$recordset->close();
// Create another book and chapter.
$book2 = $this->getDataGenerator()->create_module('book', array('course' => $course1->id));
$bookgenerator->create_chapter(array('bookid' => $book2->id,
'content' => 'Chapter3', 'title' => 'Title3'));
// Query by context, first book.
$recordset = $searcharea->get_document_recordset(0, \context_module::instance($book->cmid));
$this->assertEquals(2, iterator_count($recordset));
$recordset->close();
// Second book.
$recordset = $searcharea->get_document_recordset(0, \context_module::instance($book2->cmid));
$this->assertEquals(1, iterator_count($recordset));
$recordset->close();
// Course.
$recordset = $searcharea->get_document_recordset(0, \context_course::instance($course1->id));
$this->assertEquals(3, iterator_count($recordset));
$recordset->close();
}
/**
......
......@@ -47,16 +47,25 @@ class entry extends \core_search\base_mod {
* Returns recordset containing required data for indexing database entries.
*
* @param int $modifiedfrom timestamp
* @return moodle_recordset
* @param \context|null $context Optional context to restrict scope of returned results
* @return moodle_recordset|null Recordset (or null if no results)
*/
public function get_recordset_by_timestamp($modifiedfrom = 0) {
public function get_document_recordset($modifiedfrom = 0, \context $context = null) {
global $DB;
list ($contextjoin, $contextparams) = $this->get_context_restriction_sql(
$context, 'data', 'd', SQL_PARAMS_NAMED);
if ($contextjoin === null) {
return null;
}
$sql = "SELECT dr.*, d.course
FROM {data_records} dr
JOIN {data} d ON d.id = dr.dataid
$contextjoin
WHERE dr.timemodified >= :timemodified";
return $DB->get_recordset_sql($sql, array('timemodified' => $modifiedfrom));
return $DB->get_recordset_sql($sql,
array_merge($contextparams, ['timemodified' => $modifiedfrom]));
}
/**
......
......@@ -314,6 +314,22 @@ class mod_data_search_test extends advanced_testcase {
// No new records.
$this->assertFalse($recordset->valid());
$recordset->close();
// Create a second database, also with one record.
$data2 = $this->getDataGenerator()->create_module('data', ['course' => $course1->id]);
$this->create_default_data_fields($fieldtypes, $data2);
$this->create_default_data_record($data2);
// Test indexing with contexts.
$rs = $searcharea->get_document_recordset(0, context_module::instance($data1->cmid));
$this->assertEquals(1, iterator_count($rs));
$rs->close();
$rs = $searcharea->get_document_recordset(0, context_module::instance($data2->cmid));
$this->assertEquals(1, iterator_count($rs));
$rs->close();
$rs = $searcharea->get_document_recordset(0, context_course::instance($course1->id));
$this->assertEquals(2, iterator_count($rs));
$rs->close();
}
/**
......
......@@ -56,17 +56,25 @@ class post extends \core_search\base_mod {
* Returns recordset containing required data for indexing forum posts.
*
* @param int $modifiedfrom timestamp
* @return moodle_recordset
* @param \context|null $context Optional context to restrict scope of returned results
* @return moodle_recordset|null Recordset (or null if no results)
*/
public function get_recordset_by_timestamp($modifiedfrom = 0) {
public function get_document_recordset($modifiedfrom = 0, \context $context = null) {
global $DB;
$sql = 'SELECT fp.*, f.id AS forumid, f.course AS courseid
list ($contextjoin, $contextparams) = $this->get_context_restriction_sql(
$context, 'forum', 'f');
if ($contextjoin === null) {
return null;
}
$sql = "SELECT fp.*, f.id AS forumid, f.course AS courseid
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';
return $DB->get_recordset_sql($sql, array($modifiedfrom));
$contextjoin
WHERE fp.modified >= ? ORDER BY fp.modified ASC";
return $DB->get_recordset_sql($sql, array_merge($contextparams, [$modifiedfrom]));
}
/**
......
......@@ -144,6 +144,26 @@ class mod_forum_search_testcase extends advanced_testcase {
// No new records.
$this->assertFalse($recordset->valid());
$recordset->close();
// Context test: create another forum with 1 post.
$forum2 = self::getDataGenerator()->create_module('forum', ['course' => $course1->id]);
$record = new stdClass();
$record->course = $course1->id;
$record->userid = $user1->id;
$record->forum = $forum2->id;
$record->message = 'discussion';
self::getDataGenerator()->get_plugin_generator('mod_forum')->create_discussion($record);
// Test indexing with each forum then combined course context.
$rs = $searcharea->get_document_recordset(0, context_module::instance($forum1->cmid));
$this->assertEquals(2, iterator_count($rs));
$rs->close();
$rs = $searcharea->get_document_recordset(0, context_module::instance($forum2->cmid));
$this->assertEquals(1, iterator_count($rs));
$rs->close();
$rs = $searcharea->get_document_recordset(0, context_course::instance($course1->id));
$this->assertEquals(3, iterator_count($rs));
$rs->close();
}
/**
......
......@@ -46,15 +46,23 @@ class entry extends \core_search\base_mod {
* Returns recordset containing required data for indexing glossary entries.
*
* @param int $modifiedfrom timestamp
* @return moodle_recordset
* @param \context|null $context Optional context to restrict scope of returned results
* @return moodle_recordset|null Recordset (or null if no results)
*/
public function get_recordset_by_timestamp($modifiedfrom = 0) {
public function get_document_recordset($modifiedfrom = 0, \context $context = null) {
global $DB;
list ($contextjoin, $contextparams) = $this->get_context_restriction_sql(
$context, 'glossary', 'g');
if ($contextjoin === null) {
return null;
}
$sql = "SELECT ge.*, g.course FROM {glossary_entries} ge
JOIN {glossary} g ON g.id = ge.glossaryid
WHERE ge.timemodified >= ? ORDER BY ge.timemodified ASC";
return $DB->get_recordset_sql($sql, array($modifiedfrom));
$contextjoin
WHERE ge.timemodified >= ? ORDER BY ge.timemodified ASC";
return $DB->get_recordset_sql($sql, array_merge($contextparams, [$modifiedfrom]));
}
/**
......
......@@ -132,6 +132,21 @@ class mod_glossary_search_testcase extends advanced_testcase {
// No new records.
$this->assertFalse($recordset->valid());
$recordset->close();