Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improvement to the speed of the emailing certificate task (#531) #610

Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
130 changes: 102 additions & 28 deletions classes/task/email_certificate_task.php
Original file line number Diff line number Diff line change
Expand Up @@ -49,27 +49,81 @@ 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;
}

// The renderers used for sending emails.
$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
Expand Down Expand Up @@ -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;
}

Expand Down Expand Up @@ -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;
}
Expand All @@ -175,6 +235,7 @@ public function execute() {
return;
}

$issueids = [];
// Now, email the people we need to.
foreach ($issuedusers as $user) {
// Set up the user.
Expand Down Expand Up @@ -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)) {
mdjnelson marked this conversation as resolved.
Show resolved Hide resolved
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.
mdjnelson marked this conversation as resolved.
Show resolved Hide resolved
if ($certificatesperrun > 0) {
$newlastprocessed = $lastprocessed + count($certificates);
$DB->set_field('customcert_email_task_prgrs', 'last_processed', $newlastprocessed, [
'taskname' => 'email_certificate_task',
]);
}
}
}
47 changes: 47 additions & 0 deletions db/install.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
<?php
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this file really needed? I am reluctant to add a new file. Could you simply not check if there were no rows then assume the variables are 0? I would prefer not adding more code than is needed, especially a whole new file that manipulates the database.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

While I agree that minimizing code is ideal, directly setting default values within the table structure during installation is not possible in Moodle.

I've implemented default values in the upgrade process.

However, for fresh installations, the table will be created without initial data. Are there alternative approaches to consider?

// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.

/**
* Customcert module upgrade code.
*
* @package mod_customcert
* @copyright 2024 Mohamed Atia <matia12[@gmail.com>
* @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;
}
13 changes: 12 additions & 1 deletion db/install.xml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<?xml version="1.0" encoding="UTF-8" ?>
<XMLDB PATH="mod/customcert/db" VERSION="20220613" COMMENT="XMLDB file for Moodle mod/customcert"
<XMLDB PATH="mod/customcert/db" VERSION="20240313" COMMENT="XMLDB file for Moodle mod/customcert"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="../../../lib/xmldb/xmldb.xsd"
>
Expand Down Expand Up @@ -100,5 +100,16 @@
<KEY NAME="page" TYPE="foreign" FIELDS="pageid" REFTABLE="customcert_pages" REFFIELDS="id"/>
</KEYS>
</TABLE>
<TABLE NAME="customcert_email_task_prgrs" COMMENT="to track email task progress">
<FIELDS>
<FIELD NAME="id" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="true"/>
<FIELD NAME="taskname" TYPE="char" LENGTH="255" NOTNULL="true" DEFAULT="email_certificate_task" SEQUENCE="false"/>
<FIELD NAME="last_processed" TYPE="int" LENGTH="20" NOTNULL="true" DEFAULT="0" SEQUENCE="false"/>
<FIELD NAME="total_certificate_to_process" TYPE="int" LENGTH="20" NOTNULL="true" DEFAULT="0" SEQUENCE="false" COMMENT="to store the total count of certificates that should be processed"/>
</FIELDS>
<KEYS>
<KEY NAME="primary" TYPE="primary" FIELDS="id"/>
</KEYS>
</TABLE>
</TABLES>
</XMLDB>
31 changes: 31 additions & 0 deletions db/upgrade.php
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

New line here please.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

upgrade_mod_savepoint(true, 2023042409, 'customcert');
}
return true;
}
8 changes: 8 additions & 0 deletions lang/en/customcert.php
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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.';
Expand Down Expand Up @@ -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.';
Expand Down
21 changes: 20 additions & 1 deletion settings.php
Original file line number Diff line number Diff line change
Expand Up @@ -58,14 +58,33 @@
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',
mdjnelson marked this conversation as resolved.
Show resolved Hide resolved
get_string('includeinnotvisiblecourses', 'customcert'),
get_string('includeinnotvisiblecourses_desc', 'customcert'), 0));
$settings->add(
mdjnelson marked this conversation as resolved.
Show resolved Hide resolved
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')));

$yesnooptions = [
0 => get_string('no'),
1 => get_string('yes'),
];

mdjnelson marked this conversation as resolved.
Show resolved Hide resolved
$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',
Expand Down
Loading