From a1dc566bffa96bf9503ca9da87c9cf0edbf6d500 Mon Sep 17 00:00:00 2001 From: Dhanuka Date: Fri, 30 May 2025 09:43:07 +0530 Subject: [PATCH 01/23] Updated subplugins.json file to match latest format (#688) See MDL-83705 for more information. --- db/subplugins.json | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/db/subplugins.json b/db/subplugins.json index 7ce4aca7..0ae5e5c5 100644 --- a/db/subplugins.json +++ b/db/subplugins.json @@ -1,5 +1,9 @@ { + "subplugintypes": { + "customcertelement": "element" + }, "plugintypes": { - "customcertelement": "mod\/customcert\/element" + "customcertelement": "mod/customcert/element" } } + From 2cdc66c1f38bb0b97d3db6e9d8d7c4209e5b0229 Mon Sep 17 00:00:00 2001 From: Mark Nelson Date: Sat, 7 Jun 2025 19:53:41 +0100 Subject: [PATCH 02/23] Updated GHA for Moodle 5.0 --- .github/workflows/moodle-ci.yml | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/.github/workflows/moodle-ci.yml b/.github/workflows/moodle-ci.yml index 47f1b3aa..f50c4cdb 100644 --- a/.github/workflows/moodle-ci.yml +++ b/.github/workflows/moodle-ci.yml @@ -8,7 +8,7 @@ jobs: services: postgres: - image: postgres:13 + image: postgres:14 env: POSTGRES_USER: 'postgres' POSTGRES_HOST_AUTH_METHOD: 'trust' @@ -16,7 +16,7 @@ jobs: - 5432:5432 options: --health-cmd pg_isready --health-interval 10s --health-timeout 5s --health-retries 3 mariadb: - image: mariadb:10.6.10 + image: mariadb:10.11 env: MYSQL_USER: 'root' MYSQL_ALLOW_EMPTY_PASSWORD: "true" @@ -28,17 +28,17 @@ jobs: fail-fast: false matrix: include: - - php: '8.3' - moodle-branch: 'MOODLE_404_STABLE' + - php: '8.4' + moodle-branch: 'MOODLE_500_STABLE' database: pgsql - - php: '8.3' - moodle-branch: 'MOODLE_404_STABLE' + - php: '8.4' + moodle-branch: 'MOODLE_500_STABLE' database: mariadb - - php: '8.1' - moodle-branch: 'MOODLE_404_STABLE' + - php: '8.2' + moodle-branch: 'MOODLE_500_STABLE' database: pgsql - - php: '8.1' - moodle-branch: 'MOODLE_404_STABLE' + - php: '8.2' + moodle-branch: 'MOODLE_500_STABLE' database: mariadb steps: @@ -56,7 +56,7 @@ jobs: - name: Initialise moodle-plugin-ci run: | - composer create-project -n --no-dev --prefer-dist moodlehq/moodle-plugin-ci ci ^3 + composer create-project -n --no-dev --prefer-dist moodlehq/moodle-plugin-ci ci ^4 echo $(cd ci/bin; pwd) >> $GITHUB_PATH echo $(cd ci/vendor/bin; pwd) >> $GITHUB_PATH sudo locale-gen en_AU.UTF-8 From b41d838be31b48b692d68ab508411384d164f9c2 Mon Sep 17 00:00:00 2001 From: Mark Nelson Date: Sat, 7 Jun 2025 20:07:31 +0100 Subject: [PATCH 03/23] Updated CHANGES.md --- CHANGES.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGES.md b/CHANGES.md index 5d7b9b84..fca964c0 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -4,6 +4,12 @@ All notable changes to this project will be documented in this file. Note - All hash comments refer to the issue number. Eg. #169 refers to https://github.com/mdjnelson/moodle-mod_customcert/issues/169. +## [5.0.0] - 2025-06-07 + +### Fixed + +- Updated subplugins.json file to match Moodle 5.0 format (#688). + ## [4.4.6] - 2025-06-07 ### Added From 13565ec7800bb9dc0c96c256303476d9d618bee0 Mon Sep 17 00:00:00 2001 From: Mark Nelson Date: Sat, 7 Jun 2025 20:23:16 +0100 Subject: [PATCH 04/23] Fix code style complaints --- classes/certificate.php | 2 +- lang/en/customcert.php | 9 +++++---- settings.php | 2 +- 3 files changed, 7 insertions(+), 6 deletions(-) diff --git a/classes/certificate.php b/classes/certificate.php index e8e4fd4e..10e4a9dd 100644 --- a/classes/certificate.php +++ b/classes/certificate.php @@ -545,7 +545,7 @@ public static function issue_certificate($certificateid, $userid) { $event = \mod_customcert\event\issue_created::create([ 'objectid' => $issueid, 'context' => $context, - 'relateduserid' => $userid + 'relateduserid' => $userid, ]); $event->trigger(); diff --git a/lang/en/customcert.php b/lang/en/customcert.php index 2e9a5bef..cbfe11e9 100644 --- a/lang/en/customcert.php +++ b/lang/en/customcert.php @@ -38,6 +38,10 @@ $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['codegenerationmethod'] = 'Code generation method'; +$string['codegenerationmethod_desc'] = 'Choose between the two methods for generating certificate codes.'; +$string['codegenerationmethod_digitshyphens'] = '0123-4567-8901 (Digits with hyphens)'; +$string['codegenerationmethod_upperlowerdigits'] = '6aOdbLEuoC (Upper/lower/digits random string)'; $string['copy'] = 'Copy'; $string['coursetimereq'] = 'Required minutes in course'; $string['coursetimereq_help'] = 'Enter here the minimum amount of time, in minutes, that a student must be logged into the course before they will be able to receive @@ -245,7 +249,4 @@ $string['verifycertificatedesc'] = 'This link will take you to a new screen where you will be able to verify certificates on the site'; $string['width'] = 'Width'; $string['width_help'] = 'This is the width of the certificate PDF in mm. For reference an A4 piece of paper is 210mm wide and a letter is 216mm wide.'; -$string['codegenerationmethod'] = 'Code generation method'; -$string['codegenerationmethod_desc'] = 'Choose between the two methods for generating certificate codes.'; -$string['codegenerationmethod_upperlowerdigits'] = '6aOdbLEuoC (Upper/lower/digits random string)'; -$string['codegenerationmethod_digitshyphens'] = '0123-4567-8901 (Digits with hyphens)'; + diff --git a/settings.php b/settings.php index 27a34974..60ca3e4e 100644 --- a/settings.php +++ b/settings.php @@ -88,7 +88,7 @@ 0, // Default option (0 = Upper/lower/digits random string method). [ 0 => get_string('codegenerationmethod_upperlowerdigits', 'customcert'), // Upper/lower/digits random string. - 1 => get_string('codegenerationmethod_digitshyphens', 'customcert') // Digits with hyphens numeric code. + 1 => get_string('codegenerationmethod_digitshyphens', 'customcert'), // Digits with hyphens numeric code. ] )); From 14790755507f1803ac32747d1dcc9f665ab65a93 Mon Sep 17 00:00:00 2001 From: Mark Nelson Date: Sat, 7 Jun 2025 23:34:55 +0100 Subject: [PATCH 05/23] Fixed null array offset warning in QR code element on PHP 8.4 in GHA --- element/qrcode/classes/element.php | 30 +++++++++++++++++++++++------- 1 file changed, 23 insertions(+), 7 deletions(-) diff --git a/element/qrcode/classes/element.php b/element/qrcode/classes/element.php index 4cd88977..8acbe21f 100644 --- a/element/qrcode/classes/element.php +++ b/element/qrcode/classes/element.php @@ -177,13 +177,21 @@ public function render($pdf, $preview, $user) { $qrcodeurl = $qrcodeurl->out(false); } - $barcode = new \TCPDF2DBarcode($qrcodeurl, self::BARCODETYPE); - $image = $barcode->getBarcodePngData($imageinfo->width, $imageinfo->height); + try { + $barcode = new \TCPDF2DBarcode($qrcodeurl, self::BARCODETYPE); + $image = $barcode->getBarcodePngData($imageinfo->width, $imageinfo->height); - $location = make_request_directory() . '/target'; - file_put_contents($location, $image); + $location = make_request_directory() . '/target'; + file_put_contents($location, $image); - $pdf->Image($location, $this->get_posx(), $this->get_posy(), $imageinfo->width, $imageinfo->height); + $pdf->Image($location, $this->get_posx(), $this->get_posy(), $imageinfo->width, $imageinfo->height); + } catch (\Throwable $e) { + if (!defined('PHPUNIT_TEST') && !defined('BEHAT_SITE_RUNNING')) { + debugging('QR code render failed: ' . $e->getMessage(), DEBUG_DEVELOPER); + } + + return; + } } /** @@ -205,7 +213,15 @@ public function render_html() { $qrcodeurl = new \moodle_url('/'); $qrcodeurl = $qrcodeurl->out(false); - $barcode = new \TCPDF2DBarcode($qrcodeurl, self::BARCODETYPE); - return $barcode->getBarcodeHTML($imageinfo->width / 10, $imageinfo->height / 10); + try { + $barcode = new \TCPDF2DBarcode($qrcodeurl, self::BARCODETYPE); + return $barcode->getBarcodeHTML($imageinfo->width / 10, $imageinfo->height / 10); + } catch (\Throwable $e) { + if (!defined('PHPUNIT_TEST') && !defined('BEHAT_SITE_RUNNING')) { + debugging('QR code render failed: ' . $e->getMessage(), DEBUG_DEVELOPER); + } + + return ''; + } } } From 8484023762a14c5602b5db9cef6401aba95077e4 Mon Sep 17 00:00:00 2001 From: Mark Nelson Date: Sat, 7 Jun 2025 19:47:02 +0100 Subject: [PATCH 06/23] Bumped version --- version.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/version.php b/version.php index 365a99d2..9a1bde8c 100644 --- a/version.php +++ b/version.php @@ -24,10 +24,10 @@ defined('MOODLE_INTERNAL') || die('Direct access to this script is forbidden.'); -$plugin->version = 2024042212; // The current module version (Date: YYYYMMDDXX). -$plugin->requires = 2024042200; // Requires this Moodle version (4.4). +$plugin->version = 2025041400; // The current module version (Date: YYYYMMDDXX). +$plugin->requires = 2025041400; // Requires this Moodle version (5.0). $plugin->cron = 0; // Period for cron to check this module (secs). $plugin->component = 'mod_customcert'; $plugin->maturity = MATURITY_STABLE; -$plugin->release = "4.4.6"; // User-friendly version number. +$plugin->release = "5.0.0"; // User-friendly version number. From c7f98e3694af9f877de298e3b7a81305dce8eef8 Mon Sep 17 00:00:00 2001 From: Andrew Hancox Date: Mon, 16 Jun 2025 15:46:33 +0100 Subject: [PATCH 07/23] Fix notifications when the instance of the activity is on site home #693 --- classes/task/issue_certificates_task.php | 4 +- tests/email_certificate_task_test.php | 80 ++++++++++++++++++++++++ 2 files changed, 82 insertions(+), 2 deletions(-) diff --git a/classes/task/issue_certificates_task.php b/classes/task/issue_certificates_task.php index fdd47106..aad74e4a 100644 --- a/classes/task/issue_certificates_task.php +++ b/classes/task/issue_certificates_task.php @@ -71,7 +71,7 @@ public function execute() { ON c.templateid = ct.id JOIN {course} co ON c.course = co.id - JOIN {course_categories} cat + LEFT JOIN {course_categories} cat ON co.category = cat.id LEFT JOIN {customcert_issues} ci ON c.id = ci.customcertid @@ -84,7 +84,7 @@ public function execute() { // Check the includeinnotvisiblecourses configuration. if (!$includeinnotvisiblecourses) { // Exclude certificates from hidden courses. - $sql .= " AND co.visible = 1 AND cat.visible = 1"; + $sql .= " AND co.visible = 1 AND (cat.visible = 1 OR cat.id IS NULL)"; } // Add condition based on certificate execution period. diff --git a/tests/email_certificate_task_test.php b/tests/email_certificate_task_test.php index 98ea5719..6432c219 100644 --- a/tests/email_certificate_task_test.php +++ b/tests/email_certificate_task_test.php @@ -26,6 +26,7 @@ namespace mod_customcert; use completion_info; +use context_module; use stdClass; use context_course; use advanced_testcase; @@ -223,6 +224,85 @@ public function test_email_certificates_students(): void { $this->assertCount(0, $emails); } + /** + * Tests the email certificate task for instance on the site home course. + * + * @covers \mod_customcert\task\issue_certificates_task + * @covers \mod_customcert\task\email_certificate_task + */ + public function test_email_certificates_sitehome(): void { + global $CFG, $DB, $SITE; + + // Create some users. + $user1 = $this->getDataGenerator()->create_user(); + $user2 = $this->getDataGenerator()->create_user(); + + // Create a custom certificate. + $customcert = $this->getDataGenerator()->create_module('customcert', ['course' => $SITE->id, + 'emailstudents' => 1]); + + $role = $DB->get_record('role', ['archetype' => 'user']); + role_change_permission($role->id, context_module::instance($customcert->cmid), 'mod/customcert:view', CAP_ALLOW); + role_change_permission($role->id, context_module::instance($customcert->cmid), 'mod/customcert:receiveissue', CAP_ALLOW); + + // Create template object. + $template = new stdClass(); + $template->id = $customcert->templateid; + $template->name = 'A template'; + $template->contextid = context_course::instance($SITE->id)->id; + $template = new template($template); + + // Add a page to this template. + $pageid = $template->add_page(); + + // Add an element to the page. + $element = new stdClass(); + $element->pageid = $pageid; + $element->name = 'Image'; + $DB->insert_record('customcert_elements', $element); + + // Ok, now issue this to one user. + \mod_customcert\certificate::issue_certificate($customcert->id, $user1->id); + + // Confirm there is only one entry in this table. + $this->assertEquals(1, $DB->count_records('customcert_issues')); + + // Run the task. + $sink = $this->redirectEmails(); + $task = new issue_certificates_task(); + $task->execute(); + $emails = $sink->get_messages(); + + // Get the issues from the issues table now. + $issues = $DB->get_records('customcert_issues'); + $this->assertCount(3, $issues); + + // Confirm that it was marked as emailed and was not issued to the teacher. + foreach ($issues as $issue) { + $this->assertEquals(1, $issue->emailed); + } + + // Confirm that we sent out emails to the two users. + $this->assertCount(3, $emails); + + $this->assertEquals($CFG->noreplyaddress, $emails[1]->from); + $this->assertEquals($user1->email, $emails[1]->to); + + $this->assertEquals($CFG->noreplyaddress, $emails[2]->from); + $this->assertEquals($user2->email, $emails[2]->to); + + // Now, run the task again and ensure we did not issue any more certificates. + $sink = $this->redirectEmails(); + $task = new issue_certificates_task(); + $task->execute(); + $emails = $sink->get_messages(); + + $issues = $DB->get_records('customcert_issues'); + + $this->assertCount(3, $issues); + $this->assertCount(0, $emails); + } + /** * Tests the email certificate task for teachers. * From 3fe710bb4e2fad68d421a78c1738f0e30254db11 Mon Sep 17 00:00:00 2001 From: Raza403 Date: Sun, 11 May 2025 12:43:19 +0500 Subject: [PATCH 08/23] Added customisable filename options for certificates (#684) --- classes/template.php | 67 +++++++++++++++++++++++++++++++++++++++--- db/upgrade.php | 18 ++++++++++++ lang/en/customcert.php | 15 +++++++++- mod_form.php | 20 +++++++++++++ version.php | 4 +-- 5 files changed, 117 insertions(+), 7 deletions(-) diff --git a/classes/template.php b/classes/template.php index 79dfdbd6..d5645885 100644 --- a/classes/template.php +++ b/classes/template.php @@ -314,24 +314,83 @@ public function generate_pdf(bool $preview = false, ?int $userid = null, bool $r $deliveryoption = $customcert->deliveryoption; } - // Remove full-stop at the end, if it exists, to avoid "..pdf" being created and being filtered by clean_filename. - $filename = rtrim(format_string($this->name, true, ['context' => $this->get_context()]), '.'); - + // Set up PDF document properties — no header/footer, auto page break. $pdf->setPrintHeader(false); $pdf->setPrintFooter(false); - $pdf->SetTitle($filename); $pdf->SetAutoPageBreak(true, 0); + // Get filename pattern from global settings. + 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], '*', IGNORE_MISSING); + + $values = [ + '{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. + $groups = groups_get_all_groups($course->id, $user->id); + if (!empty($groups)) { + $groupnames = array_map(function($g) { + return $g->name; + }, $groups); + $values['{GROUP}'] = implode(', ', $groupnames); + } else { + $values['{GROUP}'] = ''; + } + + // Replace placeholders with actual values. + $filename = strtr($filenamepattern, $values); + + // Remove trailing dot to avoid "..pdf" issues. + $filename = rtrim($filename, '.'); + } + // This is the logic the TCPDF library uses when processing the name. This makes names // such as 'الشهادة' become empty, so set a default name in these cases. $filename = preg_replace('/[\s]+/', '_', $filename); $filename = preg_replace('/[^a-zA-Z0-9_\.-]/', '', $filename); + // If filename ends up empty (e.g. after removing unsupported characters), use default string. if (empty($filename)) { $filename = get_string('certificate', 'customcert'); } + // Remove existing ".pdf" extension if present to avoid duplication. + $filename = preg_replace('/\.pdf$/i', '', $filename); + + // Clean the final filename and append ".pdf". $filename = clean_filename($filename . '.pdf'); + + // Set the PDF document title (for metadata, not the filename itself). + $pdf->SetTitle($filename); + // Loop through the pages and display their content. foreach ($pages as $page) { // Add the page to the PDF. diff --git a/db/upgrade.php b/db/upgrade.php index f0cf6a77..0fe8cc9c 100644 --- a/db/upgrade.php +++ b/db/upgrade.php @@ -316,5 +316,23 @@ function xmldb_customcert_upgrade($oldversion) { upgrade_plugin_savepoint(true, 2024042210, 'mod', 'customcert'); } + if ($oldversion < 2025062100) { + $table = new xmldb_table('customcert'); + + // Add 'usecustomfilename' field. + $field = new xmldb_field('usecustomfilename', XMLDB_TYPE_INTEGER, '1', null, XMLDB_NOTNULL, null, '0', 'deliveryoption'); + if (!$dbman->field_exists($table, $field)) { + $dbman->add_field($table, $field); + } + + // Add 'customfilenamepattern' field. + $field = new xmldb_field('customfilenamepattern', XMLDB_TYPE_TEXT, null, null, null, null, null, 'usecustomfilename'); + if (!$dbman->field_exists($table, $field)) { + $dbman->add_field($table, $field); + } + + // Savepoint reached. + upgrade_mod_savepoint(true, 2025062100, 'customcert'); + } return true; } diff --git a/lang/en/customcert.php b/lang/en/customcert.php index cbfe11e9..bf1cf4eb 100644 --- a/lang/en/customcert.php +++ b/lang/en/customcert.php @@ -22,6 +22,7 @@ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ +$string['Upper/lower/digits'] = '6aOdbLEuoC (Upper/lower/digits random string)'; $string['activity'] = 'Activity'; $string['addcertpage'] = 'Add page'; $string['addelement'] = 'Add element'; @@ -63,6 +64,10 @@ $string['customcert:viewallcertificates'] = 'View all certificates'; $string['customcert:viewreport'] = 'View course report'; $string['customcertsettings'] = 'Custom certificate settings'; +$string['customfilenamepattern'] = 'Custom file name pattern'; +$string['customfilenamepattern_help'] = 'Enter the pattern for naming certificate files. You can use placeholders such as {firstname}, {lastname}, {group}, {coursename}, {date}.'; +$string['custompattern'] = 'First name, Last name, Course short name, Issue date'; +$string['defaultpattern'] = 'Default (certificate name)'; $string['deletecertpage'] = 'Delete page'; $string['deleteconfirm'] = 'Delete confirmation'; $string['deleteelement'] = 'Delete element'; @@ -71,10 +76,14 @@ $string['deleteissuedcertificates'] = 'Delete issued certificates'; $string['deletepageconfirm'] = 'Are you sure you want to delete this certificate page?'; $string['deletetemplateconfirm'] = 'Are you sure you want to delete this certificate template?'; +$string['deliveryoption'] = 'Delivery option'; +$string['deliveryoption_help'] = 'Choose how the certificate should be delivered to users.'; $string['deliveryoptiondownload'] = 'Send to the browser and force a file download'; $string['deliveryoptioninline'] = 'Send the file inline to the browser'; $string['deliveryoptions'] = 'Delivery options'; $string['description'] = 'Description'; +$string['digits-with-hyphens'] = '0123-4567-8901 (Digits with hyphens)'; +$string['download'] = 'Force download'; $string['downloadallissuedcertificates'] = 'Download all issued certificates'; $string['downloadallsitecertificates'] = 'Download all site certificates'; $string['downloadallsitecertificatesdesc'] = 'This will download all the certificates on the site in a zip file.'; @@ -123,6 +132,8 @@ $string['eventtemplatedeleted'] = 'Custom certificate template deleted'; $string['eventtemplateupdated'] = 'Custom certificate template updated'; $string['exampledatawarning'] = 'Some of these values may just be an example to ensure positioning of the elements is possible.'; +$string['filenamepattern'] = 'File name pattern (legacy)'; +$string['filenamepattern_help'] = 'Choose the pattern for naming certificate files (legacy setting).'; $string['font'] = 'Font'; $string['font_help'] = 'The font used when generating this element.'; $string['fontcolour'] = 'Colour'; @@ -135,6 +146,7 @@ $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'] = 'Certificates from hidden courses are not proccesed by default. If you want to include them, enable this setting.'; +$string['inline'] = 'Display inline'; $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.'; @@ -235,6 +247,8 @@ $string['useadhoc_desc'] = 'When enabled, emails related to certificates will be handled immediately through an ad-hoc task created for each issue. When disabled, emails will be managed by the regular scheduled task. Enabling this option may improve the performance of the scheduled task by offloading email processing to ad-hoc tasks.'; +$string['usecustomfilename'] = 'Use custom file name pattern'; +$string['usecustomfilename_help'] = 'If enabled, you can define a custom file name pattern for certificates using placeholders such as {FIRST NAME}, {LAST NAME}, {GROUP}, {COUSRE SHORT NAME}, {DATE}.'; $string['userlanguage'] = 'Use user preferences'; $string['userlanguage_help'] = 'You can force the language of the certificate to override the user\'s language preferences.'; $string['verified'] = 'Verified'; @@ -249,4 +263,3 @@ $string['verifycertificatedesc'] = 'This link will take you to a new screen where you will be able to verify certificates on the site'; $string['width'] = 'Width'; $string['width_help'] = 'This is the width of the certificate PDF in mm. For reference an A4 piece of paper is 210mm wide and a letter is 216mm wide.'; - diff --git a/mod_form.php b/mod_form.php index fb284a14..38382b96 100644 --- a/mod_form.php +++ b/mod_form.php @@ -66,6 +66,26 @@ public function definition() { $mform->addElement('select', 'deliveryoption', get_string('deliveryoptions', 'customcert'), $deliveryoptions); $mform->setDefault('deliveryoption', certificate::DELIVERY_OPTION_INLINE); + // Checkbox to enable custom file name pattern. + $mform->addElement('advcheckbox', 'usecustomfilename', get_string('usecustomfilename', 'customcert')); + $mform->addHelpButton('usecustomfilename', 'usecustomfilename', 'customcert'); + $mform->setDefault('usecustomfilename', 0); + + // Text field for custom file name pattern. + $mform->addElement('text', 'customfilenamepattern', get_string('customfilenamepattern', 'customcert'), ['size' => '50']); + $mform->setType('customfilenamepattern', PARAM_TEXT); + $mform->addHelpButton('customfilenamepattern', 'customfilenamepattern', 'customcert'); + $mform->disabledIf('customfilenamepattern', 'usecustomfilename', 'notchecked'); + + if ($this->current) { + if (property_exists($this->current, 'usecustomfilename')) { + $mform->setDefault('usecustomfilename', $this->current->usecustomfilename); + } + if (property_exists($this->current, 'customfilenamepattern')) { + $mform->setDefault('customfilenamepattern', $this->current->customfilenamepattern); + } + } + if (has_capability('mod/customcert:manageemailstudents', $this->get_context())) { $mform->addElement('selectyesno', 'emailstudents', get_string('emailstudents', 'customcert')); $mform->setDefault('emailstudents', get_config('customcert', 'emailstudents')); diff --git a/version.php b/version.php index 9a1bde8c..cba997f2 100644 --- a/version.php +++ b/version.php @@ -24,8 +24,8 @@ defined('MOODLE_INTERNAL') || die('Direct access to this script is forbidden.'); -$plugin->version = 2025041400; // The current module version (Date: YYYYMMDDXX). -$plugin->requires = 2025041400; // Requires this Moodle version (5.0). +$plugin->version = 2025062100; // The current module version (Date: YYYYMMDDXX). +$plugin->requires = 2024042200; // Requires this Moodle version (5.0). $plugin->cron = 0; // Period for cron to check this module (secs). $plugin->component = 'mod_customcert'; From 00c6da59ce1d41349476788c669aa6b04bdd5859 Mon Sep 17 00:00:00 2001 From: Mark Nelson Date: Sun, 6 Jul 2025 14:44:26 +0100 Subject: [PATCH 09/23] Fixed version bump issue (#684) --- db/upgrade.php | 5 +++-- version.php | 4 ++-- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/db/upgrade.php b/db/upgrade.php index 0fe8cc9c..43b873dc 100644 --- a/db/upgrade.php +++ b/db/upgrade.php @@ -316,7 +316,7 @@ function xmldb_customcert_upgrade($oldversion) { upgrade_plugin_savepoint(true, 2024042210, 'mod', 'customcert'); } - if ($oldversion < 2025062100) { + if ($oldversion < 2025041401) { $table = new xmldb_table('customcert'); // Add 'usecustomfilename' field. @@ -332,7 +332,8 @@ function xmldb_customcert_upgrade($oldversion) { } // Savepoint reached. - upgrade_mod_savepoint(true, 2025062100, 'customcert'); + upgrade_mod_savepoint(true, 2025041401, 'customcert'); } + return true; } diff --git a/version.php b/version.php index cba997f2..b68819c1 100644 --- a/version.php +++ b/version.php @@ -24,8 +24,8 @@ defined('MOODLE_INTERNAL') || die('Direct access to this script is forbidden.'); -$plugin->version = 2025062100; // The current module version (Date: YYYYMMDDXX). -$plugin->requires = 2024042200; // Requires this Moodle version (5.0). +$plugin->version = 2025041401; // The current module version (Date: YYYYMMDDXX). +$plugin->requires = 2025041400; // Requires this Moodle version (5.0). $plugin->cron = 0; // Period for cron to check this module (secs). $plugin->component = 'mod_customcert'; From 5f982ff8bf80df18286a8eb12e5e083d7f7d63d7 Mon Sep 17 00:00:00 2001 From: Mark Nelson Date: Sun, 6 Jul 2025 14:49:30 +0100 Subject: [PATCH 10/23] Added newly introduced fields to install.xml (#684) --- db/install.xml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/db/install.xml b/db/install.xml index ab6106a0..9dff73d2 100644 --- a/db/install.xml +++ b/db/install.xml @@ -15,6 +15,8 @@ + + From cf542a6d3fce102152e6addfc0974fb86bb94380 Mon Sep 17 00:00:00 2001 From: Mark Nelson Date: Sun, 6 Jul 2025 15:01:56 +0100 Subject: [PATCH 11/23] Make language string consistent with others --- lang/en/customcert.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lang/en/customcert.php b/lang/en/customcert.php index bf1cf4eb..e0a9faac 100644 --- a/lang/en/customcert.php +++ b/lang/en/customcert.php @@ -243,7 +243,7 @@ $string['uploadimage'] = 'Upload image'; $string['uploadimagedesc'] = 'This link will take you to a new screen where you will be able to upload images. Images uploaded using this method will be available throughout your site to all users who are able to create a certificate.'; -$string['useadhoc'] = 'Use Email Certificate Ad-hoc Task'; +$string['useadhoc'] = 'Use email certificate ad-hoc task'; $string['useadhoc_desc'] = 'When enabled, emails related to certificates will be handled immediately through an ad-hoc task created for each issue. When disabled, emails will be managed by the regular scheduled task. Enabling this option may improve the performance of the scheduled task by offloading email processing to ad-hoc tasks.'; From 733428cb4a9e33c44c7b2d25f6c98728de595ec7 Mon Sep 17 00:00:00 2001 From: Mark Nelson Date: Sun, 6 Jul 2025 15:04:05 +0100 Subject: [PATCH 12/23] Removed unused language strings (#684) --- lang/en/customcert.php | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/lang/en/customcert.php b/lang/en/customcert.php index e0a9faac..600c939e 100644 --- a/lang/en/customcert.php +++ b/lang/en/customcert.php @@ -22,7 +22,6 @@ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ -$string['Upper/lower/digits'] = '6aOdbLEuoC (Upper/lower/digits random string)'; $string['activity'] = 'Activity'; $string['addcertpage'] = 'Add page'; $string['addelement'] = 'Add element'; @@ -66,8 +65,6 @@ $string['customcertsettings'] = 'Custom certificate settings'; $string['customfilenamepattern'] = 'Custom file name pattern'; $string['customfilenamepattern_help'] = 'Enter the pattern for naming certificate files. You can use placeholders such as {firstname}, {lastname}, {group}, {coursename}, {date}.'; -$string['custompattern'] = 'First name, Last name, Course short name, Issue date'; -$string['defaultpattern'] = 'Default (certificate name)'; $string['deletecertpage'] = 'Delete page'; $string['deleteconfirm'] = 'Delete confirmation'; $string['deleteelement'] = 'Delete element'; @@ -76,14 +73,10 @@ $string['deleteissuedcertificates'] = 'Delete issued certificates'; $string['deletepageconfirm'] = 'Are you sure you want to delete this certificate page?'; $string['deletetemplateconfirm'] = 'Are you sure you want to delete this certificate template?'; -$string['deliveryoption'] = 'Delivery option'; -$string['deliveryoption_help'] = 'Choose how the certificate should be delivered to users.'; $string['deliveryoptiondownload'] = 'Send to the browser and force a file download'; $string['deliveryoptioninline'] = 'Send the file inline to the browser'; $string['deliveryoptions'] = 'Delivery options'; $string['description'] = 'Description'; -$string['digits-with-hyphens'] = '0123-4567-8901 (Digits with hyphens)'; -$string['download'] = 'Force download'; $string['downloadallissuedcertificates'] = 'Download all issued certificates'; $string['downloadallsitecertificates'] = 'Download all site certificates'; $string['downloadallsitecertificatesdesc'] = 'This will download all the certificates on the site in a zip file.'; @@ -132,8 +125,6 @@ $string['eventtemplatedeleted'] = 'Custom certificate template deleted'; $string['eventtemplateupdated'] = 'Custom certificate template updated'; $string['exampledatawarning'] = 'Some of these values may just be an example to ensure positioning of the elements is possible.'; -$string['filenamepattern'] = 'File name pattern (legacy)'; -$string['filenamepattern_help'] = 'Choose the pattern for naming certificate files (legacy setting).'; $string['font'] = 'Font'; $string['font_help'] = 'The font used when generating this element.'; $string['fontcolour'] = 'Colour'; @@ -146,7 +137,6 @@ $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'] = 'Certificates from hidden courses are not proccesed by default. If you want to include them, enable this setting.'; -$string['inline'] = 'Display inline'; $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.'; From 349a4b4c303b348897c32137acc79cc421e14e7d Mon Sep 17 00:00:00 2001 From: Mark Nelson Date: Sun, 6 Jul 2025 16:22:32 +0100 Subject: [PATCH 13/23] Removed unnecessary code for filling in form inputs (#684) --- mod_form.php | 9 --------- 1 file changed, 9 deletions(-) diff --git a/mod_form.php b/mod_form.php index 38382b96..448fbba3 100644 --- a/mod_form.php +++ b/mod_form.php @@ -77,15 +77,6 @@ public function definition() { $mform->addHelpButton('customfilenamepattern', 'customfilenamepattern', 'customcert'); $mform->disabledIf('customfilenamepattern', 'usecustomfilename', 'notchecked'); - if ($this->current) { - if (property_exists($this->current, 'usecustomfilename')) { - $mform->setDefault('usecustomfilename', $this->current->usecustomfilename); - } - if (property_exists($this->current, 'customfilenamepattern')) { - $mform->setDefault('customfilenamepattern', $this->current->customfilenamepattern); - } - } - if (has_capability('mod/customcert:manageemailstudents', $this->get_context())) { $mform->addElement('selectyesno', 'emailstudents', get_string('emailstudents', 'customcert')); $mform->setDefault('emailstudents', get_config('customcert', 'emailstudents')); From 06af2ac1808e75ebe47c5abd6dfed33876c6137e Mon Sep 17 00:00:00 2001 From: Mark Nelson Date: Sun, 6 Jul 2025 17:12:03 +0100 Subject: [PATCH 14/23] Fixed issue with language strings not matching logic for placeholders (#684) Also did some minor changes. --- classes/template.php | 30 +++++++++++------------------- lang/en/customcert.php | 2 +- 2 files changed, 12 insertions(+), 20 deletions(-) diff --git a/classes/template.php b/classes/template.php index d5645885..870ffc44 100644 --- a/classes/template.php +++ b/classes/template.php @@ -320,23 +320,15 @@ 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)) { - $filenamepattern = $customcert->customfilenamepattern; - } else { - $filenamepattern = '{DEFAULT}'; - } - - if (empty($filenamepattern) || $filenamepattern === '{DEFAULT}') { + if (empty($customcert->usecustomfilename) || empty($customcert->customfilenamepattern)) { // 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); @@ -344,14 +336,14 @@ public function generate_pdf(bool $preview = false, ?int $userid = null, bool $r $issuedate = date('Y-m-d'); } - $course = $DB->get_record('course', ['id' => $customcert->course], '*', IGNORE_MISSING); + $course = $DB->get_record('course', ['id' => $customcert->course]); $values = [ - '{FIRST NAME}' => $user->firstname ?? '', - '{LAST NAME}' => $user->lastname ?? '', - '{COURSE SHORT NAME}' => $course ? $course->shortname : '', - '{COURSE FULL NAME}' => $course ? $course->fullname : '', - '{DATE}' => $issuedate, + '{FIRST_NAME}' => $user->firstname ?? '', + '{LAST_NAME}' => $user->lastname ?? '', + '{COURSE_SHORT_NAME}' => $course ? $course->shortname : '', + '{COURSE_FULL_NAME}' => $course ? $course->fullname : '', + '{ISSUE_DATE}' => $issuedate, ]; // Handle group if needed. @@ -360,13 +352,13 @@ public function generate_pdf(bool $preview = false, ?int $userid = null, bool $r $groupnames = array_map(function($g) { return $g->name; }, $groups); - $values['{GROUP}'] = implode(', ', $groupnames); + $values['{GROUP_NAME}'] = implode(', ', $groupnames); } else { - $values['{GROUP}'] = ''; + $values['{GROUP_NAME}'] = ''; } // Replace placeholders with actual values. - $filename = strtr($filenamepattern, $values); + $filename = strtr($customcert->customfilenamepattern, $values); // Remove trailing dot to avoid "..pdf" issues. $filename = rtrim($filename, '.'); diff --git a/lang/en/customcert.php b/lang/en/customcert.php index 600c939e..e5c5a7c5 100644 --- a/lang/en/customcert.php +++ b/lang/en/customcert.php @@ -238,7 +238,7 @@ Enabling this option may improve the performance of the scheduled task by offloading email processing to ad-hoc tasks.'; $string['usecustomfilename'] = 'Use custom file name pattern'; -$string['usecustomfilename_help'] = 'If enabled, you can define a custom file name pattern for certificates using placeholders such as {FIRST NAME}, {LAST NAME}, {GROUP}, {COUSRE SHORT NAME}, {DATE}.'; +$string['usecustomfilename_help'] = 'If enabled, you can define a custom file name pattern for certificates using placeholders. The placeholders are {FIRST_NAME}, {LAST_NAME}, {GROUP_NAME}, {COURSE_SHORT_NAME}, {COURSE_FULL_NAME} and {ISSUE_DATE}.'; $string['userlanguage'] = 'Use user preferences'; $string['userlanguage_help'] = 'You can force the language of the certificate to override the user\'s language preferences.'; $string['verified'] = 'Verified'; From 8305d26b01e9ae9cf1b65c8c0febd611bd2c3776 Mon Sep 17 00:00:00 2001 From: Mark Nelson Date: Sun, 6 Jul 2025 18:18:43 +0100 Subject: [PATCH 15/23] Added missing fields to backups (#705) --- backup/moodle2/backup_customcert_stepslib.php | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/backup/moodle2/backup_customcert_stepslib.php b/backup/moodle2/backup_customcert_stepslib.php index 01dc0f75..04625aec 100644 --- a/backup/moodle2/backup_customcert_stepslib.php +++ b/backup/moodle2/backup_customcert_stepslib.php @@ -40,8 +40,9 @@ protected function define_structure() { // The instance. $customcert = new backup_nested_element('customcert', ['id'], [ - 'templateid', 'name', 'intro', 'introformat', 'requiredtime', 'verifyany', 'emailstudents', - 'emailteachers', 'emailothers', 'protection', 'timecreated', 'timemodified']); + 'templateid', 'name', 'intro', 'introformat', 'requiredtime', 'verifyany', + 'deliveryoption', 'emailstudents', 'emailteachers', 'emailothers', + 'protection', 'language', 'timecreated', 'timemodified']); // The template. $template = new backup_nested_element('template', ['id'], [ From 1472921455662f996b41a301c6a71a8852b4ae6a Mon Sep 17 00:00:00 2001 From: Mark Nelson Date: Sun, 6 Jul 2025 18:20:14 +0100 Subject: [PATCH 16/23] Added fields to backup file (#684) --- backup/moodle2/backup_customcert_stepslib.php | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/backup/moodle2/backup_customcert_stepslib.php b/backup/moodle2/backup_customcert_stepslib.php index 04625aec..c3e61020 100644 --- a/backup/moodle2/backup_customcert_stepslib.php +++ b/backup/moodle2/backup_customcert_stepslib.php @@ -41,8 +41,9 @@ protected function define_structure() { // The instance. $customcert = new backup_nested_element('customcert', ['id'], [ 'templateid', 'name', 'intro', 'introformat', 'requiredtime', 'verifyany', - 'deliveryoption', 'emailstudents', 'emailteachers', 'emailothers', - 'protection', 'language', 'timecreated', 'timemodified']); + 'deliveryoption', 'usecustomfilename', 'customfilenamepattern', 'emailstudents', + 'emailteachers', 'emailothers', 'protection', 'language', 'timecreated', + 'timemodified']); // The template. $template = new backup_nested_element('template', ['id'], [ From ac2fdda175b9c7d185b7d5686814a7084e03159b Mon Sep 17 00:00:00 2001 From: Mark Nelson Date: Sun, 13 Jul 2025 13:47:13 +0100 Subject: [PATCH 17/23] Removed hack for Oracle databases as they are no longer supported (#701) --- classes/task/issue_certificates_task.php | 14 +++----------- 1 file changed, 3 insertions(+), 11 deletions(-) diff --git a/classes/task/issue_certificates_task.php b/classes/task/issue_certificates_task.php index aad74e4a..fa444b75 100644 --- a/classes/task/issue_certificates_task.php +++ b/classes/task/issue_certificates_task.php @@ -45,24 +45,16 @@ public function get_name(): string { * Execute. */ public function execute() { - global $CFG, $DB; + 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'); $offset = (int)get_config('customcert', 'certificate_offset'); - - if ($CFG->dbtype === 'oci') { - // For Oracle, convert the CLOB to a VARCHAR2 (limiting to 4000 characters) since we are using DISTINCT. - $emailothersselect = "DBMS_LOB.SUBSTR(c.emailothers, 4000, 1) AS emailothers"; - $emailotherslengthsql = "DBMS_LOB.GETLENGTH(c.emailothers)"; - } else { - $emailothersselect = "c.emailothers"; - $emailotherslengthsql = $DB->sql_length('c.emailothers'); - } - + $emailothersselect = "c.emailothers"; $emailotherslengthsql = $DB->sql_length('c.emailothers'); + $sql = "SELECT DISTINCT c.id, c.templateid, c.course, c.requiredtime, c.emailstudents, c.emailteachers, $emailothersselect, ct.id AS templateid, ct.name AS templatename, ct.contextid, co.id AS courseid, co.fullname AS coursefullname, co.shortname AS courseshortname From 1d74bcb7fc4399e26150c88d1ece553976dc676d Mon Sep 17 00:00:00 2001 From: guillaumebarat Date: Wed, 27 Nov 2024 14:45:58 +1000 Subject: [PATCH 18/23] Add setting to add a button to return to course (#655) --- lang/en/customcert.php | 3 +++ settings.php | 5 +++++ view.php | 10 ++++++++++ 3 files changed, 18 insertions(+) diff --git a/lang/en/customcert.php b/lang/en/customcert.php index e5c5a7c5..68724624 100644 --- a/lang/en/customcert.php +++ b/lang/en/customcert.php @@ -203,6 +203,9 @@ $string['refpoint_help'] = 'The reference point is the location of an element from which its x and y coordinates are determined. It is indicated by the \'+\' that appears in the centre or corners of the element.'; $string['replacetemplate'] = 'Replace'; $string['requiredtimenotmet'] = 'You must spend at least a minimum of {$a->requiredtime} minutes in the course before you can access this certificate.'; +$string['returncourse'] = 'Return to course button'; +$string['returncourse_desc'] = 'Display a "return to course" button'; +$string['returncourselabel'] = 'Return to course menu'; $string['rightmargin'] = 'Right margin'; $string['rightmargin_help'] = 'This is the right margin of the certificate PDF in mm.'; $string['save'] = 'Save'; diff --git a/settings.php b/settings.php index 60ca3e4e..22589ae9 100644 --- a/settings.php +++ b/settings.php @@ -78,6 +78,11 @@ get_string('certificateexecutionperiod', 'customcert'), get_string('certificateexecutionperiod_desc', 'customcert'), 365 * DAYSECS)); +$settings->add(new admin_setting_configcheckbox('customcert/returncourse', + get_string('returncourse', 'customcert'), + get_string('returncourse_desc', 'customcert'), + 0)); + $settings->add(new admin_setting_heading('defaults', get_string('modeditdefaults', 'admin'), get_string('condifmodeditdefaults', 'admin'))); diff --git a/view.php b/view.php index f6d76551..7d7a5492 100644 --- a/view.php +++ b/view.php @@ -157,12 +157,19 @@ // Create the button to download the customcert. $downloadbutton = ''; + $renderbuttoncourse = ''; + $displayreturnbutton = get_config('customcert', 'returncourse'); if ($canreceive) { $linkname = get_string('getcustomcert', 'customcert'); $link = new moodle_url('/mod/customcert/view.php', ['id' => $cm->id, 'downloadown' => true]); $downloadbutton = new single_button($link, $linkname, 'get', single_button::BUTTON_PRIMARY); $downloadbutton->class .= ' m-b-1'; // Seems a bit hackish, ahem. $downloadbutton = $OUTPUT->render($downloadbutton); + if ($displayreturnbutton) { + $url = new moodle_url('/course/view.php', ['id' => $course->id]); + $buttonreturntocourse = new single_button($url, get_string('returncourselabel', 'customcert'), 'get', true); + $renderbuttoncourse = $OUTPUT->render($buttonreturntocourse); + } } $numissues = \mod_customcert\certificate::get_number_of_issues($customcert->id, $cm, $groupmode); @@ -187,6 +194,9 @@ echo $issuehtml; echo $downloadbutton; echo $downloadallbutton; + if ($displayreturnbutton) { + echo $renderbuttoncourse; + } if (isset($reporttable)) { echo $OUTPUT->heading(get_string('listofissues', 'customcert', $numissues), 3); groups_print_activity_menu($cm, $pageurl); From df719c4c090c0bbefe33e5a436f385b245fddd07 Mon Sep 17 00:00:00 2001 From: Mark Nelson Date: Sun, 13 Jul 2025 15:15:38 +0100 Subject: [PATCH 19/23] Make the setting description more descriptive (#655) --- lang/en/customcert.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lang/en/customcert.php b/lang/en/customcert.php index 68724624..f4e1d92e 100644 --- a/lang/en/customcert.php +++ b/lang/en/customcert.php @@ -204,7 +204,7 @@ $string['replacetemplate'] = 'Replace'; $string['requiredtimenotmet'] = 'You must spend at least a minimum of {$a->requiredtime} minutes in the course before you can access this certificate.'; $string['returncourse'] = 'Return to course button'; -$string['returncourse_desc'] = 'Display a "return to course" button'; +$string['returncourse_desc'] = 'Display a "Return to course" button on the view certificate page.'; $string['returncourselabel'] = 'Return to course menu'; $string['rightmargin'] = 'Right margin'; $string['rightmargin_help'] = 'This is the right margin of the certificate PDF in mm.'; From 9b462e5413636d16f286be1778c37dc96f2d1969 Mon Sep 17 00:00:00 2001 From: Mark Nelson Date: Sun, 13 Jul 2025 15:25:22 +0100 Subject: [PATCH 20/23] Removed incorrect passing of boolean value no longer used (#655) --- view.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/view.php b/view.php index 7d7a5492..bf38d1c6 100644 --- a/view.php +++ b/view.php @@ -167,7 +167,7 @@ $downloadbutton = $OUTPUT->render($downloadbutton); if ($displayreturnbutton) { $url = new moodle_url('/course/view.php', ['id' => $course->id]); - $buttonreturntocourse = new single_button($url, get_string('returncourselabel', 'customcert'), 'get', true); + $buttonreturntocourse = new single_button($url, get_string('returncourselabel', 'customcert'), 'get'); $renderbuttoncourse = $OUTPUT->render($buttonreturntocourse); } } From 14e91a81e92eccc507fdc36092f1f6f5c8b33421 Mon Sep 17 00:00:00 2001 From: Mark Nelson Date: Sun, 13 Jul 2025 15:36:19 +0100 Subject: [PATCH 21/23] Do not refer to the course page as a menu (#655) --- lang/en/customcert.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lang/en/customcert.php b/lang/en/customcert.php index f4e1d92e..70108186 100644 --- a/lang/en/customcert.php +++ b/lang/en/customcert.php @@ -205,7 +205,7 @@ $string['requiredtimenotmet'] = 'You must spend at least a minimum of {$a->requiredtime} minutes in the course before you can access this certificate.'; $string['returncourse'] = 'Return to course button'; $string['returncourse_desc'] = 'Display a "Return to course" button on the view certificate page.'; -$string['returncourselabel'] = 'Return to course menu'; +$string['returncourselabel'] = 'Return to course page'; $string['rightmargin'] = 'Right margin'; $string['rightmargin_help'] = 'This is the right margin of the certificate PDF in mm.'; $string['save'] = 'Save'; From 0637ca10fe3475471945caee8eab4410e0a4d6e9 Mon Sep 17 00:00:00 2001 From: Giorgio Consorti Date: Fri, 13 Oct 2023 12:24:58 +0200 Subject: [PATCH 22/23] Implement new feature keeplocalcopy: - add keeplocalcopy field to the customcert table - add managekeeplocalcopy, deletelocalcopy capabilities - add keeplocalcopy to the backup structure - add keeplocalcopy setting element to forms - add localfile class to manage local PDF files - add logic to store and serve local PDF files - add deletelocalcopy to the actions column - add deletelocalcopy logic - add 'download all certificates' link to nav menus - add file to download all certificates - italian translation for new lang strings --- backup/moodle2/backup_customcert_stepslib.php | 2 +- classes/localfile.php | 249 ++++++++++++++++++ classes/report_table.php | 40 ++- classes/template.php | 18 +- db/access.php | 20 ++ db/install.xml | 1 + db/upgrade.php | 11 + downloadcerts.php | 108 ++++++++ lang/en/customcert.php | 7 + lang/it/customcert.php | 31 +++ lib.php | 72 +++++ mod_form.php | 8 + settings.php | 3 + version.php | 2 +- view.php | 38 +-- 15 files changed, 585 insertions(+), 25 deletions(-) create mode 100644 classes/localfile.php create mode 100644 downloadcerts.php create mode 100644 lang/it/customcert.php diff --git a/backup/moodle2/backup_customcert_stepslib.php b/backup/moodle2/backup_customcert_stepslib.php index c3e61020..d2fecac6 100644 --- a/backup/moodle2/backup_customcert_stepslib.php +++ b/backup/moodle2/backup_customcert_stepslib.php @@ -42,7 +42,7 @@ protected function define_structure() { $customcert = new backup_nested_element('customcert', ['id'], [ 'templateid', 'name', 'intro', 'introformat', 'requiredtime', 'verifyany', 'deliveryoption', 'usecustomfilename', 'customfilenamepattern', 'emailstudents', - 'emailteachers', 'emailothers', 'protection', 'language', 'timecreated', + 'emailteachers', 'emailothers', 'protection', 'language', 'keeplocalcopy', 'timecreated', 'timemodified']); // The template. diff --git a/classes/localfile.php b/classes/localfile.php new file mode 100644 index 00000000..e4102bd1 --- /dev/null +++ b/classes/localfile.php @@ -0,0 +1,249 @@ +. + +/** + * Class represents a local file of an issued certificate. + * + * @package mod_customcert + * @copyright 2023 Giorgio Consorti + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace mod_customcert; + +use file_exception; + +defined('MOODLE_INTERNAL') || die(); + +/** + * Class represents a local file of an issued certificate. + * + * @package mod_customcert + * @copyright 023 Giorgio Consorti + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class localfile { + + /** + * The template representing the content of the file. + * + * @var \mod_customcert\template + */ + protected $template; + + /** + * The component name for the file storage. + */ + const component = 'mod_customcert'; + + /** + * The filearea name for the file storage. + */ + const filearea = 'customcert_issues'; + + /** + * The constructor. + * + * @param \mod_customcert\template $template + */ + public function __construct(\mod_customcert\template $template) { + $this->template = $template; + } + + /** + * Save the PDF to the file storage. + * + * @param string $pdfcontent string content of the pdf + * @param integer|null $userid the id of the user whose certificate we want to save + * @return stored_file|false the stored_file object on success, false on error + */ + public function savePDF(string $pdfcontent, ?int $userid = null) { + global $CFG, $USER; + require_once($CFG->libdir . '/filelib.php'); + + if (empty($userid)) { + $userid = $USER->id; + } + + try { + $file = $this->getPDF($userid); + if (!$file) { + // Create file containing the pdf + $fs = get_file_storage(); + $file = $fs->create_file_from_string($this->buildFileInfo($userid), $pdfcontent); + } + return $file; + } catch (file_exception $e) { + // maybe log the exception + return false; + } + } + + /** + * Get the PDF from the file storage. + * + * @param integer|null $userid the id of the user whose certificate we want to get + * @return \stored_file|false the stored_file object on success, false on error + */ + public function getPDF(?int $userid = null) { + global $CFG, $USER; + require_once($CFG->libdir . '/filelib.php'); + + if (empty($userid)) { + $userid = $USER->id; + } + + $fileinfo = $this->buildFileInfo($userid); + $fs = get_file_storage(); + return $fs->get_file($fileinfo['contextid'], $fileinfo['component'], $fileinfo['filearea'], + $fileinfo['itemid'], $fileinfo['filepath'], $fileinfo['filename']); + } + + /** + * Delete the PDF from the file storage. + * + * @param integer|null $userid the id of the user whose certificate we want to get + * @return bool true on success + */ + public function deletePDF(?int $userid = null) { + global $USER; + + if (empty($userid)) { + $userid = $USER->id; + } + + try { + $file = $this->getPDF($userid); + if ($file) { + return $file->delete(); + } + return false; + } catch (file_exception $e) { + // maybe log the exception + return false; + } + } + + /** + * Send the PDF to the browser or return it as a string. + * + * @param int $userid the id of the user whose certificate we want to view + * @param string $deliveryoption the delivery option of the customcert + * @param bool $return Do we want to return the contents of the PDF? + * @return string|void Can return the PDF in string format if specified. + */ + public function sendPDF(?int $userid = NULL, string $deliveryoption = certificate::DELIVERY_OPTION_DOWNLOAD, bool $return = false) { + global $USER; + + if (empty($userid)) { + $userid = $USER->id; + } + + $file = $this->getPDF($userid); + if ($file) { + if ($return) { + return $file->get_content(); + } else { + // send the file to the browser + send_stored_file( + $file, + 0, + 0, + $deliveryoption == certificate::DELIVERY_OPTION_DOWNLOAD, + ['filename' => $file->get_filename()] + ); + die(); + } + } + } + + /** + * Check if a pdf exists in the file storage area. + * + * @param \stdClass $cm the course module + * @param integer|null $userid the id of the user whose PDF we want to check + * @param integer|null $templateid the template id of the customcert we want to check + * @return \stored_file|false the stored_file object on success, false on error + */ + public static function existsPDF($cm, ?int $userid = null, ?int $templateid = null) { + + $fileinfo = self::buildFileInfoArr($cm, $userid, $templateid); + $fs = get_file_storage(); + return $fs->get_file($fileinfo['contextid'], $fileinfo['component'], $fileinfo['filearea'], + $fileinfo['itemid'], $fileinfo['filepath'], $fileinfo['filename']); + } + + /** + * Build the fileinfo array needed by the file storage. + * + * @param integer|null $userid the id of the user whose fileinfo array we want to generate + * @return array the fileinfo array + */ + protected function buildFileInfo(?int $userid = null) { + + return self::buildFileInfoArr($this->template->get_cm(), $userid, $this->template->get_id()); + } + + /** + * Build the fileinfo array needed by the file storage, static version. + * + * @param \stdClass $cm the course module + * @param integer|null $userid the id of the user whose fileinfo array we want to generate + * @param integer|null $templateid the template id of the customcert of the array we want to generate + * @return array the fileinfo array + */ + private static function buildFileInfoArr ($cm, ?int $userid = null, ?int $templateid = null) { + + /** @var \moodle_database $DB */ + global $DB, $USER; + + if (empty($userid)) { + $userid = $USER->id; + } + + if (empty($templateid)) { + $customcert = $DB->get_record('customcert', array('id' => $cm->instance), '*', MUST_EXIST); + $templateid = $customcert->templateid; + } + + $course = $DB->get_record('course', ['id' => $cm->course]); + $context = $DB->get_record('context', ['contextlevel' => '50', 'instanceid' => $course->id]); + $user_info = $DB->get_record('user', ['id' => $userid]); + + return [ + 'contextid' => $context->id, + 'component' => self::component, + 'filearea' => self::filearea, + 'itemid' => $templateid, + 'userid' => $USER->id, + 'author' => fullname($USER), + 'filepath' => '/' . $course->id . '/', + 'filename' => self::buildFileName($user_info->username, $templateid, $course->shortname), + ]; + } + + /** + * Build the PDF filename. + * + * @param string $username + * @param string $templateid + * @param string $courseShortname + * @return string the PDF file name + */ + public static function buildFileName($username, $templateid, $courseShortname) { + return $username . '_cert-' . $templateid . '_course-' . $courseShortname . '.pdf'; + } +} diff --git a/classes/report_table.php b/classes/report_table.php index 6405e3f3..f27081c9 100644 --- a/classes/report_table.php +++ b/classes/report_table.php @@ -212,16 +212,42 @@ public function col_download($user) { public function col_actions($user) { global $OUTPUT; - $icon = new \pix_icon('i/delete', get_string('delete')); - $link = new \moodle_url('/mod/customcert/view.php', + $actions = [ [ - 'id' => $this->cm->id, - 'deleteissue' => $user->issueid, - 'sesskey' => sesskey(), + 'icon' => new \pix_icon('i/delete', get_string('delete')), + 'link' => new \moodle_url( + '/mod/customcert/view.php', + [ + 'id' => $this->cm->id, + 'deleteissue' => $user->issueid, + 'sesskey' => sesskey() + ] + ), + 'attributes' => ['class' => 'action-icon delete-icon'], ] - ); + ]; + + if (has_capability('mod/customcert:deletelocalcopy', \context_module::instance($this->cm->id)) && localfile::existsPDF($this->cm, $user->id)) { + $actions[] = [ + 'icon' => new \pix_icon('deletelocalcopy', get_string('deletelocalcopy', 'customcert'), 'customcert'), + 'link' => new \moodle_url( + '/mod/customcert/view.php', + [ + 'id' => $this->cm->id, + 'deleteissue' => $user->issueid, + 'deletelocalcopy' => 1, + 'sesskey' => sesskey() + ] + ), + 'attributes' => ['class' => 'action-icon deletelocalcopy-icon'], + ]; + } - return $OUTPUT->action_icon($link, $icon, null, ['class' => 'action-icon delete-icon']); + return implode(" ", array_map( + fn ($action) => + $OUTPUT->action_icon($action['link'], $action['icon'], null, $action['attributes'] ?? []), + $actions + )); } /** diff --git a/classes/template.php b/classes/template.php index 870ffc44..4a3d883e 100644 --- a/classes/template.php +++ b/classes/template.php @@ -48,6 +48,11 @@ class template { */ protected $contextid; + /** + * @var \mod_customcert\localfile the local file for the template. + */ + protected $localfile; + /** * The constructor. * @@ -57,6 +62,7 @@ public function __construct($template) { $this->id = $template->id; $this->name = $template->name; $this->contextid = $template->contextid; + $this->localfile = new localfile($this); } /** @@ -314,7 +320,13 @@ public function generate_pdf(bool $preview = false, ?int $userid = null, bool $r $deliveryoption = $customcert->deliveryoption; } - // Set up PDF document properties — no header/footer, auto page break. + if ($customcert->keeplocalcopy) { + $retval = $this->localfile->sendPDF($userid, $deliveryoption, $return); + if ($return && !empty($retval)) { + return $retval; + } + } + $pdf->setPrintHeader(false); $pdf->setPrintFooter(false); $pdf->SetAutoPageBreak(true, 0); @@ -413,6 +425,10 @@ public function generate_pdf(bool $preview = false, ?int $userid = null, bool $r } } + if ($customcert->keeplocalcopy) { + $this->localfile->savePDF($pdf->Output('', 'S'), $userid); + } + if ($return) { return $pdf->Output('', 'S'); } diff --git a/db/access.php b/db/access.php index 51966b02..b2f986fc 100644 --- a/db/access.php +++ b/db/access.php @@ -104,6 +104,26 @@ ], ], + 'mod/customcert:managekeeplocalcopy' => [ + 'captype' => 'write', + 'contextlevel' => CONTEXT_COURSE, + 'archetypes' => array( + 'editingteacher' => CAP_ALLOW, + 'manager' => CAP_ALLOW + ), + 'clonepermissionsfrom' => 'moodle/course:manageactivities' + ], + + 'mod/customcert:deletelocalcopy' => [ + 'captype' => 'write', + 'contextlevel' => CONTEXT_COURSE, + 'archetypes' => array( + 'editingteacher' => CAP_ALLOW, + 'manager' => CAP_ALLOW + ), + 'clonepermissionsfrom' => 'moodle/course:manageactivities' + ], + 'mod/customcert:manageemailstudents' => [ 'captype' => 'write', 'contextlevel' => CONTEXT_COURSE, diff --git a/db/install.xml b/db/install.xml index 9dff73d2..5517675d 100644 --- a/db/install.xml +++ b/db/install.xml @@ -24,6 +24,7 @@ + diff --git a/db/upgrade.php b/db/upgrade.php index 43b873dc..186ed062 100644 --- a/db/upgrade.php +++ b/db/upgrade.php @@ -335,5 +335,16 @@ function xmldb_customcert_upgrade($oldversion) { upgrade_mod_savepoint(true, 2025041401, 'customcert'); } + if ($oldversion < 2025041402) { + $table = new xmldb_table('customcert'); + $field = new xmldb_field('keeplocalcopy', XMLDB_TYPE_INTEGER, '1', null, XMLDB_NOTNULL, null, '0', 'language'); + + if (!$dbman->field_exists($table, $field)) { + $dbman->add_field($table, $field); + } + + upgrade_mod_savepoint(true, 2025041402, 'customcert'); + } + return true; } diff --git a/downloadcerts.php b/downloadcerts.php new file mode 100644 index 00000000..d6779317 --- /dev/null +++ b/downloadcerts.php @@ -0,0 +1,108 @@ +. + +/** + * Handles zip and download of certificates. + * + * Derived from the local_bulkcustomcert by Gonzalo Romero. + * + * @package mod_customcert + * @author Gonzalo Romero + * @author Giorgio Consorti + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +// defined('MOODLE_INTERNAL') || die(); + +require_once(__DIR__ . '/../../config.php'); + +$courseid = optional_param('courseid', null, PARAM_INT); +$customcertid = optional_param('customcertid', null, PARAM_INT); + +if (!has_capability('mod/customcert:viewallcertificates', context_system::instance()) && !$courseid && !$customcert) { + die(); +} + +/** @var \moodle_database $DB */ +global $DB; + +// Increase the server timeout to handle the creation and sending of large zip files. +core_php_time_limit::raise(); + +if ($courseid) { + $course = $DB->get_record('course', ['id' => $courseid]); + $certs = $DB->get_records('customcert', ['course' => $courseid]); +} else if ($customcertid) { + $cert = $DB->get_record('customcert', ['id' => $customcertid], '*', MUST_EXIST); + $certs[$cert->id] = $cert; + $course = $DB->get_record('course', ['id' => $certs[$cert->id]->course], '*', MUST_EXIST); + $courseid = $course->id; + unset($cert); +} + +// Build a list of files to zip. +$filesforzipping = []; + +foreach ($certs as $certid => $cert_fields) { + $issues = $DB->get_records('customcert_issues', ['customcertid' => $certid]); + list($userssql, $params) = $DB->get_in_or_equal(array_map(fn($i) => $i->userid, $issues), SQL_PARAMS_NAMED); + $usersObjs = $DB->get_records_select('user', "id {$userssql}", $params); + $template = $DB->get_record('customcert_templates', ['id' => $cert_fields->templateid], '*', MUST_EXIST); + $template = new \mod_customcert\template($template); + $lf = new \mod_customcert\localfile($template); + foreach ($issues as $issue) { + if (false === $file = $lf->getPDF($issue->userid)) { + // must generate the pdf + $pdf = $template->generate_pdf(false, $issue->userid, true); + if (!empty($pdf)) { + if ($cert_fields->keeplocalcopy) { + $file = $lf->getPDF($issue->userid); + } else { + $file = [ + 'content' => $pdf, + ]; + } + } + } + if ($file) { + $filename = \mod_customcert\localfile::buildFileName($usersObjs[$issue->userid]->username, $template->get_id(), $course->shortname); + $filesforzipping['/' . $course->shortname . '/' . $cert_fields->name . '/' . $filename] = $file; + } + } +} + +if (count($filesforzipping) == 0) { + // This should never happen. The option only show up if there is available certs. + $url = new moodle_url('/course/view.php?id=' . $courseid); + redirect($url); +} else if ($zipfile = pack_files($filesforzipping)) { + send_temp_file($zipfile, get_string('modulenameplural', 'customcert') . '-' . $course->shortname . '.zip'); +} +die(); + + +function pack_files($filesforzipping) +{ + global $CFG; + // Create path for new zip file. + $tempzip = tempnam($CFG->tempdir . '/', 'customcert_'); + // Zip files. + $zipper = new zip_packer(); + if ($zipper->archive_to_pathname($filesforzipping, $tempzip)) { + return $tempzip; + } + return false; +} diff --git a/lang/en/customcert.php b/lang/en/customcert.php index 70108186..00e5b2e8 100644 --- a/lang/en/customcert.php +++ b/lang/en/customcert.php @@ -49,6 +49,8 @@ $string['createtemplate'] = 'Create template'; $string['customcert:addinstance'] = 'Add a new custom certificate instance'; $string['customcert:manage'] = 'Manage a custom certificate'; +$string['customcert:managekeeplocalcopy'] = 'Manage keep local certificate copy setting'; +$string['customcert:deletelocalcopy'] = 'Delete local certificate copies'; $string['customcert:manageemailothers'] = 'Manage email others setting'; $string['customcert:manageemailstudents'] = 'Manage email students setting'; $string['customcert:manageemailteachers'] = 'Manage email teachers setting'; @@ -70,6 +72,7 @@ $string['deleteelement'] = 'Delete element'; $string['deleteelementconfirm'] = 'Are you sure you want to delete this element?'; $string['deleteissueconfirm'] = 'Are you sure you want to delete this certificate issue?'; +$string['deletelocalcopyconfirm'] = 'Are you sure you want to delete this certificate PDF file?'; $string['deleteissuedcertificates'] = 'Delete issued certificates'; $string['deletepageconfirm'] = 'Are you sure you want to delete this certificate page?'; $string['deletetemplateconfirm'] = 'Are you sure you want to delete this certificate template?'; @@ -110,6 +113,10 @@ $string['emailstudentgreeting'] = 'Dear {$a}'; $string['emailstudents'] = 'Email students'; $string['emailstudents_help'] = 'If set this will email the students a copy of the certificate when it becomes available. Warning: Setting this to \'Yes\' before you have finished creating the certificate will email the student an incomplete certificate.'; +$string['keeplocalcopy'] = 'Keep local certificate copy'; +$string['keeplocalcopy_help'] = 'If set this will keep a local copy of the certificate PDF. The copy will be served on all future certificate downloads until the file is deleted from the moodle files table.'; +$string['deletelocalcopy'] = 'Delete local certificate copy'; +$string['bulkdownloadlink'] = 'Download certificates'; $string['emailstudentsubject'] = '{$a->coursefullname}: {$a->certificatename}'; $string['emailteachers'] = 'Email teachers'; $string['emailteachers_help'] = 'If set this will email the teachers a copy of the certificate when it becomes available. Warning: Setting this to \'Yes\' before you have finished creating the certificate will email the teacher an incomplete certificate.'; diff --git a/lang/it/customcert.php b/lang/it/customcert.php new file mode 100644 index 00000000..0a8753e5 --- /dev/null +++ b/lang/it/customcert.php @@ -0,0 +1,31 @@ +. + +/** + * Language strings for the customcert module. + * + * @package mod_customcert + * @copyright 2013 Mark Nelson + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + + $string['keeplocalcopy'] = 'Mantieni copia dei certificati'; + $string['keeplocalcopy_help'] = 'Se impostato a Sì, mantiene una copia del PDF dei certificati. La copia sarà scaricata ad ogni futura richiesta del certificato, finché il file non è cancellato dalla tabella files di moodle.'; + $string['deletelocalcopy'] = 'Cancella copia locale del certificato'; + $string['bulkdownloadlink'] = 'Download certificati'; + $string['deletelocalcopyconfirm'] = 'Sei sicuro di voler cancellare il file PDF del certificato?'; + $string['customcert:managekeeplocalcopy'] = 'Gestire impostazioni copia locale certificati'; + $string['customcert:deletelocalcopy'] = 'Cancella copia locale dei certificati'; diff --git a/lib.php b/lib.php index 69afda51..b2c4a084 100644 --- a/lib.php +++ b/lib.php @@ -326,6 +326,27 @@ function mod_customcert_output_fragment_editelement($args) { return $form->render(); } +/** + * This function extends the course navigation block for the site. + * + * @param \navigation_node $parentnode + * @param \stdClass $course + * @param \context_course $context + */ +function customcert_extend_navigation_course(\navigation_node $parentnode, \stdClass $course, \context_course $context) { + global $PAGE; + + $addnode = $context->contextlevel === 50; + $addnode = $addnode && !($context->instanceid === SITEID); + $addnode = $addnode && has_capability('mod/customcert:viewallcertificates', $context); + $isCourseNav = !is_null($PAGE->cm) && is_null($PAGE->cm->instance); + if ($addnode && $isCourseNav) { + if ($node = build_downloadall_node($isCourseNav, $course, $context)) { + $parentnode->add_node($node); + } + } +} + /** * This function extends the settings navigation block for the site. * @@ -357,6 +378,12 @@ function customcert_extend_settings_navigation(settings_navigation $settings, na $customcertnode->add_node($node, $beforekey); } + if (has_capability('mod/customcert:viewallcertificates', $PAGE->cm->context)) { + if ($node = build_downloadall_node(false, $PAGE->cm->get_course(), null)) { + $customcertnode->add_node($node, $beforekey); + } + } + if (has_capability('mod/customcert:verifycertificate', $settings->get_page()->cm->context)) { $node = navigation_node::create(get_string('verifycertificate', 'customcert'), new moodle_url('/mod/customcert/verify_certificate.php', ['contextid' => $settings->get_page()->cm->context->id]), @@ -368,6 +395,50 @@ function customcert_extend_settings_navigation(settings_navigation $settings, na return $customcertnode->trim_if_empty(); } +/** + * Build the navigation node for the 'Download certificates' item. + * + * @param boolean $isCourseNav + * @param \stdClass $course + * @param \course_context $context + * @return \navigation_node|null null if there are no certifcates available for the download + */ +function build_downloadall_node(bool $isCourseNav, stdClass $course, \context_course $context = null) { + + global $DB, $PAGE; + + if (!$isCourseNav) { + $courseContext = \context_course::instance($course->id); + //Check if there is available certs + $certs = $DB->get_records('customcert', ['id' => $PAGE->cm->instance]); + $users = $DB->get_records('role_assignments', ['contextid' => $courseContext->id]); + $urlparams = ['customcertid' => $PAGE->cm->instance]; + } else { + $certs = $DB->get_records('customcert', ['course' => $context->instanceid]); + $users = $DB->get_records('role_assignments', ['contextid' => $context->id]); + $urlparams = ['courseid' => $course->id]; + } + $availablecerts = false; + foreach ($certs as $certid => $cert_fields) { + foreach ($users as $userid => $user_fields) { + if (!$DB->get_record('customcert_issues', ['userid' => $user_fields->userid, 'customcertid' => $certid])) { + continue; + } + $availablecerts = true; + break; + } + if ($availablecerts) break; + } + if ($availablecerts) { + $node = \navigation_node::create(get_string('bulkdownloadlink', 'mod_customcert'), + new \moodle_url('/mod/customcert/downloadcerts.php', $urlparams), + \navigation_node::TYPE_SETTING, null, 'mod_customcert_downloadcerts', + new \pix_icon('a/download_all', 'certificates')); + return $node; + } + return null; +} + /** * Add nodes to myprofile page. * @@ -441,6 +512,7 @@ function mod_customcert_inplace_editable($itemtype, $itemid, $newvalue) { */ function mod_customcert_get_fontawesome_icon_map() { return [ + 'mod_customcert:deletelocalcopy' => 'fa-minus-square', 'mod_customcert:download' => 'fa-download', ]; } diff --git a/mod_form.php b/mod_form.php index 448fbba3..9e7a02b8 100644 --- a/mod_form.php +++ b/mod_form.php @@ -77,6 +77,13 @@ public function definition() { $mform->addHelpButton('customfilenamepattern', 'customfilenamepattern', 'customcert'); $mform->disabledIf('customfilenamepattern', 'usecustomfilename', 'notchecked'); + if (has_capability('mod/customcert:managekeeplocalcopy', $this->get_context())) { + $mform->addElement('selectyesno', 'keeplocalcopy', get_string('keeplocalcopy', 'customcert')); + $mform->setDefault('keeplocalcopy', get_config('customcert', 'keeplocalcopy')); + $mform->addHelpButton('keeplocalcopy', 'keeplocalcopy', 'customcert'); + $mform->setType('keeplocalcopy', PARAM_INT); + } + if (has_capability('mod/customcert:manageemailstudents', $this->get_context())) { $mform->addElement('selectyesno', 'emailstudents', get_string('emailstudents', 'customcert')); $mform->setDefault('emailstudents', get_config('customcert', 'emailstudents')); @@ -218,6 +225,7 @@ public function validation($data, $files) { */ protected function get_options_elements_with_required_caps() { return [ + 'keeplocalcopy' => 'mod/customcert:managekeeplocalcopy', 'emailstudents' => 'mod/customcert:manageemailstudents', 'emailteachers' => 'mod/customcert:manageemailteachers', 'emailothers' => 'mod/customcert:manageemailothers', diff --git a/settings.php b/settings.php index 22589ae9..74e6c745 100644 --- a/settings.php +++ b/settings.php @@ -101,6 +101,9 @@ 0 => get_string('no'), 1 => get_string('yes'), ]; + +$settings->add(new admin_setting_configselect('customcert/keeplocalcopy', + get_string('keeplocalcopy', 'customcert'), get_string('keeplocalcopy_help', 'customcert'), 0, $yesnooptions)); $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', diff --git a/version.php b/version.php index b68819c1..b595f0dc 100644 --- a/version.php +++ b/version.php @@ -24,7 +24,7 @@ defined('MOODLE_INTERNAL') || die('Direct access to this script is forbidden.'); -$plugin->version = 2025041401; // The current module version (Date: YYYYMMDDXX). +$plugin->version = 2025041402; // The current module version (Date: YYYYMMDDXX). $plugin->requires = 2025041400; // Requires this Moodle version (5.0). $plugin->cron = 0; // Period for cron to check this module (secs). $plugin->component = 'mod_customcert'; diff --git a/view.php b/view.php index bf38d1c6..e6ba5d81 100644 --- a/view.php +++ b/view.php @@ -30,6 +30,7 @@ $downloadtable = optional_param('download', null, PARAM_ALPHA); $downloadissue = optional_param('downloadissue', 0, PARAM_INT); $deleteissue = optional_param('deleteissue', 0, PARAM_INT); +$deletelocalcopy = optional_param('deletelocalcopy', 0, PARAM_INT); $confirm = optional_param('confirm', false, PARAM_BOOL); $page = optional_param('page', 0, PARAM_INT); $perpage = optional_param('perpage', \mod_customcert\certificate::CUSTOMCERT_PER_PAGE, PARAM_INT); @@ -47,6 +48,7 @@ $canreceive = has_capability('mod/customcert:receiveissue', $context); $canmanage = has_capability('mod/customcert:manage', $context); $canviewreport = has_capability('mod/customcert:viewreport', $context); +$candeletelocalcopy = has_capability('mod/customcert:deletelocalcopy', $context); // Initialise $PAGE. $pageurl = new moodle_url('/mod/customcert/view.php', ['id' => $cm->id]); @@ -71,14 +73,20 @@ [ 'id' => $id, 'deleteissue' => $deleteissue, + 'deletelocalcopy' => $deletelocalcopy, 'confirm' => 1, 'sesskey' => sesskey(), ] ); // Show a confirmation page. - $PAGE->navbar->add(get_string('deleteconfirm', 'customcert')); - $message = get_string('deleteissueconfirm', 'customcert'); + if ($deletelocalcopy) { + $PAGE->navbar->add(get_string('deleteconfirm', 'customcert')); + $message = get_string('deletelocalcopyconfirm', 'customcert'); + } else if ($deleteissue) { + $PAGE->navbar->add(get_string('deleteconfirm', 'customcert')); + $message = get_string('deleteissueconfirm', 'customcert'); + } echo $OUTPUT->header(); echo $OUTPUT->heading(format_string($customcert->name)); echo $OUTPUT->confirm($message, $yesurl, $nourl); @@ -86,19 +94,19 @@ exit(); } - // Delete the issue. - $issue = $DB->get_record('customcert_issues', ['id' => $deleteissue, 'customcertid' => $customcert->id], '*', MUST_EXIST); - $DB->delete_records('customcert_issues', ['id' => $deleteissue, 'customcertid' => $customcert->id]); - - // Trigger event. - $cm = get_coursemodule_from_instance('customcert', $customcert->id, 0, false, MUST_EXIST); - $context = \context_module::instance($cm->id); - $event = \mod_customcert\event\issue_deleted::create([ - 'objectid' => $issue->id, - 'context' => $context, - 'relateduserid' => $issue->userid, - ]); - $event->trigger(); + // Always delete local copy. + if ($candeletelocalcopy) { + $issues = $DB->get_records('customcert_issues', ['id' => $deleteissue, 'customcertid' => $customcert->id]); + if (!empty($issues)) { + $lf = new \mod_customcert\localfile(new \mod_customcert\template($template)); + array_map(fn($issue) => $lf->deletePDF($issue->userid), $issues); + } + } + + if (!$deletelocalcopy) { + // Delete the issue. + $DB->delete_records('customcert_issues', ['id' => $deleteissue, 'customcertid' => $customcert->id]); + } // Redirect back to the manage templates page. redirect(new moodle_url('/mod/customcert/view.php', ['id' => $id])); From ab5e388f6787103bb5777024c405992104792506 Mon Sep 17 00:00:00 2001 From: Giorgio Consorti Date: Sun, 20 Jul 2025 22:14:15 +0200 Subject: [PATCH 23/23] rename methods, add return types and remove downloadcerts custom feature --- classes/localfile.php | 67 +++++++++++++----------- classes/report_table.php | 2 +- classes/template.php | 4 +- downloadcerts.php | 108 --------------------------------------- lib.php | 71 ------------------------- view.php | 19 +++++-- 6 files changed, 55 insertions(+), 216 deletions(-) delete mode 100644 downloadcerts.php diff --git a/classes/localfile.php b/classes/localfile.php index e4102bd1..724da911 100644 --- a/classes/localfile.php +++ b/classes/localfile.php @@ -32,27 +32,25 @@ * Class represents a local file of an issued certificate. * * @package mod_customcert - * @copyright 023 Giorgio Consorti + * @copyright 2023 Giorgio Consorti * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class localfile { /** - * The template representing the content of the file. - * - * @var \mod_customcert\template + * @var \mod_customcert\template the template representing the content of the file. */ protected $template; /** * The component name for the file storage. */ - const component = 'mod_customcert'; + private const component = 'mod_customcert'; /** * The filearea name for the file storage. */ - const filearea = 'customcert_issues'; + private const filearea = 'customcert_issues'; /** * The constructor. @@ -67,10 +65,11 @@ public function __construct(\mod_customcert\template $template) { * Save the PDF to the file storage. * * @param string $pdfcontent string content of the pdf - * @param integer|null $userid the id of the user whose certificate we want to save - * @return stored_file|false the stored_file object on success, false on error + * @param int|null $userid the id of the user whose certificate we want to save + * + * @return \stored_file|bool the stored_file object on success, false on error */ - public function savePDF(string $pdfcontent, ?int $userid = null) { + public function save_pdf(string $pdfcontent, ?int $userid = null): \stored_file|bool { global $CFG, $USER; require_once($CFG->libdir . '/filelib.php'); @@ -79,7 +78,7 @@ public function savePDF(string $pdfcontent, ?int $userid = null) { } try { - $file = $this->getPDF($userid); + $file = $this->get_pdf($userid); if (!$file) { // Create file containing the pdf $fs = get_file_storage(); @@ -95,10 +94,11 @@ public function savePDF(string $pdfcontent, ?int $userid = null) { /** * Get the PDF from the file storage. * - * @param integer|null $userid the id of the user whose certificate we want to get - * @return \stored_file|false the stored_file object on success, false on error + * @param int|null $userid the id of the user whose certificate we want to get + * + * @return \stored_file|bool the stored_file object on success, false on error */ - public function getPDF(?int $userid = null) { + public function get_pdf(?int $userid = null): \stored_file|bool { global $CFG, $USER; require_once($CFG->libdir . '/filelib.php'); @@ -115,10 +115,11 @@ public function getPDF(?int $userid = null) { /** * Delete the PDF from the file storage. * - * @param integer|null $userid the id of the user whose certificate we want to get + * @param int|null $userid the id of the user whose certificate we want to get + * * @return bool true on success */ - public function deletePDF(?int $userid = null) { + public function delete_pdf(?int $userid = null): bool { global $USER; if (empty($userid)) { @@ -126,13 +127,12 @@ public function deletePDF(?int $userid = null) { } try { - $file = $this->getPDF($userid); + $file = $this->get_pdf($userid); if ($file) { return $file->delete(); } return false; } catch (file_exception $e) { - // maybe log the exception return false; } } @@ -143,16 +143,17 @@ public function deletePDF(?int $userid = null) { * @param int $userid the id of the user whose certificate we want to view * @param string $deliveryoption the delivery option of the customcert * @param bool $return Do we want to return the contents of the PDF? - * @return string|void Can return the PDF in string format if specified. + * + * @return string|null Can return the PDF in string format if specified. */ - public function sendPDF(?int $userid = NULL, string $deliveryoption = certificate::DELIVERY_OPTION_DOWNLOAD, bool $return = false) { + public function send_pdf(?int $userid = NULL, string $deliveryoption = certificate::DELIVERY_OPTION_DOWNLOAD, bool $return = false): string|null { global $USER; if (empty($userid)) { $userid = $USER->id; } - $file = $this->getPDF($userid); + $file = $this->get_pdf($userid); if ($file) { if ($return) { return $file->get_content(); @@ -168,19 +169,21 @@ public function sendPDF(?int $userid = NULL, string $deliveryoption = certificat die(); } } + return null; } /** * Check if a pdf exists in the file storage area. * * @param \stdClass $cm the course module - * @param integer|null $userid the id of the user whose PDF we want to check - * @param integer|null $templateid the template id of the customcert we want to check + * @param int|null $userid the id of the user whose PDF we want to check + * @param int|null $templateid the template id of the customcert we want to check + * * @return \stored_file|false the stored_file object on success, false on error */ - public static function existsPDF($cm, ?int $userid = null, ?int $templateid = null) { + public static function does_pdf_exist($cm, ?int $userid = null, ?int $templateid = null): \stored_file|bool { - $fileinfo = self::buildFileInfoArr($cm, $userid, $templateid); + $fileinfo = self::build_file_info($cm, $userid, $templateid); $fs = get_file_storage(); return $fs->get_file($fileinfo['contextid'], $fileinfo['component'], $fileinfo['filearea'], $fileinfo['itemid'], $fileinfo['filepath'], $fileinfo['filename']); @@ -189,23 +192,25 @@ public static function existsPDF($cm, ?int $userid = null, ?int $templateid = nu /** * Build the fileinfo array needed by the file storage. * - * @param integer|null $userid the id of the user whose fileinfo array we want to generate + * @param int|null $userid the id of the user whose fileinfo array we want to generate + * * @return array the fileinfo array */ - protected function buildFileInfo(?int $userid = null) { + protected function buildFileInfo(?int $userid = null): array { - return self::buildFileInfoArr($this->template->get_cm(), $userid, $this->template->get_id()); + return self::build_file_info($this->template->get_cm(), $userid, $this->template->get_id()); } /** * Build the fileinfo array needed by the file storage, static version. * * @param \stdClass $cm the course module - * @param integer|null $userid the id of the user whose fileinfo array we want to generate - * @param integer|null $templateid the template id of the customcert of the array we want to generate + * @param int|null $userid the id of the user whose fileinfo array we want to generate + * @param int|null $templateid the template id of the customcert of the array we want to generate + * * @return array the fileinfo array */ - private static function buildFileInfoArr ($cm, ?int $userid = null, ?int $templateid = null) { + private static function build_file_info ($cm, ?int $userid = null, ?int $templateid = null): array { /** @var \moodle_database $DB */ global $DB, $USER; @@ -243,7 +248,7 @@ private static function buildFileInfoArr ($cm, ?int $userid = null, ?int $templa * @param string $courseShortname * @return string the PDF file name */ - public static function buildFileName($username, $templateid, $courseShortname) { + public static function buildFileName($username, $templateid, $courseShortname): string { return $username . '_cert-' . $templateid . '_course-' . $courseShortname . '.pdf'; } } diff --git a/classes/report_table.php b/classes/report_table.php index f27081c9..33f87bf9 100644 --- a/classes/report_table.php +++ b/classes/report_table.php @@ -227,7 +227,7 @@ public function col_actions($user) { ] ]; - if (has_capability('mod/customcert:deletelocalcopy', \context_module::instance($this->cm->id)) && localfile::existsPDF($this->cm, $user->id)) { + if (has_capability('mod/customcert:deletelocalcopy', \context_module::instance($this->cm->id)) && localfile::does_pdf_exist($this->cm, $user->id)) { $actions[] = [ 'icon' => new \pix_icon('deletelocalcopy', get_string('deletelocalcopy', 'customcert'), 'customcert'), 'link' => new \moodle_url( diff --git a/classes/template.php b/classes/template.php index 4a3d883e..cdac9919 100644 --- a/classes/template.php +++ b/classes/template.php @@ -321,7 +321,7 @@ public function generate_pdf(bool $preview = false, ?int $userid = null, bool $r } if ($customcert->keeplocalcopy) { - $retval = $this->localfile->sendPDF($userid, $deliveryoption, $return); + $retval = $this->localfile->send_pdf($userid, $deliveryoption, $return); if ($return && !empty($retval)) { return $retval; } @@ -426,7 +426,7 @@ public function generate_pdf(bool $preview = false, ?int $userid = null, bool $r } if ($customcert->keeplocalcopy) { - $this->localfile->savePDF($pdf->Output('', 'S'), $userid); + $this->localfile->save_pdf($pdf->Output('', 'S'), $userid); } if ($return) { diff --git a/downloadcerts.php b/downloadcerts.php deleted file mode 100644 index d6779317..00000000 --- a/downloadcerts.php +++ /dev/null @@ -1,108 +0,0 @@ -. - -/** - * Handles zip and download of certificates. - * - * Derived from the local_bulkcustomcert by Gonzalo Romero. - * - * @package mod_customcert - * @author Gonzalo Romero - * @author Giorgio Consorti - * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - */ - -// defined('MOODLE_INTERNAL') || die(); - -require_once(__DIR__ . '/../../config.php'); - -$courseid = optional_param('courseid', null, PARAM_INT); -$customcertid = optional_param('customcertid', null, PARAM_INT); - -if (!has_capability('mod/customcert:viewallcertificates', context_system::instance()) && !$courseid && !$customcert) { - die(); -} - -/** @var \moodle_database $DB */ -global $DB; - -// Increase the server timeout to handle the creation and sending of large zip files. -core_php_time_limit::raise(); - -if ($courseid) { - $course = $DB->get_record('course', ['id' => $courseid]); - $certs = $DB->get_records('customcert', ['course' => $courseid]); -} else if ($customcertid) { - $cert = $DB->get_record('customcert', ['id' => $customcertid], '*', MUST_EXIST); - $certs[$cert->id] = $cert; - $course = $DB->get_record('course', ['id' => $certs[$cert->id]->course], '*', MUST_EXIST); - $courseid = $course->id; - unset($cert); -} - -// Build a list of files to zip. -$filesforzipping = []; - -foreach ($certs as $certid => $cert_fields) { - $issues = $DB->get_records('customcert_issues', ['customcertid' => $certid]); - list($userssql, $params) = $DB->get_in_or_equal(array_map(fn($i) => $i->userid, $issues), SQL_PARAMS_NAMED); - $usersObjs = $DB->get_records_select('user', "id {$userssql}", $params); - $template = $DB->get_record('customcert_templates', ['id' => $cert_fields->templateid], '*', MUST_EXIST); - $template = new \mod_customcert\template($template); - $lf = new \mod_customcert\localfile($template); - foreach ($issues as $issue) { - if (false === $file = $lf->getPDF($issue->userid)) { - // must generate the pdf - $pdf = $template->generate_pdf(false, $issue->userid, true); - if (!empty($pdf)) { - if ($cert_fields->keeplocalcopy) { - $file = $lf->getPDF($issue->userid); - } else { - $file = [ - 'content' => $pdf, - ]; - } - } - } - if ($file) { - $filename = \mod_customcert\localfile::buildFileName($usersObjs[$issue->userid]->username, $template->get_id(), $course->shortname); - $filesforzipping['/' . $course->shortname . '/' . $cert_fields->name . '/' . $filename] = $file; - } - } -} - -if (count($filesforzipping) == 0) { - // This should never happen. The option only show up if there is available certs. - $url = new moodle_url('/course/view.php?id=' . $courseid); - redirect($url); -} else if ($zipfile = pack_files($filesforzipping)) { - send_temp_file($zipfile, get_string('modulenameplural', 'customcert') . '-' . $course->shortname . '.zip'); -} -die(); - - -function pack_files($filesforzipping) -{ - global $CFG; - // Create path for new zip file. - $tempzip = tempnam($CFG->tempdir . '/', 'customcert_'); - // Zip files. - $zipper = new zip_packer(); - if ($zipper->archive_to_pathname($filesforzipping, $tempzip)) { - return $tempzip; - } - return false; -} diff --git a/lib.php b/lib.php index b2c4a084..1f27a126 100644 --- a/lib.php +++ b/lib.php @@ -326,27 +326,6 @@ function mod_customcert_output_fragment_editelement($args) { return $form->render(); } -/** - * This function extends the course navigation block for the site. - * - * @param \navigation_node $parentnode - * @param \stdClass $course - * @param \context_course $context - */ -function customcert_extend_navigation_course(\navigation_node $parentnode, \stdClass $course, \context_course $context) { - global $PAGE; - - $addnode = $context->contextlevel === 50; - $addnode = $addnode && !($context->instanceid === SITEID); - $addnode = $addnode && has_capability('mod/customcert:viewallcertificates', $context); - $isCourseNav = !is_null($PAGE->cm) && is_null($PAGE->cm->instance); - if ($addnode && $isCourseNav) { - if ($node = build_downloadall_node($isCourseNav, $course, $context)) { - $parentnode->add_node($node); - } - } -} - /** * This function extends the settings navigation block for the site. * @@ -378,12 +357,6 @@ function customcert_extend_settings_navigation(settings_navigation $settings, na $customcertnode->add_node($node, $beforekey); } - if (has_capability('mod/customcert:viewallcertificates', $PAGE->cm->context)) { - if ($node = build_downloadall_node(false, $PAGE->cm->get_course(), null)) { - $customcertnode->add_node($node, $beforekey); - } - } - if (has_capability('mod/customcert:verifycertificate', $settings->get_page()->cm->context)) { $node = navigation_node::create(get_string('verifycertificate', 'customcert'), new moodle_url('/mod/customcert/verify_certificate.php', ['contextid' => $settings->get_page()->cm->context->id]), @@ -395,50 +368,6 @@ function customcert_extend_settings_navigation(settings_navigation $settings, na return $customcertnode->trim_if_empty(); } -/** - * Build the navigation node for the 'Download certificates' item. - * - * @param boolean $isCourseNav - * @param \stdClass $course - * @param \course_context $context - * @return \navigation_node|null null if there are no certifcates available for the download - */ -function build_downloadall_node(bool $isCourseNav, stdClass $course, \context_course $context = null) { - - global $DB, $PAGE; - - if (!$isCourseNav) { - $courseContext = \context_course::instance($course->id); - //Check if there is available certs - $certs = $DB->get_records('customcert', ['id' => $PAGE->cm->instance]); - $users = $DB->get_records('role_assignments', ['contextid' => $courseContext->id]); - $urlparams = ['customcertid' => $PAGE->cm->instance]; - } else { - $certs = $DB->get_records('customcert', ['course' => $context->instanceid]); - $users = $DB->get_records('role_assignments', ['contextid' => $context->id]); - $urlparams = ['courseid' => $course->id]; - } - $availablecerts = false; - foreach ($certs as $certid => $cert_fields) { - foreach ($users as $userid => $user_fields) { - if (!$DB->get_record('customcert_issues', ['userid' => $user_fields->userid, 'customcertid' => $certid])) { - continue; - } - $availablecerts = true; - break; - } - if ($availablecerts) break; - } - if ($availablecerts) { - $node = \navigation_node::create(get_string('bulkdownloadlink', 'mod_customcert'), - new \moodle_url('/mod/customcert/downloadcerts.php', $urlparams), - \navigation_node::TYPE_SETTING, null, 'mod_customcert_downloadcerts', - new \pix_icon('a/download_all', 'certificates')); - return $node; - } - return null; -} - /** * Add nodes to myprofile page. * diff --git a/view.php b/view.php index e6ba5d81..97a9ff77 100644 --- a/view.php +++ b/view.php @@ -69,7 +69,8 @@ if ($deleteissue && $canmanage && confirm_sesskey()) { if (!$confirm) { $nourl = new moodle_url('/mod/customcert/view.php', ['id' => $id]); - $yesurl = new moodle_url('/mod/customcert/view.php', + $yesurl = new moodle_url( + '/mod/customcert/view.php', [ 'id' => $id, 'deleteissue' => $deleteissue, @@ -99,13 +100,24 @@ $issues = $DB->get_records('customcert_issues', ['id' => $deleteissue, 'customcertid' => $customcert->id]); if (!empty($issues)) { $lf = new \mod_customcert\localfile(new \mod_customcert\template($template)); - array_map(fn($issue) => $lf->deletePDF($issue->userid), $issues); + array_map(fn($issue) => $lf->delete_pdf($issue->userid), $issues); } } if (!$deletelocalcopy) { // Delete the issue. + $issue = $DB->get_record('customcert_issues', ['id' => $deleteissue, 'customcertid' => $customcert->id], '*', MUST_EXIST); $DB->delete_records('customcert_issues', ['id' => $deleteissue, 'customcertid' => $customcert->id]); + + // Trigger event. + $cm = get_coursemodule_from_instance('customcert', $customcert->id, 0, false, MUST_EXIST); + $context = \context_module::instance($cm->id); + $event = \mod_customcert\event\issue_deleted::create([ + 'objectid' => $issue->id, + 'context' => $context, + 'relateduserid' => $issue->userid, + ]); + $event->trigger(); } // Redirect back to the manage templates page. @@ -185,7 +197,8 @@ $downloadallbutton = ''; if ($canviewreport && $numissues > 0) { $linkname = get_string('downloadallissuedcertificates', 'customcert'); - $link = new moodle_url('/mod/customcert/view.php', + $link = new moodle_url( + '/mod/customcert/view.php', [ 'id' => $cm->id, 'downloadall' => true,