From 826dc8d572857822a0945d9f49a508241dbbc8bd Mon Sep 17 00:00:00 2001 From: Mohamed Atia Date: Sun, 5 May 2024 15:38:31 +0300 Subject: [PATCH] Optimise email certificate task. (#531) By reducing database reads/writes and introducing configurable settings for task efficiency. --- classes/task/email_certificate_task.php | 130 +++++++++++++++++++----- db/install.php | 47 +++++++++ db/install.xml | 13 ++- db/upgrade.php | 31 ++++++ lang/en/customcert.php | 8 ++ settings.php | 21 +++- 6 files changed, 220 insertions(+), 30 deletions(-) create mode 100644 db/install.php diff --git a/classes/task/email_certificate_task.php b/classes/task/email_certificate_task.php index 8b672346..d47c9472 100644 --- a/classes/task/email_certificate_task.php +++ b/classes/task/email_certificate_task.php @@ -49,19 +49,48 @@ public function get_name() { public function execute() { global $DB; + // Get the certificatesperrun, includeinnotvisiblecourses, and certificateexecutionperiod configurations. + $certificatesperrun = (int)get_config('customcert', 'certificatesperrun'); + $includeinnotvisiblecourses = (bool)get_config('customcert', 'includeinnotvisiblecourses'); + $certificateexecutionperiod = (int)get_config('customcert', 'certificateexecutionperiod'); + + // Get the last processed batch and total certificates to process. + $taskprogress = $DB->get_record('customcert_email_task_prgrs', ['taskname' => 'email_certificate_task']); + $lastprocessed = $taskprogress->last_processed; + // Get all the certificates that have requested someone get emailed. $emailotherslengthsql = $DB->sql_length('c.emailothers'); $sql = "SELECT c.*, ct.id as templateid, ct.name as templatename, ct.contextid, co.id as courseid, - co.fullname as coursefullname, co.shortname as courseshortname - FROM {customcert} c - JOIN {customcert_templates} ct + co.fullname as coursefullname, co.shortname as courseshortname + FROM {customcert} c + JOIN {customcert_templates} ct ON c.templateid = ct.id - JOIN {course} co - ON c.course = co.id - WHERE (c.emailstudents = :emailstudents - OR c.emailteachers = :emailteachers - OR $emailotherslengthsql >= 3)"; - if (!$customcerts = $DB->get_records_sql($sql, ['emailstudents' => 1, 'emailteachers' => 1])) { + JOIN {course} co + ON c.course = co.id"; + + // Add JOIN with mdl_course_categories to exclude certificates from hidden courses. + $sql .= " JOIN {course_categories} cat ON co.category = cat.id"; + + // Add conditions to exclude certificates from hidden courses. + $sql .= " WHERE (c.emailstudents = :emailstudents + OR c.emailteachers = :emailteachers + OR $emailotherslengthsql >= 3)"; + + // Check the includeinnotvisiblecourses configuration. + if (!$includeinnotvisiblecourses) { + // Exclude certificates from hidden courses. + $sql .= " AND co.visible = 1 AND cat.visible = 1"; + } + + // Add condition based on certificate execution period. + if ($certificateexecutionperiod > 0) { + // Include courses with no end date or end date greater than the specified period. + $sql .= " AND (co.enddate = 0 OR co.enddate > :enddate)"; + $params['enddate'] = time() - $certificateexecutionperiod; + } + + // Execute the SQL query. + if (!$customcerts = $DB->get_records_sql($sql, ['emailstudents' => 1, 'emailteachers' => 1] + $params)) { return; } @@ -69,7 +98,32 @@ public function execute() { $page = new \moodle_page(); $htmlrenderer = $page->get_renderer('mod_customcert', 'email', 'htmlemail'); $textrenderer = $page->get_renderer('mod_customcert', 'email', 'textemail'); - foreach ($customcerts as $customcert) { + + // Store the total count of certificates in the database. + $totalcertificatestoprocess = count($customcerts); + $DB->set_field('customcert_email_task_prgrs', 'total_certificate_to_process', $totalcertificatestoprocess, [ + 'taskname' => 'email_certificate_task', + ]); + + // Check if we need to reset and start from the beginning. + if ($lastprocessed >= count($customcerts)) { + $lastprocessed = 0; // Reset to the beginning. + } + + if ($certificatesperrun <= 0) { + // Process all certificates in a single run. + $certificates = $customcerts; + } else { + // Process certificates in batches, starting from the last processed batch. + $certificates = array_slice($customcerts, $lastprocessed, $certificatesperrun); + } + + foreach ($certificates as $customcert) { + // Check if the certificate is hidden, quit early. + $fastmoduleinfo = get_fast_modinfo($customcert->courseid)->instances['customcert'][$customcert->id]; + if (!$fastmoduleinfo->visible) { + continue; + } // Do not process an empty certificate. $sql = "SELECT ce.* FROM {customcert_elements} ce @@ -111,27 +165,33 @@ public function execute() { WHERE ci.customcertid = :customcertid"; $issuedusers = $DB->get_records_sql($sql, ['customcertid' => $customcert->id]); - // Now, get a list of users who can access the certificate but have not yet. - $enrolledusers = get_enrolled_users(\context_course::instance($customcert->courseid), 'mod/customcert:view'); - foreach ($enrolledusers as $enroluser) { - // Check if the user has already been issued. - if (in_array($enroluser->id, array_keys((array) $issuedusers))) { - continue; - } + // Now, get a list of users who can Manage the certificate. + $userswithmanage = get_users_by_capability($context, 'mod/customcert:manage', 'u.id'); - // Now check if the certificate is not visible to the current user. - $cm = get_fast_modinfo($customcert->courseid, $enroluser->id)->instances['customcert'][$customcert->id]; - if (!$cm->uservisible) { - continue; - } + // Get the context of the Custom Certificate module. + $cm = get_coursemodule_from_instance('customcert', $customcert->id, $customcert->course); + $context = \context_module::instance($cm->id); - // Don't want to email those with the capability to manage the certificate. - if (has_capability('mod/customcert:manage', $context, $enroluser->id)) { + // Now, get a list of users who can view and issue the certificate but have not yet. + // Get users with the mod/customcert:receiveissue capability in the Custom Certificate module context. + $userswithissue = get_users_by_capability($context, 'mod/customcert:receiveissue'); + // Get users with mod/customcert:view capability. + $userswithview = get_users_by_capability($context, 'mod/customcert:view'); + // Users with both mod/customcert:view and mod/customcert:receiveissue cabapilities. + $userswithissueview = array_intersect_key($userswithissue, $userswithview); + + $infomodule = new \core_availability\info_module($fastmoduleinfo); + // Filter who can't access due to availability restriction, from the full list. + $userscanissue = $infomodule->filter_user_list($userswithissueview); + + foreach ($userscanissue as $enroluser) { + // Check if the user has already been issued. + if (in_array($enroluser->id, array_keys((array)$issuedusers))) { continue; } - // Only email those with the capability to receive the certificate. - if (!has_capability('mod/customcert:receiveissue', $context, $enroluser->id)) { + // Don't want to email those with the capability to manage the certificate. + if (in_array($enroluser->id, array_keys((array)$userswithmanage))) { continue; } @@ -164,7 +224,7 @@ public function execute() { } } - // If there are no users to email we can return early. + // If there are no users to email, we can return early. if (!$issuedusers) { continue; } @@ -175,6 +235,7 @@ public function execute() { return; } + $issueids = []; // Now, email the people we need to. foreach ($issuedusers as $user) { // Set up the user. @@ -248,8 +309,21 @@ public function execute() { } // Set the field so that it is emailed. - $DB->set_field('customcert_issues', 'emailed', 1, ['id' => $user->issueid]); + $issueids[] = $user->issueid; } + + if (!empty($issueids)) { + list($sql, $params) = $DB->get_in_or_equal($issueids, SQL_PARAMS_NAMED, 'id'); + $DB->set_field_select('customcert_issues', 'emailed', 1, 'id ' . $sql, $params); + } + } + + // Update the last processed position, if run in batches. + if ($certificatesperrun > 0) { + $newlastprocessed = $lastprocessed + count($certificates); + $DB->set_field('customcert_email_task_prgrs', 'last_processed', $newlastprocessed, [ + 'taskname' => 'email_certificate_task', + ]); } } } diff --git a/db/install.php b/db/install.php new file mode 100644 index 00000000..c78723fc --- /dev/null +++ b/db/install.php @@ -0,0 +1,47 @@ +. + +/** + * Customcert module upgrade code. + * + * @package mod_customcert + * @copyright 2024 Mohamed Atia + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +/** + * Customcert module upgrade code. + * + * @param int $oldversion the version we are upgrading from + * @return bool always true + */ + +/** + * Custom code to be run on installing the plugin. + */ +function xmldb_customcert_install() { + global $DB; + + // Add a default row to the customcert_email_task_prgrs table. + $defaultdata = new stdClass(); + $defaultdata->taskname = 'email_certificate_task'; + $defaultdata->last_processed = 0; + $defaultdata->total_certificate_to_process = 0; + + // Insert the default data into the table. + $DB->insert_record('customcert_email_task_prgrs', $defaultdata); + return true; +} diff --git a/db/install.xml b/db/install.xml index f3863464..19d4439b 100644 --- a/db/install.xml +++ b/db/install.xml @@ -1,5 +1,5 @@ - @@ -100,5 +100,16 @@ + + + + + + + + + + +
diff --git a/db/upgrade.php b/db/upgrade.php index 3453e437..4440093d 100644 --- a/db/upgrade.php +++ b/db/upgrade.php @@ -235,5 +235,36 @@ function xmldb_customcert_upgrade($oldversion) { upgrade_mod_savepoint(true, 2023042405, 'customcert'); } + if ($oldversion < 2023042409) { + + // Define table customcert_email_task_prgrs to be created. + $table = new xmldb_table('customcert_email_task_prgrs'); + + // Adding fields to table customcert_email_task_prgrs. + $table->add_field('id', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, XMLDB_SEQUENCE, null); + $table->add_field('taskname', XMLDB_TYPE_CHAR, '255', null, XMLDB_NOTNULL, null, 'email_certificate_task'); + $table->add_field('last_processed', XMLDB_TYPE_INTEGER, '20', null, XMLDB_NOTNULL, null, '0'); + $table->add_field('total_certificate_to_process', XMLDB_TYPE_INTEGER, '20', null, XMLDB_NOTNULL, null, '0'); + + // Adding keys to table customcert_email_task_prgrs. + $table->add_key('primary', XMLDB_KEY_PRIMARY, ['id']); + + // Conditionally launch create table for customcert_email_task_prgrs. + if (!$dbman->table_exists($table)) { + $dbman->create_table($table); + + // Add a default row to the customcert_email_task_prgrs table. + $defaultdata = new stdClass(); + $defaultdata->taskname = 'email_certificate_task'; + $defaultdata->last_processed = 0; + $defaultdata->total_certificate_to_process = 0; + + // Insert the default data into the table. + $DB->insert_record('customcert_email_task_prgrs', $defaultdata); + } + + // Customcert savepoint reached. + upgrade_mod_savepoint(true, 2023042409, 'customcert'); + } return true; } diff --git a/lang/en/customcert.php b/lang/en/customcert.php index dcd218c4..9cd1686a 100644 --- a/lang/en/customcert.php +++ b/lang/en/customcert.php @@ -33,6 +33,10 @@ $string['awardedto'] = 'Awarded to'; $string['cannotverifyallcertificates'] = 'You do not have the permission to verify all certificates on the site.'; $string['certificate'] = 'Certificate'; +$string['certificateexecutionperiod'] = 'Certificate execution period'; +$string['certificateexecutionperiod_desc'] = 'Specify the period for which certificates should be executed based on their end date. Set to 0 to execute all certificates, regardless of their age.'; +$string['certificatesperrun'] = 'Certificates per run'; +$string['certificatesperrun_desc'] = 'Enter the number of certificates to process per scheduled task run where 0 means it will process all certificates.'; $string['code'] = 'Code'; $string['copy'] = 'Copy'; $string['coursetimereq'] = 'Required minutes in course'; @@ -122,6 +126,8 @@ $string['gradeoutcome'] = 'Outcome'; $string['height'] = 'Height'; $string['height_help'] = 'This is the height of the certificate PDF in mm. For reference an A4 piece of paper is 297mm high and a letter is 279mm high.'; +$string['includeinnotvisiblecourses'] = 'Include certificates in hidden courses'; +$string['includeinnotvisiblecourses_desc'] = 'Check this box to include certificates in courses that are not visible to the user.'; $string['invalidcode'] = 'Invalid code supplied.'; $string['invalidcolour'] = 'Invalid colour chosen, please enter a valid HTML colour name, or a six-digit, or three-digit hexadecimal colour.'; $string['invalidelementwidthorheightnotnumber'] = 'Please enter a valid number.'; @@ -192,6 +198,8 @@ $string['saveandcontinue'] = 'Save and continue'; $string['savechangespreview'] = 'Save changes and preview'; $string['savetemplate'] = 'Save template'; +$string['scheduledtaskconfigdesc'] = 'Configure the settings for the scheduled task that processes certificates.'; +$string['scheduledtaskconfigheading'] = 'Scheduled task configuration'; $string['search:activity'] = 'Custom certificate - activity information'; $string['setprotection'] = 'Set protection'; $string['setprotection_help'] = 'Choose the actions you wish to prevent users from performing on this certificate.'; diff --git a/settings.php b/settings.php index 5b291566..a7fd5266 100644 --- a/settings.php +++ b/settings.php @@ -58,6 +58,26 @@ new moodle_url('/mod/customcert/download_all_certificates.php'), '')); } +$settings->add(new admin_setting_heading('scheduledtaskconfig', + get_string('scheduledtaskconfigheading', 'customcert'), + get_string('scheduledtaskconfigdesc', 'customcert'))); + +$settings->add(new admin_setting_configtext('customcert/certificatesperrun', + get_string('certificatesperrun', 'customcert'), + get_string('certificatesperrun_desc', 'customcert'), + 0, PARAM_INT)); +$settings->add(new admin_setting_configcheckbox('customcert/includeinnotvisiblecourses', + get_string('includeinnotvisiblecourses', 'customcert'), + get_string('includeinnotvisiblecourses_desc', 'customcert'), 0)); +$settings->add( + new admin_setting_configduration( + 'customcert/certificateexecutionperiod', + new \lang_string('certificateexecutionperiod', 'customcert'), + new \lang_string('certificateexecutionperiod_desc', 'customcert'), + 365 * DAYSECS + ) + ); + $settings->add(new admin_setting_heading('defaults', get_string('modeditdefaults', 'admin'), get_string('condifmodeditdefaults', 'admin'))); @@ -65,7 +85,6 @@ 0 => get_string('no'), 1 => get_string('yes'), ]; - $settings->add(new admin_setting_configselect('customcert/emailstudents', get_string('emailstudents', 'customcert'), get_string('emailstudents_help', 'customcert'), 0, $yesnooptions)); $settings->add(new admin_setting_configselect('customcert/emailteachers',