diff --git a/reportbuilder/classes/local/helpers/format.php b/reportbuilder/classes/local/helpers/format.php new file mode 100644 index 00000000000..660a3f1554c --- /dev/null +++ b/reportbuilder/classes/local/helpers/format.php @@ -0,0 +1,53 @@ +. + +declare(strict_types=1); + +namespace core_reportbuilder\local\helpers; + +use stdClass; + +/** + * Class containing helper methods for format columns data as callbacks. + * + * @package core_reportbuilder + * @copyright 2021 Sara Arjona based on Alberto Lara Hernández code. + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class format { + + /** + * Returns formatted date. + * + * @param int $value Unix timestamp + * @param stdClass $row + * @param string|null $format Format string for strftime + * @return string + */ + public static function userdate(int $value, stdClass $row, ?string $format = null): string { + return $value ? userdate($value, $format) : ''; + } + + /** + * Returns yes/no string depending on the given value + * + * @param bool $value + * @return string + */ + public static function boolean_as_text(bool $value): string { + return $value ? get_string('yes') : get_string('no'); + } +} diff --git a/reportbuilder/classes/local/models/report.php b/reportbuilder/classes/local/models/report.php new file mode 100644 index 00000000000..0c874e3bcfa --- /dev/null +++ b/reportbuilder/classes/local/models/report.php @@ -0,0 +1,101 @@ +. + +declare(strict_types=1); + +namespace core_reportbuilder\local\models; + +use context; +use context_system; +use core\persistent; +use core_reportbuilder\local\report\base; + +/** + * Persistent class to represent a report + * + * @package core_reportbuilder + * @copyright 2018 Alberto Lara Hernández + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class report extends persistent { + + /** @var string The table name. */ + public const TABLE = 'reportbuilder_report'; + + /** + * Return the definition of the properties of this model + * + * @return array + */ + protected static function define_properties(): array { + return [ + 'source' => [ + 'type' => PARAM_RAW, + ], + 'type' => [ + 'type' => PARAM_INT, + 'choices' => [ + base::TYPE_CUSTOM_REPORT, + base::TYPE_SYSTEM_REPORT, + ], + ], + 'contextid' => [ + 'type' => PARAM_INT, + 'default' => static function(): int { + return context_system::instance()->id; + } + ], + 'component' => [ + 'type' => PARAM_COMPONENT, + 'default' => '', + ], + 'area' => [ + 'type' => PARAM_AREA, + 'default' => '', + ], + 'itemid' => [ + 'type' => PARAM_INT, + 'default' => 0, + ], + 'usercreated' => [ + 'type' => PARAM_INT, + 'default' => static function(): int { + global $USER; + + return (int) $USER->id; + }, + ], + ]; + } + + /** + * Return report ID + * + * @return int + */ + protected function get_id(): int { + return (int) $this->raw_get('id'); + } + + /** + * Return report context, used by exporters + * + * @return context + */ + public function get_context(): context { + return context::instance_by_id($this->raw_get('contextid')); + } +} diff --git a/reportbuilder/classes/local/report/base.php b/reportbuilder/classes/local/report/base.php new file mode 100644 index 00000000000..d4616d99781 --- /dev/null +++ b/reportbuilder/classes/local/report/base.php @@ -0,0 +1,420 @@ +. + +declare(strict_types=1); + +namespace core_reportbuilder\local\report; + +use coding_exception; +use context; +use lang_string; +use core_reportbuilder\local\filters\base as filter_base; +use core_reportbuilder\local\helpers\database; +use core_reportbuilder\local\helpers\user_filter_manager; +use core_reportbuilder\local\models\report; + +/** + * Base class for all reports + * + * @package core_reportbuilder + * @copyright 2020 Paul Holden + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +abstract class base { + + /** @var int Custom report type value */ + public const TYPE_CUSTOM_REPORT = 0; + + /** @var int System report type value */ + public const TYPE_SYSTEM_REPORT = 1; + + /** @var int Default paging limit */ + public const DEFAULT_PAGESIZE = 30; + + /** @var report $report Report persistent */ + private $report; + + /** @var string $maintable */ + private $maintable = ''; + + /** @var string $maintablealias */ + private $maintablealias = ''; + + /** @var array $sqljoins */ + private $sqljoins = []; + + /** @var array $sqlwheres */ + private $sqlwheres = []; + + /** @var array $sqlparams */ + private $sqlparams = []; + + /** @var lang_string[] */ + private $entitytitles = []; + + /** @var column[] $columns */ + private $columns = []; + + /** @var filter[] $filters */ + private $filters = []; + + /** @var bool $downloadable Set if the report can be downloaded */ + private $downloadable = false; + + /** @var string $downloadfilename Name of the downloaded file */ + private $downloadfilename = ''; + + /** + * Base report constructor + * + * @param report $report + */ + public function __construct(report $report) { + $this->report = $report; + + // Initialise and validate the report. + $this->initialise(); + $this->validate(); + } + + /** + * Returns persistent class used when initialising this report + * + * @return report + */ + final public function get_report_persistent(): report { + return $this->report; + } + + /** + * Initialise report. Specify which columns, filters, etc should be present + * + * To set the base query use: + * - {@see set_main_table} + * - {@see add_base_condition_simple} or {@see add_base_condition_sql} + * - {@see add_join} + * + * To add content to the report use: + * - {@see add_entity} + * - {@see add_column} + * - {@see add_filter} + * - etc + */ + abstract protected function initialise(): void; + + /** + * Get the report availability. Sub-classes should override this method to declare themselves unavailable, for example if + * they require classes that aren't present due to missing plugin + * + * @return bool + */ + public static function is_available(): bool { + return true; + } + + /** + * Perform some basic validation about expected class properties + * + * @throws coding_exception + */ + protected function validate(): void { + if (empty($this->maintable)) { + throw new coding_exception('Report must define main table by calling $this->set_main_table()'); + } + + if (empty($this->columns)) { + throw new coding_exception('Report must define at least one column by calling $this->add_column()'); + } + } + + /** + * Set the main table and alias for the SQL query + * + * @param string $tablename + * @param string $tablealias + */ + final public function set_main_table(string $tablename, string $tablealias = ''): void { + $this->maintable = $tablename; + $this->maintablealias = $tablealias; + } + + /** + * Get the main table name + * + * @return string + */ + final public function get_main_table(): string { + return $this->maintable; + } + + /** + * Get the alias for the main table + * + * @return string + */ + final public function get_main_table_alias(): string { + return $this->maintablealias; + } + + /** + * Adds report JOIN clause that is always added + * + * @param string $join + * @param array $params + * @param bool $validateparams Some queries might add non-standard params and validation could fail + */ + protected function add_join(string $join, array $params = [], bool $validateparams = true): void { + if ($validateparams) { + database::validate_params($params); + } + + $this->sqljoins[trim($join)] = trim($join); + $this->sqlparams += $params; + } + + /** + * Return report JOIN clauses + * + * @return array + */ + public function get_joins(): array { + return array_values($this->sqljoins); + } + + /** + * Define simple "field = value" clause to apply to the report query + * + * @param string $fieldname + * @param mixed $fieldvalue + */ + final public function add_base_condition_simple(string $fieldname, $fieldvalue): void { + if ($fieldvalue === null) { + $this->add_base_condition_sql("{$fieldname} IS NULL"); + } else { + $fieldvalueparam = database::generate_param_name(); + $this->add_base_condition_sql("{$fieldname} = :{$fieldvalueparam}", [ + $fieldvalueparam => $fieldvalue, + ]); + } + } + + /** + * Define more complex clause that will always be applied to the report query + * + * @param string $where + * @param array $params Note that the param names should be generated by {@see database::generate_param_name} + */ + final public function add_base_condition_sql(string $where, array $params = []): void { + database::validate_params($params); + + $this->sqlwheres[] = trim($where); + $this->sqlparams = $params + $this->sqlparams; + } + + /** + * Return base select/params for the report query + * + * @return array [string $select, array $params] + */ + final public function get_base_condition(): array { + return [ + implode(' AND ', $this->sqlwheres), + $this->sqlparams, + ]; + } + + /** + * Define a new entity for the report + * + * @param string $name + * @param lang_string $title + * @throws coding_exception + */ + final protected function annotate_entity(string $name, lang_string $title): void { + if (empty($name) || $name !== clean_param($name, PARAM_ALPHANUMEXT)) { + throw new coding_exception('Entity name must be comprised of alphanumeric character, underscore or dash'); + } + + $this->entitytitles[$name] = $title; + } + + /** + * Adds a column to the report + * + * @param column $column + * @return column + * @throws coding_exception + */ + final protected function add_column(column $column): column { + if (!array_key_exists($column->get_entity_name(), $this->entitytitles)) { + throw new coding_exception('Invalid entity name', $column->get_entity_name()); + } + + $name = $column->get_name(); + if (empty($name) || $name !== clean_param($name, PARAM_ALPHANUMEXT)) { + throw new coding_exception('Column name must be comprised of alphanumeric character, underscore or dash'); + } + + $uniqueidentifier = $column->get_unique_identifier(); + if (array_key_exists($uniqueidentifier, $this->columns)) { + throw new coding_exception('Duplicate column identifier', $uniqueidentifier); + } + + $this->columns[$uniqueidentifier] = $column; + + return $column; + } + + /** + * Return report column by unique identifier + * + * @param string $uniqueidentifier + * @return column|null + */ + final public function get_column(string $uniqueidentifier): ?column { + return $this->columns[$uniqueidentifier] ?? null; + } + + /** + * Return all available report columns + * + * @return column[] + */ + final public function get_columns(): array { + return array_filter($this->columns, static function(column $column): bool { + return $column->get_is_available(); + }); + } + + /** + * Adds a filter to the report + * + * @param filter $filter + * @return filter + * @throws coding_exception + */ + final protected function add_filter(filter $filter): filter { + if (!array_key_exists($filter->get_entity_name(), $this->entitytitles)) { + throw new coding_exception('Invalid entity name', $filter->get_entity_name()); + } + + $name = $filter->get_name(); + if (empty($name) || $name !== clean_param($name, PARAM_ALPHANUMEXT)) { + throw new coding_exception('Filter name must be comprised of alphanumeric character, underscore or dash'); + } + + $uniqueidentifier = $filter->get_unique_identifier(); + if (array_key_exists($uniqueidentifier, $this->filters)) { + throw new coding_exception('Duplicate filter identifier', $uniqueidentifier); + } + + $this->filters[$uniqueidentifier] = $filter; + + return $filter; + } + + /** + * Return report filter by unique identifier + * + * @param string $uniqueidentifier + * @return filter|null + */ + final public function get_filter(string $uniqueidentifier): ?filter { + return $this->filters[$uniqueidentifier] ?? null; + } + + /** + * Return all available report filters + * + * @return filter[] + */ + final public function get_filters(): array { + return array_filter($this->filters, static function(filter $filter): bool { + return $filter->get_is_available(); + }); + } + + /** + * Return all report filter instances + * + * @return filter_base[] + */ + final public function get_filter_instances(): array { + return array_map(static function(filter $filter): filter_base { + /** @var filter_base $filterclass */ + $filterclass = $filter->get_filter_class(); + + return $filterclass::create($filter); + }, $this->get_filters()); + } + + /** + * Set the filter values of the report + * + * @param array $values + * @return bool + */ + final public function set_filter_values(array $values): bool { + return user_filter_manager::set($this->report->get('id'), $values); + } + + /** + * Get the filter values of the report + * + * @return array + */ + final public function get_filter_values(): array { + return user_filter_manager::get($this->report->get('id')); + } + + /** + * Set if the report can be downloaded. + * + * @param bool $downloadable + * @param string $downloadfilename If the report is downloadable, then a filename should be provided here + */ + final public function set_downloadable(bool $downloadable, string $downloadfilename = 'export'): void { + $this->downloadable = $downloadable; + $this->downloadfilename = $downloadfilename; + } + + /** + * Get if the report can be downloaded. + * + * @return bool + */ + final public function is_downloadable(): bool { + return $this->downloadable; + } + + /** + * Return the downloadable report filename + * + * @return string + */ + final public function get_downloadfilename(): string { + return $this->downloadfilename; + } + + /** + * Returns the report context + * + * @return context + */ + public function get_context(): context { + return $this->report->get_context(); + } +} diff --git a/reportbuilder/classes/manager.php b/reportbuilder/classes/manager.php new file mode 100644 index 00000000000..550e7f3153a --- /dev/null +++ b/reportbuilder/classes/manager.php @@ -0,0 +1,87 @@ +. + +declare(strict_types=1); + +namespace core_reportbuilder; + +use stdClass; +use core_reportbuilder\local\models\report; +use core_reportbuilder\local\report\base; + +/** + * Report management class + * + * @package core_reportbuilder + * @copyright 2020 Paul Holden + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class manager { + + /** + * Return an instance of a report class from the given report persistent + * + * @param report $report + * @param array $parameters + * @return base + * @throws source_invalid_exception + * @throws source_unavailable_exception + */ + public static function get_report_from_persistent(report $report, array $parameters = []): base { + $source = $report->get('source'); + + // Throw exception for invalid or unavailable report source. + if (!self::report_source_exists($source)) { + throw new source_invalid_exception($source); + } else if (!self::report_source_available($source)) { + throw new source_unavailable_exception($source); + } + + return new $source($report, $parameters); + } + + /** + * Verify that report source exists and extends appropriate base classes + * + * @param string $source Full namespaced path to report definition + * @param string $additionalbaseclass Specify addition base class that given classname should extend + * @return bool + */ + public static function report_source_exists(string $source, string $additionalbaseclass = ''): bool { + return (class_exists($source) && is_subclass_of($source, base::class) && + (empty($additionalbaseclass) || is_subclass_of($source, $additionalbaseclass))); + } + + /** + * Verify given report source is available. Note that it is assumed caller has already checked that it exists + * + * @param string $source + * @return bool + */ + public static function report_source_available(string $source): bool { + return call_user_func([$source, 'is_available']); + } + + /** + * Create new report persistent + * + * @param stdClass $reportdata + * @return report + */ + public static function create_report_persistent(stdClass $reportdata): report { + return (new report(0, $reportdata))->create(); + } +} diff --git a/reportbuilder/classes/report_access_exception.php b/reportbuilder/classes/report_access_exception.php new file mode 100644 index 00000000000..3783747553f --- /dev/null +++ b/reportbuilder/classes/report_access_exception.php @@ -0,0 +1,38 @@ +. + +declare(strict_types=1); + +namespace core_reportbuilder; + +use moodle_exception; + +/** + * User cannot access report exception + * + * @package core_reportbuilder + * @copyright 2021 Paul Holden + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class report_access_exception extends moodle_exception { + + /** + * Constructor + */ + public function __construct() { + parent::__construct('errorreportaccess', 'reportbuilder'); + } +} diff --git a/reportbuilder/classes/source_invalid_exception.php b/reportbuilder/classes/source_invalid_exception.php new file mode 100644 index 00000000000..6edd067a1f4 --- /dev/null +++ b/reportbuilder/classes/source_invalid_exception.php @@ -0,0 +1,40 @@ +. + +declare(strict_types=1); + +namespace core_reportbuilder; + +use moodle_exception; + +/** + * Invalid report source exception + * + * @package core_reportbuilder + * @copyright 2020 Paul Holden + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class source_invalid_exception extends moodle_exception { + + /** + * Constructor + * + * @param string $source + */ + public function __construct(string $source) { + parent::__construct('errorsourceinvalid', 'reportbuilder', '', null, $source); + } +} diff --git a/reportbuilder/classes/source_unavailable_exception.php b/reportbuilder/classes/source_unavailable_exception.php new file mode 100644 index 00000000000..dc439a691be --- /dev/null +++ b/reportbuilder/classes/source_unavailable_exception.php @@ -0,0 +1,40 @@ +. + +declare(strict_types=1); + +namespace core_reportbuilder; + +use moodle_exception; + +/** + * Unavailable report source exception + * + * @package core_reportbuilder + * @copyright 2020 Paul Holden + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class source_unavailable_exception extends moodle_exception { + + /** + * Constructor + * + * @param string $source + */ + public function __construct(string $source) { + parent::__construct('errorsourceunavailable', 'reportbuilder', '', null, $source); + } +} diff --git a/reportbuilder/classes/system_report.php b/reportbuilder/classes/system_report.php new file mode 100644 index 00000000000..691e693365b --- /dev/null +++ b/reportbuilder/classes/system_report.php @@ -0,0 +1,265 @@ +. + +declare(strict_types=1); + +namespace core_reportbuilder; + +use coding_exception; +use stdClass; +use core_reportbuilder\local\models\report; +use core_reportbuilder\local\report\action; +use core_reportbuilder\local\report\base; +use core_reportbuilder\local\report\column; + +/** + * Base class for system reports + * + * @package core_reportbuilder + * @copyright 2020 Paul Holden + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +abstract class system_report extends base { + + /** @var array $parameters */ + private $parameters; + + /** @var string[] $basefields List of base fields */ + private $basefields = []; + + /** @var action[] $actions */ + private $actions = []; + + /** @var column $initialsortcolumn */ + private $initialsortcolumn; + + /** @var int $initialsortdirection */ + private $initialsortdirection; + + /** + * System report constructor. + * + * @param report $report + * @param array $parameters + */ + final public function __construct(report $report, array $parameters) { + $this->parameters = $parameters; + + parent::__construct($report); + } + + /** + * Validates access to view this report + * + * This is necessary to implement independently of the page that would typically embed the report because + * subsequent pages are requested via AJAX requests, and access should be validated each time + * + * @return bool + */ + abstract protected function can_view(): bool; + + /** + * Validate access to the report + * + * @throws report_access_exception + */ + final public function require_can_view(): void { + if (!$this->can_view()) { + throw new report_access_exception(); + } + } + + /** + * Report validation + * + * @throws report_access_exception If user cannot access the report + * @throws coding_exception If no default column are specified + */ + protected function validate(): void { + parent::validate(); + + $this->require_can_view(); + + // Ensure the report has some default columns specified. + if (empty($this->get_columns())) { + throw new coding_exception('No columns added'); + } + } + + /** + * Add list of fields that have to be always included in SQL query for actions and row classes + * + * Base fields are only available in system reports because they are not compatible with aggregation + * + * @param string $sql SQL clause for the list of fields that only uses main table or base joins + */ + final protected function add_base_fields(string $sql): void { + $this->basefields[] = $sql; + } + + /** + * Return report base fields + * + * @return array + */ + final public function get_base_fields(): array { + return $this->basefields; + } + + /** + * Adds an action to the report + * + * @param action $action + */ + final public function add_action(action $action): void { + $this->actions[] = $action; + } + + /** + * Whether report has any actions + * + * @return bool + */ + final public function has_actions(): bool { + return !empty($this->actions); + } + + /** + * Return report actions + * + * @return action[] + */ + final public function get_actions(): array { + return $this->actions; + } + + /** + * Set all report parameters + * + * @param array $parameters + */ + final public function set_parameters(array $parameters): void { + $this->parameters = $parameters; + } + + /** + * Return all report parameters + * + * @return array + */ + final public function get_parameters(): array { + return $this->parameters; + } + + /** + * Return specific report parameter + * + * @param string $param + * @param mixed $default + * @param string $type + * @return mixed + */ + final public function get_parameter(string $param, $default, string $type) { + if (!array_key_exists($param, $this->parameters)) { + return $default; + } + + return clean_param($this->parameters[$param], $type); + } + + /** + * CSS classes to add to the row. Can be overridden by system reports do define class to be added to output according to + * content of each row + * + * @param stdClass $row + * @return string + */ + public function get_row_class(stdClass $row): string { + return ''; + } + + /** + * Default 'per page' size. Can be overridden by system reports to define a different paging value + * + * @return int + */ + public function get_default_per_page(): int { + return self::DEFAULT_PAGESIZE; + } + + /** + * Called before rendering each row. Can be overridden to pre-fetch/create objects and store them in the class, which can + * later be used in column and action callbacks + * + * @param stdClass $row + */ + public function row_callback(stdClass $row): void { + return; + } + + /** + * Validates access to download this report. + * + * @return bool + */ + final public function can_be_downloaded(): bool { + return $this->can_view() && $this->is_downloadable(); + } + + /** + * Return list of column names that will be excluded when table is downloaded. Extending classes should override this method + * as appropriate + * + * @return string[] Array of column unique identifiers + */ + public function get_exclude_columns_for_download(): array { + return []; + } + + /** + * Set initial sort column and sort direction for the report + * + * @param string $uniqueidentifier + * @param int $sortdirection One of SORT_ASC or SORT_DESC + * @throws coding_exception + */ + public function set_initial_sort_column(string $uniqueidentifier, int $sortdirection): void { + if (!$sortcolumn = $this->get_column($uniqueidentifier)) { + throw new coding_exception('Unknown column identifier', $uniqueidentifier); + } + + $this->initialsortcolumn = $sortcolumn; + $this->initialsortdirection = $sortdirection; + } + + /** + * Get initial sort column + * + * @return column|null + */ + public function get_initial_sort_column(): ?column { + return $this->initialsortcolumn; + } + + /** + * Get initial sort column direction + * + * @return int + */ + public function get_initial_sort_direction(): int { + return $this->initialsortdirection; + } +} diff --git a/reportbuilder/classes/system_report_factory.php b/reportbuilder/classes/system_report_factory.php new file mode 100644 index 00000000000..36cbb4a15a0 --- /dev/null +++ b/reportbuilder/classes/system_report_factory.php @@ -0,0 +1,87 @@ +. + +declare(strict_types=1); + +namespace core_reportbuilder; + +use context; +use core_reportbuilder\local\models\report; +use core_reportbuilder\local\report\base; + +/** + * Factory class for creating system report instances + * + * @package core_reportbuilder + * @copyright 2020 Paul Holden + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class system_report_factory { + + /** + * Create and return instance of given system report source + * + * @param string $source Class path of system report definition + * @param context $context + * @param string $component + * @param string $area + * @param int $itemid + * @param array $parameters Simple key/value pairs, accessed inside reports using $this->get_parameter() + * @return system_report + * @throws source_invalid_exception + */ + public static function create(string $source, context $context, string $component = '', string $area = '', + int $itemid = 0, array $parameters = []): system_report { + + // Exit early if source isn't a system report. + if (!manager::report_source_exists($source, system_report::class)) { + throw new source_invalid_exception($source); + } + + $report = static::get_report_persistent($source, $context, $component, $area, $itemid); + + return manager::get_report_from_persistent($report, $parameters); + } + + /** + * Given a report source, with accompanying context information, return a persistent report instance + * + * @param string $source + * @param context $context + * @param string $component + * @param string $area + * @param int $itemid + * @return report + */ + private static function get_report_persistent(string $source, context $context, string $component = '', string $area = '', + int $itemid = 0): report { + + $reportdata = [ + 'type' => base::TYPE_SYSTEM_REPORT, + 'source' => $source, + 'contextid' => $context->id, + 'component' => $component, + 'area' => $area, + 'itemid' => $itemid, + ]; + + if ($report = report::get_record($reportdata)) { + return $report; + } + + return manager::create_report_persistent((object) $reportdata); + } +} diff --git a/reportbuilder/tests/local/helpers/format_test.php b/reportbuilder/tests/local/helpers/format_test.php new file mode 100644 index 00000000000..dd45a4cdb24 --- /dev/null +++ b/reportbuilder/tests/local/helpers/format_test.php @@ -0,0 +1,67 @@ +. + +declare(strict_types=1); + +namespace core_reportbuilder\local\helpers; + +use advanced_testcase; +use stdClass; + +/** + * Unit tests for the format helper + * + * @package core_reportbuilder + * @covers \core_reportbuilder\local\helpers\format + * @copyright 2021 Paul Holden + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class format_testcase extends advanced_testcase { + + /** + * Test userdate method + */ + public function test_userdate(): void { + $now = time(); + + $userdate = format::userdate($now, new stdClass()); + $this->assertEquals(userdate($now), $userdate); + } + + /** + * Data provider for {@see test_boolean_as_text} + * + * @return array + */ + public function boolean_as_text_provider(): array { + return [ + [false, get_string('no')], + [true, get_string('yes')], + ]; + } + + /** + * Test boolean as text + * + * @param bool $value + * @param string $expected + * + * @dataProvider boolean_as_text_provider + */ + public function test_boolean_as_text(bool $value, string $expected): void { + $this->assertEquals($expected, format::boolean_as_text($value)); + } +}