diff --git a/badges/classes/reportbuilder/datasource/badges.php b/badges/classes/reportbuilder/datasource/badges.php new file mode 100644 index 00000000000..707f77351cb --- /dev/null +++ b/badges/classes/reportbuilder/datasource/badges.php @@ -0,0 +1,125 @@ +. + +declare(strict_types=1); + +namespace core_badges\reportbuilder\datasource; + +use lang_string; +use core_reportbuilder\datasource; +use core_reportbuilder\local\entities\{course, user}; +use core_badges\reportbuilder\local\entities\{badge, badge_issued}; + +/** + * Badges datasource + * + * @package core_badges + * @copyright 2022 Paul Holden + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class badges extends datasource { + + /** + * Return user friendly name of the report source + * + * @return string + */ + public static function get_name(): string { + return get_string('badges', 'core_badges'); + } + + /** + * Initialise report + */ + protected function initialise(): void { + $badgeentity = new badge(); + $badgealias = $badgeentity->get_table_alias('badge'); + + $this->set_main_table('badge', $badgealias); + + $this->add_entity($badgeentity); + + // Join the badge issued entity to the badge entity. + $badgeissuedentity = new badge_issued(); + $badgeissuedalias = $badgeissuedentity->get_table_alias('badge_issued'); + + $this->add_entity($badgeissuedentity + ->add_join("LEFT JOIN {badge_issued} {$badgeissuedalias} + ON {$badgeissuedalias}.badgeid = {$badgealias}.id") + ); + + // Join the user entity to the badge issued entity. + $userentity = new user(); + $useralias = $userentity->get_table_alias('user'); + + $this->add_entity($userentity + ->add_joins($badgeissuedentity->get_joins()) + ->add_join("LEFT JOIN {user} {$useralias} + ON {$useralias}.id = {$badgeissuedalias}.userid") + ->set_entity_title(new lang_string('recipient', 'core_badges')) + ); + + // Join the course entity to the badge entity, coalescing courseid with the siteid for site badges. + $courseentity = new course(); + $coursealias = $courseentity->get_table_alias('course'); + $this->add_entity($courseentity + ->add_join("LEFT JOIN {course} {$coursealias} + ON {$coursealias}.id = COALESCE({$badgealias}.courseid, 1)") + ); + + // Add report elements from each of the entities we added to the report. + $this->add_all_from_entities(); + } + + /** + * Return the columns that will be added to the report upon creation + * + * @return string[] + */ + public function get_default_columns(): array { + return [ + 'badge:name', + 'badge:description', + 'user:fullname', + 'badge_issued:issued', + ]; + } + + /** + * Return the filters that will be added to the report upon creation + * + * @return string[] + */ + public function get_default_filters(): array { + return [ + 'badge:name', + 'user:fullname', + 'badge_issued:issued', + ]; + } + + /** + * Return the conditions that will be added to the report upon creation + * + * @return string[] + */ + public function get_default_conditions(): array { + return [ + 'badge:type', + 'badge:name', + ]; + } +} diff --git a/badges/classes/reportbuilder/local/entities/badge.php b/badges/classes/reportbuilder/local/entities/badge.php new file mode 100644 index 00000000000..44bdb2f2d39 --- /dev/null +++ b/badges/classes/reportbuilder/local/entities/badge.php @@ -0,0 +1,280 @@ +. + +declare(strict_types=1); + +namespace core_badges\reportbuilder\local\entities; + +use context_course; +use context_system; +use html_writer; +use lang_string; +use moodle_url; +use stdClass; +use core_reportbuilder\local\entities\base; +use core_reportbuilder\local\filters\{select, text}; +use core_reportbuilder\local\report\{column, filter}; + +defined('MOODLE_INTERNAL') or die; + +global $CFG; +require_once("{$CFG->libdir}/badgeslib.php"); + +/** + * Badge entity + * + * @package core_badges + * @copyright 2022 Paul Holden + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class badge extends base { + + /** + * Database tables that this entity uses and their default aliases + * + * @return array + */ + protected function get_default_table_aliases(): array { + return ['badge' => 'b']; + } + + /** + * The default title for this entity + * + * @return lang_string + */ + protected function get_default_entity_title(): lang_string { + return new lang_string('badgedetails', 'core_badges'); + } + + /** + * Initialise the entity + * + * @return base + */ + public function initialise(): base { + $columns = $this->get_all_columns(); + foreach ($columns as $column) { + $this->add_column($column); + } + + // All the filters defined by the entity can also be used as conditions. + $filters = $this->get_all_filters(); + foreach ($filters as $filter) { + $this + ->add_filter($filter) + ->add_condition($filter); + } + + return $this; + } + + /** + * Returns list of all available columns + * + * @return column[] + */ + protected function get_all_columns(): array { + $badgealias = $this->get_table_alias('badge'); + + // Name. + $columns[] = (new column( + 'name', + new lang_string('name'), + $this->get_entity_name() + )) + ->add_joins($this->get_joins()) + ->set_type(column::TYPE_TEXT) + ->add_field("{$badgealias}.name") + ->set_is_sortable(true); + + // Description. + $columns[] = (new column( + 'description', + new lang_string('description', 'core_badges'), + $this->get_entity_name() + )) + ->add_joins($this->get_joins()) + ->set_type(column::TYPE_LONGTEXT) + ->add_field("{$badgealias}.description"); + + // Criteria. + $columns[] = (new column( + 'criteria', + new lang_string('bcriteria', 'core_badges'), + $this->get_entity_name() + )) + ->add_joins($this->get_joins()) + ->set_type(column::TYPE_TEXT) + ->add_field("{$badgealias}.id") + ->set_disabled_aggregation_all() + ->add_callback(static function($badgeid): string { + global $PAGE; + + $badge = new \core_badges\badge($badgeid); + + $renderer = $PAGE->get_renderer('core_badges'); + return $renderer->print_badge_criteria($badge, 'short'); + }); + + // Image. + $columns[] = (new column( + 'image', + new lang_string('badgeimage', 'core_badges'), + $this->get_entity_name() + )) + ->add_joins($this->get_joins()) + ->set_type(column::TYPE_INTEGER) + ->add_fields("{$badgealias}.id, {$badgealias}.type, {$badgealias}.courseid, {$badgealias}.imagecaption") + ->set_disabled_aggregation_all() + ->add_callback(static function(int $badgeid, stdClass $badge): string { + $context = $badge->type == BADGE_TYPE_SITE + ? context_system::instance() + : context_course::instance($badge->courseid); + + $badgeimage = moodle_url::make_pluginfile_url($context->id, 'badges', 'badgeimage', $badgeid, '/', 'f2'); + return html_writer::img($badgeimage, $badge->imagecaption); + }); + + // Language. + $columns[] = (new column( + 'language', + new lang_string('language'), + $this->get_entity_name() + )) + ->add_joins($this->get_joins()) + ->set_type(column::TYPE_TEXT) + ->add_field("{$badgealias}.language") + ->set_is_sortable(true) + ->add_callback(static function($language): string { + $languages = get_string_manager()->get_list_of_languages(); + return $languages[$language] ?? $language; + }); + + // Version. + $columns[] = (new column( + 'version', + new lang_string('version', 'core_badges'), + $this->get_entity_name() + )) + ->add_joins($this->get_joins()) + ->set_type(column::TYPE_TEXT) + ->add_field("{$badgealias}.version") + ->set_is_sortable(true); + + // Status. + $columns[] = (new column( + 'status', + new lang_string('status', 'core_badges'), + $this->get_entity_name() + )) + ->add_joins($this->get_joins()) + ->set_type(column::TYPE_TEXT) + ->add_field("{$badgealias}.status") + ->set_is_sortable(true) + ->add_callback(static function($status): string { + return get_string("badgestatus_{$status}", 'core_badges'); + }); + + // Expiry date/period. + $columns[] = (new column( + 'expiry', + new lang_string('expirydate', 'core_badges'), + $this->get_entity_name() + )) + ->add_joins($this->get_joins()) + ->set_type(column::TYPE_TIMESTAMP) + ->add_fields("{$badgealias}.expiredate, {$badgealias}.expireperiod") + ->set_is_sortable(true, ["{$badgealias}.expiredate", "{$badgealias}.expireperiod"]) + ->set_disabled_aggregation_all() + ->add_callback(static function(int $expiredate, stdClass $badge): string { + if ($expiredate) { + return userdate($expiredate); + } else if ($badge->expireperiod) { + return format_time($badge->expireperiod); + } else { + return get_string('never', 'core_badges'); + } + }); + + // Image author details. + foreach (['imageauthorname', 'imageauthoremail', 'imageauthorurl'] as $imageauthorfield) { + $columns[] = (new column( + $imageauthorfield, + new lang_string($imageauthorfield, 'core_badges'), + $this->get_entity_name() + )) + ->add_joins($this->get_joins()) + ->set_type(column::TYPE_TEXT) + ->add_field("{$badgealias}.{$imageauthorfield}") + ->set_is_sortable(true); + } + + return $columns; + } + + /** + * Return list of all available filters + * + * @return filter[] + */ + protected function get_all_filters(): array { + $badgealias = $this->get_table_alias('badge'); + + // Name. + $filters[] = (new filter( + text::class, + 'name', + new lang_string('name'), + $this->get_entity_name(), + "{$badgealias}.name" + )) + ->add_joins($this->get_joins()); + + // Status. + $filters[] = (new filter( + select::class, + 'status', + new lang_string('status', 'core_badges'), + $this->get_entity_name(), + "{$badgealias}.status" + )) + ->add_joins($this->get_joins()) + ->set_options([ + BADGE_STATUS_INACTIVE => new lang_string('badgestatus_0', 'core_badges'), + BADGE_STATUS_ACTIVE => new lang_string('badgestatus_1', 'core_badges'), + BADGE_STATUS_INACTIVE_LOCKED => new lang_string('badgestatus_2', 'core_badges'), + BADGE_STATUS_ACTIVE_LOCKED => new lang_string('badgestatus_3', 'core_badges'), + BADGE_STATUS_ARCHIVED => new lang_string('badgestatus_4', 'core_badges'), + ]); + + // Type. + $filters[] = (new filter( + select::class, + 'type', + new lang_string('type', 'core_badges'), + $this->get_entity_name(), + "{$badgealias}.type" + )) + ->add_joins($this->get_joins()) + ->set_options([ + BADGE_TYPE_SITE => new lang_string('site'), + BADGE_TYPE_COURSE => new lang_string('course'), + ]); + + return $filters; + } +} diff --git a/badges/classes/reportbuilder/local/entities/badge_issued.php b/badges/classes/reportbuilder/local/entities/badge_issued.php new file mode 100644 index 00000000000..fd5df1d0bd8 --- /dev/null +++ b/badges/classes/reportbuilder/local/entities/badge_issued.php @@ -0,0 +1,162 @@ +. + +declare(strict_types=1); + +namespace core_badges\reportbuilder\local\entities; + +use lang_string; +use core_reportbuilder\local\entities\base; +use core_reportbuilder\local\filters\{boolean_select, date}; +use core_reportbuilder\local\helpers\format; +use core_reportbuilder\local\report\{column, filter}; + +/** + * Badge issued entity + * + * @package core_badges + * @copyright 2022 Paul Holden + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class badge_issued extends base { + + /** + * Database tables that this entity uses and their default aliases + * + * @return array + */ + protected function get_default_table_aliases(): array { + return ['badge_issued' => 'bi']; + } + + /** + * The default title for this entity + * + * @return lang_string + */ + protected function get_default_entity_title(): lang_string { + return new lang_string('badgeissued', 'core_badges'); + } + + /** + * Initialise the entity + * + * @return base + */ + public function initialise(): base { + $columns = $this->get_all_columns(); + foreach ($columns as $column) { + $this->add_column($column); + } + + // All the filters defined by the entity can also be used as conditions. + $filters = $this->get_all_filters(); + foreach ($filters as $filter) { + $this + ->add_filter($filter) + ->add_condition($filter); + } + + return $this; + } + + /** + * Returns list of all available columns + * + * @return column[] + */ + protected function get_all_columns(): array { + $badgeissuedalias = $this->get_table_alias('badge_issued'); + + // Date issued. + $columns[] = (new column( + 'issued', + new lang_string('dateawarded', 'core_badges'), + $this->get_entity_name() + )) + ->add_joins($this->get_joins()) + ->set_type(column::TYPE_TIMESTAMP) + ->add_field("{$badgeissuedalias}.dateissued") + ->set_is_sortable(true) + ->add_callback([format::class, 'userdate']); + + // Date expires. + $columns[] = (new column( + 'expire', + new lang_string('expirydate', 'core_badges'), + $this->get_entity_name() + )) + ->add_joins($this->get_joins()) + ->set_type(column::TYPE_TIMESTAMP) + ->add_field("{$badgeissuedalias}.dateexpire") + ->set_is_sortable(true) + ->add_callback([format::class, 'userdate']); + + // Visible. + $columns[] = (new column( + 'visible', + new lang_string('visible', 'core_badges'), + $this->get_entity_name() + )) + ->add_joins($this->get_joins()) + ->set_type(column::TYPE_BOOLEAN) + ->add_fields("{$badgeissuedalias}.visible") + ->add_callback([format::class, 'boolean_as_text']); + + return $columns; + } + + /** + * Return list of all available filters + * + * @return filter[] + */ + protected function get_all_filters(): array { + $badgealias = $this->get_table_alias('badge_issued'); + + // Date issued. + $filters[] = (new filter( + date::class, + 'issued', + new lang_string('dateawarded', 'core_badges'), + $this->get_entity_name(), + "{$badgealias}.dateissued" + )) + ->add_joins($this->get_joins()); + + // Date expires. + $filters[] = (new filter( + date::class, + 'expires', + new lang_string('expirydate', 'core_badges'), + $this->get_entity_name(), + "{$badgealias}.dateexpire" + )) + ->add_joins($this->get_joins()); + + // Visible. + $filters[] = (new filter( + boolean_select::class, + 'visible', + new lang_string('visible', 'core_badges'), + $this->get_entity_name(), + "{$badgealias}.visible" + )) + ->add_joins($this->get_joins()); + + return $filters; + } +} diff --git a/badges/tests/reportbuilder/datasource/badges_test.php b/badges/tests/reportbuilder/datasource/badges_test.php new file mode 100644 index 00000000000..06db5e24162 --- /dev/null +++ b/badges/tests/reportbuilder/datasource/badges_test.php @@ -0,0 +1,135 @@ +. + +declare(strict_types=1); + +namespace core_badges\reportbuilder\datasource; + +use core_badges\badge; +use core_reportbuilder_generator; +use core_reportbuilder_testcase; + +defined('MOODLE_INTERNAL') || die(); + +global $CFG; +require_once("{$CFG->dirroot}/reportbuilder/tests/helpers.php"); + +/** + * Unit tests for badges datasource + * + * @package core_badges + * @covers \core_badges\reportbuilder\datasource\badges + * @copyright 2022 Paul Holden + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class badges_test extends core_reportbuilder_testcase { + + /** + * Load required test libraries + */ + public static function setUpBeforeClass(): void { + global $CFG; + require_once("{$CFG->libdir}/badgeslib.php"); + } + + /** + * Test datasource + */ + public function test_datasource(): void { + $this->resetAfterTest(); + $this->setAdminUser(); + + // Test users with a badge we can issue them with. + $user1 = $this->getDataGenerator()->create_user(['firstname' => 'Alan', 'lastname' => 'Apple']); + $user2 = $this->getDataGenerator()->create_user(['firstname' => 'Barry', 'lastname' => 'Banana']); + + $sitebadge = $this->create_badge(['name' => 'Badge 1']); + $sitebadge->issue($user1->id, true); + $sitebadge->issue($user2->id, true); + + // Another badge, in a course, no issues. + $course = $this->getDataGenerator()->create_course(); + $coursebadge = $this->create_badge(['name' => 'Badge 2', 'type' => BADGE_TYPE_COURSE, 'courseid' => $course->id]); + + /** @var core_reportbuilder_generator $generator */ + $generator = $this->getDataGenerator()->get_plugin_generator('core_reportbuilder'); + $report = $generator->create_report(['name' => 'Badges', 'source' => badges::class, 'default' => 0]); + + // Badge course. + $generator->create_column(['reportid' => $report->get('id'), 'uniqueidentifier' => 'course:fullname']) + ->set_many(['sortenabled' => true, 'sortdirection' => SORT_ASC])->update(); + + // Badge name. + $generator->create_column(['reportid' => $report->get('id'), 'uniqueidentifier' => 'badge:name']) + ->set_many(['sortenabled' => true, 'sortdirection' => SORT_ASC])->update(); + + // User fullname. + $generator->create_column(['reportid' => $report->get('id'), 'uniqueidentifier' => 'user:fullname']) + ->set_many(['sortenabled' => true, 'sortdirection' => SORT_ASC])->update(); + + $content = $this->get_custom_report_content($report->get('id')); + $this->assertCount(3, $content); + + $this->assertEquals([ + ['PHPUnit test site', $sitebadge->name, fullname($user1, true)], + ['PHPUnit test site', $sitebadge->name, fullname($user2, true)], + [$course->fullname, $coursebadge->name, ''], + ], array_map(static function(array $row): array { + return array_values($row); + }, $content)); + } + + /** + * Helper method to create a badge + * + * @param array $params + * @return badge + */ + protected function create_badge(array $params = []): badge { + global $DB, $USER; + + $record = (object) array_merge([ + 'name' => 'Test badge', + 'description' => 'Testing badges', + 'timecreated' => time(), + 'timemodified' => time(), + 'usercreated' => $USER->id, + 'usermodified' => $USER->id, + 'issuername' => 'Test issuer', + 'issuerurl' => 'http://issuer-url.domain.co.nz', + 'issuercontact' => 'issuer@example.com', + 'expiredate' => null, + 'expireperiod' => null, + 'type' => BADGE_TYPE_SITE, + 'courseid' => null, + 'messagesubject' => 'Test message subject', + 'message' => 'Test message body', + 'attachment' => 1, + 'notification' => 0, + 'status' => BADGE_STATUS_ACTIVE, + 'version' => OPEN_BADGES_V2, + 'language' => 'en', + 'imageauthorname' => 'Image author', + 'imageauthoremail' => 'author@example.com', + 'imageauthorurl' => 'http://image.example.com/', + 'imagecaption' => 'Image caption' + ], $params); + + $record->id = $DB->insert_record('badge', $record); + + return new badge($record->id); + } +} diff --git a/lang/en/badges.php b/lang/en/badges.php index 390c4bfea1a..b787b04d237 100644 --- a/lang/en/badges.php +++ b/lang/en/badges.php @@ -141,7 +141,7 @@ $string['badges'] = 'Badges'; $string['badgedetails'] = 'Badge details'; $string['badgeimage'] = 'Image'; $string['badgeimage_help'] = 'The image should be at least 300 x 300 pixels in size. It will be displayed as 300 x 300 pixels on the badge page and 100 x 100 pixels on the user\'s profile page.'; - +$string['badgeissued'] = 'Badge issued'; $string['badgeprivacysetting'] = 'Badge privacy settings'; $string['badgeprivacysetting_help'] = 'Badges you earn can be displayed on your account profile page. This setting allows you to automatically set the visibility of the newly earned badges. @@ -493,6 +493,7 @@ $string['privacy:metadata:manualaward:datemet'] = 'The date when the user was aw $string['privacy:metadata:manualaward:issuerid'] = 'The ID of the user awarding the badge'; $string['privacy:metadata:manualaward:issuerrole'] = 'The role of the user awarding the badge'; $string['privacy:metadata:manualaward:recipientid'] = 'The ID of the user who is manually awarded a badge'; +$string['recipient'] = 'Badge recipient'; $string['recipients'] = 'Badge recipients'; $string['recipientvalidationproblem'] = 'This user cannot be verified as a recipient of this badge.'; $string['relative'] = 'Relative date';