MDL-74519 badges: Fix date validation in course completion criteria

Ensures that badge awarding correctly evaluates the configured by-date
condition in course and course set completion criteria.

Co-authored by: Diego Campos
This commit is contained in:
Anupama Sarjoshi
2026-03-11 09:58:22 +05:30
parent 6ebf41a6c2
commit b2d7c1d147
3 changed files with 167 additions and 3 deletions
@@ -234,7 +234,7 @@ class award_criteria_course extends award_criteria {
$params['courseid'] = $this->courseid;
// Add by date parameter.
if (isset($param['bydate'])) {
if (isset($coursecriteria['bydate'])) {
$where .= ' AND cc.timecompleted <= :completebydate';
$params['completebydate'] = $coursecriteria['bydate'];
}
@@ -211,7 +211,10 @@ class award_criteria_courseset extends award_criteria {
$check_grade = ($grade->grade >= $param['grade']);
}
if (!$filtered && isset($param['bydate'])) {
// When the user list is not filtered, or the aggregation method is ANY,
// ensure the course completion date is before the bydate (if set).
$comparedates = !$filtered || $this->method == BADGE_CRITERIA_AGGREGATION_ANY;
if ($comparedates && isset($param['bydate'])) {
$cparams = array(
'userid' => $userid,
'course' => $course->id,
@@ -254,9 +257,16 @@ class award_criteria_courseset extends award_criteria {
$params = array();
if ($this->method == BADGE_CRITERIA_AGGREGATION_ANY) {
$coursedata = [];
foreach ($this->params as $param) {
$coursedata[] = " cc.course = :completedcourse{$param['course']} ";
$conditionsql = " cc.course = :completedcourse{$param['course']} ";
$params["completedcourse{$param['course']}"] = $param['course'];
if (isset($param['bydate'])) {
$conditionsql .= " AND cc.timecompleted <= :completedbydate{$param['course']} ";
$params["completedbydate{$param['course']}"] = $param['bydate'];
}
$coursedata[] = "({$conditionsql})";
}
if (!empty($coursedata)) {
$extraon = implode(' OR ', $coursedata);
@@ -272,6 +282,11 @@ class award_criteria_courseset extends award_criteria {
cc{$param['course']}.timecompleted > 0 ";
$where .= " AND cc{$param['course']}.course IS NOT NULL ";
$params["completedcourse{$param['course']}"] = $param['course'];
if (isset($param['bydate'])) {
$where .= " AND cc{$param['course']}.timecompleted <= :completedbydate{$param['course']} ";
$params["completedbydate{$param['course']}"] = $param['bydate'];
}
}
return array($join, $where, $params);
}
@@ -0,0 +1,149 @@
@mod @mod_quiz @core_badges @core_completion @javascript
Feature: Award badges based on course completion
In order to ensure a student has learned the required material
As a teacher
I need a badge to be awarded only when the student completes the course or a set of courses.
Background:
Given the following "users" exist:
| username | firstname | lastname | email |
| student1 | Student | 1 | student1@example.com |
| student2 | Student | 2 | student2@example.com |
| teacher1 | Teacher | 1 | teacher1@example.com |
And the following "courses" exist:
| fullname | shortname | category | enablecompletion |
| Course 1 | C1 | 0 | 1 |
| Course 2 | C2 | 0 | 1 |
And the following "course enrolments" exist:
| user | course | role |
| teacher1 | C1 | editingteacher |
| student1 | C1 | student |
| student2 | C1 | student |
| teacher1 | C2 | editingteacher |
| student1 | C2 | student |
| student2 | C2 | student |
And the following "question categories" exist:
| contextlevel | reference | name |
| Course | C1 | Test questions |
| Course | C2 | Test questions |
And the following "questions" exist:
| questioncategory | qtype | name | questiontext |
| Test questions | truefalse | First question | Answer the first question |
And the following "activities" exist:
| activity | name | course | idnumber | attempts | gradepass | completion | completionpassgrade | completionusegrade |
| quiz | Test quiz name C1 | C1 | quiz1 | 2 | 5.00 | 2 | 1 | 1 |
| quiz | Test quiz name C2 | C2 | quiz1 | 2 | 5.00 | 2 | 1 | 1 |
And quiz "Test quiz name C1" contains the following questions:
| question | page |
| First question | 1 |
And quiz "Test quiz name C2" contains the following questions:
| question | page |
| First question | 1 |
And the following "blocks" exist:
| blockname | contextlevel | reference | pagetypepattern | defaultregion |
| completionstatus | Course | C1 | course-view-* | side-pre |
| completionstatus | Course | C2 | course-view-* | side-pre |
And I am on the "Course 1" course page logged in as teacher1
And I navigate to "Course completion" in current page administration
And I expand all fieldsets
And I set the field "Test quiz name C1" to "1"
And I press "Save changes"
And I am on the "Course 2" course page logged in as teacher1
And I navigate to "Course completion" in current page administration
And I expand all fieldsets
And I set the field "Test quiz name C2" to "1"
And I press "Save changes"
Scenario Outline: Badge awarded depending on the course completion by-date
Given I am on the "Course 1" course page
And the following "core_badges > Badge" exists:
| name | Course Badge 1 |
| status | 0 |
| type | 2 |
| course | C1 |
| description | Course badge 1 description |
| image | badges/tests/behat/badge.png |
And I navigate to "Badges" in current page administration
And I click on "Course Badge 1" "link"
And I select "Criteria" from the "jump" singleselect
And I set the field "type" to "Course completion"
And I set the field "Enable" to "1"
And I set the field "Day" to "1"
And I set the field "Month" to "January"
And I set the field "Year" to "<year>"
And I press "Save"
And I press "Enable access"
And I click on "Enable" "button" in the "Confirm" "dialogue"
And user "student1" has attempted "Test quiz name C1" with responses:
| slot | response |
| 1 | True |
# Completion cron won't mark the whole course completed unless the
# individual criteria was marked completed more than a second ago. So
# run it twice, first to mark the criteria and second for the course.
And I run the scheduled task "core\task\completion_regular_task"
And I wait "1" seconds
And I run the scheduled task "core\task\completion_regular_task"
When I run the scheduled task "core\task\badges_cron_task"
And I run all adhoc tasks
And I navigate to "Badges" in current page administration
And I click on "Course Badge 1" "link"
Then I should see "Recipients (<count>)"
And I select "Recipients (<count>)" from the "jump" singleselect
And I <student1_visibility> see "Student 1"
And I should not see "Student 2"
Examples:
| year | count | student1_visibility |
| ## +1 year ## %Y ## | 1 | should |
| ## -1 year ## %Y ## | 0 | should not |
Scenario Outline: Badge awarded depending on the courseset completion by-date
Given I am logged in as "admin"
And the following "core_badges > Badge" exists:
| name | Site Badge |
| status | 0 |
| description | Site badge description |
| image | badges/tests/behat/badge.png |
And I navigate to "Badges > Manage badges" in site administration
And I press "Edit" action in the "Site Badge" report row
And I select "Criteria" from the "jump" singleselect
And I set the field "type" to "Completing a set of courses"
And I set the field "courses" to "Course 1, Course 2"
And I press "Add courses"
And I expand all fieldsets
# Set a very high grade requirement to ensure that the course completion criteria won't be marked completed just by passing the quiz.
And I set the field with xpath "(//input[contains(@name, 'grade')])[1]" to "200"
And I set the field with xpath "(//input[contains(@name, 'enabled')])[2]" to "1"
And I set the field with xpath "(//select[contains(@name, 'day')])[2]" to "1"
And I set the field with xpath "(//select[contains(@name, 'month')])[2]" to "January"
And I set the field with xpath "(//select[contains(@name, 'year')])[2]" to "<year>"
And I click on "Any of the selected courses is complete" "radio"
And I press "Save"
And I press "Enable access"
And I click on "Enable" "button" in the "Confirm" "dialogue"
And user "student1" has attempted "Test quiz name C1" with responses:
| slot | response |
| 1 | True |
And user "student1" has attempted "Test quiz name C2" with responses:
| slot | response |
| 1 | True |
# Completion cron won't mark the whole course completed unless the
# individual criteria was marked completed more than a second ago. So
# run it twice, first to mark the criteria and second for the course.
And I run the scheduled task "core\task\completion_regular_task"
And I wait "1" seconds
And I run the scheduled task "core\task\completion_regular_task"
When I run the scheduled task "core\task\badges_cron_task"
And I run all adhoc tasks
And I navigate to "Badges > Manage badges" in site administration
And I follow "Site Badge"
Then I should see "Recipients (<count>)"
And I select "Recipients (<count>)" from the "jump" singleselect
And I <student1_visibility> see "Student 1"
And I should not see "Student 2"
Examples:
| year | count | student1_visibility |
| ## +1 year ## %Y ## | 1 | should |
| ## -1 year ## %Y ## | 0 | should not |