Skip to content

Commit 10aa843

Browse files
Optimize email certificate task by reducing database reads/writes
and introduce configurable settings for task efficiency. Bumped version Fixed date in CHANGES.md Fixed local_codechecker errors Fixed copyright (#205) Added missing lang string (#615) Resolve PHPUnit test failures
1 parent baaa1a0 commit 10aa843

File tree

34 files changed

+261
-62
lines changed

34 files changed

+261
-62
lines changed

CHANGES.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,9 @@ All notable changes to this project will be documented in this file.
44

55
Note - All hash comments refer to the issue number. Eg. #169 refers to https://github.com/mdjnelson/moodle-mod_customcert/issues/169.
66

7-
## [4.2.5] - 2024-XX-YY
7+
## [4.2.5] - 2024-04-23
88

9-
### Fixed
9+
### Fixed
1010

1111
- Fixed issue when restoring `date`, `daterange`, `gradeitemname` and `grade` that have been linked to a manual grade item (#582).
1212
- Removed unnecessary set_context() call causing a PHP notice to be shown (#443).

classes/element_helper.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -477,7 +477,7 @@ public static function get_courseid($elementid) {
477477
* @param int $elementid The element id
478478
* @return \context The context
479479
*/
480-
public static function get_context(int $elementid) : \context {
480+
public static function get_context(int $elementid): \context {
481481
global $DB;
482482

483483
$sql = "SELECT ct.contextid

classes/privacy/provider.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ class provider implements
4949
* @param collection $items a reference to the collection to use to store the metadata.
5050
* @return collection the updated collection of metadata items.
5151
*/
52-
public static function get_metadata(collection $items) : collection {
52+
public static function get_metadata(collection $items): collection {
5353
$items->add_database_table(
5454
'customcert_issues',
5555
[
@@ -71,7 +71,7 @@ public static function get_metadata(collection $items) : collection {
7171
* @param int $userid the userid.
7272
* @return contextlist the list of contexts containing user info for the user.
7373
*/
74-
public static function get_contexts_for_userid(int $userid) : contextlist {
74+
public static function get_contexts_for_userid(int $userid): contextlist {
7575
$sql = "SELECT c.id
7676
FROM {context} c
7777
INNER JOIN {course_modules} cm

classes/task/email_certificate_task.php

Lines changed: 101 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,35 @@ 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+
'u.id, username, firstname, lastname, email, firstnamephonetic, lastnamephonetic, middlename, alternatename');
179+
// Get users with mod/customcert:view capability.
180+
$userswithview = get_users_by_capability($context, 'mod/customcert:view',
181+
'u.id, username, firstname, lastname, email, firstnamephonetic, lastnamephonetic, middlename, alternatename');
182+
// Users with both mod/customcert:view and mod/customcert:receiveissue cabapilities.
183+
$userswithissueview = array_intersect_key($userswithissue, $userswithview);
184+
185+
$infomodule = new \core_availability\info_module($fastmoduleinfo);
186+
// Filter who can't access due to availability restriction, from the full list.
187+
$userscanissue = $infomodule->filter_user_list($userswithissueview);
188+
189+
foreach ($userscanissue as $enroluser) {
190+
// Check if the user has already been issued.
191+
if (in_array($enroluser->id, array_keys((array)$issuedusers))) {
130192
continue;
131193
}
132194

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

@@ -164,7 +226,7 @@ public function execute() {
164226
}
165227
}
166228

167-
// If there are no users to email we can return early.
229+
// If there are no users to email, we can return early.
168230
if (!$issuedusers) {
169231
continue;
170232
}
@@ -175,6 +237,7 @@ public function execute() {
175237
return;
176238
}
177239

240+
$issueids = [];
178241
// Now, email the people we need to.
179242
foreach ($issuedusers as $user) {
180243
// Set up the user.
@@ -248,8 +311,18 @@ public function execute() {
248311
}
249312

250313
// Set the field so that it is emailed.
251-
$DB->set_field('customcert_issues', 'emailed', 1, ['id' => $user->issueid]);
314+
$issueids[] = $user->issueid;
315+
}
316+
if (!empty($issueids)) {
317+
$DB->set_field_select('customcert_issues', 'emailed', 1, 'id IN (' . implode(',', $issueids) . ')');
252318
}
253319
}
320+
// Update the last processed position, if run in batches.
321+
if ($certificatesperrun > 0) {
322+
$newlastprocessed = $lastprocessed + count($certificates);
323+
$DB->set_field('customcert_task_progress', 'last_processed', $newlastprocessed, [
324+
'taskname' => 'email_certificate_task',
325+
]);
326+
}
254327
}
255328
}

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
}

download_all_certificates.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
* Handles downloading all certificates on the site.
1919
*
2020
* @package mod_customcert
21-
* @copyright 2013 Mark Nelson <markn@moodle.com>
21+
* @copyright 2024 Mark Nelson <mdjnelson@gmail.com>
2222
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
2323
*/
2424

element/bgimage/classes/privacy/provider.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ class provider implements \core_privacy\local\metadata\null_provider {
3838
*
3939
* @return string
4040
*/
41-
public static function get_reason() : string {
41+
public static function get_reason(): string {
4242
return 'privacy:metadata';
4343
}
4444
}

element/border/classes/privacy/provider.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ class provider implements \core_privacy\local\metadata\null_provider {
3838
*
3939
* @return string
4040
*/
41-
public static function get_reason() : string {
41+
public static function get_reason(): string {
4242
return 'privacy:metadata';
4343
}
4444
}

element/categoryname/classes/element.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ public function render_html() {
6161
*
6262
* @return string
6363
*/
64-
protected function get_category_name() : string {
64+
protected function get_category_name(): string {
6565
global $DB, $SITE;
6666

6767
$courseid = \mod_customcert\element_helper::get_courseid($this->get_id());

element/categoryname/classes/privacy/provider.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ class provider implements \core_privacy\local\metadata\null_provider {
3838
*
3939
* @return string
4040
*/
41-
public static function get_reason() : string {
41+
public static function get_reason(): string {
4242
return 'privacy:metadata';
4343
}
4444
}

element/code/classes/privacy/provider.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ class provider implements \core_privacy\local\metadata\null_provider {
3838
*
3939
* @return string
4040
*/
41-
public static function get_reason() : string {
41+
public static function get_reason(): string {
4242
return 'privacy:metadata';
4343
}
4444
}

element/coursefield/classes/element.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -125,7 +125,7 @@ public function definition_after_data($mform) {
125125
* @param bool $preview Is this a preview?
126126
* @return string
127127
*/
128-
protected function get_course_field_value(\stdClass $course, bool $preview) : string {
128+
protected function get_course_field_value(\stdClass $course, bool $preview): string {
129129

130130
// The user field to display.
131131
$field = $this->get_data();

0 commit comments

Comments
 (0)