From b6cecffa627ed0ec6f7680321ee3f91e6c0f8c01 Mon Sep 17 00:00:00 2001 From: Mark Johnson Date: Tue, 3 Dec 2024 14:45:44 +0000 Subject: [PATCH 1/3] Add question bank filter API guide This functionality was added in MDL-72321. MDL-78220 will add several new filters, so this guide may expand as the children of that tracker are developed. --- docs/apis/plugintypes/qbank/filters.md | 244 +++++++++++++++++++++++++ docs/apis/plugintypes/qbank/index.md | 1 + 2 files changed, 245 insertions(+) create mode 100644 docs/apis/plugintypes/qbank/filters.md diff --git a/docs/apis/plugintypes/qbank/filters.md b/docs/apis/plugintypes/qbank/filters.md new file mode 100644 index 0000000000..3f80d21772 --- /dev/null +++ b/docs/apis/plugintypes/qbank/filters.md @@ -0,0 +1,244 @@ +--- +title: Question bank filters +tags: + - Plugins + - Question + - qbank +description: Question bank plugins allow you to define new filters for the question bank view and random question sets. +documentationDraft: true +--- + + + +Question bank plugins allow you define additional filters. These can be used when viewing the question bank, and are included +in the URL so that a filtered view of the question bank can be shared. They are also used when defining the criteria for adding +random questions to a quiz. + +## Creating a new filter condition + +A filter condition consists of two parts - the backend "condition" PHP class, and the frontend "filter" JavaScript class. + +The "condition" class defines the general properties of the filter - its name, various options, and how it is applied to the +question bank query. +The "filter" class defines how the filter is displayed in the UI, and how values selected in the UI are passed back to the condition. + +Each new filter condition must define a new "condition" class in the qbank plugin based on `core_question\local\bank\condition`. +By default this will use the `core/datafilter/filtertype` "filter" class, although this can be overridden too if required. + +### Basic example + +This outlines the bare minimum required to implement a new filter condition. This will give you a field that allows you to +enter keywords and add them to a list of selected search terms, the filter the questions by that list of terms. +This assumes that you already have the basic framework of a qbank plugin in place. For real-world examples, +look for classes that extend `core_question\local\bank\condition`. + +Create a `condition` class within your plugin's namespace. For a plugin called `qbank_myplugin` this would look something like: + +```php title="question/bank/myplugin/classes/myfilter_condition.php" +namespace qbank_myplugin; + +use core_question\local\bank\condition; + +class myfilter_condition extends condition { + +} +``` + +Modify your `plugin_feature` class to return an instance of your condition from the `get_question_filters()` method: + +```php title="question/bank/myplugin/classes/plugin_feature.php" +namespace qbank_myplugin; + +class plugin_feature extends core_question\local\bank\plugin_features_base { + public function get_question_filters(?core_question\local\bank\view $qbank = null): array { + return [ + new myfilter_condition($qbank), + ]; + } +} +``` + +Back in your `condition` class, define the `get_name()` method, which returns the label displayed in the filter UI. + +```php title="Define the condition name" +public function get_name(): string { + return get_string('myfilter_name', 'myplugin'); +} +``` + +Define `get_condition_key()`, which returns a unique machine-readable ID for this filter condition, used when passing the filter +as a parameter. + +```php title="Define the condition key" +public function get_condition_key(): string { + return 'myfilter'; +} +``` + +To actually filter the results, define `build_query_from_filter()` which returns an SQL `WHERE` condition, and an array of parameters. +The `$filter` parameter receives an array with a `'values'` key, containing an array of the selected values, and a `'jointype'` key, +containing one of the `JOINTTYPE_ANY`, `JOINTYPE_ALL` or `JOINTYPE_NONE` constants. Use these to build your condition as required. + +The conditions from each filter are combined with the query in +[`core_question\local\bank\view::build_query()`](https://github.com/moodle/moodle/blob/c741492c38b9945abbfc7e90dfe8f943279f8265/question/classes/local/bank/view.php#L733) +to select the filtered question list. + +```php title="Filter questions" +public function build_query_from_filter(array $filter): array { + $andor = ' AND '; + $equal = '='; + if ($filter['jointype'] === self::JOINTYPE_ANY) { + $andor = ' OR '; + } else if ($filter['jointype'] === self::JOINTYPE_NONE) { + $equal = '!='; + } + $conditions = []; + $params = []; + // In real life we'd probably use $DB->get_in_or_equal here. + foreach ($filter['values'] as $key => $value) { + $conditions[] = 'q.fieldname ' . $equal . ' :myfilter' . $key; + $params['myfilter' . $key] = $value; + } + return [ + '(' . implode($andor, $conditions) . ')', + $params, + ]; +} +``` + +Following this pattern with your own fields and options will give you a basic functional filter. Most filters will require +more complex functionality, which can be achieved through additional methods. + +### Additional options + +#### Pre-defined values + +To define the list of possible filter values, define `get_initial_values()`, which returns an array of `['value', 'title']` for each +option. These will then be searchable and selectable in the autocomplete field. + +```php title="Define initial filter values" +public function get_initial_values(): string { + return [ + [ + 'value' => 0, + 'title' => 'Option 1', + ], + [ + 'value' => 1, + 'title' => 'Option 2', + ] + ]; +} +``` + +#### Restrict custom keywords + +To restrict the possible filter terms to only those returned from `get_initial_values()`, define `allow_custom()` and have it return `false`. + +```php title="Disable custom terms" +public function allow_custom(): bool { + return false; +} +``` + +#### Restrict join types + +Not all join types are relevant to all filters. If each question will only match one of the selected values, it does not make +sense to allow `JOINTYPE_ALL`. Define `get_join_list()` and return an array of the applicable join types. + +```php title="Define a restricted list of join types" +public function get_join_list(): array { + return [ + datafilter::JOINTYPE_ANY, + datafilter::JOINTYPE_NONE, + ]; +} +``` + +#### Allow multiple values? + +By default, conditions allow multiple values to be selected and use the selected join type to decide how they are applied. +If your condition should only allow a single value at a time, override `allow_multiple()` to return false. + +```php title="Disable selection of multiple values" +public function allow_multiple(): bool { + return false; +} +``` + +#### Allow empty values? + +By default, conditions can be left empty, and therefore will not be included in the filter. To make it compulsory to select a +value for this condition when it is added, override `allow_empty()` to return false. + +```php title="Disable empty values" +public function allow_empty(): bool { + return false; +} +``` + +#### Is the condition required? + +If it is compulsory that your condition is always displayed, override `is_required()` to return true. + +```php title="Make the condition compulsory" +public function is_required(): bool { + return true; +} +``` + +#### Custom filter class + +By default, the filter will be displayed and processed using the `core/datafilter/filtertype` JavaScript class. +This will provide a single autocomplete field for selecting one or multiple numeric IDs with textual labels. +If this does not fit your filter's use case, you can tell your condition to use a different filter class. + +You can either use a different core filter type from `/lib/amd/src/datafilter/filtertypes`, or define your own. + +To tell your filter condition to use a different filter class, override the `get_filter_class()` method to return the namespaced +path to your JavaScript class. + +```php title="Override the default filter class" +public function get_filter_class(): string { + return 'qbank_myplugin/datafilter/filtertype/myfilter'; +} +``` + +To create your own filter class, a new JavaScript file in your plugin under `amd/src/datafilter/filtertypes/myfilter.js`. +In this file, export a default class that extends `core/datafilter/filtertype` +(or another core filter type from `/lib/amd/src/datafilter/filtertypes`) and override the base methods as required. +For example, if your filter uses textual rather than numeric values, you can override `get values()` to return the raw values +without running `parseInt()` (see +[`qbank_viewquestiontype/datafilter/filtertypes/type`](https://github.com/moodle/moodle/blob/main/mod/quiz/tests/behat/editing_add_from_question_bank.feature)). + +If you want a different UI for selecting your filter values instead of a single autocomplete, you can override `addValueSelector()`. +This also provides flexibility over how the values provided by `get_initial_values()` are used by the UI. + +#### Filter options + +If your condition supports additional options as to how the selected values are applied to the query, such as whether child +categories are included when parent categories are selected, you can define "Filter options". + +In your condition class, define `get_filteroptions()` which returns an object containing the current filter options. You will +probably want to add some code to the constructor to read in the current filter options, and some code the `build_query_from_filter()` +to use the option. See +[`qbank_managecategories\category_condition`](https://github.com/moodle/moodle/blob/main/question/bank/managecategories/classes/category_condition.php) +as an example. + +You JavaScript filter class will also need to support your filter options. Override the constructor an add additional code +for the UI required to set your filter options, and override `get filterOptions()` to return the current value for any options set +in this UI. See +[`qbank_managecategories/datafilter/filtertypes/categories`](https://github.com/moodle/moodle/blob/main/question/bank/managecategories/amd/src/datafilter/filtertypes/categories.js) +as an example. + +#### Context-sensitive configuration + +You may want your filter to behave differently depending on where it is being displayed. In this case you can override the +constructor which receives the current `$qbank` view object, and extract some data that is used later on by your other methods. + +For example, the +[tag condition](https://github.com/moodle/moodle/blob/main/question/bank/tagquestion/classes/tag_condition.php) +will find the context of the current page, and use that to control which tags are available in the filter. diff --git a/docs/apis/plugintypes/qbank/index.md b/docs/apis/plugintypes/qbank/index.md index 655b570ee6..6b0a61ecda 100644 --- a/docs/apis/plugintypes/qbank/index.md +++ b/docs/apis/plugintypes/qbank/index.md @@ -22,5 +22,6 @@ Question bank plugins can extend the question bank in many ways, including: - Bulk actions - Navigation node (tabs) - Question preview additions (via callback) +- [Question filters](./filters.md) The place to start implementing most of these is with a class `classes/plugin_features.php` in your plugin, that declares which features you want to add to the question bank. Until more documentation is written, looking at the examples of the plugins in Moodle core should give you a good idea what you need to do. From feac6c1443686eb974ebc5653c369b18e0a7fc2b Mon Sep 17 00:00:00 2001 From: Mark Johnson Date: Thu, 9 Jan 2025 14:30:29 +0000 Subject: [PATCH 2/3] MDL-83862 Add question bank filter validation documentation --- docs/apis/plugintypes/qbank/filters.md | 89 +++++++++++--------------- 1 file changed, 39 insertions(+), 50 deletions(-) diff --git a/docs/apis/plugintypes/qbank/filters.md b/docs/apis/plugintypes/qbank/filters.md index 3f80d21772..a0c70e409c 100644 --- a/docs/apis/plugintypes/qbank/filters.md +++ b/docs/apis/plugintypes/qbank/filters.md @@ -9,20 +9,17 @@ documentationDraft: true --- -Question bank plugins allow you define additional filters. These can be used when viewing the question bank, and are included -in the URL so that a filtered view of the question bank can be shared. They are also used when defining the criteria for adding -random questions to a quiz. +Question bank plugins allow you define additional filters. These can be used when viewing the question bank, and are included in the URL so that a filtered view of the question bank can be shared. They are also used when defining the criteria for adding random questions to a quiz. ## Creating a new filter condition A filter condition consists of two parts - the backend "condition" PHP class, and the frontend "filter" JavaScript class. -The "condition" class defines the general properties of the filter - its name, various options, and how it is applied to the -question bank query. +The "condition" class defines the general properties of the filter - its name, various options, and how it is applied to the question bank query. The "filter" class defines how the filter is displayed in the UI, and how values selected in the UI are passed back to the condition. Each new filter condition must define a new "condition" class in the qbank plugin based on `core_question\local\bank\condition`. @@ -30,10 +27,8 @@ By default this will use the `core/datafilter/filtertype` "filter" class, althou ### Basic example -This outlines the bare minimum required to implement a new filter condition. This will give you a field that allows you to -enter keywords and add them to a list of selected search terms, the filter the questions by that list of terms. -This assumes that you already have the basic framework of a qbank plugin in place. For real-world examples, -look for classes that extend `core_question\local\bank\condition`. +This outlines the bare minimum required to implement a new filter condition. This will give you a field that allows you to enter keywords and add them to a list of selected search terms, the filter the questions by that list of terms. +This assumes that you already have the basic framework of a qbank plugin in place. For real-world examples, look for classes that extend `core_question\local\bank\condition`. Create a `condition` class within your plugin's namespace. For a plugin called `qbank_myplugin` this would look something like: @@ -69,8 +64,7 @@ public function get_name(): string { } ``` -Define `get_condition_key()`, which returns a unique machine-readable ID for this filter condition, used when passing the filter -as a parameter. +Define `get_condition_key()`, which returns a unique machine-readable ID for this filter condition, used when passing the filter as a parameter. ```php title="Define the condition key" public function get_condition_key(): string { @@ -79,12 +73,9 @@ public function get_condition_key(): string { ``` To actually filter the results, define `build_query_from_filter()` which returns an SQL `WHERE` condition, and an array of parameters. -The `$filter` parameter receives an array with a `'values'` key, containing an array of the selected values, and a `'jointype'` key, -containing one of the `JOINTTYPE_ANY`, `JOINTYPE_ALL` or `JOINTYPE_NONE` constants. Use these to build your condition as required. +The `$filter` parameter receives an array with a `'values'` key, containing an array of the selected values, and a `'jointype'` key, containing one of the `JOINTTYPE_ANY`, `JOINTYPE_ALL` or `JOINTYPE_NONE` constants. Use these to build your condition as required. -The conditions from each filter are combined with the query in -[`core_question\local\bank\view::build_query()`](https://github.com/moodle/moodle/blob/c741492c38b9945abbfc7e90dfe8f943279f8265/question/classes/local/bank/view.php#L733) -to select the filtered question list. +The conditions from each filter are combined with the query in [`core_question\local\bank\view::build_query()`](https://github.com/moodle/moodle/blob/c741492c38b9945abbfc7e90dfe8f943279f8265/question/classes/local/bank/view.php#L733) ```php title="Filter questions" public function build_query_from_filter(array $filter): array { @@ -109,15 +100,13 @@ public function build_query_from_filter(array $filter): array { } ``` -Following this pattern with your own fields and options will give you a basic functional filter. Most filters will require -more complex functionality, which can be achieved through additional methods. +Following this pattern with your own fields and options will give you a basic functional filter. Most filters will require more complex functionality, which can be achieved through additional methods. ### Additional options #### Pre-defined values -To define the list of possible filter values, define `get_initial_values()`, which returns an array of `['value', 'title']` for each -option. These will then be searchable and selectable in the autocomplete field. +To define the list of possible filter values, define `get_initial_values()`, which returns an array of `['value', 'title']` for each option. These will then be searchable and selectable in the autocomplete field. ```php title="Define initial filter values" public function get_initial_values(): string { @@ -146,8 +135,7 @@ public function allow_custom(): bool { #### Restrict join types -Not all join types are relevant to all filters. If each question will only match one of the selected values, it does not make -sense to allow `JOINTYPE_ALL`. Define `get_join_list()` and return an array of the applicable join types. +Not all join types are relevant to all filters. If each question will only match one of the selected values, it does not make sense to allow `JOINTYPE_ALL`. Define `get_join_list()` and return an array of the applicable join types. ```php title="Define a restricted list of join types" public function get_join_list(): array { @@ -171,8 +159,7 @@ public function allow_multiple(): bool { #### Allow empty values? -By default, conditions can be left empty, and therefore will not be included in the filter. To make it compulsory to select a -value for this condition when it is added, override `allow_empty()` to return false. +By default, conditions can be left empty, and therefore will not be included in the filter. To make it compulsory to select a value for this condition when it is added, override `allow_empty()` to return false. ```php title="Disable empty values" public function allow_empty(): bool { @@ -198,8 +185,7 @@ If this does not fit your filter's use case, you can tell your condition to use You can either use a different core filter type from `/lib/amd/src/datafilter/filtertypes`, or define your own. -To tell your filter condition to use a different filter class, override the `get_filter_class()` method to return the namespaced -path to your JavaScript class. +To tell your filter condition to use a different filter class, override the `get_filter_class()` method to return the namespaced path to your JavaScript class. ```php title="Override the default filter class" public function get_filter_class(): string { @@ -208,37 +194,40 @@ public function get_filter_class(): string { ``` To create your own filter class, a new JavaScript file in your plugin under `amd/src/datafilter/filtertypes/myfilter.js`. -In this file, export a default class that extends `core/datafilter/filtertype` -(or another core filter type from `/lib/amd/src/datafilter/filtertypes`) and override the base methods as required. -For example, if your filter uses textual rather than numeric values, you can override `get values()` to return the raw values -without running `parseInt()` (see -[`qbank_viewquestiontype/datafilter/filtertypes/type`](https://github.com/moodle/moodle/blob/main/mod/quiz/tests/behat/editing_add_from_question_bank.feature)). +In this file, export a default class that extends `core/datafilter/filtertype` (or another core filter type from `/lib/amd/src/datafilter/filtertypes`) and override the base methods as required. +For example, if your filter uses textual rather than numeric values, you can override `get values()` to return the raw values without running `parseInt()` (see [`qbank_viewquestiontype/datafilter/filtertypes/type`](https://github.com/moodle/moodle/blob/main/mod/quiz/tests/behat/editing_add_from_question_bank.feature)). If you want a different UI for selecting your filter values instead of a single autocomplete, you can override `addValueSelector()`. This also provides flexibility over how the values provided by `get_initial_values()` are used by the UI. #### Filter options -If your condition supports additional options as to how the selected values are applied to the query, such as whether child -categories are included when parent categories are selected, you can define "Filter options". +If your condition supports additional options as to how the selected values are applied to the query, such as whether child categories are included when parent categories are selected, you can define "Filter options". -In your condition class, define `get_filteroptions()` which returns an object containing the current filter options. You will -probably want to add some code to the constructor to read in the current filter options, and some code the `build_query_from_filter()` -to use the option. See -[`qbank_managecategories\category_condition`](https://github.com/moodle/moodle/blob/main/question/bank/managecategories/classes/category_condition.php) -as an example. +In your condition class, define `get_filteroptions()` which returns an object containing the current filter options. You will probably want to add some code to the constructor to read in the current filter options, and some code the `build_query_from_filter()` to use the option. +See [`qbank_managecategories\category_condition`](https://github.com/moodle/moodle/blob/main/question/bank/managecategories/classes/category_condition.php) as an example. -You JavaScript filter class will also need to support your filter options. Override the constructor an add additional code -for the UI required to set your filter options, and override `get filterOptions()` to return the current value for any options set -in this UI. See -[`qbank_managecategories/datafilter/filtertypes/categories`](https://github.com/moodle/moodle/blob/main/question/bank/managecategories/amd/src/datafilter/filtertypes/categories.js) -as an example. +You JavaScript filter class will also need to support your filter options. Override the constructor an add additional code for the UI required to set your filter options, and override `get filterOptions()` to return the current value for any options set in this UI. +See [`qbank_managecategories/datafilter/filtertypes/categories`](https://github.com/moodle/moodle/blob/main/question/bank/managecategories/amd/src/datafilter/filtertypes/categories.js) as an example. #### Context-sensitive configuration -You may want your filter to behave differently depending on where it is being displayed. In this case you can override the -constructor which receives the current `$qbank` view object, and extract some data that is used later on by your other methods. +You may want your filter to behave differently depending on where it is being displayed. In this case you can override the constructor which receives the current `$qbank` view object, and extract some data that is used later on by your other methods. -For example, the -[tag condition](https://github.com/moodle/moodle/blob/main/question/bank/tagquestion/classes/tag_condition.php) -will find the context of the current page, and use that to control which tags are available in the filter. +For example, the [tag condition](https://github.com/moodle/moodle/blob/main/question/bank/tagquestion/classes/tag_condition.php) will find the context of the current page, and use that to control which tags are available in the filter. + +#### Validation + + + +Filters support standard [Client-side form validation](https://developer.mozilla.org/en-US/docs/Learn_web_development/Extensions/Forms/Form_validation). +The simplest way to implement this is to set validation properties on your inputs in the mustache template used by your `addValueSelector()` method. + +If you need something more advanced, you can define a `validation()` method in your filter class. This is called when the "Apply filters" button is clicked, giving you the opportunity to inspect the current values of the filter, and perform validation checks. +If validation fails, you should display errors using the standard `setCustomValidity()` and `reportValidity()` methods on your filter's input elements, and return `false`. +See `core/datafilter/filtertypes/datetime` for an example. + +This client-side validation is only to prevent invalid values being entered in the UI. You should also validate data received by the `build_query_from_filter()` method in your condition class, and throw exceptions in the event of validation failures. From 24264ad81a6e6244ee8b0295e080b1b09f108146 Mon Sep 17 00:00:00 2001 From: Andrew Nicols Date: Fri, 24 Jan 2025 09:11:17 +0800 Subject: [PATCH 3/3] [docs] Backport quesiton bank filter docs to 4.3/4.4/4.5 --- .../apis/plugintypes/qbank/filters.md | 217 ++++++++++++++++++ .../apis/plugintypes/qbank/index.md | 10 +- .../apis/plugintypes/qbank/filters.md | 217 ++++++++++++++++++ .../apis/plugintypes/qbank/index.md | 1 + .../apis/plugintypes/qbank/filters.md | 217 ++++++++++++++++++ .../apis/plugintypes/qbank/index.md | 1 + 6 files changed, 659 insertions(+), 4 deletions(-) create mode 100644 versioned_docs/version-4.3/apis/plugintypes/qbank/filters.md create mode 100644 versioned_docs/version-4.4/apis/plugintypes/qbank/filters.md create mode 100644 versioned_docs/version-4.5/apis/plugintypes/qbank/filters.md diff --git a/versioned_docs/version-4.3/apis/plugintypes/qbank/filters.md b/versioned_docs/version-4.3/apis/plugintypes/qbank/filters.md new file mode 100644 index 0000000000..5ae4a6f85d --- /dev/null +++ b/versioned_docs/version-4.3/apis/plugintypes/qbank/filters.md @@ -0,0 +1,217 @@ +--- +title: Question bank filters +tags: + - Plugins + - Question + - qbank +description: Question bank plugins allow you to define new filters for the question bank view and random question sets. +documentationDraft: true +--- + + + +Question bank plugins allow you define additional filters. These can be used when viewing the question bank, and are included in the URL so that a filtered view of the question bank can be shared. They are also used when defining the criteria for adding random questions to a quiz. + +## Creating a new filter condition + +A filter condition consists of two parts - the backend "condition" PHP class, and the frontend "filter" JavaScript class. + +The "condition" class defines the general properties of the filter - its name, various options, and how it is applied to the question bank query. +The "filter" class defines how the filter is displayed in the UI, and how values selected in the UI are passed back to the condition. + +Each new filter condition must define a new "condition" class in the qbank plugin based on `core_question\local\bank\condition`. +By default this will use the `core/datafilter/filtertype` "filter" class, although this can be overridden too if required. + +### Basic example + +This outlines the bare minimum required to implement a new filter condition. This will give you a field that allows you to enter keywords and add them to a list of selected search terms, the filter the questions by that list of terms. +This assumes that you already have the basic framework of a qbank plugin in place. For real-world examples, look for classes that extend `core_question\local\bank\condition`. + +Create a `condition` class within your plugin's namespace. For a plugin called `qbank_myplugin` this would look something like: + +```php title="question/bank/myplugin/classes/myfilter_condition.php" +namespace qbank_myplugin; + +use core_question\local\bank\condition; + +class myfilter_condition extends condition { + +} +``` + +Modify your `plugin_feature` class to return an instance of your condition from the `get_question_filters()` method: + +```php title="question/bank/myplugin/classes/plugin_feature.php" +namespace qbank_myplugin; + +class plugin_feature extends core_question\local\bank\plugin_features_base { + public function get_question_filters(?core_question\local\bank\view $qbank = null): array { + return [ + new myfilter_condition($qbank), + ]; + } +} +``` + +Back in your `condition` class, define the `get_name()` method, which returns the label displayed in the filter UI. + +```php title="Define the condition name" +public function get_name(): string { + return get_string('myfilter_name', 'myplugin'); +} +``` + +Define `get_condition_key()`, which returns a unique machine-readable ID for this filter condition, used when passing the filter as a parameter. + +```php title="Define the condition key" +public function get_condition_key(): string { + return 'myfilter'; +} +``` + +To actually filter the results, define `build_query_from_filter()` which returns an SQL `WHERE` condition, and an array of parameters. +The `$filter` parameter receives an array with a `'values'` key, containing an array of the selected values, and a `'jointype'` key, containing one of the `JOINTTYPE_ANY`, `JOINTYPE_ALL` or `JOINTYPE_NONE` constants. Use these to build your condition as required. + +The conditions from each filter are combined with the query in [`core_question\local\bank\view::build_query()`](https://github.com/moodle/moodle/blob/c741492c38b9945abbfc7e90dfe8f943279f8265/question/classes/local/bank/view.php#L733) + +```php title="Filter questions" +public function build_query_from_filter(array $filter): array { + $andor = ' AND '; + $equal = '='; + if ($filter['jointype'] === self::JOINTYPE_ANY) { + $andor = ' OR '; + } else if ($filter['jointype'] === self::JOINTYPE_NONE) { + $equal = '!='; + } + $conditions = []; + $params = []; + // In real life we'd probably use $DB->get_in_or_equal here. + foreach ($filter['values'] as $key => $value) { + $conditions[] = 'q.fieldname ' . $equal . ' :myfilter' . $key; + $params['myfilter' . $key] = $value; + } + return [ + '(' . implode($andor, $conditions) . ')', + $params, + ]; +} +``` + +Following this pattern with your own fields and options will give you a basic functional filter. Most filters will require more complex functionality, which can be achieved through additional methods. + +### Additional options + +#### Pre-defined values + +To define the list of possible filter values, define `get_initial_values()`, which returns an array of `['value', 'title']` for each option. These will then be searchable and selectable in the autocomplete field. + +```php title="Define initial filter values" +public function get_initial_values(): string { + return [ + [ + 'value' => 0, + 'title' => 'Option 1', + ], + [ + 'value' => 1, + 'title' => 'Option 2', + ] + ]; +} +``` + +#### Restrict custom keywords + +To restrict the possible filter terms to only those returned from `get_initial_values()`, define `allow_custom()` and have it return `false`. + +```php title="Disable custom terms" +public function allow_custom(): bool { + return false; +} +``` + +#### Restrict join types + +Not all join types are relevant to all filters. If each question will only match one of the selected values, it does not make sense to allow `JOINTYPE_ALL`. Define `get_join_list()` and return an array of the applicable join types. + +```php title="Define a restricted list of join types" +public function get_join_list(): array { + return [ + datafilter::JOINTYPE_ANY, + datafilter::JOINTYPE_NONE, + ]; +} +``` + +#### Allow multiple values? + +By default, conditions allow multiple values to be selected and use the selected join type to decide how they are applied. +If your condition should only allow a single value at a time, override `allow_multiple()` to return false. + +```php title="Disable selection of multiple values" +public function allow_multiple(): bool { + return false; +} +``` + +#### Allow empty values? + +By default, conditions can be left empty, and therefore will not be included in the filter. To make it compulsory to select a value for this condition when it is added, override `allow_empty()` to return false. + +```php title="Disable empty values" +public function allow_empty(): bool { + return false; +} +``` + +#### Is the condition required? + +If it is compulsory that your condition is always displayed, override `is_required()` to return true. + +```php title="Make the condition compulsory" +public function is_required(): bool { + return true; +} +``` + +#### Custom filter class + +By default, the filter will be displayed and processed using the `core/datafilter/filtertype` JavaScript class. +This will provide a single autocomplete field for selecting one or multiple numeric IDs with textual labels. +If this does not fit your filter's use case, you can tell your condition to use a different filter class. + +You can either use a different core filter type from `/lib/amd/src/datafilter/filtertypes`, or define your own. + +To tell your filter condition to use a different filter class, override the `get_filter_class()` method to return the namespaced path to your JavaScript class. + +```php title="Override the default filter class" +public function get_filter_class(): string { + return 'qbank_myplugin/datafilter/filtertype/myfilter'; +} +``` + +To create your own filter class, a new JavaScript file in your plugin under `amd/src/datafilter/filtertypes/myfilter.js`. +In this file, export a default class that extends `core/datafilter/filtertype` (or another core filter type from `/lib/amd/src/datafilter/filtertypes`) and override the base methods as required. +For example, if your filter uses textual rather than numeric values, you can override `get values()` to return the raw values without running `parseInt()` (see [`qbank_viewquestiontype/datafilter/filtertypes/type`](https://github.com/moodle/moodle/blob/main/mod/quiz/tests/behat/editing_add_from_question_bank.feature)). + +If you want a different UI for selecting your filter values instead of a single autocomplete, you can override `addValueSelector()`. +This also provides flexibility over how the values provided by `get_initial_values()` are used by the UI. + +#### Filter options + +If your condition supports additional options as to how the selected values are applied to the query, such as whether child categories are included when parent categories are selected, you can define "Filter options". + +In your condition class, define `get_filteroptions()` which returns an object containing the current filter options. You will probably want to add some code to the constructor to read in the current filter options, and some code the `build_query_from_filter()` to use the option. +See [`qbank_managecategories\category_condition`](https://github.com/moodle/moodle/blob/main/question/bank/managecategories/classes/category_condition.php) as an example. + +You JavaScript filter class will also need to support your filter options. Override the constructor an add additional code for the UI required to set your filter options, and override `get filterOptions()` to return the current value for any options set in this UI. +See [`qbank_managecategories/datafilter/filtertypes/categories`](https://github.com/moodle/moodle/blob/main/question/bank/managecategories/amd/src/datafilter/filtertypes/categories.js) as an example. + +#### Context-sensitive configuration + +You may want your filter to behave differently depending on where it is being displayed. In this case you can override the constructor which receives the current `$qbank` view object, and extract some data that is used later on by your other methods. + +For example, the [tag condition](https://github.com/moodle/moodle/blob/main/question/bank/tagquestion/classes/tag_condition.php) will find the context of the current page, and use that to control which tags are available in the filter. diff --git a/versioned_docs/version-4.3/apis/plugintypes/qbank/index.md b/versioned_docs/version-4.3/apis/plugintypes/qbank/index.md index 88a0198db2..d81b0756a3 100644 --- a/versioned_docs/version-4.3/apis/plugintypes/qbank/index.md +++ b/versioned_docs/version-4.3/apis/plugintypes/qbank/index.md @@ -1,11 +1,10 @@ --- -title: Question Bank plugins +title: Question bank plugins tags: - Plugins - Question - qbank - - Quiz -description: Question type plugins allow you to extend the functionality of the Moodle Question bank. +description: Question bank plugins allow you to extend the functionality of the Moodle Question bank. documentationDraft: true --- @@ -14,10 +13,13 @@ documentationDraft: true issueNumber="MDL-70329" /> -Question type plugins allow you to extend the functionality of the Moodle Question bank, and support features including: +Question bank plugins allow you to extend the functionality of the Moodle Question bank. They just one of the plugin types used by core_question. To see how they fit in, please read [this overview of the question subsystems](../subsystems/question/). + +Question bank plugins can extend the question bank in many ways, including: - Table columns - Action menu items - Bulk actions - Navigation node (tabs) - Question preview additions (via callback) +- [Question filters](./filters.md) diff --git a/versioned_docs/version-4.4/apis/plugintypes/qbank/filters.md b/versioned_docs/version-4.4/apis/plugintypes/qbank/filters.md new file mode 100644 index 0000000000..5ae4a6f85d --- /dev/null +++ b/versioned_docs/version-4.4/apis/plugintypes/qbank/filters.md @@ -0,0 +1,217 @@ +--- +title: Question bank filters +tags: + - Plugins + - Question + - qbank +description: Question bank plugins allow you to define new filters for the question bank view and random question sets. +documentationDraft: true +--- + + + +Question bank plugins allow you define additional filters. These can be used when viewing the question bank, and are included in the URL so that a filtered view of the question bank can be shared. They are also used when defining the criteria for adding random questions to a quiz. + +## Creating a new filter condition + +A filter condition consists of two parts - the backend "condition" PHP class, and the frontend "filter" JavaScript class. + +The "condition" class defines the general properties of the filter - its name, various options, and how it is applied to the question bank query. +The "filter" class defines how the filter is displayed in the UI, and how values selected in the UI are passed back to the condition. + +Each new filter condition must define a new "condition" class in the qbank plugin based on `core_question\local\bank\condition`. +By default this will use the `core/datafilter/filtertype` "filter" class, although this can be overridden too if required. + +### Basic example + +This outlines the bare minimum required to implement a new filter condition. This will give you a field that allows you to enter keywords and add them to a list of selected search terms, the filter the questions by that list of terms. +This assumes that you already have the basic framework of a qbank plugin in place. For real-world examples, look for classes that extend `core_question\local\bank\condition`. + +Create a `condition` class within your plugin's namespace. For a plugin called `qbank_myplugin` this would look something like: + +```php title="question/bank/myplugin/classes/myfilter_condition.php" +namespace qbank_myplugin; + +use core_question\local\bank\condition; + +class myfilter_condition extends condition { + +} +``` + +Modify your `plugin_feature` class to return an instance of your condition from the `get_question_filters()` method: + +```php title="question/bank/myplugin/classes/plugin_feature.php" +namespace qbank_myplugin; + +class plugin_feature extends core_question\local\bank\plugin_features_base { + public function get_question_filters(?core_question\local\bank\view $qbank = null): array { + return [ + new myfilter_condition($qbank), + ]; + } +} +``` + +Back in your `condition` class, define the `get_name()` method, which returns the label displayed in the filter UI. + +```php title="Define the condition name" +public function get_name(): string { + return get_string('myfilter_name', 'myplugin'); +} +``` + +Define `get_condition_key()`, which returns a unique machine-readable ID for this filter condition, used when passing the filter as a parameter. + +```php title="Define the condition key" +public function get_condition_key(): string { + return 'myfilter'; +} +``` + +To actually filter the results, define `build_query_from_filter()` which returns an SQL `WHERE` condition, and an array of parameters. +The `$filter` parameter receives an array with a `'values'` key, containing an array of the selected values, and a `'jointype'` key, containing one of the `JOINTTYPE_ANY`, `JOINTYPE_ALL` or `JOINTYPE_NONE` constants. Use these to build your condition as required. + +The conditions from each filter are combined with the query in [`core_question\local\bank\view::build_query()`](https://github.com/moodle/moodle/blob/c741492c38b9945abbfc7e90dfe8f943279f8265/question/classes/local/bank/view.php#L733) + +```php title="Filter questions" +public function build_query_from_filter(array $filter): array { + $andor = ' AND '; + $equal = '='; + if ($filter['jointype'] === self::JOINTYPE_ANY) { + $andor = ' OR '; + } else if ($filter['jointype'] === self::JOINTYPE_NONE) { + $equal = '!='; + } + $conditions = []; + $params = []; + // In real life we'd probably use $DB->get_in_or_equal here. + foreach ($filter['values'] as $key => $value) { + $conditions[] = 'q.fieldname ' . $equal . ' :myfilter' . $key; + $params['myfilter' . $key] = $value; + } + return [ + '(' . implode($andor, $conditions) . ')', + $params, + ]; +} +``` + +Following this pattern with your own fields and options will give you a basic functional filter. Most filters will require more complex functionality, which can be achieved through additional methods. + +### Additional options + +#### Pre-defined values + +To define the list of possible filter values, define `get_initial_values()`, which returns an array of `['value', 'title']` for each option. These will then be searchable and selectable in the autocomplete field. + +```php title="Define initial filter values" +public function get_initial_values(): string { + return [ + [ + 'value' => 0, + 'title' => 'Option 1', + ], + [ + 'value' => 1, + 'title' => 'Option 2', + ] + ]; +} +``` + +#### Restrict custom keywords + +To restrict the possible filter terms to only those returned from `get_initial_values()`, define `allow_custom()` and have it return `false`. + +```php title="Disable custom terms" +public function allow_custom(): bool { + return false; +} +``` + +#### Restrict join types + +Not all join types are relevant to all filters. If each question will only match one of the selected values, it does not make sense to allow `JOINTYPE_ALL`. Define `get_join_list()` and return an array of the applicable join types. + +```php title="Define a restricted list of join types" +public function get_join_list(): array { + return [ + datafilter::JOINTYPE_ANY, + datafilter::JOINTYPE_NONE, + ]; +} +``` + +#### Allow multiple values? + +By default, conditions allow multiple values to be selected and use the selected join type to decide how they are applied. +If your condition should only allow a single value at a time, override `allow_multiple()` to return false. + +```php title="Disable selection of multiple values" +public function allow_multiple(): bool { + return false; +} +``` + +#### Allow empty values? + +By default, conditions can be left empty, and therefore will not be included in the filter. To make it compulsory to select a value for this condition when it is added, override `allow_empty()` to return false. + +```php title="Disable empty values" +public function allow_empty(): bool { + return false; +} +``` + +#### Is the condition required? + +If it is compulsory that your condition is always displayed, override `is_required()` to return true. + +```php title="Make the condition compulsory" +public function is_required(): bool { + return true; +} +``` + +#### Custom filter class + +By default, the filter will be displayed and processed using the `core/datafilter/filtertype` JavaScript class. +This will provide a single autocomplete field for selecting one or multiple numeric IDs with textual labels. +If this does not fit your filter's use case, you can tell your condition to use a different filter class. + +You can either use a different core filter type from `/lib/amd/src/datafilter/filtertypes`, or define your own. + +To tell your filter condition to use a different filter class, override the `get_filter_class()` method to return the namespaced path to your JavaScript class. + +```php title="Override the default filter class" +public function get_filter_class(): string { + return 'qbank_myplugin/datafilter/filtertype/myfilter'; +} +``` + +To create your own filter class, a new JavaScript file in your plugin under `amd/src/datafilter/filtertypes/myfilter.js`. +In this file, export a default class that extends `core/datafilter/filtertype` (or another core filter type from `/lib/amd/src/datafilter/filtertypes`) and override the base methods as required. +For example, if your filter uses textual rather than numeric values, you can override `get values()` to return the raw values without running `parseInt()` (see [`qbank_viewquestiontype/datafilter/filtertypes/type`](https://github.com/moodle/moodle/blob/main/mod/quiz/tests/behat/editing_add_from_question_bank.feature)). + +If you want a different UI for selecting your filter values instead of a single autocomplete, you can override `addValueSelector()`. +This also provides flexibility over how the values provided by `get_initial_values()` are used by the UI. + +#### Filter options + +If your condition supports additional options as to how the selected values are applied to the query, such as whether child categories are included when parent categories are selected, you can define "Filter options". + +In your condition class, define `get_filteroptions()` which returns an object containing the current filter options. You will probably want to add some code to the constructor to read in the current filter options, and some code the `build_query_from_filter()` to use the option. +See [`qbank_managecategories\category_condition`](https://github.com/moodle/moodle/blob/main/question/bank/managecategories/classes/category_condition.php) as an example. + +You JavaScript filter class will also need to support your filter options. Override the constructor an add additional code for the UI required to set your filter options, and override `get filterOptions()` to return the current value for any options set in this UI. +See [`qbank_managecategories/datafilter/filtertypes/categories`](https://github.com/moodle/moodle/blob/main/question/bank/managecategories/amd/src/datafilter/filtertypes/categories.js) as an example. + +#### Context-sensitive configuration + +You may want your filter to behave differently depending on where it is being displayed. In this case you can override the constructor which receives the current `$qbank` view object, and extract some data that is used later on by your other methods. + +For example, the [tag condition](https://github.com/moodle/moodle/blob/main/question/bank/tagquestion/classes/tag_condition.php) will find the context of the current page, and use that to control which tags are available in the filter. diff --git a/versioned_docs/version-4.4/apis/plugintypes/qbank/index.md b/versioned_docs/version-4.4/apis/plugintypes/qbank/index.md index 655b570ee6..6b0a61ecda 100644 --- a/versioned_docs/version-4.4/apis/plugintypes/qbank/index.md +++ b/versioned_docs/version-4.4/apis/plugintypes/qbank/index.md @@ -22,5 +22,6 @@ Question bank plugins can extend the question bank in many ways, including: - Bulk actions - Navigation node (tabs) - Question preview additions (via callback) +- [Question filters](./filters.md) The place to start implementing most of these is with a class `classes/plugin_features.php` in your plugin, that declares which features you want to add to the question bank. Until more documentation is written, looking at the examples of the plugins in Moodle core should give you a good idea what you need to do. diff --git a/versioned_docs/version-4.5/apis/plugintypes/qbank/filters.md b/versioned_docs/version-4.5/apis/plugintypes/qbank/filters.md new file mode 100644 index 0000000000..5ae4a6f85d --- /dev/null +++ b/versioned_docs/version-4.5/apis/plugintypes/qbank/filters.md @@ -0,0 +1,217 @@ +--- +title: Question bank filters +tags: + - Plugins + - Question + - qbank +description: Question bank plugins allow you to define new filters for the question bank view and random question sets. +documentationDraft: true +--- + + + +Question bank plugins allow you define additional filters. These can be used when viewing the question bank, and are included in the URL so that a filtered view of the question bank can be shared. They are also used when defining the criteria for adding random questions to a quiz. + +## Creating a new filter condition + +A filter condition consists of two parts - the backend "condition" PHP class, and the frontend "filter" JavaScript class. + +The "condition" class defines the general properties of the filter - its name, various options, and how it is applied to the question bank query. +The "filter" class defines how the filter is displayed in the UI, and how values selected in the UI are passed back to the condition. + +Each new filter condition must define a new "condition" class in the qbank plugin based on `core_question\local\bank\condition`. +By default this will use the `core/datafilter/filtertype` "filter" class, although this can be overridden too if required. + +### Basic example + +This outlines the bare minimum required to implement a new filter condition. This will give you a field that allows you to enter keywords and add them to a list of selected search terms, the filter the questions by that list of terms. +This assumes that you already have the basic framework of a qbank plugin in place. For real-world examples, look for classes that extend `core_question\local\bank\condition`. + +Create a `condition` class within your plugin's namespace. For a plugin called `qbank_myplugin` this would look something like: + +```php title="question/bank/myplugin/classes/myfilter_condition.php" +namespace qbank_myplugin; + +use core_question\local\bank\condition; + +class myfilter_condition extends condition { + +} +``` + +Modify your `plugin_feature` class to return an instance of your condition from the `get_question_filters()` method: + +```php title="question/bank/myplugin/classes/plugin_feature.php" +namespace qbank_myplugin; + +class plugin_feature extends core_question\local\bank\plugin_features_base { + public function get_question_filters(?core_question\local\bank\view $qbank = null): array { + return [ + new myfilter_condition($qbank), + ]; + } +} +``` + +Back in your `condition` class, define the `get_name()` method, which returns the label displayed in the filter UI. + +```php title="Define the condition name" +public function get_name(): string { + return get_string('myfilter_name', 'myplugin'); +} +``` + +Define `get_condition_key()`, which returns a unique machine-readable ID for this filter condition, used when passing the filter as a parameter. + +```php title="Define the condition key" +public function get_condition_key(): string { + return 'myfilter'; +} +``` + +To actually filter the results, define `build_query_from_filter()` which returns an SQL `WHERE` condition, and an array of parameters. +The `$filter` parameter receives an array with a `'values'` key, containing an array of the selected values, and a `'jointype'` key, containing one of the `JOINTTYPE_ANY`, `JOINTYPE_ALL` or `JOINTYPE_NONE` constants. Use these to build your condition as required. + +The conditions from each filter are combined with the query in [`core_question\local\bank\view::build_query()`](https://github.com/moodle/moodle/blob/c741492c38b9945abbfc7e90dfe8f943279f8265/question/classes/local/bank/view.php#L733) + +```php title="Filter questions" +public function build_query_from_filter(array $filter): array { + $andor = ' AND '; + $equal = '='; + if ($filter['jointype'] === self::JOINTYPE_ANY) { + $andor = ' OR '; + } else if ($filter['jointype'] === self::JOINTYPE_NONE) { + $equal = '!='; + } + $conditions = []; + $params = []; + // In real life we'd probably use $DB->get_in_or_equal here. + foreach ($filter['values'] as $key => $value) { + $conditions[] = 'q.fieldname ' . $equal . ' :myfilter' . $key; + $params['myfilter' . $key] = $value; + } + return [ + '(' . implode($andor, $conditions) . ')', + $params, + ]; +} +``` + +Following this pattern with your own fields and options will give you a basic functional filter. Most filters will require more complex functionality, which can be achieved through additional methods. + +### Additional options + +#### Pre-defined values + +To define the list of possible filter values, define `get_initial_values()`, which returns an array of `['value', 'title']` for each option. These will then be searchable and selectable in the autocomplete field. + +```php title="Define initial filter values" +public function get_initial_values(): string { + return [ + [ + 'value' => 0, + 'title' => 'Option 1', + ], + [ + 'value' => 1, + 'title' => 'Option 2', + ] + ]; +} +``` + +#### Restrict custom keywords + +To restrict the possible filter terms to only those returned from `get_initial_values()`, define `allow_custom()` and have it return `false`. + +```php title="Disable custom terms" +public function allow_custom(): bool { + return false; +} +``` + +#### Restrict join types + +Not all join types are relevant to all filters. If each question will only match one of the selected values, it does not make sense to allow `JOINTYPE_ALL`. Define `get_join_list()` and return an array of the applicable join types. + +```php title="Define a restricted list of join types" +public function get_join_list(): array { + return [ + datafilter::JOINTYPE_ANY, + datafilter::JOINTYPE_NONE, + ]; +} +``` + +#### Allow multiple values? + +By default, conditions allow multiple values to be selected and use the selected join type to decide how they are applied. +If your condition should only allow a single value at a time, override `allow_multiple()` to return false. + +```php title="Disable selection of multiple values" +public function allow_multiple(): bool { + return false; +} +``` + +#### Allow empty values? + +By default, conditions can be left empty, and therefore will not be included in the filter. To make it compulsory to select a value for this condition when it is added, override `allow_empty()` to return false. + +```php title="Disable empty values" +public function allow_empty(): bool { + return false; +} +``` + +#### Is the condition required? + +If it is compulsory that your condition is always displayed, override `is_required()` to return true. + +```php title="Make the condition compulsory" +public function is_required(): bool { + return true; +} +``` + +#### Custom filter class + +By default, the filter will be displayed and processed using the `core/datafilter/filtertype` JavaScript class. +This will provide a single autocomplete field for selecting one or multiple numeric IDs with textual labels. +If this does not fit your filter's use case, you can tell your condition to use a different filter class. + +You can either use a different core filter type from `/lib/amd/src/datafilter/filtertypes`, or define your own. + +To tell your filter condition to use a different filter class, override the `get_filter_class()` method to return the namespaced path to your JavaScript class. + +```php title="Override the default filter class" +public function get_filter_class(): string { + return 'qbank_myplugin/datafilter/filtertype/myfilter'; +} +``` + +To create your own filter class, a new JavaScript file in your plugin under `amd/src/datafilter/filtertypes/myfilter.js`. +In this file, export a default class that extends `core/datafilter/filtertype` (or another core filter type from `/lib/amd/src/datafilter/filtertypes`) and override the base methods as required. +For example, if your filter uses textual rather than numeric values, you can override `get values()` to return the raw values without running `parseInt()` (see [`qbank_viewquestiontype/datafilter/filtertypes/type`](https://github.com/moodle/moodle/blob/main/mod/quiz/tests/behat/editing_add_from_question_bank.feature)). + +If you want a different UI for selecting your filter values instead of a single autocomplete, you can override `addValueSelector()`. +This also provides flexibility over how the values provided by `get_initial_values()` are used by the UI. + +#### Filter options + +If your condition supports additional options as to how the selected values are applied to the query, such as whether child categories are included when parent categories are selected, you can define "Filter options". + +In your condition class, define `get_filteroptions()` which returns an object containing the current filter options. You will probably want to add some code to the constructor to read in the current filter options, and some code the `build_query_from_filter()` to use the option. +See [`qbank_managecategories\category_condition`](https://github.com/moodle/moodle/blob/main/question/bank/managecategories/classes/category_condition.php) as an example. + +You JavaScript filter class will also need to support your filter options. Override the constructor an add additional code for the UI required to set your filter options, and override `get filterOptions()` to return the current value for any options set in this UI. +See [`qbank_managecategories/datafilter/filtertypes/categories`](https://github.com/moodle/moodle/blob/main/question/bank/managecategories/amd/src/datafilter/filtertypes/categories.js) as an example. + +#### Context-sensitive configuration + +You may want your filter to behave differently depending on where it is being displayed. In this case you can override the constructor which receives the current `$qbank` view object, and extract some data that is used later on by your other methods. + +For example, the [tag condition](https://github.com/moodle/moodle/blob/main/question/bank/tagquestion/classes/tag_condition.php) will find the context of the current page, and use that to control which tags are available in the filter. diff --git a/versioned_docs/version-4.5/apis/plugintypes/qbank/index.md b/versioned_docs/version-4.5/apis/plugintypes/qbank/index.md index 655b570ee6..6b0a61ecda 100644 --- a/versioned_docs/version-4.5/apis/plugintypes/qbank/index.md +++ b/versioned_docs/version-4.5/apis/plugintypes/qbank/index.md @@ -22,5 +22,6 @@ Question bank plugins can extend the question bank in many ways, including: - Bulk actions - Navigation node (tabs) - Question preview additions (via callback) +- [Question filters](./filters.md) The place to start implementing most of these is with a class `classes/plugin_features.php` in your plugin, that declares which features you want to add to the question bank. Until more documentation is written, looking at the examples of the plugins in Moodle core should give you a good idea what you need to do.