MDL-53613 search: Add collaborative wiki pages to global search
authorEric Merrill <merrill@oakland.edu>
Tue, 5 Apr 2016 02:21:29 +0000 (22:21 -0400)
committerEric Merrill <merrill@oakland.edu>
Thu, 7 Apr 2016 18:56:00 +0000 (14:56 -0400)
mod/wiki/classes/search/collaborative_page.php [new file with mode: 0644]
mod/wiki/lang/en/wiki.php
mod/wiki/tests/search_test.php [new file with mode: 0644]

diff --git a/mod/wiki/classes/search/collaborative_page.php b/mod/wiki/classes/search/collaborative_page.php
new file mode 100644 (file)
index 0000000..8ed300e
--- /dev/null
@@ -0,0 +1,179 @@
+<?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 mod_wiki collaborative pages.
+ *
+ * @package    mod_wiki
+ * @copyright  2016 Eric Merrill {@link http://www.merrilldigital.com}
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace mod_wiki\search;
+
+defined('MOODLE_INTERNAL') || die();
+
+require_once($CFG->dirroot . '/mod/wiki/locallib.php');
+
+/**
+ * Search area for mod_wiki collaborative pages.
+ *
+ * @package    mod_wiki
+ * @copyright  2016 Eric Merrill {@link http://www.merrilldigital.com}
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class collaborative_page extends \core_search\area\base_mod {
+    /**
+     * @var array Cache of wiki records.
+     */
+    protected $wikiscache = array();
+
+    /**
+     * Returns a recordset with all required page information.
+     *
+     * @param int $modifiedfrom
+     * @return moodle_recordset
+     */
+    public function get_recordset_by_timestamp($modifiedfrom = 0) {
+        global $DB;
+
+        $sql = 'SELECT p.*, w.id AS wikiid, w.course AS courseid
+                  FROM {wiki_pages} p
+                  JOIN {wiki_subwikis} s ON s.id = p.subwikiid
+                  JOIN {wiki} w ON w.id = s.wikiid
+                 WHERE p.timemodified >= ?
+                   AND w.wikimode = ?
+              ORDER BY p.timemodified ASC';
+        return $DB->get_recordset_sql($sql, array($modifiedfrom, 'collaborative'));
+    }
+
+    /**
+     * Returns the document for a particular page.
+     *
+     * @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
+     */
+    public function get_document($record, $options = array()) {
+        try {
+            $cm = $this->get_cm('wiki', $record->wikiid, $record->courseid);
+            $context = \context_module::instance($cm->id);
+        } catch (\dml_missing_record_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;
+        } catch (\dml_exception $ex) {
+            // Notify it as we run here as admin, we should see everything.
+            debugging('Error retrieving ' . $this->areaid . ' ' . $record->id . ' document: ' . $ex->getMessage(), DEBUG_DEVELOPER);
+            return false;
+        }
+
+        // Make a page object without extra fields.
+        $page = clone $record;
+        unset($page->courseid);
+        unset($page->wikiid);
+
+        // Conversion based wiki_print_page_content().
+        // Check if we have passed the cache time.
+        if ($page->timerendered + WIKI_REFRESH_CACHE_TIME < time()) {
+            $content = wiki_refresh_cachedcontent($page);
+            $page = $content['page'];
+        }
+        // Convert to HTML, then to text. Makes sure content is cleaned.
+        $html = format_text($page->cachedcontent, FORMAT_MOODLE, array('overflowdiv' => true, 'allowid' => true));
+        $content = content_to_text($page->cachedcontent, FORMAT_HTML);
+
+        // Prepare associative array with data from DB.
+        $doc = \core_search\document_factory::instance($record->id, $this->componentname, $this->areaname);
+        $doc->set('title', $record->title);
+        $doc->set('content', $content);
+        $doc->set('contextid', $context->id);
+        $doc->set('courseid', $record->courseid);
+        $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;
+    }
+
+    /**
+     * Can the current user see the document.
+     *
+     * @param int $id The internal search area entity id.
+     * @return bool True if the user can see it, false otherwise
+     */
+    public function check_access($id) {
+        global $DB;
+
+        try {
+            $page = $DB->get_record('wiki_pages', array('id' => $id), '*', MUST_EXIST);
+            if (!isset($this->wikiscache[$page->subwikiid])) {
+                $sql = 'SELECT w.*
+                          FROM {wiki_subwikis} s
+                          JOIN {wiki} w ON w.id = s.wikiid
+                         WHERE s.id = ?';
+                $this->wikiscache[$page->subwikiid] = $DB->get_record_sql($sql, array('id' => $page->subwikiid), MUST_EXIST);
+            }
+            $wiki = $this->wikiscache[$page->subwikiid];
+            $cminfo = $this->get_cm('wiki', $wiki->id, $wiki->course);
+        } catch (\dml_missing_record_exception $ex) {
+            return \core_search\manager::ACCESS_DELETED;
+        } catch (\dml_exception $ex) {
+            return \core_search\manager::ACCESS_DENIED;
+        }
+
+        // Recheck uservisible although it should have already been checked in core_search.
+        if ($cminfo->uservisible === false) {
+            return \core_search\manager::ACCESS_DENIED;
+        }
+
+        $context = \context_module::instance($cminfo->id);
+
+        if (!has_capability('mod/wiki:viewpage', $context)) {
+            return \core_search\manager::ACCESS_DENIED;
+        }
+
+        return \core_search\manager::ACCESS_GRANTED;
+    }
+
+    /**
+     * Returns a url to the page.
+     *
+     * @param \core_search\document $doc
+     * @return \moodle_url
+     */
+    public function get_doc_url(\core_search\document $doc) {
+        $params = array('pageid' => $doc->get('itemid'));
+        return new \moodle_url('/mod/wiki/view.php', $params);
+    }
+
+    /**
+     * Returns a url to the wiki.
+     *
+     * @param \core_search\document $doc
+     * @return \moodle_url
+     */
+    public function get_context_url(\core_search\document $doc) {
+        $contextmodule = \context::instance_by_id($doc->get('contextid'));
+        return new \moodle_url('/mod/wiki/view.php', array('id' => $contextmodule->instanceid));
+    }
+}
index 65bf37f..3b9b707 100644 (file)
@@ -214,6 +214,7 @@ $string['save'] = 'Save';
 $string['saving'] = 'Saving wiki page';
 $string['savingerror'] = 'Saving error';
 $string['search:activity'] = 'Wiki activities';
+$string['search:collaborative_page'] = 'Wiki Pages - Collaborative';
 $string['searchcontent'] = 'Search in page content';
 $string['searchresult'] = 'Search results:';
 $string['searchterms'] = 'Search terms';
diff --git a/mod/wiki/tests/search_test.php b/mod/wiki/tests/search_test.php
new file mode 100644 (file)
index 0000000..424348c
--- /dev/null
@@ -0,0 +1,160 @@
+<?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/>.
+
+/**
+ * Wiki global search unit tests.
+ *
+ * @package     mod_wiki
+ * @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 wiki global search.
+ *
+ * @package     mod_wiki
+ * @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_wiki_search_testcase extends advanced_testcase {
+
+    /**
+     * @var string Area id
+     */
+    protected $wikicollabpageareaid = null;
+
+    public function setUp() {
+        $this->resetAfterTest(true);
+        $this->setAdminUser();
+        set_config('enableglobalsearch', true);
+
+        $this->wikicollabpageareaid = \core_search\manager::generate_areaid('mod_wiki', 'collaborative_page');
+
+        // 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();
+    }
+
+    /**
+     * Availability.
+     *
+     * @return void
+     */
+    public function test_search_enabled() {
+        $searcharea = \core_search\manager::get_search_area($this->wikicollabpageareaid);
+        list($componentname, $varname) = $searcharea->get_config_var_name();
+
+        // Enabled by default once global search is enabled.
+        $this->assertTrue($searcharea->is_enabled());
+
+        set_config($varname . '_enabled', false, $componentname);
+        $this->assertFalse($searcharea->is_enabled());
+
+        set_config($varname . '_enabled', true, $componentname);
+        $this->assertTrue($searcharea->is_enabled());
+    }
+
+    /**
+     * Indexing collaborative page contents.
+     *
+     * @return void
+     */
+    public function test_collaborative_page_indexing() {
+        global $DB;
+
+        // Returns the instance as long as the area is supported.
+        $searcharea = \core_search\manager::get_search_area($this->wikicollabpageareaid);
+        $this->assertInstanceOf('\mod_wiki\search\collaborative_page', $searcharea);
+
+        $wikigenerator = $this->getDataGenerator()->get_plugin_generator('mod_wiki');
+        $course1 = self::getDataGenerator()->create_course();
+
+        $collabwiki = $this->getDataGenerator()->create_module('wiki', array('course' => $course1->id));
+        $cpage1 = $wikigenerator->create_first_page($collabwiki);
+        $cpage2 = $wikigenerator->create_content($collabwiki);
+        $cpage3 = $wikigenerator->create_content($collabwiki);
+
+        $indwiki = $this->getDataGenerator()->create_module('wiki', array('course' => $course1->id, 'wikimode' => 'individual'));
+        $ipage1 = $wikigenerator->create_first_page($indwiki);
+        $ipage2 = $wikigenerator->create_content($indwiki);
+        $ipage3 = $wikigenerator->create_content($indwiki);
+
+        // 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);
+
+            // Static caches are working.
+            $dbreads = $DB->perf_get_reads();
+            $doc = $searcharea->get_document($record);
+            $this->assertEquals($dbreads, $DB->perf_get_reads());
+            $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();
+
+        // We expect 3 (not 6) pages.
+        $this->assertEquals(3, $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();
+    }
+
+    /**
+     * Check collaborative_page check access.
+     *
+     * @return void
+     */
+    public function test_collaborative_page_check_access() {
+        global $DB;
+
+        // Returns the instance as long as the area is supported.
+        $searcharea = \core_search\manager::get_search_area($this->wikicollabpageareaid);
+        $this->assertInstanceOf('\mod_wiki\search\collaborative_page', $searcharea);
+
+        $user1 = self::getDataGenerator()->create_user();
+        $course1 = self::getDataGenerator()->create_course();
+        $this->getDataGenerator()->enrol_user($user1->id, $course1->id, 'student');
+
+        $wikigenerator = $this->getDataGenerator()->get_plugin_generator('mod_wiki');
+
+        $collabwiki = $this->getDataGenerator()->create_module('wiki', array('course' => $course1->id));
+        $cpage1 = $wikigenerator->create_first_page($collabwiki);
+
+        $this->setAdminUser();
+        $this->assertEquals(\core_search\manager::ACCESS_GRANTED, $searcharea->check_access($cpage1->id));
+
+        $this->setUser($user1);
+        $this->assertEquals(\core_search\manager::ACCESS_GRANTED, $searcharea->check_access($cpage1->id));
+
+        $this->assertEquals(\core_search\manager::ACCESS_DELETED, $searcharea->check_access($cpage1->id + 10));
+    }
+}