diff --git a/admin/classes/local/entities/task_log.php b/admin/classes/local/entities/task_log.php index 4f8136ada8b..d54f260b386 100644 --- a/admin/classes/local/entities/task_log.php +++ b/admin/classes/local/entities/task_log.php @@ -17,6 +17,7 @@ namespace core_admin\local\entities; use core_reportbuilder\local\filters\date; +use core_reportbuilder\local\filters\duration; use core_reportbuilder\local\filters\select; use core_reportbuilder\local\filters\text; use core_reportbuilder\local\helpers\format; @@ -282,6 +283,16 @@ class task_log extends base { )) ->add_joins($this->get_joins()); + // Duration filter. + $filters[] = (new filter( + duration::class, + 'duration', + new lang_string('task_duration', 'admin'), + $this->get_entity_name(), + "${tablealias}.timeend - {$tablealias}.timestart" + )) + ->add_joins($this->get_joins()); + return $filters; } } diff --git a/admin/classes/local/systemreports/task_logs.php b/admin/classes/local/systemreports/task_logs.php index 8cf9959c256..9688fa3d591 100644 --- a/admin/classes/local/systemreports/task_logs.php +++ b/admin/classes/local/systemreports/task_logs.php @@ -124,6 +124,7 @@ class task_logs extends system_report { 'task_log:output', 'task_log:result', 'task_log:timestart', + 'task_log:duration', ]; $this->add_filters_from_entities($filters); diff --git a/admin/tests/behat/task_logs.feature b/admin/tests/behat/task_logs.feature index 686ca4bbd9f..b17cd935d11 100644 --- a/admin/tests/behat/task_logs.feature +++ b/admin/tests/behat/task_logs.feature @@ -29,3 +29,22 @@ Feature: View task logs report and use its filters | name | match | nonmatch | | task\\clean_events | Cleanup event monitor events | Incoming email pickup | | task\\pickup_task | Incoming email pickup | Cleanup event monitor events | + + @javascript + # Task duration is dependent on many factors, we are asserting here that no task has a duration >2 minutes. + Scenario Outline: Filter task logs by duration + Given I log in as "admin" + And I change window size to "large" + And I navigate to "Server > Tasks > Task logs" in site administration + When I click on "Filters" "button" + And I set the following fields in the "Duration" "core_reportbuilder > Filter" to these values: + | Duration operator | | + | Duration value | 2 | + | Duration unit | minutes | + And I click on "Apply" "button" in the "[data-region='report-filters']" "css_element" + Then I should see "Filters applied" + And I "Nothing to display" + Examples: + | operator | shouldornotsee | + | Less than | should not see | + | Greater than | should see | diff --git a/lang/en/reportbuilder.php b/lang/en/reportbuilder.php index 1bdb8a76232..50641d1bd8f 100644 --- a/lang/en/reportbuilder.php +++ b/lang/en/reportbuilder.php @@ -37,6 +37,7 @@ $string['filtercontains'] = 'Contains'; $string['filterdatefrom'] = 'Date from'; $string['filterdateto'] = 'Date to'; $string['filterdoesnotcontain'] = 'Does not contain'; +$string['filterdurationunit'] = '{$a} unit'; $string['filterendswith'] = 'Ends with'; $string['filterequalorgreaterthan'] = 'Greater than or equal'; $string['filterequalorlessthan'] = 'Less than or equal'; diff --git a/reportbuilder/classes/local/filters/duration.php b/reportbuilder/classes/local/filters/duration.php new file mode 100644 index 00000000000..6738e9d1a76 --- /dev/null +++ b/reportbuilder/classes/local/filters/duration.php @@ -0,0 +1,137 @@ +. + +declare(strict_types=1); + +namespace core_reportbuilder\local\filters; + +use lang_string; +use MoodleQuickForm; +use core_reportbuilder\local\helpers\database; + +/** + * Duration report filter + * + * This filter accepts a number of seconds to perform filtering on + * + * @package core_reportbuilder + * @copyright 2021 Paul Holden + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class duration extends base { + + /** @var int Any value */ + public const DURATION_ANY = 0; + + /** @var int Maximum duration */ + public const DURATION_MAXIMUM = 1; + + /** @var int Minimum duration */ + public const DURATION_MINIMUM = 2; + + + /** + * Return an array of operators available for this filter + * + * @return lang_string[] + */ + private function get_operators(): array { + $operators = [ + self::DURATION_ANY => new lang_string('filterisanyvalue', 'core_reportbuilder'), + self::DURATION_MAXIMUM => new lang_string('filterlessthan', 'core_reportbuilder'), + self::DURATION_MINIMUM => new lang_string('filtergreaterthan', 'core_reportbuilder'), + ]; + + return $this->filter->restrict_limited_operators($operators); + } + + /** + * Setup form + * + * @param MoodleQuickForm $mform + */ + public function setup_form(MoodleQuickForm $mform): void { + $elements = []; + + // Operator. + $operatorlabel = get_string('filterfieldoperator', 'core_reportbuilder', $this->get_header()); + + $elements[] = $mform->createElement('select', "{$this->name}_operator", $operatorlabel, $this->get_operators()); + $mform->setType("{$this->name}_operator", PARAM_INT); + $mform->setDefault("{$this->name}_operator", self::DURATION_ANY); + + // Value. + $valuelabel = get_string('filterfieldvalue', 'core_reportbuilder', $this->get_header()); + + $elements[] = $mform->createElement('text', "{$this->name}_value", $valuelabel, ['size' => 3]); + $mform->setType("{$this->name}_value", PARAM_FLOAT); + $mform->setDefault("{$this->name}_value", 0); + $mform->hideIf("{$this->name}_value", "{$this->name}_operator", 'eq', self::DURATION_ANY); + + // Unit. + $unitlabel = get_string('filterdurationunit', 'core_reportbuilder', $this->get_header()); + $units = [ + 1 => get_string('seconds'), + MINSECS => get_string('minutes'), + HOURSECS => get_string('hours'), + DAYSECS => get_string('days'), + ]; + + $elements[] = $mform->createElement('select', "{$this->name}_unit", $unitlabel, $units); + $mform->setType("{$this->name}_unit", PARAM_INT); + $mform->setDefault("{$this->name}_unit", 1); + $mform->hideIf("{$this->name}_unit", "{$this->name}_operator", 'eq', self::DURATION_ANY); + + $mform->addGroup($elements, "{$this->name}_group", '', null, false); + } + + /** + * Return filter SQL + * + * @param array $values + * @return array + */ + public function get_sql_filter(array $values): array { + $fieldsql = $this->filter->get_field_sql(); + $params = $this->filter->get_field_params(); + + $durationvalue = unformat_float($values["{$this->name}_value"] ?? 0); + $durationunit = (int) ($values["{$this->name}_unit"] ?? 0); + + $operator = $values["{$this->name}_operator"] ?? self::DURATION_ANY; + switch ($operator) { + case self::DURATION_MAXIMUM: + $paramduration = database::generate_param_name(); + + $sql = "{$fieldsql} <= :{$paramduration}"; + $params[$paramduration] = $durationvalue * $durationunit; + + break; + case self::DURATION_MINIMUM: + $paramduration = database::generate_param_name(); + + $sql = "{$fieldsql} >= :{$paramduration}"; + $params[$paramduration] = $durationvalue * $durationunit; + + break; + default: + // Invalid or inactive filter. + return ['', []]; + } + + return [$sql, $params]; + } +} diff --git a/reportbuilder/tests/local/filters/duration_test.php b/reportbuilder/tests/local/filters/duration_test.php new file mode 100644 index 00000000000..ee903b52787 --- /dev/null +++ b/reportbuilder/tests/local/filters/duration_test.php @@ -0,0 +1,128 @@ +. + +declare(strict_types=1); + +namespace core_reportbuilder\local\filters; + +use advanced_testcase; +use lang_string; +use core_reportbuilder\local\report\filter; + +/** + * Unit tests for duration report filter + * + * @package core_reportbuilder + * @covers \core_reportbuilder\local\filters\base + * @covers \core_reportbuilder\local\filters\duration + * @copyright 2021 Paul Holden + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class duration_test extends advanced_testcase { + + /** + * Data provider for {@see test_get_sql_filter} + * + * @return array + */ + public function get_sql_filter_provider(): array { + return [ + 'Any duration' => + [duration::DURATION_ANY, true], + + // Maximum operator. + 'Maximum seconds non-match' => + [duration::DURATION_MAXIMUM, false, HOURSECS, 1], + 'Maximum seconds match' => + [duration::DURATION_MAXIMUM, true, HOURSECS * 3, 1], + 'Maximum minutes non-match' => + [duration::DURATION_MAXIMUM, false, 60, MINSECS], + 'Maximum minutes match' => + [duration::DURATION_MAXIMUM, true, 150, MINSECS], + 'Maximum hours non-match (float)' => + [duration::DURATION_MAXIMUM, false, 0.5, HOURSECS], + 'Maximum hours non-match' => + [duration::DURATION_MAXIMUM, false, 1, HOURSECS], + 'Maximum hours match (float)' => + [duration::DURATION_MAXIMUM, true, 2.5, HOURSECS], + 'Maximum hours match' => + [duration::DURATION_MAXIMUM, true, 3, HOURSECS], + + // Minimum operator. + 'Minimum seconds match' => + [duration::DURATION_MINIMUM, true, HOURSECS, 1], + 'Minimum seconds non-match' => + [duration::DURATION_MINIMUM, false, HOURSECS * 3, 1], + 'Minimum minutes match' => + [duration::DURATION_MINIMUM, true, 60, MINSECS], + 'Minimum minutes non-match' => + [duration::DURATION_MINIMUM, false, 150, MINSECS], + 'Minimum hours match (float)' => + [duration::DURATION_MINIMUM, true, 0.5, HOURSECS], + 'Minimum hours match' => + [duration::DURATION_MINIMUM, true, 1, HOURSECS], + 'Minimum hours non-match (float)' => + [duration::DURATION_MINIMUM, false, 2.5, HOURSECS], + 'Minimum hours non-match' => + [duration::DURATION_MINIMUM, false, 3, HOURSECS], + ]; + } + + /** + * Test getting filter SQL + * + * @param int $operator + * @param bool $expectuser + * @param float $value + * @param int $unit + * + * @dataProvider get_sql_filter_provider + */ + public function test_get_sql_filter(int $operator, bool $expectuser, float $value = 0, int $unit = MINSECS): void { + global $DB; + + $this->resetAfterTest(); + + // We are going to enrol our student from now, with a duration of two hours (timeend is two hours later). + $timestart = time(); + $timeend = $timestart + (HOURSECS * 2); + + $course = $this->getDataGenerator()->create_course(); + $user = $this->getDataGenerator()->create_and_enrol($course, 'student', null, 'manual', $timestart, $timeend); + + $filter = new filter( + duration::class, + 'test', + new lang_string('yes'), + 'testentity', + 'timeend - timestart' + ); + + // Create instance of our filter, passing given values. + [$select, $params] = duration::create($filter)->get_sql_filter([ + $filter->get_unique_identifier() . '_operator' => $operator, + $filter->get_unique_identifier() . '_value' => $value, + $filter->get_unique_identifier() . '_unit' => $unit, + ]); + + $useridfield = $DB->get_field_select('user_enrolments', 'userid', $select, $params); + if ($expectuser) { + $this->assertEquals($useridfield, $user->id); + } else { + $this->assertFalse($useridfield); + } + } +}