diff --git a/badges/classes/observer.php b/badges/classes/observer.php index 34cf105b9dcc7..31be94ed61871 100644 --- a/badges/classes/observer.php +++ b/badges/classes/observer.php @@ -106,6 +106,37 @@ public static function course_criteria_review(\core\event\course_completed $even } } + /** + * Triggered when 'badge_awarded' event happens. + * + * @param \core\event\badge_awarded $event event generated when a badge is awarded. + */ + public static function badge_criteria_review(\core\event\badge_awarded $event) { + global $DB, $CFG; + + if (!empty($CFG->enablebadges)) { + require_once($CFG->dirroot.'/lib/badgeslib.php'); + $userid = $event->relateduserid; + + if ($rs = $DB->get_records('badge_criteria', array('criteriatype' => BADGE_CRITERIA_TYPE_BADGE))) { + foreach ($rs as $r) { + $badge = new badge($r->badgeid); + if (!$badge->is_active() || $badge->is_issued($userid)) { + continue; + } + + if ($badge->criteria[BADGE_CRITERIA_TYPE_BADGE]->review($userid)) { + $badge->criteria[BADGE_CRITERIA_TYPE_BADGE]->mark_complete($userid); + + if ($badge->criteria[BADGE_CRITERIA_TYPE_OVERALL]->review($userid)) { + $badge->criteria[BADGE_CRITERIA_TYPE_OVERALL]->mark_complete($userid); + $badge->issue($userid); + } + } + } + } + } + } /** * Triggered when 'user_updated' event happens. * diff --git a/badges/criteria/award_criteria.php b/badges/criteria/award_criteria.php index ee13151e1c3f7..54277830528d9 100644 --- a/badges/criteria/award_criteria.php +++ b/badges/criteria/award_criteria.php @@ -68,6 +68,12 @@ */ define('BADGE_CRITERIA_TYPE_PROFILE', 6); +/* + * Badge completion criteria type + * Criteria type constant, primarily for storing criteria type in the database. + */ +define('BADGE_CRITERIA_TYPE_BADGE', 7); + /* * Criteria type constant to class name mapping */ @@ -79,7 +85,8 @@ BADGE_CRITERIA_TYPE_SOCIAL => 'social', BADGE_CRITERIA_TYPE_COURSE => 'course', BADGE_CRITERIA_TYPE_COURSESET => 'courseset', - BADGE_CRITERIA_TYPE_PROFILE => 'profile' + BADGE_CRITERIA_TYPE_PROFILE => 'profile', + BADGE_CRITERIA_TYPE_BADGE => 'badge', ); /** diff --git a/badges/criteria/award_criteria_badge.php b/badges/criteria/award_criteria_badge.php new file mode 100644 index 0000000000000..d9bd7957dc459 --- /dev/null +++ b/badges/criteria/award_criteria_badge.php @@ -0,0 +1,271 @@ +. + +/** + * This file contains the badge earned badge award criteria type class + * + * @package core + * @subpackage badges + * @copyright 2017 Stephen Bourget + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +/** + * Badge award criteria -- award on badge completion + * + * @package core + * @subpackage badges + * @copyright 2017 Stephen Bourget + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class award_criteria_badge extends award_criteria { + + /* @var int Criteria [BADGE_CRITERIA_TYPE_BADGE] */ + public $criteriatype = BADGE_CRITERIA_TYPE_BADGE; + + public $required_param = 'badge'; + public $optional_params = array(); + + /** + * Get criteria details for displaying to users + * @param string $short Print short version of criteria + * @return string + */ + public function get_details($short = '') { + global $DB, $OUTPUT; + $output = array(); + foreach ($this->params as $p) { + $badgename = $DB->get_field('badge', 'name', array('id' => $p['badge'])); + if (!$badgename) { + $str = $OUTPUT->error_text(get_string('error:nosuchbadge', 'badges')); + } else { + $str = html_writer::tag('b', '"' . $badgename . '"'); + } + $output[] = $str; + } + + if ($short) { + return implode(', ', $output); + } else { + return html_writer::alist($output, array(), 'ul'); + } + } + + /** + * Add appropriate new criteria options to the form + * @param object $mform moodle form + */ + public function get_options(&$mform) { + global $DB; + $none = false; + $availablebadges = null; + + $mform->addElement('header', 'first_header', $this->get_title()); + $mform->addHelpButton('first_header', 'criteria_' . $this->criteriatype, 'badges'); + + // Determine if this badge is a course badge or a site badge. + $thisbadge = $DB->get_record('badge', array('id' => $this->badgeid)); + + if ($thisbadge->type == BADGE_TYPE_SITE) { + // Only list site badges that are enabled. + $select = " type = :site AND (status = :status1 OR status = :status2)"; + $params = array('site' => BADGE_TYPE_SITE, + 'status1' => BADGE_STATUS_ACTIVE, + 'status2' => BADGE_STATUS_ACTIVE_LOCKED); + $availablebadges = $DB->get_records_select_menu('badge', $select, $params, 'name ASC', 'id, name'); + + } else if ($thisbadge->type == BADGE_TYPE_COURSE) { + // List both site badges and course badges belonging to this course. + $select = " (type = :site OR (type = :course AND courseid = :courseid)) AND (status = :status1 OR status = :status2)"; + $params = array('site' => BADGE_TYPE_SITE, + 'course' => BADGE_TYPE_COURSE, + 'courseid' => $thisbadge->courseid, + 'status1' => BADGE_STATUS_ACTIVE, + 'status2' => BADGE_STATUS_ACTIVE_LOCKED); + $availablebadges = $DB->get_records_select_menu('badge', $select, $params, 'name ASC', 'id, name'); + } + if (!empty($availablebadges)) { + $select = array(); + $selected = array(); + foreach ($availablebadges as $bid => $badgename) { + if ($bid != $this->badgeid) { + // Do not let it use itself as criteria. + $select[$bid] = format_string($badgename, true); + } + } + + if ($this->id !== 0) { + $selected = array_keys($this->params); + } + $settings = array('multiple' => 'multiple', 'size' => 20, 'class' => 'selectbadge'); + $mform->addElement('select', 'badge_badges', get_string('addbadge', 'badges'), $select, $settings); + $mform->addRule('badge_badges', get_string('requiredbadge', 'badges'), 'required'); + $mform->addHelpButton('badge_badges', 'addbadge', 'badges'); + + if ($this->id !== 0) { + $mform->setDefault('badge_badges', $selected); + } + } else { + $mform->addElement('static', 'nobadges', '', get_string('error:nobadges', 'badges')); + $none = true; + } + + // Add aggregation. + if (!$none) { + $mform->addElement('header', 'aggregation', get_string('method', 'badges')); + $agg = array(); + $agg[] =& $mform->createElement('radio', 'agg', '', get_string('allmethodbadges', 'badges'), 1); + $agg[] =& $mform->createElement('radio', 'agg', '', get_string('anymethodbadges', 'badges'), 2); + $mform->addGroup($agg, 'methodgr', '', array('
'), false); + if ($this->id !== 0) { + $mform->setDefault('agg', $this->method); + } else { + $mform->setDefault('agg', BADGE_CRITERIA_AGGREGATION_ANY); + } + } + + return array($none, get_string('noparamstoadd', 'badges')); + } + + /** + * Save criteria records + * + * @param array $params Values from the form or any other array. + */ + public function save($params = array()) { + $badges = $params['badge_badges']; + unset($params['badge_badges']); + foreach ($badges as $badgeid) { + $params["badge_{$badgeid}"] = $badgeid; + } + + parent::save($params); + } + + /** + * Review this criteria and decide if it has been completed + * + * @param int $userid User whose criteria completion needs to be reviewed. + * @param bool $filtered An additional parameter indicating that user list + * has been reduced and some expensive checks can be skipped. + * + * @return bool Whether criteria is complete. + */ + public function review($userid, $filtered = false) { + + global $DB; + $overall = false; + + foreach ($this->params as $param) { + $badge = $DB->get_record('badge', array('id' => $param['badge'])); + // See if the user has earned this badge. + $awarded = $DB->get_record('badge_issued', array('badgeid' => $param['badge'], 'userid' => $userid)); + + // Extra check in case a badge was deleted while this badge is still active. + if (!$badge) { + if ($this->method == BADGE_CRITERIA_AGGREGATION_ALL) { + return false; + } else { + continue; + } + } + + if ($this->method == BADGE_CRITERIA_AGGREGATION_ALL) { + + if ($awarded) { + $overall = true; + continue; + } else { + return false; + } + } else if ($this->method == BADGE_CRITERIA_AGGREGATION_ANY) { + if ($awarded) { + return true; + } else { + $overall = false; + continue; + } + } + } + + return $overall; + } + + /** + * Checks criteria for any major problems. + * + * @return array A list containing status and an error message (if any). + */ + public function validate() { + global $DB; + $params = array_keys($this->params); + $method = ($this->method == BADGE_CRITERIA_AGGREGATION_ALL); + $singleparam = (count($params) == 1); + + foreach ($params as $param) { + // Perform check if there only one parameter with any type of aggregation, + // Or there are more than one parameter with aggregation ALL. + + if (($singleparam || $method) && !$DB->record_exists('badge', array('id' => $param))) { + return array(false, get_string('error:invalidparambadge', 'badges')); + } + } + + return array(true, ''); + } + + /** + * Returns array with sql code and parameters returning all ids + * of users who meet this particular criterion. + * + * @return array list($join, $where, $params) + */ + public function get_completed_criteria_sql() { + $join = ''; + $where = ''; + $params = array(); + + if ($this->method == BADGE_CRITERIA_AGGREGATION_ANY) { + // User has received ANY of the required badges. + $join = " LEFT JOIN {badge_issued} bi2 ON bi2.userid = u.id"; + $where = "AND ("; + $i = 0; + foreach ($this->params as $param) { + if ($i == 0) { + $where .= ' bi2.badgeid = :badgeid'.$i; + } else { + $where .= ' OR bi2.badgeid = :badgeid'.$i; + } + $params['badgeid'.$i] = $param['badge']; + $i++; + } + $where .= ") "; + return array($join, $where, $params); + } else { + // User has received ALL of the required badges. + $join = " LEFT JOIN {badge_issued} bi2 ON bi2.userid = u.id"; + $i = 0; + foreach ($this->params as $param) { + $i++; + $where = ' AND bi2.badgeid = :badgeid'.$i; + $params['badgeid'.$i] = $param['badge']; + } + return array($join, $where, $params); + } + } +} diff --git a/badges/tests/behat/award_badge.feature b/badges/tests/behat/award_badge.feature index 13c040a17b975..cd64b325b726a 100644 --- a/badges/tests/behat/award_badge.feature +++ b/badges/tests/behat/award_badge.feature @@ -4,6 +4,71 @@ Feature: Award badges As an admin I need to add criteria to badges in the system + @javascript + Scenario: Award badge on other badges as criteria + Given the following "users" exist: + | username | firstname | lastname | email | + | teacher1 | Teacher | 1 | teacher1@example.com | + | student1 | Student | 1 | student1@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 | + And I log in as "teacher1" + And I am on "Course 1" course homepage + # Create course badge 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 1 | + | Description | Course badge 1 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 expand all fieldsets + # Set to ANY of the roles awards badge. + And I set the field "Teacher" to "1" + And I set the field "Any of the selected roles awards the badge" to "1" + And I press "Save" + And I press "Enable access" + And I press "Continue" + # Badge #2 + 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 2 | + | Description | Course badge 2 description | + | issuername | Tester of course badge | + And I upload "badges/tests/behat/badge.png" file to "Image" filemanager + And I press "Create badge" + # Set "course badge 1" as criteria + And I set the field "type" to "Awarded badges" + And I set the field "id_badge_badges" to "Course Badge 1" + And I press "Save" + And I press "Enable access" + And I press "Continue" + And I follow "Manage badges" + And I follow "Course Badge 1" + And I follow "Recipients (0)" + And I press "Award badge" + # Award course badge 1 to student 1. + And I set the field "potentialrecipients[]" to "Student 1 (student1@example.com)" + When I press "Award badge" + And I follow "Course Badge 1" + And I follow "Recipients (1)" + Then I should see "Recipients (1)" + And I log out + # Student 1 should have both badges. + And I log in as "student1" + And I follow "Profile" in the user menu + When I click on "Course 1" "link" in the "region-main" "region" + Then I should see "Course Badge 1" + And I should see "Course Badge 2" + @javascript Scenario: Award profile badge Given I log in as "admin" diff --git a/lang/en/badges.php b/lang/en/badges.php index 59833af5331b9..2364312f5bbdc 100644 --- a/lang/en/badges.php +++ b/lang/en/badges.php @@ -27,6 +27,8 @@ $string['actions'] = 'Actions'; $string['activate'] = 'Enable access'; $string['activatesuccess'] = 'Access to the badges was successfully enabled.'; +$string['addbadge'] = 'Add a badge as criteria'; +$string['addbadge_help'] = 'Select all badges that should be added to this badge requirement. Hold CTRL key to select multiple items.'; $string['addbadgecriteria'] = 'Add badge criteria'; $string['addcriteria'] = 'Add criteria'; $string['addcriteriatext'] = 'To start adding criteria, please select one of the options from the drop-down menu.'; @@ -39,6 +41,7 @@ $string['all'] = 'All'; $string['allmethod'] = 'All of the selected conditions are met'; $string['allmethodactivity'] = 'All of the selected activities are complete'; +$string['allmethodbadges'] = 'All of the selected badges have been earned'; $string['allmethodcourseset'] = 'All of the selected courses are complete'; $string['allmethodmanual'] = 'All of the selected roles award the badge'; $string['allmethodprofile'] = 'All of the selected profile fields have been completed'; @@ -51,6 +54,7 @@ $string['any'] = 'Any'; $string['anymethod'] = 'Any of the selected conditions is met'; $string['anymethodactivity'] = 'Any of the selected activities is complete'; +$string['anymethodbadges'] = 'Any of the selected badges have been earned'; $string['anymethodcourseset'] = 'Any of the selected courses is complete'; $string['anymethodmanual'] = 'Any of the selected roles awards the badge'; $string['anymethodprofile'] = 'Any of the selected profile fields has been completed'; @@ -176,22 +180,26 @@ $string['criteria_descr_short4'] = 'Complete the course '; $string['criteria_descr_short5'] = 'Complete {$a} of: '; $string['criteria_descr_short6'] = 'Complete {$a} of: '; +$string['criteria_descr_short7'] = 'Complete {$a} of: '; $string['criteria_descr_single_short1'] = 'Complete: '; $string['criteria_descr_single_short2'] = 'Awarded by: '; $string['criteria_descr_single_short4'] = 'Complete the course '; $string['criteria_descr_single_short5'] = 'Complete: '; $string['criteria_descr_single_short6'] = 'Complete: '; +$string['criteria_descr_single_short7'] = 'Complete: '; $string['criteria_descr_single_1'] = 'The following activity has to be completed:'; $string['criteria_descr_single_2'] = 'This badge has to be awarded by a user with the following role:'; $string['criteria_descr_single_4'] = 'Users must complete the course'; $string['criteria_descr_single_5'] = 'The following course has to be completed:'; $string['criteria_descr_single_6'] = 'The following user profile field has to be completed:'; +$string['criteria_descr_single_7'] = 'The following badge has to be earned:'; $string['criteria_descr_0'] = 'Users are awarded this badge when they complete {$a} of the listed requirements.'; $string['criteria_descr_1'] = '{$a} of the following activities are completed:'; $string['criteria_descr_2'] = 'This badge has to be awarded by the users with {$a} of the following roles:'; $string['criteria_descr_4'] = 'Users must complete the course'; $string['criteria_descr_5'] = '{$a} of the following courses have to be completed:'; $string['criteria_descr_6'] = '{$a} of the following user profile fields have to be completed:'; +$string['criteria_descr_7'] = '{$a} of the following badges have to be earned:'; $string['criteria_0'] = 'This badge is awarded when...'; $string['criteria_1'] = 'Activity completion'; $string['criteria_1_help'] = 'Allows a badge to be awarded to users based on the completion of a set of activities within a course.'; @@ -205,6 +213,8 @@ $string['criteria_5_help'] = 'Allows a badge to be awarded to users who have completed a set of courses. Each course can have additional parameters such as minimum grade and date of course completion. '; $string['criteria_6'] = 'Profile completion'; $string['criteria_6_help'] = 'Allows a badge to be awarded to users for completing certain fields in their profile. You can select from default and custom profile fields that are available to users. '; +$string['criteria_7'] = 'Awarded badges'; +$string['criteria_7_help'] = 'Allows a badge to be awarded to users based on the other badges thay have earned.'; $string['criterror'] = 'Current parameters issues'; $string['criterror_help'] = 'This fieldset shows all parameters that were initially added to this badge requirement but are no longer available. It is recommended that you un-check such parameters to make sure that users can earn this badge in the future.'; $string['currentimage'] = 'Current image'; @@ -248,7 +258,9 @@ $string['error:invalidcriteriatype'] = 'Invalid criteria type.'; $string['error:invalidexpiredate'] = 'Expiry date has to be in the future.'; $string['error:invalidexpireperiod'] = 'Expiry period cannot be negative or equal 0.'; +$string['error:invalidparambadge'] = 'Badge does not exist. '; $string['error:noactivities'] = 'There are no activities with completion criteria enabled in this course.'; +$string['error:nobadges'] = 'There are no course or site badges available to be added as criteria. Make sure that your other badges are enabled '; $string['error:nocourses'] = 'Course completion is not enabled for any of the courses in this site, so none can be displayed. Course completion may be enabled in the course settings.'; $string['error:nogroups'] = '

There are no public collections of badges available in your backpack.

Only public collections are shown, visit your backpack to create some public collections.

'; @@ -369,6 +381,7 @@ $string['relative'] = 'Relative date'; $string['revoke'] = 'Revoke badge'; $string['requiredcourse'] = 'At least one course should be added to the courseset criterion.'; +$string['requiredbadge'] = 'At least one badge should be added to the badge 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/lib/badgeslib.php b/lib/badgeslib.php index 8c42e62845cbc..e86eb8a32ee53 100644 --- a/lib/badgeslib.php +++ b/lib/badgeslib.php @@ -191,6 +191,7 @@ public function get_accepted_criteria() { BADGE_CRITERIA_TYPE_OVERALL, BADGE_CRITERIA_TYPE_MANUAL, BADGE_CRITERIA_TYPE_COURSE, + BADGE_CRITERIA_TYPE_BADGE, BADGE_CRITERIA_TYPE_ACTIVITY ); } else if ($this->type == BADGE_TYPE_SITE) { @@ -198,6 +199,7 @@ public function get_accepted_criteria() { BADGE_CRITERIA_TYPE_OVERALL, BADGE_CRITERIA_TYPE_MANUAL, BADGE_CRITERIA_TYPE_COURSESET, + BADGE_CRITERIA_TYPE_BADGE, BADGE_CRITERIA_TYPE_PROFILE, ); } diff --git a/lib/db/events.php b/lib/db/events.php index 8045f8e04eb15..de2be5165cc44 100644 --- a/lib/db/events.php +++ b/lib/db/events.php @@ -46,6 +46,10 @@ 'eventname' => '\core\event\course_module_completion_updated', 'callback' => 'core_badges_observer::course_module_criteria_review', ), + array( + 'eventname' => '\core\event\badge_awarded', + 'callback' => 'core_badges_observer::badge_criteria_review', + ), array( 'eventname' => '\core\event\course_completed', 'callback' => 'core_badges_observer::course_criteria_review',