MDL-60913 search: add search area categories
authorDmitrii Metelkin <dmitriim@catalyst-au.net>
Tue, 22 Jan 2019 23:28:43 +0000 (10:28 +1100)
committerDmitrii Metelkin <dmitriim@catalyst-au.net>
Tue, 22 Jan 2019 23:28:43 +0000 (10:28 +1100)
23 files changed:
admin/searchareas.php
admin/settings/plugins.php
course/classes/search/mycourse.php
course/classes/search/section.php
course/tests/search_test.php
lang/en/admin.php
lang/en/search.php
message/classes/search/base_message.php
message/tests/search_received_test.php
message/tests/search_sent_test.php
search/classes/area_category.php [new file with mode: 0644]
search/classes/base.php
search/classes/base_block.php
search/classes/base_mod.php
search/classes/manager.php
search/classes/output/form/search.php
search/classes/output/renderer.php
search/index.php
search/tests/area_category_test.php [new file with mode: 0644]
search/tests/base_test.php
search/tests/manager_test.php
user/classes/search/user.php
user/tests/search_test.php

index 911a86b..fb5b0a3 100644 (file)
@@ -153,6 +153,7 @@ $table = new html_table();
 $table->id = 'core-search-areas';
 $table->head = [
     get_string('searcharea', 'search'),
+    get_string('searchareacategories', 'search'),
     get_string('enable'),
     get_string('newestdocindexed', 'admin'),
     get_string('searchlastrun', 'admin'),
@@ -165,6 +166,14 @@ foreach ($searchareas as $area) {
     $areaid = $area->get_area_id();
     $columns = array(new html_table_cell($area->get_visible_name()));
 
+    $areacategories = [];
+    foreach (\core_search\manager::get_search_area_categories() as $category) {
+        if (key_exists($areaid, $category->get_areas())) {
+            $areacategories[] = $category->get_visiblename();
+        }
+    }
+    $columns[] = new html_table_cell(implode(', ', $areacategories));
+
     if ($area->is_enabled()) {
         $columns[] = $OUTPUT->action_icon(admin_searcharea_action_url('disable', $areaid),
             new pix_icon('t/hide', get_string('disable'), 'moodle', array('title' => '', 'class' => 'iconsmall')),
index 9a2202f..94399c7 100644 (file)
@@ -573,6 +573,25 @@ if ($hassiteconfig) {
             new lang_string('searchallavailablecourses_desc', 'admin'),
             0, $options));
 
+    // Search display options.
+    $temp->add(new admin_setting_heading('searchdisplay', new lang_string('searchdisplay', 'admin'), ''));
+    $temp->add(new admin_setting_configcheckbox('searchenablecategories',
+        new lang_string('searchenablecategories', 'admin'),
+        new lang_string('searchenablecategories_desc', 'admin'),
+        0));
+    $options = [];
+    foreach (\core_search\manager::get_search_area_categories() as $category) {
+        $options[$category->get_name()] = $category->get_visiblename();
+    }
+    $temp->add(new admin_setting_configselect('searchdefaultcategory',
+        new lang_string('searchdefaultcategory', 'admin'),
+        new lang_string('searchdefaultcategory_desc', 'admin'),
+        \core_search\manager::SEARCH_AREA_CATEGORY_ALL, $options));
+    $temp->add(new admin_setting_configcheckbox('searchhideallcategory',
+        new lang_string('searchhideallcategory', 'admin'),
+        new lang_string('searchhideallcategory_desc', 'admin'),
+        0));
+
     $ADMIN->add('searchplugins', $temp);
     $ADMIN->add('searchplugins', new admin_externalpage('searchareas', new lang_string('searchareas', 'admin'),
         new moodle_url('/admin/searchareas.php')));
index 9cbdc58..bac0127 100644 (file)
@@ -184,4 +184,13 @@ class mycourse extends \core_search\base {
     public function get_doc_icon(\core_search\document $doc) : \core_search\document_icon {
         return new \core_search\document_icon('i/course');
     }
+
+    /**
+     * Returns a list of category names associated with the area.
+     *
+     * @return array
+     */
+    public function get_category_names() {
+        return [\core_search\manager::SEARCH_AREA_CATEGORY_COURSES];
+    }
 }
index 42badb4..6be829d 100644 (file)
@@ -205,4 +205,13 @@ class section extends \core_search\base {
     public function get_doc_icon(\core_search\document $doc) : \core_search\document_icon {
         return new \core_search\document_icon('i/section');
     }
+
+    /**
+     * Returns a list of category names associated with the area.
+     *
+     * @return array
+     */
+    public function get_category_names() {
+        return [\core_search\manager::SEARCH_AREA_CATEGORY_COURSE_CONTENT];
+    }
 }
index ac9d44d..cc3e9be 100644 (file)
@@ -478,4 +478,15 @@ class course_search_testcase extends advanced_testcase {
         $this->assertEquals('i/section', $result->get_name());
         $this->assertEquals('moodle', $result->get_component());
     }
+
+    /**
+     * Test assigned search categories.
+     */
+    public function test_get_category_names() {
+        $coursessearcharea = \core_search\manager::get_search_area($this->mycoursesareaid);
+        $sectionsearcharea = \core_search\manager::get_search_area($this->sectionareaid);
+
+        $this->assertEquals(['core-courses'], $coursessearcharea->get_category_names());
+        $this->assertEquals(['core-course-content'], $sectionsearcharea->get_category_names());
+    }
 }
index dbb7577..ddb32c6 100644 (file)
@@ -1054,6 +1054,13 @@ $string['searchallavailablecourses'] = 'Searchable courses';
 $string['searchallavailablecourses_off'] = 'Search within enrolled courses only';
 $string['searchallavailablecourses_on'] = 'Search within all courses the user can access';
 $string['searchallavailablecourses_desc'] = 'In some situations the search engine may not work when searching across a large number of courses. Set to search only enrolled courses if you need to restrict the number of courses searched.';
+$string['searchdisplay'] = 'Search results display options';
+$string['searchenablecategories'] = 'Display results in separate categories';
+$string['searchenablecategories_desc'] = 'If enabled, search results will be displayed in separate categories.';
+$string['searchhideallcategory'] = 'Hide All results category';
+$string['searchhideallcategory_desc'] = 'If checked, the category with all results will be hidden on the search result screen.';
+$string['searchdefaultcategory'] = 'Default search category';
+$string['searchdefaultcategory_desc'] = 'Results from the selected search area category will be displayed by default.';
 $string['searchalldeleted'] = 'All indexed contents have been deleted';
 $string['searchareaenabled'] = 'Search area enabled';
 $string['searchareadisabled'] = 'Search area disabled';
index e956098..3623b3f 100644 (file)
@@ -36,6 +36,11 @@ $string['confirm_delete'] = 'Are you sure you want to delete the index for {$a}?
 $string['confirm_indexall'] = 'Are you sure you want to update indexed contents now? If a large amount of content needs indexing, this can take a long time. For live servers, you should normally leave indexing to the \'Global search indexing\' scheduled task.';
 $string['confirm_reindexall'] = 'Are you sure you want to reindex all site contents now? If your site contains a large amount of content, this will take a long time, and users may not get full search results until it completes.';
 $string['confirm_deleteall'] = 'Are you sure you want to delete all indexed contents now? Until the site is indexed again, users will not get search results.';
+$string['core-all'] = 'All';
+$string['core-course-content'] = 'Course content';
+$string['core-courses'] = 'Courses';
+$string['core-users'] = 'Users';
+$string['core-other'] = 'Other';
 $string['createanindex'] = 'create an index';
 $string['createdon'] = 'Created on';
 $string['database'] = 'Database';
@@ -111,6 +116,7 @@ $string['search:mycourse'] = 'My courses';
 $string['search:section'] = 'Course sections';
 $string['search:user'] = 'Users';
 $string['searcharea'] = 'Search area';
+$string['searchareacategories'] = 'Seach area categories';
 $string['searching'] = 'Searching in ...';
 $string['searchnotpermitted'] = 'You are not allowed to do a search';
 $string['searchsetupdescription'] = 'The following steps help you to set up Moodle global search.';
index 5a3912f..a4aa6fe 100644 (file)
@@ -207,4 +207,13 @@ abstract class base_message extends \core_search\base {
     public function get_doc_icon(\core_search\document $doc) : \core_search\document_icon {
         return new \core_search\document_icon('t/message');
     }
+
+    /**
+     * Returns a list of category names associated with the area.
+     *
+     * @return array
+     */
+    public function get_category_names() {
+        return [\core_search\manager::SEARCH_AREA_CATEGORY_USERS];
+    }
 }
index 79b7137..c8f53ac 100644 (file)
@@ -352,4 +352,14 @@ class message_received_search_testcase extends advanced_testcase {
         $this->assertEquals('t/message', $result->get_name());
         $this->assertEquals('moodle', $result->get_component());
     }
+
+    /**
+     * Test assigned search categories.
+     */
+    public function test_get_category_names() {
+        $searcharea = \core_search\manager::get_search_area($this->messagereceivedareaid);
+
+        $expected = ['core-users'];
+        $this->assertEquals($expected, $searcharea->get_category_names());
+    }
 }
index 7edee7d..eb42216 100644 (file)
@@ -367,4 +367,14 @@ class message_sent_search_testcase extends advanced_testcase {
         $this->assertEquals('t/message', $result->get_name());
         $this->assertEquals('moodle', $result->get_component());
     }
+
+    /**
+     * Test assigned search categories.
+     */
+    public function test_get_category_names() {
+        $searcharea = \core_search\manager::get_search_area($this->messagesentareaid);
+
+        $expected = ['core-users'];
+        $this->assertEquals($expected, $searcharea->get_category_names());
+    }
 }
diff --git a/search/classes/area_category.php b/search/classes/area_category.php
new file mode 100644 (file)
index 0000000..975ed61
--- /dev/null
@@ -0,0 +1,126 @@
+<?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 category.
+ *
+ * @package     core_search
+ * @copyright   Dmitrii Metelkin <dmitriim@catalyst-au.net>
+ * @license     http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace core_search;
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Search area category.
+ *
+ * @package     core_search
+ * @copyright   Dmitrii Metelkin <dmitriim@catalyst-au.net>
+ * @license     http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class area_category {
+
+    /**
+     * Category name.
+     * @var string
+     */
+    protected $name;
+
+    /**
+     * Category visible name.
+     * @var string
+     */
+    protected $visiblename;
+
+    /**
+     * Category order.
+     * @var int
+     */
+    protected $order = 0;
+
+    /**
+     * Category areas.
+     * @var \core_search\base[]
+     */
+    protected $areas = [];
+
+    /**
+     * Constructor.
+     *
+     * @param string $name Unique name of the category.
+     * @param string $visiblename Visible name of the category.
+     * @param int $order Category position in the list (smaller numbers will be displayed first).
+     * @param \core_search\base[] $areas A list of search areas associated with this category.
+     */
+    public function __construct(string $name, string $visiblename, int $order = 0, array  $areas = []) {
+        $this->name = $name;
+        $this->visiblename = $visiblename;
+        $this->order = $order;
+        $this->set_areas($areas);
+    }
+
+    /**
+     * Get name.
+     *
+     * @return string
+     */
+    public function get_name() {
+        return $this->name;
+    }
+
+    /**
+     * Get visible name.
+     *
+     * @return string
+     */
+    public function get_visiblename() {
+        return $this->visiblename;
+    }
+
+    /**
+     * Get order to display.
+     *
+     * @return int
+     */
+    public function get_order() {
+        return $this->order;
+    }
+
+    /**
+     * Return a keyed by area id list of areas for this category.
+     *
+     * @return \core_search\base[]
+     */
+    public function get_areas() {
+        return $this->areas;
+    }
+
+    /**
+     * Set list of search areas for this category,
+     *
+     * @param \core_search\base[] $areas
+     */
+    public function set_areas(array $areas) {
+        foreach ($areas as $area) {
+            if ($area instanceof base && !key_exists($area->get_area_id(), $this->areas)) {
+                $this->areas[$area->get_area_id()] = $area;
+            }
+        }
+    }
+
+}
index ff1006f..c231548 100644 (file)
@@ -532,4 +532,13 @@ abstract class base {
     public function get_doc_icon(document $doc) : document_icon {
         return new document_icon('i/empty');
     }
+
+    /**
+     * Returns a list of category names associated with the area.
+     *
+     * @return array
+     */
+    public function get_category_names() {
+        return [manager::SEARCH_AREA_CATEGORY_OTHER];
+    }
 }
index 6f4bdc8..28e7b10 100644 (file)
@@ -408,4 +408,13 @@ abstract class base_block extends base {
     public function get_doc_icon(document $doc) : document_icon {
         return new document_icon('e/anchor');
     }
+
+    /**
+     * Returns a list of category names associated with the area.
+     *
+     * @return array
+     */
+    public function get_category_names() {
+        return [manager::SEARCH_AREA_CATEGORY_COURSE_CONTENT];
+    }
 }
index fcf5af6..ae5e459 100644 (file)
@@ -296,4 +296,13 @@ abstract class base_mod extends base {
     public function get_doc_icon(document $doc) : document_icon {
         return new document_icon('icon', $this->get_module_name());
     }
+
+    /**
+     * Returns a list of category names associated with the area.
+     *
+     * @return array
+     */
+    public function get_category_names() {
+        return [manager::SEARCH_AREA_CATEGORY_COURSE_CONTENT];
+    }
 }
index 007c669..01d3b65 100644 (file)
@@ -97,6 +97,31 @@ class manager {
      */
     const INDEX_PRIORITY_REINDEXING = 50;
 
+    /**
+     * @var string Core search area category for all results.
+     */
+    const SEARCH_AREA_CATEGORY_ALL = 'core-all';
+
+    /**
+     * @var string Core search area category for course content.
+     */
+    const SEARCH_AREA_CATEGORY_COURSE_CONTENT = 'core-course-content';
+
+    /**
+     * @var string Core search area category for courses.
+     */
+    const SEARCH_AREA_CATEGORY_COURSES = 'core-courses';
+
+    /**
+     * @var string Core search area category for users.
+     */
+    const SEARCH_AREA_CATEGORY_USERS = 'core-users';
+
+    /**
+     * @var string Core search area category for results that do not fit into any of existing categories.
+     */
+    const SEARCH_AREA_CATEGORY_OTHER = 'core-other';
+
     /**
      * @var \core_search\base[] Enabled search areas.
      */
@@ -107,6 +132,11 @@ class manager {
      */
     protected static $allsearchareas = null;
 
+    /**
+     * @var \core_search\area_category[] A list of search area categories.
+     */
+    protected static $searchareacategories = null;
+
     /**
      * @var \core_search\manager
      */
@@ -359,6 +389,162 @@ class manager {
         return static::$allsearchareas;
     }
 
+    /**
+     * Return search area category instance by category name.
+     *
+     * @param string $name Category name. If name is not valid will return default category.
+     *
+     * @return \core_search\area_category
+     */
+    public static function get_search_area_category_by_name($name) {
+        if (key_exists($name, self::get_search_area_categories())) {
+            return self::get_search_area_categories()[$name];
+        } else {
+            return self::get_search_area_categories()[self::get_default_area_category_name()];
+        }
+    }
+
+    /**
+     * Return a list of existing search area categories.
+     *
+     * @return \core_search\area_category[]
+     */
+    public static function get_search_area_categories() {
+        if (!isset(static::$searchareacategories)) {
+            $categories = self::get_core_search_area_categories();
+
+            // Go through all existing search areas and get categories they are assigned to.
+            $areacategories = [];
+            foreach (self::get_search_areas_list() as $searcharea) {
+                foreach ($searcharea->get_category_names() as $categoryname) {
+                    if (!key_exists($categoryname, $areacategories)) {
+                        $areacategories[$categoryname] = [];
+                    }
+
+                    $areacategories[$categoryname][] = $searcharea;
+                }
+            }
+
+            // Populate core categories by areas.
+            foreach ($areacategories as $name => $searchareas) {
+                if (key_exists($name, $categories)) {
+                    $categories[$name]->set_areas($searchareas);
+                } else {
+                    throw new \coding_exception('Unknown core search area category ' . $name);
+                }
+            }
+
+            // Get additional categories.
+            $additionalcategories = self::get_additional_search_area_categories();
+            foreach ($additionalcategories as $additionalcategory) {
+                if (!key_exists($additionalcategory->get_name(), $categories)) {
+                    $categories[$additionalcategory->get_name()] = $additionalcategory;
+                }
+            }
+
+            // Remove categories without areas.
+            foreach ($categories as $key => $category) {
+                if (empty($category->get_areas())) {
+                    unset($categories[$key]);
+                }
+            }
+
+            // Sort categories by order.
+            uasort($categories, function($category1, $category2) {
+                return $category1->get_order() > $category2->get_order();
+            });
+
+            static::$searchareacategories = $categories;
+        }
+
+        return static::$searchareacategories;
+    }
+
+    /**
+     * Get list of core search area categories.
+     *
+     * @return \core_search\area_category[]
+     */
+    protected static function get_core_search_area_categories() {
+        $categories = [];
+
+        $categories[self::SEARCH_AREA_CATEGORY_ALL] = new area_category(
+            self::SEARCH_AREA_CATEGORY_ALL,
+            get_string('core-all', 'search'),
+            0,
+            self::get_search_areas_list(true)
+        );
+
+        $categories[self::SEARCH_AREA_CATEGORY_COURSE_CONTENT] = new area_category(
+            self::SEARCH_AREA_CATEGORY_COURSE_CONTENT,
+            get_string('core-course-content', 'search'),
+            1
+        );
+
+        $categories[self::SEARCH_AREA_CATEGORY_COURSES] = new area_category(
+            self::SEARCH_AREA_CATEGORY_COURSES,
+            get_string('core-courses', 'search'),
+            2
+        );
+
+        $categories[self::SEARCH_AREA_CATEGORY_USERS] = new area_category(
+            self::SEARCH_AREA_CATEGORY_USERS,
+            get_string('core-users', 'search'),
+            3
+        );
+
+        $categories[self::SEARCH_AREA_CATEGORY_OTHER] = new area_category(
+            self::SEARCH_AREA_CATEGORY_OTHER,
+            get_string('core-other', 'search'),
+            4
+        );
+
+        return $categories;
+    }
+
+    /**
+     * Gets a list of additional search area categories.
+     *
+     * @return \core_search\area_category[]
+     */
+    protected static function get_additional_search_area_categories() {
+        $additionalcategories = [];
+
+        // Allow plugins to add custom search area categories.
+        if ($pluginsfunction = get_plugins_with_function('search_area_categories')) {
+            foreach ($pluginsfunction as $plugintype => $plugins) {
+                foreach ($plugins as $pluginfunction) {
+                    $plugincategories = $pluginfunction();
+                    // We're expecting a list of valid area categories.
+                    if (is_array($plugincategories)) {
+                        foreach ($plugincategories as $plugincategory) {
+                            if (self::is_valid_area_category($plugincategory)) {
+                                $additionalcategories[] = $plugincategory;
+                            } else {
+                                throw  new \coding_exception('Invalid search area category!');
+                            }
+                        }
+                    } else {
+                        throw  new \coding_exception($pluginfunction . ' should return a list of search area categories!');
+                    }
+                }
+            }
+        }
+
+        return $additionalcategories;
+    }
+
+    /**
+     * Check if provided instance of area category is valid.
+     *
+     * @param mixed $areacategory Area category instance. Potentially could be anything.
+     *
+     * @return bool
+     */
+    protected static function is_valid_area_category($areacategory) {
+        return $areacategory instanceof area_category;
+    }
+
     /**
      * Clears all static caches.
      *
@@ -369,6 +555,7 @@ class manager {
         static::$enabledsearchareas = null;
         static::$allsearchareas = null;
         static::$instance = null;
+        static::$searchareacategories = null;
 
         base_block::clear_static();
         engine::clear_users_cache();
@@ -671,6 +858,19 @@ class manager {
     public function paged_search(\stdClass $formdata, $pagenum) {
         $out = new \stdClass();
 
+        if (self::is_search_area_categories_enabled() && !empty($formdata->cat)) {
+            $cat = self::get_search_area_category_by_name($formdata->cat);
+            if (empty($formdata->areaids)) {
+                $formdata->areaids = array_keys($cat->get_areas());
+            } else {
+                foreach ($formdata->areaids as $key => $areaid) {
+                    if (!key_exists($areaid, $cat->get_areas())) {
+                        unset($formdata->areaids[$key]);
+                    }
+                }
+            }
+        }
+
         $perpage = static::DISPLAY_RESULTS_PER_PAGE;
 
         // Make sure we only allow request up to max page.
@@ -1435,4 +1635,41 @@ class manager {
         }
         return microtime(true);
     }
+
+    /**
+     * Check if search area categories functionality is enabled.
+     *
+     * @return bool
+     */
+    public static function is_search_area_categories_enabled() {
+        return !empty(get_config('core', 'searchenablecategories'));
+    }
+
+    /**
+     * Check if all results category should be hidden.
+     *
+     * @return bool
+     */
+    public static function should_hide_all_results_category() {
+        return get_config('core', 'searchhideallcategory');
+    }
+
+    /**
+     * Returns default search area category name.
+     *
+     * @return string
+     */
+    public static function get_default_area_category_name() {
+        $default = get_config('core', 'searchdefaultcategory');
+
+        if (empty($default)) {
+            $default = self::SEARCH_AREA_CATEGORY_ALL;
+        }
+
+        if ($default == self::SEARCH_AREA_CATEGORY_ALL && self::should_hide_all_results_category()) {
+            $default = self::SEARCH_AREA_CATEGORY_COURSE_CONTENT;
+        }
+
+        return $default;
+    }
 }
index 8d633df..f4c8e45 100644 (file)
@@ -40,6 +40,13 @@ class search extends \moodleform {
         global $USER, $DB, $OUTPUT;
 
         $mform =& $this->_form;
+
+        if (\core_search\manager::is_search_area_categories_enabled() && !empty($this->_customdata['cat'])) {
+            $mform->addElement('hidden', 'cat');
+            $mform->setType('cat', PARAM_NOTAGS);
+            $mform->setDefault('cat', $this->_customdata['cat']);
+        }
+
         $mform->disable_form_change_checker();
         $mform->addElement('header', 'search', get_string('search', 'search'));
 
@@ -72,11 +79,21 @@ class search extends \moodleform {
         $mform->setType('title', PARAM_TEXT);
 
         $search = \core_search\manager::instance(true);
-
-        $searchareas = \core_search\manager::get_search_areas_list(true);
+        $enabledsearchareas = \core_search\manager::get_search_areas_list(true);
         $areanames = array();
-        foreach ($searchareas as $areaid => $searcharea) {
-            $areanames[$areaid] = $searcharea->get_visible_name();
+
+        if (\core_search\manager::is_search_area_categories_enabled() && !empty($this->_customdata['cat'])) {
+            $searchareacategory = \core_search\manager::get_search_area_category_by_name($this->_customdata['cat']);
+            $searchareas = $searchareacategory->get_areas();
+            foreach ($searchareas as $areaid => $searcharea) {
+                if (key_exists($areaid, $enabledsearchareas)) {
+                    $areanames[$areaid] = $searcharea->get_visible_name();
+                }
+            }
+        } else {
+            foreach ($enabledsearchareas as $areaid => $searcharea) {
+                $areanames[$areaid] = $searcharea->get_visible_name();
+            }
         }
 
         // Sort the array by the text.
index 97cbe29..793611d 100644 (file)
@@ -53,13 +53,32 @@ class renderer extends \plugin_renderer_base {
      * @param int $page Zero based page number.
      * @param int $totalcount Total number of results available.
      * @param \moodle_url $url
+     * @param \core_search\area_category|null $cat Selected search are category or null if category functionality is disabled.
      * @return string HTML
      */
-    public function render_results($results, $page, $totalcount, $url) {
+    public function render_results($results, $page, $totalcount, $url, $cat = null) {
+        $content = '';
+
+        if (\core_search\manager::is_search_area_categories_enabled() && !empty($cat)) {
+            $toprow = [];
+            foreach (\core_search\manager::get_search_area_categories() as $category) {
+                $taburl = clone $url;
+                $taburl->param('cat', $category->get_name());
+                $taburl->param('page', 0);
+                $taburl->remove_params(['page', 'areaids']);
+                $toprow[$category->get_name()] = new \tabobject($category->get_name(), $taburl, $category->get_visiblename());
+            }
+
+            if (\core_search\manager::should_hide_all_results_category()) {
+                unset($toprow[\core_search\manager::SEARCH_AREA_CATEGORY_ALL]);
+            }
+
+            $content .= $this->tabtree($toprow, $cat->get_name());
+        }
 
         // Paging bar.
         $perpage = \core_search\manager::DISPLAY_RESULTS_PER_PAGE;
-        $content = $this->output->paging_bar($totalcount, $page, $perpage, $url);
+        $content .= $this->output->paging_bar($totalcount, $page, $perpage, $url);
 
         // Results.
         $resultshtml = array();
index 02f7ea8..ef80dc3 100644 (file)
@@ -28,6 +28,12 @@ $page = optional_param('page', 0, PARAM_INT);
 $q = optional_param('q', '', PARAM_NOTAGS);
 $title = optional_param('title', '', PARAM_NOTAGS);
 $contextid = optional_param('context', 0, PARAM_INT);
+$cat = optional_param('cat', '', PARAM_NOTAGS);
+
+if (\core_search\manager::is_search_area_categories_enabled()) {
+    $cat = \core_search\manager::get_search_area_category_by_name($cat);
+}
+
 // Moving areaids, courseids, timestart, and timeend further down as they might come as an array if they come from the form.
 
 $context = context_system::instance();
@@ -80,6 +86,10 @@ if ($contextid) {
 // Get available ordering options from search engine.
 $customdata['orderoptions'] = $search->get_engine()->get_supported_orders($context);
 
+if ($cat instanceof \core_search\area_category) {
+    $customdata['cat'] = $cat->get_name();
+}
+
 $mform = new \core_search\output\form\search(null, $customdata);
 
 $data = $mform->get_data();
@@ -125,6 +135,10 @@ if (!empty($context) && $data) {
     $data->context = $context;
 }
 
+if ($data && $cat instanceof \core_search\area_category) {
+    $data->cat = $cat->get_name();
+}
+
 // Set the page URL.
 $urlparams = array('page' => $page);
 if ($data) {
@@ -139,6 +153,11 @@ if ($data) {
     $urlparams['timestart'] = $data->timestart;
     $urlparams['timeend'] = $data->timeend;
 }
+
+if ($cat instanceof \core_search\area_category) {
+    $urlparams['cat'] = $cat->get_name();
+}
+
 $url = new moodle_url('/search/index.php', $urlparams);
 $PAGE->set_url($url);
 
@@ -160,7 +179,7 @@ if ($errorstr = $search->get_engine()->get_query_error()) {
 $mform->display();
 
 if (!empty($results)) {
-    echo $searchrenderer->render_results($results->results, $results->actualpage, $results->totalcount, $url);
+    echo $searchrenderer->render_results($results->results, $results->actualpage, $results->totalcount, $url, $cat);
 
     \core_search\manager::trigger_search_results_viewed([
         'q' => $data->q,
diff --git a/search/tests/area_category_test.php b/search/tests/area_category_test.php
new file mode 100644 (file)
index 0000000..9aa2562
--- /dev/null
@@ -0,0 +1,120 @@
+<?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/>.
+
+/**
+ * Area category unit tests.
+ *
+ * @package    core_search
+ * @copyright  2018 Dmitrii Metelkin <dmitriim@catalyst-au.net>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Area category unit tests.
+ *
+ * @package    core_search
+ * @copyright  2018 Dmitrii Metelkin <dmitriim@catalyst-au.net>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class search_area_category_testcase extends advanced_testcase {
+
+    /**
+     * A helper function to get a mocked search area.
+     * @param string $areaid
+     *
+     * @return \PHPUnit\Framework\MockObject\MockObject
+     */
+    protected function get_mocked_area($areaid) {
+        $builder = $this->getMockBuilder('\core_search\base');
+        $builder->disableOriginalConstructor();
+        $builder->setMethods(array('get_area_id'));
+        $area = $builder->getMockForAbstractClass();
+        $area->method('get_area_id')->willReturn($areaid);
+
+        return $area;
+    }
+
+    /**
+     * A helper function to get a list of search areas.
+     *
+     * @return array
+     */
+    protected function get_areas() {
+        $areas = [];
+        $areas[] = $this->get_mocked_area('area1');
+        $areas[] = 'String';
+        $areas[] = 1;
+        $areas[] = '12';
+        $areas[] = true;
+        $areas[] = false;
+        $areas[] = null;
+        $areas[] = [$this->get_mocked_area('area2')];
+        $areas[] = $this;
+        $areas[] = new stdClass();
+        $areas[] = $this->get_mocked_area('area3');
+        $areas[] = $this->get_mocked_area('area4');
+
+        return $areas;
+    }
+
+    /**
+     * Test default values.
+     */
+    public function test_default_values() {
+        $category = new \core_search\area_category('test_name', 'test_visiblename');
+
+        $this->assertEquals('test_name', $category->get_name());
+        $this->assertEquals('test_visiblename', $category->get_visiblename());
+        $this->assertEquals(0, $category->get_order());
+        $this->assertEquals([], $category->get_areas());
+    }
+
+    /**
+     * Test that all get functions work as expected.
+     */
+    public function test_getters() {
+        $category = new \core_search\area_category('test_name', 'test_visiblename', 4, $this->get_areas());
+
+        $this->assertEquals('test_name', $category->get_name());
+        $this->assertEquals('test_visiblename', $category->get_visiblename());
+        $this->assertEquals(4, $category->get_order());
+
+        $this->assertTrue(is_array($category->get_areas()));
+        $this->assertCount(3, $category->get_areas());
+        $this->assertTrue(key_exists('area1', $category->get_areas()));
+        $this->assertTrue(key_exists('area3', $category->get_areas()));
+        $this->assertTrue(key_exists('area4', $category->get_areas()));
+    }
+
+    /**
+     * Test that a list of areas could be set correctly.
+     */
+    public function test_list_of_areas_could_be_set() {
+        $category = new \core_search\area_category('test_name', 'test_visiblename');
+        $this->assertEquals([], $category->get_areas());
+
+        $category->set_areas($this->get_areas());
+
+        $this->assertTrue(is_array($category->get_areas()));
+        $this->assertCount(3, $category->get_areas());
+        $this->assertTrue(key_exists('area1', $category->get_areas()));
+        $this->assertTrue(key_exists('area3', $category->get_areas()));
+        $this->assertTrue(key_exists('area4', $category->get_areas()));
+    }
+
+}
index 0d7cb18..dcce079 100644 (file)
@@ -157,4 +157,16 @@ class search_base_testcase extends advanced_testcase {
         $this->assertEquals('i/empty', $result->get_name());
         $this->assertEquals('moodle', $result->get_component());
     }
+
+    /**
+     * Test base search area category names.
+     */
+    public function test_get_category_names() {
+        $builder = $this->getMockBuilder('\core_search\base');
+        $builder->disableOriginalConstructor();
+        $stub = $builder->getMockForAbstractClass();
+
+        $expected = ['core-course-content'];
+        $this->assertEquals($expected, $stub->get_category_names());
+    }
 }
index 124e04a..d978eff 100644 (file)
@@ -1210,4 +1210,83 @@ class search_manager_testcase extends advanced_testcase {
         // Confirm request table is now empty.
         $this->assertEquals(0, $DB->count_records('search_index_requests'));
     }
+
+    /**
+     * Test search area categories.
+     */
+    public function test_get_search_area_categories() {
+        $categories = \core_search\manager::get_search_area_categories();
+
+        $this->assertTrue(is_array($categories));
+        $this->assertTrue(count($categories) >= 4); // We always should have 4 core categories.
+        $this->assertArrayHasKey('core-all', $categories);
+        $this->assertArrayHasKey('core-course-content', $categories);
+        $this->assertArrayHasKey('core-courses', $categories);
+        $this->assertArrayHasKey('core-users', $categories);
+
+        foreach ($categories as $category) {
+            $this->assertInstanceOf('\core_search\area_category', $category);
+        }
+    }
+
+    /**
+     * Test that we can find out if search area categories functionality is enabled.
+     */
+    public function test_is_search_area_categories_enabled() {
+        $this->resetAfterTest();
+
+        $this->assertFalse(\core_search\manager::is_search_area_categories_enabled());
+        set_config('searchenablecategories', 1);
+        $this->assertTrue(\core_search\manager::is_search_area_categories_enabled());
+        set_config('searchenablecategories', 0);
+        $this->assertFalse(\core_search\manager::is_search_area_categories_enabled());
+    }
+
+    /**
+     * Test that we can find out if hiding all results category is enabled.
+     */
+    public function test_should_hide_all_results_category() {
+        $this->resetAfterTest();
+
+        $this->assertEquals(0, \core_search\manager::should_hide_all_results_category());
+        set_config('searchhideallcategory', 1);
+        $this->assertEquals(1, \core_search\manager::should_hide_all_results_category());
+        set_config('searchhideallcategory', 0);
+        $this->assertEquals(0, \core_search\manager::should_hide_all_results_category());
+    }
+
+    /**
+     * Test that we can get default search category name.
+     */
+    public function test_get_default_area_category_name() {
+        $this->resetAfterTest();
+
+        $expected = 'core-all';
+        $this->assertEquals($expected, \core_search\manager::get_default_area_category_name());
+
+        set_config('searchhideallcategory', 1);
+        $expected = 'core-course-content';
+        $this->assertEquals($expected, \core_search\manager::get_default_area_category_name());
+
+        set_config('searchhideallcategory', 0);
+        $expected = 'core-all';
+        $this->assertEquals($expected, \core_search\manager::get_default_area_category_name());
+    }
+
+    /**
+     * Test that we can get correct search area category by its name.
+     */
+    public function test_get_search_area_category_by_name() {
+        $this->resetAfterTest();
+
+        $testcategory = \core_search\manager::get_search_area_category_by_name('test_random_name');
+        $this->assertEquals('core-all', $testcategory->get_name());
+
+        $testcategory = \core_search\manager::get_search_area_category_by_name('core-courses');
+        $this->assertEquals('core-courses', $testcategory->get_name());
+
+        set_config('searchhideallcategory', 1);
+        $testcategory = \core_search\manager::get_search_area_category_by_name('test_random_name');
+        $this->assertEquals('core-course-content', $testcategory->get_name());
+    }
 }
index 479b4e0..03f034a 100644 (file)
@@ -216,4 +216,13 @@ class user extends \core_search\base {
         return new \core_search\document_icon('i/user');
     }
 
+    /**
+     * Returns a list of category names associated with the area.
+     *
+     * @return array
+     */
+    public function get_category_names() {
+        return [\core_search\manager::SEARCH_AREA_CATEGORY_USERS];
+    }
+
 }
index 030145f..710681d 100644 (file)
@@ -229,4 +229,14 @@ class user_search_testcase extends advanced_testcase {
         $this->assertEquals('i/user', $result->get_name());
         $this->assertEquals('moodle', $result->get_component());
     }
+
+    /**
+     * Test assigned search categories.
+     */
+    public function test_get_category_names() {
+        $searcharea = \core_search\manager::get_search_area($this->userareaid);
+
+        $expected = ['core-users'];
+        $this->assertEquals($expected, $searcharea->get_category_names());
+    }
 }