Skip to content

Commit 606e80b

Browse files
committed
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.
1 parent 52e8379 commit 606e80b

File tree

2 files changed

+216
-0
lines changed

2 files changed

+216
-0
lines changed
Lines changed: 215 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,215 @@
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.

docs/apis/plugintypes/qbank/index.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,5 +22,6 @@ Question bank plugins can extend the question bank in many ways, including:
2222
- Bulk actions
2323
- Navigation node (tabs)
2424
- Question preview additions (via callback)
25+
- [Question filters](./filters.md)
2526

2627
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.

0 commit comments

Comments
 (0)