diff --git a/badges/criteria/award_criteria.php b/badges/criteria/award_criteria.php index 96526c43fc849..4287cddc646c6 100644 --- a/badges/criteria/award_criteria.php +++ b/badges/criteria/award_criteria.php @@ -236,9 +236,20 @@ public function config_form_criteria($data) { * Review this criteria and decide if the user has 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 */ - abstract public function review($userid); + abstract public function review($userid, $filtered = false); + + /** + * Returns array with sql code and parameters returning all ids + * of users who meet this particular criterion. + * + * @return array list($join, $where, $params) + */ + abstract public function get_completed_criteria_sql(); /** * Mark this criteria as complete for a user diff --git a/badges/criteria/award_criteria_activity.php b/badges/criteria/award_criteria_activity.php index ff9b92eecd330..8f4d4b039c6ad 100644 --- a/badges/criteria/award_criteria_activity.php +++ b/badges/criteria/award_criteria_activity.php @@ -37,13 +37,20 @@ class award_criteria_activity extends award_criteria { public $criteriatype = BADGE_CRITERIA_TYPE_ACTIVITY; private $courseid; + private $coursestartdate; public $required_param = 'module'; public $optional_params = array('bydate'); public function __construct($record) { + global $DB; parent::__construct($record); - $this->courseid = self::get_course(); + + $course = $DB->get_record_sql('SELECT b.courseid, c.startdate + FROM {badge} b INNER JOIN {course} c ON b.courseid = c.id + WHERE b.id = :badgeid ', array('badgeid' => $this->badgeid)); + $this->courseid = $course->courseid; + $this->coursestartdate = $course->startdate; } /** @@ -95,17 +102,6 @@ public function get_details($short = '') { } } - /** - * Return course ID for activities - * - * @return int - */ - private function get_course() { - global $DB; - $courseid = $DB->get_field('badge', 'courseid', array('id' => $this->badgeid)); - return $courseid; - } - /** * Add appropriate new criteria options to the form * @@ -184,14 +180,17 @@ public function get_options(&$mform) { * 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) { - global $DB; + public function review($userid, $filtered = false) { $completionstates = array(COMPLETION_COMPLETE, COMPLETION_COMPLETE_PASS); - $course = $DB->get_record('course', array('id' => $this->courseid)); + $course = new stdClass(); + $course->id = $this->courseid; - if ($course->startdate > time()) { + if ($this->coursestartdate > time()) { return false; } @@ -217,7 +216,7 @@ public function review($userid) { } else { return false; } - } else if ($this->method == BADGE_CRITERIA_AGGREGATION_ANY) { + } else { if (in_array($data->completionstate, $completionstates) && $check_date) { return true; } else { @@ -229,4 +228,44 @@ public function review($userid) { return $overall; } + + /** + * 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) { + foreach ($this->params as $param) { + $moduledata[] = " cmc.coursemoduleid = :completedmodule{$param['module']} "; + $params["completedmodule{$param['module']}"] = $param['module']; + } + if (!empty($moduledata)) { + $extraon = implode(' OR ', $moduledata); + $join = " JOIN {course_modules_completion} cmc ON cmc.userid = u.id AND + ( cmc.completionstate = :completionpass OR cmc.completionstate = :completioncomplete ) AND ({$extraon})"; + $params["completionpass"] = COMPLETION_COMPLETE_PASS; + $params["completioncomplete"] = COMPLETION_COMPLETE; + } + return array($join, $where, $params); + } else { + foreach ($this->params as $param) { + $join .= " LEFT JOIN {course_modules_completion} cmc{$param['module']} ON + cmc{$param['module']}.userid = u.id AND + cmc{$param['module']}.coursemoduleid = :completedmodule{$param['module']} AND + ( cmc{$param['module']}.completionstate = :completionpass{$param['module']} OR + cmc{$param['module']}.completionstate = :completioncomplete{$param['module']} )"; + $where .= " AND cmc{$param['module']}.coursemoduleid IS NOT NULL "; + $params["completedmodule{$param['module']}"] = $param['module']; + $params["completionpass{$param['module']}"] = COMPLETION_COMPLETE_PASS; + $params["completioncomplete{$param['module']}"] = COMPLETION_COMPLETE; + } + return array($join, $where, $params); + } + } } diff --git a/badges/criteria/award_criteria_course.php b/badges/criteria/award_criteria_course.php index c6089a22c889c..295d927df5d19 100644 --- a/badges/criteria/award_criteria_course.php +++ b/badges/criteria/award_criteria_course.php @@ -38,9 +38,23 @@ class award_criteria_course extends award_criteria { /* @var int Criteria [BADGE_CRITERIA_TYPE_COURSE] */ public $criteriatype = BADGE_CRITERIA_TYPE_COURSE; + private $courseid; + private $coursestartdate; + public $required_param = 'course'; public $optional_params = array('grade', 'bydate'); + public function __construct($record) { + global $DB; + parent::__construct($record); + + $course = $DB->get_record_sql('SELECT b.courseid, c.startdate + FROM {badge} b INNER JOIN {course} c ON b.courseid = c.id + WHERE b.id = :badgeid ', array('badgeid' => $this->badgeid)); + $this->courseid = $course->courseid; + $this->coursestartdate = $course->startdate; + } + /** * Add appropriate form elements to the criteria form * @@ -151,18 +165,22 @@ public function get_options(&$mform) { * 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) { - global $DB; - foreach ($this->params as $param) { - $course = $DB->get_record('course', array('id' => $param['course'])); + public function review($userid, $filtered = false) { + $course = new stdClass(); + $course->id = $this->courseid; - if ($course->startdate > time()) { - return false; - } + if ($this->coursestartdate > time()) { + return false; + } - $info = new completion_info($course); + $info = new completion_info($course); + + foreach ($this->params as $param) { $check_grade = true; $check_date = true; @@ -171,7 +189,7 @@ public function review($userid) { $check_grade = ($grade->grade >= $param['grade']); } - if (isset($param['bydate'])) { + if (!$filtered && isset($param['bydate'])) { $cparams = array( 'userid' => $userid, 'course' => $course->id, @@ -188,4 +206,27 @@ public function review($userid) { return false; } -} \ No newline at end of file + + /** + * 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() { + // We have only one criterion here, so taking the first one. + $coursecriteria = reset($this->params); + + $join = " LEFT JOIN {course_completions} cc ON cc.userid = u.id AND cc.timecompleted > 0"; + $where = ' AND cc.course = :courseid '; + $params['courseid'] = $this->courseid; + + // Add by date parameter. + if (isset($param['bydate'])) { + $where .= ' AND cc.timecompleted <= :completebydate'; + $params['completebydate'] = $coursecriteria['bydate']; + } + + return array($join, $where, $params); + } +} diff --git a/badges/criteria/award_criteria_courseset.php b/badges/criteria/award_criteria_courseset.php index a20e70b7d0634..5aabfe8ec3e8b 100644 --- a/badges/criteria/award_criteria_courseset.php +++ b/badges/criteria/award_criteria_courseset.php @@ -202,12 +202,17 @@ public function get_options(&$mform) { /** * 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) { - global $DB; + public function review($userid, $filtered = false) { foreach ($this->params as $param) { - $course = $DB->get_record('course', array('id' => $param['course'])); + $course = new stdClass(); + $course->id = $param['course']; + $info = new completion_info($course); $check_grade = true; $check_date = true; @@ -217,7 +222,7 @@ public function review($userid) { $check_grade = ($grade->grade >= $param['grade']); } - if (isset($param['bydate'])) { + if (!$filtered && isset($param['bydate'])) { $cparams = array( 'userid' => $userid, 'course' => $course->id, @@ -235,7 +240,7 @@ public function review($userid) { } else { return false; } - } else if ($this->method == BADGE_CRITERIA_AGGREGATION_ANY) { + } else { if ($info->is_course_complete($userid) && $check_grade && $check_date) { return true; } else { @@ -247,4 +252,39 @@ public function review($userid) { return $overall; } + + /** + * 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) { + foreach ($this->params as $param) { + $coursedata[] = " cc.course = :completedcourse{$param['course']} "; + $params["completedcourse{$param['course']}"] = $param['course']; + } + if (!empty($coursedata)) { + $extraon = implode(' OR ', $coursedata); + $join = " JOIN {course_completions} cc ON cc.userid = u.id AND + cc.timecompleted > 0 AND ({$extraon})"; + } + return array($join, $where, $params); + } else { + foreach ($this->params as $param) { + $join .= " LEFT JOIN {course_completions} cc{$param['course']} ON + cc{$param['course']}.userid = u.id AND + cc{$param['course']}.course = :completedcourse{$param['course']} AND + cc{$param['course']}.timecompleted > 0 "; + $where .= " AND cc{$param['course']}.course IS NOT NULL "; + $params["completedcourse{$param['course']}"] = $param['course']; + } + return array($join, $where, $params); + } + } } diff --git a/badges/criteria/award_criteria_manual.php b/badges/criteria/award_criteria_manual.php index 672c9f8b89c68..616ba97aa474e 100644 --- a/badges/criteria/award_criteria_manual.php +++ b/badges/criteria/award_criteria_manual.php @@ -142,11 +142,19 @@ public function get_details($short = '') { * 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) { + public function review($userid, $filtered = false) { global $DB; + // Users were already filtered by criteria completion. + if ($filtered) { + return true; + } + $overall = false; foreach ($this->params as $param) { $crit = $DB->get_record('badge_manual_award', array('issuerrole' => $param['role'], 'recipientid' => $userid, 'badgeid' => $this->badgeid)); @@ -157,7 +165,7 @@ public function review($userid) { $overall = true; continue; } - } else if ($this->method == BADGE_CRITERIA_AGGREGATION_ANY) { + } else { if (!$crit) { $overall = false; continue; @@ -169,6 +177,41 @@ public function review($userid) { return $overall; } + /** + * 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) { + foreach ($this->params as $param) { + $roledata[] = " bma.issuerrole = :issuerrole{$param['role']} "; + $params["issuerrole{$param['role']}"] = $param['role']; + } + if (!empty($roledata)) { + $extraon = implode(' OR ', $roledata); + $join = " JOIN {badge_manual_award} bma ON bma.recipientid = u.id + AND bma.badgeid = :badgeid{$this->badgeid} AND ({$extraon})"; + $params["badgeid{$this->badgeid}"] = $this->badgeid; + } + return array($join, $where, $params); + } else { + foreach ($this->params as $param) { + $join .= " LEFT JOIN {badge_manual_award} bma{$param['role']} ON + bma{$param['role']}.recipientid = u.id AND + bma{$param['role']}.issuerrole = :issuerrole{$param['role']} "; + $where .= " AND bma{$param['role']}.issuerrole IS NOT NULL "; + $params["issuerrole{$param['role']}"] = $param['role']; + } + return array($join, $where, $params); + } + } + /** * Delete this criterion * diff --git a/badges/criteria/award_criteria_overall.php b/badges/criteria/award_criteria_overall.php index f04e3a4556825..32e871117b42d 100644 --- a/badges/criteria/award_criteria_overall.php +++ b/badges/criteria/award_criteria_overall.php @@ -86,9 +86,12 @@ public function get_details($short = '') { * Overall criteria review should be called only from other criteria handlers. * * @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) { + public function review($userid, $filtered = false) { global $DB; $sql = "SELECT bc.*, bcm.critid, bcm.userid, bcm.datemet @@ -114,7 +117,7 @@ public function review($userid) { $overall = true; continue; } - } else if ($this->method == BADGE_CRITERIA_AGGREGATION_ANY) { + } else { if ($crit->datemet === null) { $overall = false; continue; @@ -127,6 +130,16 @@ public function review($userid) { return $overall; } + /** + * 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() { + return array('', '', array()); + } + /** * Add appropriate criteria elements to the form * diff --git a/badges/criteria/award_criteria_profile.php b/badges/criteria/award_criteria_profile.php index 9940e9007ac79..ec871ab26bfec 100644 --- a/badges/criteria/award_criteria_profile.php +++ b/badges/criteria/award_criteria_profile.php @@ -156,35 +156,84 @@ public function get_details($short = '') { * 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) { + public function review($userid, $filtered = false) { global $DB; - $overall = false; + // Users were already filtered by criteria completion, no checks required. + if ($filtered) { + return true; + } + + $join = ''; + $where = ''; + $sqlparams = array(); + $rule = ($this->method == BADGE_CRITERIA_AGGREGATION_ANY) ? ' OR ' : ' AND '; + foreach ($this->params as $param) { if (is_numeric($param['field'])) { - $crit = $DB->get_field('user_info_data', 'data', array('userid' => $userid, 'fieldid' => $param['field'])); + $infodata[] = " uid.fieldid = :fieldid{$param['field']} "; + $sqlparams["fieldid{$param['field']}"] = $param['field']; } else { - $crit = $DB->get_field('user', $param['field'], array('id' => $userid)); + $userdata[] = " u.{$param['field']} != '' "; } + } - if ($this->method == BADGE_CRITERIA_AGGREGATION_ALL) { - if (!$crit) { - return false; - } else { - $overall = true; - continue; - } - } else if ($this->method == BADGE_CRITERIA_AGGREGATION_ANY) { - if (!$crit) { - $overall = false; - continue; - } else { - return true; - } - } + // Add user custom field parameters if there are any. + if (!empty($infodata)) { + $extraon = implode($rule, $infodata); + $join = " LEFT JOIN {user_info_data} uid ON uid.userid = u.id AND ({$extraon})"; + } + + // Add user table field parameters if there are any. + if (!empty($userdata)) { + $extraon = implode($rule, $userdata); + $where = " AND ({$extraon})"; } + + $sqlparams['userid'] = $userid; + $sql = "SELECT u.* FROM {user} u " . $join . " WHERE u.id = :userid " . $where; + $overall = $DB->record_exists_sql($sql, $sqlparams); + return $overall; } + + /** + * 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(); + $rule = ($this->method == BADGE_CRITERIA_AGGREGATION_ANY) ? ' OR ' : ' AND '; + + foreach ($this->params as $param) { + if (is_numeric($param['field'])) { + $infodata[] = " uid.fieldid = :fieldid{$param['field']} "; + $params["fieldid{$param['field']}"] = $param['field']; + } else { + $userdata[] = " u.{$param['field']} != '' "; + } + } + + // Add user custom fields if there are any. + if (!empty($infodata)) { + $extraon = implode($rule, $infodata); + $join = " LEFT JOIN {user_info_data} uid ON uid.userid = u.id AND ({$extraon})"; + } + + // Add user table fields if there are any. + if (!empty($userdata)) { + $extraon = implode($rule, $userdata); + $where = " AND ({$extraon})"; + } + return array($join, $where, $params); + } } diff --git a/badges/tests/badgeslib_test.php b/badges/tests/badgeslib_test.php index b70d377f1b004..97b327176ef1d 100644 --- a/badges/tests/badgeslib_test.php +++ b/badges/tests/badgeslib_test.php @@ -292,9 +292,10 @@ public function test_badges_observer_profile_criteria_review() { $criteria_overall = award_criteria::build(array('criteriatype' => BADGE_CRITERIA_TYPE_OVERALL, 'badgeid' => $badge->id)); $criteria_overall->save(array('agg' => BADGE_CRITERIA_AGGREGATION_ANY)); $criteria_overall1 = award_criteria::build(array('criteriatype' => BADGE_CRITERIA_TYPE_PROFILE, 'badgeid' => $badge->id)); - $criteria_overall1->save(array('agg' => BADGE_CRITERIA_AGGREGATION_ALL, 'field_address' => 'address')); + $criteria_overall1->save(array('agg' => BADGE_CRITERIA_AGGREGATION_ALL, 'field_address' => 'address', 'field_aim' => 'aim')); $this->user->address = 'Test address'; + $this->user->aim = '999999999'; $sink = $this->redirectEmails(); user_update_user($this->user, false); $this->assertCount(1, $sink->get_messages()); diff --git a/badges/upgrade.txt b/badges/upgrade.txt new file mode 100644 index 0000000000000..1a48ffd9cfcf1 --- /dev/null +++ b/badges/upgrade.txt @@ -0,0 +1,12 @@ +This files describes API changes in /badges/*, +information provided here is intended especially for developers. + +=== 2.7 === + +* get_completed_criteria_sql() - This method was added to award_criteria class and must be overriden + in all criteria classes. This method returns an array consisting of SQL JOIN statement, WHERE conditions, + and any parameters that might be required. The results are used in lib/badgeslib.php in review_all_criteria() + to reduce to the minimum the number of users to review and award badges. + +* New optional parameter $filtered in review() allows to indicate that some expensive checks can be skipped + if the list of users has been initially filtered based on met criteria. diff --git a/lib/badgeslib.php b/lib/badgeslib.php index 8219cdb854866..36e3358a4d998 100644 --- a/lib/badgeslib.php +++ b/lib/badgeslib.php @@ -429,51 +429,63 @@ public function review_all_criteria() { set_time_limit(0); raise_memory_limit(MEMORY_HUGE); - // For site level badges, get all active site users who can earn this badge and haven't got it yet. - if ($this->type == BADGE_TYPE_SITE) { - $sql = 'SELECT DISTINCT u.id, bi.badgeid + foreach ($this->criteria as $crit) { + // Overall criterion is decided when other criteria are reviewed. + if ($crit->criteriatype == BADGE_CRITERIA_TYPE_OVERALL) { + continue; + } + + list($extrajoin, $extrawhere, $extraparams) = $crit->get_completed_criteria_sql(); + // For site level badges, get all active site users who can earn this badge and haven't got it yet. + if ($this->type == BADGE_TYPE_SITE) { + $sql = "SELECT DISTINCT u.id, bi.badgeid FROM {user} u + {$extrajoin} LEFT JOIN {badge_issued} bi ON u.id = bi.userid AND bi.badgeid = :badgeid - WHERE bi.badgeid IS NULL AND u.id != :guestid AND u.deleted = 0'; - $toearn = $DB->get_fieldset_sql($sql, array('badgeid' => $this->id, 'guestid' => $CFG->siteguest)); - } else { - // For course level badges, get users who can earn this badge in the course. - // These are all enrolled users with capability moodle/badges:earnbadge. - $earned = $DB->get_fieldset_select('badge_issued', 'userid AS id', 'badgeid = :badgeid', array('badgeid' => $this->id)); - $users = get_enrolled_users($this->get_context(), 'moodle/badges:earnbadge', 0, 'u.id'); - $toearn = array_diff(array_keys($users), $earned); - } + WHERE bi.badgeid IS NULL AND u.id != :guestid AND u.deleted = 0 " . $extrawhere; + $params = array_merge(array('badgeid' => $this->id, 'guestid' => $CFG->siteguest), $extraparams); + $toearn = $DB->get_fieldset_sql($sql, $params); + } else { + // For course level badges, get all users who already earned the badge in this course. + // Then find the ones who are enrolled in the course and don't have a badge yet. + $earned = $DB->get_fieldset_select('badge_issued', 'userid AS id', 'badgeid = :badgeid', array('badgeid' => $this->id)); + $wheresql = ''; + $earnedparams = array(); + if (!empty($earned)) { + list($earnedsql, $earnedparams) = $DB->get_in_or_equal($earned, SQL_PARAMS_NAMED, 'u', false); + $wheresql = ' WHERE u.id ' . $earnedsql; + } + list($enrolledsql, $enrolledparams) = get_enrolled_sql($this->get_context(), 'moodle/badges:earnbadge', 0, true); + $sql = "SELECT u.id + FROM {user} u + {$extrajoin} + JOIN ({$enrolledsql}) je ON je.id = u.id " . $wheresql . $extrawhere; + $params = array_merge($enrolledparams, $earnedparams, $extraparams); + $toearn = $DB->get_fieldset_sql($sql, $params); + } - foreach ($toearn as $uid) { - $toreview = false; - foreach ($this->criteria as $crit) { - if ($crit->criteriatype != BADGE_CRITERIA_TYPE_OVERALL) { - if ($crit->review($uid)) { - $crit->mark_complete($uid); - if ($this->criteria[BADGE_CRITERIA_TYPE_OVERALL]->method == BADGE_CRITERIA_AGGREGATION_ANY) { - $this->criteria[BADGE_CRITERIA_TYPE_OVERALL]->mark_complete($uid); - $this->issue($uid); - $awards++; - break; - } else { - $toreview = true; - continue; - } + foreach ($toearn as $uid) { + $reviewoverall = false; + if ($crit->review($uid, true)) { + $crit->mark_complete($uid); + if ($this->criteria[BADGE_CRITERIA_TYPE_OVERALL]->method == BADGE_CRITERIA_AGGREGATION_ANY) { + $this->criteria[BADGE_CRITERIA_TYPE_OVERALL]->mark_complete($uid); + $this->issue($uid); + $awards++; } else { - if ($this->criteria[BADGE_CRITERIA_TYPE_OVERALL]->method == BADGE_CRITERIA_AGGREGATION_ANY) { - continue; - } else { - break; - } + $reviewoverall = true; } + } else { + // Will be reviewed some other time. + $reviewoverall = false; + } + // Review overall if it is required. + if ($reviewoverall && $this->criteria[BADGE_CRITERIA_TYPE_OVERALL]->review($uid)) { + $this->criteria[BADGE_CRITERIA_TYPE_OVERALL]->mark_complete($uid); + $this->issue($uid); + $awards++; } - } - // Review overall if it is required. - if ($toreview && $this->criteria[BADGE_CRITERIA_TYPE_OVERALL]->review($uid)) { - $this->criteria[BADGE_CRITERIA_TYPE_OVERALL]->mark_complete($uid); - $this->issue($uid); - $awards++; } }