diff --git a/CHANGES.md b/CHANGES.md index c88afe03..5ecefb5d 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -4,6 +4,27 @@ 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. +## [4.1.6] - 2024-09-28 + +### Fixed + +- Mobile app: Stop using deprecated module-description. +- Fixed auto-linking filters moving text element positions if reference point is center (#629). + +### Changed + +- Mobile app: Update Mobile template to Ionic 7. +- Mobile app: Remove Ionic 3 template. + +### Added + +- Optimise email certificate task by reducing database reads/writes and introducing + configurable settings for task efficiency (#531). +- New element `expiry` which when used will display the expiry date on the list of issued certificates + and the verification pages.
+ Any Custom Certificates that are using the `date` element and selected the expiry dates will + automatically be upgraded to use this new element (#499). + ## [4.1.5] - 2024-05-28 ### Fixed diff --git a/classes/element_helper.php b/classes/element_helper.php index 2bad9f21..bb72d6b5 100644 --- a/classes/element_helper.php +++ b/classes/element_helper.php @@ -73,7 +73,8 @@ public static function render_content($pdf, $element, $content) { $y = $element->get_posy(); $w = $element->get_width(); $refpoint = $element->get_refpoint(); - $actualwidth = $pdf->GetStringWidth($content); + $cleanedcontent = clean_param($content, PARAM_NOTAGS); + $actualwidth = $pdf->GetStringWidth($cleanedcontent); $alignment = $element->get_alignment(); if ($w && $w < $actualwidth) { @@ -685,4 +686,117 @@ public static function get_grade_item_info($gradeitemid, $gradeformat, $userid) $grade->get_dategraded() ); } + + /** + * Helper function to return all the date formats. + * + * @return array the list of date formats + */ + public static function get_date_formats(): array { + // Hard-code date so users can see the difference between short dates with and without the leading zero. + // Eg. 06/07/18 vs 6/07/18. + $date = 1530849658; + + $suffix = self::get_ordinal_number_suffix((int)userdate($date, '%d')); + + $dateformats = [ + 1 => userdate($date, '%B %d, %Y'), + 2 => userdate($date, '%B %d' . $suffix . ', %Y'), + ]; + + $strdateformats = [ + 'strftimedate', + 'strftimedatefullshort', + 'strftimedatefullshortwleadingzero', + 'strftimedateshort', + 'strftimedatetime', + 'strftimedatetimeshort', + 'strftimedatetimeshortwleadingzero', + 'strftimedaydate', + 'strftimedaydatetime', + 'strftimedayshort', + 'strftimedaytime', + 'strftimemonthyear', + 'strftimerecent', + 'strftimerecentfull', + 'strftimetime', + ]; + + foreach ($strdateformats as $strdateformat) { + if ($strdateformat == 'strftimedatefullshortwleadingzero') { + $dateformats[$strdateformat] = userdate($date, get_string('strftimedatefullshort', 'langconfig'), 99, false); + } else if ($strdateformat == 'strftimedatetimeshortwleadingzero') { + $dateformats[$strdateformat] = userdate($date, get_string('strftimedatetimeshort', 'langconfig'), 99, false); + } else { + $dateformats[$strdateformat] = userdate($date, get_string($strdateformat, 'langconfig')); + } + } + + return $dateformats; + } + + /** + * Returns the date in a readable format. + * + * @param int $date + * @param string $dateformat + * @return string + */ + public static function get_date_format_string(int $date, string $dateformat): string { + // Keeping for backwards compatibility. + if (is_number($dateformat)) { + switch ($dateformat) { + case 1: + $certificatedate = userdate($date, '%B %d, %Y'); + break; + case 2: + $suffix = self::get_ordinal_number_suffix((int)userdate($date, '%d')); + $certificatedate = userdate($date, '%B %d' . $suffix . ', %Y'); + break; + case 3: + $certificatedate = userdate($date, '%d %B %Y'); + break; + case 4: + $certificatedate = userdate($date, '%B %Y'); + break; + default: + $certificatedate = userdate($date, get_string('strftimedate', 'langconfig')); + } + } + + // Ok, so we must have been passed the actual format in the lang file. + if (!isset($certificatedate)) { + if ($dateformat == 'strftimedatefullshortwleadingzero') { + $certificatedate = userdate($date, get_string('strftimedatefullshort', 'langconfig'), 99, false); + } else if ($dateformat == 'strftimedatetimeshortwleadingzero') { + $certificatedate = userdate($date, get_string('strftimedatetimeshort', 'langconfig'), 99, false); + } else { + $certificatedate = userdate($date, get_string($dateformat, 'langconfig')); + } + } + + return $certificatedate; + } + + /** + * Helper function to return the suffix of the day of + * the month, eg 'st' if it is the 1st of the month. + * + * @param int $day the day of the month + * @return string the suffix. + */ + private static function get_ordinal_number_suffix(int $day): string { + if (!in_array(($day % 100), [11, 12, 13])) { + switch ($day % 10) { + // Handle 1st, 2nd, 3rd. + case 1: + return get_string('numbersuffix_st_as_in_first', 'customcert'); + case 2: + return get_string('numbersuffix_nd_as_in_second', 'customcert'); + case 3: + return get_string('numbersuffix_rd_as_in_third', 'customcert'); + } + } + return 'th'; + } } diff --git a/classes/output/mobile.php b/classes/output/mobile.php index 2ede1027..26931412 100644 --- a/classes/output/mobile.php +++ b/classes/output/mobile.php @@ -42,7 +42,7 @@ public static function mobile_view_activity($args) { global $OUTPUT, $DB, $USER; $args = (object) $args; - $versionname = $args->appversioncode >= 3950 ? 'latest' : 'ionic3'; + $versionname = $args->appversioncode >= 44000 ? 'latest' : 'ionic5'; $cmid = $args->cmid; $groupid = empty($args->group) ? 0 : (int) $args->group; // By default, group 0. diff --git a/classes/output/verify_certificate_result.php b/classes/output/verify_certificate_result.php index 970bdd3c..d376342a 100644 --- a/classes/output/verify_certificate_result.php +++ b/classes/output/verify_certificate_result.php @@ -39,27 +39,32 @@ class verify_certificate_result implements templatable, renderable { /** * @var string The URL to the user's profile. */ - public $userprofileurl; + public string $userprofileurl; /** * @var string The user's fullname. */ - public $userfullname; + public string $userfullname; /** * @var string The URL to the course page. */ - public $courseurl; + public string $courseurl; /** * @var string The course's fullname. */ - public $coursefullname; + public string $coursefullname; /** * @var string The certificate's name. */ - public $certificatename; + public string $certificatename; + + /** + * @var int|null The certificate's expiry date (optional). + */ + public ?int $expiry; /** * Constructor. @@ -76,6 +81,12 @@ public function __construct($result) { $this->courseurl = new \moodle_url('/course/view.php', ['id' => $result->courseid]); $this->coursefullname = format_string($result->coursefullname, true, ['context' => $context]); $this->certificatename = format_string($result->certificatename, true, ['context' => $context]); + + if (property_exists($result, 'expiry')) { + $this->expiry = $result->expiry; + } else { + $this->expiry = null; + } } /** @@ -92,6 +103,10 @@ public function export_for_template(\renderer_base $output) { $result->courseurl = $this->courseurl; $result->certificatename = $this->certificatename; + if (!empty($this->expiry)) { + $result->expiry = $this->expiry; + } + return $result; } } diff --git a/classes/report_table.php b/classes/report_table.php index 2568ef75..6405e3f3 100644 --- a/classes/report_table.php +++ b/classes/report_table.php @@ -24,6 +24,8 @@ namespace mod_customcert; +use customcertelement_expiry\element as expiry_element; + defined('MOODLE_INTERNAL') || die; global $CFG; @@ -67,6 +69,11 @@ public function __construct($customcertid, $cm, $groupmode, $download = null) { $context = \context_module::instance($cm->id); $extrafields = \core_user\fields::for_identity($context)->get_required_fields(); + $showexpiry = false; + + if (class_exists('\customcertelement_expiry\element')) { + $showexpiry = expiry_element::has_expiry($customcertid); + } $columns = []; $columns[] = 'fullname'; @@ -74,6 +81,11 @@ public function __construct($customcertid, $cm, $groupmode, $download = null) { $columns[] = $extrafield; } $columns[] = 'timecreated'; + + if ($showexpiry) { + $columns[] = 'timeexpires'; + } + $columns[] = 'code'; $headers = []; @@ -82,6 +94,11 @@ public function __construct($customcertid, $cm, $groupmode, $download = null) { $headers[] = \core_user\fields::get_display_name($extrafield); } $headers[] = get_string('receiveddate', 'customcert'); + + if ($showexpiry) { + $headers[] = get_string('expireson', 'customcertelement_expiry'); + } + $headers[] = get_string('code', 'customcert'); // Check if we were passed a filename, which means we want to download it. @@ -142,6 +159,20 @@ public function col_timecreated($user) { return userdate($user->timecreated, $format); } + /** + * Generate the optional certificate expires time column. + * + * @param \stdClass $user + * @return string + */ + public function col_timeexpires($user) { + if ($this->is_downloading() === '') { + return expiry_element::get_expiry_html($this->customcertid, $user->id); + } + $format = '%Y-%m-%d %H:%M'; + return userdate(expiry_element::get_expiry_date($this->customcertid, $user->id), $format); + } + /** * Generate the code column. * @@ -223,4 +254,3 @@ public function download() { exit; } } - diff --git a/classes/task/email_certificate_task.php b/classes/task/email_certificate_task.php index 33c314e7..f75d8812 100644 --- a/classes/task/email_certificate_task.php +++ b/classes/task/email_certificate_task.php @@ -15,7 +15,7 @@ // along with Moodle. If not, see . /** - * A scheduled task for emailing certificates. + * An adhoc task for emailing certificates. * * @package mod_customcert * @copyright 2017 Mark Nelson @@ -26,13 +26,13 @@ use mod_customcert\helper; /** - * A scheduled task for emailing certificates. + * An adhoc task for emailing certificates per issue. * * @package mod_customcert * @copyright 2017 Mark Nelson * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ -class email_certificate_task extends \core\task\scheduled_task { +class email_certificate_task extends \core\task\adhoc_task { /** * Get a descriptive name for this task (shown to admins). @@ -49,207 +49,131 @@ public function get_name() { public function execute() { global $DB; - // Get all the certificates that have requested someone get emailed. - $emailotherslengthsql = $DB->sql_length('c.emailothers'); + $customdata = $this->get_custom_data(); + + $issueid = $customdata->issueid; + $customcertid = $customdata->customcertid; $sql = "SELECT c.*, ct.id as templateid, ct.name as templatename, ct.contextid, co.id as courseid, co.fullname as coursefullname, co.shortname as courseshortname FROM {customcert} c - JOIN {customcert_templates} ct - ON c.templateid = ct.id - JOIN {course} co - ON c.course = co.id - WHERE (c.emailstudents = :emailstudents - OR c.emailteachers = :emailteachers - OR $emailotherslengthsql >= 3)"; - if (!$customcerts = $DB->get_records_sql($sql, ['emailstudents' => 1, 'emailteachers' => 1])) { - return; - } + JOIN {customcert_templates} ct ON c.templateid = ct.id + JOIN {course} co ON c.course = co.id + WHERE c.id = :id"; + + $customcert = $DB->get_record_sql($sql, ['id' => $customcertid]); // The renderers used for sending emails. $page = new \moodle_page(); $htmlrenderer = $page->get_renderer('mod_customcert', 'email', 'htmlemail'); $textrenderer = $page->get_renderer('mod_customcert', 'email', 'textemail'); - foreach ($customcerts as $customcert) { - // Do not process an empty certificate. - $sql = "SELECT ce.* - FROM {customcert_elements} ce - JOIN {customcert_pages} cp - ON cp.id = ce.pageid - JOIN {customcert_templates} ct - ON ct.id = cp.templateid - WHERE ct.contextid = :contextid"; - if (!$DB->record_exists_sql($sql, ['contextid' => $customcert->contextid])) { - continue; - } - - // Get the context. - $context = \context::instance_by_id($customcert->contextid); - - // Get the person we are going to send this email on behalf of. - $userfrom = \core_user::get_noreply_user(); - - // Store teachers for later. - $teachers = get_enrolled_users($context, 'moodle/course:update'); - - $courseshortname = format_string($customcert->courseshortname, true, ['context' => $context]); - $coursefullname = format_string($customcert->coursefullname, true, ['context' => $context]); - $certificatename = format_string($customcert->name, true, ['context' => $context]); - - // Used to create the email subject. - $info = new \stdClass; - $info->coursename = $courseshortname; // Added for BC, so users who have edited the string don't lose this value. - $info->courseshortname = $courseshortname; - $info->coursefullname = $coursefullname; - $info->certificatename = $certificatename; - - // Get a list of all the issues. - $userfields = helper::get_all_user_name_fields('u'); - $sql = "SELECT u.id, u.username, $userfields, u.email, ci.id as issueid, ci.emailed - FROM {customcert_issues} ci - JOIN {user} u - ON ci.userid = u.id - WHERE ci.customcertid = :customcertid"; - $issuedusers = $DB->get_records_sql($sql, ['customcertid' => $customcert->id]); - - // Now, get a list of users who can access the certificate but have not yet. - $enrolledusers = get_enrolled_users(\context_course::instance($customcert->courseid), 'mod/customcert:view'); - foreach ($enrolledusers as $enroluser) { - // Check if the user has already been issued. - if (in_array($enroluser->id, array_keys((array) $issuedusers))) { - continue; - } - - // Now check if the certificate is not visible to the current user. - $cm = get_fast_modinfo($customcert->courseid, $enroluser->id)->instances['customcert'][$customcert->id]; - if (!$cm->uservisible) { - continue; - } - - // Don't want to email those with the capability to manage the certificate. - if (has_capability('mod/customcert:manage', $context, $enroluser->id)) { - continue; - } - - // Only email those with the capability to receive the certificate. - if (!has_capability('mod/customcert:receiveissue', $context, $enroluser->id)) { - continue; - } - - // Check that they have passed the required time. - if (!empty($customcert->requiredtime)) { - if (\mod_customcert\certificate::get_course_time($customcert->courseid, - $enroluser->id) < ($customcert->requiredtime * 60)) { - continue; - } - } - - // Ensure the cert hasn't already been issued, e.g via the UI (view.php) - a race condition. - $issueid = $DB->get_field('customcert_issues', 'id', - ['userid' => $enroluser->id, 'customcertid' => $customcert->id], IGNORE_MULTIPLE); - if (empty($issueid)) { - // Ok, issue them the certificate. - $issueid = \mod_customcert\certificate::issue_certificate($customcert->id, $enroluser->id); - } - // Add them to the array so we email them. - $enroluser->issueid = $issueid; - $enroluser->emailed = 0; - $issuedusers[] = $enroluser; - } + // Get the context. + $context = \context::instance_by_id($customcert->contextid); + + // Get the person we are going to send this email on behalf of. + $userfrom = \core_user::get_noreply_user(); + + // Store teachers for later. + $teachers = get_enrolled_users($context, 'moodle/course:update'); + + $courseshortname = format_string($customcert->courseshortname, true, ['context' => $context]); + $coursefullname = format_string($customcert->coursefullname, true, ['context' => $context]); + $certificatename = format_string($customcert->name, true, ['context' => $context]); + + // Used to create the email subject. + $info = new \stdClass(); + $info->coursename = $courseshortname; // Added for BC, so users who have edited the string don't lose this value. + $info->courseshortname = $courseshortname; + $info->coursefullname = $coursefullname; + $info->certificatename = $certificatename; + + // Get the information about the user and the certificate issue. + $userfields = helper::get_all_user_name_fields('u'); + $sql = "SELECT u.id, u.username, $userfields, u.email, ci.id as issueid, ci.emailed + FROM {customcert_issues} ci + JOIN {user} u + ON ci.userid = u.id + WHERE ci.customcertid = :customcertid + AND ci.id = :issueid"; + $user = $DB->get_record_sql($sql, ['customcertid' => $customcertid, 'issueid' => $issueid]); + + // Create a directory to store the PDF we will be sending. + $tempdir = make_temp_directory('certificate/attachment'); + if (!$tempdir) { + return; + } - // Remove all the users who have already been emailed. - foreach ($issuedusers as $key => $issueduser) { - if ($issueduser->emailed) { - unset($issuedusers[$key]); - } - } + // Setup the user for the cron. + cron_setup_user($user); + + $userfullname = fullname($user); + $info->userfullname = $userfullname; + + // Now, get the PDF. + $template = new \stdClass(); + $template->id = $customcert->templateid; + $template->name = $customcert->templatename; + $template->contextid = $customcert->contextid; + $template = new \mod_customcert\template($template); + $filecontents = $template->generate_pdf(false, $user->id, true); + + // Set the name of the file we are going to send. + $filename = $courseshortname . '_' . $certificatename; + $filename = \core_text::entities_to_utf8($filename); + $filename = strip_tags($filename); + $filename = rtrim($filename, '.'); + $filename = str_replace('&', '_', $filename) . '.pdf'; + + // Create the file we will be sending. + $tempfile = $tempdir . '/' . md5(microtime() . $user->id) . '.pdf'; + file_put_contents($tempfile, $filecontents); + + if ($customcert->emailstudents) { + $renderable = new \mod_customcert\output\email_certificate(true, $userfullname, $courseshortname, + $coursefullname, $certificatename, $context->instanceid); + + $subject = get_string('emailstudentsubject', 'customcert', $info); + $message = $textrenderer->render($renderable); + $messagehtml = $htmlrenderer->render($renderable); + email_to_user($user, $userfrom, html_entity_decode($subject, ENT_COMPAT), $message, + $messagehtml, $tempfile, $filename); + } - // If there are no users to email we can return early. - if (!$issuedusers) { - continue; - } + if ($customcert->emailteachers) { + $renderable = new \mod_customcert\output\email_certificate(false, $userfullname, $courseshortname, + $coursefullname, $certificatename, $context->instanceid); - // Create a directory to store the PDF we will be sending. - $tempdir = make_temp_directory('certificate/attachment'); - if (!$tempdir) { - return; + $subject = get_string('emailnonstudentsubject', 'customcert', $info); + $message = $textrenderer->render($renderable); + $messagehtml = $htmlrenderer->render($renderable); + foreach ($teachers as $teacher) { + email_to_user($teacher, $userfrom, html_entity_decode($subject, ENT_COMPAT), + $message, $messagehtml, $tempfile, $filename); } + } - // Now, email the people we need to. - foreach ($issuedusers as $user) { - // Set up the user. - cron_setup_user($user); - - $userfullname = fullname($user); - $info->userfullname = $userfullname; - - // Now, get the PDF. - $template = new \stdClass(); - $template->id = $customcert->templateid; - $template->name = $customcert->templatename; - $template->contextid = $customcert->contextid; - $template = new \mod_customcert\template($template); - $filecontents = $template->generate_pdf(false, $user->id, true); - - // Set the name of the file we are going to send. - $filename = $courseshortname . '_' . $certificatename; - $filename = \core_text::entities_to_utf8($filename); - $filename = strip_tags($filename); - $filename = rtrim($filename, '.'); - $filename = str_replace('&', '_', $filename) . '.pdf'; - - // Create the file we will be sending. - $tempfile = $tempdir . '/' . md5(microtime() . $user->id) . '.pdf'; - file_put_contents($tempfile, $filecontents); - - if ($customcert->emailstudents) { - $renderable = new \mod_customcert\output\email_certificate(true, $userfullname, $courseshortname, - $coursefullname, $certificatename, $context->instanceid); - - $subject = get_string('emailstudentsubject', 'customcert', $info); - $message = $textrenderer->render($renderable); - $messagehtml = $htmlrenderer->render($renderable); - email_to_user($user, $userfrom, html_entity_decode($subject, ENT_COMPAT), $message, - $messagehtml, $tempfile, $filename); - } - - if ($customcert->emailteachers) { - $renderable = new \mod_customcert\output\email_certificate(false, $userfullname, $courseshortname, - $coursefullname, $certificatename, $context->instanceid); + if (!empty($customcert->emailothers)) { + $others = explode(',', $customcert->emailothers); + foreach ($others as $email) { + $email = trim($email); + if (validate_email($email)) { + $renderable = new \mod_customcert\output\email_certificate(false, $userfullname, + $courseshortname, $coursefullname, $certificatename, $context->instanceid); $subject = get_string('emailnonstudentsubject', 'customcert', $info); $message = $textrenderer->render($renderable); $messagehtml = $htmlrenderer->render($renderable); - foreach ($teachers as $teacher) { - email_to_user($teacher, $userfrom, html_entity_decode($subject, ENT_COMPAT), - $message, $messagehtml, $tempfile, $filename); - } - } - if (!empty($customcert->emailothers)) { - $others = explode(',', $customcert->emailothers); - foreach ($others as $email) { - $email = trim($email); - if (validate_email($email)) { - $renderable = new \mod_customcert\output\email_certificate(false, $userfullname, - $courseshortname, $coursefullname, $certificatename, $context->instanceid); - - $subject = get_string('emailnonstudentsubject', 'customcert', $info); - $message = $textrenderer->render($renderable); - $messagehtml = $htmlrenderer->render($renderable); - - $emailuser = new \stdClass(); - $emailuser->id = -1; - $emailuser->email = $email; - email_to_user($emailuser, $userfrom, html_entity_decode($subject, ENT_COMPAT), $message, - $messagehtml, $tempfile, $filename); - } - } + $emailuser = new \stdClass(); + $emailuser->id = -1; + $emailuser->email = $email; + email_to_user($emailuser, $userfrom, html_entity_decode($subject, ENT_COMPAT), $message, + $messagehtml, $tempfile, $filename); } - - // Set the field so that it is emailed. - $DB->set_field('customcert_issues', 'emailed', 1, ['id' => $user->issueid]); } } + + // Set the field so that it is emailed. + $DB->set_field('customcert_issues', 'emailed', 1, ['id' => $issueid]); } } diff --git a/classes/task/issue_certificates_task.php b/classes/task/issue_certificates_task.php new file mode 100644 index 00000000..c2844f0f --- /dev/null +++ b/classes/task/issue_certificates_task.php @@ -0,0 +1,231 @@ +. + +/** + * A scheduled task for issuing certificates that have requested someone get emailed. + * + * @package mod_customcert + * @copyright 2024 Oscar Nadjar + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +namespace mod_customcert\task; + +/** + * A scheduled task for issuing certificates that have requested someone get emailed. + * + * @package mod_customcert + * @copyright 2024 Oscar Nadjar + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class issue_certificates_task extends \core\task\scheduled_task { + + private bool $VERBOSE = true; + + /** + * Get a descriptive name for this task (shown to admins). + * + * @return string + */ + public function get_name(): string { + return get_string('taskissuecertificate', 'customcert'); + } + + /** + * Execute. + */ + public function execute() { + 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'); + + // We are going to issue certificates that have requested someone get emailed. + $emailotherslengthsql = $DB->sql_length('c.emailothers'); + $sql = "SELECT c.*, ct.id as templateid, ct.name as templatename, ct.contextid, co.id as courseid, + co.fullname as coursefullname, co.shortname as courseshortname + FROM {customcert} c + JOIN {customcert_templates} ct + ON c.templateid = ct.id + JOIN {course} co + ON c.course = co.id + JOIN {course_categories} cat + ON co.category = cat.id + LEFT JOIN {customcert_issues} ci + ON c.id = ci.customcertid"; + + // Add conditions to exclude certificates from hidden courses. + $sql .= " WHERE (c.emailstudents = :emailstudents + OR c.emailteachers = :emailteachers + OR $emailotherslengthsql >= 3)"; + + $params = ['emailstudents' => 1, 'emailteachers' => 1]; + + // Check the includeinnotvisiblecourses configuration. + if (!$includeinnotvisiblecourses) { + // Exclude certificates from hidden courses. + $sql .= " AND co.visible = 1 AND cat.visible = 1"; + } + + // Add condition based on certificate execution period. + if ($certificateexecutionperiod > 0) { + // Include courses with no end date or end date greater than the specified period. + $sql .= " AND (co.enddate > :enddate OR (co.enddate = 0 AND ci.timecreated > :enddate2))"; + $params['enddate'] = time() - $certificateexecutionperiod; + $params['enddate2'] = $params['enddate']; + } + + $sql .= " GROUP BY c.id, ct.id, ct.name, ct.contextid, co.id, co.fullname, co.shortname"; + + // Execute the SQL query. + $customcerts = $DB->get_records_sql($sql, $params, $offset, $certificatesperrun); + + // When we get to the end of the list, reset the offset. + set_config('certificate_offset', !empty($customcerts) ? $offset + $certificatesperrun : 0, 'customcert'); + + if (empty($customcerts)) { + if ($this->VERBOSE) { + mtrace("No certificates to be processed, terminating."); + } + return; + } + + if ($this->VERBOSE) { + $n_certs = count($customcerts); + mtrace("Found $n_certs certificates to be processed"); + } + + foreach ($customcerts as $customcert) { + mtrace("Processing certificate ID#$customcert->id - Name: $customcert->name - Course ID#$customcert->courseid"); + + // Check if the certificate is hidden, quit early. + $cm = get_course_and_cm_from_instance($customcert->id, 'customcert', $customcert->course)[1]; + if (!$cm->visible) { + if ($this->VERBOSE) { + mtrace(">... Skip (Hidden)"); + } + continue; + } + + // Do not process an empty certificate. + $sql = "SELECT ce.* + FROM {customcert_elements} ce + JOIN {customcert_pages} cp + ON cp.id = ce.pageid + JOIN {customcert_templates} ct + ON ct.id = cp.templateid + WHERE ct.contextid = :contextid"; + if (!$DB->record_exists_sql($sql, ['contextid' => $customcert->contextid])) { + if ($this->VERBOSE) { + mtrace(">... Skip (Empty)"); + } + continue; + } + + // Get the context. + $context = \context::instance_by_id($customcert->contextid); + + // Get a list of all the issues. + $sql = "SELECT u.id + FROM {customcert_issues} ci + JOIN {user} u + ON ci.userid = u.id + WHERE ci.customcertid = :customcertid + AND ci.emailed = 1"; + $issuedusers = $DB->get_records_sql($sql, ['customcertid' => $customcert->id]); + + // Now, get a list of users who can Manage the certificate. + $userswithmanage = get_users_by_capability($context, 'mod/customcert:manage', 'u.id'); + + // Get the context of the Custom Certificate module. + $cmcontext = \context_module::instance($cm->id); + + // Now, get a list of users who can view and issue the certificate but have not yet. + // Get users with the mod/customcert:receiveissue capability in the Custom Certificate module context. + $userswithissue = get_users_by_capability($cmcontext, 'mod/customcert:receiveissue'); + // Get users with mod/customcert:view capability. + $userswithview = get_users_by_capability($cmcontext, 'mod/customcert:view'); + // Users with both mod/customcert:view and mod/customcert:receiveissue cabapilities. + $userswithissueview = array_intersect_key($userswithissue, $userswithview); + + // Filter the remaining users by determining whether they can actually see the CM or not + // (Note: filter_user_list only takes into account those availability condition which actually implement this function, so the second check with get_fast_modinfo must be still performed - but we can reduce the size of the users list here already) + $infomodule = new \core_availability\info_module($cm); + $filteredusers = $infomodule->filter_user_list($userswithissueview); + + if ($this->VERBOSE) { + $n_users = count($filteredusers); + mtrace("> Found $n_users users to be processed"); + } + + $new_certs = 0; + foreach ($filteredusers as $enroluser) { + // Check if the user has already been issued and emailed. + if (in_array($enroluser->id, array_keys((array)$issuedusers))) { + continue; + } + + // Don't want to issue to teachers. + if (in_array($enroluser->id, array_keys((array)$userswithmanage))) { + continue; + } + + // Now check if the certificate is not visible to the current user. + $cm = get_fast_modinfo($customcert->courseid, $enroluser->id)->instances['customcert'][$customcert->id]; + if (!$cm->uservisible) { + continue; + } + + // Check that they have passed the required time. + if (!empty($customcert->requiredtime)) { + if (\mod_customcert\certificate::get_course_time($customcert->courseid, + $enroluser->id) < ($customcert->requiredtime * 60)) { + continue; + } + } + + // Ensure the cert hasn't already been issued, e.g via the UI (view.php) - a race condition. + $issue = $DB->get_record('customcert_issues', + ['userid' => $enroluser->id, 'customcertid' => $customcert->id], 'id, emailed'); + + // Ok, issue them the certificate. + $issueid = empty($issue) ? + \mod_customcert\certificate::issue_certificate($customcert->id, $enroluser->id) : $issue->id; + + // Validate issueid and one last check for emailed. + if (!empty($issueid) && empty($issue->emailed)) { + $new_certs += 1; + + // We create a new adhoc task to send the email. + $task = new \mod_customcert\task\email_certificate_task(); + $task->set_custom_data(['issueid' => $issueid, 'customcertid' => $customcert->id]); + $useadhoc = get_config('customcert', 'useadhoc'); + if ($useadhoc) { + \core\task\manager::queue_adhoc_task($task); + } else { + $task->execute(); + } + } + } + + if ($this->VERBOSE) { + mtrace("> Issued $new_certs new certificates"); + } + } + } +} diff --git a/classes/template.php b/classes/template.php index 444af034..79dfdbd6 100644 --- a/classes/template.php +++ b/classes/template.php @@ -266,11 +266,11 @@ public function delete_element($elementid) { * Generate the PDF for the template. * * @param bool $preview true if it is a preview, false otherwise - * @param int $userid the id of the user whose certificate we want to view + * @param int|null $userid the id of the user whose certificate we want to view * @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 generate_pdf(bool $preview = false, int $userid = null, bool $return = false) { + public function generate_pdf(bool $preview = false, ?int $userid = null, bool $return = false) { global $CFG, $DB, $USER; if (empty($userid)) { diff --git a/db/install.xml b/db/install.xml index f3863464..58d94b20 100644 --- a/db/install.xml +++ b/db/install.xml @@ -1,5 +1,5 @@ - diff --git a/db/tasks.php b/db/tasks.php index fd2ddb07..cfde7be3 100644 --- a/db/tasks.php +++ b/db/tasks.php @@ -27,7 +27,7 @@ $tasks = [ [ - 'classname' => 'mod_customcert\task\email_certificate_task', + 'classname' => 'mod_customcert\task\issue_certificates_task', 'blocking' => 0, 'minute' => '*', 'hour' => '*', diff --git a/db/upgrade.php b/db/upgrade.php index bfcba5c0..2c504988 100644 --- a/db/upgrade.php +++ b/db/upgrade.php @@ -235,5 +235,68 @@ function xmldb_customcert_upgrade($oldversion) { upgrade_mod_savepoint(true, 2022112804, 'customcert'); } + if ($oldversion < 2022112809) { + + // Define table customcert_email_task_prgrs to be created. + $table = new xmldb_table('customcert_email_task_prgrs'); + + // Adding fields to table customcert_email_task_prgrs. + $table->add_field('id', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, XMLDB_SEQUENCE, null); + $table->add_field('taskname', XMLDB_TYPE_CHAR, '255', null, XMLDB_NOTNULL, null, 'email_certificate_task'); + $table->add_field('last_processed', XMLDB_TYPE_INTEGER, '20', null, XMLDB_NOTNULL, null, '0'); + $table->add_field('total_certificate_to_process', XMLDB_TYPE_INTEGER, '20', null, XMLDB_NOTNULL, null, '0'); + + // Adding keys to table customcert_email_task_prgrs. + $table->add_key('primary', XMLDB_KEY_PRIMARY, ['id']); + + // Conditionally launch create table for customcert_email_task_prgrs. + if (!$dbman->table_exists($table)) { + $dbman->create_table($table); + + // Add a default row to the customcert_email_task_prgrs table. + $defaultdata = new stdClass(); + $defaultdata->taskname = 'email_certificate_task'; + $defaultdata->last_processed = 0; + $defaultdata->total_certificate_to_process = 0; + + // Insert the default data into the table. + $DB->insert_record('customcert_email_task_prgrs', $defaultdata); + } + + // Customcert savepoint reached. + upgrade_mod_savepoint(true, 2022112809, 'customcert'); + } + + if ($oldversion < 2022112810) { + $elements = $DB->get_records('customcert_elements', ['element' => 'date']); + + foreach ($elements as $element) { + $data = json_decode($element->data); + + // If dateitem is between CUSTOMCERT_DATE_EXPIRY_ONE and CUSTOMCERT_DATE_EXPIRY_FIVE. + if ((intval($data->dateitem) <= -8) && (intval($data->dateitem) >= -12)) { + $data->startfrom = 'award'; + $element->data = json_encode($data); + $element->element = 'expiry'; + $DB->update_record('customcert_elements', $element); + } + } + + // Customcert savepoint reached. + upgrade_mod_savepoint(true, 2022112810, 'customcert'); + } + + if ($oldversion < 2022112812) { + // Drop unused table customcert_email_task_prgrs. + $table = new xmldb_table('customcert_email_task_prgrs'); + + if ($dbman->table_exists($table)) { + $dbman->drop_table($table); + } + + // Customcert savepoint reached. + upgrade_mod_savepoint(true, 2022112812, 'customcert'); + } + return true; } diff --git a/element/date/classes/element.php b/element/date/classes/element.php index 4d067354..30551fd9 100644 --- a/element/date/classes/element.php +++ b/element/date/classes/element.php @@ -24,6 +24,8 @@ namespace customcertelement_date; +use mod_customcert\element_helper; + defined('MOODLE_INTERNAL') || die(); /** @@ -66,31 +68,6 @@ */ define('CUSTOMCERT_DATE_ENROLMENT_END', '-7'); -/** - * Date - Relative expiry date of 1 year - */ -define('CUSTOMCERT_DATE_EXPIRY_ONE', '-8'); - -/** - * Date - Relative expiry date of 2 year - */ -define('CUSTOMCERT_DATE_EXPIRY_TWO', '-9'); - -/** - * Date - Relative expiry date of 3 year - */ -define('CUSTOMCERT_DATE_EXPIRY_THREE', '-10'); - -/** - * Date - Relative expiry date of 4 year - */ -define('CUSTOMCERT_DATE_EXPIRY_FOUR', '-11'); - -/** - * Date - Relative expiry date of 5 year - */ -define('CUSTOMCERT_DATE_EXPIRY_FIVE', '-12'); - require_once($CFG->dirroot . '/lib/grade/constants.php'); /** @@ -120,11 +97,6 @@ public function render_form_elements($mform) { } $dateoptions[CUSTOMCERT_DATE_ENROLMENT_START] = get_string('enrolmentstartdate', 'customcertelement_date'); $dateoptions[CUSTOMCERT_DATE_ENROLMENT_END] = get_string('enrolmentenddate', 'customcertelement_date'); - $dateoptions[CUSTOMCERT_DATE_EXPIRY_ONE] = get_string('expirydateone', 'customcertelement_date'); - $dateoptions[CUSTOMCERT_DATE_EXPIRY_TWO] = get_string('expirydatetwo', 'customcertelement_date'); - $dateoptions[CUSTOMCERT_DATE_EXPIRY_THREE] = get_string('expirydatethree', 'customcertelement_date'); - $dateoptions[CUSTOMCERT_DATE_EXPIRY_FOUR] = get_string('expirydatefour', 'customcertelement_date'); - $dateoptions[CUSTOMCERT_DATE_EXPIRY_FIVE] = get_string('expirydatefive', 'customcertelement_date'); $dateoptions[CUSTOMCERT_DATE_COURSE_START] = get_string('coursestartdate', 'customcertelement_date'); $dateoptions[CUSTOMCERT_DATE_COURSE_END] = get_string('courseenddate', 'customcertelement_date'); $dateoptions[CUSTOMCERT_DATE_COURSE_GRADE] = get_string('coursegradedate', 'customcertelement_date'); @@ -133,7 +105,8 @@ public function render_form_elements($mform) { $mform->addElement('select', 'dateitem', get_string('dateitem', 'customcertelement_date'), $dateoptions); $mform->addHelpButton('dateitem', 'dateitem', 'customcertelement_date'); - $mform->addElement('select', 'dateformat', get_string('dateformat', 'customcertelement_date'), self::get_date_formats()); + $mform->addElement('select', 'dateformat', get_string('dateformat', 'customcertelement_date'), + element_helper::get_date_formats()); $mform->addHelpButton('dateformat', 'dateformat', 'customcertelement_date'); parent::render_form_elements($mform); @@ -193,16 +166,6 @@ public function render($pdf, $preview, $user) { if ($dateitem == CUSTOMCERT_DATE_ISSUE) { $date = $issue->timecreated; - } else if ($dateitem == CUSTOMCERT_DATE_EXPIRY_ONE) { - $date = strtotime('+1 years', $issue->timecreated); - } else if ($dateitem == CUSTOMCERT_DATE_EXPIRY_TWO) { - $date = strtotime('+2 years', $issue->timecreated); - } else if ($dateitem == CUSTOMCERT_DATE_EXPIRY_THREE) { - $date = strtotime('+3 years', $issue->timecreated); - } else if ($dateitem == CUSTOMCERT_DATE_EXPIRY_FOUR) { - $date = strtotime('+4 years', $issue->timecreated); - } else if ($dateitem == CUSTOMCERT_DATE_EXPIRY_FIVE) { - $date = strtotime('+5 years', $issue->timecreated); } else if ($dateitem == CUSTOMCERT_DATE_CURRENT_DATE) { $date = time(); } else if ($dateitem == CUSTOMCERT_DATE_COMPLETION) { @@ -270,7 +233,7 @@ public function render($pdf, $preview, $user) { // Ensure that a date has been set. if (!empty($date)) { - \mod_customcert\element_helper::render_content($pdf, $this, $this->get_date_format_string($date, $dateformat)); + \mod_customcert\element_helper::render_content($pdf, $this, element_helper::get_date_format_string($date, $dateformat)); } } @@ -292,7 +255,8 @@ public function render_html() { $dateinfo = json_decode($this->get_data()); $dateformat = $dateinfo->dateformat; - return \mod_customcert\element_helper::render_html_content($this, $this->get_date_format_string(time(), $dateformat)); + return \mod_customcert\element_helper::render_html_content($this, + element_helper::get_date_format_string(time(), $dateformat)); } /** @@ -352,110 +316,8 @@ public function after_restore($restore) { * @return array the list of date formats */ public static function get_date_formats() { - // Hard-code date so users can see the difference between short dates with and without the leading zero. - // Eg. 06/07/18 vs 6/07/18. - $date = 1530849658; - - $suffix = self::get_ordinal_number_suffix(userdate($date, '%d')); - - $dateformats = [ - 1 => userdate($date, '%B %d, %Y'), - 2 => userdate($date, '%B %d' . $suffix . ', %Y'), - ]; - - $strdateformats = [ - 'strftimedate', - 'strftimedatefullshort', - 'strftimedatefullshortwleadingzero', - 'strftimedateshort', - 'strftimedatetime', - 'strftimedatetimeshort', - 'strftimedatetimeshortwleadingzero', - 'strftimedaydate', - 'strftimedaydatetime', - 'strftimedayshort', - 'strftimedaytime', - 'strftimemonthyear', - 'strftimerecent', - 'strftimerecentfull', - 'strftimetime', - ]; - - foreach ($strdateformats as $strdateformat) { - if ($strdateformat == 'strftimedatefullshortwleadingzero') { - $dateformats[$strdateformat] = userdate($date, get_string('strftimedatefullshort', 'langconfig'), 99, false); - } else if ($strdateformat == 'strftimedatetimeshortwleadingzero') { - $dateformats[$strdateformat] = userdate($date, get_string('strftimedatetimeshort', 'langconfig'), 99, false); - } else { - $dateformats[$strdateformat] = userdate($date, get_string($strdateformat, 'langconfig')); - } - } - - return $dateformats; - } - - /** - * Returns the date in a readable format. - * - * @param int $date - * @param string $dateformat - * @return string - */ - protected function get_date_format_string($date, $dateformat) { - // Keeping for backwards compatibility. - if (is_number($dateformat)) { - switch ($dateformat) { - case 1: - $certificatedate = userdate($date, '%B %d, %Y'); - break; - case 2: - $suffix = self::get_ordinal_number_suffix(userdate($date, '%d')); - $certificatedate = userdate($date, '%B %d' . $suffix . ', %Y'); - break; - case 3: - $certificatedate = userdate($date, '%d %B %Y'); - break; - case 4: - $certificatedate = userdate($date, '%B %Y'); - break; - default: - $certificatedate = userdate($date, get_string('strftimedate', 'langconfig')); - } - } - - // Ok, so we must have been passed the actual format in the lang file. - if (!isset($certificatedate)) { - if ($dateformat == 'strftimedatefullshortwleadingzero') { - $certificatedate = userdate($date, get_string('strftimedatefullshort', 'langconfig'), 99, false); - } else if ($dateformat == 'strftimedatetimeshortwleadingzero') { - $certificatedate = userdate($date, get_string('strftimedatetimeshort', 'langconfig'), 99, false); - } else { - $certificatedate = userdate($date, get_string($dateformat, 'langconfig')); - } - } - - return $certificatedate; - } - - /** - * Helper function to return the suffix of the day of - * the month, eg 'st' if it is the 1st of the month. - * - * @param int $day the day of the month - * @return string the suffix. - */ - protected static function get_ordinal_number_suffix($day) { - if (!in_array(($day % 100), [11, 12, 13])) { - switch ($day % 10) { - // Handle 1st, 2nd, 3rd. - case 1: - return get_string('numbersuffix_st_as_in_first', 'customcertelement_date'); - case 2: - return get_string('numbersuffix_nd_as_in_second', 'customcertelement_date'); - case 3: - return get_string('numbersuffix_rd_as_in_third', 'customcertelement_date'); - } - } - return 'th'; + debugging("The method customcertelement_date::get_date_formats is deprecated, " . + "please use element_helper::get_date_formats() instead", DEBUG_DEVELOPER); + return element_helper::get_date_formats(); } } diff --git a/element/date/lang/en/customcertelement_date.php b/element/date/lang/en/customcertelement_date.php index 83ccb3f6..5892ff4c 100644 --- a/element/date/lang/en/customcertelement_date.php +++ b/element/date/lang/en/customcertelement_date.php @@ -28,11 +28,6 @@ $string['coursestartdate'] = 'Course start date'; $string['enrolmentenddate'] = 'Enrolment end date'; $string['enrolmentstartdate'] = 'Enrolment start date'; -$string['expirydateone'] = 'Expiry date (1 year)'; -$string['expirydatetwo'] = 'Expiry date (2 year)'; -$string['expirydatethree'] = 'Expiry date (3 year)'; -$string['expirydatefour'] = 'Expiry date (4 year)'; -$string['expirydatefive'] = 'Expiry date (5 year)'; $string['currentdate'] = 'Current date'; $string['dateformat'] = 'Date format'; $string['dateformat_help'] = 'This is the format of the date that will be displayed'; @@ -41,7 +36,4 @@ $string['issueddate'] = 'Issued date'; $string['pluginname'] = 'Date'; $string['privacy:metadata'] = 'The Date plugin does not store any personal data.'; -$string['numbersuffix_nd_as_in_second'] = 'nd'; -$string['numbersuffix_rd_as_in_third'] = 'rd'; -$string['numbersuffix_st_as_in_first'] = 'st'; $string['userdateformat'] = 'User date format'; diff --git a/element/expiry/classes/element.php b/element/expiry/classes/element.php new file mode 100644 index 00000000..2d04c1d1 --- /dev/null +++ b/element/expiry/classes/element.php @@ -0,0 +1,393 @@ +. + +/** + * This file contains the customcert element expiry's core interaction API. + * + * @package customcertelement_expiry + * @copyright 2024 Leon Stringer + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace customcertelement_expiry; + +use mod_customcert\element_helper; + +/** + * The customcert element expiry's core interaction API. + * + * @package customcertelement_expiry + * @copyright 2024 Leon Stringer + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class element extends \mod_customcert\element { + + /** + * Date - Relative expiry date of 1 year + */ + private const EXPIRY_ONE = '-8'; + + /** + * Date - Relative expiry date of 2 year + */ + private const EXPIRY_TWO = '-9'; + + /** + * Date - Relative expiry date of 3 year + */ + private const EXPIRY_THREE = '-10'; + + /** + * Date - Relative expiry date of 4 year + */ + private const EXPIRY_FOUR = '-11'; + + /** + * Date - Relative expiry date of 5 year + */ + private const EXPIRY_FIVE = '-12'; + + /** @var array Map EXPIRY_ consts to strtotime()'s $datetime param. */ + private array $relative = [ + self::EXPIRY_ONE => '+1 year', + self::EXPIRY_TWO => '+2 years', + self::EXPIRY_THREE => '+3 years', + self::EXPIRY_FOUR => '+4 years', + self::EXPIRY_FIVE => '+5 years', + ]; + + /** + * This function renders the form elements when adding a customcert element. + * + * @param \MoodleQuickForm $mform the edit_form instance + */ + public function render_form_elements($mform) { + global $CFG, $COURSE; + + $dateoptions[self::EXPIRY_ONE] = get_string('expirydateone', 'customcertelement_expiry'); + $dateoptions[self::EXPIRY_TWO] = get_string('expirydatetwo', 'customcertelement_expiry'); + $dateoptions[self::EXPIRY_THREE] = get_string('expirydatethree', 'customcertelement_expiry'); + $dateoptions[self::EXPIRY_FOUR] = get_string('expirydatefour', 'customcertelement_expiry'); + $dateoptions[self::EXPIRY_FIVE] = get_string('expirydatefive', 'customcertelement_expiry'); + + $mform->addElement('select', 'dateitem', get_string('dateitem', 'customcertelement_expiry'), $dateoptions); + $mform->addHelpButton('dateitem', 'dateitem', 'customcertelement_expiry'); + + $mform->addElement('select', 'dateformat', get_string('dateformat', 'customcertelement_expiry'), self::get_date_formats()); + $mform->addHelpButton('dateformat', 'dateformat', 'customcertelement_expiry'); + + $startdates['award'] = get_string('awarddate', 'customcertelement_expiry'); + + if ($CFG->enablecompletion && ($COURSE->id == SITEID || $COURSE->enablecompletion)) { + $startdates['coursecomplete'] = get_string('completiondate', 'customcertelement_expiry'); + } + + $mform->addElement('select', 'startfrom', get_string('startfrom', 'customcertelement_expiry'), $startdates); + $mform->addHelpButton('startfrom', 'startfrom', 'customcertelement_expiry'); + + parent::render_form_elements($mform); + } + + /** + * This will handle how form data will be saved into the data column in the + * customcert_elements table. + * + * @param \stdClass $data the form data + * @return string the json encoded array + */ + public function save_unique_data($data) { + // Array of data we will be storing in the database. + $arrtostore = [ + 'dateitem' => $data->dateitem, + 'dateformat' => $data->dateformat, + 'startfrom' => $data->startfrom, + ]; + + // Encode these variables before saving into the DB. + return json_encode($arrtostore); + } + + /** + * Handles rendering the element on the pdf. + * + * @param \pdf $pdf the pdf object + * @param bool $preview true if it is a preview, false otherwise + * @param \stdClass $user the user we are rendering this for + */ + public function render($pdf, $preview, $user) { + global $DB; + + // If there is no element data, we have nothing to display. + if (empty($this->get_data())) { + return; + } + + $courseid = element_helper::get_courseid($this->id); + $dateinfo = json_decode($this->get_data()); + $dateformat = $dateinfo->dateformat; + $dateitem = $dateinfo->dateitem; + $date = $this->expiry($user->id, $preview); + + // Ensure that a date has been set. + if (!empty($date)) { + if ($dateformat == 'validfor') { + if ($dateitem == self::EXPIRY_ONE) { + element_helper::render_content($pdf, $this, 'Valid for 1 year'); + } else if ($dateitem == self::EXPIRY_TWO) { + element_helper::render_content($pdf, $this, 'Valid for 2 years'); + } else if ($dateitem == self::EXPIRY_THREE) { + element_helper::render_content($pdf, $this, 'Valid for 3 years'); + } else if ($dateitem == self::EXPIRY_FOUR) { + element_helper::render_content($pdf, $this, 'Valid for 4 years'); + } else if ($dateitem == self::EXPIRY_FIVE) { + element_helper::render_content($pdf, $this, 'Valid for 5 years'); + } + } else { + element_helper::render_content($pdf, $this, element_helper::get_date_format_string($date, $dateformat)); + } + } + } + + /** + * Render the element in html. + * + * This function is used to render the element when we are using the + * drag and drop interface to position it. + * + * @return string the html + */ + public function render_html() { + // If there is no element data, we have nothing to display. + if (empty($this->get_data())) { + return; + } + + // Decode the information stored in the database. + $dateinfo = json_decode($this->get_data()); + $dateformat = $dateinfo->dateformat; + $dateitem = $dateinfo->dateitem; + + if ($dateformat == 'validfor') { + if ($dateitem == self::EXPIRY_ONE) { + return element_helper::render_html_content($this, get_string('validfor1year', 'customcertelement_expiry')); + } else if ($dateitem == self::EXPIRY_TWO) { + return element_helper::render_html_content($this, get_string('validfor2years', 'customcertelement_expiry')); + } else if ($dateitem == self::EXPIRY_THREE) { + return element_helper::render_html_content($this, get_string('validfor3years', 'customcertelement_expiry')); + } else if ($dateitem == self::EXPIRY_FOUR) { + return element_helper::render_html_content($this, get_string('validfor4years', 'customcertelement_expiry')); + } else if ($dateitem == self::EXPIRY_FIVE) { + return element_helper::render_html_content($this, get_string('validfor5years', 'customcertelement_expiry')); + } + } else { + return element_helper::render_html_content($this, element_helper::get_date_format_string( + strtotime($this->relative[$dateitem], time()), $dateformat)); + } + } + + /** + * Sets the data on the form when editing an element. + * + * @param \MoodleQuickForm $mform the edit_form instance + */ + public function definition_after_data($mform) { + // Set the item and format for this element. + if (!empty($this->get_data())) { + $dateinfo = json_decode($this->get_data()); + + $element = $mform->getElement('dateitem'); + $element->setValue($dateinfo->dateitem); + + $element = $mform->getElement('dateformat'); + $element->setValue($dateinfo->dateformat); + + $element = $mform->getElement('startfrom'); + $element->setValue($dateinfo->startfrom); + } + + parent::definition_after_data($mform); + } + + /** + * This function is responsible for handling the restoration process of the element. + * + * We will want to update the course module the date element is pointing to as it will + * have changed in the course restore. + * + * @param \restore_customcert_activity_task $restore + */ + public function after_restore($restore) { + global $DB; + + $dateinfo = json_decode($this->get_data()); + + $isgradeitem = false; + $oldid = $dateinfo->dateitem; + if (strpos($dateinfo->dateitem, 'gradeitem:') === 0) { + $isgradeitem = true; + $oldid = str_replace('gradeitem:', '', $dateinfo->dateitem); + } + + $itemname = $isgradeitem ? 'grade_item' : 'course_module'; + if ($newitem = \restore_dbops::get_backup_ids_record($restore->get_restoreid(), $itemname, $oldid)) { + $dateinfo->dateitem = ''; + if ($isgradeitem) { + $dateinfo->dateitem = 'gradeitem:'; + } + $dateinfo->dateitem = $dateinfo->dateitem . $newitem->newitemid; + $DB->set_field('customcert_elements', 'data', $this->save_unique_data($dateinfo), ['id' => $this->get_id()]); + } + } + + /** + * Helper function to return all the date formats. + * + * @return array the list of date formats + */ + private static function get_date_formats(): array { + $dateformats = element_helper::get_date_formats(); + $dateformats['validfor'] = get_string('validfor', 'customcertelement_expiry'); + + return $dateformats; + } + + /** + * Get expiry date for user. + * + * @param int $userid User who has been awarded certificate. + * @param bool $preview True if it is a preview in which case calculate + * expiry date from now, false otherwise. + * @return int Timestamp in Unix format (number of seconds since epoch). + */ + private function expiry($userid, $preview = false) { + global $DB; + + $dateinfo = json_decode($this->get_data()); + $dateitem = $dateinfo->dateitem; + $startfrom = $dateinfo->startfrom; + $starttime = null; + + if ($preview) { + $starttime = time(); + } else if ($startfrom == 'coursecomplete') { + $courseid = \mod_customcert\element_helper::get_courseid($this->id); + // Get the last completion date. + $sql = "SELECT MAX(c.timecompleted) as timecompleted + FROM {course_completions} c + WHERE c.userid = :userid + AND c.course = :courseid"; + if ($timecompleted = $DB->get_record_sql($sql, ['userid' => $userid, 'courseid' => $courseid])) { + if (!empty($timecompleted->timecompleted)) { + $starttime = $timecompleted->timecompleted; + } + } + } else { // Expiry date calculated from certificate award date. + // Get the page. + $page = $DB->get_record('customcert_pages', ['id' => $this->get_pageid()], '*', MUST_EXIST); + // Get the customcert this page belongs to. + $customcert = $DB->get_record('customcert', ['templateid' => $page->templateid], '*', MUST_EXIST); + // Now we can get the issue for this user. + $issue = $DB->get_record('customcert_issues', ['userid' => $userid, 'customcertid' => $customcert->id], + '*', IGNORE_MULTIPLE); + $starttime = $issue->timecreated; + } + + if (is_null($starttime)) { + return 0; + } + + return strtotime($this->relative[$dateitem], $starttime); + } + + /** + * Does this certificate have one or more expiry elements? + * + * @param int $customcertid ID of the certificate. + * @return bool True if this certificate has an expiry element (and thus + * can show an expiry date for reports), false otherwise. + */ + public static function has_expiry($customcertid): bool { + global $DB; + $sql = "SELECT e.id + FROM {customcert_elements} e + JOIN {customcert_pages} p ON e.pageid = p.id + JOIN {customcert} c ON p.templateid = c.templateid + WHERE element = 'expiry' AND c.id = :customcertid"; + return !empty($DB->get_records_sql($sql, ['customcertid' => $customcertid])); + } + + /** + * Return the expiry date for this certificate wrapped in a . + * + * @param int $customcertid The certificate. + * @param int $userid The user who has been awarded this certificate. + * @return string HTML fragment, for example, 'Monday, 6 July 2026, 2:40 PM' + */ + public static function get_expiry_html(int $customcertid, int $userid): string { + global $OUTPUT; + $expiry = self::get_expiry_date($customcertid, $userid); + + // This can happen if the 'startfrom' date is course completion and the + // student hasn't completed the course but has been awarded a + // certificate. + if (empty($expiry)) { + return ''; + } + + $data = new \stdClass(); + $data->date = userdate($expiry); + $expired = ($expiry - time()) / DAYSECS; + + if ($expired < 0) { + $data->expiry = "expired"; + } else if ($expired < 14) { + $data->expiry = "expire-soon"; + } else { + $data->expiry = "ok"; + } + + return $OUTPUT->render_from_template('customcertelement_expiry/date', $data); + } + + /** + * Return the expiry date for this certificate. If there are multiple + * expiry elements for the given certificate then the date is calculated + * using the settings for the first element returned by the database. + * (Multiple elements are supported as date elements using dateitem = -8 to + * -12 are migrated to this element with no restriction on the number of + * elements). + * + * @param int $customcertid The certificate. + * @param int $userid The user who has been awarded this certificate. + * @return int Timestamp in Unix format (number of seconds since epoch). + */ + public static function get_expiry_date(int $customcertid, int $userid): int { + global $DB; + $sql = "SELECT e.* + FROM {customcert_elements} e + JOIN {customcert_pages} p ON e.pageid = p.id + JOIN {customcert} c ON p.templateid = c.templateid + WHERE element = 'expiry' AND c.id = :customcertid"; + + // As it's permitted to have more than one expiry element on a + // certificate we use the first returned by this query to calculate the + // expiry date for reporting. + $expirydata = $DB->get_records_sql($sql, ['customcertid' => $customcertid], 0, 1); + $element = new self(reset($expirydata)); + return $element->expiry($userid); + } +} diff --git a/element/expiry/classes/privacy/provider.php b/element/expiry/classes/privacy/provider.php new file mode 100644 index 00000000..bffe24cf --- /dev/null +++ b/element/expiry/classes/privacy/provider.php @@ -0,0 +1,44 @@ +. + +/** + * Privacy Subsystem implementation for customcertelement_expiry. + * + * @package customcertelement_expiry + * @copyright 2024 Leon Stringer + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace customcertelement_expiry\privacy; + +/** + * Privacy Subsystem for customcertelement_expiry implementing null_provider. + * + * @copyright 2024 Leon Stringer + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class provider implements \core_privacy\local\metadata\null_provider { + + /** + * Get the language string identifier with the component's language + * file to explain why this plugin stores no data. + * + * @return string + */ + public static function get_reason(): string { + return 'privacy:metadata'; + } +} diff --git a/element/expiry/lang/en/customcertelement_expiry.php b/element/expiry/lang/en/customcertelement_expiry.php new file mode 100644 index 00000000..768ac721 --- /dev/null +++ b/element/expiry/lang/en/customcertelement_expiry.php @@ -0,0 +1,54 @@ +. + +/** + * Strings for component 'customcertelement_expiry', language 'en'. + * + * @package customcertelement_expiry + * @copyright 2024 Leon Stringer + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +$string['awarddate'] = 'Certificate awarded'; +$string['completiondate'] = 'Completion date'; +$string['courseenddate'] = 'Course end date'; +$string['coursegradedate'] = 'Course grade date'; +$string['coursestartdate'] = 'Course start date'; +$string['currentdate'] = 'Current date'; +$string['dateformat'] = 'Date format'; +$string['dateformat_help'] = 'This is the format of the date that will be displayed'; +$string['dateitem'] = 'Date item'; +$string['dateitem_help'] = 'This will be the date that is printed on the certificate'; +$string['enrolmentenddate'] = 'Enrolment end date'; +$string['enrolmentstartdate'] = 'Enrolment start date'; +$string['expireson'] = 'Expires on'; +$string['expirydatefive'] = 'Expiry date (5 years)'; +$string['expirydatefour'] = 'Expiry date (4 years)'; +$string['expirydateone'] = 'Expiry date (1 year)'; +$string['expirydatethree'] = 'Expiry date (3 years)'; +$string['expirydatetwo'] = 'Expiry date (2 years)'; +$string['issueddate'] = 'Issued date'; +$string['pluginname'] = 'Expiry'; +$string['privacy:metadata'] = 'The Expiry plugin does not store any personal data.'; +$string['startfrom'] = 'Start date'; +$string['startfrom_help'] = 'Date when the expiry date should be calculated from'; +$string['userdateformat'] = 'User date format'; +$string['validfor'] = 'Valid for ... year(s)'; +$string['validfor1year'] = 'Valid for one year'; +$string['validfor2years'] = 'Valid for two years'; +$string['validfor3years'] = 'Valid for three years'; +$string['validfor4years'] = 'Valid for four years'; +$string['validfor5years'] = 'Valid for five years'; diff --git a/element/expiry/templates/date.mustache b/element/expiry/templates/date.mustache new file mode 100644 index 00000000..27ec7b6d --- /dev/null +++ b/element/expiry/templates/date.mustache @@ -0,0 +1,35 @@ +{{! + This file is part of Moodle - http://moodle.org/ + + Moodle is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Moodle is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Moodle. If not, see . +}} +{{! + @template customcertelement_expiry/date + + The certificate's expiry date as shown in HTML reports (this is not used to + render the date on the certificate). + + Context variables required for this template: + * expiry A string describing the expiry status, one of "ok" (certificate + has not expired and is valid for the next two weeks, "soon" (certificate + expires in the next two weeks), "expired" (certificate has expired). + * date Human-readable date string. + + Example context (json): + { + "expiry": "ok", + "date": "Thursday, 16 July 2026, 3:40 PM" + } +}} +{{date}} diff --git a/element/expiry/version.php b/element/expiry/version.php new file mode 100644 index 00000000..482df91e --- /dev/null +++ b/element/expiry/version.php @@ -0,0 +1,29 @@ +. + +/** + * This file contains the version information for the date plugin. + * + * @package customcertelement_expiry + * @copyright 2024 Leon Stringer + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die('Direct access to this script is forbidden.'); + +$plugin->version = 2024072200; // The current module version (Date: YYYYMMDDXX). +$plugin->requires = 2022112800; // Requires this Moodle version (4.1). +$plugin->component = 'customcertelement_expiry'; diff --git a/lang/en/customcert.php b/lang/en/customcert.php index dcd218c4..29b5880a 100644 --- a/lang/en/customcert.php +++ b/lang/en/customcert.php @@ -33,6 +33,10 @@ $string['awardedto'] = 'Awarded to'; $string['cannotverifyallcertificates'] = 'You do not have the permission to verify all certificates on the site.'; $string['certificate'] = 'Certificate'; +$string['certificateexecutionperiod'] = 'Certificate execution period'; +$string['certificateexecutionperiod_desc'] = 'If not 0, the task will not process certificates whose course has been inactive or the last issue is older than the configured time. This may help to improve the performance of the scheduled task.'; +$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['copy'] = 'Copy'; $string['coursetimereq'] = 'Required minutes in course'; @@ -41,22 +45,20 @@ $string['createtemplate'] = 'Create template'; $string['customcert:addinstance'] = 'Add a new custom certificate instance'; $string['customcert:manage'] = 'Manage a custom certificate'; +$string['customcert:manageemailothers'] = 'Manage email others setting'; $string['customcert:manageemailstudents'] = 'Manage email students setting'; $string['customcert:manageemailteachers'] = 'Manage email teachers setting'; -$string['customcert:manageemailothers'] = 'Manage email others setting'; -$string['customcert:manageverifyany'] = 'Manage verification setting'; -$string['customcert:managerequiredtime'] = 'Manage time required setting'; +$string['customcert:managelanguages'] = 'Manage language on edit form'; $string['customcert:manageprotection'] = 'Manage protection setting'; +$string['customcert:managerequiredtime'] = 'Manage time required setting'; +$string['customcert:manageverifyany'] = 'Manage verification setting'; $string['customcert:receiveissue'] = 'Receive a certificate'; -$string['customcert:view'] = 'View a custom certificate'; -$string['customcert:viewreport'] = 'View course report'; -$string['customcert:viewallcertificates'] = 'View all certificates'; $string['customcert:verifyallcertificates'] = 'Verify all certificates on the site'; $string['customcert:verifycertificate'] = 'Verify a certificate'; +$string['customcert:view'] = 'View a custom certificate'; +$string['customcert:viewallcertificates'] = 'View all certificates'; +$string['customcert:viewreport'] = 'View course report'; $string['customcertsettings'] = 'Custom certificate settings'; -$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.'; $string['deletecertpage'] = 'Delete page'; $string['deleteconfirm'] = 'Delete confirmation'; $string['deleteelement'] = 'Delete element'; @@ -69,6 +71,9 @@ $string['deliveryoptioninline'] = 'Send the file inline to the browser'; $string['deliveryoptions'] = 'Delivery options'; $string['description'] = 'Description'; +$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.'; $string['duplicate'] = 'Duplicate'; $string['duplicateconfirm'] = 'Duplicate confirmation'; $string['duplicatetemplateconfirm'] = 'Are you sure you want to duplicate this certificate template?'; @@ -91,17 +96,17 @@ $string['emailnonstudentcertificatelinktext'] = 'View certificate report'; $string['emailnonstudentgreeting'] = 'Hi'; $string['emailnonstudentsubject'] = '{$a->coursefullname}: {$a->certificatename}'; +$string['emailothers'] = 'Email others'; +$string['emailothers_help'] = 'If set this will email the email addresses listed here (separated by a comma) with a copy of the certificate when it becomes available. Warning: Setting this field before you have finished creating the certificate will email the addresses an incomplete certificate.'; $string['emailstudentbody'] = 'Attached is your certificate \'{$a->certificatename}\' for the course \'{$a->coursefullname}\'.'; $string['emailstudentbodyplaintext'] = 'Attached is your certificate \'{$a->certificatename}\' for the course \'{$a->coursefullname}\'.'; $string['emailstudentcertificatelinktext'] = 'View certificate'; $string['emailstudentgreeting'] = 'Dear {$a}'; -$string['emailstudentsubject'] = '{$a->coursefullname}: {$a->certificatename}'; $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['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.'; -$string['emailothers'] = 'Email others'; -$string['emailothers_help'] = 'If set this will email the email addresses listed here (separated by a comma) with a copy of the certificate when it becomes available. Warning: Setting this field before you have finished creating the certificate will email the addresses an incomplete certificate.'; $string['eventelementcreated'] = 'Custom certificate element created'; $string['eventelementdeleted'] = 'Custom certificate element deleted'; $string['eventelementupdated'] = 'Custom certificate element updated'; @@ -122,16 +127,19 @@ $string['gradeoutcome'] = 'Outcome'; $string['height'] = 'Height'; $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['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.'; $string['invalidelementwidthorheightzeroallowed'] = 'Please enter a number greater than or equal to 0.'; $string['invalidelementwidthorheightzeronotallowed'] = 'Please enter a number greater than 0.'; -$string['invalidposition'] = 'Please select a positive number for position {$a}.'; $string['invalidheight'] = 'The height has to be a valid number greater than 0.'; $string['invalidmargin'] = 'The margin has to be a valid number greater than 0.'; +$string['invalidposition'] = 'Please select a positive number for position {$a}.'; $string['invalidwidth'] = 'The width has to be a valid number greater than 0.'; $string['landscape'] = 'Landscape'; +$string['languageoptions'] = 'Force Certificate Language'; $string['leftmargin'] = 'Left margin'; $string['leftmargin_help'] = 'This is the left margin of the certificate PDF in mm.'; $string['listofissues'] = 'Recipients: {$a}'; @@ -142,9 +150,9 @@ $string['managetemplatesdesc'] = 'This link will take you to a new screen where you will be able to manage templates used by Custom certificate activities in courses.'; $string['modify'] = 'Modify'; $string['modulename'] = 'Custom certificate'; -$string['modulenameplural'] = 'Custom certificates'; $string['modulename_help'] = 'This module allows for the dynamic generation of PDF certificates.'; $string['modulename_link'] = 'Custom_certificate_module'; +$string['modulenameplural'] = 'Custom certificates'; $string['mycertificates'] = 'My certificates'; $string['mycertificatesdescription'] = 'These are the certificates you have been issued by either email or downloading manually.'; $string['name'] = 'Name'; @@ -155,6 +163,9 @@ $string['notemplates'] = 'No templates'; $string['notissued'] = 'Not awarded'; $string['notverified'] = 'Not verified'; +$string['numbersuffix_nd_as_in_second'] = 'nd'; +$string['numbersuffix_rd_as_in_third'] = 'rd'; +$string['numbersuffix_st_as_in_first'] = 'st'; $string['options'] = 'Options'; $string['page'] = 'Page {$a}'; $string['pluginadministration'] = 'Custom certificate administration'; @@ -166,10 +177,10 @@ $string['posy_help'] = 'This is the position in mm from the top left corner you wish the element\'s reference point to locate in the y direction.'; $string['preventcopy'] = 'Prevent copy'; $string['preventcopy_desc'] = 'Enable protection from copy action.'; -$string['preventprint'] = 'Prevent print'; -$string['preventprint_desc'] = 'Enable protection from print action.'; $string['preventmodify'] = 'Prevent modify'; $string['preventmodify_desc'] = 'Enable protection from modify action.'; +$string['preventprint'] = 'Prevent print'; +$string['preventprint_desc'] = 'Enable protection from print action.'; $string['print'] = 'Print'; $string['privacy:metadata:customcert_issues'] = 'The list of issued certificates'; $string['privacy:metadata:customcert_issues:code'] = 'The code that belongs to the certificate'; @@ -187,11 +198,13 @@ $string['rightmargin'] = 'Right margin'; $string['rightmargin_help'] = 'This is the right margin of the certificate PDF in mm.'; $string['save'] = 'Save'; -$string['savechanges'] = 'Save changes'; $string['saveandclose'] = 'Save and close'; $string['saveandcontinue'] = 'Save and continue'; +$string['savechanges'] = 'Save changes'; $string['savechangespreview'] = 'Save changes and preview'; $string['savetemplate'] = 'Save template'; +$string['scheduledtaskconfigdesc'] = 'Configure the settings for the scheduled task that processes certificates.'; +$string['scheduledtaskconfigheading'] = 'Scheduled task configuration'; $string['search:activity'] = 'Custom certificate - activity information'; $string['setprotection'] = 'Set protection'; $string['setprotection_help'] = 'Choose the actions you wish to prevent users from performing on this certificate.'; @@ -202,6 +215,7 @@ $string['subplugintype_customcertelement'] = 'Element'; $string['subplugintype_customcertelement_plural'] = 'Elements'; $string['taskemailcertificate'] = 'Handles emailing certificates.'; +$string['taskissuecertificate'] = 'Issue certificates task'; $string['templatename'] = 'Template name'; $string['templatenameexists'] = 'That template name is currently in use, please choose another.'; $string['topcenter'] = 'Center'; @@ -211,6 +225,12 @@ $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_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['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'; $string['verify'] = 'Verify'; $string['verifyallcertificates'] = 'Allow verification of all certificates'; @@ -218,15 +238,8 @@ Note - this only applies to certificates where \'Allow anyone to verify a certificate\' has been set to \'Yes\' in the certificate settings.'; $string['verifycertificate'] = 'Verify certificate'; -$string['verifycertificatedesc'] = 'This link will take you to a new screen where you will be able to verify certificates on the site'; $string['verifycertificateanyone'] = 'Allow anyone to verify a certificate'; $string['verifycertificateanyone_help'] = 'This setting enables anyone with the certificate verification link (including users not logged in) to verify a certificate.'; +$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['userlanguage'] = 'Use user preferences'; -$string['languageoptions'] = 'Force Certificate Language'; -$string['userlanguage_help'] = 'You can force the language of the certificate to override the user\'s language preferences.'; - -// Acess API. -$string['customcert:managelanguages'] = 'Manage language on edit form'; diff --git a/settings.php b/settings.php index 5b291566..955430de 100644 --- a/settings.php +++ b/settings.php @@ -58,6 +58,26 @@ new moodle_url('/mod/customcert/download_all_certificates.php'), '')); } +$settings->add(new admin_setting_heading('scheduledtaskconfig', + get_string('scheduledtaskconfigheading', 'customcert'), + get_string('scheduledtaskconfigdesc', 'customcert'))); + +$settings->add(new admin_setting_configtext('customcert/certificatesperrun', + get_string('certificatesperrun', 'customcert'), + get_string('certificatesperrun_desc', 'customcert'), 0, PARAM_INT)); + +$settings->add(new admin_setting_configcheckbox('customcert/includeinnotvisiblecourses', + get_string('includeinnotvisiblecourses', 'customcert'), + get_string('includeinnotvisiblecourses_desc', 'customcert'), 0)); + +$settings->add(new admin_setting_configcheckbox('customcert/useadhoc', + get_string('useadhoc', 'customcert'), + get_string('useadhoc_desc', 'customcert'), 0)); + +$settings->add(new admin_setting_configduration('customcert/certificateexecutionperiod', + get_string('certificateexecutionperiod', 'customcert'), + get_string('certificateexecutionperiod_desc', 'customcert'), 365 * DAYSECS)); + $settings->add(new admin_setting_heading('defaults', get_string('modeditdefaults', 'admin'), get_string('condifmodeditdefaults', 'admin'))); @@ -65,7 +85,6 @@ 0 => get_string('no'), 1 => get_string('yes'), ]; - $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/templates/mobile_view_activity_page_ionic3.mustache b/templates/mobile_view_activity_page_ionic3.mustache deleted file mode 100644 index 72d2e2de..00000000 --- a/templates/mobile_view_activity_page_ionic3.mustache +++ /dev/null @@ -1,175 +0,0 @@ -{{! - This file is part of Moodle - http://moodle.org/ - - Moodle is free software: you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - Moodle is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with Moodle. If not, see . -}} -{{! - @template mod_customcert/mobile_view_activity_page - - The main page to view the custom certificate activity - - Classes required for JS: - * None - - Data attibutes required for JS: - * All data attributes are required - - Context variables required for this template: - * certificate - * cmid - * hasissues - * issues - * showgroups - * groups - * canmanage - * requiredtimemet - * hasrecipients - * recipients - * fileurl - * showreport - * currenttimestamp - - Example context (json): - { - "certificate": { - "id": "1", - "course": "2", - "name": "A rad certificate name!", - "intro": "A certificate", - "requiredtime": "60" - }, - "cmid": "25", - "issue": { - "timecreated": "1528370177" - }, - "showgroups": "true", - "groups": [ - { - "id": "2", - "selected": "false", - "name": "Group A" - } - ], - "canmanage": "true", - "requiredtimemet": "true", - "fileurl": "http://yoursite.com/mod/customcert/mobile/pluginfile.php?id=4", - "showreport": "true", - "hasrecipients": "true", - "recipients": [ - { - "id": "2", - "issueid": "3", - "displayname": "Michaelangelo (Mickey)", - "fileurl": "http://yoursite.com/mod/customcert/mobile/pluginfile.php?id=4", - "timecreated": "1528370177" - } - ], - "currenttimestamp": "1528370177" - } -}} -{{=<% %>=}} -
- - <%^canmanage%> - <%#requiredtimemet%> - - - - - {{ 'plugin.mod_customcert.receiveddate' | translate }} -
-
- <%#issue%> - {{ <% timecreated %> | coreToLocaleString }} - <%/issue%> - <%^issue%> - {{ 'plugin.mod_customcert.notissued' | translate }} - <%/issue%> -
-
- - - -
-
-
- <%/requiredtimemet%> - <%^requiredtimemet%> - -

{{ 'plugin.mod_customcert.requiredtimenotmet' | translate: {$a: { requiredtime: <% certificate.requiredtime %>} } }}

-
- <%/requiredtimemet%> - <%/canmanage%> - <%#canmanage%> - - - - <%/canmanage%> - <%#showreport%> - - {{ 'plugin.mod_customcert.listofissues' | translate }} - - <%#showgroups%> - - {{ 'plugin.mod_customcert.selectagroup' | translate }} - - <%#groups%> - selected<%/selected%>><% name %> - <%/groups%> - - - <%/showgroups%> - <%#hasrecipients%> - - <%#recipients%> - - - - - <% displayname %> -
-
{{ <% timecreated %> | coreToLocaleString }}
-
- - - <%#canmanage%> - - <%/canmanage%> - -
-
-
- <%/recipients%> -
- <%/hasrecipients%> - <%^hasrecipients%> - - {{ 'plugin.mod_customcert.nothingtodisplay' | translate }} - - <%/hasrecipients%> - <%/showreport%> -
diff --git a/templates/mobile_view_activity_page_ionic5.mustache b/templates/mobile_view_activity_page_ionic5.mustache new file mode 100644 index 00000000..6559f42e --- /dev/null +++ b/templates/mobile_view_activity_page_ionic5.mustache @@ -0,0 +1,169 @@ +{{! + This file is part of Moodle - http://moodle.org/ + + Moodle is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Moodle is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Moodle. If not, see . +}} +{{! + @template mod_customcert/mobile_view_activity_page + + The main page to view the custom certificate activity + + Classes required for JS: + * None + + Data attibutes required for JS: + * All data attributes are required + + Context variables required for this template: + * certificate + * cmid + * hasissues + * issues + * showgroups + * groups + * canmanage + * requiredtimemet + * hasrecipients + * recipients + * fileurl + * showreport + * currenttimestamp + + Example context (json): + { + "certificate": { + "id": "1", + "course": "2", + "name": "A rad certificate name!", + "intro": "A certificate", + "requiredtime": "60" + }, + "cmid": "25", + "issue": { + "timecreated": "1528370177" + }, + "showgroups": "true", + "groups": [ + { + "id": "2", + "selected": "false", + "name": "Group A" + } + ], + "canmanage": "true", + "requiredtimemet": "true", + "fileurl": "http://yoursite.com/mod/customcert/mobile/pluginfile.php?id=4", + "showreport": "true", + "hasrecipients": "true", + "recipients": [ + { + "id": "2", + "issueid": "3", + "displayname": "Michaelangelo (Mickey)", + "fileurl": "http://yoursite.com/mod/customcert/mobile/pluginfile.php?id=4", + "timecreated": "1528370177" + } + ], + "currenttimestamp": "1528370177" + } +}} +{{=<% %>=}} + + +<%^canmanage%> + <%#requiredtimemet%> + + + {{ 'plugin.mod_customcert.receiveddate' | translate }} +
+
+ <%#issue%> + {{ <% timecreated %> * 1000 | coreFormatDate }} + <%/issue%> + <%^issue%> + {{ 'plugin.mod_customcert.notissued' | translate }} + <%/issue%> +
+
+
+ + + +
+
+ <%/requiredtimemet%> + <%^requiredtimemet%> + + +

{{ 'plugin.mod_customcert.requiredtimenotmet' | translate: {$a: { requiredtime: <% certificate.requiredtime %>} } }}

+
+
+ <%/requiredtimemet%> +<%/canmanage%> +<%#canmanage%> + + + {{ 'plugin.mod_customcert.getcustomcert' | translate }} + +<%/canmanage%> +<%#showreport%> + + + {{ 'plugin.mod_customcert.listofissues' | translate: { $a: <% numrecipients %> } }} + + + <%#showgroups%> + + {{ 'plugin.mod_customcert.selectagroup' | translate }} + + <%#groups%> + <% name %> + <%/groups%> + + + <%/showgroups%> + <%#hasrecipients%> + <%#recipients%> + + + <% displayname %> +
+
{{ <% timecreated %> * 1000 | coreFormatDate }}
+
+
+ + + + <%#canmanage%> + + + + <%/canmanage%> +
+
+ <%/recipients%> + <%/hasrecipients%> + <%^hasrecipients%> + + + {{ 'plugin.mod_customcert.nothingtodisplay' | translate }} + + + <%/hasrecipients%> +<%/showreport%> +
diff --git a/templates/mobile_view_activity_page_latest.mustache b/templates/mobile_view_activity_page_latest.mustache index 6559f42e..6eb9fac0 100644 --- a/templates/mobile_view_activity_page_latest.mustache +++ b/templates/mobile_view_activity_page_latest.mustache @@ -79,7 +79,8 @@ } }} {{=<% %>=}} - + <%^canmanage%> <%#requiredtimemet%> @@ -125,8 +126,8 @@ <%#showgroups%> - {{ 'plugin.mod_customcert.selectagroup' | translate }} +

{{ 'plugin.mod_customcert.selectagroup' | translate }}

<%#groups%> <% name %> <%/groups%> diff --git a/templates/verify_certificate_result.mustache b/templates/verify_certificate_result.mustache index 8b71a6b2..933cc566 100644 --- a/templates/verify_certificate_result.mustache +++ b/templates/verify_certificate_result.mustache @@ -45,4 +45,7 @@
  • {{#str}}fullname{{/str}}: {{userfullname}}
  • {{#str}}course{{/str}}: {{coursefullname}}
  • {{#str}}certificate, customcert{{/str}}: {{certificatename}}
  • + {{#expiry}} +
  • {{#str}}expireson, customcertelement_expiry{{/str}}: {{{.}}}
  • + {{/expiry}} diff --git a/tests/behat/managing_elements.feature b/tests/behat/managing_elements.feature index 4bb7bea1..d52ed8ca 100644 --- a/tests/behat/managing_elements.feature +++ b/tests/behat/managing_elements.feature @@ -139,6 +139,30 @@ Feature: Being able to manage elements in a certificate template | Width | 20 | | Reference point location | Top left | And I press "Save changes" + # Expiry. + And I add the element "Expiry" to page "1" of the "Custom certificate 1" certificate template + And I set the following fields to these values: + | Date item | Expiry date (1 year) | + | Date format | 2 | + | Start date | award | + | Font | Helvetica | + | Size | 20 | + | Colour | #045ECD | + | Width | 20 | + | Reference point location | Top left | + And I press "Save changes" + And I should see "Expiry" in the "elementstable" "table" + And I click on ".edit-icon" "css_element" in the "Expiry" "table_row" + And the following fields match these values: + | Date item | Expiry date (1 year) | + | Date format | 2 | + | Start date | award | + | Font | Helvetica | + | Size | 20 | + | Colour | #045ECD | + | Width | 20 | + | Reference point location | Top left | + And I press "Save changes" # Date range. And I add the element "Date range" to page "1" of the "Custom certificate 1" certificate template And I set the following fields to these values: diff --git a/tests/email_certificate_task_test.php b/tests/email_certificate_task_test.php index 93910d8b..cc9f3ad8 100644 --- a/tests/email_certificate_task_test.php +++ b/tests/email_certificate_task_test.php @@ -25,10 +25,12 @@ namespace mod_customcert; +use completion_info; use stdClass; use context_course; use advanced_testcase; use mod_customcert\task\email_certificate_task; +use mod_customcert\task\issue_certificates_task; /** * Unit tests for the email certificate task. @@ -46,11 +48,13 @@ class email_certificate_task_test extends advanced_testcase { */ public function setUp(): void { $this->resetAfterTest(); + set_config('certificateexecutionperiod', 0, 'customcert'); } /** * Tests the email certificate task when there are no elements. * + * @covers \mod_customcert\task\issue_certificates_task * @covers \mod_customcert\task\email_certificate_task */ public function test_email_certificates_no_elements() { @@ -68,7 +72,7 @@ public function test_email_certificates_no_elements() { // Run the task. $sink = $this->redirectEmails(); - $task = new email_certificate_task(); + $task = new issue_certificates_task(); $task->execute(); $emails = $sink->get_messages(); @@ -79,6 +83,7 @@ public function test_email_certificates_no_elements() { /** * Tests the email certificate task for users without a capability to receive a certificate. * + * @covers \mod_customcert\task\issue_certificates_task * @covers \mod_customcert\task\email_certificate_task */ public function test_email_certificates_no_cap() { @@ -119,7 +124,7 @@ public function test_email_certificates_no_cap() { // Run the task. $sink = $this->redirectEmails(); - $task = new email_certificate_task(); + $task = new issue_certificates_task(); $task->execute(); $emails = $sink->get_messages(); @@ -130,6 +135,7 @@ public function test_email_certificates_no_cap() { /** * Tests the email certificate task for students. * + * @covers \mod_customcert\task\issue_certificates_task * @covers \mod_customcert\task\email_certificate_task */ public function test_email_certificates_students() { @@ -179,7 +185,7 @@ public function test_email_certificates_students() { // Run the task. $sink = $this->redirectEmails(); - $task = new email_certificate_task(); + $task = new issue_certificates_task(); $task->execute(); $emails = $sink->get_messages(); @@ -204,7 +210,7 @@ public function test_email_certificates_students() { // Now, run the task again and ensure we did not issue any more certificates. $sink = $this->redirectEmails(); - $task = new email_certificate_task(); + $task = new issue_certificates_task(); $task->execute(); $emails = $sink->get_messages(); @@ -217,6 +223,7 @@ public function test_email_certificates_students() { /** * Tests the email certificate task for teachers. * + * @covers \mod_customcert\task\issue_certificates_task * @covers \mod_customcert\task\email_certificate_task */ public function test_email_certificates_teachers() { @@ -260,7 +267,7 @@ public function test_email_certificates_teachers() { // Run the task. $sink = $this->redirectEmails(); - $task = new email_certificate_task(); + $task = new issue_certificates_task(); $task->execute(); $emails = $sink->get_messages(); @@ -277,6 +284,7 @@ public function test_email_certificates_teachers() { /** * Tests the email certificate task for others. * + * @covers \mod_customcert\task\issue_certificates_task * @covers \mod_customcert\task\email_certificate_task */ public function test_email_certificates_others() { @@ -315,7 +323,7 @@ public function test_email_certificates_others() { // Run the task. $sink = $this->redirectEmails(); - $task = new email_certificate_task(); + $task = new issue_certificates_task(); $task->execute(); $emails = $sink->get_messages(); @@ -332,6 +340,7 @@ public function test_email_certificates_others() { /** * Tests the email certificate task when the certificate is not visible. * + * @covers \mod_customcert\task\issue_certificates_task * @covers \mod_customcert\task\email_certificate_task */ public function test_email_certificates_students_not_visible() { @@ -371,7 +380,7 @@ public function test_email_certificates_students_not_visible() { // Run the task. $sink = $this->redirectEmails(); - $task = new email_certificate_task(); + $task = new issue_certificates_task(); $task->execute(); $emails = $sink->get_messages(); @@ -386,6 +395,7 @@ public function test_email_certificates_students_not_visible() { /** * Tests the email certificate task when the student has not met the required time for the course. * + * @covers \mod_customcert\task\issue_certificates_task * @covers \mod_customcert\task\email_certificate_task */ public function test_email_certificates_students_havent_met_required_time() { @@ -425,7 +435,7 @@ public function test_email_certificates_students_havent_met_required_time() { // Run the task. $sink = $this->redirectEmails(); - $task = new email_certificate_task(); + $task = new issue_certificates_task(); $task->execute(); $emails = $sink->get_messages(); @@ -436,4 +446,280 @@ public function test_email_certificates_students_havent_met_required_time() { // Confirm no emails were sent. $this->assertCount(0, $emails); } + + /** + * Tests the email certificate task when the student has not met the completion criteria. + * + * @covers \mod_customcert\task\issue_certificates_task + * @covers \mod_customcert\task\email_certificate_task + */ + public function test_email_certificates_students_havent_met_required_criteria(): void { + global $CFG, $DB; + + $CFG->enablecompletion = true; + + // Create a course. + $course = $this->getDataGenerator()->create_course(['enablecompletion' => 1]); + + // Create a user. + $user1 = $this->getDataGenerator()->create_user(); + + // Enrol them in the course. + $this->getDataGenerator()->enrol_user($user1->id, $course->id); + + // Create a quiz. + $quiz = $this->getDataGenerator()->create_module('quiz', ['course' => $course->id]); + + $quizmodule = $DB->get_record('course_modules', ['id' => $quiz->cmid]); + + // Set completion criteria for the quiz. + $quizmodule->completion = COMPLETION_TRACKING_AUTOMATIC; + $quizmodule->completionview = 1; // Require view to complete. + $quizmodule->completionexpected = 0; + $DB->update_record('course_modules', $quizmodule); + + // Set restrict access to the customcert activity based on the completion of the quiz. + $customcert = $this->getDataGenerator()->create_module('customcert', [ + 'course' => $course->id, + 'emailstudents' => 1, + 'availability' => json_encode( + [ + 'op' => '&', + 'c' => [ + [ + 'type' => 'completion', + 'cm' => $quiz->cmid, + 'e' => COMPLETION_COMPLETE, // Ensure the quiz is marked as complete. + ], + ], + 'showc' => [ + false, + ], + ], + ), + ]); + + // Create template object. + $template = new stdClass(); + $template->id = $customcert->templateid; + $template->name = 'A template'; + $template->contextid = context_course::instance($course->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); + + // Run the task. + $sink = $this->redirectEmails(); + $task = new issue_certificates_task(); + $task->execute(); + $emails = $sink->get_messages(); + + // Confirm there are no issues as the user can not view the certificate. + $issues = $DB->get_records('customcert_issues'); + $this->assertCount(0, $issues); + + // Confirm no emails were sent. + $this->assertCount(0, $emails); + } + + /** + * Tests the email certificate task when the student has met the completion criteria. + * + * @covers \mod_customcert\task\issue_certificates_task + * @covers \mod_customcert\task\email_certificate_task + */ + public function test_email_certificates_students_have_met_required_criteria(): void { + global $CFG, $DB; + + $CFG->enablecompletion = true; + + // Create a course. + $course = $this->getDataGenerator()->create_course(['enablecompletion' => 1]); + + // Create a user. + $user1 = $this->getDataGenerator()->create_user(); + + // Enrol them in the course. + $this->getDataGenerator()->enrol_user($user1->id, $course->id); + + // Create a quiz. + $quiz = $this->getDataGenerator()->create_module('quiz', ['course' => $course->id]); + + $quizmodule = $DB->get_record('course_modules', ['id' => $quiz->cmid]); + + // Set completion criteria for the quiz. + $quizmodule->completion = COMPLETION_TRACKING_AUTOMATIC; + $quizmodule->completionview = 1; // Require view to complete. + $quizmodule->completionexpected = 0; + $DB->update_record('course_modules', $quizmodule); + + // Mark the quiz as complete for the user. + $completion = new completion_info($course); + $completion->update_state($quizmodule, COMPLETION_COMPLETE, $user1->id); + + // Set restrict access to the customcert activity based on the completion of the quiz. + $customcert = $this->getDataGenerator()->create_module('customcert', [ + 'course' => $course->id, + 'emailstudents' => 1, + 'availability' => json_encode( + [ + 'op' => '&', + 'c' => [ + [ + 'type' => 'completion', + 'cm' => $quiz->cmid, + 'e' => COMPLETION_COMPLETE, // Ensure the quiz is marked as complete. + ], + ], + 'showc' => [ + false, + ], + ], + ), + ]); + + // Create template object. + $template = new stdClass(); + $template->id = $customcert->templateid; + $template->name = 'A template'; + $template->contextid = context_course::instance($course->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); + + // Run the task. + $sink = $this->redirectEmails(); + $task = new issue_certificates_task(); + $task->execute(); + $emails = $sink->get_messages(); + + // Confirm there is an issue as the user can view the certificate. + $issues = $DB->get_records('customcert_issues'); + $this->assertCount(1, $issues); + + // Confirm an email was sent. + $this->assertCount(1, $emails); + } + + /** + * Tests the email certificate task running adhoc. + * + * @covers \mod_customcert\task\email_certificate_task + * @covers \mod_customcert\task\issue_certificates_task + */ + public function test_email_certificates_adhoc(): void { + global $CFG, $DB; + + set_config('useadhoc', 1, 'customcert'); + + // Create a course. + $course = $this->getDataGenerator()->create_course(); + + // Create some users. + $user1 = $this->getDataGenerator()->create_user(); + $user2 = $this->getDataGenerator()->create_user(); + $user3 = $this->getDataGenerator()->create_user(['firstname' => 'Teacher', 'lastname' => 'One']); + + // Enrol two of them in the course as students. + $roleids = $DB->get_records_menu('role', null, '', 'shortname, id'); + $this->getDataGenerator()->enrol_user($user1->id, $course->id); + $this->getDataGenerator()->enrol_user($user2->id, $course->id); + + // Enrol one of the users as a teacher. + $this->getDataGenerator()->enrol_user($user3->id, $course->id, $roleids['editingteacher']); + + // Create a custom certificate. + $customcert = $this->getDataGenerator()->create_module('customcert', ['course' => $course->id, + 'emailstudents' => 1]); + + // Create template object. + $template = new stdClass(); + $template->id = $customcert->templateid; + $template->name = 'A template'; + $template->contextid = context_course::instance($course->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(2, $issues); + + // Confirm that it wasn't marked as emailed and was not issued to the teacher. + foreach ($issues as $issue) { + $this->assertEquals(0, $issue->emailed); + $this->assertNotEquals($user3->id, $issue->userid); + } + + // Now we send emails to the two users using the adhoc method. + $this->assertCount(0, $emails); + $issues = array_values($issues); + $task = new email_certificate_task(); + $task->set_custom_data((object)['issueid' => $issues[0]->id, 'customcertid' => $customcert->id]); + $task->execute(); + $task->set_custom_data((object)['issueid' => $issues[1]->id, 'customcertid' => $customcert->id]); + $task->execute(); + $emails = $sink->get_messages(); + + // Get the issues from the issues table now. + $issues = $DB->get_records('customcert_issues'); + // Confirm that it wasn't marked as emailed and was not issued to the teacher. + foreach ($issues as $issue) { + $this->assertEquals(1, $issue->emailed); + $this->assertNotEquals($user3->id, $issue->userid); + } + + // Confirm that we sent out emails to the two users. + $this->assertCount(2, $emails); + + $this->assertEquals($CFG->noreplyaddress, $emails[0]->from); + $this->assertEquals($user1->email, $emails[0]->to); + + $this->assertEquals($CFG->noreplyaddress, $emails[1]->from); + $this->assertEquals($user2->email, $emails[1]->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(2, $issues); + $this->assertCount(0, $emails); + } } diff --git a/tests/generator/lib.php b/tests/generator/lib.php index 931d4a44..f15273c0 100644 --- a/tests/generator/lib.php +++ b/tests/generator/lib.php @@ -40,7 +40,7 @@ class mod_customcert_generator extends testing_module_generator { * @param array|null $options * @return stdClass */ - public function create_instance($record = null, array $options = null) { + public function create_instance($record = null, ?array $options = null) { $record = (object)(array)$record; $defaultsettings = [ diff --git a/verify_certificate.php b/verify_certificate.php index acd60f71..73448252 100644 --- a/verify_certificate.php +++ b/verify_certificate.php @@ -118,6 +118,13 @@ // It is possible (though unlikely) that there is the same code for issued certificates. if ($issues = $DB->get_records_sql($sql, $params)) { + foreach ($issues as $issue) { + if (class_exists('\customcertelement_expiry\element') && + \customcertelement_expiry\element::has_expiry($issue->certificateid)) { + $issue->expiry = \customcertelement_expiry\element::get_expiry_html($issue->certificateid, $issue->userid); + } + } + $result->success = true; $result->issues = $issues; } else { diff --git a/version.php b/version.php index 47f95f7e..07dcd2bb 100644 --- a/version.php +++ b/version.php @@ -24,10 +24,10 @@ defined('MOODLE_INTERNAL') || die('Direct access to this script is forbidden.'); -$plugin->version = 2022112808; // The current module version (Date: YYYYMMDDXX). +$plugin->version = 2022112813; // The current module version (Date: YYYYMMDDXX). $plugin->requires = 2022112800; // Requires this Moodle version (4.1). $plugin->cron = 0; // Period for cron to check this module (secs). $plugin->component = 'mod_customcert'; $plugin->maturity = MATURITY_STABLE; -$plugin->release = "4.1.5"; // User-friendly version number. +$plugin->release = "4.1.6"; // User-friendly version number.