From 501a170cb63397df3475a7a3734813375b91dc61 Mon Sep 17 00:00:00 2001 From: Paul Holden Date: Wed, 2 Aug 2023 12:26:54 +0100 Subject: [PATCH] MDL-78879 reportbuilder: allow for negation of category filter. Add "Equal to" and "Not equal to" operators to the filter class. AMOS BEGIN CPY [subcats,qtype_randomsamatch],[includesubcategories,moodle] AMOS END --- .../reportbuilder/datasource/courses_test.php | 6 ++++ lang/en/moodle.php | 1 + .../classes/local/filters/category.php | 36 +++++++++++++++++-- .../tests/local/filters/category_test.php | 35 ++++++++++++++---- 4 files changed, 69 insertions(+), 9 deletions(-) diff --git a/course/tests/reportbuilder/datasource/courses_test.php b/course/tests/reportbuilder/datasource/courses_test.php index a089db49117..07eccce09e6 100644 --- a/course/tests/reportbuilder/datasource/courses_test.php +++ b/course/tests/reportbuilder/datasource/courses_test.php @@ -22,6 +22,7 @@ use context_course; use core_reportbuilder_testcase; use core_reportbuilder_generator; use core_reportbuilder\local\filters\boolean_select; +use core_reportbuilder\local\filters\category; use core_reportbuilder\local\filters\date; use core_reportbuilder\local\filters\select; use core_reportbuilder\local\filters\tags; @@ -219,6 +220,11 @@ class courses_test extends core_reportbuilder_testcase { return [ // Category. 'Filter category' => ['course_category:name', [ + 'course_category:name_operator' => category::NOT_EQUAL_TO, + 'course_category:name_value' => -1, + ], true], + 'Filter category (no match)' => ['course_category:name', [ + 'course_category:name_operator' => category::EQUAL_TO, 'course_category:name_value' => -1, ], false], 'Filter category name' => ['course_category:text', [ diff --git a/lang/en/moodle.php b/lang/en/moodle.php index 7e70bb7dd4b..10c821f5876 100644 --- a/lang/en/moodle.php +++ b/lang/en/moodle.php @@ -1118,6 +1118,7 @@ $string['includeneededusers'] = 'Include needed users'; $string['includenoneusers'] = 'Include no users'; $string['includeroleassignments'] = 'Include role assignments'; $string['includesitefiles'] = 'Include site files used in this course'; +$string['includesubcategories'] = 'Include subcategories'; $string['includeuserfiles'] = 'Include user files'; $string['increasesections'] = 'Increase the number of sections'; $string['indicator:accessesafterend'] = 'Course accessed after end date'; diff --git a/reportbuilder/classes/local/filters/category.php b/reportbuilder/classes/local/filters/category.php index 578cfc4ca5e..8f1012c8a14 100644 --- a/reportbuilder/classes/local/filters/category.php +++ b/reportbuilder/classes/local/filters/category.php @@ -19,6 +19,7 @@ declare(strict_types=1); namespace core_reportbuilder\local\filters; use core_course_category; +use lang_string; use MoodleQuickForm; use core_reportbuilder\local\helpers\database; @@ -36,20 +37,43 @@ use core_reportbuilder\local\helpers\database; */ class category extends base { + /** @var int Category is equal to */ + public const EQUAL_TO = 0; + + /** @var int Category is not equal to */ + public const NOT_EQUAL_TO = 1; + + /** + * Returns an array of comparison operators + * + * @return array + */ + private function get_operators(): array { + $operators = [ + self::EQUAL_TO => new lang_string('filterisequalto', 'core_reportbuilder'), + self::NOT_EQUAL_TO => new lang_string('filterisnotequalto', 'core_reportbuilder'), + ]; + + return $this->filter->restrict_limited_operators($operators); + } + /** * Setup form * * @param MoodleQuickForm $mform */ public function setup_form(MoodleQuickForm $mform): void { - $label = get_string('filterfieldvalue', 'core_reportbuilder', $this->get_header()); + $operatorlabel = get_string('filterfieldoperator', 'core_reportbuilder', $this->get_header()); + $mform->addElement('select', "{$this->name}_operator", $operatorlabel, $this->get_operators()) + ->setHiddenLabel(true); // See MDL-74627: in order to set the default value to "No selection" we need to prepend an empty value. $requiredcapabilities = $this->filter->get_options()['requiredcapabilities'] ?? ''; $categories = [0 => ''] + core_course_category::make_categories_list($requiredcapabilities); - $mform->addElement('autocomplete', "{$this->name}_value", $label, $categories)->setHiddenLabel(true); - $mform->addElement('advcheckbox', "{$this->name}_subcategories", get_string('viewallsubcategories')); + $valuelabel = get_string('filterfieldvalue', 'core_reportbuilder', $this->get_header()); + $mform->addElement('autocomplete', "{$this->name}_value", $valuelabel, $categories)->setHiddenLabel(true); + $mform->addElement('advcheckbox', "{$this->name}_subcategories", get_string('includesubcategories')); } /** @@ -63,6 +87,7 @@ class category extends base { [$fieldsql, $params] = $this->filter->get_field_sql_and_params(); + $operator = (int) ($values["{$this->name}_operator"] ?? self::EQUAL_TO); $category = (int) ($values["{$this->name}_value"] ?? 0); $subcategories = !empty($values["{$this->name}_subcategories"]); @@ -92,6 +117,11 @@ class category extends base { )"; } + // If specified "Not equal to", then negate the entire clause. + if ($operator === self::NOT_EQUAL_TO) { + $sql = "NOT ({$sql})"; + } + return [$sql, $params]; } diff --git a/reportbuilder/tests/local/filters/category_test.php b/reportbuilder/tests/local/filters/category_test.php index 06e60ad8b09..5b57b5edfa1 100644 --- a/reportbuilder/tests/local/filters/category_test.php +++ b/reportbuilder/tests/local/filters/category_test.php @@ -40,11 +40,20 @@ class category_test extends advanced_testcase { */ public function get_sql_filter_provider(): array { return [ - ['One', false, ['One']], - ['One', true, ['One', 'Two', 'Three']], - ['Two', true, ['Two', 'Three']], - ['Three', true, ['Three']], - [null, false, ['Category 1', 'One', 'Two', 'Three']], + // Equal to. + ['One', category::EQUAL_TO, false, ['One']], + ['One', category::EQUAL_TO, true, ['One', 'Two', 'Three']], + ['Two', category::EQUAL_TO, true, ['Two', 'Three']], + ['Three', category::EQUAL_TO, true, ['Three']], + + // Not equal to. + ['One', category::NOT_EQUAL_TO, false, ['Category 1', 'Two', 'Three', 'Four', 'Five', 'Six']], + ['One', category::NOT_EQUAL_TO, true, ['Category 1', 'Four', 'Five', 'Six']], + ['Two', category::NOT_EQUAL_TO, true, ['Category 1', 'One', 'Four', 'Five', 'Six']], + ['Three', category::NOT_EQUAL_TO, true, ['Category 1', 'One', 'Two', 'Four', 'Five', 'Six']], + + // Default/empty state. + [null, category::EQUAL_TO, false, ['Category 1', 'One', 'Two', 'Three', 'Four', 'Five', 'Six']], ]; } @@ -52,20 +61,33 @@ class category_test extends advanced_testcase { * Test getting filter SQL * * @param string|null $categoryname + * @param int $operator * @param bool $subcategories * @param string[] $expectedcategories * * @dataProvider get_sql_filter_provider */ - public function test_get_sql_filter(?string $categoryname, bool $subcategories, array $expectedcategories): void { + public function test_get_sql_filter( + ?string $categoryname, + int $operator, + bool $subcategories, + array $expectedcategories, + ): void { + global $DB; $this->resetAfterTest(); + // Create category tree "One -> Two -> Three". $category1 = $this->getDataGenerator()->create_category(['name' => 'One']); $category2 = $this->getDataGenerator()->create_category(['name' => 'Two', 'parent' => $category1->id]); $category3 = $this->getDataGenerator()->create_category(['name' => 'Three', 'parent' => $category2->id]); + // Second category tree "Four -> Five -> Six". + $category4 = $this->getDataGenerator()->create_category(['name' => 'Four']); + $category5 = $this->getDataGenerator()->create_category(['name' => 'Five', 'parent' => $category4->id]); + $category6 = $this->getDataGenerator()->create_category(['name' => 'Six', 'parent' => $category5->id]); + if ($categoryname !== null) { $categoryid = $DB->get_field('course_categories', 'id', ['name' => $categoryname], MUST_EXIST); } else { @@ -82,6 +104,7 @@ class category_test extends advanced_testcase { // Create instance of our filter, passing given operator. [$select, $params] = category::create($filter)->get_sql_filter([ + $filter->get_unique_identifier() . '_operator' => $operator, $filter->get_unique_identifier() . '_value' => $categoryid, $filter->get_unique_identifier() . '_subcategories' => $subcategories, ]);