MDL-42965 badges: Improve badge criteria review performance

This commit is contained in:
Yuliya Bozhko 2013-11-25 08:32:34 +13:00
parent 9b37cd72a2
commit c8d2f392c5
10 changed files with 360 additions and 99 deletions

View File

@ -236,9 +236,20 @@ abstract class award_criteria {
* 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

View File

@ -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 @@ class award_criteria_activity extends award_criteria {
}
}
/**
* 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 @@ class award_criteria_activity extends award_criteria {
* 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 @@ class award_criteria_activity extends award_criteria {
} 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 @@ class award_criteria_activity extends award_criteria {
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);
}
}
}

View File

@ -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 @@ class award_criteria_course extends award_criteria {
* 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) {
$course = new stdClass();
$course->id = $this->courseid;
if ($this->coursestartdate > time()) {
return false;
}
$info = new completion_info($course);
foreach ($this->params as $param) {
$course = $DB->get_record('course', array('id' => $param['course']));
if ($course->startdate > time()) {
return false;
}
$info = new completion_info($course);
$check_grade = true;
$check_date = true;
@ -171,7 +189,7 @@ class award_criteria_course extends award_criteria {
$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 @@ class award_criteria_course extends award_criteria {
return false;
}
}
/**
* 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);
}
}

View File

@ -202,12 +202,17 @@ class award_criteria_courseset extends award_criteria {
/**
* 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 @@ class award_criteria_courseset extends award_criteria {
$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 @@ class award_criteria_courseset extends award_criteria {
} 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 @@ class award_criteria_courseset extends award_criteria {
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);
}
}
}

View File

@ -142,11 +142,19 @@ class award_criteria_manual extends award_criteria {
* 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 @@ class award_criteria_manual extends award_criteria {
$overall = true;
continue;
}
} else if ($this->method == BADGE_CRITERIA_AGGREGATION_ANY) {
} else {
if (!$crit) {
$overall = false;
continue;
@ -169,6 +177,41 @@ class award_criteria_manual extends award_criteria {
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
*

View File

@ -86,9 +86,12 @@ class award_criteria_overall extends award_criteria {
* 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 @@ class award_criteria_overall extends award_criteria {
$overall = true;
continue;
}
} else if ($this->method == BADGE_CRITERIA_AGGREGATION_ANY) {
} else {
if ($crit->datemet === null) {
$overall = false;
continue;
@ -127,6 +130,16 @@ class award_criteria_overall extends award_criteria {
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
*

View File

@ -156,35 +156,84 @@ class award_criteria_profile extends award_criteria {
* 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));
}
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;
}
$userdata[] = " u.{$param['field']} != '' ";
}
}
// 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);
}
}

View File

@ -292,9 +292,10 @@ class core_badges_badgeslib_testcase extends advanced_testcase {
$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());

12
badges/upgrade.txt Normal file
View File

@ -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.

View File

@ -429,51 +429,63 @@ class badge {
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);
}
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;
}
} else {
if ($this->criteria[BADGE_CRITERIA_TYPE_OVERALL]->method == BADGE_CRITERIA_AGGREGATION_ANY) {
continue;
} else {
break;
}
}
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);
}
// 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++;
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 {
$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++;
}
}
}