Skip to content

Commit

Permalink
MDL-32103 completion: Allow instant completion updates.
Browse files Browse the repository at this point in the history
For activity based course completion criteria allow instant
course completion updates if the activity completion state was changed
for a single user.
  • Loading branch information
ilyatregubov committed Jul 13, 2021
1 parent c357779 commit 4819625
Show file tree
Hide file tree
Showing 26 changed files with 941 additions and 240 deletions.
Expand Up @@ -61,10 +61,7 @@ Feature: Enable Block Completion in a course using activity completion
And I am on "Course 1" course homepage
And I follow "Test page name"
And I am on "Course 1" course homepage
Then I should see "Status: Pending" in the "Course completion status" "block"
And I should see "0 of 1" in the "Activity completion" "table_row"
And I trigger cron
And I am on "Course 1" course homepage
Then I should see "Status: Complete" in the "Course completion status" "block"
And I should see "1 of 1" in the "Activity completion" "table_row"
And I follow "More details"
And I should see "Yes" in the "Activity completion" "table_row"
61 changes: 61 additions & 0 deletions completion/classes/api.php
Expand Up @@ -123,4 +123,65 @@ public static function update_completion_date_event($cmid, $modulename, $instanc

return true;
}

/**
* Mark users who completed course based on activity criteria.
* @param array $userdata If set only marks specified user in given course else checks all courses/users.
* @return int Completion record id if $userdata is set, 0 else.
* @since Moodle 4.0
*/
public static function mark_course_completions_activity_criteria($userdata = null): int {
global $DB;

// Get all users who meet this criteria
$sql = "SELECT DISTINCT c.id AS course,
cr.id AS criteriaid,
ra.userid AS userid,
mc.timemodified AS timecompleted
FROM {course_completion_criteria} cr
INNER JOIN {course} c ON cr.course = c.id
INNER JOIN {context} con ON con.instanceid = c.id
INNER JOIN {role_assignments} ra ON ra.contextid = con.id
INNER JOIN {course_modules_completion} mc ON mc.coursemoduleid = cr.moduleinstance AND mc.userid = ra.userid
LEFT JOIN {course_completion_crit_compl} cc ON cc.criteriaid = cr.id AND cc.userid = ra.userid
WHERE cr.criteriatype = :criteriatype
AND con.contextlevel = :contextlevel
AND c.enablecompletion = 1
AND cc.id IS NULL
AND (
mc.completionstate = :completionstate
OR mc.completionstate = :completionstatepass
OR mc.completionstate = :completionstatefail
)";

$params = [
'criteriatype' => COMPLETION_CRITERIA_TYPE_ACTIVITY,
'contextlevel' => CONTEXT_COURSE,
'completionstate' => COMPLETION_COMPLETE,
'completionstatepass' => COMPLETION_COMPLETE_PASS,
'completionstatefail' => COMPLETION_COMPLETE_FAIL
];

if ($userdata) {
$params['courseid'] = $userdata['courseid'];
$params['userid'] = $userdata['userid'];
$sql .= " AND c.id = :courseid AND ra.userid = :userid";
// Mark as complete.
$record = $DB->get_record_sql($sql, $params);
if ($record) {
$completion = new \completion_criteria_completion((array) $record, DATA_OBJECT_FETCH_BY_KEY);
$result = $completion->mark_complete($record->timecompleted);
return $result;
}
} else {
// Loop through completions, and mark as complete.
$rs = $DB->get_recordset_sql($sql, $params);
foreach ($rs as $record) {
$completion = new \completion_criteria_completion((array) $record, DATA_OBJECT_FETCH_BY_KEY);
$completion->mark_complete($record->timecompleted);
}
$rs->close();
}
return 0;
}
}
21 changes: 11 additions & 10 deletions completion/completion_completion.php
Expand Up @@ -101,6 +101,7 @@ public function is_complete() {
* If the user is already marked as started, no change will occur
*
* @param integer $timeenrolled Time enrolled (optional)
* @return int|null id of completion record on successful update.
*/
public function mark_enrolled($timeenrolled = null) {

Expand All @@ -122,6 +123,7 @@ public function mark_enrolled($timeenrolled = null) {
* If the user is already marked as inprogress, the time will not be changed
*
* @param integer $timestarted Time started (optional)
* @return int|null id of completion record on successful update.
*/
public function mark_inprogress($timestarted = null) {

Expand Down Expand Up @@ -149,14 +151,14 @@ public function mark_inprogress($timestarted = null) {
* in the course are complete.
*
* @param integer $timecomplete Time completed (optional)
* @return void
* @return int|null id of completion record on successful update.
*/
public function mark_complete($timecomplete = null) {
global $USER;

// Never change a completion time.
if ($this->timecompleted) {
return;
return null;
}

// Use current time if nothing supplied.
Expand All @@ -166,7 +168,6 @@ public function mark_complete($timecomplete = null) {

// Set time complete.
$this->timecompleted = $timecomplete;

// Save record.
if ($result = $this->_save()) {
$data = $this->get_record_data();
Expand Down Expand Up @@ -211,17 +212,16 @@ public function mark_complete($timecomplete = null) {
*
* This method creates a course_completions record if none exists
* @access private
* @return bool
* @return int|null id of completion record on successful update.
*/
private function _save() {
if ($this->timeenrolled === null) {
$this->timeenrolled = 0;
}

$result = false;
// Save record
if ($this->id) {
$result = $this->update();
if (isset($this->id)) {
$success = $this->update();
} else {
// Make sure reaggregate field is not null
if (!$this->reaggregate) {
Expand All @@ -233,17 +233,18 @@ private function _save() {
$this->timestarted = 0;
}

$result = $this->insert();
$success = $this->insert();
}

if ($result) {
if ($success) {
// Update the cached record.
$cache = cache::make('core', 'coursecompletion');
$data = $this->get_record_data();
$key = $data->userid . '_' . $data->course;
$cache->set($key, ['value' => $data]);
return $this->id;
}

return $result;
return null;
}
}
4 changes: 3 additions & 1 deletion completion/completion_criteria_completion.php
Expand Up @@ -102,6 +102,7 @@ public function is_complete() {
* Mark this criteria complete for the associated user
*
* This method creates a course_completion_crit_compl record
* @return int id of completion record.
*/
public function mark_complete() {
// Create record
Expand All @@ -120,7 +121,8 @@ public function mark_complete() {
'userid' => $this->userid
);
$ccompletion = new completion_completion($cc);
$ccompletion->mark_inprogress($this->timecompleted);
$result = $ccompletion->mark_inprogress($this->timecompleted);
return $result;
}

/**
Expand Down
48 changes: 1 addition & 47 deletions completion/criteria/completion_criteria_activity.php
Expand Up @@ -203,53 +203,7 @@ public function get_type_title() {
* Find users who have completed this criteria and mark them accordingly
*/
public function cron() {
global $DB;

// Get all users who meet this criteria
$sql = '
SELECT DISTINCT
c.id AS course,
cr.id AS criteriaid,
ra.userid AS userid,
mc.timemodified AS timecompleted
FROM
{course_completion_criteria} cr
INNER JOIN
{course} c
ON cr.course = c.id
INNER JOIN
{context} con
ON con.instanceid = c.id
INNER JOIN
{role_assignments} ra
ON ra.contextid = con.id
INNER JOIN
{course_modules_completion} mc
ON mc.coursemoduleid = cr.moduleinstance
AND mc.userid = ra.userid
LEFT JOIN
{course_completion_crit_compl} cc
ON cc.criteriaid = cr.id
AND cc.userid = ra.userid
WHERE
cr.criteriatype = '.COMPLETION_CRITERIA_TYPE_ACTIVITY.'
AND con.contextlevel = '.CONTEXT_COURSE.'
AND c.enablecompletion = 1
AND cc.id IS NULL
AND (
mc.completionstate = '.COMPLETION_COMPLETE.'
OR mc.completionstate = '.COMPLETION_COMPLETE_PASS.'
OR mc.completionstate = '.COMPLETION_COMPLETE_FAIL.'
)
';

// Loop through completions, and mark as complete
$rs = $DB->get_recordset_sql($sql);
foreach ($rs as $record) {
$completion = new completion_criteria_completion((array) $record, DATA_OBJECT_FETCH_BY_KEY);
$completion->mark_complete($record->timecompleted);
}
$rs->close();
\core_completion\api::mark_course_completions_activity_criteria();
}

/**
Expand Down
74 changes: 74 additions & 0 deletions completion/tests/api_test.php
Expand Up @@ -222,4 +222,78 @@ public function test_update_completion_date_event_delete_completion_disabled() {
// Check that there is now no event in the database.
$this->assertEquals(0, $DB->count_records('event'));
}

/**
* Test for mark_course_completions_activity_criteria().
*/
public function test_mark_course_completions_activity_criteria() {
global $DB, $CFG;
require_once($CFG->dirroot.'/completion/criteria/completion_criteria_activity.php');
$this->resetAfterTest(true);

$course = $this->getDataGenerator()->create_course(array('enablecompletion' => 1));
$student1 = $this->getDataGenerator()->create_user();
$student2 = $this->getDataGenerator()->create_user();

$teacher = $this->getDataGenerator()->create_user();
$studentrole = $DB->get_record('role', array('shortname' => 'student'));
$teacherrole = $DB->get_record('role', array('shortname' => 'editingteacher'));

$this->getDataGenerator()->enrol_user($teacher->id, $course->id, $teacherrole->id);
$this->getDataGenerator()->enrol_user($student1->id, $course->id, $studentrole->id);
$this->getDataGenerator()->enrol_user($student2->id, $course->id, $studentrole->id);

$data = $this->getDataGenerator()->create_module('data', array('course' => $course->id),
array('completion' => 1));
$cmdata = get_coursemodule_from_id('data', $data->cmid);
$cm = get_coursemodule_from_instance('data', $data->id);
$c = new completion_info($course);

// Add activity completion criteria.
$criteriadata = new stdClass();
$criteriadata->id = $course->id;
$criteriadata->criteria_activity = array();
// Some activities.
$criteriadata->criteria_activity[$cmdata->id] = 1;
$criterion = new completion_criteria_activity();
$criterion->update_config($criteriadata);

$this->setUser($teacher);

// Mark activity complete for both users.
$completion = new stdClass();
$completion->coursemoduleid = $cm->id;
$completion->completionstate = COMPLETION_COMPLETE;
$completion->timemodified = time();
$completion->viewed = COMPLETION_NOT_VIEWED;
$completion->overrideby = null;

$completion->id = 0;
$completion->userid = $student1->id;
$c->internal_set_data($cm, $completion, true);

$completion->id = 0;
$completion->userid = $student2->id;
$c->internal_set_data($cm, $completion, true);

// Run instant course completions for student1. Only student1 will be marked as completed a course.
$userdata = ['userid' => $student1->id, 'courseid' => $course->id];
$actual = $DB->get_records('course_completions');
$this->assertEmpty($actual);

$coursecompletionid = \core_completion\api::mark_course_completions_activity_criteria($userdata);

$actual = $DB->get_records('course_completions');
$this->assertEquals(reset($actual)->id, $coursecompletionid);
$this->assertEquals(1, count($actual));
$this->assertEquals($student1->id, reset($actual)->userid);

// Run course completions cron. Both students will be marked as completed a course.
$coursecompletionid = \core_completion\api::mark_course_completions_activity_criteria();
$this->assertEquals(0, $coursecompletionid);
$actual = $DB->get_records('course_completions');
$this->assertEquals(2, count($actual));
$this->assertEquals($student1->id, reset($actual)->userid);
$this->assertEquals($student2->id, end($actual)->userid);
}
}

0 comments on commit 4819625

Please sign in to comment.