MDL-57898 core_course: global search for course custom fields
authorDaniel Neis Araujo <danielneis@gmail.com>
Fri, 11 Jan 2019 09:56:51 +0000 (10:56 +0100)
committerMarina Glancy <marina@moodle.com>
Fri, 18 Jan 2019 13:28:24 +0000 (14:28 +0100)
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.

course/classes/search/customfield.php [new file with mode: 0644]
course/tests/search_test.php
lang/en/search.php

diff --git a/course/classes/search/customfield.php b/course/classes/search/customfield.php
new file mode 100644 (file)
index 0000000..2b6538b
--- /dev/null
@@ -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');
+    }
+}
index ac9d44d..191402c 100644 (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.
      */
index e956098..0e6b600 100644 (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';