From b7374faca6420dd3b2e8b8addc6b975655c65ece Mon Sep 17 00:00:00 2001 From: Matt Davidson Date: Wed, 28 Sep 2016 10:39:56 -0400 Subject: [PATCH] MDL-43230 badges: manually revoke awarded badges A user can be manually awarded a badge, but if given by mistake an awarded badge cannot be removed. --- badges/assertion.php | 9 +- badges/award.php | 19 ++++ badges/badge.php | 64 ++++++++------ badges/classes/assertion.php | 6 +- badges/lib/awardlib.php | 30 +++++++ badges/renderer.php | 6 ++ badges/tests/behat/award_badge.feature | 53 +++++++++++ lang/en/badges.php | 5 ++ lang/en/role.php | 1 + lib/classes/event/badge_revoked.php | 118 +++++++++++++++++++++++++ lib/db/access.php | 12 +++ 11 files changed, 292 insertions(+), 31 deletions(-) create mode 100644 lib/classes/event/badge_revoked.php diff --git a/badges/assertion.php b/badges/assertion.php index be307147927..3c3ae4a4675 100644 --- a/badges/assertion.php +++ b/badges/assertion.php @@ -43,7 +43,14 @@ if (!is_null($action)) { $json = ($action) ? $assertion->get_badge_class() : $assertion->get_issuer(); } else { // Otherwise, get badge assertion. - $json = $assertion->get_badge_assertion(); + $column = $DB->sql_compare_text('uniquehash', 255); + if ($DB->record_exists_sql(sprintf('SELECT * FROM {badge_issued} WHERE %s = ?', $column), array($hash))) { + $json = $assertion->get_badge_assertion(); + } else { // Revoked badge. + header("HTTP/1.0 410 Gone"); + echo json_encode(array("revoked" => true)); + die(); + } } diff --git a/badges/award.php b/badges/award.php index 938c795cb73..22c1acfdb80 100644 --- a/badges/award.php +++ b/badges/award.php @@ -31,6 +31,7 @@ require_once($CFG->dirroot . '/badges/lib/awardlib.php'); $badgeid = required_param('id', PARAM_INT); $role = optional_param('role', 0, PARAM_INT); $award = optional_param('award', false, PARAM_BOOL); +$revoke = optional_param('revoke', false, PARAM_BOOL); require_login(); @@ -169,6 +170,24 @@ if ($award && data_submitted() && has_capability('moodle/badges:awardbadge', $co } } + $recipientselector->invalidate_selected_users(); + $existingselector->invalidate_selected_users(); + $recipientselector->set_existing_recipients($existingselector->find_users('')); +} else if ($revoke && data_submitted() && has_capability('moodle/badges:revokebadge', $context)) { + require_sesskey(); + $users = $existingselector->get_selected_users(); + + foreach ($users as $user) { + if (!process_manual_revoke($user->id, $USER->id, $issuerrole->roleid, $badgeid)) { + echo $OUTPUT->error_text(get_string('error:cannotrevokebadge', 'badges')); + } else { + // Trigger event, badge revoked. + $eventparams = array('objectid' => $badgeid, 'context' => $PAGE->context); + $event = \core\event\badge_revoked::create($eventparams); + $event->trigger(); + } + } + $recipientselector->invalidate_selected_users(); $existingselector->invalidate_selected_users(); $recipientselector->set_existing_recipients($existingselector->find_users('')); diff --git a/badges/badge.php b/badges/badge.php index 662dc25744e..d21b96efb08 100644 --- a/badges/badge.php +++ b/badges/badge.php @@ -34,43 +34,49 @@ $bake = optional_param('bake', 0, PARAM_BOOL); $PAGE->set_context(context_system::instance()); $output = $PAGE->get_renderer('core', 'badges'); -$badge = new issued_badge($id); - -if ($bake && ($badge->recipient->id == $USER->id)) { - $name = str_replace(' ', '_', $badge->badgeclass['name']) . '.png'; - $filehash = badges_bake($id, $badge->badgeid, $USER->id, true); - $fs = get_file_storage(); - $file = $fs->get_file_by_hash($filehash); - send_stored_file($file, 0, 0, true, array('filename' => $name)); -} - $PAGE->set_url('/badges/badge.php', array('hash' => $id)); $PAGE->set_pagelayout('base'); $PAGE->set_title(get_string('issuedbadge', 'badges')); -if (isloggedin()) { - $PAGE->set_heading($badge->badgeclass['name']); - $PAGE->navbar->add($badge->badgeclass['name']); - if ($badge->recipient->id == $USER->id) { - $url = new moodle_url('/badges/mybadges.php'); - } else { - $url = new moodle_url($CFG->wwwroot); +$badge = new issued_badge($id); +if (!empty($badge->recipient->id)) { + if ($bake && ($badge->recipient->id == $USER->id)) { + $name = str_replace(' ', '_', $badge->badgeclass['name']) . '.png'; + $filehash = badges_bake($id, $badge->badgeid, $USER->id, true); + $fs = get_file_storage(); + $file = $fs->get_file_by_hash($filehash); + send_stored_file($file, 0, 0, true, array('filename' => $name)); } - navigation_node::override_active_url($url); + + if (isloggedin()) { + $PAGE->set_heading($badge->badgeclass['name']); + $PAGE->navbar->add($badge->badgeclass['name']); + if ($badge->recipient->id == $USER->id) { + $url = new moodle_url('/badges/mybadges.php'); + } else { + $url = new moodle_url($CFG->wwwroot); + } + navigation_node::override_active_url($url); + } else { + $PAGE->set_heading($badge->badgeclass['name']); + $PAGE->navbar->add($badge->badgeclass['name']); + $url = new moodle_url($CFG->wwwroot); + navigation_node::override_active_url($url); + } + + // Include JS files for backpack support. + badges_setup_backpack_js(); + + echo $OUTPUT->header(); + + echo $output->render($badge); } else { - $PAGE->set_heading($badge->badgeclass['name']); - $PAGE->navbar->add($badge->badgeclass['name']); - $url = new moodle_url($CFG->wwwroot); - navigation_node::override_active_url($url); + echo $OUTPUT->header(); + + echo $OUTPUT->container($OUTPUT->error_text(get_string('error:badgeawardnotfound', 'badges')) . + html_writer::tag('p', $OUTPUT->close_window_button()), 'important', 'notice'); } -// Include JS files for backpack support. -badges_setup_backpack_js(); - -echo $OUTPUT->header(); - -echo $output->render($badge); - // Trigger event, badge viewed. $other = array('badgeid' => $badge->badgeid, 'badgehash' => $id); $eventparams = array('context' => $PAGE->context, 'other' => $other); diff --git a/badges/classes/assertion.php b/badges/classes/assertion.php index ba0efb5a2f4..c5dba463363 100644 --- a/badges/classes/assertion.php +++ b/badges/classes/assertion.php @@ -73,7 +73,11 @@ class core_badges_assertion { WHERE ' . $DB->sql_compare_text('bi.uniquehash', 40) . ' = ' . $DB->sql_compare_text(':hash', 40), array('hash' => $hash), IGNORE_MISSING); - $this->_url = new moodle_url('/badges/badge.php', array('hash' => $this->_data->uniquehash)); + if ($this->_data) { + $this->_url = new moodle_url('/badges/badge.php', array('hash' => $this->_data->uniquehash)); + } else { + $this->_url = new moodle_url('/badges/badge.php'); + } } /** diff --git a/badges/lib/awardlib.php b/badges/lib/awardlib.php index 8bccf53be4b..8d7466337bc 100644 --- a/badges/lib/awardlib.php +++ b/badges/lib/awardlib.php @@ -236,6 +236,36 @@ function process_manual_award($recipientid, $issuerid, $issuerrole, $badgeid) { return true; } } + return false; +} +/** + * Manually revoke awarded badges. + * + * @param int $recipientid + * @param int $issuerid + * @param int $issuerrole + * @param int $badgeid + * @return bool + */ +function process_manual_revoke($recipientid, $issuerid, $issuerrole, $badgeid) { + global $DB; + $params = array( + 'badgeid' => $badgeid, + 'issuerid' => $issuerid, + 'issuerrole' => $issuerrole, + 'recipientid' => $recipientid + ); + if ($DB->record_exists('badge_manual_award', $params)) { + if ($DB->delete_records('badge_manual_award', array('badgeid' => $badgeid, + 'issuerid' => $issuerid, + 'recipientid' => $recipientid)) + && $DB->delete_records('badge_issued', array('badgeid' => $badgeid, + 'userid' => $recipientid))) { + return true; + } + } else { + throw new moodle_exception('error:badgenotfound', 'badges'); + } return false; } diff --git a/badges/renderer.php b/badges/renderer.php index 184fef0de56..d23ff4e7b8b 100644 --- a/badges/renderer.php +++ b/badges/renderer.php @@ -115,6 +115,12 @@ class core_badges_renderer extends plugin_renderer_base { 'value' => $this->output->larrow() . ' ' . get_string('award', 'badges'), 'class' => 'actionbutton') ); + $actioncell->text .= html_writer::empty_tag('input', array( + 'type' => 'submit', + 'name' => 'revoke', + 'value' => get_string('revoke', 'badges') . ' ' . $this->output->rarrow(), + 'class' => 'actionbutton') + ); $actioncell->text .= html_writer::end_tag('div', array()); $actioncell->attributes['class'] = 'actions'; $potentialcell = new html_table_cell(); diff --git a/badges/tests/behat/award_badge.feature b/badges/tests/behat/award_badge.feature index 206ac426cbd..941b8a09cfa 100644 --- a/badges/tests/behat/award_badge.feature +++ b/badges/tests/behat/award_badge.feature @@ -310,3 +310,56 @@ Feature: Award badges When I follow "Course 1" Then I should see "Course Badge 2" Then I should not see "Course Badge 1" + + @javascript + Scenario: Revoke badge + Given the following "users" exist: + | username | firstname | lastname | email | + | teacher1 | Teacher | 1 | teacher1@example.com | + | student1 | Student | 1 | student1@example.com | + | student2 | Student | 2 | student2@example.com | + And the following "courses" exist: + | fullname | shortname | category | groupmode | + | Course 1 | C1 | 0 | 1 | + And the following "course enrolments" exist: + | user | course | role | + | teacher1 | C1 | editingteacher | + | student1 | C1 | student | + | student2 | C1 | student | + And I log in as "teacher1" + And I follow "Course 1" + And I navigate to "Add a new badge" node in "Course administration > Badges" + And I follow "Add a new badge" + And I set the following fields to these values: + | Name | Course Badge | + | Description | Course badge description | + | issuername | Tester of course badge | + And I upload "badges/tests/behat/badge.png" file to "Image" filemanager + And I press "Create badge" + And I set the field "type" to "Manual issue by role" + And I set the field "Teacher" to "1" + And I press "Save" + And I press "Enable access" + And I press "Continue" + And I follow "Recipients (0)" + And I press "Award badge" + And I set the field "potentialrecipients[]" to "Student 2 (student2@example.com)" + And I press "Award badge" + And I set the field "potentialrecipients[]" to "Student 1 (student1@example.com)" + When I press "Award badge" + And I follow "Course Badge" + Then I should see "Recipients (2)" + And I follow "Recipients (2)" + And I press "Award badge" + And I set the field "existingrecipients[]" to "Student 2 (student2@example.com)" + And I press "Remove badge" + And I set the field "existingrecipients[]" to "Student 1 (student1@example.com)" + When I press "Award badge" + And I follow "Remove Badge" + Then I should see "Recipients (0)" + And I log out + And I log in as "student1" + And I follow "Profile" in the user menu + And I follow "Course 1" + And I should see "Course Badge" + diff --git a/lang/en/badges.php b/lang/en/badges.php index 5d47fe9c822..ac1445db89c 100644 --- a/lang/en/badges.php +++ b/lang/en/badges.php @@ -216,9 +216,12 @@ $string['error:backpackemailnotfound'] = 'The email \'{$a}\' is not associated w $string['error:backpacknotavailable'] = 'Your site is not accessible from the Internet, so any badges issued from this site cannot be verified by external backpack services.'; $string['error:backpackloginfailed'] = 'You could not be connected to an external backpack for the following reason: {$a}'; $string['error:backpackproblem'] = 'There was a problem connecting to your backpack service provider. Please try again later.'; +$string['error:badgeawardnotfound'] = 'Cannot verify this awarded badge. This badge may have been revoked.'; +$string['error:badgenotfound'] = 'Badge not found'; $string['error:badjson'] = 'The connection attempt returned invalid data.'; $string['error:cannotact'] = 'Cannot activate the badge. '; $string['error:cannotawardbadge'] = 'Cannot award badge to a user.'; +$string['error:cannotrevokebadge'] = 'Cannot revoke badge from a user.'; $string['error:cannotdeletecriterion'] = 'This criterion cannot be deleted. '; $string['error:connectionunknownreason'] = 'The connection was unsuccessful but no reason was given.'; $string['error:clone'] = 'Cannot clone the badge.'; @@ -259,6 +262,7 @@ $string['eventbadgedisabled'] = 'Badge disabled'; $string['eventbadgeduplicated'] = 'Badge duplicated'; $string['eventbadgeenabled'] = 'Badge enabled'; $string['eventbadgelistingviewed'] = 'Badge listing viewed'; +$string['eventbadgerevoked'] = 'Badge revoked'; $string['eventbadgeupdated'] = 'Badge updated'; $string['eventbadgeviewed'] = 'Badge viewed'; $string['evidence'] = 'Evidence'; @@ -349,6 +353,7 @@ $string['recipientdetails'] = 'Recipient details'; $string['recipientidentificationproblem'] = 'Cannot find a recipient of this badge among the existing users.'; $string['recipientvalidationproblem'] = 'Current user cannot be verified as a recipient of this badge.'; $string['relative'] = 'Relative date'; +$string['revoke'] = 'Revoke badge'; $string['requiredcourse'] = 'At least one course should be added to the courseset criterion.'; $string['reviewbadge'] = 'Changes in badge access'; $string['reviewconfirm'] = '

This will make your badge visible to users and allow them to start earning it.

diff --git a/lang/en/role.php b/lang/en/role.php index b7ede5d1b7c..27b8921b0ea 100644 --- a/lang/en/role.php +++ b/lang/en/role.php @@ -76,6 +76,7 @@ $string['badges:deletebadge'] = 'Delete badges'; $string['badges:earnbadge'] = 'Earn badge'; $string['badges:manageglobalsettings'] = 'Manage badges global settings'; $string['badges:manageownbadges'] = 'View and manage own earned badges'; +$string['badges:revokebadge'] = 'Revoke badge from a user'; $string['badges:viewawarded'] = 'View users who earned a specific badge without being able to award a badge'; $string['badges:viewbadges'] = 'View available badges without earning them'; $string['badges:viewotherbadges'] = 'View public badges in other users\' profiles'; diff --git a/lib/classes/event/badge_revoked.php b/lib/classes/event/badge_revoked.php new file mode 100644 index 00000000000..e2bd5e2fe67 --- /dev/null +++ b/lib/classes/event/badge_revoked.php @@ -0,0 +1,118 @@ +. + +/** + * Badge revoked event. + * + * @property-read array $other { + * Extra information about event. + * + * - int expiredate: Badge expire timestamp. + * - int badgeissuedid: Badge issued ID. + * } + * + * @package core + * @copyright 2016 Matt Davidson + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace core\event; +defined('MOODLE_INTERNAL') || die(); + +/** + * Event triggered after a badge is revoked from a user. + * + * @package core + * @since Moodle 3.2 + * @copyright 2016 Matt Davidson + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class badge_revoked extends base { + + /** + * Set basic properties for the event. + */ + protected function init() { + $this->data['objecttable'] = 'badge'; + $this->data['crud'] = 'c'; + $this->data['edulevel'] = self::LEVEL_TEACHING; + } + + /** + * Returns localised general event name. + * + * @return string + */ + public static function get_name() { + return get_string('eventbadgerevoked', 'badges'); + } + + /** + * Returns non-localised event description with id's for admin use only. + * + * @return string + */ + public function get_description() { + return "The user with id '$this->relateduserid' has had the badge with id '$this->objectid' revoked."; + } + + /** + * Returns relevant URL. + * @return \moodle_url + */ + public function get_url() { + return new \moodle_url('/badges/overview.php', array('id' => $this->objectid)); + } + + /** + * Custom validations. + * + * @throws \coding_exception + * @return void + */ + protected function validate_data() { + parent::validate_data(); + + if (!isset($this->relateduserid)) { + throw new \coding_exception('The \'relateduserid\' must be set.'); + } + + if (!isset($this->objectid)) { + throw new \coding_exception('The \'objectid\' must be set.'); + } + } + + /** + * Get_objectid_mapping method. + * + * @return array + */ + public static function get_objectid_mapping() { + return array('db' => 'badge', 'restore' => 'badge'); + } + + /** + * Get_other_mapping method. + * + * @return array + */ + public static function get_other_mapping() { + $othermapped = array(); + $othermapped['badgeissuedid'] = array('db' => 'badge_issued', 'restore' => base::NOT_MAPPED); + + return $othermapped; + } +} diff --git a/lib/db/access.php b/lib/db/access.php index 7d8366194c5..4a193da399d 100644 --- a/lib/db/access.php +++ b/lib/db/access.php @@ -2025,6 +2025,18 @@ $capabilities = array( ) ), + // Revoke badge from a user. + 'moodle/badges:revokebadge' => array( + 'riskbitmask' => RISK_SPAM, + 'captype' => 'write', + 'contextlevel' => CONTEXT_COURSE, + 'archetypes' => array( + 'manager' => CAP_ALLOW, + 'teacher' => CAP_ALLOW, + 'editingteacher' => CAP_ALLOW, + ) + ), + // View users who earned a specific badge without being able to award a badge. 'moodle/badges:viewawarded' => array( 'riskbitmask' => RISK_PERSONAL,