diff --git a/badges/criteria/award_criteria_activity.php b/badges/criteria/award_criteria_activity.php index e6a12de9f0e..f303abf7ed6 100644 --- a/badges/criteria/award_criteria_activity.php +++ b/badges/criteria/award_criteria_activity.php @@ -236,6 +236,7 @@ class award_criteria_activity extends award_criteria { * @return array list($join, $where, $params) */ public function get_completed_criteria_sql() { + global $DB; $join = ''; $where = ''; $params = array(); @@ -257,18 +258,41 @@ class award_criteria_activity extends award_criteria { } 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 = :completionfail{$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["completionfail{$param['module']}"] = COMPLETION_COMPLETE_FAIL; - $params["completioncomplete{$param['module']}"] = COMPLETION_COMPLETE; + // Get all cmids of modules related to the criteria. + $cmids = array_map(fn ($x) => $x['module'], $this->params); + list($cmcmodulessql, $paramscmc) = $DB->get_in_or_equal($cmids, SQL_PARAMS_NAMED); + + // Create a sql query to get all users who have worked on these course modules. + $sql = "SELECT DISTINCT userid FROM {course_modules_completion} cmc " + . "WHERE coursemoduleid " . $cmcmodulessql . " AND " + . "( cmc.completionstate IN ( :completionpass, :completionfail, :completioncomplete ) )"; + $paramscmcs = [ + 'completionpass' => COMPLETION_COMPLETE_PASS, + 'completionfail' => COMPLETION_COMPLETE_FAIL, + 'completioncomplete' => COMPLETION_COMPLETE + ]; + $paramscmc = array_merge($paramscmc, $paramscmcs); + $userids = $DB->get_records_sql($sql, $paramscmc); + + // Now check each user if the user has a completion of each module. + $useridsbadgeable = array_keys(array_filter( + $userids, + function ($user) use ($cmcmodulessql, $paramscmc, $cmids) { + global $DB; + $params = array_merge($paramscmc, ['userid' => $user->userid]); + $select = "coursemoduleid " . $cmcmodulessql . " AND userid = :userid"; + $cmidsuser = $DB->get_fieldset_select('course_modules_completion', 'coursemoduleid', $select, $params); + return empty(array_diff($cmidsuser, $cmids)); + } + )); + + // Finally create a where statement (if neccessary) with all userids who are allowed to get the badge. + // This list also includes all users who have previously received the badge. These are filtered out in the badge.php. + $join = ""; + $where = ""; + if (!empty($useridsbadgeable)) { + list($wherepart, $params) = $DB->get_in_or_equal($useridsbadgeable, SQL_PARAMS_NAMED); + $where = " AND u.id " . $wherepart; } return array($join, $where, $params); } diff --git a/badges/tests/badgeslib_test.php b/badges/tests/badgeslib_test.php index e1469437d45..3d981fb6103 100644 --- a/badges/tests/badgeslib_test.php +++ b/badges/tests/badgeslib_test.php @@ -31,6 +31,7 @@ require_once($CFG->libdir . '/badgeslib.php'); require_once($CFG->dirroot . '/badges/lib.php'); use core_badges\helper; +use core\task\manager; class badgeslib_test extends advanced_testcase { protected $badgeid; @@ -511,6 +512,88 @@ class badgeslib_test extends advanced_testcase { $this->assertEquals(badge_message_from_template($message, $params), $result); } + /** + * Test for working around the 61 tables join limit of mysql in award_criteria_activity in combination with the scheduled task. + * @return void + * @throws dml_transaction_exception + * @throws coding_exception + * @throws dml_exception + * @throws moodle_exception + * @throws ddl_exception + * @throws AssertionFailedError + * @throws InvalidArgumentException + * @throws ExpectationFailedException + * @covers \core_badges\badge->review_all_criteria() + */ + public function test_badge_activity_criteria_with_a_huge_number_of_coursemodules() { + global $CFG; + require_once($CFG->dirroot.'/completion/criteria/completion_criteria_activity.php'); + + // Messaging is not compatible with transactions. + $this->preventResetByRollback(); + + // Create more than 61 modules to potentially trigger an mysql db error. + $assigncount = 75; + $assigns = []; + for ($i = 1; $i <= $assigncount; $i++) { + $assigns[] = $this->getDataGenerator()->create_module('assign', ['course' => $this->course->id], ['completion' => 1]); + } + $assigncmids = array_flip(array_map(fn ($assign) => $assign->cmid, $assigns)); + $criteriaactivityarray = array_fill_keys(array_keys($assigncmids), 1); + + // Set completion criteria. + $criteriadata = (object) [ + 'id' => $this->course->id, + 'criteria_activity' => $criteriaactivityarray, + ]; + $criterion = new completion_criteria_activity(); + $criterion->update_config($criteriadata); + + $badge = new badge($this->coursebadge); + + $criteriaoverall = award_criteria::build(array('criteriatype' => BADGE_CRITERIA_TYPE_OVERALL, 'badgeid' => $badge->id)); + $criteriaoverall->save(array('agg' => BADGE_CRITERIA_AGGREGATION_ANY)); + $criteriaactivity = award_criteria::build(['criteriatype' => BADGE_CRITERIA_TYPE_ACTIVITY, 'badgeid' => $badge->id]); + + $modulescrit = ['agg' => BADGE_CRITERIA_AGGREGATION_ALL]; + foreach ($assigns as $assign) { + $modulescrit['module_' . $assign->cmid] = $assign->cmid; + } + $criteriaactivity->save($modulescrit); + + // Take one assign to complete it later. + $assingtemp = array_shift($assigns); + + // Mark the user to complete the modules. + foreach ($assigns as $assign) { + $cmassign = get_coursemodule_from_id('assign', $assign->cmid); + $completion = new \completion_info($this->course); + $completion->update_state($cmassign, COMPLETION_COMPLETE, $this->user->id); + } + + // Run the scheduled task to issue the badge. But the badge should not be issued. + ob_start(); + $task = manager::get_scheduled_task('core\task\badges_cron_task'); + $task->execute(); + ob_end_clean(); + + $this->assertFalse($badge->is_issued($this->user->id)); + + // Now complete the last uncompleted module. + $cmassign = get_coursemodule_from_id('assign', $assingtemp->cmid); + $completion = new \completion_info($this->course); + $completion->update_state($cmassign, COMPLETION_COMPLETE, $this->user->id); + + // Run the scheduled task to issue the badge. Now the badge schould be issued. + ob_start(); + $task = manager::get_scheduled_task('core\task\badges_cron_task'); + $task->execute(); + ob_end_clean(); + + $this->assertDebuggingCalled('Error baking badge image!'); + $this->assertTrue($badge->is_issued($this->user->id)); + } + /** * Test badges observer when course module completion event id fired. */