Skip to content
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
6 changes: 3 additions & 3 deletions CHANGES.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ Note - All hash comments refer to the issue number. Eg. #169 refers to https://g

### Added

- Only fetch teachers during the email process when necessary, reducing the number of SQL queries if they are not included (#531).
- Only fetch teachers during the email process when necessary, reducing the number of SQL queries if they are not included (#531).
- Filter users before process to speed up certificate task (#634).

## [4.4.2] - 2024-09-28
Expand All @@ -62,7 +62,7 @@ Note - All hash comments refer to the issue number. Eg. #169 refers to https://g

- Optimise email certificate task by reducing database reads/writes and introducing
configurable settings for task efficiency (#531).
- New element `expiry` which when used will display the expiry date on the list of issued certificates
- New element `expiry` which when used will display the expiry date on the list of issued certificates
and the verification pages.<br />
Any Custom Certificates that are using the `date` element and selected the expiry dates will
automatically be upgraded to use this new element (#499).
Expand Down Expand Up @@ -144,7 +144,7 @@ Note - All hash comments refer to the issue number. Eg. #169 refers to https://g
- An event for when a page is created.
- An event for when a page is updated.
- An event for when a page is deleted.
- An event for when a template is created.
- An event for when a template is created.
- An event for when a template is updated.
- An event for when a template is deleted.

Expand Down
181 changes: 147 additions & 34 deletions classes/task/email_certificate_task.php
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,14 @@ public function execute() {

$issueid = $customdata->issueid;
$customcertid = $customdata->customcertid;

// Check if already emailed to prevent duplicates on retry.
$issue = $DB->get_record('customcert_issues', ['id' => $issueid], 'emailed');
if ($issue && $issue->emailed) {
mtrace("Certificate issue ID $issueid already emailed, skipping.");
return; // Already processed, skip to prevent duplicate emails.
}

$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
Expand All @@ -62,6 +70,11 @@ public function execute() {

$customcert = $DB->get_record_sql($sql, ['id' => $customcertid]);

if (!$customcert) {
mtrace("Certificate with ID $customcertid not found.");
return;
}

// The renderers used for sending emails.
$page = new \moodle_page();
$htmlrenderer = $page->get_renderer('mod_customcert', 'email', 'htmlemail');
Expand Down Expand Up @@ -94,10 +107,15 @@ public function execute() {
AND ci.id = :issueid";
$user = $DB->get_record_sql($sql, ['customcertid' => $customcertid, 'issueid' => $issueid]);

if (!$user) {
mtrace("User or certificate issue not found for issue ID $issueid.");
return;
}

// Create a directory to store the PDF we will be sending.
$tempdir = make_temp_directory('certificate/attachment');
if (!$tempdir) {
return;
throw new \moodle_exception('Failed to create temporary directory for certificate attachment');
}

// Setup the user for the cron.
Expand All @@ -112,7 +130,15 @@ public function execute() {
$template->name = $customcert->templatename;
$template->contextid = $customcert->contextid;
$template = new \mod_customcert\template($template);
$filecontents = $template->generate_pdf(false, $user->id, true);

try {
$filecontents = $template->generate_pdf(false, $user->id, true);
} catch (\Exception $e) {
// Log PDF generation failure and allow retry by throwing exception.
mtrace('Certificate PDF generation failed for issue ID ' . $issueid . ': ' . $e->getMessage());
debugging('Certificate PDF generation failed: ' . $e->getMessage(), DEBUG_DEVELOPER);
throw new \moodle_exception('PDF generation failed: ' . $e->getMessage());
}

// Set the name of the file we are going to send.
$filename = $courseshortname . '_' . $certificatename;
Expand All @@ -122,57 +148,144 @@ public function execute() {
$filename = str_replace('&', '_', $filename) . '.pdf';

// Create the file we will be sending.
$tempfile = $tempdir . '/' . md5(microtime() . $user->id) . '.pdf';
file_put_contents($tempfile, $filecontents);
$tempfile = $tempdir . '/' . md5(microtime() . $user->id . random_int(1000, 9999)) . '.pdf';
if (file_put_contents($tempfile, $filecontents) === false) {
mtrace('Certificate PDF could not be written to temp file for issue ID ' . $issueid);
debugging('Certificate PDF write failed for issue ID ' . $issueid, DEBUG_DEVELOPER);
throw new \moodle_exception('Failed to write PDF to temporary file');
}

$transaction = $DB->start_delegated_transaction();
try {
// Note: emailed flag is set before email sending.
// This is intentional to prevent infinite retries if emails fail.
$DB->set_field('customcert_issues', 'emailed', 1, ['id' => $issueid]);
mtrace("Marked certificate issue ID $issueid as emailed to prevent retries.");

// Track email sending results for logging
$emailresults = [];
$emailfailures = [];

// Now try to send emails; log any failures but DO NOT retry.
if ($customcert->emailstudents) {
$renderable = new \mod_customcert\output\email_certificate(true, $userfullname, $courseshortname,
$coursefullname, $certificatename, $context->instanceid);

$subject = get_string('emailstudentsubject', 'customcert', $info);
$message = $textrenderer->render($renderable);
$messagehtml = $htmlrenderer->render($renderable);
email_to_user($user, $userfrom, html_entity_decode($subject, ENT_COMPAT), $message,
$messagehtml, $tempfile, $filename);
try {
$renderable = new \mod_customcert\output\email_certificate(
true,
$userfullname,
$courseshortname,
$coursefullname,
$certificatename,
$context->instanceid
);

$subject = get_string('emailstudentsubject', 'customcert', $info);
$message = $textrenderer->render($renderable);
$messagehtml = $htmlrenderer->render($renderable);

$result = email_to_user($user, $userfrom, html_entity_decode($subject, ENT_COMPAT), $message,
$messagehtml, $tempfile, $filename);

if ($result) {
$emailresults[] = "Student email sent to {$user->email}";
} else {
$emailfailures[] = "Failed to send student email to {$user->email}";
}
} catch (\Exception $e) {
$emailfailures[] = "Exception sending student email: " . $e->getMessage();
}
}

if ($customcert->emailteachers) {
$teachers = get_enrolled_users($context, 'moodle/course:update');
try {
$teachers = get_enrolled_users($context, 'moodle/course:update');

$renderable = new \mod_customcert\output\email_certificate(false, $userfullname, $courseshortname,
$coursefullname, $certificatename, $context->instanceid);
$renderable = new \mod_customcert\output\email_certificate(false, $userfullname, $courseshortname,
$coursefullname, $certificatename, $context->instanceid);

$subject = get_string('emailnonstudentsubject', 'customcert', $info);
$message = $textrenderer->render($renderable);
$messagehtml = $htmlrenderer->render($renderable);
foreach ($teachers as $teacher) {
email_to_user($teacher, $userfrom, html_entity_decode($subject, ENT_COMPAT),
$message, $messagehtml, $tempfile, $filename);
$subject = get_string('emailnonstudentsubject', 'customcert', $info);
$message = $textrenderer->render($renderable);
$messagehtml = $htmlrenderer->render($renderable);

foreach ($teachers as $teacher) {
try {
$result = email_to_user($teacher, $userfrom, html_entity_decode($subject, ENT_COMPAT),
$message, $messagehtml, $tempfile, $filename);

if ($result) {
$emailresults[] = "Teacher email sent to {$teacher->email}";
} else {
$emailfailures[] = "Failed to send teacher email to {$teacher->email}";
}
} catch (\Exception $e) {
$emailfailures[] = "Exception sending teacher email to {$teacher->email}: " . $e->getMessage();
}
}
} catch (\Exception $e) {
$emailfailures[] = "Exception getting teachers or sending teacher emails: " . $e->getMessage();
}
}

if (!empty($customcert->emailothers)) {
$others = explode(',', $customcert->emailothers);
foreach ($others as $email) {
$email = trim($email);
if (validate_email($email)) {
$renderable = new \mod_customcert\output\email_certificate(false, $userfullname,
$courseshortname, $coursefullname, $certificatename, $context->instanceid);
try {
$others = explode(',', $customcert->emailothers);
$renderable = new \mod_customcert\output\email_certificate(false, $userfullname,
$courseshortname, $coursefullname, $certificatename, $context->instanceid);

$subject = get_string('emailnonstudentsubject', 'customcert', $info);
$message = $textrenderer->render($renderable);
$messagehtml = $htmlrenderer->render($renderable);

$emailuser = new \stdClass();
$emailuser->id = -1;
$emailuser->email = $email;
email_to_user($emailuser, $userfrom, html_entity_decode($subject, ENT_COMPAT), $message,
$messagehtml, $tempfile, $filename);
foreach ($others as $email) {
$email = trim($email);
if (validate_email($email)) {
try {
$emailuser = new \stdClass();
$emailuser->id = -1;
$emailuser->email = $email;

$result = email_to_user($emailuser, $userfrom, html_entity_decode($subject, ENT_COMPAT), $message,
$messagehtml, $tempfile, $filename);

if ($result) {
$emailresults[] = "Other email sent to {$email}";
} else {
$emailfailures[] = "Failed to send other email to {$email}";
}
} catch (\Exception $e) {
$emailfailures[] = "Exception sending other email to {$email}: " . $e->getMessage();
}
} else {
$emailfailures[] = "Invalid email address in others list: {$email}";
}
}
} catch (\Exception $e) {
$emailfailures[] = "Exception processing other email addresses: " . $e->getMessage();
}
}

// Set the field so that it is emailed.
$DB->set_field('customcert_issues', 'emailed', 1, ['id' => $issueid]);
// Log results
if (!empty($emailresults)) {
mtrace("Email successes for issue ID $issueid: " . implode(', ', $emailresults));
}

if (!empty($emailfailures)) {
mtrace("Email failures for issue ID $issueid: " . implode(', ', $emailfailures));
debugging("Certificate email failures for issue ID $issueid: " . implode('; ', $emailfailures), DEBUG_DEVELOPER);
}

if (empty($emailresults)) {
throw new \moodle_exception("No emails sent successfully for issue ID $issueid; retrying later.");
}
$transaction->allow_commit();
// Clean up temporary file
if (file_exists($tempfile)) {
unlink($tempfile);
}
} catch (\Exception $e) {
$emailfailures[] = "Email sending failed: " . $e->getMessage();
$transaction->rollback($e);
throw $e;
}
mtrace("Certificate email task completed for issue ID $issueid.");
}
}
30 changes: 19 additions & 11 deletions classes/template.php
Original file line number Diff line number Diff line change
Expand Up @@ -320,30 +320,38 @@ public function generate_pdf(bool $preview = false, ?int $userid = null, bool $r
$pdf->SetAutoPageBreak(true, 0);

// Get filename pattern from global settings.
if (empty($customcert->usecustomfilename) || empty($customcert->customfilenamepattern)) {
if (!empty($customcert->usecustomfilename) && !empty($customcert->customfilenamepattern)) {
$filenamepattern = $customcert->customfilenamepattern;
} else {
$filenamepattern = '{DEFAULT}';
}

if (empty($filenamepattern) || $filenamepattern === '{DEFAULT}') {
// Use the custom cert name as the base filename (strip any trailing dot).
$filename = rtrim(format_string($this->name, true, ['context' => $this->get_context()]), '.');
} else {
// Build filename from pattern substitutions.

// Get issue record for date (if issued); fallback to current date if not found.
$issue = $DB->get_record('customcert_issues', [
'userid' => $user->id,
'customcertid' => $customcert->id,
]);
], '*', IGNORE_MISSING);

if ($issue && !empty($issue->timecreated)) {
$issuedate = date('Y-m-d', $issue->timecreated);
} else {
$issuedate = date('Y-m-d');
}

$course = $DB->get_record('course', ['id' => $customcert->course]);
$course = $DB->get_record('course', ['id' => $customcert->course], '*', IGNORE_MISSING);

$values = [
'{FIRST_NAME}' => $user->firstname ?? '',
'{LAST_NAME}' => $user->lastname ?? '',
'{COURSE_SHORT_NAME}' => $course ? $course->shortname : '',
'{COURSE_FULL_NAME}' => $course ? $course->fullname : '',
'{ISSUE_DATE}' => $issuedate,
'{FIRST NAME}' => $user->firstname ?? '',
'{LAST NAME}' => $user->lastname ?? '',
'{COURSE SHORT NAME}' => $course ? $course->shortname : '',
'{COURSE FULL NAME}' => $course ? $course->fullname : '',
'{DATE}' => $issuedate,
];

// Handle group if needed.
Expand All @@ -352,13 +360,13 @@ public function generate_pdf(bool $preview = false, ?int $userid = null, bool $r
$groupnames = array_map(function($g) {
return $g->name;
}, $groups);
$values['{GROUP_NAME}'] = implode(', ', $groupnames);
$values['{GROUP}'] = implode(', ', $groupnames);
} else {
$values['{GROUP_NAME}'] = '';
$values['{GROUP}'] = '';
}

// Replace placeholders with actual values.
$filename = strtr($customcert->customfilenamepattern, $values);
$filename = strtr($filenamepattern, $values);

// Remove trailing dot to avoid "..pdf" issues.
$filename = rtrim($filename, '.');
Expand Down
2 changes: 1 addition & 1 deletion db/install.xml
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@
</KEYS>
<INDEXES>
<INDEX NAME="userid-customcertid" UNIQUE="false" FIELDS="userid, customcertid"/>
<INDEX NAME="code" UNIQUE="false" FIELDS="code"/>
<INDEX NAME="code" UNIQUE="true" FIELDS="code"/>
</INDEXES>
</TABLE>
<TABLE NAME="customcert_pages" COMMENT="Stores each page of a custom cert">
Expand Down
13 changes: 7 additions & 6 deletions db/upgrade.php
Original file line number Diff line number Diff line change
Expand Up @@ -298,25 +298,27 @@ function xmldb_customcert_upgrade($oldversion) {
upgrade_mod_savepoint(true, 2024042205, 'customcert');
}

// Drop the unique index on 'code' and add a non-unique one.
if ($oldversion < 2024042210) {
$table = new xmldb_table('customcert_issues');
$index = new xmldb_index('code', XMLDB_INDEX_UNIQUE, ['code']);

// Drop existing unique index if it exists.
$index = new xmldb_index('code', XMLDB_INDEX_UNIQUE, ['code']);
if ($dbman->index_exists($table, $index)) {
$dbman->drop_index($table, $index);
}

// Add non-unique index.
$index = new xmldb_index('code', XMLDB_INDEX_NOTUNIQUE, ['code']);

if (!$dbman->index_exists($table, $index)) {
$dbman->add_index($table, $index);
}

// Update the plugin version in the database.
// Save the upgrade step.
upgrade_plugin_savepoint(true, 2024042210, 'mod', 'customcert');
}

if ($oldversion < 2024042213) {
if ($oldversion < 2025062100) {
$table = new xmldb_table('customcert');

// Add 'usecustomfilename' field.
Expand All @@ -332,8 +334,7 @@ function xmldb_customcert_upgrade($oldversion) {
}

// Savepoint reached.
upgrade_mod_savepoint(true, 2024042213, 'customcert');
upgrade_mod_savepoint(true, 2025062100, 'customcert');
}

return true;
}
5 changes: 2 additions & 3 deletions element/qrcode/classes/element.php
Original file line number Diff line number Diff line change
Expand Up @@ -158,8 +158,7 @@ public function render($pdf, $preview, $user) {
$context = \context::instance_by_id($issue->contextid);

$urlparams = [
'code' => $code,
'qrcode' => 1,
'certId' => $code,
];

// We only add the 'contextid' to the link if the site setting for verifying all certificates is off,
Expand All @@ -173,7 +172,7 @@ public function render($pdf, $preview, $user) {
$urlparams['contextid'] = $issue->contextid;
}

$qrcodeurl = new \moodle_url('/mod/customcert/verify_certificate.php', $urlparams);
$qrcodeurl = new \moodle_url('https://app.pacificmedicaltraining.com/verify', $urlparams);
$qrcodeurl = $qrcodeurl->out(false);
}

Expand Down
Loading
Loading