Merge branch 'MDL-60680-master' of git://github.com/jleyva/moodle

This commit is contained in:
David Monllaó 2019-04-25 18:54:19 +02:00
commit b63c0b9079
47 changed files with 556 additions and 36 deletions

View File

@ -290,13 +290,29 @@ class core_badges_badgeslib_testcase extends advanced_testcase {
}
public function test_badge_awards() {
global $DB;
$this->preventResetByRollback(); // Messaging is not compatible with transactions.
$badge = new badge($this->badgeid);
$user1 = $this->getDataGenerator()->create_user();
$badge->issue($user1->id, true);
$sink = $this->redirectMessages();
$DB->set_field_select('message_processors', 'enabled', 0, "name <> 'email'");
set_user_preference('message_provider_moodle_badgerecipientnotice_loggedoff', 'email', $user1);
$badge->issue($user1->id, false);
$this->assertDebuggingCalled(); // Expect debugging while baking a badge via phpunit.
$this->assertTrue($badge->is_issued($user1->id));
$messages = $sink->get_messages();
$sink->close();
$this->assertCount(1, $messages);
$message = array_pop($messages);
// Check we have the expected data.
$customdata = json_decode($message->customdata);
$this->assertObjectHasAttribute('notificationiconurl', $customdata);
$this->assertObjectHasAttribute('hash', $customdata);
$user2 = $this->getDataGenerator()->create_user();
$badge->issue($user2->id, true);
$this->assertTrue($badge->is_issued($user2->id));

View File

@ -38,7 +38,7 @@ use core_competency\user_evidence;
* @return array
*/
function core_competency_comment_add($comment, $params) {
global $USER;
global $USER, $PAGE;
if (!get_config('core_competency', 'enabled')) {
return;
@ -132,10 +132,16 @@ function core_competency_comment_add($comment, $params) {
$message->contexturl = $url->out(false);
$message->contexturlname = $urlname;
$userpicture = new \user_picture($user);
// Message each recipient.
foreach ($recipients as $recipient) {
$msgcopy = clone($message);
$msgcopy->userto = $recipient;
// Generate an out-of-session token for the user receiving the message.
$userpicture->includetoken = $recipient;
$msgcopy->customdata = [
'notificationiconurl' => $userpicture->get_url($PAGE)->out(false),
];
message_send($msgcopy);
}
@ -201,10 +207,16 @@ function core_competency_comment_add($comment, $params) {
$message->contexturl = $url->out(false);
$message->contexturlname = $urlname;
$userpicture = new \user_picture($user);
// Message each recipient.
foreach ($recipients as $recipient) {
$msgcopy = clone($message);
$msgcopy->userto = $recipient;
// Generate an out-of-session token for the user receiving the message.
$userpicture->includetoken = $recipient;
$msgcopy->customdata = [
'notificationiconurl' => $userpicture->get_url($PAGE)->out(false),
];
message_send($msgcopy);
}
}

View File

@ -40,12 +40,12 @@ global $CFG;
class core_competency_lib_testcase extends advanced_testcase {
public function test_comment_add_user_competency() {
global $DB;
global $DB, $PAGE;
$this->resetAfterTest();
$dg = $this->getDataGenerator();
$lpg = $dg->get_plugin_generator('core_competency');
$u1 = $dg->create_user();
$u1 = $dg->create_user(['picture' => 1]);
$u2 = $dg->create_user();
$u3 = $dg->create_user();
$reviewerroleid = $dg->create_role();
@ -96,6 +96,13 @@ class core_competency_lib_testcase extends advanced_testcase {
$this->assertEquals(FORMAT_MOODLE, $message->fullmessageformat);
$this->assertEquals($expectedurl->out(false), $message->contexturl);
$this->assertEquals($expectedurlname, $message->contexturlname);
// Test customdata.
$customdata = json_decode($message->customdata);
$this->assertObjectHasAttribute('notificationiconurl', $customdata);
$this->assertContains('tokenpluginfile.php', $customdata->notificationiconurl);
$userpicture = new \user_picture($u1);
$userpicture->includetoken = $u2->id;
$this->assertEquals($userpicture->get_url($PAGE)->out(false), $customdata->notificationiconurl);
// Reviewer posts a comment for the user competency being in two plans. Owner is messaged.
$this->setUser($u2);
@ -218,6 +225,9 @@ class core_competency_lib_testcase extends advanced_testcase {
$message = array_pop($messages);
$this->assertEquals(core_user::get_noreply_user()->id, $message->useridfrom);
$this->assertEquals($u1->id, $message->useridto);
// Test customdata.
$customdata = json_decode($message->customdata);
$this->assertObjectHasAttribute('notificationiconurl', $customdata);
// Post a comment in a plan with reviewer. The reviewer is messaged.
$p1->set('reviewerid', $u2->id);

View File

@ -157,6 +157,7 @@ $string['privacy:metadata:messages:useridfrom'] = 'The ID of the user who sent t
$string['privacy:metadata:messages:smallmessage'] = 'A small version of the message';
$string['privacy:metadata:messages:subject'] = 'The subject of the message';
$string['privacy:metadata:messages:timecreated'] = 'The time when the message was created';
$string['privacy:metadata:messages:customdata'] = 'Custom data, usually contains internal ids and a public URL of the sender image (user or group).';
$string['privacy:metadata:message_contacts'] = 'The list of contacts';
$string['privacy:metadata:message_contacts:contactid'] = 'The ID of the user who is a contact';
$string['privacy:metadata:message_contacts:timecreated'] = 'The time when the contact was created';
@ -197,6 +198,7 @@ $string['privacy:metadata:notifications:timeread'] = 'The time when the notifica
$string['privacy:metadata:notifications:timecreated'] = 'The time when the notification was created';
$string['privacy:metadata:notifications:useridfrom'] = 'The ID of the user who sent the notification';
$string['privacy:metadata:notifications:useridto'] = 'The ID of the user who received the notification';
$string['privacy:metadata:notifications:customdata'] = 'Custom data, usually contains internal ids and a public URL of the sender picture (if any).';
$string['privacy:metadata:preference:core_message_settings'] = 'Settings related to messaging';
$string['privacy:request:preference:set'] = 'The value of the setting \'{$a->name}\' was \'{$a->value}\'';
$string['privacy:export:conversationprefix'] = 'Conversation: ';

View File

@ -23,6 +23,7 @@
*/
$string['abouttobeinstalled'] = 'about to be installed';
$string['accept'] = 'Accept';
$string['action'] = 'Action';
$string['actionchoice'] = 'What do you want to do with the file \'{$a}\'?';
$string['actions'] = 'Actions';

View File

@ -1012,6 +1012,11 @@ function badges_notify_badge_award(badge $badge, $userid, $issued, $filepathhash
$eventdata->fullmessageformat = FORMAT_HTML;
$eventdata->fullmessagehtml = $message;
$eventdata->smallmessage = '';
$eventdata->customdata = [
'notificationiconurl' => moodle_url::make_pluginfile_url(
$badge->get_context()->id, 'badges', 'badgeimage', $badge->id, '/', 'f1')->out(),
'hash' => $issued,
];
// Attach badge image if possible.
if (!empty($CFG->allowattachments) && $badge->attachment && is_string($filepathhash)) {
@ -1049,6 +1054,11 @@ function badges_notify_badge_award(badge $badge, $userid, $issued, $filepathhash
$eventdata->fullmessageformat = FORMAT_HTML;
$eventdata->fullmessagehtml = $creatormessage;
$eventdata->smallmessage = '';
$eventdata->customdata = [
'notificationiconurl' => moodle_url::make_pluginfile_url(
$badge->get_context()->id, 'badges', 'badgeimage', $badge->id, '/', 'f1')->out(),
'hash' => $issued,
];
message_send($eventdata);
$DB->set_field('badge_issued', 'issuernotified', time(), array('badgeid' => $badge->id, 'userid' => $userid));

View File

@ -169,6 +169,14 @@ class manager {
// Spoof the userto based on the current member id.
$localisedeventdata->userto = $recipient;
// Check if the notification is including images that will need a user token to be displayed outside Moodle.
if (!empty($localisedeventdata->customdata)) {
$customdata = json_decode($localisedeventdata->customdata);
if (is_object($customdata) && !empty($customdata->notificationiconurl)) {
$customdata->tokenpluginfile = get_user_key('core_files', $localisedeventdata->userto->id);
$localisedeventdata->customdata = $customdata; // Message class will JSON encode again.
}
}
$s = new \stdClass();
$s->sitename = format_string($SITE->shortname, true, array('context' => \context_course::instance(SITEID)));

View File

@ -52,6 +52,7 @@ defined('MOODLE_INTERNAL') || die();
* replyto string An email address which can be used to send an reply.
* attachment stored_file File instance that needs to be sent as attachment.
* attachname string Name of the attachment.
* customdata mixed Custom data to be passed to the message processor. Must be serialisable using json_encode().
*
* @package core_message
* @since Moodle 2.9
@ -125,9 +126,12 @@ class message {
/** @var int The time the message was created.*/
private $timecreated;
/** @var boolean Mark trust content. */
/** @var boolean Mark trust content. */
private $fullmessagetrust;
/** @var mixed Custom data to be passed to the message processor. Must be serialisable using json_encode(). */
private $customdata;
/** @var array a list of properties that is allowed for each message. */
private $properties = array(
'courseid',
@ -152,8 +156,9 @@ class message {
'attachment',
'attachname',
'timecreated',
'fullmessagetrust'
);
'fullmessagetrust',
'customdata',
);
/** @var array property to store any additional message processor specific content */
private $additionalcontent = array();
@ -203,6 +208,20 @@ class message {
}
}
/**
* Always JSON encode customdata.
*
* @param mixed $customdata a data structure that must be serialisable using json_encode().
*/
protected function set_customdata($customdata) {
// Always include the courseid (because is not stored in the notifications or messages table).
if (!empty($this->courseid) && (is_object($customdata) || is_array($customdata))) {
$customdata = (array) $customdata;
$customdata['courseid'] = $this->courseid;
}
$this->customdata = json_encode($customdata);
}
/**
* Helper method used to get message content added with processor specific content.
*
@ -255,6 +274,12 @@ class message {
* @throws \coding_exception
*/
public function __set($prop, $value) {
// Custom data must be JSON encoded always.
if ($prop == 'customdata') {
return $this->set_customdata($value);
}
if (in_array($prop, $this->properties)) {
return $this->$prop = $value;
}

View File

@ -552,6 +552,7 @@
<FIELD NAME="timeusertodeleted" TYPE="int" LENGTH="10" NOTNULL="true" DEFAULT="0" SEQUENCE="false"/>
<FIELD NAME="component" TYPE="char" LENGTH="100" NOTNULL="false" SEQUENCE="false"/>
<FIELD NAME="eventtype" TYPE="char" LENGTH="100" NOTNULL="false" SEQUENCE="false"/>
<FIELD NAME="customdata" TYPE="text" NOTNULL="false" SEQUENCE="false" COMMENT="Custom data to be passed to the message processor. Must be serialisable using json_encode()"/>
</FIELDS>
<KEYS>
<KEY NAME="primary" TYPE="primary" FIELDS="id"/>
@ -604,6 +605,7 @@
<FIELD NAME="smallmessage" TYPE="text" NOTNULL="false" SEQUENCE="false"/>
<FIELD NAME="timecreated" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="false"/>
<FIELD NAME="fullmessagetrust" TYPE="int" LENGTH="2" NOTNULL="true" DEFAULT="0" SEQUENCE="false"/>
<FIELD NAME="customdata" TYPE="text" NOTNULL="false" SEQUENCE="false" COMMENT="Custom data to be passed to the message processor. Must be serialisable using json_encode()"/>
</FIELDS>
<KEYS>
<KEY NAME="primary" TYPE="primary" FIELDS="id"/>
@ -698,6 +700,7 @@
<FIELD NAME="contexturlname" TYPE="text" NOTNULL="false" SEQUENCE="false"/>
<FIELD NAME="timeread" TYPE="int" LENGTH="10" NOTNULL="false" SEQUENCE="false"/>
<FIELD NAME="timecreated" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="false"/>
<FIELD NAME="customdata" TYPE="text" NOTNULL="false" SEQUENCE="false" COMMENT="Custom data to be passed to the message processor. Must be serialisable using json_encode()"/>
</FIELDS>
<KEYS>
<KEY NAME="primary" TYPE="primary" FIELDS="id"/>

View File

@ -72,12 +72,18 @@ $messageproviders = array (
// Course request approval notification
'courserequestapproved' => array (
'capability' => 'moodle/course:request'
'capability' => 'moodle/course:request',
'defaults' => array(
'airnotifier' => MESSAGE_PERMITTED + MESSAGE_DEFAULT_LOGGEDIN + MESSAGE_DEFAULT_LOGGEDOFF,
),
),
// Course request rejection notification
'courserequestrejected' => array (
'capability' => 'moodle/course:request'
'capability' => 'moodle/course:request',
'defaults' => array(
'airnotifier' => MESSAGE_PERMITTED + MESSAGE_DEFAULT_LOGGEDIN + MESSAGE_DEFAULT_LOGGEDOFF,
),
),
// Badge award notification to a badge recipient.
@ -85,6 +91,7 @@ $messageproviders = array (
'defaults' => array(
'popup' => MESSAGE_PERMITTED + MESSAGE_DEFAULT_LOGGEDIN + MESSAGE_DEFAULT_LOGGEDOFF,
'email' => MESSAGE_PERMITTED + MESSAGE_DEFAULT_LOGGEDOFF,
'airnotifier' => MESSAGE_PERMITTED + MESSAGE_DEFAULT_LOGGEDIN + MESSAGE_DEFAULT_LOGGEDOFF,
),
'capability' => 'moodle/badges:earnbadge'
),
@ -107,6 +114,7 @@ $messageproviders = array (
'defaults' => [
'popup' => MESSAGE_PERMITTED + MESSAGE_DEFAULT_LOGGEDIN + MESSAGE_DEFAULT_LOGGEDOFF,
'email' => MESSAGE_PERMITTED + MESSAGE_DEFAULT_LOGGEDOFF,
'airnotifier' => MESSAGE_PERMITTED + MESSAGE_DEFAULT_LOGGEDIN + MESSAGE_DEFAULT_LOGGEDOFF,
]
),
@ -115,6 +123,7 @@ $messageproviders = array (
'defaults' => [
'popup' => MESSAGE_PERMITTED + MESSAGE_DEFAULT_LOGGEDIN + MESSAGE_DEFAULT_LOGGEDOFF,
'email' => MESSAGE_PERMITTED + MESSAGE_DEFAULT_LOGGEDOFF,
'airnotifier' => MESSAGE_PERMITTED + MESSAGE_DEFAULT_LOGGEDIN + MESSAGE_DEFAULT_LOGGEDOFF,
]
],

View File

@ -3240,5 +3240,35 @@ function xmldb_main_upgrade($oldversion) {
upgrade_main_savepoint(true, 2019042300.01);
}
if ($oldversion < 2019042300.03) {
// Add new customdata field to message table.
$table = new xmldb_table('message');
$field = new xmldb_field('customdata', XMLDB_TYPE_TEXT, null, null, null, null, null, 'eventtype');
// Conditionally launch add field output.
if (!$dbman->field_exists($table, $field)) {
$dbman->add_field($table, $field);
}
// Add new customdata field to notifications and messages table.
$table = new xmldb_table('notifications');
$field = new xmldb_field('customdata', XMLDB_TYPE_TEXT, null, null, null, null, null, 'timecreated');
// Conditionally launch add field output.
if (!$dbman->field_exists($table, $field)) {
$dbman->add_field($table, $field);
}
$table = new xmldb_table('messages');
// Conditionally launch add field output.
if (!$dbman->field_exists($table, $field)) {
$dbman->add_field($table, $field);
}
// Main savepoint reached.
upgrade_main_savepoint(true, 2019042300.03);
}
return true;
}

View File

@ -470,7 +470,7 @@ function file_prepare_draft_area(&$draftitemid, $contextid, $component, $fileare
* @param array $options
* bool $options.forcehttps Force the user of https
* bool $options.reverse Reverse the behaviour of the function
* bool $options.includetoken Use a token for authentication
* mixed $options.includetoken Use a token for authentication. True for current user, int value for other user id.
* string The processed text.
*/
function file_rewrite_pluginfile_urls($text, $file, $contextid, $component, $filearea, $itemid, array $options=null) {
@ -483,7 +483,8 @@ function file_rewrite_pluginfile_urls($text, $file, $contextid, $component, $fil
$baseurl = "{$CFG->wwwroot}/{$file}";
if (!empty($options['includetoken'])) {
$token = get_user_key('core_files', $USER->id);
$userid = $options['includetoken'] === true ? $USER->id : $options['includetoken'];
$token = get_user_key('core_files', $userid);
$finalfile = basename($file);
$tokenfile = "token{$finalfile}";
$file = substr($file, 0, strlen($file) - strlen($finalfile)) . $tokenfile;

View File

@ -165,6 +165,7 @@ function message_send(\core\message\message $eventdata) {
$tabledata->fullmessagehtml = $eventdata->fullmessagehtml;
$tabledata->smallmessage = $eventdata->smallmessage;
$tabledata->timecreated = time();
$tabledata->customdata = $eventdata->customdata;
// The Trusted Content system.
// Texts created or uploaded by such users will be marked as trusted and will not be cleaned before display.
@ -267,6 +268,7 @@ function message_send(\core\message\message $eventdata) {
$tabledata->eventtype = $eventdata->name;
$tabledata->component = $eventdata->component;
$tabledata->timecreated = time();
$tabledata->customdata = $eventdata->customdata;
if (!empty($eventdata->contexturl)) {
$tabledata->contexturl = (string)$eventdata->contexturl;
} else {

View File

@ -207,7 +207,8 @@ class user_picture implements renderable {
public $includefullname = false;
/**
* @var bool Include user authentication token.
* @var mixed Include user authentication token. True indicates to generate a token for current user, and integer value
* indicates to generate a token for the user whose id is the value indicated.
*/
public $includetoken = false;

View File

@ -2434,7 +2434,7 @@ class core_renderer extends renderer_base {
* - class = image class attribute (default 'userpicture')
* - visibletoscreenreaders=true (whether to be visible to screen readers)
* - includefullname=false (whether to include the user's full name together with the user picture)
* - includetoken = false
* - includetoken = false (whether to use a token for authentication. True for current user, int value for other user id)
* @return string HTML fragment
*/
public function user_picture(stdClass $user, array $options = null) {

View File

@ -1150,6 +1150,19 @@ EOF;
// Compare the final text is the same that the original.
$this->assertEquals($originaltext, $finaltext);
// Now indicates a user different than $USER.
$user = $this->getDataGenerator()->create_user();
$options = ['includetoken' => $user->id];
// Rewrite the content. This will generate a new token.
$finaltext = file_rewrite_pluginfile_urls(
$originaltext, 'pluginfile.php', $syscontext->id, 'user', 'private', 0, $options);
$token = get_user_key('core_files', $user->id);
$expectedurl = new \moodle_url("/tokenpluginfile.php/{$token}/{$syscontext->id}/user/private/0/image.png");
$expectedtext = "Fake test with an image <img src=\"{$expectedurl}\">";
$this->assertEquals($expectedtext, $finaltext);
}
/**

View File

@ -202,6 +202,7 @@ class core_messagelib_testcase extends advanced_testcase {
$message->fullmessagehtml = '<p>message body</p>';
$message->smallmessage = 'small message';
$message->notification = '0';
$message->customdata = ['datakey' => 'data'];
$sink = $this->redirectMessages();
$this->setCurrentTimeStart();
@ -218,6 +219,12 @@ class core_messagelib_testcase extends advanced_testcase {
$this->assertEquals($message->smallmessage, $savedmessage->smallmessage);
$this->assertEquals($message->smallmessage, $savedmessage->smallmessage);
$this->assertEquals($message->notification, $savedmessage->notification);
$this->assertEquals($message->customdata, $savedmessage->customdata);
$this->assertContains('datakey', $savedmessage->customdata);
// Check it was a unserialisable json.
$customdata = json_decode($savedmessage->customdata);
$this->assertEquals('data', $customdata->datakey);
$this->assertEquals(1, $customdata->courseid);
$this->assertTimeCurrent($savedmessage->timecreated);
$record = $DB->get_record('messages', array('id' => $savedmessage->id), '*', MUST_EXIST);
unset($savedmessage->useridto);

View File

@ -113,7 +113,7 @@ class core_outputcomponents_testcase extends advanced_testcase {
}
public function test_get_url() {
global $DB, $CFG;
global $DB, $CFG, $USER;
$this->resetAfterTest();
@ -219,6 +219,18 @@ class core_outputcomponents_testcase extends advanced_testcase {
$up1 = new user_picture($user1);
$this->assertSame($CFG->wwwroot.'/pluginfile.php/'.$context1->id.'/user/icon/boost/f2?rev=11', $up1->get_url($page, $renderer)->out(false));
// Uploaded image with token-based access for current user.
$up1 = new user_picture($user1);
$up1->includetoken = true;
$token = get_user_key('core_files', $USER->id);
$this->assertSame($CFG->wwwroot.'/tokenpluginfile.php/'.$token.'/'.$context1->id.'/user/icon/boost/f2?rev=11', $up1->get_url($page, $renderer)->out(false));
// Uploaded image with token-based access for other user.
$up1 = new user_picture($user1);
$up1->includetoken = $user2->id;
$token = get_user_key('core_files', $user2->id);
$this->assertSame($CFG->wwwroot.'/tokenpluginfile.php/'.$token.'/'.$context1->id.'/user/icon/boost/f2?rev=11', $up1->get_url($page, $renderer)->out(false));
// Https version.
$CFG->wwwroot = str_replace('http:', 'https:', $CFG->wwwroot);

View File

@ -42,6 +42,12 @@ is disabled).
matching record found or when there are multiple records found for the given field value. If false, it will simply return false.
Defaults to false when not set.
* Exposed submit button to allow custom styling (via customclassoverride variable) which can override btn-primary/btn-secondary classes
* `$includetoken` parameter type has been changed. Now supports:
boolean: False indicates to not include the token, true indicates to generate a token for the current user ($USER).
integer: Indicates to generate a token for the user whose id is the integer value.
* The following functions have been updated to support the new usage:
- make_pluginfile_url
- file_rewrite_pluginfile_urls
=== 3.6 ===

View File

@ -773,7 +773,9 @@ class moodle_url {
* @param string $pathname
* @param string $filename
* @param bool $forcedownload
* @param boolean $includetoken Whether to use a user token when displaying this group image.
* @param mixed $includetoken Whether to use a user token when displaying this group image.
* True indicates to generate a token for current user, and integer value indicates to generate a token for the
* user whose id is the value indicated.
* If the group picture is included in an e-mail or some other location where the audience is a specific
* user who will not be logged in when viewing, then we use a token to authenticate the user.
* @return moodle_url
@ -786,7 +788,8 @@ class moodle_url {
if ($includetoken) {
$urlbase = "$CFG->wwwroot/tokenpluginfile.php";
$token = get_user_key('core_files', $USER->id);
$userid = $includetoken === true ? $USER->id : $includetoken;
$token = get_user_key('core_files', $userid);
if ($CFG->slasharguments) {
$path[] = $token;
}
@ -2491,6 +2494,8 @@ function print_collapsible_region_end($return = false) {
* @param boolean $return If false print picture, otherwise return the output as string
* @param boolean $link Enclose image in a link to view specified course?
* @param boolean $includetoken Whether to use a user token when displaying this group image.
* True indicates to generate a token for current user, and integer value indicates to generate a token for the
* user whose id is the value indicated.
* If the group picture is included in an e-mail or some other location where the audience is a specific
* user who will not be logged in when viewing, then we use a token to authenticate the user.
* @return string|void Depending on the setting of $return
@ -2545,6 +2550,8 @@ function print_group_picture($group, $courseid, $large = false, $return = false,
* @param int $courseid The course ID for the group.
* @param bool $large A large or small group picture? Default is small.
* @param boolean $includetoken Whether to use a user token when displaying this group image.
* True indicates to generate a token for current user, and integer value indicates to generate a token for the
* user whose id is the value indicated.
* If the group picture is included in an e-mail or some other location where the audience is a specific
* user who will not be logged in when viewing, then we use a token to authenticate the user.
* @return moodle_url Returns the url for the group picture.

View File

@ -1929,7 +1929,7 @@ class api {
*/
public static function send_message_to_conversation(int $userid, int $conversationid, string $message,
int $format) : \stdClass {
global $DB;
global $DB, $PAGE;
if (!self::can_send_message_to_conversation($userid, $conversationid)) {
throw new \moodle_exception("User $userid cannot send a message to conversation $conversationid");
@ -1939,7 +1939,7 @@ class api {
$eventdata->courseid = 1;
$eventdata->component = 'moodle';
$eventdata->name = 'instantmessage';
$eventdata->userfrom = $userid;
$eventdata->userfrom = \core_user::get_user($userid);
$eventdata->convid = $conversationid;
if ($format == FORMAT_HTML) {
@ -1957,6 +1957,37 @@ class api {
$eventdata->timecreated = time();
$eventdata->notification = 0;
// Custom data for event.
$customdata = [
'actionbuttons' => [
'send' => get_string('send', 'message'),
],
'placeholders' => [
'send' => get_string('writeamessage', 'message'),
],
];
$conv = $DB->get_record('message_conversations', ['id' => $conversationid]);
if ($conv->type == self::MESSAGE_CONVERSATION_TYPE_GROUP) {
$convextrafields = self::get_linked_conversation_extra_fields([$conv]);
// Conversation image.
$imageurl = isset($convextrafields[$conv->id]) ? $convextrafields[$conv->id]['imageurl'] : null;
if ($imageurl) {
$customdata['notificationiconurl'] = $imageurl;
}
// Conversation name.
if (is_null($conv->contextid)) {
$convcontext = \context_user::instance($userid);
} else {
$convcontext = \context::instance_by_id($conv->contextid);
}
$customdata['conversationname'] = format_string($conv->name, true, ['context' => $convcontext]);
} else if ($conv->type == self::MESSAGE_CONVERSATION_TYPE_INDIVIDUAL) {
$userpicture = new \user_picture($eventdata->userfrom);
$customdata['notificationiconurl'] = $userpicture->get_url($PAGE)->out(false);
}
$eventdata->customdata = $customdata;
$messageid = message_send($eventdata);
$messagerecord = $DB->get_record('messages', ['id' => $messageid], 'id, useridfrom, fullmessage,
@ -2578,7 +2609,7 @@ class api {
* @return \stdClass the request
*/
public static function create_contact_request(int $userid, int $requesteduserid) : \stdClass {
global $DB;
global $DB, $PAGE;
$request = new \stdClass();
$request->userid = $userid;
@ -2609,6 +2640,15 @@ class api {
$message->fullmessagehtml = $fullmessage;
$message->smallmessage = '';
$message->contexturl = $url->out(false);
$userpicture = new \user_picture($userfrom);
$userpicture->includetoken = $userto->id; // Generate an out-of-session token for the user receiving the message.
$message->customdata = [
'notificationiconurl' => $userpicture->get_url($PAGE)->out(false),
'actionbuttons' => [
'accept' => get_string_manager()->get_string('accept', 'moodle', null, $userto->lang),
'reject' => get_string_manager()->get_string('reject', 'moodle', null, $userto->lang),
],
];
message_send($message);

View File

@ -73,7 +73,8 @@ class provider implements
'fullmessageformat' => 'privacy:metadata:messages:fullmessageformat',
'fullmessagehtml' => 'privacy:metadata:messages:fullmessagehtml',
'smallmessage' => 'privacy:metadata:messages:smallmessage',
'timecreated' => 'privacy:metadata:messages:timecreated'
'timecreated' => 'privacy:metadata:messages:timecreated',
'customdata' => 'privacy:metadata:messages:customdata',
],
'privacy:metadata:messages'
);
@ -155,6 +156,7 @@ class provider implements
'contexturlname' => 'privacy:metadata:notifications:contexturlname',
'timeread' => 'privacy:metadata:notifications:timeread',
'timecreated' => 'privacy:metadata:notifications:timecreated',
'customdata' => 'privacy:metadata:notifications:customdata',
],
'privacy:metadata:notifications'
);
@ -930,7 +932,8 @@ class provider implements
'issender' => transform::yesno($issender),
'message' => message_format_message_text($message),
'timecreated' => transform::datetime($message->timecreated),
'timeread' => $timeread
'timeread' => $timeread,
'customdata' => $message->customdata,
];
if ($conversation->type == \core_message\api::MESSAGE_CONVERSATION_TYPE_GROUP && !$issender) {
// Only export sender for group conversations when is not the current user.
@ -1042,7 +1045,8 @@ class provider implements
'contexturl' => $notification->contexturl,
'contexturlname' => $notification->contexturlname,
'timeread' => $timeread,
'timecreated' => transform::datetime($notification->timecreated)
'timecreated' => transform::datetime($notification->timecreated),
'customdata' => $notification->customdata,
];
$notificationdata[] = $data;

View File

@ -3208,7 +3208,12 @@ class core_message_external extends external_api {
'timecreated' => new external_value(PARAM_INT, 'Time created'),
'timeread' => new external_value(PARAM_INT, 'Time read'),
'usertofullname' => new external_value(PARAM_TEXT, 'User to full name'),
'userfromfullname' => new external_value(PARAM_TEXT, 'User from full name')
'userfromfullname' => new external_value(PARAM_TEXT, 'User from full name'),
'component' => new external_value(PARAM_TEXT, 'The component that generated the notification',
VALUE_OPTIONAL),
'eventtype' => new external_value(PARAM_TEXT, 'The type of notification', VALUE_OPTIONAL),
'customdata' => new external_value(PARAM_RAW, 'Custom data to be passed to the message processor.
The data here is serialised using json_encode().', VALUE_OPTIONAL),
), 'message'
)
),

View File

@ -325,7 +325,7 @@ function message_format_contexturl($message) {
* @return int|false the ID of the new message or false
*/
function message_post_message($userfrom, $userto, $message, $format) {
global $SITE, $CFG, $USER;
global $PAGE;
$eventdata = new \core\message\message();
$eventdata->courseid = 1;
@ -351,6 +351,18 @@ function message_post_message($userfrom, $userto, $message, $format) {
$eventdata->smallmessage = $message;//store the message unfiltered. Clean up on output.
$eventdata->timecreated = time();
$eventdata->notification = 0;
// User image.
$userpicture = new user_picture($userfrom);
$userpicture->includetoken = $userto->id; // Generate an out-of-session token for the user receiving the message.
$eventdata->customdata = [
'notificationiconurl' => $userpicture->get_url($PAGE)->out(false),
'actionbuttons' => [
'send' => get_string_manager()->get_string('send', 'message', null, $eventdata->userto->lang),
],
'placeholders' => [
'send' => get_string_manager()->get_string('writeamessage', 'message', null, $eventdata->userto->lang),
],
];
return message_send($eventdata);
}

View File

@ -129,7 +129,7 @@ class message_airnotifier_external_testcase extends externallib_advanced_testcas
$expected = array(
array(
'userid' => $user1->id,
'configured' => 0
'configured' => 1
)
);
$this->assertEquals($expected, $preferences['users']);

View File

@ -72,7 +72,7 @@ class api {
n.subject, n.fullmessage, n.fullmessageformat,
n.fullmessagehtml, n.smallmessage, n.contexturl,
n.contexturlname, n.timecreated, n.component,
n.eventtype, n.timeread
n.eventtype, n.timeread, n.customdata
FROM {notifications} n
WHERE n.id IN (SELECT notificationid FROM {message_popup_notifications})
AND n.useridto = ?

View File

@ -165,6 +165,8 @@ class message_popup_external extends external_api {
'component' => new external_value(PARAM_TEXT, 'The component that generated the notification',
VALUE_OPTIONAL),
'eventtype' => new external_value(PARAM_TEXT, 'The type of notification', VALUE_OPTIONAL),
'customdata' => new external_value(PARAM_RAW, 'Custom data to be passed to the message processor.
The data here is serialised using json_encode().', VALUE_OPTIONAL),
), 'message'
)
),

View File

@ -49,6 +49,7 @@ trait message_popup_test_helper {
$record->fullmessage = $message;
$record->smallmessage = $message;
$record->timecreated = $timecreated ? $timecreated : time();
$record->customdata = json_encode(['datakey' => 'data']);
$id = $DB->insert_record('notifications', $record);

View File

@ -96,6 +96,15 @@ class message_popup_externallib_testcase extends advanced_testcase {
$this->setAdminUser();
$result = message_popup_external::get_popup_notifications($recipient->id, false, 0, 0);
$this->assertCount(4, $result['notifications']);
// Check we receive custom data as a unserialisable json.
$found = 0;
foreach ($result['notifications'] as $notification) {
if (!empty($notification->customdata)) {
$this->assertObjectHasAttribute('datakey', json_decode($notification->customdata));
$found++;
}
}
$this->assertEquals(2, $found);
$this->setUser($recipient);
$result = message_popup_external::get_popup_notifications($recipient->id, false, 0, 0);

View File

@ -4955,7 +4955,15 @@ class core_message_api_testcase extends core_message_messagelib_testcase {
$user1 = self::getDataGenerator()->create_user();
$user2 = self::getDataGenerator()->create_user();
$sink = $this->redirectMessages();
$request = \core_message\api::create_contact_request($user1->id, $user2->id);
$messages = $sink->get_messages();
$sink->close();
// Test customdata.
$customdata = json_decode($messages[0]->customdata);
$this->assertObjectHasAttribute('notificationiconurl', $customdata);
$this->assertObjectHasAttribute('actionbuttons', $customdata);
$this->assertCount(2, (array) $customdata->actionbuttons);
$this->assertEquals($user1->id, $request->userid);
$this->assertEquals($user2->id, $request->requesteduserid);
@ -6007,8 +6015,17 @@ class core_message_api_testcase extends core_message_messagelib_testcase {
// Send a message to an individual conversation.
$sink = $this->redirectEvents();
$messagessink = $this->redirectMessages();
$message1 = \core_message\api::send_message_to_conversation($user1->id, $ic1->id, 'this is a message', FORMAT_MOODLE);
$events = $sink->get_events();
$messages = $messagessink->get_messages();
// Test customdata.
$customdata = json_decode($messages[0]->customdata);
$this->assertObjectHasAttribute('notificationiconurl', $customdata);
$this->assertObjectHasAttribute('actionbuttons', $customdata);
$this->assertCount(1, (array) $customdata->actionbuttons);
$this->assertObjectHasAttribute('placeholders', $customdata);
$this->assertCount(1, (array) $customdata->placeholders);
// Verify the message returned.
$this->assertInstanceOf(\stdClass::class, $message1);
@ -6048,15 +6065,23 @@ class core_message_api_testcase extends core_message_messagelib_testcase {
// Send a message to a group conversation.
$sink = $this->redirectEvents();
$messagessink = $this->redirectMessages();
$message1 = \core_message\api::send_message_to_conversation($user1->id, $gc2->id, 'message to the group', FORMAT_MOODLE);
$events = $sink->get_events();
$messages = $messagessink->get_messages();
// Verify the message returned.
$this->assertInstanceOf(\stdClass::class, $message1);
$this->assertObjectHasAttribute('id', $message1);
$this->assertAttributeEquals($user1->id, 'useridfrom', $message1);
$this->assertAttributeEquals('message to the group', 'text', $message1);
$this->assertObjectHasAttribute('timecreated', $message1);
// Test customdata.
$customdata = json_decode($messages[0]->customdata);
$this->assertObjectHasAttribute('actionbuttons', $customdata);
$this->assertCount(1, (array) $customdata->actionbuttons);
$this->assertObjectHasAttribute('placeholders', $customdata);
$this->assertCount(1, (array) $customdata->placeholders);
$this->assertObjectNotHasAttribute('notificationiconurl', $customdata); // No group image means no image.
// Verify events. Note: the event is a message read event because of an if (PHPUNIT) conditional within message_send(),
// however, we can still determine the number and ids of any recipients this way.
@ -6067,6 +6092,66 @@ class core_message_api_testcase extends core_message_messagelib_testcase {
$this->assertContains($user4->id, $userids);
}
/**
* Test verifying that messages can be sent to existing linked group conversations.
*/
public function test_send_message_to_conversation_linked_group_conversation() {
global $CFG;
// Create some users.
$user1 = self::getDataGenerator()->create_user();
$user2 = self::getDataGenerator()->create_user();
$user3 = self::getDataGenerator()->create_user();
$course = $this->getDataGenerator()->create_course();
// Create a group with a linked conversation and a valid image.
$this->setAdminUser();
$this->getDataGenerator()->enrol_user($user1->id, $course->id);
$this->getDataGenerator()->enrol_user($user2->id, $course->id);
$this->getDataGenerator()->enrol_user($user3->id, $course->id);
$group = $this->getDataGenerator()->create_group([
'courseid' => $course->id,
'enablemessaging' => 1,
'picturepath' => $CFG->dirroot . '/lib/tests/fixtures/gd-logo.png'
]);
// Add users to group.
$this->getDataGenerator()->create_group_member(array('groupid' => $group->id, 'userid' => $user1->id));
$this->getDataGenerator()->create_group_member(array('groupid' => $group->id, 'userid' => $user2->id));
// Verify the group with the image works as expected.
$conversations = \core_message\api::get_conversations($user1->id);
$this->assertEquals(2, $conversations[0]->membercount);
$this->assertEquals($course->shortname, $conversations[0]->subname);
$groupimageurl = get_group_picture_url($group, $group->courseid, true);
$this->assertEquals($groupimageurl, $conversations[0]->imageurl);
// Redirect messages.
// This marks messages as read, but we can still observe and verify the number of conversation recipients,
// based on the message_viewed events generated as part of marking the message as read for each user.
$this->preventResetByRollback();
$sink = $this->redirectMessages();
// Send a message to a group conversation.
$messagessink = $this->redirectMessages();
$message1 = \core_message\api::send_message_to_conversation($user1->id, $conversations[0]->id,
'message to the group', FORMAT_MOODLE);
$messages = $messagessink->get_messages();
// Verify the message returned.
$this->assertInstanceOf(\stdClass::class, $message1);
$this->assertObjectHasAttribute('id', $message1);
$this->assertAttributeEquals($user1->id, 'useridfrom', $message1);
$this->assertAttributeEquals('message to the group', 'text', $message1);
$this->assertObjectHasAttribute('timecreated', $message1);
// Test customdata.
$customdata = json_decode($messages[0]->customdata);
$this->assertObjectHasAttribute('notificationiconurl', $customdata);
$this->assertEquals($groupimageurl, $customdata->notificationiconurl);
$this->assertEquals($group->name, $customdata->conversationname);
}
/**
* Test verifying that messages cannot be sent to conversations that don't exist.
*/

View File

@ -1612,6 +1612,7 @@ class core_message_externallib_testcase extends externallib_advanced_testcase {
$eventdata->fullmessageformat = FORMAT_PLAIN;
$eventdata->fullmessagehtml = '<strong>Feedback submitted</strong>';
$eventdata->smallmessage = '';
$eventdata->customdata = ['datakey' => 'data'];
message_send($eventdata);
$this->setUser($user1);
@ -1644,6 +1645,10 @@ class core_message_externallib_testcase extends externallib_advanced_testcase {
$messages = core_message_external::get_messages(0, $user1->id, 'notifications', true, true, 0, 0);
$messages = external_api::clean_returnvalue(core_message_external::get_messages_returns(), $messages);
$this->assertCount(1, $messages['messages']);
// Check we receive custom data as a unserialisable json.
$this->assertObjectHasAttribute('datakey', json_decode($messages['messages'][0]['customdata']));
$this->assertEquals('mod_feedback', $messages['messages'][0]['component']);
$this->assertEquals('submission', $messages['messages'][0]['eventtype']);
// Test warnings.
$CFG->messaging = 0;

View File

@ -89,6 +89,7 @@ class core_message_privacy_provider_testcase extends \core_privacy\tests\provide
$this->assertArrayHasKey('fullmessagehtml', $privacyfields);
$this->assertArrayHasKey('smallmessage', $privacyfields);
$this->assertArrayHasKey('timecreated', $privacyfields);
$this->assertArrayHasKey('customdata', $privacyfields);
$this->assertEquals('privacy:metadata:messages', $messagestable->get_summary());
$privacyfields = $messageuseractionstable->get_privacy_fields();
@ -136,6 +137,7 @@ class core_message_privacy_provider_testcase extends \core_privacy\tests\provide
$this->assertArrayHasKey('contexturlname', $privacyfields);
$this->assertArrayHasKey('timeread', $privacyfields);
$this->assertArrayHasKey('timecreated', $privacyfields);
$this->assertArrayHasKey('customdata', $privacyfields);
$this->assertEquals('privacy:metadata:notifications', $notificationstable->get_summary());
}
@ -2724,6 +2726,7 @@ class core_message_privacy_provider_testcase extends \core_privacy\tests\provide
$record->fullmessage = 'A rad message ' . $i;
$record->smallmessage = 'A rad message ' . $i;
$record->timecreated = $timecreated;
$record->customdata = json_encode(['akey' => 'avalue']);
$i++;
@ -2763,6 +2766,7 @@ class core_message_privacy_provider_testcase extends \core_privacy\tests\provide
$record->smallmessage = 'Yo homie, you got some stuff to do, yolo. ' . $i;
$record->timeread = $timeread;
$record->timecreated = $timecreated;
$record->customdata = json_encode(['akey' => 'avalue']);
$i++;

View File

@ -12,6 +12,13 @@ information provided here is intended especially for developers.
* A new parameter 'mergeself' has been added to the methods \core_message\api::get_conversations() and
core_message_external::get_conversations(), to decide whether the self-conversations should be included or not when the
private ones are requested, to display them together.
* A new 'customdata' field for both messages and notifications has been added. This new field can store any custom data
serialised using json_encode().
This new field can be used for storing any data not fitting in the current message structure. For example, it will be used
to store additional information for the "Mobile notifications" processor.
Existing external functions: core_message_get_messages and message_popup_get_popup_notifications has been udated to return the
new field.
* External function core_message_get_messages now returns the component and eventtype.
=== 3.6 ===

View File

@ -6194,7 +6194,7 @@ class assign {
$assignmentname,
$blindmarking,
$uniqueidforuser) {
global $CFG;
global $CFG, $PAGE;
$info = new stdClass();
if ($blindmarking) {
@ -6244,6 +6244,20 @@ class assign {
$eventdata->notification = 1;
$eventdata->contexturl = $info->url;
$eventdata->contexturlname = $info->assignment;
$customdata = [
'cmid' => $coursemodule->id,
'instance' => $coursemodule->instance,
'messagetype' => $messagetype,
'blindmarking' => $blindmarking,
'uniqueidforuser' => $uniqueidforuser,
];
// Check if the userfrom is real and visible.
if (!empty($userfrom->id) && core_user::is_real_user($userfrom->id)) {
$userpicture = new user_picture($userfrom);
$userpicture->includetoken = $userto->id; // Generate an out-of-session token for the user receiving the message.
$customdata['notificationiconurl'] = $userpicture->get_url($PAGE)->out(false);
}
$eventdata->customdata = $customdata;
message_send($eventdata);
}

View File

@ -1490,6 +1490,7 @@ class mod_assign_locallib_testcase extends advanced_testcase {
}
public function test_cron() {
global $PAGE;
$this->resetAfterTest();
// First run cron so there are no messages waiting to be sent (from other tests).
@ -1519,6 +1520,15 @@ class mod_assign_locallib_testcase extends advanced_testcase {
$this->assertEquals(1, count($messages));
$this->assertEquals(1, $messages[0]->notification);
$this->assertEquals($assign->get_instance()->name, $messages[0]->contexturlname);
// Test customdata.
$customdata = json_decode($messages[0]->customdata);
$this->assertEquals($assign->get_course_module()->id, $customdata->cmid);
$this->assertEquals($assign->get_instance()->id, $customdata->instance);
$this->assertEquals('feedbackavailable', $customdata->messagetype);
$userpicture = new user_picture($teacher);
$this->assertEquals($userpicture->get_url($PAGE)->out(false), $customdata->notificationiconurl);
$this->assertEquals(0, $customdata->uniqueidforuser); // Not used in this case.
$this->assertFalse($customdata->blindmarking);
}
public function test_cron_without_notifications() {

View File

@ -2550,7 +2550,7 @@ function feedback_print_numeric_option_list() {
* @return void
*/
function feedback_send_email($cm, $feedback, $course, $user, $completed = null) {
global $CFG, $DB;
global $CFG, $DB, $PAGE;
if ($feedback->email_notification == 0) { // No need to do anything
return;
@ -2617,6 +2617,10 @@ function feedback_send_email($cm, $feedback, $course, $user, $completed = null)
$posthtml = '';
}
$customdata = [
'cmid' => $cm->id,
'instance' => $feedback->id,
];
if ($feedback->anonymous == FEEDBACK_ANONYMOUS_NO) {
$eventdata = new \core\message\message();
$eventdata->courseid = $course->id;
@ -2632,6 +2636,11 @@ function feedback_send_email($cm, $feedback, $course, $user, $completed = null)
$eventdata->courseid = $course->id;
$eventdata->contexturl = $info->url;
$eventdata->contexturlname = $info->feedback;
// User image.
$userpicture = new user_picture($user);
$userpicture->includetoken = $teacher->id; // Generate an out-of-session token for the user receiving the message.
$customdata['notificationiconurl'] = $userpicture->get_url($PAGE)->out(false);
$eventdata->customdata = $customdata;
message_send($eventdata);
} else {
$eventdata = new \core\message\message();
@ -2648,6 +2657,9 @@ function feedback_send_email($cm, $feedback, $course, $user, $completed = null)
$eventdata->courseid = $course->id;
$eventdata->contexturl = $info->url;
$eventdata->contexturlname = $info->feedback;
// Feedback icon if can be easily reachable.
$customdata['notificationiconurl'] = ($cm instanceof cm_info) ? $cm->get_icon_url()->out() : '';
$eventdata->customdata = $customdata;
message_send($eventdata);
}
}
@ -2710,6 +2722,12 @@ function feedback_send_email_anonym($cm, $feedback, $course) {
$eventdata->courseid = $course->id;
$eventdata->contexturl = $info->url;
$eventdata->contexturlname = $info->feedback;
$eventdata->customdata = [
'cmid' => $cm->id,
'instance' => $feedback->id,
'notificationiconurl' => ($cm instanceof cm_info) ? $cm->get_icon_url()->out() : '', // Performance wise.
];
message_send($eventdata);
}
}

View File

@ -54,7 +54,8 @@ class mod_feedback_external_testcase extends externallib_advanced_testcase {
// Setup test data.
$this->course = $this->getDataGenerator()->create_course();
$this->feedback = $this->getDataGenerator()->create_module('feedback', array('course' => $this->course->id));
$this->feedback = $this->getDataGenerator()->create_module('feedback',
array('course' => $this->course->id, 'email_notification' => 1));
$this->context = context_module::instance($this->feedback->cmid);
$this->cm = get_coursemodule_from_instance('feedback', $this->feedback->id);
@ -518,6 +519,7 @@ class mod_feedback_external_testcase extends externallib_advanced_testcase {
$this->assertCount(7, $tmpitems); // 2 from the first page + 5 from the second page.
// And finally, save everything! We are going to modify one previous recorded value.
$messagessink = $this->redirectMessages();
$data[2]['value'] = 2; // 2 is value of the option 'b'.
$secondpagedata = [$data[2], $data[3], $data[4], $data[5], $data[6]];
$result = mod_feedback_external::process_page($this->feedback->id, 1, $secondpagedata);
@ -540,6 +542,15 @@ class mod_feedback_external_testcase extends externallib_advanced_testcase {
}
$completed = $DB->get_record('feedback_completed', []);
$this->assertEquals(0, $completed->courseid);
// Test notifications sent.
$messages = $messagessink->get_messages();
$messagessink->close();
// Test customdata.
$customdata = json_decode($messages[0]->customdata);
$this->assertEquals($this->feedback->id, $customdata->instance);
$this->assertEquals($this->feedback->cmid, $customdata->cmid);
$this->assertObjectHasAttribute('notificationiconurl', $customdata);
}
/**

View File

@ -273,7 +273,7 @@ class send_user_notifications extends \core\task\adhoc_task {
* @param \context $context
*/
protected function send_post($course, $forum, $discussion, $post, $cm, $context) {
global $CFG;
global $CFG, $PAGE;
$author = $this->get_post_author($post->userid, $course, $forum, $cm, $context);
if (empty($author)) {
@ -351,6 +351,19 @@ class send_user_notifications extends \core\task\adhoc_task {
$contexturl = new \moodle_url('/mod/forum/discuss.php', ['d' => $discussion->id], "p{$post->id}");
$eventdata->contexturl = $contexturl->out();
$eventdata->contexturlname = $discussion->name;
// User image.
$userpicture = new \user_picture($author);
$userpicture->includetoken = $this->recipient->id; // Generate an out-of-session token for the user receiving the message.
$eventdata->customdata = [
'cmid' => $cm->id,
'instance' => $forum->id,
'discussionid' => $discussion->id,
'postid' => $post->id,
'notificationiconurl' => $userpicture->get_url($PAGE)->out(false),
'actionbuttons' => [
'reply' => get_string_manager()->get_string('reply', 'forum', null, $eventdata->userto->lang),
],
];
return message_send($eventdata);
}

View File

@ -26,6 +26,9 @@
$messageproviders = array (
// Ordinary single forum posts.
'posts' => array(
'defaults' => array(
'airnotifier' => MESSAGE_PERMITTED + MESSAGE_DEFAULT_LOGGEDIN + MESSAGE_DEFAULT_LOGGEDOFF,
),
),
// Forum digest messages.

View File

@ -1496,4 +1496,47 @@ class mod_forum_mail_testcase extends advanced_testcase {
$this->send_notifications_and_assert($recipient, [$post]);
$this->send_notifications_and_assert($editor, [$post]);
}
/**
* Test notification comes with customdata.
*/
public function test_notification_customdata() {
$this->resetAfterTest(true);
$course = $this->getDataGenerator()->create_course();
$options = array('course' => $course->id, 'forcesubscribe' => FORUM_FORCESUBSCRIBE);
$forum = $this->getDataGenerator()->create_module('forum', $options);
list($author) = $this->helper_create_users($course, 1);
list($commenter) = $this->helper_create_users($course, 1);
$strre = get_string('re', 'forum');
// New posts should not have Re: in the subject.
list($discussion, $post) = $this->helper_post_to_forum($forum, $author);
$expect = [
'author' => (object) [
'userid' => $author->id,
'messages' => 1,
],
'commenter' => (object) [
'userid' => $commenter->id,
'messages' => 1,
],
];
$this->queue_tasks_and_assert($expect);
$this->send_notifications_and_assert($author, [$post]);
$this->send_notifications_and_assert($commenter, [$post]);
$messages = $this->messagesink->get_messages();
$customdata = json_decode($messages[0]->customdata);
$this->assertEquals($forum->id, $customdata->instance);
$this->assertEquals($forum->cmid, $customdata->cmid);
$this->assertEquals($post->id, $customdata->postid);
$this->assertEquals($discussion->id, $customdata->discussionid);
$this->assertObjectHasAttribute('notificationiconurl', $customdata);
$this->assertObjectHasAttribute('actionbuttons', $customdata);
$this->assertCount(1, (array) $customdata->actionbuttons);
}
}

View File

@ -27,6 +27,9 @@ $messageproviders = array (
// essay graded notification
'graded_essay' => array (
'defaults' => array(
'airnotifier' => MESSAGE_PERMITTED + MESSAGE_DEFAULT_LOGGEDIN + MESSAGE_DEFAULT_LOGGEDOFF,
),
)
);

View File

@ -224,6 +224,7 @@ switch ($mode) {
print_error('cannotfindanswer', 'lesson');
}
$userpicture = new user_picture($USER);
foreach ($attempts as $attempt) {
$essayinfo = lesson_page_type_essay::extract_useranswer($attempt->useranswer);
if ($essayinfo->graded && !$essayinfo->sent) {
@ -261,6 +262,9 @@ switch ($mode) {
$message = get_string('essayemailmessage2', 'lesson', $a);
$plaintext = format_text_email($message, FORMAT_HTML);
$smallmessage = get_string('essayemailmessagesmall', 'lesson', $a);
$smallmessage = format_text_email($smallmessage, FORMAT_HTML);
// Subject
$subject = get_string('essayemailsubject', 'lesson');
@ -276,8 +280,15 @@ switch ($mode) {
$eventdata->fullmessage = $plaintext;
$eventdata->fullmessageformat = FORMAT_PLAIN;
$eventdata->fullmessagehtml = $message;
$eventdata->smallmessage = '';
$eventdata->contexturl = $contexturl;
$eventdata->smallmessage = $smallmessage;
$eventdata->contexturl = $contexturl->out(false);
$userpicture->includetoken = $attempt->userid; // Generate an out-of-session token for the destinatary.
$eventdata->customdata = [
'cmid' => $cm->id,
'instance' => $lesson->id,
'retake' => $lesson->id,
'notificationiconurl' => $userpicture->get_url($PAGE)->out(false),
];
// Required for messaging framework
$eventdata->component = 'mod_lesson';

View File

@ -204,6 +204,7 @@ $string['eolstudentoutoftime'] = 'Attention: You ran out of time for this lesso
$string['eolstudentoutoftimenoanswers'] = 'You did not answer any questions. You have received a 0 for this lesson.';
$string['essay'] = 'Essay';
$string['essayemailmessage2'] = '<p>Essay prompt: {$a->question}</p><p>Your response: <em>{$a->response}</em></p><p>Grader\'s comments: <em>{$a->comment}</em></p><p>You have received {$a->earned} out of {$a->outof} for this essay question.</p><p>Your grade for the {$a->lesson} lesson has been changed to {$a->newgrade}&#37;.</p>';
$string['essayemailmessagesmall'] = '<p>You have received {$a->earned} out of {$a->outof} for this essay question.</p><p>Your grade for the {$a->lesson} lesson has been changed to {$a->newgrade}&#37;.</p>';
$string['essayemailsubject'] = 'Grade available for lesson question';
$string['essayresponses'] = 'Essay responses';
$string['essays'] = 'Essays';

View File

@ -32,12 +32,18 @@ $messageproviders = array(
// Confirm a student's quiz attempt.
'confirmation' => array(
'capability' => 'mod/quiz:emailconfirmsubmission'
'capability' => 'mod/quiz:emailconfirmsubmission',
'defaults' => array(
'airnotifier' => MESSAGE_PERMITTED + MESSAGE_DEFAULT_LOGGEDIN + MESSAGE_DEFAULT_LOGGEDOFF,
),
),
// Warning to the student that their quiz attempt is now overdue, if the quiz
// has a grace period.
'attempt_overdue' => array(
'capability' => 'mod/quiz:emailwarnoverdue'
'capability' => 'mod/quiz:emailwarnoverdue',
'defaults' => array(
'airnotifier' => MESSAGE_PERMITTED + MESSAGE_DEFAULT_LOGGEDIN + MESSAGE_DEFAULT_LOGGEDOFF,
),
),
);

View File

@ -1544,6 +1544,11 @@ function quiz_send_confirmation($recipient, $a) {
$eventdata->smallmessage = get_string('emailconfirmsmall', 'quiz', $a);
$eventdata->contexturl = $a->quizurl;
$eventdata->contexturlname = $a->quizname;
$eventdata->customdata = [
'cmid' => $a->quizcmid,
'instance' => $a->quizid,
'attemptid' => $a->attemptid,
];
// ... and send it.
return message_send($eventdata);
@ -1558,6 +1563,7 @@ function quiz_send_confirmation($recipient, $a) {
* @return int|false as for {@link message_send()}.
*/
function quiz_send_notification($recipient, $submitter, $a) {
global $PAGE;
// Recipient info for template.
$a->useridnumber = $recipient->idnumber;
@ -1581,6 +1587,14 @@ function quiz_send_notification($recipient, $submitter, $a) {
$eventdata->smallmessage = get_string('emailnotifysmall', 'quiz', $a);
$eventdata->contexturl = $a->quizreviewurl;
$eventdata->contexturlname = $a->quizname;
$userpicture = new user_picture($submitter);
$userpicture->includetoken = $recipient->id; // Generate an out-of-session token for the user receiving the message.
$eventdata->customdata = [
'cmid' => $a->quizcmid,
'instance' => $a->quizid,
'attemptid' => $a->attemptid,
'notificationiconurl' => $userpicture->get_url($PAGE)->out(false),
];
// ... and send it.
return message_send($eventdata);
@ -1649,12 +1663,15 @@ function quiz_send_notification_messages($course, $quiz, $attempt, $context, $cm
format_string($quiz->name) . ' report</a>';
$a->quizurl = $CFG->wwwroot . '/mod/quiz/view.php?id=' . $cm->id;
$a->quizlink = '<a href="' . $a->quizurl . '">' . format_string($quiz->name) . '</a>';
$a->quizid = $quiz->id;
$a->quizcmid = $cm->id;
// Attempt info.
$a->submissiontime = userdate($attempt->timefinish);
$a->timetaken = format_time($attempt->timefinish - $attempt->timestart);
$a->quizreviewurl = $CFG->wwwroot . '/mod/quiz/review.php?attempt=' . $attempt->id;
$a->quizreviewlink = '<a href="' . $a->quizreviewurl . '">' .
format_string($quiz->name) . ' review</a>';
$a->attemptid = $attempt->id;
// Student who sat the quiz info.
$a->studentidnumber = $submitter->idnumber;
$a->studentname = fullname($submitter);
@ -1748,6 +1765,11 @@ function quiz_send_overdue_message($attemptobj) {
$eventdata->smallmessage = get_string('emailoverduesmall', 'quiz', $a);
$eventdata->contexturl = $a->quizurl;
$eventdata->contexturlname = $a->quizname;
$eventdata->customdata = [
'cmid' => $attemptobj->get_cmid(),
'instance' => $attemptobj->get_quizid(),
'attemptid' => $attemptobj->get_attemptid(),
];
// Send the message.
return message_send($eventdata);

View File

@ -95,6 +95,10 @@ class mod_quiz_external_testcase extends externallib_advanced_testcase {
// Users enrolments.
$this->studentrole = $DB->get_record('role', array('shortname' => 'student'));
$this->teacherrole = $DB->get_record('role', array('shortname' => 'editingteacher'));
// Allow student to receive messages.
$coursecontext = context_course::instance($this->course->id);
assign_capability('mod/quiz:emailnotifysubmission', CAP_ALLOW, $this->teacherrole->id, $coursecontext, true);
$this->getDataGenerator()->enrol_user($this->student->id, $this->course->id, $this->studentrole->id, 'manual');
$this->getDataGenerator()->enrol_user($this->teacher->id, $this->course->id, $this->teacherrole->id, 'manual');
}
@ -1184,9 +1188,21 @@ class mod_quiz_external_testcase extends externallib_advanced_testcase {
$this->assertTrue($result['questions'][1]['flagged']);
// Finish the attempt.
$sink = $this->redirectMessages();
$result = mod_quiz_external::process_attempt($attempt->id, array(), true);
$result = external_api::clean_returnvalue(mod_quiz_external::process_attempt_returns(), $result);
$this->assertEquals(quiz_attempt::FINISHED, $result['state']);
$messages = $sink->get_messages();
$message = reset($messages);
$sink->close();
// Test customdata.
if (!empty($message->customdata)) {
$customdata = json_decode($message->customdata);
$this->assertEquals($quizobj->get_quizid(), $customdata->instance);
$this->assertEquals($quizobj->get_cmid(), $customdata->cmid);
$this->assertEquals($attempt->id, $customdata->attemptid);
$this->assertObjectHasAttribute('notificationiconurl', $customdata);
}
// Start new attempt.
$quba = question_engine::make_questions_usage_by_activity('mod_quiz', $quizobj->get_context());

View File

@ -29,7 +29,7 @@
defined('MOODLE_INTERNAL') || die();
$version = 2019042300.01; // YYYYMMDD = weekly release date of this DEV branch.
$version = 2019042300.03; // YYYYMMDD = weekly release date of this DEV branch.
// RR = release increments - 00 in DEV branches.
// .XX = incremental changes.