diff --git a/question/bank/viewcreator/classes/plugin_feature.php b/question/bank/viewcreator/classes/plugin_feature.php index 850b1c5e746..703d9188432 100644 --- a/question/bank/viewcreator/classes/plugin_feature.php +++ b/question/bank/viewcreator/classes/plugin_feature.php @@ -17,6 +17,7 @@ namespace qbank_viewcreator; use core_question\local\bank\plugin_features_base; +use core_question\local\bank\view; /** * Class plugin_feature is the entrypoint for the columns. @@ -27,11 +28,18 @@ use core_question\local\bank\plugin_features_base; * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class plugin_feature extends plugin_features_base { - + #[\Override] public function get_question_columns($qbank): array { return [ new creator_name_column($qbank), new modifier_name_column($qbank), ]; } + + #[\Override] + public function get_question_filters(?view $qbank = null): array { + return [ + new timemodified_condition($qbank), + ]; + } } diff --git a/question/bank/viewcreator/classes/timemodified_condition.php b/question/bank/viewcreator/classes/timemodified_condition.php new file mode 100644 index 00000000000..81130c69d35 --- /dev/null +++ b/question/bank/viewcreator/classes/timemodified_condition.php @@ -0,0 +1,152 @@ +. + +namespace qbank_viewcreator; + +use core\exception\moodle_exception; +use core\plugininfo\filter; +use core_question\local\bank\condition; + +/** + * Filter condition for date/time modified + * + * @package qbank_viewcreator + * @copyright 2024 onwards Catalyst IT EU {@link https://catalyst-eu.net} + * @author Mark Johnson + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class timemodified_condition extends condition { + /** + * @var string Search for times before the specified date. + */ + const MODE_BEFORE = 'before'; + + /** + * @var string Search for times after the specified date. + */ + const MODE_AFTER = 'after'; + + /** + * @var string Search for times between the specified dates. + */ + const MODE_BETWEEN = 'between'; + + #[\Override] + public function get_title() { + return get_string('timemodified', 'qbank_viewcreator'); + } + + #[\Override] + public static function get_condition_key() { + return 'timemodified'; + } + + #[\Override] + public function get_filter_class() { + return 'core/datafilter/filtertypes/datetime'; + } + + /** + * Set a single valid jointype, so we don't display the jointype selector. + * + * We have a separate filter option to control how this condition is applied, Any/All/None doesn't apply here. + * + * @return array + */ + public function get_join_list(): array { + return [ + self::JOINTYPE_DEFAULT, + ]; + } + + /** + * Build an SQL WHERE condition to filter questions based on q.timemodified. + * + * $filter['values'][0] contains the datetime to search after, $filter['values'][1] contains the datetime + * to search before. Whether to use these dates to search after, before, or between these dates is determined + * by the value of $filter['fileroptions']['mode']. + * + * The datetime values are in the format YYYY-MM-DDTHH:mm, as provided by the datetime-local input type. + * + * @param array $filter ['values' => [$before, $after], 'filteroptions' => ['mode' => $mode]] + * @return array + * @throws moodle_exception If an invalid mode or range is provided. + */ + public static function build_query_from_filter(array $filter): array { + if (!isset($filter['filteroptions']['mode']) || empty($filter['values'])) { + return ['', []]; + } + $mode = $filter['filteroptions']['mode']; + if (!in_array($mode, [self::MODE_AFTER, self::MODE_BEFORE, self::MODE_BETWEEN])) { + throw new moodle_exception('invaliddatetimemode', 'error', a: $filter['filteroptions']['mode']); + } + $tz = new \DateTimeZone(\core_date::get_user_timezone(99)); + $datetimeafter = new \DateTime($filter['values'][0], $tz); + $datetimebefore = new \DateTime($filter['values'][1], $tz); + if ($mode === self::MODE_AFTER) { + $conditions = 'q.timemodified > :timemodifiedafter'; + $params['timemodifiedafter'] = $datetimeafter->getTimestamp(); + } else if ($mode === self::MODE_BEFORE) { + $conditions = 'q.timemodified < :timemodifiedbefore'; + $params['timemodifiedbefore'] = $datetimebefore->getTimestamp(); + } else { + if ($datetimeafter > $datetimebefore) { + throw new moodle_exception( + 'invaliddatetimebetween', + 'error', + a: (object) [ + 'before' => $datetimebefore->format('Y-m-d H:i'), + 'after' => $datetimeafter->format('Y-m-d H:i'), + ], + ); + } + $conditions = 'q.timemodified > :timemodifiedafter AND q.timemodified < :timemodifiedbefore'; + $params = [ + 'timemodifiedafter' => $datetimeafter->getTimestamp(), + 'timemodifiedbefore' => $datetimebefore->getTimestamp(), + ]; + } + + return [$conditions, $params]; + } + + /** + * Return the default datetime values for the filter. + * + * This generates values formatted for datetime-local fields. The first value returned is the current time, + * for use as the default "before" datetime. The second is midnight 1 week ago, for use as the default "after" + * datetime. + * + * @return array[] + */ + public function get_initial_values(): array { + $tz = new \DateTimeZone(\core_date::get_user_timezone()); + // Datetime format used by the field. + $format = 'Y-m-d\TH:i'; + $now = (new \DateTime('now', $tz))->format($format); + $oneweek = (new \DateTime('midnight 1 week ago', $tz))->format($format); + return [ + [ + 'value' => $now, + 'title' => $now, + ], + [ + 'value' => $oneweek, + 'title' => $oneweek, + ], + ]; + } +} diff --git a/question/bank/viewcreator/lang/en/qbank_viewcreator.php b/question/bank/viewcreator/lang/en/qbank_viewcreator.php index 8fded1983d6..accadaeab4a 100644 --- a/question/bank/viewcreator/lang/en/qbank_viewcreator.php +++ b/question/bank/viewcreator/lang/en/qbank_viewcreator.php @@ -24,6 +24,7 @@ */ $string['modifiedby'] = 'Modified by'; +$string['timemodified'] = 'Time modified'; $string['pluginname'] = 'View creator'; $string['privacy:metadata'] = 'The View creator question bank plugin does not store any personal data.'; $string['version'] = 'Version {$a}'; diff --git a/question/bank/viewcreator/tests/behat/filter_condition_timemodified.feature b/question/bank/viewcreator/tests/behat/filter_condition_timemodified.feature new file mode 100644 index 00000000000..eabbaf26da2 --- /dev/null +++ b/question/bank/viewcreator/tests/behat/filter_condition_timemodified.feature @@ -0,0 +1,83 @@ +@qbank @qbank_viewcreator @javascript +Feature: Time modified filter condition + As a teacher + In order to organise my questions + I want to filter the list of questions by the time and date of last modification + + Background: + Given the following "courses" exist: + | fullname | shortname | category | + | Course 1 | C1 | 0 | + And the following "activities" exist: + | activity | name | intro | course | idnumber | + | qbank | Qbank 1 | Question bank 1 | C1 | qbank1 | + And the following "question categories" exist: + | contextlevel | reference | name | + | Activity module | qbank1 | Test questions | + And the following "questions" exist: + | questioncategory | qtype | name | questiontext | + | Test questions | truefalse | First question | Answer the first question | + | Test questions | truefalse | Second question | Answer the second question | + | Test questions | truefalse | Third question | Answer the third question | + And the following "core_question > updated questions" exist: + | questioncategory | question | name | timemodified | + | Test questions | First question | First question | ## 2024-01-10 10:00 ## | + | Test questions | Second question | Second question | ## 2024-01-10 11:00 ## | + | Test questions | Third question | Third question | ## 2024-01-10 12:00 ## | + Given I am on the "Qbank 1" "core_question > question bank" page logged in as "admin" + And I should see "First question" + And I should see "Second question" + And I should see "Third question" + + Scenario: Filter by questions modified before time + When I add question bank filter "Time modified" + And the field "Time modified before" matches value "## now ##%FT%R##" + And I set the field "Time modified before" to "2024-01-10T10:59" + And I press "Apply filters" + Then I should see "First question" + And I should not see "Second question" + And I should not see "Third question" + + Scenario: Filter by questions modified after time + When I add question bank filter "Time modified" + And I set the field "Select dates" to "After" + And the field "Time modified after" matches value "## midnight 1 week ago ##%FT%R##" + And I set the field "Time modified after" to "2024-01-10T10:59" + And I press "Apply filters" + Then I should not see "First question" + And I should see "Second question" + And I should see "Third question" + + Scenario: Filter by questions modified between times + When I add question bank filter "Time modified" + And I set the field "Select dates" to "Between" + And I set the field "Time modified after" to "2024-01-10T10:59" + And I set the field "Time modified before" to "2024-01-10T11:01" + And I press "Apply filters" + Then I should not see "First question" + And I should see "Second question" + And I should not see "Third question" + + Scenario: Apply filter between invalid times + When I add question bank filter "Time modified" + And I set the field "Select dates" to "Between" + And I set the field "Time modified after" to "2024-01-10T10:59" + And I set the field "Time modified before" to "2024-01-10T11:01" + And I press "Apply filters" + And I should not see "First question" + And I should see "Second question" + And I should not see "Third question" + And I set the field "Time modified after" to "2024-01-10T11:01" + And I set the field "Time modified before" to "2024-01-10T10:59" + And I press "Apply filters" + # Invalid filters should not be applied. + And I should not see "First question" + And I should see "Second question" + And I should not see "Third question" + # Invalid values should not be set in the URL. + And I reload the page + And the field "Time modified after" matches value "2024-01-10T10:59" + And the field "Time modified before" matches value "2024-01-10T11:01" + And I should not see "First question" + And I should see "Second question" + And I should not see "Third question"