From 2cbe9eaf42e4080c816d32590af2b6d695dfc22b Mon Sep 17 00:00:00 2001 From: Shamim Rezaie Date: Thu, 30 Apr 2020 15:40:58 +1000 Subject: [PATCH 1/8] MDL-68076 core: new admin settings related to user feedback --- admin/settings/top.php | 3 ++ admin/settings/userfeedback.php | 49 +++++++++++++++++++++++++++++++++ lang/en/admin.php | 9 ++++++ lib/classes/userfeedback.php | 48 ++++++++++++++++++++++++++++++++ 4 files changed, 109 insertions(+) create mode 100644 admin/settings/userfeedback.php create mode 100644 lib/classes/userfeedback.php diff --git a/admin/settings/top.php b/admin/settings/top.php index 87178430ea1..992436beccf 100644 --- a/admin/settings/top.php +++ b/admin/settings/top.php @@ -20,6 +20,9 @@ $moodleservices = new admin_settingpage('moodleservices', new lang_string('moodl 'admin')); $ADMIN->add('root', $moodleservices); +$userfeedback = new admin_settingpage('userfeedback', new lang_string('feedbacksettings', 'admin')); +$ADMIN->add('root', $userfeedback); + if ($hassiteconfig) { $optionalsubsystems = new admin_settingpage('optionalsubsystems', new lang_string('advancedfeatures', 'admin')); $ADMIN->add('root', $optionalsubsystems); diff --git a/admin/settings/userfeedback.php b/admin/settings/userfeedback.php new file mode 100644 index 00000000000..b7f31281c90 --- /dev/null +++ b/admin/settings/userfeedback.php @@ -0,0 +1,49 @@ +. + +/** + * This file contains call to feedback settings + * + * @package core + * @copyright 2020 Shamim Rezaie + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +if ($hassiteconfig) { + + $userfeedback->add(new admin_setting_configcheckbox('enableuserfeedback', + new lang_string('enableuserfeedback', 'admin'), + new lang_string('enableuserfeedback_desc', 'admin'), 1, 1, 0)); + + $options = [ + core_userfeedback::REMIND_AFTER_UPGRADE => new lang_string('userfeedbackafterupgrade', 'admin'), + core_userfeedback::REMIND_PERIODICALLY => new lang_string('userfeedbackperiodically', 'admin'), + core_userfeedback::REMIND_NEVER => new lang_string('never'), + ]; + $userfeedback->add(new admin_setting_configselect('userfeedback_nextreminder', + new lang_string('userfeedbacknextreminder', 'admin'), + new lang_string('userfeedbacknextreminder_desc', 'admin'), 1, $options)); + $userfeedback->hide_if('userfeedback_nextreminder', 'enableuserfeedback'); + + $userfeedback->add(new admin_setting_configtext('userfeedback_remindafter', + new lang_string('userfeedbackremindafter', 'admin'), + new lang_string('userfeedbackremindafter_desc', 'admin'), 90, PARAM_INT)); + $userfeedback->hide_if('userfeedback_remindafter', 'enableuserfeedback'); + $userfeedback->hide_if('userfeedback_remindafter', 'userfeedback_nextreminder', 'eq', 3); + +} diff --git a/lang/en/admin.php b/lang/en/admin.php index d47cd39a47f..66611103f0a 100644 --- a/lang/en/admin.php +++ b/lang/en/admin.php @@ -546,6 +546,8 @@ $string['enablerssfeeds'] = 'Enable RSS feeds'; $string['enablesearchareas'] = 'Enable search areas'; $string['enablestats'] = 'Enable statistics'; $string['enabletrusttext'] = 'Enable trusted content'; +$string['enableuserfeedback'] = 'Enable feedback about Moodle'; +$string['enableuserfeedback_desc'] = 'If enabled, a \'Give feedback\' link is displayed in a Dashboard alert and in the footer for users to give feedback about the Moodle LMS to Moodle HQ. The Dashboard alert also has a \'Remind me later\' option.'; $string['enablewebservices'] = 'Enable web services'; $string['enablewsdocumentation'] = 'Web services documentation'; $string['enrolinstancedefaults'] = 'Enrolment instance defaults'; @@ -578,6 +580,7 @@ $string['experimentalsettings'] = 'Experimental settings'; $string['extendedusernamechars'] = 'Allow extended characters in usernames'; $string['extramemorylimit'] = 'Extra PHP memory limit'; $string['fatalsessionautostart'] = '

Serious configuration error detected, please notify server administrator.

To operate properly, Moodle requires that administrator changes PHP settings.

session.auto_start must be set to off.

This setting is controlled by editing php.ini, Apache/IIS
configuration or .htaccess file on the server.

'; +$string['feedbacksettings'] = 'Feedback settings'; $string['filescleanupperiod'] = 'Clean up trash pool files'; $string['filescleanupperiod_help'] = 'How often trash pool files are deleted. These are files that are associated with a context that no longer exists, for example when a course is deleted. Please note: This setting can result in missing files in a course which is backed up, deleted and then restored if the setting \'Include files\' (backup_auto_files) in \'Automated backup settings\' is disabled.'; $string['fileconversioncleanuptask'] = 'Cleanup of temporary records for file conversions.'; @@ -1413,6 +1416,12 @@ $string['useexternalyui'] = 'Use online YUI libraries'; $string['user'] = 'User'; $string['userbulk'] = 'Bulk user actions'; $string['userbulkdownload'] = 'Export users as'; +$string['userfeedbackafterupgrade'] = 'After every major upgrade'; +$string['userfeedbacknextreminder'] = 'Next feedback reminder'; +$string['userfeedbacknextreminder_desc'] = 'When should we ask the user to give feedback again?'; +$string['userfeedbackperiodically'] = 'Periodically'; +$string['userfeedbackremindafter'] = 'Show reminder after'; +$string['userfeedbackremindafter_desc'] = 'Remind users again to give feedback after the given number of days.'; $string['userimagesdisabled'] = 'Profile user images are disabled'; $string['userlist'] = 'Browse list of users'; $string['userdefaultpreferences'] = 'User default preferences'; diff --git a/lib/classes/userfeedback.php b/lib/classes/userfeedback.php new file mode 100644 index 00000000000..ec2c9953506 --- /dev/null +++ b/lib/classes/userfeedback.php @@ -0,0 +1,48 @@ +. + +/** + * This file contains the core_userfeedback class + * + * @package core + * @copyright 2020 Shamim Rezaie + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +/** + * This Class contains helper functions for user feedback functionality. + * + * @copyright 2020 Shamim Rezaie + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class core_userfeedback { + /** + * @var int Ask user to give feedback a few days after each major upgrade. + */ + public const REMIND_AFTER_UPGRADE = 1; + + /** + * @var int Ask user to give feedback periodically. + */ + public const REMIND_PERIODICALLY = 2; + + /** + * @var int Do not ask user to give feedback. + */ + public const REMIND_NEVER = 3; +} From d21ae8c2974e1e40c765548aae6f900373be2351 Mon Sep 17 00:00:00 2001 From: Shamim Rezaie Date: Thu, 30 Apr 2020 14:49:58 +1000 Subject: [PATCH 2/8] MDL-68076 core: Added bullhorn icon to the core icon map --- lib/classes/output/icon_system_fontawesome.php | 1 + pix/i/bullhorn.svg | 2 ++ 2 files changed, 3 insertions(+) create mode 100644 pix/i/bullhorn.svg diff --git a/lib/classes/output/icon_system_fontawesome.php b/lib/classes/output/icon_system_fontawesome.php index 2389dc4aaee..ba4ccdbe3d4 100644 --- a/lib/classes/output/icon_system_fontawesome.php +++ b/lib/classes/output/icon_system_fontawesome.php @@ -197,6 +197,7 @@ class icon_system_fontawesome extends icon_system_font { 'core:i/backup' => 'fa-file-zip-o', 'core:i/badge' => 'fa-shield', 'core:i/breadcrumbdivider' => 'fa-angle-right', + 'core:i/bullhorn' => 'fa-bullhorn', 'core:i/calc' => 'fa-calculator', 'core:i/calendar' => 'fa-calendar', 'core:i/calendareventdescription' => 'fa-align-left', diff --git a/pix/i/bullhorn.svg b/pix/i/bullhorn.svg new file mode 100644 index 00000000000..3c54a7e7a4e --- /dev/null +++ b/pix/i/bullhorn.svg @@ -0,0 +1,2 @@ + + \ No newline at end of file From 3fecf7b547d638959e5e3ebc6e52bbd15030289d Mon Sep 17 00:00:00 2001 From: Shamim Rezaie Date: Thu, 30 Apr 2020 15:10:56 +1000 Subject: [PATCH 3/8] MDL-68076 core: Introduced \core\notification::cta() --- lang/en/moodle.php | 3 + lib/classes/notification.php | 45 ++++++++++++ lib/templates/local/notification/cta.mustache | 68 +++++++++++++++++++ theme/boost/scss/moodle/core.scss | 20 +++++- theme/boost/style/moodle.css | 12 ++++ theme/classic/style/moodle.css | 12 ++++ 6 files changed, 159 insertions(+), 1 deletion(-) create mode 100644 lib/templates/local/notification/cta.mustache diff --git a/lang/en/moodle.php b/lang/en/moodle.php index 50ea6d2aed1..497fdb0cc3f 100644 --- a/lang/en/moodle.php +++ b/lang/en/moodle.php @@ -225,6 +225,9 @@ $string['bycourseorder'] = 'By course order'; $string['byname'] = 'by {$a}'; $string['bypassed'] = 'Bypassed'; $string['cachecontrols'] = 'Cache controls'; +$string['calltofeedback'] = 'Moodle HQ would like your feedback on the Moodle LMS.'; +$string['calltofeedback_give'] = 'Give feedback'; +$string['calltofeedback_remind'] = 'Remind me later'; $string['cancel'] = 'Cancel'; $string['cancelled'] = 'Cancelled'; $string['categories'] = 'Course categories'; diff --git a/lib/classes/notification.php b/lib/classes/notification.php index 3c8783682f4..59fceeada5b 100644 --- a/lib/classes/notification.php +++ b/lib/classes/notification.php @@ -96,6 +96,51 @@ class notification { ); } + /** + * @param string[] $icon The icon to use. Required keys are 'pix' and 'component'. + * @param string $message The message to display. + * @param array $actions An array of action links + * @param string $region Optional region name + * @throws \coding_exception + */ + public static function add_call_to_action(array $icon, string $message, array $actions, string $region = ''): void { + global $OUTPUT, $PAGE; + + $context = new stdClass(); + $context->icon = $icon; + $context->message = $message; + $context->region = $region; + + $context->actions = array_map(function($action) { + $data = []; + foreach ($action['data'] as $name => $value) { + $data[] = ['name' => $name, 'value' => $value]; + } + $action['data'] = $data; + + return $action; + }, $actions); + + $notification = $OUTPUT->render_from_template('core/local/notification/cta', $context); + + if ($PAGE && $PAGE->state === \moodle_page::STATE_IN_BODY) { + $id = uniqid(); + echo \html_writer::span($notification, '', ['id' => $id]); + echo \html_writer::script( + "(function() {" . + "var notificationHolder = document.getElementById('user-notifications');" . + "if (!notificationHolder) { return; }" . + "var thisNotification = document.getElementById('{$id}');" . + "if (!thisNotification) { return; }" . + "notificationHolder.insertBefore(thisNotification.firstChild, notificationHolder.firstChild);" . + "thisNotification.remove();" . + "})();" + ); + } else { + throw new \coding_exception('You are calling add_call_to_action() either too early or too late.'); + } + } + /** * Fetch all of the notifications in the stack and clear the stack. * diff --git a/lib/templates/local/notification/cta.mustache b/lib/templates/local/notification/cta.mustache new file mode 100644 index 00000000000..1af3c1c3c9f --- /dev/null +++ b/lib/templates/local/notification/cta.mustache @@ -0,0 +1,68 @@ +{{! + 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 core/local/notification/cta + + Moodle cta notification template. + + The purpose of this template is to render a call to action notification. + + Classes required for JS: + * none + + Data attributes required for JS: + * none + + Context variables required for this template: + * message A cleaned string (use clean_text()) to display. + * extraclasses Additional classes to apply to the notification. + * actions List of action links. + * icon An icon.pix and icon.componrnt for the icon to be displauyed as the icon of CTA notification. + + Example context (json): + { + "message": "What do you think about Moodle?", + "actions": [ + { + "title": "Give feedback", + "url": "#", + "data": [ + {"name": "action", "value": "give"}, + {"name": "contextid", "value": "3"} + ] + } + ] + } +}} + +
+
+
+ {{# pix }} {{ icon.pix }}, {{ icon.component }} {{/ pix }} +
+
+ {{{ message }}}
+ {{# actions }} + {{ title }} + {{/ actions }} +
+
+
diff --git a/theme/boost/scss/moodle/core.scss b/theme/boost/scss/moodle/core.scss index 2bfa166876b..cf02227caa2 100644 --- a/theme/boost/scss/moodle/core.scss +++ b/theme/boost/scss/moodle/core.scss @@ -2653,4 +2653,22 @@ $picker-emojis-per-row: 7 !default; position: relative; z-index: inherit; } -} \ No newline at end of file +} + +.link-underline { + text-decoration: underline; + &:focus { + text-decoration: none; + } +} + +.alert.cta { + .icon { + padding: 0.3rem; + &.fa { + border-radius: 50%; + border-style: solid; + border-width: 0.125rem; + } + } +} diff --git a/theme/boost/style/moodle.css b/theme/boost/style/moodle.css index 0a03d632c7b..2b419040eb8 100644 --- a/theme/boost/style/moodle.css +++ b/theme/boost/style/moodle.css @@ -11822,6 +11822,18 @@ body.h5p-embed .h5pmessages { position: relative; z-index: inherit; } } +.link-underline { + text-decoration: underline; } + .link-underline:focus { + text-decoration: none; } + +.alert.cta .icon { + padding: 0.3rem; } + .alert.cta .icon.fa { + border-radius: 50%; + border-style: solid; + border-width: 0.125rem; } + .icon { font-size: 16px; width: 16px; diff --git a/theme/classic/style/moodle.css b/theme/classic/style/moodle.css index 9212c9654b2..961aae71fed 100644 --- a/theme/classic/style/moodle.css +++ b/theme/classic/style/moodle.css @@ -12036,6 +12036,18 @@ body.h5p-embed .h5pmessages { position: relative; z-index: inherit; } } +.link-underline { + text-decoration: underline; } + .link-underline:focus { + text-decoration: none; } + +.alert.cta .icon { + padding: 0.3rem; } + .alert.cta .icon.fa { + border-radius: 50%; + border-style: solid; + border-width: 0.125rem; } + .icon { font-size: 16px; width: 16px; From 1a4c8e02751a055d665ad0e6141d623f0991d885 Mon Sep 17 00:00:00 2001 From: Shamim Rezaie Date: Thu, 30 Apr 2020 15:39:06 +1000 Subject: [PATCH 4/8] MDL-68076 core: webservice to record feedback actions users take --- .../external/userfeedback/record_action.php | 83 +++++++++++++++++++ lib/db/services.php | 9 ++ .../userfeedback/record_action_test.php | 82 ++++++++++++++++++ version.php | 2 +- 4 files changed, 175 insertions(+), 1 deletion(-) create mode 100644 lib/classes/external/userfeedback/record_action.php create mode 100644 lib/tests/external/userfeedback/record_action_test.php diff --git a/lib/classes/external/userfeedback/record_action.php b/lib/classes/external/userfeedback/record_action.php new file mode 100644 index 00000000000..26f79fed5e9 --- /dev/null +++ b/lib/classes/external/userfeedback/record_action.php @@ -0,0 +1,83 @@ +. + +/** + * External API to record users action on the feedback notification. + * + * @package core + * @copyright 2020 Shamim Rezaie + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace core\external\userfeedback; + +defined('MOODLE_INTERNAL') || die(); + +require_once("$CFG->libdir/externallib.php"); + +use external_api; +use external_function_parameters; +use external_value; + +/** + * The external API to record users action on the feedback notification. + * + * @copyright 2020 Shamim Rezaie + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class record_action extends external_api { + /** + * Returns description of parameters. + * + * @return external_function_parameters + */ + public static function execute_parameters() { + return new external_function_parameters([ + 'action' => new external_value(PARAM_ALPHA, 'The action taken by user'), + ]); + } + + /** + * Record users action to the feedback CTA + * + * @param string $action The action the user took + * @throws \invalid_parameter_exception + */ + public static function execute(string $action) { + external_api::validate_parameters(self::execute_parameters(), ['action' => $action]); + + switch ($action) { + case 'give': + set_user_preference('core_userfeedback_give', time()); + break; + case 'remind': + set_user_preference('core_userfeedback_remind', time()); + break; + default: + throw new \invalid_parameter_exception('Invalid value for action parameter (value: ' . $action . '),' . + 'allowed values are: give,remind'); + } + } + + /** + * Returns description of method result value + * + * @return null + */ + public static function execute_returns() { + return null; + } +} diff --git a/lib/db/services.php b/lib/db/services.php index e8c1350e4b1..3a1521dfa83 100644 --- a/lib/db/services.php +++ b/lib/db/services.php @@ -2800,6 +2800,15 @@ $functions = array( 'ajax' => 'true', 'capabilities' => 'moodle/contentbank:manageowncontent', ], + 'core_create_userfeedback_action_record' => [ + 'classname' => 'core\external\userfeedback\record_action', + 'methodname' => 'execute', + 'classpath' => '', + 'description' => 'Record the action that the user takes in the user feedback notification for future use.', + 'type' => 'write', + 'ajax' => 'true', + 'capabilities' => '', + ], ); $services = array( diff --git a/lib/tests/external/userfeedback/record_action_test.php b/lib/tests/external/userfeedback/record_action_test.php new file mode 100644 index 00000000000..610acad35bd --- /dev/null +++ b/lib/tests/external/userfeedback/record_action_test.php @@ -0,0 +1,82 @@ +. + +/** + * External functions test for record_action. + * + * @package core + * @category test + * @copyright 2020 Shamim Rezaie + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace core\external\userfeedback; + +defined('MOODLE_INTERNAL') || die(); + +use externallib_advanced_testcase; +use context_system; + +global $CFG; + +require_once($CFG->dirroot . '/webservice/tests/helpers.php'); + +/** + * Class record_action_testcase + * + * @copyright 2020 Shamim Rezaie + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @coversDefaultClass record_action + */ +class record_action_testcase extends externallib_advanced_testcase { + + /** + * Data provider for test_record_action. + * + * @return array + */ + public function record_action_provider() { + return [ + 'give action' => ['give'], + 'remind action' => ['remind'], + ]; + } + + /** + * Test the behaviour of record_action(). + * + * @dataProvider record_action_provider + * @param string $action The action taken by the user + * + * @covers ::execute + */ + public function test_record_action(string $action) { + $this->resetAfterTest(); + + $context = context_system::instance(); + $user = $this->getDataGenerator()->create_user(); + $this->setUser($user); + + $now = time(); + + // Call the WS and check the action is recorded as expected. + $result = record_action::execute($action, $context->id); + $this->assertNull($result); + + $preference = get_user_preferences('core_userfeedback_' . $action); + $this->assertGreaterThanOrEqual($now, $preference); + } +} diff --git a/version.php b/version.php index 32f956cffac..17fac9b5ecb 100644 --- a/version.php +++ b/version.php @@ -29,7 +29,7 @@ defined('MOODLE_INTERNAL') || die(); -$version = 2020052900.00; // YYYYMMDD = weekly release date of this DEV branch. +$version = 2020052900.01; // YYYYMMDD = weekly release date of this DEV branch. // RR = release increments - 00 in DEV branches. // .XX = incremental changes. $release = '3.9dev+ (Build: 20200529)'; // Human-friendly version name From 6872c965698c0c76004b07c1c1d959632d59ae6e Mon Sep 17 00:00:00 2001 From: Shamim Rezaie Date: Sun, 3 May 2020 16:05:19 +1000 Subject: [PATCH 5/8] MDL-68076 core: webservice to get the feedback url --- .../external/userfeedback/generate_url.php | 98 +++++++++++++++ lib/db/services.php | 10 ++ .../userfeedback/generate_url_test.php | 118 ++++++++++++++++++ version.php | 2 +- 4 files changed, 227 insertions(+), 1 deletion(-) create mode 100644 lib/classes/external/userfeedback/generate_url.php create mode 100644 lib/tests/external/userfeedback/generate_url_test.php diff --git a/lib/classes/external/userfeedback/generate_url.php b/lib/classes/external/userfeedback/generate_url.php new file mode 100644 index 00000000000..821e3c82be1 --- /dev/null +++ b/lib/classes/external/userfeedback/generate_url.php @@ -0,0 +1,98 @@ +. + +/** + * External API to generate and return the URL of the feedback site. + * + * @package core + * @copyright 2020 Shamim Rezaie + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace core\external\userfeedback; + +defined('MOODLE_INTERNAL') || die(); + +require_once("$CFG->libdir/externallib.php"); + +use external_api; +use external_function_parameters; +use external_value; +use external_single_structure; +use external_multiple_structure; + +/** + * The external API to generate and return the feedback url. + * + * @copyright 2020 Shamim Rezaie + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class generate_url extends external_api { + /** + * Returns description of parameters. + * + * @return external_function_parameters + */ + public static function execute_parameters() { + return new external_function_parameters([ + 'contextid' => new external_value(PARAM_INT, 'The context id of the page the user is in'), + ]); + } + + /** + * Prepare and return the URL of the feedback site + * + * @param int $contextid The context id + * @return \stdClass + */ + public static function execute(int $contextid) { + global $CFG, $PAGE; + + require_once($CFG->libdir . '/adminlib.php'); + + external_api::validate_parameters(self::execute_parameters(), ['contextid' => $contextid]); + + $context = \context::instance_by_id($contextid); + self::validate_context($context); + $PAGE->set_context($context); + + $baseurl = $CFG->userfeedback_url ?? 'https://feedback.moodle.org/lms'; + $lang = clean_param(current_language(), PARAM_LANG); // Avoid breaking WS because of incorrect package langs. + $moodleurl = $CFG->wwwroot; + $moodleversion = $CFG->release; + $theme = $PAGE->theme->name; + $themeversion = get_component_version('theme_' . $theme); + + $firstseparator = strpos($baseurl, '?') === false ? '?' : '&'; + + $url = $baseurl . $firstseparator . 'lang=' . $lang; + $url .= '&moodle_url=' . rawurlencode($moodleurl) . + '&moodle_version=' . rawurlencode($moodleversion) . + '&theme=' . rawurlencode($theme) . + '&theme_version=' . $themeversion; + + return $url; + } + + /** + * Returns description of method result value + * + * @return external_value + */ + public static function execute_returns() { + return new external_value(PARAM_URL, 'Feedback site\'s URL'); + } +} diff --git a/lib/db/services.php b/lib/db/services.php index 3a1521dfa83..71d22dbc351 100644 --- a/lib/db/services.php +++ b/lib/db/services.php @@ -2809,6 +2809,16 @@ $functions = array( 'ajax' => 'true', 'capabilities' => '', ], + 'core_get_userfeedback_url' => [ + 'classname' => 'core\external\userfeedback\generate_url', + 'methodname' => 'execute', + 'classpath' => '', + 'description' => 'Generate a dynamic URL for the external user feedback site.' . + ' The URL includes some parameters to pre-fill the user feedback form.', + 'type' => 'read', + 'ajax' => 'true', + 'capabilities' => '', + ], ); $services = array( diff --git a/lib/tests/external/userfeedback/generate_url_test.php b/lib/tests/external/userfeedback/generate_url_test.php new file mode 100644 index 00000000000..c205038abf8 --- /dev/null +++ b/lib/tests/external/userfeedback/generate_url_test.php @@ -0,0 +1,118 @@ +. + +/** + * External functions test for generate_url. + * + * @package core + * @category test + * @copyright 2020 Shamim Rezaie + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace core\external\userfeedback; + +defined('MOODLE_INTERNAL') || die(); + +use externallib_advanced_testcase; +use context_system; +use context_course; +use external_api; + +global $CFG; + +require_once($CFG->dirroot . '/webservice/tests/helpers.php'); + +/** + * Class generate_url_testcase + * + * @copyright 2020 Shamim Rezaie + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @coversDefaultClass generate_url + */ +class generate_url_testcase extends externallib_advanced_testcase { + + /** + * Test the behaviour of generate_url(). + * + * @covers ::execute + */ + public function test_record_action_system() { + $this->resetAfterTest(); + + $course = $this->getDataGenerator()->create_course(); + $user = $this->getDataGenerator()->create_and_enrol($course, 'student'); + $context = context_system::instance(); + + $this->setUser($user); + + // Call the WS and check the requested data is returned as expected. + $result = generate_url::execute($context->id); + $result = external_api::clean_returnvalue(generate_url::execute_returns(), $result); + + $this->assertStringStartsWith('https://feedback.moodle.org/lms', $result); + $this->assertStringContainsString('?lang=en', $result); + $this->assertStringContainsString('&moodle_url=https%3A%2F%2Fwww.example.com%2Fmoodle', $result); + $this->assertStringContainsString('&theme=boost', $result); + } + + /** + * Test the behaviour of generate_url() in a course with a course theme. + * + * @covers ::execute + */ + public function test_record_action_course_theme() { + $this->resetAfterTest(); + + // Enable course themes. + set_config('allowcoursethemes', 1); + + $course = $this->getDataGenerator()->create_course(['theme' => 'classic']); + $user = $this->getDataGenerator()->create_and_enrol($course, 'student'); + $context = context_course::instance($course->id); + + $this->setUser($user); + + // Call the WS and check the requested data is returned as expected. + $result = generate_url::execute($context->id); + $result = external_api::clean_returnvalue(generate_url::execute_returns(), $result); + + $this->assertStringContainsString('&theme=classic', $result); + } + + /** + * Test the behaviour of generate_url() when a custom feedback url is set. + * + * @covers ::execute + */ + public function test_record_action_custom_feedback_url() { + $this->resetAfterTest(); + + // Enable course themes. + set_config('userfeedback_url', 'https://feedback.moodle.org/abc'); + + $user = $this->getDataGenerator()->create_user(); + $context = context_system::instance(); + + $this->setUser($user); + + // Call the WS and check the requested data is returned as expected. + $result = generate_url::execute($context->id); + $result = external_api::clean_returnvalue(generate_url::execute_returns(), $result); + + $this->assertStringStartsWith('https://feedback.moodle.org/abc', $result); + } +} diff --git a/version.php b/version.php index 17fac9b5ecb..9dec8821b66 100644 --- a/version.php +++ b/version.php @@ -29,7 +29,7 @@ defined('MOODLE_INTERNAL') || die(); -$version = 2020052900.01; // YYYYMMDD = weekly release date of this DEV branch. +$version = 2020052900.02; // YYYYMMDD = weekly release date of this DEV branch. // RR = release increments - 00 in DEV branches. // .XX = incremental changes. $release = '3.9dev+ (Build: 20200529)'; // Human-friendly version name From d74a8268297a441b598393bca6f707c01a8e8ea8 Mon Sep 17 00:00:00 2001 From: Shamim Rezaie Date: Thu, 30 Apr 2020 15:36:31 +1000 Subject: [PATCH 6/8] MDL-68076 core: display call to feedback on user dashboard --- lib/amd/build/userfeedback.min.js | 2 + lib/amd/build/userfeedback.min.js.map | 1 + lib/amd/src/userfeedback.js | 103 ++++++++++++++++++++++++++ lib/classes/userfeedback.php | 88 ++++++++++++++++++++++ my/index.php | 4 + 5 files changed, 198 insertions(+) create mode 100644 lib/amd/build/userfeedback.min.js create mode 100644 lib/amd/build/userfeedback.min.js.map create mode 100644 lib/amd/src/userfeedback.js diff --git a/lib/amd/build/userfeedback.min.js b/lib/amd/build/userfeedback.min.js new file mode 100644 index 00000000000..87066590936 --- /dev/null +++ b/lib/amd/build/userfeedback.min.js @@ -0,0 +1,2 @@ +define ("core/userfeedback",["exports","core/ajax","core/notification"],function(a,b,c){"use strict";Object.defineProperty(a,"__esModule",{value:!0});a.registerEventListeners=void 0;b=d(b);c=d(c);function d(a){return a&&a.__esModule?a:{default:a}}var f={regions:{root:"[data-region=\"core/userfeedback\"]"},actions:{}};f.actions.give="".concat(f.regions.root," [data-action=\"give\"]");f.actions.remind="".concat(f.regions.root," [data-action=\"remind\"]");a.registerEventListeners=function registerEventListeners(){document.addEventListener("click",function(a){var b=a.target.closest(f.actions.give);if(b){a.preventDefault();g().then(function(){return h("give")}).then(function(){var a=b.closest(f.regions.root);a.remove()}).catch(c.default.exception)}var d=a.target.closest(f.actions.remind);if(d){a.preventDefault();h("remind").then(function(){var a=d.closest(f.regions.root);a.remove()}).catch(c.default.exception)}})};var g=function(){return b.default.call([{methodname:"core_get_userfeedback_url",args:{contextid:M.cfg.contextid}}])[0].then(function(a){if(!window.open(a)){throw new Error("Unable to open popup")}})},h=function(a){return b.default.call([{methodname:"core_create_userfeedback_action_record",args:{action:a}}])[0]}}); +//# sourceMappingURL=userfeedback.min.js.map diff --git a/lib/amd/build/userfeedback.min.js.map b/lib/amd/build/userfeedback.min.js.map new file mode 100644 index 00000000000..897ef98bd4d --- /dev/null +++ b/lib/amd/build/userfeedback.min.js.map @@ -0,0 +1 @@ +{"version":3,"sources":["../src/userfeedback.js"],"names":["Selectors","regions","root","actions","give","remind","registerEventListeners","document","addEventListener","e","giveAction","target","closest","preventDefault","giveFeedback","then","recordAction","remove","catch","Notification","exception","remindAction","Ajax","call","methodname","args","contextid","M","cfg","url","window","open","Error","action"],"mappings":"sLAuBA,OACA,O,mDAEA,GAAMA,CAAAA,CAAS,CAAG,CACdC,OAAO,CAAE,CACLC,IAAI,CAAE,qCADD,CADK,CAIdC,OAAO,CAAE,EAJK,CAAlB,CAMAH,CAAS,CAACG,OAAV,CAAkBC,IAAlB,WAA4BJ,CAAS,CAACC,OAAV,CAAkBC,IAA9C,4BACAF,CAAS,CAACG,OAAV,CAAkBE,MAAlB,WAA8BL,CAAS,CAACC,OAAV,CAAkBC,IAAhD,8B,yBAKsC,QAAzBI,CAAAA,sBAAyB,EAAM,CACxCC,QAAQ,CAACC,gBAAT,CAA0B,OAA1B,CAAmC,SAAAC,CAAC,CAAI,CACpC,GAAMC,CAAAA,CAAU,CAAGD,CAAC,CAACE,MAAF,CAASC,OAAT,CAAiBZ,CAAS,CAACG,OAAV,CAAkBC,IAAnC,CAAnB,CACA,GAAIM,CAAJ,CAAgB,CACZD,CAAC,CAACI,cAAF,GACAC,CAAY,GACPC,IADL,CACU,UAAM,CACR,MAAOC,CAAAA,CAAY,CAAC,MAAD,CACtB,CAHL,EAIKD,IAJL,CAIU,UAAM,CACR,GAAMb,CAAAA,CAAI,CAAGQ,CAAU,CAACE,OAAX,CAAmBZ,CAAS,CAACC,OAAV,CAAkBC,IAArC,CAAb,CACAA,CAAI,CAACe,MAAL,EAEH,CARL,EASKC,KATL,CASWC,UAAaC,SATxB,CAUH,CAED,GAAMC,CAAAA,CAAY,CAAGZ,CAAC,CAACE,MAAF,CAASC,OAAT,CAAiBZ,CAAS,CAACG,OAAV,CAAkBE,MAAnC,CAArB,CACA,GAAIgB,CAAJ,CAAkB,CACdZ,CAAC,CAACI,cAAF,GACAG,CAAY,CAAC,QAAD,CAAZ,CACKD,IADL,CACU,UAAM,CACR,GAAMb,CAAAA,CAAI,CAAGmB,CAAY,CAACT,OAAb,CAAqBZ,CAAS,CAACC,OAAV,CAAkBC,IAAvC,CAAb,CACAA,CAAI,CAACe,MAAL,EAEH,CALL,EAMKC,KANL,CAMWC,UAAaC,SANxB,CAOH,CACJ,CA3BD,CA4BH,C,IAOKN,CAAAA,CAAY,CAAG,UAAM,CACvB,MAAOQ,WAAKC,IAAL,CAAU,CAAC,CACdC,UAAU,CAAE,2BADE,CAEdC,IAAI,CAAE,CACFC,SAAS,CAAEC,CAAC,CAACC,GAAF,CAAMF,SADf,CAFQ,CAAD,CAAV,EAKH,CALG,EAMFX,IANE,CAMG,SAAAc,CAAG,CAAI,CACT,GAAI,CAACC,MAAM,CAACC,IAAP,CAAYF,CAAZ,CAAL,CAAuB,CACnB,KAAM,IAAIG,CAAAA,KAAJ,CAAU,sBAAV,CACT,CAEJ,CAXE,CAYV,C,CAQKhB,CAAY,CAAG,SAAAiB,CAAM,CAAI,CAC3B,MAAOX,WAAKC,IAAL,CAAU,CAAC,CACdC,UAAU,CAAE,wCADE,CAEdC,IAAI,CAAE,CACFQ,MAAM,CAANA,CADE,CAFQ,CAAD,CAAV,EAKH,CALG,CAMV,C","sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see .\n\n/**\n * Handle clicking on action links of the feedback alert.\n *\n * @module core/cta_feedback\n * @copyright 2020 Shamim Rezaie \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport Ajax from 'core/ajax';\nimport Notification from 'core/notification';\n\nconst Selectors = {\n regions: {\n root: '[data-region=\"core/userfeedback\"]',\n },\n actions: {},\n};\nSelectors.actions.give = `${Selectors.regions.root} [data-action=\"give\"]`;\nSelectors.actions.remind = `${Selectors.regions.root} [data-action=\"remind\"]`;\n\n/**\n * Attach the necessary event handlers to the action links\n */\nexport const registerEventListeners = () => {\n document.addEventListener('click', e => {\n const giveAction = e.target.closest(Selectors.actions.give);\n if (giveAction) {\n e.preventDefault();\n giveFeedback()\n .then(() => {\n return recordAction('give');\n })\n .then(() => {\n const root = giveAction.closest(Selectors.regions.root);\n root.remove();\n return;\n })\n .catch(Notification.exception);\n }\n\n const remindAction = e.target.closest(Selectors.actions.remind);\n if (remindAction) {\n e.preventDefault();\n recordAction('remind')\n .then(() => {\n const root = remindAction.closest(Selectors.regions.root);\n root.remove();\n return;\n })\n .catch(Notification.exception);\n }\n });\n};\n\n/**\n * The action function that is called when users choose to give feedback.\n *\n * @returns {Promise}\n */\nconst giveFeedback = () => {\n return Ajax.call([{\n methodname: 'core_get_userfeedback_url',\n args: {\n contextid: M.cfg.contextid,\n }\n }])[0]\n .then(url => {\n if (!window.open(url)) {\n throw new Error('Unable to open popup');\n }\n return;\n });\n};\n\n/**\n * Record the action that the user took.\n *\n * @param {string} action The action that the user took. Either give or remind.\n * @returns {Promise}\n */\nconst recordAction = action => {\n return Ajax.call([{\n methodname: 'core_create_userfeedback_action_record',\n args: {\n action,\n }\n }])[0];\n};\n"],"file":"userfeedback.min.js"} \ No newline at end of file diff --git a/lib/amd/src/userfeedback.js b/lib/amd/src/userfeedback.js new file mode 100644 index 00000000000..20d93e50b13 --- /dev/null +++ b/lib/amd/src/userfeedback.js @@ -0,0 +1,103 @@ +// 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 . + +/** + * Handle clicking on action links of the feedback alert. + * + * @module core/cta_feedback + * @copyright 2020 Shamim Rezaie + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +import Ajax from 'core/ajax'; +import Notification from 'core/notification'; + +const Selectors = { + regions: { + root: '[data-region="core/userfeedback"]', + }, + actions: {}, +}; +Selectors.actions.give = `${Selectors.regions.root} [data-action="give"]`; +Selectors.actions.remind = `${Selectors.regions.root} [data-action="remind"]`; + +/** + * Attach the necessary event handlers to the action links + */ +export const registerEventListeners = () => { + document.addEventListener('click', e => { + const giveAction = e.target.closest(Selectors.actions.give); + if (giveAction) { + e.preventDefault(); + giveFeedback() + .then(() => { + return recordAction('give'); + }) + .then(() => { + const root = giveAction.closest(Selectors.regions.root); + root.remove(); + return; + }) + .catch(Notification.exception); + } + + const remindAction = e.target.closest(Selectors.actions.remind); + if (remindAction) { + e.preventDefault(); + recordAction('remind') + .then(() => { + const root = remindAction.closest(Selectors.regions.root); + root.remove(); + return; + }) + .catch(Notification.exception); + } + }); +}; + +/** + * The action function that is called when users choose to give feedback. + * + * @returns {Promise} + */ +const giveFeedback = () => { + return Ajax.call([{ + methodname: 'core_get_userfeedback_url', + args: { + contextid: M.cfg.contextid, + } + }])[0] + .then(url => { + if (!window.open(url)) { + throw new Error('Unable to open popup'); + } + return; + }); +}; + +/** + * Record the action that the user took. + * + * @param {string} action The action that the user took. Either give or remind. + * @returns {Promise} + */ +const recordAction = action => { + return Ajax.call([{ + methodname: 'core_create_userfeedback_action_record', + args: { + action, + } + }])[0]; +}; diff --git a/lib/classes/userfeedback.php b/lib/classes/userfeedback.php index ec2c9953506..6ede8955820 100644 --- a/lib/classes/userfeedback.php +++ b/lib/classes/userfeedback.php @@ -45,4 +45,92 @@ class core_userfeedback { * @var int Do not ask user to give feedback. */ public const REMIND_NEVER = 3; + + /** + * Displays the feedback reminder block. + */ + public static function print_reminder_block(): void { + global $PAGE; + + static $jscalled = false; + + $actions = [ + [ + 'title' => get_string('calltofeedback_give'), + 'url' => '#', + 'data' => [ + 'action' => 'give', + ], + ], + [ + 'title' => get_string('calltofeedback_remind'), + 'url' => '#', + 'data' => [ + 'action' => 'remind', + ], + ], + ]; + $icon = [ + 'pix' => 'i/bullhorn', + 'component' => 'core' + ]; + + \core\notification::add_call_to_action($icon, get_string('calltofeedback'), $actions, 'core/userfeedback'); + + if (!$jscalled) { + $jscalled = true; + // Calling the following more than once will register event listeners twice. + $PAGE->requires->js_call_amd('core/userfeedback', 'registerEventListeners'); + } + } + + /** + * Indicates whether the feedback reminder block should be shown or not. + * + * @return bool + */ + public static function should_display_reminder(): bool { + global $CFG; + + if ($CFG->enableuserfeedback && isloggedin() && !isguestuser()) { + $give = get_user_preferences('core_userfeedback_give'); + $remind = get_user_preferences('core_userfeedback_remind'); + + $lastactiontime = max($give ?: 0, $remind ?: 0); + + switch ($CFG->userfeedback_nextreminder) { + case self::REMIND_AFTER_UPGRADE: + $lastupgrade = self::last_major_upgrade_time(); + if ($lastupgrade >= $lastactiontime) { + return $lastupgrade + ($CFG->userfeedback_remindafter * DAYSECS) < time(); + } + break; + case self::REMIND_PERIODICALLY: + return $lastactiontime + ($CFG->userfeedback_remindafter * DAYSECS) < time(); + break; + } + } + return false; + } + + /** + * Returns the last major upgrade time + * + * @return int + */ + private static function last_major_upgrade_time(): int { + global $DB; + + $targetversioncast = $DB->sql_cast_char2real('targetversion'); + $versioncast = $DB->sql_cast_char2real('version'); + + // A time difference more than 3 months has to be a core upgrade. + $time = $DB->get_field_sql("SELECT timemodified + FROM {upgrade_log} + WHERE plugin = 'core' AND $targetversioncast - $versioncast > 30000 + ORDER BY timemodified DESC + LIMIT 1"); + + return (int)$time; + } } diff --git a/my/index.php b/my/index.php index 7e3957e041c..9722450fe89 100644 --- a/my/index.php +++ b/my/index.php @@ -163,6 +163,10 @@ if (empty($CFG->forcedefaultmymoodle) && $PAGE->user_allowed_editing()) { echo $OUTPUT->header(); +if (core_userfeedback::should_display_reminder()) { + core_userfeedback::print_reminder_block(); +} + echo $OUTPUT->custom_block_region('content'); echo $OUTPUT->footer(); From a3a9539bd7f6155e5a0a38979a0afc1d990e3df4 Mon Sep 17 00:00:00 2001 From: Shamim Rezaie Date: Sun, 3 May 2020 16:04:05 +1000 Subject: [PATCH 7/8] MDL-68076 core: display the feedback link on footer --- lib/amd/build/userfeedback.min.js | 2 +- lib/amd/build/userfeedback.min.js.map | 2 +- lib/amd/src/userfeedback.js | 57 +++++++++++-------- .../external/userfeedback/generate_url.php | 21 +------ lib/classes/userfeedback.php | 33 +++++++++++ lib/outputrenderers.php | 6 ++ .../userfeedback_footer_link.mustache | 27 +++++++++ 7 files changed, 104 insertions(+), 44 deletions(-) create mode 100644 lib/templates/userfeedback_footer_link.mustache diff --git a/lib/amd/build/userfeedback.min.js b/lib/amd/build/userfeedback.min.js index 87066590936..5eb8d7c8df9 100644 --- a/lib/amd/build/userfeedback.min.js +++ b/lib/amd/build/userfeedback.min.js @@ -1,2 +1,2 @@ -define ("core/userfeedback",["exports","core/ajax","core/notification"],function(a,b,c){"use strict";Object.defineProperty(a,"__esModule",{value:!0});a.registerEventListeners=void 0;b=d(b);c=d(c);function d(a){return a&&a.__esModule?a:{default:a}}var f={regions:{root:"[data-region=\"core/userfeedback\"]"},actions:{}};f.actions.give="".concat(f.regions.root," [data-action=\"give\"]");f.actions.remind="".concat(f.regions.root," [data-action=\"remind\"]");a.registerEventListeners=function registerEventListeners(){document.addEventListener("click",function(a){var b=a.target.closest(f.actions.give);if(b){a.preventDefault();g().then(function(){return h("give")}).then(function(){var a=b.closest(f.regions.root);a.remove()}).catch(c.default.exception)}var d=a.target.closest(f.actions.remind);if(d){a.preventDefault();h("remind").then(function(){var a=d.closest(f.regions.root);a.remove()}).catch(c.default.exception)}})};var g=function(){return b.default.call([{methodname:"core_get_userfeedback_url",args:{contextid:M.cfg.contextid}}])[0].then(function(a){if(!window.open(a)){throw new Error("Unable to open popup")}})},h=function(a){return b.default.call([{methodname:"core_create_userfeedback_action_record",args:{action:a}}])[0]}}); +define ("core/userfeedback",["exports","core/ajax","core/notification"],function(a,b,c){"use strict";Object.defineProperty(a,"__esModule",{value:!0});a.registerEventListeners=void 0;b=d(b);c=d(c);function d(a){return a&&a.__esModule?a:{default:a}}var f={regions:{root:"[data-region=\"core/userfeedback\"]"},actions:{}};f.actions.give="".concat(f.regions.root," [data-action=\"give\"]");f.actions.remind="".concat(f.regions.root," [data-action=\"remind\"]");a.registerEventListeners=function registerEventListeners(){document.addEventListener("click",function(a){var b=a.target.closest(f.actions.give);if(b){a.preventDefault();g().then(function(){return i(b)}).then(h).catch(c.default.exception)}var d=a.target.closest(f.actions.remind);if(d){a.preventDefault();Promise.resolve(d).then(i).then(h).catch(c.default.exception)}})};var g=function(){return b.default.call([{methodname:"core_get_userfeedback_url",args:{contextid:M.cfg.contextid}}])[0].then(function(a){if(!window.open(a)){throw new Error("Unable to open popup")}})},h=function(a){if(a.dataset.record){return b.default.call([{methodname:"core_create_userfeedback_action_record",args:{action:a.dataset.action}}])[0]}return Promise.resolve()},i=function(a){if(a.dataset.hide){a.closest(f.regions.root).remove()}return a}}); //# sourceMappingURL=userfeedback.min.js.map diff --git a/lib/amd/build/userfeedback.min.js.map b/lib/amd/build/userfeedback.min.js.map index 897ef98bd4d..5f74d58a0fe 100644 --- a/lib/amd/build/userfeedback.min.js.map +++ b/lib/amd/build/userfeedback.min.js.map @@ -1 +1 @@ -{"version":3,"sources":["../src/userfeedback.js"],"names":["Selectors","regions","root","actions","give","remind","registerEventListeners","document","addEventListener","e","giveAction","target","closest","preventDefault","giveFeedback","then","recordAction","remove","catch","Notification","exception","remindAction","Ajax","call","methodname","args","contextid","M","cfg","url","window","open","Error","action"],"mappings":"sLAuBA,OACA,O,mDAEA,GAAMA,CAAAA,CAAS,CAAG,CACdC,OAAO,CAAE,CACLC,IAAI,CAAE,qCADD,CADK,CAIdC,OAAO,CAAE,EAJK,CAAlB,CAMAH,CAAS,CAACG,OAAV,CAAkBC,IAAlB,WAA4BJ,CAAS,CAACC,OAAV,CAAkBC,IAA9C,4BACAF,CAAS,CAACG,OAAV,CAAkBE,MAAlB,WAA8BL,CAAS,CAACC,OAAV,CAAkBC,IAAhD,8B,yBAKsC,QAAzBI,CAAAA,sBAAyB,EAAM,CACxCC,QAAQ,CAACC,gBAAT,CAA0B,OAA1B,CAAmC,SAAAC,CAAC,CAAI,CACpC,GAAMC,CAAAA,CAAU,CAAGD,CAAC,CAACE,MAAF,CAASC,OAAT,CAAiBZ,CAAS,CAACG,OAAV,CAAkBC,IAAnC,CAAnB,CACA,GAAIM,CAAJ,CAAgB,CACZD,CAAC,CAACI,cAAF,GACAC,CAAY,GACPC,IADL,CACU,UAAM,CACR,MAAOC,CAAAA,CAAY,CAAC,MAAD,CACtB,CAHL,EAIKD,IAJL,CAIU,UAAM,CACR,GAAMb,CAAAA,CAAI,CAAGQ,CAAU,CAACE,OAAX,CAAmBZ,CAAS,CAACC,OAAV,CAAkBC,IAArC,CAAb,CACAA,CAAI,CAACe,MAAL,EAEH,CARL,EASKC,KATL,CASWC,UAAaC,SATxB,CAUH,CAED,GAAMC,CAAAA,CAAY,CAAGZ,CAAC,CAACE,MAAF,CAASC,OAAT,CAAiBZ,CAAS,CAACG,OAAV,CAAkBE,MAAnC,CAArB,CACA,GAAIgB,CAAJ,CAAkB,CACdZ,CAAC,CAACI,cAAF,GACAG,CAAY,CAAC,QAAD,CAAZ,CACKD,IADL,CACU,UAAM,CACR,GAAMb,CAAAA,CAAI,CAAGmB,CAAY,CAACT,OAAb,CAAqBZ,CAAS,CAACC,OAAV,CAAkBC,IAAvC,CAAb,CACAA,CAAI,CAACe,MAAL,EAEH,CALL,EAMKC,KANL,CAMWC,UAAaC,SANxB,CAOH,CACJ,CA3BD,CA4BH,C,IAOKN,CAAAA,CAAY,CAAG,UAAM,CACvB,MAAOQ,WAAKC,IAAL,CAAU,CAAC,CACdC,UAAU,CAAE,2BADE,CAEdC,IAAI,CAAE,CACFC,SAAS,CAAEC,CAAC,CAACC,GAAF,CAAMF,SADf,CAFQ,CAAD,CAAV,EAKH,CALG,EAMFX,IANE,CAMG,SAAAc,CAAG,CAAI,CACT,GAAI,CAACC,MAAM,CAACC,IAAP,CAAYF,CAAZ,CAAL,CAAuB,CACnB,KAAM,IAAIG,CAAAA,KAAJ,CAAU,sBAAV,CACT,CAEJ,CAXE,CAYV,C,CAQKhB,CAAY,CAAG,SAAAiB,CAAM,CAAI,CAC3B,MAAOX,WAAKC,IAAL,CAAU,CAAC,CACdC,UAAU,CAAE,wCADE,CAEdC,IAAI,CAAE,CACFQ,MAAM,CAANA,CADE,CAFQ,CAAD,CAAV,EAKH,CALG,CAMV,C","sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see .\n\n/**\n * Handle clicking on action links of the feedback alert.\n *\n * @module core/cta_feedback\n * @copyright 2020 Shamim Rezaie \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport Ajax from 'core/ajax';\nimport Notification from 'core/notification';\n\nconst Selectors = {\n regions: {\n root: '[data-region=\"core/userfeedback\"]',\n },\n actions: {},\n};\nSelectors.actions.give = `${Selectors.regions.root} [data-action=\"give\"]`;\nSelectors.actions.remind = `${Selectors.regions.root} [data-action=\"remind\"]`;\n\n/**\n * Attach the necessary event handlers to the action links\n */\nexport const registerEventListeners = () => {\n document.addEventListener('click', e => {\n const giveAction = e.target.closest(Selectors.actions.give);\n if (giveAction) {\n e.preventDefault();\n giveFeedback()\n .then(() => {\n return recordAction('give');\n })\n .then(() => {\n const root = giveAction.closest(Selectors.regions.root);\n root.remove();\n return;\n })\n .catch(Notification.exception);\n }\n\n const remindAction = e.target.closest(Selectors.actions.remind);\n if (remindAction) {\n e.preventDefault();\n recordAction('remind')\n .then(() => {\n const root = remindAction.closest(Selectors.regions.root);\n root.remove();\n return;\n })\n .catch(Notification.exception);\n }\n });\n};\n\n/**\n * The action function that is called when users choose to give feedback.\n *\n * @returns {Promise}\n */\nconst giveFeedback = () => {\n return Ajax.call([{\n methodname: 'core_get_userfeedback_url',\n args: {\n contextid: M.cfg.contextid,\n }\n }])[0]\n .then(url => {\n if (!window.open(url)) {\n throw new Error('Unable to open popup');\n }\n return;\n });\n};\n\n/**\n * Record the action that the user took.\n *\n * @param {string} action The action that the user took. Either give or remind.\n * @returns {Promise}\n */\nconst recordAction = action => {\n return Ajax.call([{\n methodname: 'core_create_userfeedback_action_record',\n args: {\n action,\n }\n }])[0];\n};\n"],"file":"userfeedback.min.js"} \ No newline at end of file +{"version":3,"sources":["../src/userfeedback.js"],"names":["Selectors","regions","root","actions","give","remind","registerEventListeners","document","addEventListener","e","giveAction","target","closest","preventDefault","giveFeedback","then","hideRoot","recordAction","catch","Notification","exception","remindAction","Promise","resolve","Ajax","call","methodname","args","contextid","M","cfg","url","window","open","Error","clickedItem","dataset","record","action","hide","remove"],"mappings":"sLAuBA,OACA,O,mDAEA,GAAMA,CAAAA,CAAS,CAAG,CACdC,OAAO,CAAE,CACLC,IAAI,CAAE,qCADD,CADK,CAIdC,OAAO,CAAE,EAJK,CAAlB,CAMAH,CAAS,CAACG,OAAV,CAAkBC,IAAlB,WAA4BJ,CAAS,CAACC,OAAV,CAAkBC,IAA9C,4BACAF,CAAS,CAACG,OAAV,CAAkBE,MAAlB,WAA8BL,CAAS,CAACC,OAAV,CAAkBC,IAAhD,8B,yBAKsC,QAAzBI,CAAAA,sBAAyB,EAAM,CACxCC,QAAQ,CAACC,gBAAT,CAA0B,OAA1B,CAAmC,SAAAC,CAAC,CAAI,CACpC,GAAMC,CAAAA,CAAU,CAAGD,CAAC,CAACE,MAAF,CAASC,OAAT,CAAiBZ,CAAS,CAACG,OAAV,CAAkBC,IAAnC,CAAnB,CACA,GAAIM,CAAJ,CAAgB,CACZD,CAAC,CAACI,cAAF,GAEAC,CAAY,GACPC,IADL,CACU,iBAAMC,CAAAA,CAAQ,CAACN,CAAD,CAAd,CADV,EAEKK,IAFL,CAEUE,CAFV,EAGKC,KAHL,CAGWC,UAAaC,SAHxB,CAIH,CAED,GAAMC,CAAAA,CAAY,CAAGZ,CAAC,CAACE,MAAF,CAASC,OAAT,CAAiBZ,CAAS,CAACG,OAAV,CAAkBE,MAAnC,CAArB,CACA,GAAIgB,CAAJ,CAAkB,CACdZ,CAAC,CAACI,cAAF,GAEAS,OAAO,CAACC,OAAR,CAAgBF,CAAhB,EACKN,IADL,CACUC,CADV,EAEKD,IAFL,CAEUE,CAFV,EAGKC,KAHL,CAGWC,UAAaC,SAHxB,CAIH,CACJ,CApBD,CAqBH,C,IAOKN,CAAAA,CAAY,CAAG,UAAM,CACvB,MAAOU,WAAKC,IAAL,CAAU,CAAC,CACdC,UAAU,CAAE,2BADE,CAEdC,IAAI,CAAE,CACFC,SAAS,CAAEC,CAAC,CAACC,GAAF,CAAMF,SADf,CAFQ,CAAD,CAAV,EAKH,CALG,EAMFb,IANE,CAMG,SAAAgB,CAAG,CAAI,CACT,GAAI,CAACC,MAAM,CAACC,IAAP,CAAYF,CAAZ,CAAL,CAAuB,CACnB,KAAM,IAAIG,CAAAA,KAAJ,CAAU,sBAAV,CACT,CAEJ,CAXE,CAYV,C,CAQKjB,CAAY,CAAG,SAAAkB,CAAW,CAAI,CAChC,GAAIA,CAAW,CAACC,OAAZ,CAAoBC,MAAxB,CAAgC,CAC5B,MAAOb,WAAKC,IAAL,CAAU,CAAC,CACdC,UAAU,CAAE,wCADE,CAEdC,IAAI,CAAE,CACFW,MAAM,CAAEH,CAAW,CAACC,OAAZ,CAAoBE,MAD1B,CAFQ,CAAD,CAAV,EAKH,CALG,CAMV,CAED,MAAOhB,CAAAA,OAAO,CAACC,OAAR,EACV,C,CAQKP,CAAQ,CAAG,SAAAmB,CAAW,CAAI,CAC5B,GAAIA,CAAW,CAACC,OAAZ,CAAoBG,IAAxB,CAA8B,CAC1BJ,CAAW,CAACvB,OAAZ,CAAoBZ,CAAS,CAACC,OAAV,CAAkBC,IAAtC,EAA4CsC,MAA5C,EACH,CAED,MAAOL,CAAAA,CACV,C","sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see .\n\n/**\n * Handle clicking on action links of the feedback alert.\n *\n * @module core/cta_feedback\n * @copyright 2020 Shamim Rezaie \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport Ajax from 'core/ajax';\nimport Notification from 'core/notification';\n\nconst Selectors = {\n regions: {\n root: '[data-region=\"core/userfeedback\"]',\n },\n actions: {},\n};\nSelectors.actions.give = `${Selectors.regions.root} [data-action=\"give\"]`;\nSelectors.actions.remind = `${Selectors.regions.root} [data-action=\"remind\"]`;\n\n/**\n * Attach the necessary event handlers to the action links\n */\nexport const registerEventListeners = () => {\n document.addEventListener('click', e => {\n const giveAction = e.target.closest(Selectors.actions.give);\n if (giveAction) {\n e.preventDefault();\n\n giveFeedback()\n .then(() => hideRoot(giveAction))\n .then(recordAction)\n .catch(Notification.exception);\n }\n\n const remindAction = e.target.closest(Selectors.actions.remind);\n if (remindAction) {\n e.preventDefault();\n\n Promise.resolve(remindAction)\n .then(hideRoot)\n .then(recordAction)\n .catch(Notification.exception);\n }\n });\n};\n\n/**\n * The action function that is called when users choose to give feedback.\n *\n * @returns {Promise}\n */\nconst giveFeedback = () => {\n return Ajax.call([{\n methodname: 'core_get_userfeedback_url',\n args: {\n contextid: M.cfg.contextid,\n }\n }])[0]\n .then(url => {\n if (!window.open(url)) {\n throw new Error('Unable to open popup');\n }\n return;\n });\n};\n\n/**\n * Record the action that the user took.\n *\n * @param {HTMLElement} clickedItem The action element that the user chose.\n * @returns {Promise}\n */\nconst recordAction = clickedItem => {\n if (clickedItem.dataset.record) {\n return Ajax.call([{\n methodname: 'core_create_userfeedback_action_record',\n args: {\n action: clickedItem.dataset.action,\n }\n }])[0];\n }\n\n return Promise.resolve();\n};\n\n/**\n * Hide the root node of the CTA notification.\n *\n * @param {HTMLElement} clickedItem The action element that the user chose.\n * @returns {HTMLElement}\n */\nconst hideRoot = clickedItem => {\n if (clickedItem.dataset.hide) {\n clickedItem.closest(Selectors.regions.root).remove();\n }\n\n return clickedItem;\n};\n"],"file":"userfeedback.min.js"} \ No newline at end of file diff --git a/lib/amd/src/userfeedback.js b/lib/amd/src/userfeedback.js index 20d93e50b13..a74bf4eb172 100644 --- a/lib/amd/src/userfeedback.js +++ b/lib/amd/src/userfeedback.js @@ -41,27 +41,20 @@ export const registerEventListeners = () => { const giveAction = e.target.closest(Selectors.actions.give); if (giveAction) { e.preventDefault(); + giveFeedback() - .then(() => { - return recordAction('give'); - }) - .then(() => { - const root = giveAction.closest(Selectors.regions.root); - root.remove(); - return; - }) + .then(() => hideRoot(giveAction)) + .then(recordAction) .catch(Notification.exception); } const remindAction = e.target.closest(Selectors.actions.remind); if (remindAction) { e.preventDefault(); - recordAction('remind') - .then(() => { - const root = remindAction.closest(Selectors.regions.root); - root.remove(); - return; - }) + + Promise.resolve(remindAction) + .then(hideRoot) + .then(recordAction) .catch(Notification.exception); } }); @@ -90,14 +83,32 @@ const giveFeedback = () => { /** * Record the action that the user took. * - * @param {string} action The action that the user took. Either give or remind. - * @returns {Promise} + * @param {HTMLElement} clickedItem The action element that the user chose. + * @returns {Promise} */ -const recordAction = action => { - return Ajax.call([{ - methodname: 'core_create_userfeedback_action_record', - args: { - action, - } - }])[0]; +const recordAction = clickedItem => { + if (clickedItem.dataset.record) { + return Ajax.call([{ + methodname: 'core_create_userfeedback_action_record', + args: { + action: clickedItem.dataset.action, + } + }])[0]; + } + + return Promise.resolve(); +}; + +/** + * Hide the root node of the CTA notification. + * + * @param {HTMLElement} clickedItem The action element that the user chose. + * @returns {HTMLElement} + */ +const hideRoot = clickedItem => { + if (clickedItem.dataset.hide) { + clickedItem.closest(Selectors.regions.root).remove(); + } + + return clickedItem; }; diff --git a/lib/classes/external/userfeedback/generate_url.php b/lib/classes/external/userfeedback/generate_url.php index 821e3c82be1..307bbc2d19e 100644 --- a/lib/classes/external/userfeedback/generate_url.php +++ b/lib/classes/external/userfeedback/generate_url.php @@ -59,9 +59,7 @@ class generate_url extends external_api { * @return \stdClass */ public static function execute(int $contextid) { - global $CFG, $PAGE; - - require_once($CFG->libdir . '/adminlib.php'); + global $PAGE; external_api::validate_parameters(self::execute_parameters(), ['contextid' => $contextid]); @@ -69,22 +67,7 @@ class generate_url extends external_api { self::validate_context($context); $PAGE->set_context($context); - $baseurl = $CFG->userfeedback_url ?? 'https://feedback.moodle.org/lms'; - $lang = clean_param(current_language(), PARAM_LANG); // Avoid breaking WS because of incorrect package langs. - $moodleurl = $CFG->wwwroot; - $moodleversion = $CFG->release; - $theme = $PAGE->theme->name; - $themeversion = get_component_version('theme_' . $theme); - - $firstseparator = strpos($baseurl, '?') === false ? '?' : '&'; - - $url = $baseurl . $firstseparator . 'lang=' . $lang; - $url .= '&moodle_url=' . rawurlencode($moodleurl) . - '&moodle_version=' . rawurlencode($moodleversion) . - '&theme=' . rawurlencode($theme) . - '&theme_version=' . $themeversion; - - return $url; + return \core_userfeedback::make_link()->out(false); } /** diff --git a/lib/classes/userfeedback.php b/lib/classes/userfeedback.php index 6ede8955820..1bdddf79db3 100644 --- a/lib/classes/userfeedback.php +++ b/lib/classes/userfeedback.php @@ -60,6 +60,8 @@ class core_userfeedback { 'url' => '#', 'data' => [ 'action' => 'give', + 'record' => 1, + 'hide' => 1, ], ], [ @@ -67,6 +69,8 @@ class core_userfeedback { 'url' => '#', 'data' => [ 'action' => 'remind', + 'record' => 1, + 'hide' => 1, ], ], ]; @@ -113,6 +117,35 @@ class core_userfeedback { return false; } + /** + * Prepare and return the URL of the feedback site + * + * @return moodle_url + */ + public static function make_link(): moodle_url { + global $CFG, $PAGE; + require_once($CFG->libdir . '/adminlib.php'); + + $baseurl = $CFG->userfeedback_url ?? 'https://feedback.moodle.org/lms'; + $lang = clean_param(current_language(), PARAM_LANG); // Avoid breaking WS because of incorrect package langs. + $moodleurl = $CFG->wwwroot; + $moodleversion = $CFG->release; + $theme = $PAGE->theme->name; + $themeversion = get_component_version('theme_' . $theme); + + $url = new moodle_url($baseurl, [ + 'lang' => $lang, + 'moodle_url' => $moodleurl, + 'moodle_version' => $moodleversion, + 'theme' => $theme, + 'theme_version' => $themeversion, + 'newtest' => 'Y', // Respondents might be using the same device/browser to fill out the survey. + // The newtest param resets the session. + ]); + + return $url; + } + /** * Returns the last major upgrade time * diff --git a/lib/outputrenderers.php b/lib/outputrenderers.php index e3705ea3315..240d4490a9d 100644 --- a/lib/outputrenderers.php +++ b/lib/outputrenderers.php @@ -835,6 +835,12 @@ class core_renderer extends renderer_base { } } + if (isloggedin() && !isguestuser()) { + $output .= html_writer::div( + $this->render_from_template('core/userfeedback_footer_link', ['url' => core_userfeedback::make_link()->out(false)]) + ); + } + // This function is normally called from a layout.php file in {@link core_renderer::header()} // but some of the content won't be known until later, so we return a placeholder // for now. This will be replaced with the real content in {@link core_renderer::footer()}. diff --git a/lib/templates/userfeedback_footer_link.mustache b/lib/templates/userfeedback_footer_link.mustache new file mode 100644 index 00000000000..baca8520eb8 --- /dev/null +++ b/lib/templates/userfeedback_footer_link.mustache @@ -0,0 +1,27 @@ +{{! + 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 core/userfeedback_footer_link + + Adds the give feedback link. + + Example context (json): + {} +}} + +{{# pix }} i/bullhorn, core {{/ pix }} +{{#str}}calltofeedback_give{{/str}} From 363aaf6eaa9d513ee642f4883c18e530ee016191 Mon Sep 17 00:00:00 2001 From: Shamim Rezaie Date: Tue, 19 May 2020 11:14:20 +1000 Subject: [PATCH 8/8] MDL-68076 core: log user's click on feedback links --- lang/en/moodle.php | 2 + lib/amd/build/userfeedback.min.js | 2 +- lib/amd/build/userfeedback.min.js.map | 2 +- lib/amd/src/userfeedback.js | 1 + lib/classes/event/userfeedback_give.php | 63 +++++++++++++++++++ lib/classes/event/userfeedback_remind.php | 63 +++++++++++++++++++ .../external/userfeedback/record_action.php | 16 ++++- .../userfeedback/record_action_test.php | 6 ++ version.php | 2 +- 9 files changed, 152 insertions(+), 5 deletions(-) create mode 100644 lib/classes/event/userfeedback_give.php create mode 100644 lib/classes/event/userfeedback_remind.php diff --git a/lang/en/moodle.php b/lang/en/moodle.php index 497fdb0cc3f..a5754a78da1 100644 --- a/lang/en/moodle.php +++ b/lang/en/moodle.php @@ -805,6 +805,8 @@ $string['eventsearchresultsviewed'] = 'Search results viewed'; $string['eventunknownlogged'] = 'Unknown event'; $string['eventusercreated'] = 'User created'; $string['eventuserdeleted'] = 'User deleted'; +$string['eventuserfeedbackgiven'] = 'Feedback link clicked'; +$string['eventuserfeedbackremind'] = 'Remind me later feedback link clicked'; $string['eventuserlistviewed'] = 'User list viewed'; $string['eventuserloggedout'] = 'User logged out'; $string['eventuserpasswordpolicyfailed'] = 'User password failed password policy'; diff --git a/lib/amd/build/userfeedback.min.js b/lib/amd/build/userfeedback.min.js index 5eb8d7c8df9..3eee54bf623 100644 --- a/lib/amd/build/userfeedback.min.js +++ b/lib/amd/build/userfeedback.min.js @@ -1,2 +1,2 @@ -define ("core/userfeedback",["exports","core/ajax","core/notification"],function(a,b,c){"use strict";Object.defineProperty(a,"__esModule",{value:!0});a.registerEventListeners=void 0;b=d(b);c=d(c);function d(a){return a&&a.__esModule?a:{default:a}}var f={regions:{root:"[data-region=\"core/userfeedback\"]"},actions:{}};f.actions.give="".concat(f.regions.root," [data-action=\"give\"]");f.actions.remind="".concat(f.regions.root," [data-action=\"remind\"]");a.registerEventListeners=function registerEventListeners(){document.addEventListener("click",function(a){var b=a.target.closest(f.actions.give);if(b){a.preventDefault();g().then(function(){return i(b)}).then(h).catch(c.default.exception)}var d=a.target.closest(f.actions.remind);if(d){a.preventDefault();Promise.resolve(d).then(i).then(h).catch(c.default.exception)}})};var g=function(){return b.default.call([{methodname:"core_get_userfeedback_url",args:{contextid:M.cfg.contextid}}])[0].then(function(a){if(!window.open(a)){throw new Error("Unable to open popup")}})},h=function(a){if(a.dataset.record){return b.default.call([{methodname:"core_create_userfeedback_action_record",args:{action:a.dataset.action}}])[0]}return Promise.resolve()},i=function(a){if(a.dataset.hide){a.closest(f.regions.root).remove()}return a}}); +define ("core/userfeedback",["exports","core/ajax","core/notification"],function(a,b,c){"use strict";Object.defineProperty(a,"__esModule",{value:!0});a.registerEventListeners=void 0;b=d(b);c=d(c);function d(a){return a&&a.__esModule?a:{default:a}}var f={regions:{root:"[data-region=\"core/userfeedback\"]"},actions:{}};f.actions.give="".concat(f.regions.root," [data-action=\"give\"]");f.actions.remind="".concat(f.regions.root," [data-action=\"remind\"]");a.registerEventListeners=function registerEventListeners(){document.addEventListener("click",function(a){var b=a.target.closest(f.actions.give);if(b){a.preventDefault();g().then(function(){return i(b)}).then(h).catch(c.default.exception)}var d=a.target.closest(f.actions.remind);if(d){a.preventDefault();Promise.resolve(d).then(i).then(h).catch(c.default.exception)}})};var g=function(){return b.default.call([{methodname:"core_get_userfeedback_url",args:{contextid:M.cfg.contextid}}])[0].then(function(a){if(!window.open(a)){throw new Error("Unable to open popup")}})},h=function(a){if(a.dataset.record){return b.default.call([{methodname:"core_create_userfeedback_action_record",args:{action:a.dataset.action,contextid:M.cfg.contextid}}])[0]}return Promise.resolve()},i=function(a){if(a.dataset.hide){a.closest(f.regions.root).remove()}return a}}); //# sourceMappingURL=userfeedback.min.js.map diff --git a/lib/amd/build/userfeedback.min.js.map b/lib/amd/build/userfeedback.min.js.map index 5f74d58a0fe..2a4225f3097 100644 --- a/lib/amd/build/userfeedback.min.js.map +++ b/lib/amd/build/userfeedback.min.js.map @@ -1 +1 @@ -{"version":3,"sources":["../src/userfeedback.js"],"names":["Selectors","regions","root","actions","give","remind","registerEventListeners","document","addEventListener","e","giveAction","target","closest","preventDefault","giveFeedback","then","hideRoot","recordAction","catch","Notification","exception","remindAction","Promise","resolve","Ajax","call","methodname","args","contextid","M","cfg","url","window","open","Error","clickedItem","dataset","record","action","hide","remove"],"mappings":"sLAuBA,OACA,O,mDAEA,GAAMA,CAAAA,CAAS,CAAG,CACdC,OAAO,CAAE,CACLC,IAAI,CAAE,qCADD,CADK,CAIdC,OAAO,CAAE,EAJK,CAAlB,CAMAH,CAAS,CAACG,OAAV,CAAkBC,IAAlB,WAA4BJ,CAAS,CAACC,OAAV,CAAkBC,IAA9C,4BACAF,CAAS,CAACG,OAAV,CAAkBE,MAAlB,WAA8BL,CAAS,CAACC,OAAV,CAAkBC,IAAhD,8B,yBAKsC,QAAzBI,CAAAA,sBAAyB,EAAM,CACxCC,QAAQ,CAACC,gBAAT,CAA0B,OAA1B,CAAmC,SAAAC,CAAC,CAAI,CACpC,GAAMC,CAAAA,CAAU,CAAGD,CAAC,CAACE,MAAF,CAASC,OAAT,CAAiBZ,CAAS,CAACG,OAAV,CAAkBC,IAAnC,CAAnB,CACA,GAAIM,CAAJ,CAAgB,CACZD,CAAC,CAACI,cAAF,GAEAC,CAAY,GACPC,IADL,CACU,iBAAMC,CAAAA,CAAQ,CAACN,CAAD,CAAd,CADV,EAEKK,IAFL,CAEUE,CAFV,EAGKC,KAHL,CAGWC,UAAaC,SAHxB,CAIH,CAED,GAAMC,CAAAA,CAAY,CAAGZ,CAAC,CAACE,MAAF,CAASC,OAAT,CAAiBZ,CAAS,CAACG,OAAV,CAAkBE,MAAnC,CAArB,CACA,GAAIgB,CAAJ,CAAkB,CACdZ,CAAC,CAACI,cAAF,GAEAS,OAAO,CAACC,OAAR,CAAgBF,CAAhB,EACKN,IADL,CACUC,CADV,EAEKD,IAFL,CAEUE,CAFV,EAGKC,KAHL,CAGWC,UAAaC,SAHxB,CAIH,CACJ,CApBD,CAqBH,C,IAOKN,CAAAA,CAAY,CAAG,UAAM,CACvB,MAAOU,WAAKC,IAAL,CAAU,CAAC,CACdC,UAAU,CAAE,2BADE,CAEdC,IAAI,CAAE,CACFC,SAAS,CAAEC,CAAC,CAACC,GAAF,CAAMF,SADf,CAFQ,CAAD,CAAV,EAKH,CALG,EAMFb,IANE,CAMG,SAAAgB,CAAG,CAAI,CACT,GAAI,CAACC,MAAM,CAACC,IAAP,CAAYF,CAAZ,CAAL,CAAuB,CACnB,KAAM,IAAIG,CAAAA,KAAJ,CAAU,sBAAV,CACT,CAEJ,CAXE,CAYV,C,CAQKjB,CAAY,CAAG,SAAAkB,CAAW,CAAI,CAChC,GAAIA,CAAW,CAACC,OAAZ,CAAoBC,MAAxB,CAAgC,CAC5B,MAAOb,WAAKC,IAAL,CAAU,CAAC,CACdC,UAAU,CAAE,wCADE,CAEdC,IAAI,CAAE,CACFW,MAAM,CAAEH,CAAW,CAACC,OAAZ,CAAoBE,MAD1B,CAFQ,CAAD,CAAV,EAKH,CALG,CAMV,CAED,MAAOhB,CAAAA,OAAO,CAACC,OAAR,EACV,C,CAQKP,CAAQ,CAAG,SAAAmB,CAAW,CAAI,CAC5B,GAAIA,CAAW,CAACC,OAAZ,CAAoBG,IAAxB,CAA8B,CAC1BJ,CAAW,CAACvB,OAAZ,CAAoBZ,CAAS,CAACC,OAAV,CAAkBC,IAAtC,EAA4CsC,MAA5C,EACH,CAED,MAAOL,CAAAA,CACV,C","sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see .\n\n/**\n * Handle clicking on action links of the feedback alert.\n *\n * @module core/cta_feedback\n * @copyright 2020 Shamim Rezaie \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport Ajax from 'core/ajax';\nimport Notification from 'core/notification';\n\nconst Selectors = {\n regions: {\n root: '[data-region=\"core/userfeedback\"]',\n },\n actions: {},\n};\nSelectors.actions.give = `${Selectors.regions.root} [data-action=\"give\"]`;\nSelectors.actions.remind = `${Selectors.regions.root} [data-action=\"remind\"]`;\n\n/**\n * Attach the necessary event handlers to the action links\n */\nexport const registerEventListeners = () => {\n document.addEventListener('click', e => {\n const giveAction = e.target.closest(Selectors.actions.give);\n if (giveAction) {\n e.preventDefault();\n\n giveFeedback()\n .then(() => hideRoot(giveAction))\n .then(recordAction)\n .catch(Notification.exception);\n }\n\n const remindAction = e.target.closest(Selectors.actions.remind);\n if (remindAction) {\n e.preventDefault();\n\n Promise.resolve(remindAction)\n .then(hideRoot)\n .then(recordAction)\n .catch(Notification.exception);\n }\n });\n};\n\n/**\n * The action function that is called when users choose to give feedback.\n *\n * @returns {Promise}\n */\nconst giveFeedback = () => {\n return Ajax.call([{\n methodname: 'core_get_userfeedback_url',\n args: {\n contextid: M.cfg.contextid,\n }\n }])[0]\n .then(url => {\n if (!window.open(url)) {\n throw new Error('Unable to open popup');\n }\n return;\n });\n};\n\n/**\n * Record the action that the user took.\n *\n * @param {HTMLElement} clickedItem The action element that the user chose.\n * @returns {Promise}\n */\nconst recordAction = clickedItem => {\n if (clickedItem.dataset.record) {\n return Ajax.call([{\n methodname: 'core_create_userfeedback_action_record',\n args: {\n action: clickedItem.dataset.action,\n }\n }])[0];\n }\n\n return Promise.resolve();\n};\n\n/**\n * Hide the root node of the CTA notification.\n *\n * @param {HTMLElement} clickedItem The action element that the user chose.\n * @returns {HTMLElement}\n */\nconst hideRoot = clickedItem => {\n if (clickedItem.dataset.hide) {\n clickedItem.closest(Selectors.regions.root).remove();\n }\n\n return clickedItem;\n};\n"],"file":"userfeedback.min.js"} \ No newline at end of file +{"version":3,"sources":["../src/userfeedback.js"],"names":["Selectors","regions","root","actions","give","remind","registerEventListeners","document","addEventListener","e","giveAction","target","closest","preventDefault","giveFeedback","then","hideRoot","recordAction","catch","Notification","exception","remindAction","Promise","resolve","Ajax","call","methodname","args","contextid","M","cfg","url","window","open","Error","clickedItem","dataset","record","action","hide","remove"],"mappings":"sLAuBA,OACA,O,mDAEA,GAAMA,CAAAA,CAAS,CAAG,CACdC,OAAO,CAAE,CACLC,IAAI,CAAE,qCADD,CADK,CAIdC,OAAO,CAAE,EAJK,CAAlB,CAMAH,CAAS,CAACG,OAAV,CAAkBC,IAAlB,WAA4BJ,CAAS,CAACC,OAAV,CAAkBC,IAA9C,4BACAF,CAAS,CAACG,OAAV,CAAkBE,MAAlB,WAA8BL,CAAS,CAACC,OAAV,CAAkBC,IAAhD,8B,yBAKsC,QAAzBI,CAAAA,sBAAyB,EAAM,CACxCC,QAAQ,CAACC,gBAAT,CAA0B,OAA1B,CAAmC,SAAAC,CAAC,CAAI,CACpC,GAAMC,CAAAA,CAAU,CAAGD,CAAC,CAACE,MAAF,CAASC,OAAT,CAAiBZ,CAAS,CAACG,OAAV,CAAkBC,IAAnC,CAAnB,CACA,GAAIM,CAAJ,CAAgB,CACZD,CAAC,CAACI,cAAF,GAEAC,CAAY,GACPC,IADL,CACU,iBAAMC,CAAAA,CAAQ,CAACN,CAAD,CAAd,CADV,EAEKK,IAFL,CAEUE,CAFV,EAGKC,KAHL,CAGWC,UAAaC,SAHxB,CAIH,CAED,GAAMC,CAAAA,CAAY,CAAGZ,CAAC,CAACE,MAAF,CAASC,OAAT,CAAiBZ,CAAS,CAACG,OAAV,CAAkBE,MAAnC,CAArB,CACA,GAAIgB,CAAJ,CAAkB,CACdZ,CAAC,CAACI,cAAF,GAEAS,OAAO,CAACC,OAAR,CAAgBF,CAAhB,EACKN,IADL,CACUC,CADV,EAEKD,IAFL,CAEUE,CAFV,EAGKC,KAHL,CAGWC,UAAaC,SAHxB,CAIH,CACJ,CApBD,CAqBH,C,IAOKN,CAAAA,CAAY,CAAG,UAAM,CACvB,MAAOU,WAAKC,IAAL,CAAU,CAAC,CACdC,UAAU,CAAE,2BADE,CAEdC,IAAI,CAAE,CACFC,SAAS,CAAEC,CAAC,CAACC,GAAF,CAAMF,SADf,CAFQ,CAAD,CAAV,EAKH,CALG,EAMFb,IANE,CAMG,SAAAgB,CAAG,CAAI,CACT,GAAI,CAACC,MAAM,CAACC,IAAP,CAAYF,CAAZ,CAAL,CAAuB,CACnB,KAAM,IAAIG,CAAAA,KAAJ,CAAU,sBAAV,CACT,CAEJ,CAXE,CAYV,C,CAQKjB,CAAY,CAAG,SAAAkB,CAAW,CAAI,CAChC,GAAIA,CAAW,CAACC,OAAZ,CAAoBC,MAAxB,CAAgC,CAC5B,MAAOb,WAAKC,IAAL,CAAU,CAAC,CACdC,UAAU,CAAE,wCADE,CAEdC,IAAI,CAAE,CACFW,MAAM,CAAEH,CAAW,CAACC,OAAZ,CAAoBE,MAD1B,CAEFV,SAAS,CAAEC,CAAC,CAACC,GAAF,CAAMF,SAFf,CAFQ,CAAD,CAAV,EAMH,CANG,CAOV,CAED,MAAON,CAAAA,OAAO,CAACC,OAAR,EACV,C,CAQKP,CAAQ,CAAG,SAAAmB,CAAW,CAAI,CAC5B,GAAIA,CAAW,CAACC,OAAZ,CAAoBG,IAAxB,CAA8B,CAC1BJ,CAAW,CAACvB,OAAZ,CAAoBZ,CAAS,CAACC,OAAV,CAAkBC,IAAtC,EAA4CsC,MAA5C,EACH,CAED,MAAOL,CAAAA,CACV,C","sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see .\n\n/**\n * Handle clicking on action links of the feedback alert.\n *\n * @module core/cta_feedback\n * @copyright 2020 Shamim Rezaie \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport Ajax from 'core/ajax';\nimport Notification from 'core/notification';\n\nconst Selectors = {\n regions: {\n root: '[data-region=\"core/userfeedback\"]',\n },\n actions: {},\n};\nSelectors.actions.give = `${Selectors.regions.root} [data-action=\"give\"]`;\nSelectors.actions.remind = `${Selectors.regions.root} [data-action=\"remind\"]`;\n\n/**\n * Attach the necessary event handlers to the action links\n */\nexport const registerEventListeners = () => {\n document.addEventListener('click', e => {\n const giveAction = e.target.closest(Selectors.actions.give);\n if (giveAction) {\n e.preventDefault();\n\n giveFeedback()\n .then(() => hideRoot(giveAction))\n .then(recordAction)\n .catch(Notification.exception);\n }\n\n const remindAction = e.target.closest(Selectors.actions.remind);\n if (remindAction) {\n e.preventDefault();\n\n Promise.resolve(remindAction)\n .then(hideRoot)\n .then(recordAction)\n .catch(Notification.exception);\n }\n });\n};\n\n/**\n * The action function that is called when users choose to give feedback.\n *\n * @returns {Promise}\n */\nconst giveFeedback = () => {\n return Ajax.call([{\n methodname: 'core_get_userfeedback_url',\n args: {\n contextid: M.cfg.contextid,\n }\n }])[0]\n .then(url => {\n if (!window.open(url)) {\n throw new Error('Unable to open popup');\n }\n return;\n });\n};\n\n/**\n * Record the action that the user took.\n *\n * @param {HTMLElement} clickedItem The action element that the user chose.\n * @returns {Promise}\n */\nconst recordAction = clickedItem => {\n if (clickedItem.dataset.record) {\n return Ajax.call([{\n methodname: 'core_create_userfeedback_action_record',\n args: {\n action: clickedItem.dataset.action,\n contextid: M.cfg.contextid,\n }\n }])[0];\n }\n\n return Promise.resolve();\n};\n\n/**\n * Hide the root node of the CTA notification.\n *\n * @param {HTMLElement} clickedItem The action element that the user chose.\n * @returns {HTMLElement}\n */\nconst hideRoot = clickedItem => {\n if (clickedItem.dataset.hide) {\n clickedItem.closest(Selectors.regions.root).remove();\n }\n\n return clickedItem;\n};\n"],"file":"userfeedback.min.js"} \ No newline at end of file diff --git a/lib/amd/src/userfeedback.js b/lib/amd/src/userfeedback.js index a74bf4eb172..c1b1a7f56e4 100644 --- a/lib/amd/src/userfeedback.js +++ b/lib/amd/src/userfeedback.js @@ -92,6 +92,7 @@ const recordAction = clickedItem => { methodname: 'core_create_userfeedback_action_record', args: { action: clickedItem.dataset.action, + contextid: M.cfg.contextid, } }])[0]; } diff --git a/lib/classes/event/userfeedback_give.php b/lib/classes/event/userfeedback_give.php new file mode 100644 index 00000000000..e0b89dfbaca --- /dev/null +++ b/lib/classes/event/userfeedback_give.php @@ -0,0 +1,63 @@ +. + +/** + * Feedback given. + * + * @package core + * @copyright 2020 Shamim Rezaie + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +namespace core\event; + +defined('MOODLE_INTERNAL') || die(); + +/** + * Class userfeedback_give + * + * @package core + * @copyright 2020 Shamim Rezaie + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class userfeedback_give extends base { + + /** + * Initialise required event data properties. + */ + protected function init() { + $this->data['crud'] = 'r'; + $this->data['edulevel'] = self::LEVEL_OTHER; + } + + /** + * Returns description of what happened. + * + * @return string + */ + public function get_description() { + return "The user with id '$this->userid' clicked on the give feedback link"; + } + + /** + * Return localised event name. + * + * @return string + */ + public static function get_name() { + return get_string('eventuserfeedbackgiven'); + } + +} diff --git a/lib/classes/event/userfeedback_remind.php b/lib/classes/event/userfeedback_remind.php new file mode 100644 index 00000000000..e30205cb446 --- /dev/null +++ b/lib/classes/event/userfeedback_remind.php @@ -0,0 +1,63 @@ +. + +/** + * Feedback remind. + * + * @package core + * @copyright 2020 Shamim Rezaie + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +namespace core\event; + +defined('MOODLE_INTERNAL') || die(); + +/** + * Class userfeedback_remind + * + * @package core + * @copyright 2020 Shamim Rezaie + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class userfeedback_remind extends base { + + /** + * Initialise required event data properties. + */ + protected function init() { + $this->data['crud'] = 'r'; + $this->data['edulevel'] = self::LEVEL_OTHER; + } + + /** + * Returns description of what happened. + * + * @return string + */ + public function get_description() { + return "The user with id '$this->userid' clicked on the remind later to feedback link"; + } + + /** + * Return localised event name. + * + * @return string + */ + public static function get_name() { + return get_string('eventuserfeedbackremind'); + } + +} diff --git a/lib/classes/external/userfeedback/record_action.php b/lib/classes/external/userfeedback/record_action.php index 26f79fed5e9..e8950f5951a 100644 --- a/lib/classes/external/userfeedback/record_action.php +++ b/lib/classes/external/userfeedback/record_action.php @@ -47,6 +47,7 @@ class record_action extends external_api { public static function execute_parameters() { return new external_function_parameters([ 'action' => new external_value(PARAM_ALPHA, 'The action taken by user'), + 'contextid' => new external_value(PARAM_INT, 'The context id of the page the user is in'), ]); } @@ -54,17 +55,28 @@ class record_action extends external_api { * Record users action to the feedback CTA * * @param string $action The action the user took + * @param int $contextid The context id * @throws \invalid_parameter_exception */ - public static function execute(string $action) { - external_api::validate_parameters(self::execute_parameters(), ['action' => $action]); + public static function execute(string $action, int $contextid) { + external_api::validate_parameters(self::execute_parameters(), [ + 'action' => $action, + 'contextid' => $contextid, + ]); + + $context = \context::instance_by_id($contextid); + self::validate_context($context); switch ($action) { case 'give': set_user_preference('core_userfeedback_give', time()); + $event = \core\event\userfeedback_give::create(['context' => $context]); + $event->trigger(); break; case 'remind': set_user_preference('core_userfeedback_remind', time()); + $event = \core\event\userfeedback_remind::create(['context' => $context]); + $event->trigger(); break; default: throw new \invalid_parameter_exception('Invalid value for action parameter (value: ' . $action . '),' . diff --git a/lib/tests/external/userfeedback/record_action_test.php b/lib/tests/external/userfeedback/record_action_test.php index 610acad35bd..f67cf57b6d9 100644 --- a/lib/tests/external/userfeedback/record_action_test.php +++ b/lib/tests/external/userfeedback/record_action_test.php @@ -69,6 +69,7 @@ class record_action_testcase extends externallib_advanced_testcase { $context = context_system::instance(); $user = $this->getDataGenerator()->create_user(); $this->setUser($user); + $eventsink = $this->redirectEvents(); $now = time(); @@ -78,5 +79,10 @@ class record_action_testcase extends externallib_advanced_testcase { $preference = get_user_preferences('core_userfeedback_' . $action); $this->assertGreaterThanOrEqual($now, $preference); + + $events = $eventsink->get_events(); + $this->assertCount(1, $events); + $this->assertInstanceOf('\core\event\userfeedback_' . $action, $events[0]); + $eventsink->clear(); } } diff --git a/version.php b/version.php index 9dec8821b66..e89cfb9fa9d 100644 --- a/version.php +++ b/version.php @@ -29,7 +29,7 @@ defined('MOODLE_INTERNAL') || die(); -$version = 2020052900.02; // YYYYMMDD = weekly release date of this DEV branch. +$version = 2020052900.03; // YYYYMMDD = weekly release date of this DEV branch. // RR = release increments - 00 in DEV branches. // .XX = incremental changes. $release = '3.9dev+ (Build: 20200529)'; // Human-friendly version name