|
| 1 | +--- |
| 2 | +title: Question bank filters |
| 3 | +tags: |
| 4 | + - Plugins |
| 5 | + - Question |
| 6 | + - qbank |
| 7 | +description: Question bank plugins allow you to define new filters for the question bank view and random question sets. |
| 8 | +documentationDraft: true |
| 9 | +--- |
| 10 | + |
| 11 | +<Since |
| 12 | +version="4.3" |
| 13 | +issueNumber="MDL-72321" |
| 14 | +/> |
| 15 | + |
| 16 | +Question bank plugins allow you define additional filters. These can be used when viewing the question bank, and are included |
| 17 | +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 |
| 18 | +random questions to a quiz. |
| 19 | + |
| 20 | +## Creating a new filter condition |
| 21 | + |
| 22 | +A filter condition consists of two parts - the backend "condition" PHP class, and the frontend "filter" JavaScript class. |
| 23 | + |
| 24 | +The "condition" class defines the general properties of the filter - its name, various options, and how it is applied to the |
| 25 | +question bank query. |
| 26 | +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. |
| 27 | + |
| 28 | +Each new filter condition must define a new "condition" class in the qbank plugin based on `core_question\local\bank\condition`. |
| 29 | +By default this will use the `core/datafilter/filtertype` "filter" class, although this can be overridden too if required. |
| 30 | + |
| 31 | +### Basic example |
| 32 | + |
| 33 | +This outlines the bare minimum required to implement a new filter condition. This will allow you to filter based on a pre-defined |
| 34 | +list of values, selected from an autocomplete field. This assumes that you already have the basic framework of a qbank |
| 35 | +plugin in place. For real-world examples, look for classes that extend `core_question\local\bank\condition`. |
| 36 | + |
| 37 | +Create a `condition` class within your plugin's namespace. For a plugin called `qbank_myplugin` this would look something like: |
| 38 | + |
| 39 | +```php |
| 40 | +namespace qbank_myplugin; |
| 41 | + |
| 42 | +use core_question\local\bank\condition; |
| 43 | + |
| 44 | +class myfilter_condition extends condition { |
| 45 | + |
| 46 | +} |
| 47 | +``` |
| 48 | + |
| 49 | +Define the `get_name` method, which returns the label displayed in the filter UI. |
| 50 | + |
| 51 | +```php |
| 52 | + public function get_name(): string { |
| 53 | + return get_string('myfilter_name', 'myplugin'); |
| 54 | + } |
| 55 | +``` |
| 56 | + |
| 57 | +Define `get_condition_key`, which returns a unique machine-readable ID for this filter condition, used when passing the filter |
| 58 | +as a parameter. |
| 59 | + |
| 60 | +```php |
| 61 | + public function get_condition_key(): string { |
| 62 | + return 'myfilter'; |
| 63 | + } |
| 64 | +``` |
| 65 | + |
| 66 | +To define the list of possible filter values, define `get_initial_values`, which returns an array of `['value', 'title']` for each |
| 67 | +option. |
| 68 | + |
| 69 | +```php |
| 70 | + public function get_initial_values(): string { |
| 71 | + return [ |
| 72 | + [ |
| 73 | + 'value' => 0, |
| 74 | + 'title' => 'Option 1', |
| 75 | + ], |
| 76 | + [ |
| 77 | + 'value' => 1, |
| 78 | + 'title' => 'Option 2', |
| 79 | + ] |
| 80 | + ]; |
| 81 | + } |
| 82 | +``` |
| 83 | + |
| 84 | +To prevent additional values being added by typing them into the autocomplete, define `allow_custom` and have it return `false`. |
| 85 | + |
| 86 | +```php |
| 87 | + public function allow_custom(): bool { |
| 88 | + return false; |
| 89 | + } |
| 90 | +``` |
| 91 | + |
| 92 | +To actually filter the results, define `build_query_from_filter` which returns an SQL `WHERE` condition, and an array of parameters. |
| 93 | +The `$filter` parameter receives an array with a `'values'` key, containing an array of the selected values, and a `'jointype'` key, |
| 94 | +containing one of the `JOINTTYPE_ANY`, `JOINTYPE_ALL` or `JOINTYPE_NONE` constants. Use these to build your condition as required. |
| 95 | + |
| 96 | +```php |
| 97 | + public function build_query_from_filter(array $filter): array { |
| 98 | + $andor = ' AND '; |
| 99 | + $equal = '='; |
| 100 | + if ($filter['jointype'] === self::JOINTYPE_ANY) { |
| 101 | + $andor = ' OR '; |
| 102 | + } else if ($filter['jointype'] === self::JOINTYPE_NONE) { |
| 103 | + $equal = '!='; |
| 104 | + } |
| 105 | + $conditions = []; |
| 106 | + $params = []; |
| 107 | + // In real life we'd probably use $DB->get_in_or_equal here. |
| 108 | + foreach ($filter['values'] as $key => $value) { |
| 109 | + $conditions[] = 'q.fieldname ' . $equal . ' :myfilter' . $key; |
| 110 | + $params['myfilter' . $key] = $value; |
| 111 | + } |
| 112 | + return [ |
| 113 | + '(' . implode($andor, $conditions) . ')', |
| 114 | + $params, |
| 115 | + ]; |
| 116 | + } |
| 117 | +``` |
| 118 | + |
| 119 | +Following this pattern with your own fields and options will give you a basic functional filter. Most filters will require |
| 120 | +more complex functionality, which can be achieved through additional methods. |
| 121 | + |
| 122 | +### Additional options |
| 123 | + |
| 124 | +#### Restrict join types |
| 125 | + |
| 126 | +Not all join types are relevant to all filters. If each question will only match one of the selected values, it does not make |
| 127 | +sense to allow JOINTYPE_ALL. Define `get_join_list` and return an array of the applicable jointypes. |
| 128 | + |
| 129 | +```php |
| 130 | + public function get_join_list(): array { |
| 131 | + return [ |
| 132 | + datafilter::JOINTYPE_ANY, |
| 133 | + datafilter::JOINTYPE_NONE, |
| 134 | + ]; |
| 135 | + } |
| 136 | +``` |
| 137 | + |
| 138 | +#### Custom filter class |
| 139 | + |
| 140 | +By default, the filter will be displayed and processed using the `core/datafilter/filtertype` JavaScript class. |
| 141 | +This will provide a single autocomplete field for selecting one or multiple numeric IDs with textual labels. |
| 142 | +If this does not fit your filter's use case, you will need to define your own filter class. |
| 143 | + |
| 144 | +Create a new JavaScript file in your plugin under `amd/src/datafilter/filtertypes/myfilter.js`. |
| 145 | +In this file, export a default class that extends `core/datafilter/filtertype` |
| 146 | +(or another core filter type from '/lib/amd/src/datafilter/filtertypes') and override the base methods as required. |
| 147 | +For example, if your filter uses textual rather than numeric values, you can override `get values()` to return the raw values |
| 148 | +without running `parseInt()` (see `types` filter). If you want a different UI for selecting your filter values instead of a |
| 149 | +single autocomplete, you can override `addValueSelector()`. |
| 150 | + |
| 151 | +To tell your filter condition to use a custom filter class, override the `get_filter_class()` method to return the namespaced |
| 152 | +path to your JavaScript class. |
| 153 | + |
| 154 | +```php |
| 155 | + public function get_filter_class(): string { |
| 156 | + return 'qbank_myplugin/datafilter/filtertype/myfilter'; |
| 157 | + } |
| 158 | +``` |
| 159 | + |
| 160 | +#### Allow multiple values? |
| 161 | + |
| 162 | +By default, conditions allow multiple values to be selected and use the selected join type to decide how they are applied. |
| 163 | +If your condition should only allow a single value at a time, override `allow_multiple()` to return false. |
| 164 | + |
| 165 | +```php |
| 166 | + public function allow_multiple(): bool { |
| 167 | + return false; |
| 168 | + } |
| 169 | +``` |
| 170 | + |
| 171 | +#### Allow empty values? |
| 172 | + |
| 173 | +By default, conditions can be left empty, and therefore will not be included in the filter. To make it compulsory to select a |
| 174 | +value for this condition when it is added, override `allow_empty` to return false. |
| 175 | + |
| 176 | +```php |
| 177 | + public function allow_empty(): bool { |
| 178 | + return false; |
| 179 | + } |
| 180 | +``` |
| 181 | + |
| 182 | +#### Is the condition required? |
| 183 | + |
| 184 | +If it is compulsory that your condition is always displayed, override `is_required` to return true. |
| 185 | + |
| 186 | +```php |
| 187 | + public function is_required(): bool { |
| 188 | + return true; |
| 189 | + } |
| 190 | +``` |
| 191 | + |
| 192 | +#### Filter options |
| 193 | + |
| 194 | +If your condition supports additional options as to how the selected values are applied to the query, such as whether child |
| 195 | +categories are included when parent categories are selected, you can define "Filter options". |
| 196 | + |
| 197 | +In your condition class, define `get_filteroptions()` which returns an object containing the current filter options. You will |
| 198 | +probably want to add some code to the constructor to read in the current filter options, and some code the `build_query_from_filter` |
| 199 | +to use the option. See |
| 200 | +[`qbank_managecategories\category_condition`](https://github.com/moodle/moodle/blob/main/question/bank/managecategories/classes/category_condition.php#L331) |
| 201 | +as an example. |
| 202 | + |
| 203 | +You JavaScript filter class will also need to support your filter options. Override the constructor an add additional code |
| 204 | +for the UI required to set your filter options, and override `get filterOptions()` to return the current value for any options set |
| 205 | +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#L34) |
| 206 | +as an example. |
| 207 | + |
| 208 | +#### Context-sensitive configuration |
| 209 | + |
| 210 | +You may want your filter to behave differently depending on where it is being displayed. In this case you can override the |
| 211 | +constructor which receives the current `$qbank` view object, and extract some data that is used later on by your other methods. |
| 212 | + |
| 213 | +For example, the |
| 214 | +[tag condition](https://github.com/moodle/moodle/blob/main/question/bank/tagquestion/classes/tag_condition.php#L47C1-L47C49) |
| 215 | +will find the context of the current page, and use that to control which tags are available in the filter. |
0 commit comments