From 2fa7191d12449e460e729b18f2b8cb1c7d7d383a Mon Sep 17 00:00:00 2001 From: Mark Nelson Date: Wed, 23 May 2018 14:35:23 +0800 Subject: [PATCH] #70 Added mobile app support This allows students to view the activity and download their certificate. It also allows teachers to view the list of issued certificates, with the ability to revoke any. This is for Moodle Mobile v3.5.0 (not to be confused with your Moodle site version) and will not work on Mobile versions earlier than this. If you are running a Moodle site on version 3.4 or below you will need to install the local_mobile plugin in order for this to work. If you are running a Moodle site on version 3.0 or below then you will need to upgrade. --- classes/external.php | 53 ++++ classes/output/mobile.php | 262 +++++++++++++++++++ db/mobile.php | 55 ++++ db/services.php | 9 + mobile/pluginfile.php | 88 +++++++ templates/mobile_report_page.mustache | 101 +++++++ templates/mobile_view_activity_page.mustache | 71 +++++ tests/external_test.php | 150 +++++++++++ version.php | 4 +- 9 files changed, 791 insertions(+), 2 deletions(-) create mode 100644 classes/output/mobile.php create mode 100644 db/mobile.php create mode 100644 mobile/pluginfile.php create mode 100644 templates/mobile_report_page.mustache create mode 100644 templates/mobile_view_activity_page.mustache create mode 100644 tests/external_test.php diff --git a/classes/external.php b/classes/external.php index d8e48da..b428848 100644 --- a/classes/external.php +++ b/classes/external.php @@ -174,4 +174,57 @@ class external extends \external_api { public static function get_element_html_returns() { return new \external_value(PARAM_RAW, 'The HTML'); } + + /** + * Returns the delete_issue() parameters. + * + * @return \external_function_parameters + */ + public static function delete_issue_parameters() { + return new \external_function_parameters( + array( + 'certificateid' => new \external_value(PARAM_INT, 'The certificate id'), + 'issueid' => new \external_value(PARAM_INT, 'The issue id'), + ) + ); + } + + /** + * Handles deleting a customcert issue. + * + * @param int $certificateid The certificate id. + * @param int $issueid The issue id. + * @return bool + */ + public static function delete_issue($certificateid, $issueid) { + global $DB; + + $params = [ + 'certificateid' => $certificateid, + 'issueid' => $issueid + ]; + self::validate_parameters(self::delete_issue_parameters(), $params); + + $certificate = $DB->get_record('customcert', ['id' => $certificateid], '*', MUST_EXIST); + $issue = $DB->get_record('customcert_issues', ['id' => $issueid, 'customcertid' => $certificateid], '*', MUST_EXIST); + + $cm = get_coursemodule_from_instance('customcert', $certificate->id, 0, false, MUST_EXIST); + + // Make sure the user has the required capabilities. + $context = \context_module::instance($cm->id); + self::validate_context($context); + require_capability('mod/customcert:manage', $context); + + // Delete the issue. + return $DB->delete_records('customcert_issues', ['id' => $issue->id]); + } + + /** + * Returns the delete_issue result value. + * + * @return \external_value + */ + public static function delete_issue_returns() { + return new \external_value(PARAM_BOOL, 'True if successful, false otherwise'); + } } diff --git a/classes/output/mobile.php b/classes/output/mobile.php new file mode 100644 index 0000000..c215938 --- /dev/null +++ b/classes/output/mobile.php @@ -0,0 +1,262 @@ +. + +/** + * Contains the mobile output class for the custom certificate. + * + * @package mod_customcert + * @copyright 2018 Mark Nelson + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace mod_customcert\output; + +defined('MOODLE_INTERNAL') || die(); + +/** + * Mobile output class for the custom certificate. + * + * @copyright 2018 Mark Nelson + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class mobile { + + /** + * Returns the initial page when viewing the activity for the mobile app. + * + * @param array $args Arguments from tool_mobile_get_content WS + * @return array HTML, javascript and other data + */ + public static function mobile_view_activity($args) { + global $OUTPUT, $DB, $USER; + + $args = (object) $args; + + // Get the group variable. + $groupid = empty($args->group) ? 0 : $args->group; // By default, group 0. + $cmid = $args->cmid; + + // Capabilities check. + $cm = get_coursemodule_from_id('customcert', $cmid); + $context = \context_module::instance($cm->id); + self::require_capability($cm, $context, 'mod/customcert:view'); + + // Set some variables we are going to be using. + $certificate = $DB->get_record('customcert', ['id' => $cm->instance], '*', MUST_EXIST); + $certificate->name = format_string($certificate->name); + list($certificate->intro, $certificate->introformat) = external_format_text($certificate->intro, + $certificate->introformat, $context->id, 'mod_customcert', 'intro'); + + // Get the groups (if any) to display - also sets active group. + $groups = self::get_groups($cm, $groupid, $USER->id); + + // Get any issues this person may have. + $issues = $DB->get_records('customcert_issues', ['userid' => $USER->id, 'customcertid' => $certificate->id]); + + $candownload = true; + if ($certificate->requiredtime && !has_capability('mod/certificate:manage', $context)) { + if (\mod_customcert\certificate::get_course_time($certificate->course) < ($certificate->requiredtime * 60)) { + $candownload = false; + } + } + + $fileurl = ""; + if ($candownload) { + $fileurl = new \moodle_url('/mod/customcert/mobile/pluginfile.php', ['certificateid' => $certificate->id, + 'userid' => $USER->id]); + $fileurl = $fileurl->out(true); + } + + $showreport = false; + $numissues = 0; + if (has_capability('mod/customcert:viewreport', $context)) { + // Get the total number of issues. + $showreport = true; + $groupmode = groups_get_activity_groupmode($cm); + if (has_capability('moodle/site:accessallgroups', $context)) { + $groupmode = 'aag'; + } + $numissues = \mod_customcert\certificate::get_number_of_issues($certificate->id, $cm, $groupmode); + } + + $data = [ + 'certificate' => $certificate, + 'cmid' => $cm->id, + 'groupselected' => $groupid, + 'showgroups' => !empty($groups), + 'groups' => array_values($groups), + 'hasissues' => !empty($issues), + 'issues' => array_values($issues), + 'candownload' => $candownload, + 'fileurl' => $fileurl, + 'showreport' => $showreport, + 'numissuesinreport' => $numissues, + 'currenttimestamp' => time() + ]; + + return [ + 'templates' => [ + [ + 'id' => 'main', + 'html' => $OUTPUT->render_from_template('mod_customcert/mobile_view_activity_page', $data), + ], + ], + 'javascript' => '', + 'otherdata' => '' + ]; + } + + /** + * Returns the list of issues certificates for the activity for the mobile app. + * + * @param array $args Arguments from tool_mobile_get_content WS + * @return array HTML, javascript and other data + */ + public static function mobile_view_report($args) { + global $DB, $OUTPUT, $USER; + + $args = (object) $args; + + $cmid = $args->cmid; + $groupid = empty($args->group) ? 0 : $args->group; // By default, group 0. + + // Capabilities check. + $cm = get_coursemodule_from_id('customcert', $cmid); + $context = \context_module::instance($cm->id); + + self::require_capability($cm, $context, 'mod/customcert:viewreport'); + + // Get the groups (if any) to display - also sets active group. + $groups = self::get_groups($cm, $groupid, $USER->id); + + $certificate = $DB->get_record('customcert', ['id' => $cm->instance], '*', MUST_EXIST); + $certificate->name = format_string($certificate->name); + list($certificate->intro, $certificate->introformat) = external_format_text($certificate->intro, + $certificate->introformat, $context->id, 'mod_customcert', 'intro'); + + $groupmode = groups_get_activity_groupmode($cm); + if (has_capability('moodle/site:accessallgroups', $context)) { + $groupmode = 'aag'; + } + + $issues = \mod_customcert\certificate::get_issues($certificate->id, $groupmode, $cm, 0, 0); + foreach ($issues as $issue) { + $issue->displayname = fullname($issue); + $issue->fileurl = new \moodle_url('/mod/customcert/mobile/pluginfile.php', ['certificateid' => $certificate->id, + 'userid' => $issue->id]); + } + + $data = [ + 'certificate' => $certificate, + 'cmid' => $cmid, + 'showgroups' => !empty($groups), + 'groups' => array_values($groups), + 'canmanage' => has_capability('mod/customcert:manage', $context), + 'hasissues' => !empty($issues), + 'issues' => array_values($issues), + 'currenttimestamp' => time() + ]; + + return [ + 'templates' => [ + [ + 'id' => 'main', + 'html' => $OUTPUT->render_from_template('mod_customcert/mobile_report_page', $data), + ], + ], + 'javascript' => '', + 'otherdata' => '' + ]; + } + + /** + * Returns an array of groups to be displayed (if applicable) for the activity. + * + * The groups API is a mess hence the hackiness. + * + * @param \stdClass $cm The course module + * @param int $groupid The group id + * @param int $userid The user id + * @return array The array of groups, may be empty. + */ + protected static function get_groups($cm, $groupid, $userid) { + $arrgroups = []; + if ($groupmode = groups_get_activity_groupmode($cm)) { + if ($groups = groups_get_activity_allowed_groups($cm, $userid)) { + $context = \context_module::instance($cm->id); + if ($groupmode == VISIBLEGROUPS || has_capability('moodle/site:accessallgroups', $context)) { + $allparticipants = new \stdClass(); + $allparticipants->id = 0; + $allparticipants->name = get_string('allparticipants'); + $allparticipants->selected = $groupid === 0; + $arrgroups[0] = $allparticipants; + } + self::update_active_group($groupmode, $groupid, $groups, $cm); + // Detect which group is selected. + foreach ($groups as $gid => $group) { + $group->selected = $gid == $groupid; + $arrgroups[] = $group; + } + } + } + + return $arrgroups; + } + + /** + * Update the active group in the session. + * + * This is a hack. We can't call groups_get_activity_group to update the active group as it relies + * on optional_param('group' .. which we won't have when using the mobile app. + * + * @param int $groupmode The group mode we are in, eg. NOGROUPS, VISIBLEGROUPS + * @param int $groupid The id of the group that has been selected + * @param array $allowedgroups The allowed groups this user can access + * @param \stdClass $cm The course module + */ + private static function update_active_group($groupmode, $groupid, $allowedgroups, $cm) { + global $SESSION; + + $context = \context_module::instance($cm->id); + + if (has_capability('moodle/site:accessallgroups', $context)) { + $groupmode = 'aag'; + } + + if ($groupid == 0) { + // The groups are only all visible in VISIBLEGROUPS mode or if the user can access all groups. + if ($groupmode == VISIBLEGROUPS || has_capability('moodle/site:accessallgroups', $context)) { + $SESSION->activegroup[$cm->course][$groupmode][$cm->groupingid] = 0; + } + } else { + if ($allowedgroups && array_key_exists($groupid, $allowedgroups)) { + $SESSION->activegroup[$cm->course][$groupmode][$cm->groupingid] = $groupid; + } + } + } + + /** + * Confirms the user is logged in and has the specified capability. + * + * @param \stdClass $cm + * @param \context $context + * @param string $cap + */ + protected static function require_capability(\stdClass $cm, \context $context, string $cap) { + require_login($cm->course, false, $cm, true, true); + require_capability($cap, $context); + } +} diff --git a/db/mobile.php b/db/mobile.php new file mode 100644 index 0000000..e9ece98 --- /dev/null +++ b/db/mobile.php @@ -0,0 +1,55 @@ +. + +/** + * Defines mobile handlers. + * + * @package mod_customcert + * @copyright 2018 Mark Nelson + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +$addons = array( + 'mod_customcert' => [ // Plugin identifier. + 'handlers' => [ // Different places where the plugin will display content. + 'issueview' => [ // Handler unique name. + 'displaydata' => [ + 'icon' => $CFG->wwwroot . '/mod/customcert/pix/icon.png', + 'class' => '', + ], + 'delegate' => 'CoreCourseModuleDelegate', // Delegate (where to display the link to the plugin). + 'method' => 'mobile_view_activity', // Main function in \mod_customcert\output\mobile. + ] + ], + 'lang' => [ // Language strings that are used in all the handlers. + ['code', 'customcert'], + ['deleteissueconfirm', 'customcert'], + ['file', 'moodle'], + ['fullname', 'moodle'], + ['getcustomcert', 'customcert'], + ['modulenameplural', 'customcert'], + ['nothingtodisplay', 'moodle'], + ['pluginname', 'customcert'], + ['receiveddate', 'customcert'], + ['requiredtimenotmet', 'customcert'], + ['selectagroup', 'moodle'], + ['summaryofissue', 'customcert'], + ['viewcustomcertissues', 'customcert'] + ], + ] +); diff --git a/db/services.php b/db/services.php index b0b6343..923d842 100644 --- a/db/services.php +++ b/db/services.php @@ -25,6 +25,15 @@ defined('MOODLE_INTERNAL') || die(); $functions = array( + 'mod_customcert_delete_issue' => array( + 'classname' => 'mod_customcert\external', + 'methodname' => 'delete_issue', + 'classpath' => '', + 'description' => 'Delete an issue for a certificate', + 'type' => 'write', + 'ajax' => true, + 'services' => array(MOODLE_OFFICIAL_MOBILE_SERVICE) + ), 'mod_customcert_save_element' => array( 'classname' => 'mod_customcert\external', 'methodname' => 'save_element', diff --git a/mobile/pluginfile.php b/mobile/pluginfile.php new file mode 100644 index 0000000..c74c11b --- /dev/null +++ b/mobile/pluginfile.php @@ -0,0 +1,88 @@ +. + +/** + * Serves files for the mobile app. + * + * @package mod_customcert + * @copyright 2018 Mark Nelson + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +/** + * AJAX_SCRIPT - exception will be converted into JSON. + */ +define('AJAX_SCRIPT', true); + +/** + * NO_MOODLE_COOKIES - we don't want any cookie. + */ +define('NO_MOODLE_COOKIES', true); + +require_once('../../../config.php'); +require_once($CFG->libdir . '/filelib.php'); +require_once($CFG->dirroot . '/webservice/lib.php'); + +// Allow CORS requests. +header('Access-Control-Allow-Origin: *'); + +// Authenticate the user. +$token = required_param('token', PARAM_ALPHANUM); +$certificateid = required_param('certificateid', PARAM_INT); +$userid = required_param('userid', PARAM_INT); + +$webservicelib = new webservice(); +$authenticationinfo = $webservicelib->authenticate_user($token); + +// Check the service allows file download. +$enabledfiledownload = (int) ($authenticationinfo['service']->downloadfiles); +if (empty($enabledfiledownload)) { + throw new webservice_access_exception('Web service file downloading must be enabled in external service settings'); +} + +$cm = get_coursemodule_from_instance('customcert', $certificateid, 0, false, MUST_EXIST); +$certificate = $DB->get_record('customcert', ['id' => $certificateid], '*', MUST_EXIST); +$template = $DB->get_record('customcert_templates', ['id' => $certificate->templateid], '*', MUST_EXIST); + +// Capabilities check. +require_capability('mod/customcert:view', \context_module::instance($cm->id)); +if ($userid != $USER->id) { + require_capability('mod/customcert:viewreport', \context_module::instance($cm->id)); +} else { + // Make sure the user has met the required time. + if ($certificate->requiredtime) { + if (\mod_customcert\certificate::get_course_time($certificate->course) < ($certificate->requiredtime * 60)) { + exit(); + } + } +} + +$issue = $DB->get_record('customcert_issues', ['customcertid' => $certificateid, 'userid' => $userid]); + +// If we are doing it for the logged in user then we want to issue the certificate. +if (!$issue) { + // If the other user doesn't have an issue, then there is nothing to do. + if ($userid != $USER->id) { + exit(); + } + + \mod_customcert\certificate::issue_certificate($certificate->id, $USER->id); +} + +// Now we want to generate the PDF. +$template = new \mod_customcert\template($template); +$template->generate_pdf(false, $userid); +exit(); diff --git a/templates/mobile_report_page.mustache b/templates/mobile_report_page.mustache new file mode 100644 index 0000000..7f3ca66 --- /dev/null +++ b/templates/mobile_report_page.mustache @@ -0,0 +1,101 @@ +{{! + 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_report_page + + The page that lists the custom certificates issued + + Classes required for JS: + * None + + Data attibutes required for JS: + * All data attributes are required +}} +{{=<% %>=}} +
+ + <%#showgroups%> + + {{ 'plugin.mod_customcert.selectagroup' | translate }} + + <%#groups%> + selected<%/selected%>><% name %> + <%/groups%> + + + <%/showgroups%> + +

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

+
+ <%#hasissues%> + + + + + {{ 'plugin.mod_customcert.fullname' | translate }} + + + {{ 'plugin.mod_customcert.receiveddate' | translate }} + + + {{ 'plugin.mod_customcert.code' | translate }} + + + {{ 'plugin.mod_customcert.file' | translate }} + + <%#canmanage%> + + <%/canmanage%> + + <%#issues%> + + + <% displayname %> + + + {{ <% timecreated %> | coreToLocaleString }} + + + <% code %> + + + + + <%#canmanage%> + + + + <%/canmanage%> + + <%/issues%> + + + <%/hasissues%> + <%^hasissues%> + + {{ 'plugin.mod_customcert.nothingtodisplay' | translate }} + + <%/hasissues%> +
diff --git a/templates/mobile_view_activity_page.mustache b/templates/mobile_view_activity_page.mustache new file mode 100644 index 0000000..bbc39eb --- /dev/null +++ b/templates/mobile_view_activity_page.mustache @@ -0,0 +1,71 @@ +{{! + 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 +}} +{{=<% %>=}} +
+ + <%#showgroups%> + + {{ 'plugin.mod_customcert.selectagroup' | translate }} + + <%#groups%> + selected<%/selected%>><% name %> + <%/groups%> + + + <%/showgroups%> + <%#showreport%> + + {{ 'plugin.mod_customcert.viewcustomcertissues' | translate: {$a: <% numissuesinreport %>} }} + + <%/showreport%> + <%#hasissues%> + + +

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

+
+ <%#issues%> + + {{ <% timecreated %> | coreToLocaleString }} + + <%/issues%> +
+ <%/hasissues%> + <%#candownload%> + + + + <%/candownload%> + <%^candownload%> + +

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

+
+ <%/candownload%> +
diff --git a/tests/external_test.php b/tests/external_test.php new file mode 100644 index 0000000..6540e64 --- /dev/null +++ b/tests/external_test.php @@ -0,0 +1,150 @@ +. + +/** + * File contains the unit tests for the webservices. + * + * @package mod_customcert + * @category test + * @copyright 2018 Mark Nelson + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +global $CFG; + +/** + * Unit tests for the webservices. + * + * @package mod_customcert + * @category test + * @copyright 2018 Mark Nelson + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class mod_customcert_external_test_testcase extends advanced_testcase { + + /** + * Test set up. + */ + public function setUp() { + $this->resetAfterTest(); + } + + /** + * Test the delete_issue web service. + */ + public function test_delete_issue() { + global $DB; + + $this->setAdminUser(); + + // Create a course. + $course = $this->getDataGenerator()->create_course(); + + // Create a custom certificate in the course. + $customcert = $this->getDataGenerator()->create_module('customcert', ['course' => $course->id]); + + // Create two users. + $student1 = $this->getDataGenerator()->create_user(); + $student2 = $this->getDataGenerator()->create_user(); + + // Enrol them into the course. + $this->getDataGenerator()->enrol_user($student1->id, $course->id); + $this->getDataGenerator()->enrol_user($student2->id, $course->id); + + // Issue them both certificates. + $i1 = \mod_customcert\certificate::issue_certificate($customcert->id, $student1->id); + $i2 = \mod_customcert\certificate::issue_certificate($customcert->id, $student2->id); + + $this->assertEquals(2, $DB->count_records('customcert_issues')); + + $result = \mod_customcert\external::delete_issue($customcert->id, $i2); + + // We need to execute the return values cleaning process to simulate the web service server. + external_api::clean_returnvalue(\mod_customcert\external::delete_issue_returns(), $result); + + $issues = $DB->get_records('customcert_issues'); + $this->assertCount(1, $issues); + + $issue = reset($issues); + $this->assertEquals($student1->id, $issue->userid); + } + + /** + * Test the delete_issue web service. + */ + public function test_delete_issue_no_login() { + global $DB; + + // Create a course. + $course = $this->getDataGenerator()->create_course(); + + // Create a custom certificate in the course. + $customcert = $this->getDataGenerator()->create_module('customcert', ['course' => $course->id]); + + // Create two users. + $student1 = $this->getDataGenerator()->create_user(); + $student2 = $this->getDataGenerator()->create_user(); + + // Enrol them into the course. + $this->getDataGenerator()->enrol_user($student1->id, $course->id); + $this->getDataGenerator()->enrol_user($student2->id, $course->id); + + // Issue them both certificates. + $i1 = \mod_customcert\certificate::issue_certificate($customcert->id, $student1->id); + $i2 = \mod_customcert\certificate::issue_certificate($customcert->id, $student2->id); + + $this->assertEquals(2, $DB->count_records('customcert_issues')); + + // Try and delete without logging in. + $this->expectException('require_login_exception'); + \mod_customcert\external::delete_issue($customcert->id, $i2); + } + + /** + * Test the delete_issue web service. + */ + public function test_delete_issue_no_capability() { + global $DB; + + // Create a course. + $course = $this->getDataGenerator()->create_course(); + + // Create a custom certificate in the course. + $customcert = $this->getDataGenerator()->create_module('customcert', ['course' => $course->id]); + + // Create two users. + $student1 = $this->getDataGenerator()->create_user(); + $student2 = $this->getDataGenerator()->create_user(); + + $this->setUser($student1); + + // Enrol them into the course. + $this->getDataGenerator()->enrol_user($student1->id, $course->id); + $this->getDataGenerator()->enrol_user($student2->id, $course->id); + + // Issue them both certificates. + $i1 = \mod_customcert\certificate::issue_certificate($customcert->id, $student1->id); + $i2 = \mod_customcert\certificate::issue_certificate($customcert->id, $student2->id); + + $this->assertEquals(2, $DB->count_records('customcert_issues')); + + // Try and delete without the required capability. + $this->expectException('required_capability_exception'); + \mod_customcert\external::delete_issue($customcert->id, $i2); + } +} diff --git a/version.php b/version.php index bc3ebf9..5045953 100644 --- a/version.php +++ b/version.php @@ -24,10 +24,10 @@ defined('MOODLE_INTERNAL') || die('Direct access to this script is forbidden.'); -$plugin->version = 2017111301; // The current module version (Date: YYYYMMDDXX). +$plugin->version = 2017111302; // The current module version (Date: YYYYMMDDXX). $plugin->requires = 2017111300; // Requires this Moodle version (3.4). $plugin->cron = 0; // Period for cron to check this module (secs). $plugin->component = 'mod_customcert'; $plugin->maturity = MATURITY_STABLE; -$plugin->release = "3.4 release (Build: 2017111301)"; // User-friendly version number. +$plugin->release = "3.4 release (Build: 2017111302)"; // User-friendly version number.