Skip to content

Commit 4381dea

Browse files
mohamedmohamedatiamdjnelson
authored andcommitted
Optimise email certificate task. (#531)
By reducing database reads/writes and introducing configurable settings for task efficiency.
1 parent be2ffac commit 4381dea

File tree

6 files changed

+220
-30
lines changed

6 files changed

+220
-30
lines changed

classes/task/email_certificate_task.php

Lines changed: 102 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -49,27 +49,81 @@ public function get_name() {
4949
public function execute() {
5050
global $DB;
5151

52+
// Get the certificatesperrun, includeinnotvisiblecourses, and certificateexecutionperiod configurations.
53+
$certificatesperrun = (int)get_config('customcert', 'certificatesperrun');
54+
$includeinnotvisiblecourses = (bool)get_config('customcert', 'includeinnotvisiblecourses');
55+
$certificateexecutionperiod = (int)get_config('customcert', 'certificateexecutionperiod');
56+
57+
// Get the last processed batch and total certificates to process.
58+
$taskprogress = $DB->get_record('customcert_email_task_prgrs', ['taskname' => 'email_certificate_task']);
59+
$lastprocessed = $taskprogress->last_processed;
60+
5261
// Get all the certificates that have requested someone get emailed.
5362
$emailotherslengthsql = $DB->sql_length('c.emailothers');
5463
$sql = "SELECT c.*, ct.id as templateid, ct.name as templatename, ct.contextid, co.id as courseid,
55-
co.fullname as coursefullname, co.shortname as courseshortname
56-
FROM {customcert} c
57-
JOIN {customcert_templates} ct
64+
co.fullname as coursefullname, co.shortname as courseshortname
65+
FROM {customcert} c
66+
JOIN {customcert_templates} ct
5867
ON c.templateid = ct.id
59-
JOIN {course} co
60-
ON c.course = co.id
61-
WHERE (c.emailstudents = :emailstudents
62-
OR c.emailteachers = :emailteachers
63-
OR $emailotherslengthsql >= 3)";
64-
if (!$customcerts = $DB->get_records_sql($sql, ['emailstudents' => 1, 'emailteachers' => 1])) {
68+
JOIN {course} co
69+
ON c.course = co.id";
70+
71+
// Add JOIN with mdl_course_categories to exclude certificates from hidden courses.
72+
$sql .= " JOIN {course_categories} cat ON co.category = cat.id";
73+
74+
// Add conditions to exclude certificates from hidden courses.
75+
$sql .= " WHERE (c.emailstudents = :emailstudents
76+
OR c.emailteachers = :emailteachers
77+
OR $emailotherslengthsql >= 3)";
78+
79+
// Check the includeinnotvisiblecourses configuration.
80+
if (!$includeinnotvisiblecourses) {
81+
// Exclude certificates from hidden courses.
82+
$sql .= " AND co.visible = 1 AND cat.visible = 1";
83+
}
84+
85+
// Add condition based on certificate execution period.
86+
if ($certificateexecutionperiod > 0) {
87+
// Include courses with no end date or end date greater than the specified period.
88+
$sql .= " AND (co.enddate = 0 OR co.enddate > :enddate)";
89+
$params['enddate'] = time() - $certificateexecutionperiod;
90+
}
91+
92+
// Execute the SQL query.
93+
if (!$customcerts = $DB->get_records_sql($sql, ['emailstudents' => 1, 'emailteachers' => 1] + $params)) {
6594
return;
6695
}
6796

6897
// The renderers used for sending emails.
6998
$page = new \moodle_page();
7099
$htmlrenderer = $page->get_renderer('mod_customcert', 'email', 'htmlemail');
71100
$textrenderer = $page->get_renderer('mod_customcert', 'email', 'textemail');
72-
foreach ($customcerts as $customcert) {
101+
102+
// Store the total count of certificates in the database.
103+
$totalcertificatestoprocess = count($customcerts);
104+
$DB->set_field('customcert_email_task_prgrs', 'total_certificate_to_process', $totalcertificatestoprocess, [
105+
'taskname' => 'email_certificate_task',
106+
]);
107+
108+
// Check if we need to reset and start from the beginning.
109+
if ($lastprocessed >= count($customcerts)) {
110+
$lastprocessed = 0; // Reset to the beginning.
111+
}
112+
113+
if ($certificatesperrun <= 0) {
114+
// Process all certificates in a single run.
115+
$certificates = $customcerts;
116+
} else {
117+
// Process certificates in batches, starting from the last processed batch.
118+
$certificates = array_slice($customcerts, $lastprocessed, $certificatesperrun);
119+
}
120+
121+
foreach ($certificates as $customcert) {
122+
// Check if the certificate is hidden, quit early.
123+
$fastmoduleinfo = get_fast_modinfo($customcert->courseid)->instances['customcert'][$customcert->id];
124+
if (!$fastmoduleinfo->visible) {
125+
continue;
126+
}
73127
// Do not process an empty certificate.
74128
$sql = "SELECT ce.*
75129
FROM {customcert_elements} ce
@@ -111,27 +165,33 @@ public function execute() {
111165
WHERE ci.customcertid = :customcertid";
112166
$issuedusers = $DB->get_records_sql($sql, ['customcertid' => $customcert->id]);
113167

114-
// Now, get a list of users who can access the certificate but have not yet.
115-
$enrolledusers = get_enrolled_users(\context_course::instance($customcert->courseid), 'mod/customcert:view');
116-
foreach ($enrolledusers as $enroluser) {
117-
// Check if the user has already been issued.
118-
if (in_array($enroluser->id, array_keys((array) $issuedusers))) {
119-
continue;
120-
}
168+
// Now, get a list of users who can Manage the certificate.
169+
$userswithmanage = get_users_by_capability($context, 'mod/customcert:manage', 'u.id');
121170

122-
// Now check if the certificate is not visible to the current user.
123-
$cm = get_fast_modinfo($customcert->courseid, $enroluser->id)->instances['customcert'][$customcert->id];
124-
if (!$cm->uservisible) {
125-
continue;
126-
}
171+
// Get the context of the Custom Certificate module.
172+
$cm = get_coursemodule_from_instance('customcert', $customcert->id, $customcert->course);
173+
$context = \context_module::instance($cm->id);
127174

128-
// Don't want to email those with the capability to manage the certificate.
129-
if (has_capability('mod/customcert:manage', $context, $enroluser->id)) {
175+
// Now, get a list of users who can view and issue the certificate but have not yet.
176+
// Get users with the mod/customcert:receiveissue capability in the Custom Certificate module context.
177+
$userswithissue = get_users_by_capability($context, 'mod/customcert:receiveissue');
178+
// Get users with mod/customcert:view capability.
179+
$userswithview = get_users_by_capability($context, 'mod/customcert:view');
180+
// Users with both mod/customcert:view and mod/customcert:receiveissue cabapilities.
181+
$userswithissueview = array_intersect_key($userswithissue, $userswithview);
182+
183+
$infomodule = new \core_availability\info_module($fastmoduleinfo);
184+
// Filter who can't access due to availability restriction, from the full list.
185+
$userscanissue = $infomodule->filter_user_list($userswithissueview);
186+
187+
foreach ($userscanissue as $enroluser) {
188+
// Check if the user has already been issued.
189+
if (in_array($enroluser->id, array_keys((array)$issuedusers))) {
130190
continue;
131191
}
132192

133-
// Only email those with the capability to receive the certificate.
134-
if (!has_capability('mod/customcert:receiveissue', $context, $enroluser->id)) {
193+
// Don't want to email those with the capability to manage the certificate.
194+
if (in_array($enroluser->id, array_keys((array)$userswithmanage))) {
135195
continue;
136196
}
137197

@@ -164,7 +224,7 @@ public function execute() {
164224
}
165225
}
166226

167-
// If there are no users to email we can return early.
227+
// If there are no users to email, we can return early.
168228
if (!$issuedusers) {
169229
continue;
170230
}
@@ -175,6 +235,7 @@ public function execute() {
175235
return;
176236
}
177237

238+
$issueids = [];
178239
// Now, email the people we need to.
179240
foreach ($issuedusers as $user) {
180241
// Set up the user.
@@ -248,8 +309,21 @@ public function execute() {
248309
}
249310

250311
// Set the field so that it is emailed.
251-
$DB->set_field('customcert_issues', 'emailed', 1, ['id' => $user->issueid]);
312+
$issueids[] = $user->issueid;
252313
}
314+
315+
if (!empty($issueids)) {
316+
list($sql, $params) = $DB->get_in_or_equal($issueids, SQL_PARAMS_NAMED, 'id');
317+
$DB->set_field_select('customcert_issues', 'emailed', 1, 'id ' . $sql, $params);
318+
}
319+
}
320+
321+
// Update the last processed position, if run in batches.
322+
if ($certificatesperrun > 0) {
323+
$newlastprocessed = $lastprocessed + count($certificates);
324+
$DB->set_field('customcert_email_task_prgrs', 'last_processed', $newlastprocessed, [
325+
'taskname' => 'email_certificate_task',
326+
]);
253327
}
254328
}
255329
}

db/install.php

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
<?php
2+
// This file is part of Moodle - http://moodle.org/
3+
//
4+
// Moodle is free software: you can redistribute it and/or modify
5+
// it under the terms of the GNU General Public License as published by
6+
// the Free Software Foundation, either version 3 of the License, or
7+
// (at your option) any later version.
8+
//
9+
// Moodle is distributed in the hope that it will be useful,
10+
// but WITHOUT ANY WARRANTY; without even the implied warranty of
11+
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12+
// GNU General Public License for more details.
13+
//
14+
// You should have received a copy of the GNU General Public License
15+
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
16+
17+
/**
18+
* Customcert module upgrade code.
19+
*
20+
* @package mod_customcert
21+
* @copyright 2024 Mohamed Atia <matia12[@gmail.com>
22+
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
23+
*/
24+
25+
/**
26+
* Customcert module upgrade code.
27+
*
28+
* @param int $oldversion the version we are upgrading from
29+
* @return bool always true
30+
*/
31+
32+
/**
33+
* Custom code to be run on installing the plugin.
34+
*/
35+
function xmldb_customcert_install() {
36+
global $DB;
37+
38+
// Add a default row to the customcert_email_task_prgrs table.
39+
$defaultdata = new stdClass();
40+
$defaultdata->taskname = 'email_certificate_task';
41+
$defaultdata->last_processed = 0;
42+
$defaultdata->total_certificate_to_process = 0;
43+
44+
// Insert the default data into the table.
45+
$DB->insert_record('customcert_email_task_prgrs', $defaultdata);
46+
return true;
47+
}

db/install.xml

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
<?xml version="1.0" encoding="UTF-8" ?>
2-
<XMLDB PATH="mod/customcert/db" VERSION="20220613" COMMENT="XMLDB file for Moodle mod/customcert"
2+
<XMLDB PATH="mod/customcert/db" VERSION="20240313" COMMENT="XMLDB file for Moodle mod/customcert"
33
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
44
xsi:noNamespaceSchemaLocation="../../../lib/xmldb/xmldb.xsd"
55
>
@@ -100,5 +100,16 @@
100100
<KEY NAME="page" TYPE="foreign" FIELDS="pageid" REFTABLE="customcert_pages" REFFIELDS="id"/>
101101
</KEYS>
102102
</TABLE>
103+
<TABLE NAME="customcert_email_task_prgrs" COMMENT="to track email task progress">
104+
<FIELDS>
105+
<FIELD NAME="id" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="true"/>
106+
<FIELD NAME="taskname" TYPE="char" LENGTH="255" NOTNULL="true" DEFAULT="email_certificate_task" SEQUENCE="false"/>
107+
<FIELD NAME="last_processed" TYPE="int" LENGTH="20" NOTNULL="true" DEFAULT="0" SEQUENCE="false"/>
108+
<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"/>
109+
</FIELDS>
110+
<KEYS>
111+
<KEY NAME="primary" TYPE="primary" FIELDS="id"/>
112+
</KEYS>
113+
</TABLE>
103114
</TABLES>
104115
</XMLDB>

db/upgrade.php

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -235,5 +235,36 @@ function xmldb_customcert_upgrade($oldversion) {
235235
upgrade_mod_savepoint(true, 2022112804, 'customcert');
236236
}
237237

238+
if ($oldversion < 2023042409) {
239+
240+
// Define table customcert_email_task_prgrs to be created.
241+
$table = new xmldb_table('customcert_email_task_prgrs');
242+
243+
// Adding fields to table customcert_email_task_prgrs.
244+
$table->add_field('id', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, XMLDB_SEQUENCE, null);
245+
$table->add_field('taskname', XMLDB_TYPE_CHAR, '255', null, XMLDB_NOTNULL, null, 'email_certificate_task');
246+
$table->add_field('last_processed', XMLDB_TYPE_INTEGER, '20', null, XMLDB_NOTNULL, null, '0');
247+
$table->add_field('total_certificate_to_process', XMLDB_TYPE_INTEGER, '20', null, XMLDB_NOTNULL, null, '0');
248+
249+
// Adding keys to table customcert_email_task_prgrs.
250+
$table->add_key('primary', XMLDB_KEY_PRIMARY, ['id']);
251+
252+
// Conditionally launch create table for customcert_email_task_prgrs.
253+
if (!$dbman->table_exists($table)) {
254+
$dbman->create_table($table);
255+
256+
// Add a default row to the customcert_email_task_prgrs table.
257+
$defaultdata = new stdClass();
258+
$defaultdata->taskname = 'email_certificate_task';
259+
$defaultdata->last_processed = 0;
260+
$defaultdata->total_certificate_to_process = 0;
261+
262+
// Insert the default data into the table.
263+
$DB->insert_record('customcert_email_task_prgrs', $defaultdata);
264+
}
265+
266+
// Customcert savepoint reached.
267+
upgrade_mod_savepoint(true, 2023042409, 'customcert');
268+
}
238269
return true;
239270
}

lang/en/customcert.php

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,10 @@
3333
$string['awardedto'] = 'Awarded to';
3434
$string['cannotverifyallcertificates'] = 'You do not have the permission to verify all certificates on the site.';
3535
$string['certificate'] = 'Certificate';
36+
$string['certificateexecutionperiod'] = 'Certificate execution period';
37+
$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.';
38+
$string['certificatesperrun'] = 'Certificates per run';
39+
$string['certificatesperrun_desc'] = 'Enter the number of certificates to process per scheduled task run where 0 means it will process all certificates.';
3640
$string['code'] = 'Code';
3741
$string['copy'] = 'Copy';
3842
$string['coursetimereq'] = 'Required minutes in course';
@@ -122,6 +126,8 @@
122126
$string['gradeoutcome'] = 'Outcome';
123127
$string['height'] = 'Height';
124128
$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.';
129+
$string['includeinnotvisiblecourses'] = 'Include certificates in hidden courses';
130+
$string['includeinnotvisiblecourses_desc'] = 'Check this box to include certificates in courses that are not visible to the user.';
125131
$string['invalidcode'] = 'Invalid code supplied.';
126132
$string['invalidcolour'] = 'Invalid colour chosen, please enter a valid HTML colour name, or a six-digit, or three-digit hexadecimal colour.';
127133
$string['invalidelementwidthorheightnotnumber'] = 'Please enter a valid number.';
@@ -192,6 +198,8 @@
192198
$string['saveandcontinue'] = 'Save and continue';
193199
$string['savechangespreview'] = 'Save changes and preview';
194200
$string['savetemplate'] = 'Save template';
201+
$string['scheduledtaskconfigdesc'] = 'Configure the settings for the scheduled task that processes certificates.';
202+
$string['scheduledtaskconfigheading'] = 'Scheduled task configuration';
195203
$string['search:activity'] = 'Custom certificate - activity information';
196204
$string['setprotection'] = 'Set protection';
197205
$string['setprotection_help'] = 'Choose the actions you wish to prevent users from performing on this certificate.';

settings.php

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,14 +58,33 @@
5858
new moodle_url('/mod/customcert/download_all_certificates.php'), ''));
5959
}
6060

61+
$settings->add(new admin_setting_heading('scheduledtaskconfig',
62+
get_string('scheduledtaskconfigheading', 'customcert'),
63+
get_string('scheduledtaskconfigdesc', 'customcert')));
64+
65+
$settings->add(new admin_setting_configtext('customcert/certificatesperrun',
66+
get_string('certificatesperrun', 'customcert'),
67+
get_string('certificatesperrun_desc', 'customcert'),
68+
0, PARAM_INT));
69+
$settings->add(new admin_setting_configcheckbox('customcert/includeinnotvisiblecourses',
70+
get_string('includeinnotvisiblecourses', 'customcert'),
71+
get_string('includeinnotvisiblecourses_desc', 'customcert'), 0));
72+
$settings->add(
73+
new admin_setting_configduration(
74+
'customcert/certificateexecutionperiod',
75+
new \lang_string('certificateexecutionperiod', 'customcert'),
76+
new \lang_string('certificateexecutionperiod_desc', 'customcert'),
77+
365 * DAYSECS
78+
)
79+
);
80+
6181
$settings->add(new admin_setting_heading('defaults',
6282
get_string('modeditdefaults', 'admin'), get_string('condifmodeditdefaults', 'admin')));
6383

6484
$yesnooptions = [
6585
0 => get_string('no'),
6686
1 => get_string('yes'),
6787
];
68-
6988
$settings->add(new admin_setting_configselect('customcert/emailstudents',
7089
get_string('emailstudents', 'customcert'), get_string('emailstudents_help', 'customcert'), 0, $yesnooptions));
7190
$settings->add(new admin_setting_configselect('customcert/emailteachers',

0 commit comments

Comments
 (0)