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 aacff56..1a23e1a 100644 --- a/version.php +++ b/version.php @@ -24,10 +24,10 @@ defined('MOODLE_INTERNAL') || die('Direct access to this script is forbidden.'); -$plugin->version = 2018051700; // The current module version (Date: YYYYMMDDXX). +$plugin->version = 2018051701; // The current module version (Date: YYYYMMDDXX). $plugin->requires = 2018051700; // Requires this Moodle version (3.5). $plugin->cron = 0; // Period for cron to check this module (secs). $plugin->component = 'mod_customcert'; $plugin->maturity = MATURITY_STABLE; -$plugin->release = "3.5 release (Build: 2018051700)"; // User-friendly version number. +$plugin->release = "3.5 release (Build: 2018051701)"; // User-friendly version number.