Commit b7374fac authored by Matt Davidson's avatar Matt Davidson Committed by Ryan Wyllie
Browse files

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.
parent d9520bc0
......@@ -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();
}
}
......
......@@ -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(''));
......
......@@ -34,42 +34,48 @@ $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');
$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));
}
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);
}
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();
// Include JS files for backpack support.
badges_setup_backpack_js();
echo $OUTPUT->header();
echo $OUTPUT->header();
echo $output->render($badge);
echo $output->render($badge);
} else {
echo $OUTPUT->header();
echo $OUTPUT->container($OUTPUT->error_text(get_string('error:badgeawardnotfound', 'badges')) .
html_writer::tag('p', $OUTPUT->close_window_button()), 'important', 'notice');
}
// Trigger event, badge viewed.
$other = array('badgeid' => $badge->badgeid, 'badgehash' => $id);
......
......@@ -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');
}
}
/**
......
......@@ -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;
}
......@@ -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();
......
......@@ -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"
......@@ -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'] = '<p>This will make your badge visible to users and allow them to start earning it.</p>
......
......@@ -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';
......
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* 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;
}
}
......@@ -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,
......
Supports Markdown
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment