diff --git a/classes/task/reengagement_adhoc_task.php b/classes/task/reengagement_adhoc_task.php new file mode 100644 index 0000000..a1aa779 --- /dev/null +++ b/classes/task/reengagement_adhoc_task.php @@ -0,0 +1,271 @@ +. + +/** + * This page lists all the instances of reengagement in a particular course + * + * @package mod_reengagement + * @author Rajan Dangi + * @copyright 2024 Catalyst IT {@link http://www.catalyst-au.net} + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace mod_reengagement\task; + +use stdClass; +use core\task\adhoc_task; +use context_module; +use cache; + +defined('MOODLE_INTERNAL') || die(); + +require_once($CFG->libdir . '/completionlib.php'); +require_once($CFG->dirroot . '/mod/reengagement/lib.php'); + +/** + * Adhoc task for reengagement + */ +class reengagement_adhoc_task extends adhoc_task { + /** + * Execute the task. + */ + public function execute() { + global $DB; + + $data = $this->get_custom_data(); + if (empty($data)) { + return; + } + + // Get a list of users who are eligible to start this module. + $startusers = reengagement_get_startusers($data); + + // Prepare the objects for db iteration. + $timenow = time(); + $reengagementinprogress = new stdClass(); + $reengagementinprogress->reengagement = $data->rid; + $reengagementinprogress->completiontime = $timenow + $data->duration; + $reengagementinprogress->emailtime = $timenow + $data->emaildelay; + $activitycompletion = new stdClass(); + $activitycompletion->coursemoduleid = $data->cmid; + $activitycompletion->completionstate = COMPLETION_INCOMPLETE; + $activitycompletion->timemodified = $timenow; + + // Start reengagement for each user. + $userlist = array_keys($startusers); + $newripcount = count($userlist); // Count of new reengagements in progress. + + if (debugging('', DEBUG_DEVELOPER) || ($newripcount && debugging('', DEBUG_ALL))) { + mtrace("Found $newripcount users to start reengagementid " . $data->rid); + } + + foreach ($userlist as $userid) { + $reengagementinprogress->userid = $userid; + $DB->insert_record('reengagement_inprogress', $reengagementinprogress); + $activitycompletion->userid = $userid; + $DB->insert_record('course_modules_completion', $activitycompletion); + } + + // Process completed reengagements. + $this->process_completed_reengagements($timenow, $data->rid); + } + + /** + * Processes completed reengagements and sends emails to users. + * + * This function: + * - retrieves completed reengagements from the database + * - checks if the user is still enrolled in the course + * - updates the completion record, + * - and sends an email to the user. + * - also handles in-progress records where the user has reached their email time. + * + * @param int $timenow The current time. + * @param int $reengagementid The ID of the reengagement to process. + * + * @return void + */ + private function process_completed_reengagements($timenow, $reengagementid) { + global $DB; + + // Get more info about the activity, & prepare to update db + // and email users. + + $reengagementssql = "SELECT r.id as id, cm.id as cmid, r.emailcontent, r.emailcontentformat, r.emailsubject, + r.thirdpartyemails, r.emailcontentmanager, r.emailcontentmanagerformat, r.emailsubjectmanager, + r.emailcontentthirdparty, r.emailcontentthirdpartyformat, r.emailsubjectthirdparty, + r.emailuser, r.name, r.suppresstarget, r.remindercount, c.shortname as courseshortname, + c.fullname as coursefullname, c.id as courseid, r.emailrecipient, r.emaildelay + FROM {reengagement} r + INNER JOIN {course_modules} cm ON cm.instance = r.id + INNER JOIN {course} c ON cm.course = c.id + JOIN {modules} m on m.id = cm.module + WHERE m.name = 'reengagement' AND r.id = :reengagementid + ORDER BY r.id ASC"; + $params = ['reengagementid' => $reengagementid]; + $reengagements = $DB->get_records_sql($reengagementssql, $params); + + $inprogresssql = 'SELECT ri.* + FROM {reengagement_inprogress} ri + JOIN {reengagement} r ON r.id = ri.reengagement + JOIN {user} u ON u.id = ri.userid + WHERE u.deleted = 0 AND + completiontime < ? AND completed = 0 AND ri.reengagement = ?'; + $inprogresses = $DB->get_recordset_sql($inprogresssql, array($timenow, $reengagementid)); + $completeripcount = 0; + foreach ($inprogresses as $inprogress) { + $completeripcount++; + // A user has completed an instance of the reengagement module. + $inprogress->timedue = $inprogress->completiontime; + $reengagement = $reengagements[$inprogress->reengagement]; + $cmid = $reengagement->cmid; // The cm id of the module which was completed. + $userid = $inprogress->userid; // The userid which completed the module. + + // Check if user is still enrolled in the course. + $context = context_module::instance($reengagement->cmid); + if (!is_enrolled($context, $userid, 'mod/reengagement:startreengagement', true)) { + $DB->delete_records('reengagement_inprogress', array('id' => $inprogress->id)); + continue; + } + + // Update completion record to indicate completion so the user can continue with any dependant activities. + $completionrecord = $DB->get_record('course_modules_completion', array('coursemoduleid' => $cmid, 'userid' => $userid)); + if (empty($completionrecord)) { + mtrace("Could not find completion record to update complete state, userid: $userid, cmid: $cmid - recreating record."); + // This might happen when reset_all_state has been triggered, deleting an "in-progress" record. so recreate it. + $completionrecord = new stdClass(); + $completionrecord->coursemoduleid = $cmid; + $completionrecord->completionstate = COMPLETION_COMPLETE_PASS; + $completionrecord->viewed = COMPLETION_VIEWED; + $completionrecord->overrideby = null; + $completionrecord->timemodified = $timenow; + $completionrecord->userid = $userid; + $completionrecord->id = $DB->insert_record('course_modules_completion', $completionrecord); + } else { + $updaterecord = new stdClass(); + $updaterecord->id = $completionrecord->id; + $updaterecord->completionstate = COMPLETION_COMPLETE_PASS; + $updaterecord->timemodified = $timenow; + $DB->update_record('course_modules_completion', $updaterecord) . " \n"; + } + $completioncache = cache::make('core', 'completion'); + $completioncache->delete($userid . '_' . $reengagement->courseid); + + $cmcontext = context_module::instance($cmid, MUST_EXIST); + // Trigger an event for course module completion changed. + $event = \core\event\course_module_completion_updated::create(array( + 'objectid' => $completionrecord->id, + 'context' => $cmcontext, + 'relateduserid' => $userid, + 'other' => array( + 'relateduserid' => $userid + ) + )); + $event->add_record_snapshot('course_modules_completion', $completionrecord); + $event->trigger(); + + $result = false; + if (($reengagement->emailuser == REENGAGEMENT_EMAILUSER_COMPLETION) || + ($reengagement->emailuser == REENGAGEMENT_EMAILUSER_NEVER) || + ($reengagement->emailuser == REENGAGEMENT_EMAILUSER_TIME && !empty($inprogress->emailsent)) + ) { + // No need to keep 'inprogress' record for later emailing + // Delete inprogress record. + debugging('', DEBUG_DEVELOPER) && mtrace("mode $reengagement->emailuser reengagementid $reengagement->id. + User marked complete, deleting inprogress record for user $userid"); + $result = $DB->delete_records('reengagement_inprogress', array('id' => $inprogress->id)); + } else { + // Update inprogress record to indicate completion done. + debugging('', DEBUG_DEVELOPER) && mtrace("mode $reengagement->emailuser reengagementid $reengagement->id + updating inprogress record for user $userid to indicate completion"); + $updaterecord = new stdClass(); + $updaterecord->id = $inprogress->id; + $updaterecord->completed = COMPLETION_COMPLETE; + $result = $DB->update_record('reengagement_inprogress', $updaterecord); + } + if (empty($result)) { + // Skip emailing. Go on to next completion record so we don't risk emailing users continuously each cron. + debugging('', DEBUG_ALL) && mtrace("Reengagement: not sending email to $userid regarding reengagementid + $reengagement->id due to failuer to update db"); + continue; + } + if ($reengagement->emailuser == REENGAGEMENT_EMAILUSER_COMPLETION) { + debugging('', DEBUG_ALL) && mtrace("Reengagement: sending email to $userid regarding reengagementid + $reengagement->id due to completion."); + reengagement_email_user($reengagement, $inprogress); + } + } + $inprogresses->close(); + + if (debugging('', DEBUG_DEVELOPER) || ($completeripcount && debugging('', DEBUG_ALL))) { + mtrace("Found $completeripcount complete reengagements."); + } + + // Get inprogress records where the user has reached their email time, and module is email 'after delay'. + $inprogresssql = "SELECT ip.*, ip.emailtime as timedue + FROM {reengagement_inprogress} ip + INNER JOIN {reengagement} r on r.id = ip.reengagement + JOIN {user} u ON u.id = ip.userid + WHERE ip.emailtime < :emailtime + AND r.emailuser = " . REENGAGEMENT_EMAILUSER_TIME . ' + AND ip.emailsent < r.remindercount + AND u.deleted = 0 + AND ip.reengagement = :reengagementid + ORDER BY r.id ASC'; + $params = array('emailtime' => $timenow, 'reengagementid' => $reengagementid); + + $inprogresses = $DB->get_recordset_sql($inprogresssql, $params); + $emailduecount = 0; + foreach ($inprogresses as $inprogress) { + $emailduecount++; + $reengagement = $reengagements[$inprogress->reengagement]; + $userid = $inprogress->userid; // The userid which completed the module. + + // Check if user is still enrolled in the course. + $context = context_module::instance($reengagement->cmid); + if (!is_enrolled($context, $userid, 'mod/reengagement:startreengagement', true)) { + $DB->delete_records('reengagement_inprogress', array('id' => $inprogress->id)); + continue; + } + + if ($inprogress->completed == COMPLETION_COMPLETE) { + debugging('', DEBUG_DEVELOPER) && mtrace("mode $reengagement->emailuser reengagementid $reengagement->id. + User already marked complete. Deleting inprogress record for user $userid"); + $result = $DB->delete_records('reengagement_inprogress', array('id' => $inprogress->id)); + } else { + debugging('', DEBUG_DEVELOPER) && mtrace("mode $reengagement->emailuser reengagementid $reengagement->id. + Updating inprogress record to indicate email sent for user $userid"); + $updaterecord = new stdClass(); + $updaterecord->id = $inprogress->id; + if ($reengagement->remindercount > $inprogress->emailsent) { + $updaterecord->emailtime = $timenow + $reengagement->emaildelay; + } + $updaterecord->emailsent = $inprogress->emailsent + 1; + $result = $DB->update_record('reengagement_inprogress', $updaterecord); + } + if (!empty($result)) { + debugging('', DEBUG_ALL) && mtrace("Reengagement: sending email to $userid regarding reengagementid + $reengagement->id due to emailduetime."); + reengagement_email_user($reengagement, $inprogress); + } + } + $inprogresses->close(); + + if (debugging('', DEBUG_DEVELOPER) || ($emailduecount && debugging('', DEBUG_ALL))) { + mtrace("Found $emailduecount reengagements due to be emailed."); + } + } +} diff --git a/lib.php b/lib.php index 631c1c1..3fd2a43 100644 --- a/lib.php +++ b/lib.php @@ -23,6 +23,9 @@ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ +use core\task\manager; +use mod_reengagement\task\reengagement_adhoc_task; + defined('MOODLE_INTERNAL') || die(); require_once($CFG->libdir."/completionlib.php"); @@ -187,7 +190,7 @@ function reengagement_crontask() { INNER JOIN {course_modules} cm on cm.instance = r.id JOIN {modules} m on m.id = cm.module WHERE m.name = 'reengagement' AND cm.deletioninprogress = 0 - ORDER BY r.id ASC"; + ORDER BY r.duration ASC"; $reengagements = $DB->get_recordset_sql($reengagementssql); if (!$reengagements->valid()) { @@ -198,199 +201,12 @@ function reengagement_crontask() { // First: add 'in-progress' records for those users who are able to start. foreach ($reengagements as $reengagementcm) { - // Get a list of users who are eligible to start this module. - $startusers = reengagement_get_startusers($reengagementcm); - - // Prepare some objects for later db insertion. - $reengagementinprogress = new stdClass(); - $reengagementinprogress->reengagement = $reengagementcm->rid; - $reengagementinprogress->completiontime = $timenow + $reengagementcm->duration; - $reengagementinprogress->emailtime = $timenow + $reengagementcm->emaildelay; - $activitycompletion = new stdClass(); - $activitycompletion->coursemoduleid = $reengagementcm->cmid; - $activitycompletion->completionstate = COMPLETION_INCOMPLETE; - $activitycompletion->timemodified = $timenow; - $userlist = array_keys($startusers); - $newripcount = count($userlist); // Count of new reengagements-in-progress. - if (debugging('', DEBUG_DEVELOPER) || ($newripcount && debugging('', DEBUG_ALL))) { - mtrace("Adding $newripcount reengagements-in-progress to reengagementid " . $reengagementcm->rid); - } - - foreach ($userlist as $userid) { - $reengagementinprogress->userid = $userid; - $DB->insert_record('reengagement_inprogress', $reengagementinprogress); - $activitycompletion->userid = $userid; - $DB->insert_record('course_modules_completion', $activitycompletion); + if (debugging('', DEBUG_DEVELOPER) || ($reengagementcm->cmid && debugging('', DEBUG_ALL))) { + mtrace("Adding adhoc task for course module id " . $reengagementcm->cmid); } + reengagement_queue_adhoc_task($reengagementcm); } $reengagements->close(); - // All new users have now been recorded as started. - // See if any previous users are due to finish, &/or be emailed. - - // Get more info about the activity, & prepare to update db - // and email users. - - $reengagementssql = "SELECT r.id as id, cm.id as cmid, r.emailcontent, r.emailcontentformat, r.emailsubject, - r.thirdpartyemails, r.emailcontentmanager, r.emailcontentmanagerformat, r.emailsubjectmanager, - r.emailcontentthirdparty, r.emailcontentthirdpartyformat, r.emailsubjectthirdparty, - r.emailuser, r.name, r.suppresstarget, r.remindercount, c.shortname as courseshortname, - c.fullname as coursefullname, c.id as courseid, r.emailrecipient, r.emaildelay - FROM {reengagement} r - INNER JOIN {course_modules} cm ON cm.instance = r.id - INNER JOIN {course} c ON cm.course = c.id - JOIN {modules} m on m.id = cm.module - WHERE m.name = 'reengagement' - ORDER BY r.id ASC"; - - $reengagements = $DB->get_records_sql($reengagementssql); - - $inprogresssql = 'SELECT ri.* - FROM {reengagement_inprogress} ri - JOIN {reengagement} r ON r.id = ri.reengagement - JOIN {user} u ON u.id = ri.userid - WHERE u.deleted = 0 AND - completiontime < ? AND completed = 0'; - $inprogresses = $DB->get_recordset_sql($inprogresssql, array($timenow)); - $completeripcount = 0; - foreach ($inprogresses as $inprogress) { - $completeripcount++; - // A user has completed an instance of the reengagement module. - $inprogress->timedue = $inprogress->completiontime; - $reengagement = $reengagements[$inprogress->reengagement]; - $cmid = $reengagement->cmid; // The cm id of the module which was completed. - $userid = $inprogress->userid; // The userid which completed the module. - - // Check if user is still enrolled in the course. - $context = context_module::instance($reengagement->cmid); - if (!is_enrolled($context, $userid, 'mod/reengagement:startreengagement', true)) { - $DB->delete_records('reengagement_inprogress', array('id' => $inprogress->id)); - continue; - } - - // Update completion record to indicate completion so the user can continue with any dependant activities. - $completionrecord = $DB->get_record('course_modules_completion', array('coursemoduleid' => $cmid, 'userid' => $userid)); - if (empty($completionrecord)) { - mtrace("Could not find completion record to update complete state, userid: $userid, cmid: $cmid - recreating record."); - // This might happen when reset_all_state has been triggered, deleting an "in-progress" record. so recreate it. - $completionrecord = new stdClass(); - $completionrecord->coursemoduleid = $cmid; - $completionrecord->completionstate = COMPLETION_COMPLETE_PASS; - $completionrecord->viewed = COMPLETION_VIEWED; - $completionrecord->overrideby = null; - $completionrecord->timemodified = $timenow; - $completionrecord->userid = $userid; - $completionrecord->id = $DB->insert_record('course_modules_completion', $completionrecord); - } else { - $updaterecord = new stdClass(); - $updaterecord->id = $completionrecord->id; - $updaterecord->completionstate = COMPLETION_COMPLETE_PASS; - $updaterecord->timemodified = $timenow; - $DB->update_record('course_modules_completion', $updaterecord) . " \n"; - } - $completioncache = cache::make('core', 'completion'); - $completioncache->delete($userid . '_' . $reengagement->courseid); - - $cmcontext = context_module::instance($cmid, MUST_EXIST); - // Trigger an event for course module completion changed. - $event = \core\event\course_module_completion_updated::create(array( - 'objectid' => $completionrecord->id, - 'context' => $cmcontext, - 'relateduserid' => $userid, - 'other' => array( - 'relateduserid' => $userid - ) - )); - $event->add_record_snapshot('course_modules_completion', $completionrecord); - $event->trigger(); - - $result = false; - if (($reengagement->emailuser == REENGAGEMENT_EMAILUSER_COMPLETION) || - ($reengagement->emailuser == REENGAGEMENT_EMAILUSER_NEVER) || - ($reengagement->emailuser == REENGAGEMENT_EMAILUSER_TIME && !empty($inprogress->emailsent))) { - // No need to keep 'inprogress' record for later emailing - // Delete inprogress record. - debugging('', DEBUG_DEVELOPER) && mtrace("mode $reengagement->emailuser reengagementid $reengagement->id. - User marked complete, deleting inprogress record for user $userid"); - $result = $DB->delete_records('reengagement_inprogress', array('id' => $inprogress->id)); - } else { - // Update inprogress record to indicate completion done. - debugging('', DEBUG_DEVELOPER) && mtrace("mode $reengagement->emailuser reengagementid $reengagement->id - updating inprogress record for user $userid to indicate completion"); - $updaterecord = new stdClass(); - $updaterecord->id = $inprogress->id; - $updaterecord->completed = COMPLETION_COMPLETE; - $result = $DB->update_record('reengagement_inprogress', $updaterecord); - } - if (empty($result)) { - // Skip emailing. Go on to next completion record so we don't risk emailing users continuously each cron. - debugging('', DEBUG_ALL) && mtrace("Reengagement: not sending email to $userid regarding reengagementid - $reengagement->id due to failuer to update db"); - continue; - } - if ($reengagement->emailuser == REENGAGEMENT_EMAILUSER_COMPLETION) { - debugging('', DEBUG_ALL) && mtrace("Reengagement: sending email to $userid regarding reengagementid - $reengagement->id due to completion."); - reengagement_email_user($reengagement, $inprogress); - } - } - $inprogresses->close(); - - if (debugging('', DEBUG_DEVELOPER) || ($completeripcount && debugging('', DEBUG_ALL))) { - mtrace("Found $completeripcount complete reengagements."); - } - - // Get inprogress records where the user has reached their email time, and module is email 'after delay'. - $inprogresssql = "SELECT ip.*, ip.emailtime as timedue - FROM {reengagement_inprogress} ip - INNER JOIN {reengagement} r on r.id = ip.reengagement - JOIN {user} u ON u.id = ip.userid - WHERE ip.emailtime < :emailtime - AND r.emailuser = " . REENGAGEMENT_EMAILUSER_TIME . ' - AND ip.emailsent < r.remindercount - AND u.deleted = 0 - ORDER BY r.id ASC'; - $params = array('emailtime' => $timenow); - - $inprogresses = $DB->get_recordset_sql($inprogresssql, $params); - $emailduecount = 0; - foreach ($inprogresses as $inprogress) { - $emailduecount++; - $reengagement = $reengagements[$inprogress->reengagement]; - $userid = $inprogress->userid; // The userid which completed the module. - - // Check if user is still enrolled in the course. - $context = context_module::instance($reengagement->cmid); - if (!is_enrolled($context, $userid, 'mod/reengagement:startreengagement', true)) { - $DB->delete_records('reengagement_inprogress', array('id' => $inprogress->id)); - continue; - } - - if ($inprogress->completed == COMPLETION_COMPLETE) { - debugging('', DEBUG_DEVELOPER) && mtrace("mode $reengagement->emailuser reengagementid $reengagement->id. - User already marked complete. Deleting inprogress record for user $userid"); - $result = $DB->delete_records('reengagement_inprogress', array('id' => $inprogress->id)); - } else { - debugging('', DEBUG_DEVELOPER) && mtrace("mode $reengagement->emailuser reengagementid $reengagement->id. - Updating inprogress record to indicate email sent for user $userid"); - $updaterecord = new stdClass(); - $updaterecord->id = $inprogress->id; - if ($reengagement->remindercount > $inprogress->emailsent) { - $updaterecord->emailtime = $timenow + $reengagement->emaildelay; - } - $updaterecord->emailsent = $inprogress->emailsent + 1; - $result = $DB->update_record('reengagement_inprogress', $updaterecord); - } - if (!empty($result)) { - debugging('', DEBUG_ALL) && mtrace("Reengagement: sending email to $userid regarding reengagementid - $reengagement->id due to emailduetime."); - reengagement_email_user($reengagement, $inprogress); - } - } - $inprogresses->close(); - - if (debugging('', DEBUG_DEVELOPER) || ($emailduecount && debugging('', DEBUG_ALL))) { - mtrace("Found $emailduecount reengagements due to be emailed."); - } return true; } @@ -757,7 +573,7 @@ function reengagement_get_startusers($reengagement) { $alreadycompletionsql = "SELECT userid FROM {course_modules_completion} WHERE coursemoduleid = :alcmoduleid"; - $params['alcmoduleid'] = $reengagement->id; + $params['alcmoduleid'] = $reengagement->cmid; // Get a list of people who already started this reengagement // (based on reengagement_inprogress records). @@ -1003,3 +819,26 @@ function reengagement_get_coursemodule_info($coursemodule) { return $result; } + +/** + * Prepare custom data and queue it for adhoc task. + * + * @param object $reengagementcm - reengagement course module instance. + * @return bool + */ +function reengagement_queue_adhoc_task($reengagementcm) { + // Create a new ad-hoc task. + $task = new reengagement_adhoc_task(); + + // Setup the task data. + $customdata = new stdClass(); + $customdata->cmid = $reengagementcm->cmid; + $customdata->courseid = $reengagementcm->courseid; + $customdata->duration = $reengagementcm->duration; + $customdata->emaildelay = $reengagementcm->emaildelay; + $customdata->rid = $reengagementcm->rid; + $task->set_custom_data($customdata); + + // Queue the task. + manager::queue_adhoc_task($task, true); +}