diff --git a/lang/en/reportbuilder.php b/lang/en/reportbuilder.php index 1bdb8a762329d..50641d1bd8f7d 100644 --- a/lang/en/reportbuilder.php +++ b/lang/en/reportbuilder.php @@ -37,6 +37,7 @@ $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 0000000000000..6738e9d1a76b5 --- /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 0000000000000..ee903b52787c0 --- /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); + } + } +}