diff --git a/backup/moodle2/backup_stepslib.php b/backup/moodle2/backup_stepslib.php index 3628f141287..00f6e79726f 100644 --- a/backup/moodle2/backup_stepslib.php +++ b/backup/moodle2/backup_stepslib.php @@ -939,6 +939,9 @@ class backup_badges_structure_step extends backup_structure_step { $manual_award = new backup_nested_element('manual_award', array('id'), array('badgeid', 'recipientid', 'issuerid', 'issuerrole', 'datemet')); + $tags = new backup_nested_element('tags'); + $tag = new backup_nested_element('tag', ['id'], ['name', 'rawname']); + // Build the tree. $badges->add_child($badge); @@ -953,6 +956,8 @@ class backup_badges_structure_step extends backup_structure_step { $relatedbadges->add_child($relatedbadge); $badge->add_child($manual_awards); $manual_awards->add_child($manual_award); + $badge->add_child($tags); + $tags->add_child($tag); // Define sources. @@ -980,6 +985,12 @@ class backup_badges_structure_step extends backup_structure_step { $manual_award->set_source_table('badge_manual_award', array('badgeid' => backup::VAR_PARENTID)); + $tag->set_source_sql('SELECT t.id, t.name, t.rawname + FROM {tag} t + JOIN {tag_instance} ti ON ti.tagid = t.id + WHERE ti.itemtype = ? + AND ti.itemid = ?', [backup_helper::is_sqlparam('badge'), backup::VAR_PARENTID]); + // Define id annotations. $badge->annotate_ids('user', 'usercreated'); diff --git a/backup/moodle2/restore_stepslib.php b/backup/moodle2/restore_stepslib.php index e21ac211ea0..9cbd36d7167 100644 --- a/backup/moodle2/restore_stepslib.php +++ b/backup/moodle2/restore_stepslib.php @@ -2621,6 +2621,7 @@ class restore_badges_structure_step extends restore_structure_step { $paths[] = new restore_path_element('alignment', '/badges/badge/alignments/alignment'); $paths[] = new restore_path_element('relatedbadge', '/badges/badge/relatedbadges/relatedbadge'); $paths[] = new restore_path_element('manual_award', '/badges/badge/manual_awards/manual_award'); + $paths[] = new restore_path_element('tag', '/badges/badge/tags/tag'); return $paths; } @@ -2830,6 +2831,22 @@ class restore_badges_structure_step extends restore_structure_step { } } + /** + * Process tag. + * + * @param array $data The data. + * @throws base_step_exception + */ + public function process_tag(array $data): void { + $data = (object)$data; + $badgeid = $this->get_new_parentid('badge'); + + if (!empty($data->rawname)) { + core_tag_tag::add_item_tag('core_badges', 'badge', $badgeid, + context_course::instance($this->get_courseid()), $data->rawname); + } + } + protected function after_execute() { global $DB; // Add related files. diff --git a/badges/badge_json.php b/badges/badge_json.php index 60f25a589f9..abcb4fe2489 100644 --- a/badges/badge_json.php +++ b/badges/badge_json.php @@ -80,6 +80,10 @@ if ($badge->status != BADGE_STATUS_INACTIVE) { if (!empty($badge->language)) { $json['@language'] = $badge->language; } + $badgetags = $badge->get_badge_tags(); + if ($badgetags) { + $json['tags'] = $badgetags; + } $relatedbadges = $badge->get_related_badges(true); if (!empty($relatedbadges)) { diff --git a/badges/classes/assertion.php b/badges/classes/assertion.php index e15882b708d..1fc9d48c105 100644 --- a/badges/classes/assertion.php +++ b/badges/classes/assertion.php @@ -161,6 +161,10 @@ class core_badges_assertion { if (!empty($this->_data->dateexpire)) { $assertion['expires'] = $this->_data->dateexpire; } + $tags = $this->get_tags(); + if (is_array($tags) && count($tags) > 0) { + $assertion['tags'] = $tags; + } $this->embed_data_badge_version2($assertion, OPEN_BADGES_V2_TYPE_ASSERTION); } return $assertion; @@ -205,6 +209,10 @@ class core_badges_assertion { $issuerurl = new moodle_url('/badges/issuer_json.php', $params); $class['issuer'] = $issuerurl->out(false); } + $tags = $this->get_tags(); + if (is_array($tags) && count($tags) > 0) { + $class['tags'] = $tags; + } $this->embed_data_badge_version2($class, OPEN_BADGES_V2_TYPE_BADGE); if (!$issued) { unset($class['issuer']); @@ -404,4 +412,13 @@ class core_badges_assertion { } } } + + /** + * Get tags of the badge. + * + * @return array tags. + */ + public function get_tags(): array { + return array_values(\core_tag_tag::get_item_tags_array('core_badges', 'badge', $this->get_badge_id())); + } } diff --git a/badges/classes/badge.php b/badges/classes/badge.php index 93ee6d32a03..be27fe6d177 100644 --- a/badges/classes/badge.php +++ b/badges/classes/badge.php @@ -302,6 +302,7 @@ class badge { $fordb->usermodified = $USER->id; $fordb->timecreated = time(); $fordb->timemodified = time(); + $tags = $this->get_badge_tags(); unset($fordb->id); if ($fordb->notification > 1) { @@ -313,6 +314,8 @@ class badge { if ($new = $DB->insert_record('badge', $fordb, true)) { $newbadge = new badge($new); + // Copy badge tags. + \core_tag_tag::set_item_tags('core_badges', 'badge', $newbadge->id, $this->get_context(), $tags); // Copy badge image. $fs = get_file_storage(); @@ -745,6 +748,9 @@ class badge { $DB->delete_records_select('badge_related', $relatedsql, $relatedparams); $DB->delete_records('badge_alignment', array('badgeid' => $this->id)); + // Delete all tags. + \core_tag_tag::remove_all_item_tags('core_badges', 'badge', $this->id); + // Finally, remove badge itself. $DB->delete_records('badge', array('id' => $this->id)); @@ -976,4 +982,13 @@ class badge { return $issuer; } + + /** + * Get tags of badge. + * + * @return array Badge tags. + */ + public function get_badge_tags(): array { + return array_values(\core_tag_tag::get_item_tags_array('core_badges', 'badge', $this->id)); + } } diff --git a/badges/classes/form/badge.php b/badges/classes/form/badge.php index 6626b584810..f64f201e42c 100644 --- a/badges/classes/form/badge.php +++ b/badges/classes/form/badge.php @@ -92,7 +92,7 @@ class badge extends moodleform { $mform->addElement('text', 'imagecaption', get_string('imagecaption', 'badges'), array('size' => '70')); $mform->setType('imagecaption', PARAM_TEXT); $mform->addHelpButton('imagecaption', 'imagecaption', 'badges'); - + $mform->addElement('tags', 'tags', get_string('tags', 'badges'), ['itemtype' => 'badge', 'component' => 'core_badges']); if (badges_open_badges_backpack_api() == OPEN_BADGES_V1) { $mform->addElement('header', 'issuerdetails', get_string('issuerdetails', 'badges')); @@ -173,22 +173,23 @@ class badge extends moodleform { /** * Load in existing data as form defaults * - * @param stdClass|array $default_values object or array of default values + * @param stdClass|array $badge object or array of default values */ public function set_data($badge) { - $default_values = array(); + $defaultvalues = []; parent::set_data($badge); if (!empty($badge->expiredate)) { - $default_values['expiry'] = 1; - $default_values['expiredate'] = $badge->expiredate; + $defaultvalues['expiry'] = 1; + $defaultvalues['expiredate'] = $badge->expiredate; } else if (!empty($badge->expireperiod)) { - $default_values['expiry'] = 2; - $default_values['expireperiod'] = $badge->expireperiod; + $defaultvalues['expiry'] = 2; + $defaultvalues['expireperiod'] = $badge->expireperiod; } - $default_values['currentimage'] = print_badge_image($badge, $badge->get_context(), 'large'); + $defaultvalues['tags'] = \core_tag_tag::get_item_tags_array('core_badges', 'badge', $badge->id); + $defaultvalues['currentimage'] = print_badge_image($badge, $badge->get_context(), 'large'); - parent::set_data($default_values); + parent::set_data($defaultvalues); } /** diff --git a/badges/classes/output/badgeclass.php b/badges/classes/output/badgeclass.php index d66b37fd0c0..c33944c789d 100644 --- a/badges/classes/output/badgeclass.php +++ b/badges/classes/output/badgeclass.php @@ -170,6 +170,11 @@ class badgeclass implements renderable { } } + // Field: Tags. + $tags = \core_tag_tag::get_item_tags('core_badges', 'badge', $this->badgeid); + $taglist = new \core_tag\output\taglist($tags); + $data->badgetag = $taglist->export_for_template($output); + return $data; } } diff --git a/badges/classes/output/issued_badge.php b/badges/classes/output/issued_badge.php index 5291c25dd3f..6fbf793ab93 100644 --- a/badges/classes/output/issued_badge.php +++ b/badges/classes/output/issued_badge.php @@ -242,6 +242,11 @@ class issued_badge implements renderable { } } + // Field: Tags. + $tags = \core_tag_tag::get_item_tags('core_badges', 'badge', $this->badgeid); + $taglist = new \core_tag\output\taglist($tags); + $data->badgetag = $taglist->export_for_template($output); + return $data; } } diff --git a/badges/edit.php b/badges/edit.php index b556c69b3a8..0d6d99af5aa 100644 --- a/badges/edit.php +++ b/badges/edit.php @@ -117,6 +117,8 @@ if ($form->is_cancelled()) { unset($badge->message_editor); if ($badge->save()) { + core_tag_tag::set_item_tags('core_badges', 'badge', $badge->id, $context, $data->tags); + $badge->tags = core_tag_tag::get_item_tags_array('core_badges', 'badge', $badge->id); badges_process_badge_image($badge, $form->save_temp_file('image')); $form->set_data($badge); $statusmsg = get_string('changessaved'); diff --git a/badges/lib.php b/badges/lib.php index 4c2a0ff7a9b..ebc47b1eb8b 100644 --- a/badges/lib.php +++ b/badges/lib.php @@ -73,3 +73,44 @@ function core_badges_myprofile_navigation(\core_user\output\myprofile\tree $tree } } } + +/** + * Returns badges tagged with a specified tag. + * + * @param object $tag + * @param bool $exclusivemode if set to true it means that no other entities tagged with this tag + * are displayed on the page and the per-page limit may be bigger + * @param null|int $fromctx context id where the link was displayed, may be used by callbacks + * to display items in the same context first + * @param null|int $ctx context id where to search for records + * @param bool $rec search in subcontexts as well + * @param int $page 0-based number of page being displayed + * @return \core_tag\output\tagindex + */ +function badge_get_tagged_badges(object $tag, bool $exclusivemode = false, null|int $fromctx = 0, null|int $ctx = 0, + bool $rec = true, int $page = 0): object { + global $OUTPUT; + + $badgecount = $tag->count_tagged_items('core_badges', 'badge'); + $perpage = $exclusivemode ? 20 : 5; + $content = ''; + $totalpages = ceil($badgecount / $perpage); + + if ($badgecount) { + $badges = $tag->get_tagged_items('core_badges', 'badge', $page * $perpage, $perpage); + $tagfeed = new core_tag\output\tagfeed(); + foreach ($badges as $badge) { + $badgelink = new moodle_url('/badges/badgeclass.php', ['id' => $badge->id]); + $fullname = html_writer::link($badgelink, $badge->name); + $icon = html_writer::link($badgelink, html_writer::empty_tag('img', + ['src' => $OUTPUT->image_url('i/badge')])); + $tagfeed->add($icon, $fullname, '
'); + } + + $items = $tagfeed->export_for_template($OUTPUT); + + $content .= $OUTPUT->render_from_template('core_tag/tagfeed', $items); + } + return new core_tag\output\tagindex($tag, 'core_badges', 'badge', $content, + $exclusivemode, $fromctx, $ctx, $rec, $page, $totalpages); +} diff --git a/badges/newbadge.php b/badges/newbadge.php index d6bbbcdee5f..fe60d03e045 100644 --- a/badges/newbadge.php +++ b/badges/newbadge.php @@ -116,6 +116,7 @@ if ($form->is_cancelled()) { $event->trigger(); $newbadge = new badge($newid); + core_tag_tag::set_item_tags('core_badges', 'badge', $newid, $PAGE->context, $data->tags); badges_process_badge_image($newbadge, $form->save_temp_file('image')); // If a user can configure badge criteria, they will be redirected to the criteria page. if (has_capability('moodle/badges:configurecriteria', $PAGE->context)) { diff --git a/badges/renderer.php b/badges/renderer.php index 57cc70a7312..3a83326386f 100644 --- a/badges/renderer.php +++ b/badges/renderer.php @@ -192,6 +192,8 @@ class core_badges_renderer extends plugin_renderer_base { $dl[get_string('imageauthorurl', 'badges')] = html_writer::link($badge->imageauthorurl, $badge->imageauthorurl, array('target' => '_blank')); $dl[get_string('imagecaption', 'badges')] = $badge->imagecaption; + $tags = \core_tag_tag::get_item_tags('core_badges', 'badge', $badge->id); + $dl[get_string('tags', 'badges')] = $this->output->tag_list($tags, ''); $display .= $this->definition_list($dl); // Issuer details. diff --git a/badges/templates/issued_badge.mustache b/badges/templates/issued_badge.mustache index b4b870c377b..40d561926c4 100644 --- a/badges/templates/issued_badge.mustache +++ b/badges/templates/issued_badge.mustache @@ -211,6 +211,11 @@ {{/coursefullname}} + {{#badgetag}} +

+ {{> core_tag/taglist}} +

+ {{/badgetag}}

{{{badgedescription}}}

diff --git a/badges/tests/badgeslib_test.php b/badges/tests/badgeslib_test.php index e1469437d45..d6ff94f5910 100644 --- a/badges/tests/badgeslib_test.php +++ b/badges/tests/badgeslib_test.php @@ -135,9 +135,13 @@ class badgeslib_test extends advanced_testcase { $alignment->targetcode = 'CCSS.RST.11-12.3'; $DB->insert_record('badge_alignment', $alignment, true); + // Insert tags. + core_tag_tag::set_item_tags('core_badges', 'badge', $badge->id, $badge->get_context(), ['tag1', 'tag2']); + $this->assertion = new stdClass(); - $this->assertion->badge = '{"uid":"%s","recipient":{"identity":"%s","type":"email","hashed":true,"salt":"%s"},"badge":"%s","verify":{"type":"hosted","url":"%s"},"issuedOn":"%d","evidence":"%s"}'; - $this->assertion->class = '{"name":"%s","description":"%s","image":"%s","criteria":"%s","issuer":"%s"}'; + $this->assertion->badge = '{"uid":"%s","recipient":{"identity":"%s","type":"email","hashed":true,"salt":"%s"},' . + '"badge":"%s","verify":{"type":"hosted","url":"%s"},"issuedOn":"%d","evidence":"%s","tags":%s}'; + $this->assertion->class = '{"name":"%s","description":"%s","image":"%s","criteria":"%s","issuer":"%s","tags":%s}'; $this->assertion->issuer = '{"name":"%s","url":"%s","email":"%s"}'; // Format JSON-LD for Openbadge specification version 2.0. $this->assertion2 = new stdClass(); @@ -145,16 +149,16 @@ class badgeslib_test extends advanced_testcase { '"badge":{"name":"%s","description":"%s","image":"%s",' . '"criteria":{"id":"%s","narrative":"%s"},"issuer":{"name":"%s","url":"%s","email":"%s",' . '"@context":"https:\/\/w3id.org\/openbadges\/v2","id":"%s","type":"Issuer"},' . - '"@context":"https:\/\/w3id.org\/openbadges\/v2","id":"%s","type":"BadgeClass","version":"%s",' . + '"tags":%s,"@context":"https:\/\/w3id.org\/openbadges\/v2","id":"%s","type":"BadgeClass","version":"%s",' . '"@language":"en","related":[{"id":"%s","version":"%s","@language":"%s"}],"endorsement":"%s",' . '"alignments":[{"targetName":"%s","targetUrl":"%s","targetDescription":"%s","targetFramework":"%s",' . - '"targetCode":"%s"}]},"verify":{"type":"hosted","url":"%s"},"issuedOn":"%s","evidence":"%s",' . + '"targetCode":"%s"}]},"verify":{"type":"hosted","url":"%s"},"issuedOn":"%s","evidence":"%s","tags":%s,' . '"@context":"https:\/\/w3id.org\/openbadges\/v2","type":"Assertion","id":"%s"}'; $this->assertion2->class = '{"name":"%s","description":"%s","image":"%s",' . '"criteria":{"id":"%s","narrative":"%s"},"issuer":{"name":"%s","url":"%s","email":"%s",' . '"@context":"https:\/\/w3id.org\/openbadges\/v2","id":"%s","type":"Issuer"},' . - '"@context":"https:\/\/w3id.org\/openbadges\/v2","id":"%s","type":"BadgeClass","version":"%s",' . + '"tags":%s,"@context":"https:\/\/w3id.org\/openbadges\/v2","id":"%s","type":"BadgeClass","version":"%s",' . '"@language":"%s","related":[{"id":"%s","version":"%s","@language":"%s"}],"endorsement":"%s",' . '"alignments":[{"targetName":"%s","targetUrl":"%s","targetDescription":"%s","targetFramework":"%s",' . '"targetCode":"%s"}]}'; @@ -217,6 +221,8 @@ class badgeslib_test extends advanced_testcase { global $DB; $badge = new badge($this->badgeid); + // Insert tags for the badge. + core_tag_tag::set_item_tags('core_badges', 'badge', $badge->id, $badge->get_context(), ['tag1', 'tag2']); $newid1 = $badge->make_clone(); $newid2 = $badge->make_clone(); @@ -239,6 +245,9 @@ class badgeslib_test extends advanced_testcase { // Badge 1 has 4 related records. 3 where it's the badgeid, 1 where it's the relatedbadgeid. $this->assertEquals(4, $DB->count_records_select('badge_related', $relatedsql, $relatedparams)); + // Badge has 2 tag instance records. + $this->assertEquals(2, $DB->count_records('tag_instance', ['itemid' => $this->badgeid])); + // Delete the badge for real. $badge->delete(false); @@ -247,6 +256,9 @@ class badgeslib_test extends advanced_testcase { // Confirm that the records about this badge about its relations have been removed as well. $this->assertFalse($DB->record_exists_select('badge_related', $relatedsql, $relatedparams)); + + // Confirm that the tag instance of the badge has been removed. + $this->assertFalse($DB->record_exists('tag_instance', ['itemid' => $this->badgeid])); } public function test_create_badge_criteria() { @@ -1638,4 +1650,24 @@ class badgeslib_test extends advanced_testcase { $backpack = badges_get_site_backpack($backpackid); } } + + /** + * Testing function test_badge_get_tagged_badges - search tagged badges + * + * @covers ::badge_get_tagged_badges + */ + public function test_badge_get_tagged_badges() { + $this->resetAfterTest(); + $this->setAdminUser(); + + // Setup test data. + $badge = new badge($this->coursebadge); + \core_tag_tag::set_item_tags('core_badges', 'badge', $this->badgeid, $badge->get_context(), + ['Cats', 'Dogs']); + + $tag = \core_tag_tag::get_by_name(0, 'Cats'); + + $res = badge_get_tagged_badges($tag, false, 0, 0, 1, 0); + $this->assertStringContainsString("Test badge with 'apostrophe' and other friends (<>&@#)", $res->content); + } } diff --git a/badges/tests/behat/add_badge.feature b/badges/tests/behat/add_badge.feature index 276ca6546e6..561d661242d 100644 --- a/badges/tests/behat/add_badge.feature +++ b/badges/tests/behat/add_badge.feature @@ -33,6 +33,7 @@ Feature: Add badges to the system | Description | Test badge description | | Image author | http://author.example.com | | Image caption | Test caption image | + | Tags | Math, Physics | And I upload "badges/tests/behat/badge.png" file to "Image" filemanager When I press "Create badge" Then I should see "Edit details" @@ -46,6 +47,9 @@ Feature: Add badges to the system And I should see "Issuer details" And I should see "Test Badge Site" And I should see "testuser@example.com" + And I should see "Tags" + And I should see "Math" + And I should see "Physics" And I navigate to "Badges > Manage badges" in site administration And I should not see "There are currently no badges available for users to earn." @@ -181,17 +185,24 @@ Feature: Add badges to the system | Description | Test badge description | | Image author | http://author.example.com | | Image caption | Test caption image | + | Tags | Math, Physics | And I upload "badges/tests/behat/badge.png" file to "Image" filemanager And I press "Create badge" When I select "Edit details" from the "jump" singleselect And I should see "Test badge with 'apostrophe' and other friends (&@#)" And I should not see "Issuer details" + And I should see "Math" + And I should see "Physics" And I set the following fields to these values: | Name | Test badge renamed | | Version | secondversion | + | Tags | Math, History | And I press "Save changes" And I select "Overview" from the "jump" singleselect Then I should not see "Test badge with 'apostrophe' and other friends (&@#)" And I should not see "firstversion" + And I should not see "Math, Physics" And I should see "Test badge renamed" And I should see "secondversion" + And I should see "Math" + And I should see "History" diff --git a/lang/en/badges.php b/lang/en/badges.php index 463eb8b4c51..fa229e7a154 100644 --- a/lang/en/badges.php +++ b/lang/en/badges.php @@ -545,6 +545,7 @@ Once a badge has been issued to at least one user, it automatically becomes **LO We want to make sure that all users complete the same requirements to earn a badge. Currently, it is not possible to revoke badges. If we allowed badges requirements to be modified all the time, we would most likely end up with users having the same badge for meeting completely different requirements.'; $string['subject'] = 'Message subject'; +$string['tags'] = 'Tags'; $string['targetname'] = 'Name'; $string['targetname_help'] = 'The external skill or standard which the badge is aligned with.'; $string['targeturl'] = 'URL'; @@ -558,6 +559,7 @@ $string['targetcode_help'] = 'A unique string identifier for referencing the ext $string['testbackpack'] = 'Test backpack \'{$a}\''; $string['testsettings'] = 'Test settings'; $string['type'] = 'Type'; +$string['tagarea_badge'] = 'Badges'; $string['userbadges'] = 'User badges'; $string['variablesubstitution'] = 'Variable substitution in messages.'; $string['variablesubstitution_help'] = 'In a badge message, certain variables can be inserted into the subject and/or body of a message so that they will be replaced with real values when the message is sent. The variables should be inserted into the text exactly as they are shown below. The following variables can be used: diff --git a/lib/db/tag.php b/lib/db/tag.php index ed5cf19b95d..22e125ecbba 100644 --- a/lib/db/tag.php +++ b/lib/db/tag.php @@ -52,39 +52,45 @@ defined('MOODLE_INTERNAL') || die(); -$tagareas = array( - array( +$tagareas = [ + [ 'itemtype' => 'user', // Users. 'component' => 'core', 'callback' => 'user_get_tagged_users', 'callbackfile' => '/user/lib.php', 'showstandard' => core_tag_tag::HIDE_STANDARD, - ), - array( + ], + [ 'itemtype' => 'course', // Courses. 'component' => 'core', 'callback' => 'course_get_tagged_courses', 'callbackfile' => '/course/lib.php', - ), - array( + ], + [ 'itemtype' => 'question', // Questions. 'component' => 'core_question', 'multiplecontexts' => true, - ), - array( + ], + [ 'itemtype' => 'post', // Blog posts. 'component' => 'core', 'callback' => 'blog_get_tagged_posts', 'callbackfile' => '/blog/lib.php', - ), - array( + ], + [ 'itemtype' => 'blog_external', // External blogs. 'component' => 'core', - ), - array( + ], + [ 'itemtype' => 'course_modules', // Course modules. 'component' => 'core', 'callback' => 'course_get_tagged_course_modules', 'callbackfile' => '/course/lib.php', - ), -); + ], + [ + 'itemtype' => 'badge', // Badges. + 'component' => 'core_badges', + 'callback' => 'badge_get_tagged_badges', + 'callbackfile' => '/badges/lib.php', + ], +];