Skip to content

Commit cab71d5

Browse files
Optimize email certificate task by reducing database reads/writes
and introduce configurable settings for task efficiency.
1 parent baaa1a0 commit cab71d5

File tree

7 files changed

+222
-31
lines changed

7 files changed

+222
-31
lines changed

classes/task/email_certificate_task.php

Lines changed: 94 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_task_progress', ['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_task_progress', '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,28 @@ 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', '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 Issue the certificate but have not yet.
176+
// Get users with the specified capability in the Custom Certificate module context.
177+
$userwithissue = get_users_by_capability($context, 'mod/customcert:receiveissue', 'id, firstname, lastname, email');
178+
$infomodule = new \core_availability\info_module($fastmoduleinfo);
179+
// Filter who can't access due to availability restriction, from the full list.
180+
$userscanissue = $infomodule->filter_user_list($userwithissue);
181+
182+
foreach ($userscanissue as $enroluser) {
183+
// Check if the user has already been issued.
184+
if (in_array($enroluser->id, array_keys((array)$issuedusers))) {
130185
continue;
131186
}
132187

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

@@ -164,7 +219,7 @@ public function execute() {
164219
}
165220
}
166221

167-
// If there are no users to email we can return early.
222+
// If there are no users to email, we can return early.
168223
if (!$issuedusers) {
169224
continue;
170225
}
@@ -175,6 +230,7 @@ public function execute() {
175230
return;
176231
}
177232

233+
$issueids = [];
178234
// Now, email the people we need to.
179235
foreach ($issuedusers as $user) {
180236
// Set up the user.
@@ -248,8 +304,18 @@ public function execute() {
248304
}
249305

250306
// Set the field so that it is emailed.
251-
$DB->set_field('customcert_issues', 'emailed', 1, ['id' => $user->issueid]);
307+
$issueids[] = $user->issueid;
308+
}
309+
if (!empty($issueids)) {
310+
$DB->set_field_select('customcert_issues', 'emailed', 1, 'id IN (' . implode(',', $issueids) . ')');
252311
}
253312
}
313+
// Update the last processed position, if run in batches.
314+
if ($certificatesperrun > 0) {
315+
$newlastprocessed = $lastprocessed + count($certificates);
316+
$DB->set_field('customcert_task_progress', 'last_processed', $newlastprocessed, [
317+
'taskname' => 'email_certificate_task',
318+
]);
319+
}
254320
}
255321
}

db/install.php

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
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 2016 Mark Nelson <[email protected]>
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+
$dbman = $DB->get_manager();
39+
40+
// Add a default row to the customcert_task_progress table.
41+
$defaultdata = new stdClass();
42+
$defaultdata->taskname = 'email_certificate_task';
43+
$defaultdata->last_processed = 0;
44+
$defaultdata->total_certificate_to_process = 0;
45+
46+
// Write close to ensure the transaction is committed.
47+
\core\session\manager::write_close();
48+
49+
// Insert the default data into the table.
50+
$DB->insert_record('customcert_task_progress', $defaultdata);
51+
return true;
52+
}

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_task_progress" 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: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -235,5 +235,38 @@ function xmldb_customcert_upgrade($oldversion) {
235235
upgrade_mod_savepoint(true, 2023042405, 'customcert');
236236
}
237237

238+
if ($oldversion < 2023042408) {
239+
240+
// Define table customcert_task_progress to be created.
241+
$table = new xmldb_table('customcert_task_progress');
242+
243+
// Adding fields to table customcert_task_progress.
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_task_progress.
250+
$table->add_key('primary', XMLDB_KEY_PRIMARY, ['id']);
251+
$table->add_key('usermodified', XMLDB_KEY_FOREIGN, ['usermodified'], 'user', ['id']);
252+
253+
// Conditionally launch create table for customcert_task_progress.
254+
if (!$dbman->table_exists($table)) {
255+
$dbman->create_table($table);
256+
// Add a default row to the customcert_task_progress 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+
// Write close to ensure the transaction is committed.
263+
\core\session\manager::write_close();
264+
265+
// Insert the default data into the table.
266+
$DB->insert_record('customcert_task_progress', $defaultdata);
267+
}
268+
// Customcert savepoint reached.
269+
upgrade_mod_savepoint(true, 2023042408, 'customcert');
270+
}
238271
return true;
239272
}

lang/en/customcert.php

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -228,3 +228,12 @@
228228

229229
// Acess API.
230230
$string['customcert:managelanguages'] = 'Manage language on edit form';
231+
232+
$string['certificatesperrun'] = 'Certificates per run';
233+
$string['certificatesperrun_desc'] = 'Enter the number of certificates to process per scheduled task run <b>where 0 means it will process all certificates</b>.';
234+
$string['includeinnotvisiblecourses'] = 'Include certificates in hidden courses';
235+
$string['includeinnotvisiblecourses_desc'] = 'Check this box to include certificates in courses that are not visible to the user.';
236+
$string['certificateexecutionperiod'] = 'Certificate execution period';
237+
$string['certificateexecutionperiod_desc'] = 'Specify the period for which certificates should be executed based on their end date. <b>Set to 0 to execute all certificates, regardless of their age.</b>';
238+
$string['scheduledtaskconfigheading'] = 'Scheduled task configuration';
239+
$string['scheduledtaskconfigdesc'] = 'Configure the settings for the scheduled task that processes certificates.';

settings.php

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

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

6485
$yesnooptions = [
6586
0 => get_string('no'),
6687
1 => get_string('yes'),
6788
];
68-
6989
$settings->add(new admin_setting_configselect('customcert/emailstudents',
7090
get_string('emailstudents', 'customcert'), get_string('emailstudents_help', 'customcert'), 0, $yesnooptions));
7191
$settings->add(new admin_setting_configselect('customcert/emailteachers',

version.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@
2424

2525
defined('MOODLE_INTERNAL') || die('Direct access to this script is forbidden.');
2626

27-
$plugin->version = 2023042407; // The current module version (Date: YYYYMMDDXX).
27+
$plugin->version = 2023042408; // The current module version (Date: YYYYMMDDXX).
2828
$plugin->requires = 2023042400; // Requires this Moodle version (4.2).
2929
$plugin->cron = 0; // Period for cron to check this module (secs).
3030
$plugin->component = 'mod_customcert';

0 commit comments

Comments
 (0)