MDL-57898 core_course: global search for course custom fields

This commit is part of work on Custom fields API,
to minimize commit history in moodle core the work of a team of developers was split
into several commits with different authors but the authorship of individual
lines of code may be different from the commit author.
This commit is contained in:
Daniel Neis Araujo 2019-01-11 10:56:51 +01:00 committed by Marina Glancy
parent 7a0162f17a
commit 028ed12228
3 changed files with 275 additions and 0 deletions

View File

@ -0,0 +1,184 @@
<?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/>.
/**
* Search area for course custom fields.
*
* @package core_course
* @copyright Toni Barbera <toni@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace core_course\search;
use core_course\customfield\course_handler;
use core_customfield\data_controller;
use core_customfield\field_controller;
defined('MOODLE_INTERNAL') || die();
/**
* Search area for course custom fields.
*
* @package core_course
* @copyright Toni Barbera <toni@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class customfield extends \core_search\base {
/**
* Custom fields are indexed at course context.
*
* @var array
*/
protected static $levels = [CONTEXT_COURSE];
/**
* Returns recordset containing required data for indexing
* course custom fields.
*
* @param int $modifiedfrom timestamp
* @param \context|null $context Restriction context
* @return \moodle_recordset|null Recordset or null if no change possible
*/
public function get_document_recordset($modifiedfrom = 0, \context $context = null) {
global $DB;
list ($contextjoin, $contextparams) = $this->get_course_level_context_restriction_sql($context, 'c', SQL_PARAMS_NAMED);
if ($contextjoin === null) {
return null;
}
$fields = course_handler::create()->get_fields();
if (!$fields) {
return null;
}
list($fieldsql, $fieldparam) = $DB->get_in_or_equal(array_keys($fields), SQL_PARAMS_NAMED, 'fld');
// Restrict recordset to CONTEXT_COURSE (since we are implementing it to core_course\search).
$sql = "SELECT d.*
FROM {customfield_data} d
JOIN {course} c ON c.id = d.instanceid
JOIN {context} cnt ON cnt.instanceid = c.id
$contextjoin
WHERE d.timemodified >= :modifiedfrom
AND cnt.contextlevel = :contextlevel
AND d.fieldid $fieldsql
ORDER BY d.timemodified ASC";
return $DB->get_recordset_sql($sql , array_merge($contextparams,
['modifiedfrom' => $modifiedfrom, 'contextlevel' => CONTEXT_COURSE], $fieldparam));
}
/**
* Returns the document associated with this section.
*
* @param \stdClass $record
* @param array $options
* @return \core_search\document|bool
*/
public function get_document($record, $options = array()) {
global $PAGE;
try {
$context = \context_course::instance($record->instanceid);
} catch (\moodle_exception $ex) {
// Notify it as we run here as admin, we should see everything.
debugging('Error retrieving ' . $this->areaid . ' ' . $record->id . ' document, not all required data is available: ' .
$ex->getMessage(), DEBUG_DEVELOPER);
return false;
}
$handler = course_handler::create();
$field = $handler->get_fields()[$record->fieldid];
$data = data_controller::create(0, $record, $field);
// Prepare associative array with data from DB.
$doc = \core_search\document_factory::instance($record->id, $this->componentname, $this->areaname);
$doc->set('title', content_to_text($field->get('name'), false));
$doc->set('content', content_to_text($data->export_value(), FORMAT_HTML));
$doc->set('contextid', $context->id);
$doc->set('courseid', $context->instanceid);
$doc->set('owneruserid', \core_search\manager::NO_OWNER_ID);
$doc->set('modified', $record->timemodified);
// Check if this document should be considered new.
if (isset($options['lastindexedtime']) && ($options['lastindexedtime'] < $record->timecreated)) {
// If the document was created after the last index time, it must be new.
$doc->set_is_new(true);
}
return $doc;
}
/**
* Whether the user can access the document or not.
*
* @param int $id The course instance id.
* @return int
*/
public function check_access($id) {
global $DB;
$course = $DB->get_record('course', array('id' => $id));
if (!$course) {
return \core_search\manager::ACCESS_DELETED;
}
if (can_access_course($course)) {
return \core_search\manager::ACCESS_GRANTED;
}
return \core_search\manager::ACCESS_DENIED;
}
/**
* Link to the course.
*
* @param \core_search\document $doc
* @return \moodle_url
*/
public function get_doc_url(\core_search\document $doc) {
return $this->get_context_url($doc);
}
/**
* Link to the course.
*
* @param \core_search\document $doc
* @return \moodle_url
*/
public function get_context_url(\core_search\document $doc) {
return new \moodle_url('/course/view.php', array('id' => $doc->get('courseid')));
}
/**
* Returns the moodle component name.
*
* It might be the plugin name (whole frankenstyle name) or the core subsystem name.
*
* @return string
*/
public function get_component_name() {
return 'course';
}
/**
* Returns an icon instance for the document.
*
* @param \core_search\document $doc
* @return \core_search\document_icon
*/
public function get_doc_icon(\core_search\document $doc) : \core_search\document_icon {
return new \core_search\document_icon('i/course');
}
}

View File

@ -48,12 +48,18 @@ class course_search_testcase extends advanced_testcase {
*/
protected $sectionareaid = null;
/**
* @var string Area id for custom fields.
*/
protected $customfieldareaid = null;
public function setUp() {
$this->resetAfterTest(true);
set_config('enableglobalsearch', true);
$this->mycoursesareaid = \core_search\manager::generate_areaid('core_course', 'mycourse');
$this->sectionareaid = \core_search\manager::generate_areaid('core_course', 'section');
$this->customfieldareaid = \core_search\manager::generate_areaid('core_course', 'customfield');
// 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();
@ -447,6 +453,90 @@ class course_search_testcase extends advanced_testcase {
$searcharea->check_access($documents[1]->get('itemid')));
}
/**
* Indexing custom fields contents.
*
* @return void
*/
public function test_customfield_indexing() {
// Returns the instance as long as the area is supported.
$searcharea = \core_search\manager::get_search_area($this->customfieldareaid);
$this->assertInstanceOf('\core_course\search\customfield', $searcharea);
// We need to be admin for custom fields creation.
$this->setAdminUser();
// Custom fields.
$fieldcategory = self::getDataGenerator()->create_custom_field_category(['name' => 'Other fields']);
$customfield = ['shortname' => 'test', 'name' => 'Customfield', 'type' => 'text',
'categoryid' => $fieldcategory->get('id')];
$field = self::getDataGenerator()->create_custom_field($customfield);
$course1data = ['customfields' => [['shortname' => $customfield['shortname'], 'value' => 'Customvalue1']]];
$course1 = self::getDataGenerator()->create_course($course1data);
$course2data = ['customfields' => [['shortname' => $customfield['shortname'], 'value' => 'Customvalue2']]];
$course2 = self::getDataGenerator()->create_course($course2data);
// All records.
$recordset = $searcharea->get_recordset_by_timestamp(0);
$this->assertTrue($recordset->valid());
$nrecords = 0;
foreach ($recordset as $record) {
$this->assertInstanceOf('stdClass', $record);
$doc = $searcharea->get_document($record);
$this->assertInstanceOf('\core_search\document', $doc);
$nrecords++;
}
// If there would be an error/failure in the foreach above the recordset would be closed on shutdown.
$recordset->close();
$this->assertEquals(2, $nrecords);
// The +2 is to prevent race conditions.
$recordset = $searcharea->get_recordset_by_timestamp(time() + 2);
// No new records.
$this->assertFalse($recordset->valid());
$recordset->close();
}
/**
* Document contents for custom fields.
*
* @return void
*/
public function test_customfield_document() {
global $DB;
// Returns the instance as long as the area is supported.
$searcharea = \core_search\manager::get_search_area($this->customfieldareaid);
// We need to be admin for custom fields creation.
$this->setAdminUser();
// Custom fields.
$fieldcategory = self::getDataGenerator()->create_custom_field_category(['name' => 'Other fields']);
$customfield = ['shortname' => 'test', 'name' => 'Customfield', 'type' => 'text',
'categoryid' => $fieldcategory->get('id')];
$field = self::getDataGenerator()->create_custom_field($customfield);
$coursedata = ['customfields' => [['shortname' => $customfield['shortname'], 'value' => 'Customvalue1']]];
$course = self::getDataGenerator()->create_course($coursedata);
// Retrieve data we need to compare with document instance.
$record = $DB->get_record('customfield_data', ['instanceid' => $course->id]);
$field = \core_customfield\field_controller::create($record->fieldid);
$data = \core_customfield\data_controller::create(0, $record, $field);
$doc = $searcharea->get_document($record);
$this->assertInstanceOf('\core_search\document', $doc);
$this->assertEquals('Customfield', $doc->get('title'));
$this->assertEquals('Customvalue1', $doc->get('content'));
$this->assertEquals($course->id, $doc->get('courseid'));
$this->assertEquals(\core_search\manager::NO_OWNER_ID, $doc->get('owneruserid'));
$this->assertEquals($course->id, $doc->get('courseid'));
$this->assertFalse($doc->is_set('userid'));
}
/**
* Test document icon for mycourse area.
*/

View File

@ -132,3 +132,4 @@ $string['versiontoolow'] = 'Sorry, global search requires PHP 5.0.0 or later';
$string['viewresultincontext'] = 'View this result in context';
$string['whichmodulestosearch?'] = 'Which modules to search?';
$string['wordsintitle'] = 'Words in title';
$string['search:customfield'] = 'Course custom fields';