Merge branch 'MDL-57370-master' of git://github.com/ryanwyllie/moodle

This commit is contained in:
Dan Poltawski
2017-02-07 10:35:13 +00:00
12 changed files with 872 additions and 174 deletions
+5 -3
View File
@@ -1,5 +1,5 @@
<?xml version="1.0" encoding="UTF-8" ?>
<XMLDB PATH="lib/db" VERSION="20161119" COMMENT="XMLDB file for core Moodle tables"
<XMLDB PATH="lib/db" VERSION="20170207" COMMENT="XMLDB file for core Moodle tables"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="../../lib/xmldb/xmldb.xsd"
>
@@ -545,8 +545,9 @@
<KEY NAME="primary" TYPE="primary" FIELDS="id"/>
</KEYS>
<INDEXES>
<INDEX NAME="useridto" UNIQUE="false" FIELDS="useridto"/>
<INDEX NAME="useridfromtodeleted" UNIQUE="false" FIELDS="useridfrom, useridto, timeuserfromdeleted, timeusertodeleted"/>
<INDEX NAME="useridfrom_timeuserfromdeleted_notification" UNIQUE="false" FIELDS="useridfrom, timeuserfromdeleted, notification"/>
<INDEX NAME="useridto_timeusertodeleted_notification" UNIQUE="false" FIELDS="useridto, timeusertodeleted, notification"/>
</INDEXES>
</TABLE>
<TABLE NAME="message_read" COMMENT="Stores all messages that have been read">
@@ -573,9 +574,10 @@
<KEY NAME="primary" TYPE="primary" FIELDS="id"/>
</KEYS>
<INDEXES>
<INDEX NAME="useridto" UNIQUE="false" FIELDS="useridto"/>
<INDEX NAME="useridfromtodeleted" UNIQUE="false" FIELDS="useridfrom, useridto, timeuserfromdeleted, timeusertodeleted"/>
<INDEX NAME="notificationtimeread" UNIQUE="false" FIELDS="notification, timeread"/>
<INDEX NAME="useridfrom_timeuserfromdeleted_notification" UNIQUE="false" FIELDS="useridfrom, timeuserfromdeleted, notification"/>
<INDEX NAME="useridto_timeusertodeleted_notification" UNIQUE="false" FIELDS="useridto, timeusertodeleted, notification"/>
</INDEXES>
</TABLE>
<TABLE NAME="message_contacts" COMMENT="Maintains lists of relationships between users">
+60
View File
@@ -2465,5 +2465,65 @@ function xmldb_main_upgrade($oldversion) {
upgrade_main_savepoint(true, 2016122800.00);
}
if ($oldversion < 2017020200.01) {
// Define index useridfrom_timeuserfromdeleted_notification (not unique) to be added to message.
$table = new xmldb_table('message');
$index = new xmldb_index('useridfrom_timeuserfromdeleted_notification', XMLDB_INDEX_NOTUNIQUE, array('useridfrom', 'timeuserfromdeleted', 'notification'));
// Conditionally launch add index useridfrom_timeuserfromdeleted_notification.
if (!$dbman->index_exists($table, $index)) {
$dbman->add_index($table, $index);
}
// Define index useridto_timeusertodeleted_notification (not unique) to be added to message.
$index = new xmldb_index('useridto_timeusertodeleted_notification', XMLDB_INDEX_NOTUNIQUE, array('useridto', 'timeusertodeleted', 'notification'));
// Conditionally launch add index useridto_timeusertodeleted_notification.
if (!$dbman->index_exists($table, $index)) {
$dbman->add_index($table, $index);
}
$index = new xmldb_index('useridto', XMLDB_INDEX_NOTUNIQUE, array('useridto'));
// Conditionally launch drop index useridto.
if ($dbman->index_exists($table, $index)) {
$dbman->drop_index($table, $index);
}
// Main savepoint reached.
upgrade_main_savepoint(true, 2017020200.01);
}
if ($oldversion < 2017020200.02) {
// Define index useridfrom_timeuserfromdeleted_notification (not unique) to be added to message_read.
$table = new xmldb_table('message_read');
$index = new xmldb_index('useridfrom_timeuserfromdeleted_notification', XMLDB_INDEX_NOTUNIQUE, array('useridfrom', 'timeuserfromdeleted', 'notification'));
// Conditionally launch add index useridfrom_timeuserfromdeleted_notification.
if (!$dbman->index_exists($table, $index)) {
$dbman->add_index($table, $index);
}
// Define index useridto_timeusertodeleted_notification (not unique) to be added to message_read.
$index = new xmldb_index('useridto_timeusertodeleted_notification', XMLDB_INDEX_NOTUNIQUE, array('useridto', 'timeusertodeleted', 'notification'));
// Conditionally launch add index useridto_timeusertodeleted_notification.
if (!$dbman->index_exists($table, $index)) {
$dbman->add_index($table, $index);
}
$index = new xmldb_index('useridto', XMLDB_INDEX_NOTUNIQUE, array('useridto'));
// Conditionally launch drop index useridto.
if ($dbman->index_exists($table, $index)) {
$dbman->drop_index($table, $index);
}
// Main savepoint reached.
upgrade_main_savepoint(true, 2017020200.02);
}
return true;
}
+141
View File
@@ -6385,3 +6385,144 @@ function prevent_form_autofill_password() {
debugging('prevent_form_autofill_password has been deprecated and is no longer in use.', DEBUG_DEVELOPER);
return '';
}
/**
* Get the users recent conversations meaning all the people they've recently
* sent or received a message from plus the most recent message sent to or received from each other user
*
* @deprecated since Moodle 3.3 MDL-57370
* @param object|int $userorid the current user or user id
* @param int $limitfrom can be used for paging
* @param int $limitto can be used for paging
* @return array
*/
function message_get_recent_conversations($userorid, $limitfrom = 0, $limitto = 100) {
global $DB;
debugging('message_get_recent_conversations() is deprecated. Please use \core_message\api::get_conversations() instead.', DEBUG_DEVELOPER);
if (is_object($userorid)) {
$user = $userorid;
} else {
$userid = $userorid;
$user = new stdClass();
$user->id = $userid;
}
$userfields = user_picture::fields('otheruser', array('lastaccess'));
// This query retrieves the most recent message received from or sent to
// seach other user.
//
// If two messages have the same timecreated, we take the one with the
// larger id.
//
// There is a separate query for read and unread messages as they are stored
// in different tables. They were originally retrieved in one query but it
// was so large that it was difficult to be confident in its correctness.
$uniquefield = $DB->sql_concat('message.useridfrom', "'-'", 'message.useridto');
$sql = "SELECT $uniquefield, $userfields,
message.id as mid, message.notification, message.useridfrom, message.useridto,
message.smallmessage, message.fullmessage, message.fullmessagehtml,
message.fullmessageformat, message.timecreated,
contact.id as contactlistid, contact.blocked
FROM {message_read} message
JOIN (
SELECT MAX(id) AS messageid,
matchedmessage.useridto,
matchedmessage.useridfrom
FROM {message_read} matchedmessage
INNER JOIN (
SELECT MAX(recentmessages.timecreated) timecreated,
recentmessages.useridfrom,
recentmessages.useridto
FROM {message_read} recentmessages
WHERE (
(recentmessages.useridfrom = :userid1 AND recentmessages.timeuserfromdeleted = 0) OR
(recentmessages.useridto = :userid2 AND recentmessages.timeusertodeleted = 0)
)
GROUP BY recentmessages.useridfrom, recentmessages.useridto
) recent ON matchedmessage.useridto = recent.useridto
AND matchedmessage.useridfrom = recent.useridfrom
AND matchedmessage.timecreated = recent.timecreated
WHERE (
(matchedmessage.useridfrom = :userid6 AND matchedmessage.timeuserfromdeleted = 0) OR
(matchedmessage.useridto = :userid7 AND matchedmessage.timeusertodeleted = 0)
)
GROUP BY matchedmessage.useridto, matchedmessage.useridfrom
) messagesubset ON messagesubset.messageid = message.id
JOIN {user} otheruser ON (message.useridfrom = :userid4 AND message.useridto = otheruser.id)
OR (message.useridto = :userid5 AND message.useridfrom = otheruser.id)
LEFT JOIN {message_contacts} contact ON contact.userid = :userid3 AND contact.contactid = otheruser.id
WHERE otheruser.deleted = 0 AND message.notification = 0
ORDER BY message.timecreated DESC";
$params = array(
'userid1' => $user->id,
'userid2' => $user->id,
'userid3' => $user->id,
'userid4' => $user->id,
'userid5' => $user->id,
'userid6' => $user->id,
'userid7' => $user->id
);
$read = $DB->get_records_sql($sql, $params, $limitfrom, $limitto);
// We want to get the messages that have not been read. These are stored in the 'message' table. It is the
// exact same query as the one above, except for the table we are querying. So, simply replace references to
// the 'message_read' table with the 'message' table.
$sql = str_replace('{message_read}', '{message}', $sql);
$unread = $DB->get_records_sql($sql, $params, $limitfrom, $limitto);
$unreadcountssql = 'SELECT useridfrom, count(*) as count
FROM {message}
WHERE useridto = :userid
AND timeusertodeleted = 0
AND notification = 0
GROUP BY useridfrom';
$unreadcounts = $DB->get_records_sql($unreadcountssql, array('userid' => $user->id));
// Union the 2 result sets together looking for the message with the most
// recent timecreated for each other user.
// $conversation->id (the array key) is the other user's ID.
$conversations = array();
$conversation_arrays = array($unread, $read);
foreach ($conversation_arrays as $conversation_array) {
foreach ($conversation_array as $conversation) {
// Only consider it unread if $user has unread messages.
if (isset($unreadcounts[$conversation->useridfrom])) {
$conversation->isread = 0;
$conversation->unreadcount = $unreadcounts[$conversation->useridfrom]->count;
} else {
$conversation->isread = 1;
}
if (!isset($conversations[$conversation->id])) {
$conversations[$conversation->id] = $conversation;
} else {
$current = $conversations[$conversation->id];
// We need to maintain the isread and unreadcount values from existing
// parts of the conversation if we're replacing it.
$conversation->isread = ($conversation->isread && $current->isread);
if (isset($current->unreadcount) && !isset($conversation->unreadcount)) {
$conversation->unreadcount = $current->unreadcount;
}
if ($current->timecreated < $conversation->timecreated) {
$conversations[$conversation->id] = $conversation;
} else if ($current->timecreated == $conversation->timecreated) {
if ($current->mid < $conversation->mid) {
$conversations[$conversation->id] = $conversation;
}
}
}
}
}
// Sort the conversations by $conversation->timecreated, newest to oldest
// There may be multiple conversations with the same timecreated
// The conversations array contains both read and unread messages (different tables) so sorting by ID won't work
$result = core_collator::asort_objects_by_property($conversations, 'timecreated', core_collator::SORT_NUMERIC);
$conversations = array_reverse($conversations);
return $conversations;
}
+237 -5
View File
@@ -239,18 +239,250 @@ class api {
/**
* Returns the contacts and their conversation to display in the contacts area.
*
* ** WARNING **
* It is HIGHLY recommended to use a sensible limit when calling this function. Trying
* to retrieve too much information in a single call will cause performance problems.
* ** WARNING **
*
* This function has specifically been altered to break each of the data sets it
* requires into separate database calls. This is to avoid the performance problems
* observed when attempting to join large data sets (e.g. the message tables and
* the user table).
*
* While it is possible to gather the data in a single query, and it may even be
* more efficient with a correctly tuned database, we have opted to trade off some of
* the benefits of a single query in order to ensure this function will work on
* most databases with default tunings and with large data sets.
*
* @param int $userid The user id
* @param int $limitfrom
* @param int $limitnum
* @return array
*/
public static function get_conversations($userid, $limitfrom = 0, $limitnum = 0) {
$arrconversations = array();
if ($conversations = message_get_recent_conversations($userid, $limitfrom, $limitnum)) {
foreach ($conversations as $conversation) {
$arrconversations[$conversation->id] = helper::create_contact($conversation);
public static function get_conversations($userid, $limitfrom = 0, $limitnum = 20) {
global $DB;
// The case statement is used to make sure the same key is generated
// whether a user sent or received a message (it's the same conversation).
// E.g. If there is a message from user 1 to user 2 and then from user 2 to user 1 the result set
// will group those into a single record, since 1 -> 2 and 2 -> 1 is the same conversation.
$case1 = $DB->sql_concat('useridfrom', "'-'", 'useridto');
$case2 = $DB->sql_concat('useridto', "'-'", 'useridfrom');
$convocase = "CASE WHEN useridfrom > useridto
THEN $case1
ELSE $case2 END";
$convosig = "$convocase AS convo_signature";
// This is a snippet to join the message tables and filter out any messages the user has deleted
// and ignore notifications. The fields are specified by name so that the union works on MySQL.
$allmessages = "SELECT
id, useridfrom, useridto, subject, fullmessage, fullmessageformat,
fullmessagehtml, smallmessage, notification, contexturl,
contexturlname, timecreated, timeuserfromdeleted, timeusertodeleted,
component, eventtype, 0 as timeread
FROM {message}
WHERE
(useridto = ? AND timeusertodeleted = 0 AND notification = 0)
OR
(useridfrom = ? AND timeuserfromdeleted = 0 AND notification = 0)
UNION ALL
SELECT
id, useridfrom, useridto, subject, fullmessage, fullmessageformat,
fullmessagehtml, smallmessage, notification, contexturl,
contexturlname, timecreated, timeuserfromdeleted, timeusertodeleted,
component, eventtype, timeread
FROM {message_read}
WHERE
(useridto = ? AND timeusertodeleted = 0 AND notification = 0)
OR
(useridfrom = ? AND timeuserfromdeleted = 0 AND notification = 0)";
$allmessagesparams = [$userid, $userid, $userid, $userid];
// Create a transaction to protect against concurrency issues.
$transaction = $DB->start_delegated_transaction();
// First we need to get the list of conversations from the database ordered by the conversation
// with the most recent message first.
//
// This query will join the two message tables and then group the results by the combination
// of useridfrom and useridto (the 'convo_signature').
$conversationssql = "SELECT $convosig, max(timecreated) as timecreated
FROM ($allmessages) x
GROUP BY $convocase
ORDER BY max(timecreated) DESC, max(id) DESC";
$conversationrecords = $DB->get_records_sql($conversationssql, $allmessagesparams, $limitfrom, $limitnum);
// This user has no conversations so we can return early here.
if (empty($conversationrecords)) {
return [];
}
// Next we need to get the max id of the messages sent at the latest time for each conversation.
// This needs to be a separate query to above because there is no guarantee that the message with
// the highest id will also have the highest timecreated value (in fact that is fairly likely due
// to the split between the message tables).
//
// E.g. if we just added max(id) to the conversation query above and ran it on data like:
// id, userfrom, userto, timecreated
// 1, 1, 2, 2
// 2, 2, 1, 1
//
// Then the result of the query would be:
// convo_signature, timecreated, id
// 2-1, 2, 2
//
// That would be incorrect since the message with id 2 actually has a lower timecreated. Hence why
// the two queries need to be split.
//
// The same result could also be achieved with an inner join in a single query however we're specifically
// avoiding multiple joins in the messaging queries because of the size of the messaging tables.
$whereclauses = [];
$createdtimes = [];
foreach ($conversationrecords as $convoid => $record) {
$whereclauses[] = "($convocase = '$convoid' AND timecreated = {$record->timecreated})";
$createdtimes[] = $record->timecreated;
}
$messageidwhere = implode(' OR ', $whereclauses);
list($timecreatedsql, $timecreatedparams) = $DB->get_in_or_equal($createdtimes);
$allmessagestimecreated = "SELECT id, useridfrom, useridto, timecreated
FROM {message}
WHERE
(useridto = ? AND timeusertodeleted = 0 AND notification = 0)
OR
(useridfrom = ? AND timeuserfromdeleted = 0 AND notification = 0)
AND timecreated $timecreatedsql
UNION ALL
SELECT id, useridfrom, useridto, timecreated
FROM {message_read}
WHERE
(useridto = ? AND timeusertodeleted = 0 AND notification = 0)
OR
(useridfrom = ? AND timeuserfromdeleted = 0 AND notification = 0)
AND timecreated $timecreatedsql";
$messageidsql = "SELECT max(id)
FROM ($allmessagestimecreated) x
WHERE $messageidwhere
GROUP BY $convocase, timecreated";
$messageidparams = array_merge([$userid, $userid], $timecreatedparams, [$userid, $userid], $timecreatedparams);
$messageids = array_keys($DB->get_records_sql($messageidsql, $messageidparams));
// Ok, let's recap. We've pulled a descending ordered list of conversations by latest time created
// for the given user. For each of those conversations we've grabbed the max id for messages
// created at that time.
//
// So at this point we have the list of ids for the most recent message in each of the user's most
// recent conversations. Now we need to pull all of the message and user data for each message id.
list($idsql, $idparams) = $DB->get_in_or_equal($messageids);
$messagesql = "SELECT $convosig, m.smallmessage, m.id, m.useridto, m.useridfrom, m.timeread
FROM ($allmessages) m
WHERE m.id $idsql";
$messageparams = array_merge($allmessagesparams, $idparams);
// We need to handle the case where the $messageids contains two ids from the same conversation
// (which can happen because there can be id clashes between the read and unread tables). In
// this case we will prioritise the unread message.
$messageset = $DB->get_recordset_sql($messagesql, $messageparams);
$messages = [];
foreach ($messageset as $message) {
$id = $message->convo_signature;
if (!isset($messages[$id]) || empty($message->timeread)) {
$messages[$id] = $message;
}
}
$messageset->close();
// We need to pull out the list of other users that are part of each of these conversations. This
// needs to be done in a separate query to avoid doing a join on the messages tables and the user
// tables because on large sites these tables are massive which results in extremely slow
// performance (typically due to join buffer exhaustion).
$otheruserids = array_map(function($message) use ($userid) {
return ($message->useridfrom == $userid) ? $message->useridto : $message->useridfrom;
}, array_values($messages));
list($useridsql, $usersparams) = $DB->get_in_or_equal($otheruserids);
$userfields = \user_picture::fields('', array('lastaccess'));
$userssql = "SELECT $userfields
FROM {user}
WHERE id $useridsql";
$otherusers = $DB->get_records_sql($userssql, $usersparams);
// Similar to the above use case, we need to pull the contact information and again this has
// specifically been separated into another query to avoid having to do joins on the message
// tables.
$contactssql = "SELECT contactid, blocked
FROM {message_contacts}
WHERE userid = ? AND contactid $useridsql";
$contacts = $DB->get_records_sql($contactssql, array_merge([$userid], $otheruserids));
// Finally, let's get the unread messages count for this user so that we can add them
// to the conversation.
$unreadcountssql = 'SELECT useridfrom, count(*) as count
FROM {message}
WHERE useridto = ?
AND timeusertodeleted = 0
AND notification = 0
GROUP BY useridfrom';
$unreadcounts = $DB->get_records_sql($unreadcountssql, [$userid]);
// We can close off the transaction now.
$DB->commit_delegated_transaction($transaction);
// Now we need to order the messages back into the same order of the conversations.
$orderedconvosigs = array_keys($conversationrecords);
usort($messages, function($a, $b) use ($orderedconvosigs) {
$aindex = array_search($a->convo_signature, $orderedconvosigs);
$bindex = array_search($b->convo_signature, $orderedconvosigs);
return ($aindex < $bindex) ? -1 : 1;
});
// Preload the contexts before we construct the conversation to prevent the
// create_contact helper from needing to query the DB so often.
$ctxselect = \context_helper::get_preload_record_columns_sql('ctx');
$sql = "SELECT {$ctxselect}
FROM {context} ctx
WHERE ctx.contextlevel = ? AND
ctx.instanceid {$useridsql}";
$contexts = [];
$contexts = $DB->get_records_sql($sql, array_merge([CONTEXT_USER], $usersparams));
foreach ($contexts as $context) {
\context_helper::preload_from_record($context);
}
$userproperties = explode(',', $userfields);
$arrconversations = array();
// The last step now is to bring all of the data we've gathered together to create
// a conversation (or contact, as the API is named...).
foreach ($messages as $message) {
$conversation = new \stdClass();
$otheruserid = ($message->useridfrom == $userid) ? $message->useridto : $message->useridfrom;
$otheruser = isset($otherusers[$otheruserid]) ? $otherusers[$otheruserid] : null;
$contact = isset($contacts[$otheruserid]) ? $contacts[$otheruserid] : null;
// Add the other user's information to the conversation, if we have one.
foreach ($userproperties as $prop) {
$conversation->$prop = ($otheruser) ? $otheruser->$prop : null;
}
// Add the contact's information, if we have one.
$conversation->blocked = ($contact) ? $contact->blocked : null;
// Add the message information.
$conversation->messageid = $message->id;
$conversation->smallmessage = $message->smallmessage;
$conversation->useridfrom = $message->useridfrom;
// Only consider it unread if $user has unread messages.
if (isset($unreadcounts[$otheruserid])) {
$conversation->isread = false;
$conversation->unreadcount = $unreadcounts[$otheruserid]->count;
} else {
$conversation->isread = true;
}
$arrconversations[$message->convo_signature] = helper::create_contact($conversation);
}
return $arrconversations;
}
-138
View File
@@ -228,144 +228,6 @@ function message_count_unread_messages($user1=null, $user2=null) {
}
}
/**
* Get the users recent conversations meaning all the people they've recently
* sent or received a message from plus the most recent message sent to or received from each other user
*
* @param object|int $userorid the current user or user id
* @param int $limitfrom can be used for paging
* @param int $limitto can be used for paging
* @return array
*/
function message_get_recent_conversations($userorid, $limitfrom = 0, $limitto = 100) {
global $DB;
if (is_object($userorid)) {
$user = $userorid;
} else {
$userid = $userorid;
$user = new stdClass();
$user->id = $userid;
}
$userfields = user_picture::fields('otheruser', array('lastaccess'));
// This query retrieves the most recent message received from or sent to
// seach other user.
//
// If two messages have the same timecreated, we take the one with the
// larger id.
//
// There is a separate query for read and unread messages as they are stored
// in different tables. They were originally retrieved in one query but it
// was so large that it was difficult to be confident in its correctness.
$uniquefield = $DB->sql_concat('message.useridfrom', "'-'", 'message.useridto');
$sql = "SELECT $uniquefield, $userfields,
message.id as mid, message.notification, message.useridfrom, message.useridto,
message.smallmessage, message.fullmessage, message.fullmessagehtml,
message.fullmessageformat, message.timecreated,
contact.id as contactlistid, contact.blocked
FROM {message_read} message
JOIN (
SELECT MAX(id) AS messageid,
matchedmessage.useridto,
matchedmessage.useridfrom
FROM {message_read} matchedmessage
INNER JOIN (
SELECT MAX(recentmessages.timecreated) timecreated,
recentmessages.useridfrom,
recentmessages.useridto
FROM {message_read} recentmessages
WHERE (
(recentmessages.useridfrom = :userid1 AND recentmessages.timeuserfromdeleted = 0) OR
(recentmessages.useridto = :userid2 AND recentmessages.timeusertodeleted = 0)
)
GROUP BY recentmessages.useridfrom, recentmessages.useridto
) recent ON matchedmessage.useridto = recent.useridto
AND matchedmessage.useridfrom = recent.useridfrom
AND matchedmessage.timecreated = recent.timecreated
WHERE (
(matchedmessage.useridfrom = :userid6 AND matchedmessage.timeuserfromdeleted = 0) OR
(matchedmessage.useridto = :userid7 AND matchedmessage.timeusertodeleted = 0)
)
GROUP BY matchedmessage.useridto, matchedmessage.useridfrom
) messagesubset ON messagesubset.messageid = message.id
JOIN {user} otheruser ON (message.useridfrom = :userid4 AND message.useridto = otheruser.id)
OR (message.useridto = :userid5 AND message.useridfrom = otheruser.id)
LEFT JOIN {message_contacts} contact ON contact.userid = :userid3 AND contact.contactid = otheruser.id
WHERE otheruser.deleted = 0 AND message.notification = 0
ORDER BY message.timecreated DESC";
$params = array(
'userid1' => $user->id,
'userid2' => $user->id,
'userid3' => $user->id,
'userid4' => $user->id,
'userid5' => $user->id,
'userid6' => $user->id,
'userid7' => $user->id
);
$read = $DB->get_records_sql($sql, $params, $limitfrom, $limitto);
// We want to get the messages that have not been read. These are stored in the 'message' table. It is the
// exact same query as the one above, except for the table we are querying. So, simply replace references to
// the 'message_read' table with the 'message' table.
$sql = str_replace('{message_read}', '{message}', $sql);
$unread = $DB->get_records_sql($sql, $params, $limitfrom, $limitto);
$unreadcountssql = 'SELECT useridfrom, count(*) as count
FROM {message}
WHERE useridto = :userid
AND timeusertodeleted = 0
AND notification = 0
GROUP BY useridfrom';
$unreadcounts = $DB->get_records_sql($unreadcountssql, array('userid' => $user->id));
// Union the 2 result sets together looking for the message with the most
// recent timecreated for each other user.
// $conversation->id (the array key) is the other user's ID.
$conversations = array();
$conversation_arrays = array($unread, $read);
foreach ($conversation_arrays as $conversation_array) {
foreach ($conversation_array as $conversation) {
// Only consider it unread if $user has unread messages.
if (isset($unreadcounts[$conversation->useridfrom])) {
$conversation->isread = 0;
$conversation->unreadcount = $unreadcounts[$conversation->useridfrom]->count;
} else {
$conversation->isread = 1;
}
if (!isset($conversations[$conversation->id])) {
$conversations[$conversation->id] = $conversation;
} else {
$current = $conversations[$conversation->id];
// We need to maintain the isread and unreadcount values from existing
// parts of the conversation if we're replacing it.
$conversation->isread = ($conversation->isread && $current->isread);
if (isset($current->unreadcount) && !isset($conversation->unreadcount)) {
$conversation->unreadcount = $current->unreadcount;
}
if ($current->timecreated < $conversation->timecreated) {
$conversations[$conversation->id] = $conversation;
} else if ($current->timecreated == $conversation->timecreated) {
if ($current->mid < $conversation->mid) {
$conversations[$conversation->id] = $conversation;
}
}
}
}
}
// Sort the conversations by $conversation->timecreated, newest to oldest
// There may be multiple conversations with the same timecreated
// The conversations array contains both read and unread messages (different tables) so sorting by ID won't work
$result = core_collator::asort_objects_by_property($conversations, 'timecreated', core_collator::SORT_NUMERIC);
$conversations = array_reverse($conversations);
return $conversations;
}
/**
* Try to guess how to convert the message to html.
*
+2 -1
View File
@@ -1,5 +1,5 @@
<?xml version="1.0" encoding="UTF-8" ?>
<XMLDB PATH="message/output/popup/db" VERSION="20160729" COMMENT="XMLDB file for Moodle message/output/popup"
<XMLDB PATH="message/output/popup/db" VERSION="20161221" COMMENT="XMLDB file for Moodle message/output/popup"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="../../../../lib/xmldb/xmldb.xsd"
>
@@ -15,6 +15,7 @@
</KEYS>
<INDEXES>
<INDEX NAME="messageid-isread" UNIQUE="true" FIELDS="messageid, isread"/>
<INDEX NAME="isread" UNIQUE="false" FIELDS="isread"/>
</INDEXES>
</TABLE>
</TABLES>
+15
View File
@@ -74,5 +74,20 @@ function xmldb_message_popup_upgrade($oldversion) {
// Automatically generated Moodle v3.2.0 release upgrade line.
// Put any upgrade step following this.
if ($oldversion < 2016122100) {
// Define index isread (not unique) to be added to message_popup.
$table = new xmldb_table('message_popup');
$index = new xmldb_index('isread', XMLDB_INDEX_NOTUNIQUE, array('isread'));
// Conditionally launch add index isread.
if (!$dbman->index_exists($table, $index)) {
$dbman->add_index($table, $index);
}
// Popup savepoint reached.
upgrade_plugin_savepoint(true, 2016122100, 'message', 'popup');
}
return true;
}
+1 -1
View File
@@ -24,6 +24,6 @@
defined('MOODLE_INTERNAL') || die();
$plugin->version = 2016120500; // The current plugin version (Date: YYYYMMDDXX)
$plugin->version = 2016122100; // The current plugin version (Date: YYYYMMDDXX)
$plugin->requires = 2016112900; // Requires this Moodle version
$plugin->component = 'message_popup'; // Full name of the plugin (used for diagnostics)
+391 -8
View File
@@ -338,16 +338,16 @@ class core_message_api_testcase extends core_message_messagelib_testcase {
$this->send_fake_message($user1, $user2, 'Yo!', 0, $time + 1);
$this->send_fake_message($user2, $user1, 'Sup mang?', 0, $time + 2);
$this->send_fake_message($user1, $user2, 'Writing PHPUnit tests!', 0, $time + 3);
$this->send_fake_message($user2, $user1, 'Word.', 0, $time + 4);
$messageid1 = $this->send_fake_message($user2, $user1, 'Word.', 0, $time + 4);
$this->send_fake_message($user1, $user3, 'Booyah', 0, $time + 5);
$this->send_fake_message($user3, $user1, 'Whaaat?', 0, $time + 6);
$this->send_fake_message($user1, $user3, 'Nothing.', 0, $time + 7);
$this->send_fake_message($user3, $user1, 'Cool.', 0, $time + 8);
$messageid2 = $this->send_fake_message($user3, $user1, 'Cool.', 0, $time + 8);
$this->send_fake_message($user1, $user4, 'Hey mate, you see the new messaging UI in Moodle?', 0, $time + 9);
$this->send_fake_message($user4, $user1, 'Yah brah, it\'s pretty rad.', 0, $time + 10);
$this->send_fake_message($user1, $user4, 'Dope.', 0, $time + 11);
$messageid3 = $this->send_fake_message($user1, $user4, 'Dope.', 0, $time + 11);
// Retrieve the conversations.
$conversations = \core_message\api::get_conversations($user1->id);
@@ -363,17 +363,17 @@ class core_message_api_testcase extends core_message_messagelib_testcase {
$this->assertEquals($user1->id, $message1->useridfrom);
$this->assertTrue($message1->ismessaging);
$this->assertEquals('Dope.', $message1->lastmessage);
$this->assertNull($message1->messageid);
$this->assertEquals($messageid3, $message1->messageid);
$this->assertNull($message1->isonline);
$this->assertTrue($message1->isread);
$this->assertFalse($message1->isread);
$this->assertFalse($message1->isblocked);
$this->assertEquals(0, $message1->unreadcount);
$this->assertEquals(1, $message1->unreadcount);
$this->assertEquals($user3->id, $message2->userid);
$this->assertEquals($user3->id, $message2->useridfrom);
$this->assertTrue($message2->ismessaging);
$this->assertEquals('Cool.', $message2->lastmessage);
$this->assertNull($message2->messageid);
$this->assertEquals($messageid2, $message2->messageid);
$this->assertNull($message2->isonline);
$this->assertFalse($message2->isread);
$this->assertFalse($message2->isblocked);
@@ -383,13 +383,396 @@ class core_message_api_testcase extends core_message_messagelib_testcase {
$this->assertEquals($user2->id, $message3->useridfrom);
$this->assertTrue($message3->ismessaging);
$this->assertEquals('Word.', $message3->lastmessage);
$this->assertNull($message3->messageid);
$this->assertEquals($messageid1, $message3->messageid);
$this->assertNull($message3->isonline);
$this->assertFalse($message3->isread);
$this->assertFalse($message3->isblocked);
$this->assertEquals(2, $message3->unreadcount);
}
/**
* Tests retrieving conversations with a limit and offset to ensure pagination works correctly.
*/
public function test_get_conversations_limit_offset() {
// Create some users.
$user1 = self::getDataGenerator()->create_user();
$user2 = self::getDataGenerator()->create_user();
$user3 = self::getDataGenerator()->create_user();
$user4 = self::getDataGenerator()->create_user();
// The person doing the search.
$this->setUser($user1);
// Send some messages back and forth, have some different conversations with different users.
$time = 1;
$this->send_fake_message($user1, $user2, 'Yo!', 0, $time + 1);
$this->send_fake_message($user2, $user1, 'Sup mang?', 0, $time + 2);
$this->send_fake_message($user1, $user2, 'Writing PHPUnit tests!', 0, $time + 3);
$messageid1 = $this->send_fake_message($user2, $user1, 'Word.', 0, $time + 4);
$this->send_fake_message($user1, $user3, 'Booyah', 0, $time + 5);
$this->send_fake_message($user3, $user1, 'Whaaat?', 0, $time + 6);
$this->send_fake_message($user1, $user3, 'Nothing.', 0, $time + 7);
$messageid2 = $this->send_fake_message($user3, $user1, 'Cool.', 0, $time + 8);
$this->send_fake_message($user1, $user4, 'Hey mate, you see the new messaging UI in Moodle?', 0, $time + 9);
$this->send_fake_message($user4, $user1, 'Yah brah, it\'s pretty rad.', 0, $time + 10);
$messageid3 = $this->send_fake_message($user1, $user4, 'Dope.', 0, $time + 11);
// Retrieve the conversations.
$conversations = \core_message\api::get_conversations($user1->id, 1, 1);
// We should only have one conversation because of the limit.
$this->assertCount(1, $conversations);
$conversation = array_shift($conversations);
$this->assertEquals($user3->id, $conversation->userid);
$this->assertEquals($user3->id, $conversation->useridfrom);
$this->assertTrue($conversation->ismessaging);
$this->assertEquals('Cool.', $conversation->lastmessage);
$this->assertEquals($messageid2, $conversation->messageid);
$this->assertNull($conversation->isonline);
$this->assertFalse($conversation->isread);
$this->assertFalse($conversation->isblocked);
$this->assertEquals(2, $conversation->unreadcount);
// Retrieve the next conversation.
$conversations = \core_message\api::get_conversations($user1->id, 2, 1);
// We should only have one conversation because of the limit.
$this->assertCount(1, $conversations);
$conversation = array_shift($conversations);
$this->assertEquals($user2->id, $conversation->userid);
$this->assertEquals($user2->id, $conversation->useridfrom);
$this->assertTrue($conversation->ismessaging);
$this->assertEquals('Word.', $conversation->lastmessage);
$this->assertEquals($messageid1, $conversation->messageid);
$this->assertNull($conversation->isonline);
$this->assertFalse($conversation->isread);
$this->assertFalse($conversation->isblocked);
$this->assertEquals(2, $conversation->unreadcount);
// Ask for an offset that doesn't exist.
$conversations = \core_message\api::get_conversations($user1->id, 4, 1);
// We should not get any conversations back.
$this->assertCount(0, $conversations);
}
/**
* The data provider for get_conversations_mixed.
*
* This provides sets of data to for testing.
* @return array
*/
public function get_conversations_mixed_provider() {
return array(
'Test that conversations with messages contacts is correctly ordered.' => array(
'users' => array(
'user1',
'user2',
'user3',
),
'contacts' => array(
),
'messages' => array(
array(
'from' => 'user1',
'to' => 'user2',
'state' => 'unread',
'subject' => 'S1',
),
array(
'from' => 'user2',
'to' => 'user1',
'state' => 'unread',
'subject' => 'S2',
),
array(
'from' => 'user1',
'to' => 'user2',
'state' => 'unread',
'timecreated' => 0,
'subject' => 'S3',
),
array(
'from' => 'user1',
'to' => 'user3',
'state' => 'read',
'timemodifier' => 1,
'subject' => 'S4',
),
array(
'from' => 'user3',
'to' => 'user1',
'state' => 'read',
'timemodifier' => 1,
'subject' => 'S5',
),
array(
'from' => 'user1',
'to' => 'user3',
'state' => 'read',
'timecreated' => 0,
'subject' => 'S6',
),
),
'expectations' => array(
'user1' => array(
// User1 has conversed most recently with user3. The most recent message is M5.
array(
'messageposition' => 0,
'with' => 'user3',
'subject' => 'S5',
),
// User1 has also conversed with user2. The most recent message is S2.
array(
'messageposition' => 1,
'with' => 'user2',
'subject' => 'S2',
),
),
'user2' => array(
// User2 has only conversed with user1. Their most recent shared message was S2.
array(
'messageposition' => 0,
'with' => 'user1',
'subject' => 'S2',
),
),
'user3' => array(
// User3 has only conversed with user1. Their most recent shared message was S5.
array(
'messageposition' => 0,
'with' => 'user1',
'subject' => 'S5',
),
),
),
),
'Test that users with contacts and messages to self work as expected' => array(
'users' => array(
'user1',
'user2',
'user3',
),
'contacts' => array(
'user1' => array(
'user2' => 0,
'user3' => 0,
),
'user2' => array(
'user3' => 0,
),
),
'messages' => array(
array(
'from' => 'user1',
'to' => 'user1',
'state' => 'unread',
'subject' => 'S1',
),
array(
'from' => 'user1',
'to' => 'user1',
'state' => 'unread',
'subject' => 'S2',
),
),
'expectations' => array(
'user1' => array(
// User1 has conversed most recently with user1. The most recent message is S2.
array(
'messageposition' => 0,
'with' => 'user1',
'subject' => 'S2',
),
),
),
),
'Test conversations with a single user, where some messages are read and some are not.' => array(
'users' => array(
'user1',
'user2',
),
'contacts' => array(
),
'messages' => array(
array(
'from' => 'user1',
'to' => 'user2',
'state' => 'read',
'subject' => 'S1',
),
array(
'from' => 'user2',
'to' => 'user1',
'state' => 'read',
'subject' => 'S2',
),
array(
'from' => 'user1',
'to' => 'user2',
'state' => 'unread',
'timemodifier' => 1,
'subject' => 'S3',
),
array(
'from' => 'user1',
'to' => 'user2',
'state' => 'unread',
'timemodifier' => 1,
'subject' => 'S4',
),
),
'expectations' => array(
// The most recent message between user1 and user2 was S4.
'user1' => array(
array(
'messageposition' => 0,
'with' => 'user2',
'subject' => 'S4',
),
),
'user2' => array(
// The most recent message between user1 and user2 was S4.
array(
'messageposition' => 0,
'with' => 'user1',
'subject' => 'S4',
),
),
),
),
'Test conversations with a single user, where some messages are read and some are not, and messages ' .
'are out of order' => array(
// This can happen through a combination of factors including multi-master DB replication with messages
// read somehow (e.g. API).
'users' => array(
'user1',
'user2',
),
'contacts' => array(
),
'messages' => array(
array(
'from' => 'user1',
'to' => 'user2',
'state' => 'read',
'subject' => 'S1',
'timemodifier' => 1,
),
array(
'from' => 'user2',
'to' => 'user1',
'state' => 'read',
'subject' => 'S2',
'timemodifier' => 2,
),
array(
'from' => 'user1',
'to' => 'user2',
'state' => 'unread',
'subject' => 'S3',
),
array(
'from' => 'user1',
'to' => 'user2',
'state' => 'unread',
'subject' => 'S4',
),
),
'expectations' => array(
// The most recent message between user1 and user2 was S2, even though later IDs have not been read.
'user1' => array(
array(
'messageposition' => 0,
'with' => 'user2',
'subject' => 'S2',
),
),
'user2' => array(
array(
'messageposition' => 0,
'with' => 'user1',
'subject' => 'S2',
),
),
),
),
);
}
/**
* Test get_conversations with a mixture of messages.
*
* @dataProvider get_conversations_mixed_provider
* @param array $usersdata The list of users to create for this test.
* @param array $messagesdata The list of messages to create.
* @param array $expectations The list of expected outcomes.
*/
public function test_get_conversations_mixed($usersdata, $contacts, $messagesdata, $expectations) {
global $DB;
// Create all of the users.
$users = array();
foreach ($usersdata as $username) {
$users[$username] = $this->getDataGenerator()->create_user(array('username' => $username));
}
foreach ($contacts as $username => $contact) {
foreach ($contact as $contactname => $blocked) {
$record = new stdClass();
$record->userid = $users[$username]->id;
$record->contactid = $users[$contactname]->id;
$record->blocked = $blocked;
$record->id = $DB->insert_record('message_contacts', $record);
}
}
$defaulttimecreated = time();
foreach ($messagesdata as $messagedata) {
$from = $users[$messagedata['from']];
$to = $users[$messagedata['to']];
$subject = $messagedata['subject'];
if (isset($messagedata['state']) && $messagedata['state'] == 'unread') {
$table = 'message';
$messageid = $this->send_fake_message($from, $to, $subject);
} else {
// If there is no state, or the state is not 'unread', assume the message is read.
$table = 'message_read';
$messageid = message_post_message($from, $to, $subject, FORMAT_PLAIN);
}
$updatemessage = new stdClass();
$updatemessage->id = $messageid;
if (isset($messagedata['timecreated'])) {
$updatemessage->timecreated = $messagedata['timecreated'];
} else if (isset($messagedata['timemodifier'])) {
$updatemessage->timecreated = $defaulttimecreated + $messagedata['timemodifier'];
} else {
$updatemessage->timecreated = $defaulttimecreated;
}
$DB->update_record($table, $updatemessage);
}
foreach ($expectations as $username => $data) {
// Get the recent conversations for the specified user.
$user = $users[$username];
$conversations = array_values(\core_message\api::get_conversations($user->id));
foreach ($data as $expectation) {
$otheruser = $users[$expectation['with']];
$conversation = $conversations[$expectation['messageposition']];
$this->assertEquals($otheruser->id, $conversation->userid);
$this->assertEquals($expectation['subject'], $conversation->lastmessage);
}
}
}
/**
* Tests retrieving contacts.
*/
+18 -17
View File
@@ -69,7 +69,8 @@ class core_message_externallib_testcase extends externallib_advanced_testcase {
$record->fullmessage = $message;
$record->timecreated = $time;
$record->notification = $notification;
$DB->insert_record('message', $record);
return $DB->insert_record('message', $record);
}
/**
@@ -1536,16 +1537,16 @@ class core_message_externallib_testcase extends externallib_advanced_testcase {
$this->send_message($user1, $user2, 'Yo!', 0, $time);
$this->send_message($user2, $user1, 'Sup mang?', 0, $time + 1);
$this->send_message($user1, $user2, 'Writing PHPUnit tests!', 0, $time + 2);
$this->send_message($user2, $user1, 'Word.', 0, $time + 3);
$messageid1 = $this->send_message($user2, $user1, 'Word.', 0, $time + 3);
$this->send_message($user1, $user3, 'Booyah', 0, $time + 4);
$this->send_message($user3, $user1, 'Whaaat?', 0, $time + 5);
$this->send_message($user1, $user3, 'Nothing.', 0, $time + 6);
$this->send_message($user3, $user1, 'Cool.', 0, $time + 7);
$messageid2 = $this->send_message($user3, $user1, 'Cool.', 0, $time + 7);
$this->send_message($user1, $user4, 'Hey mate, you see the new messaging UI in Moodle?', 0, $time + 8);
$this->send_message($user4, $user1, 'Yah brah, it\'s pretty rad.', 0, $time + 9);
$this->send_message($user1, $user4, 'Dope.', 0, $time + 10);
$messageid3 = $this->send_message($user1, $user4, 'Dope.', 0, $time + 10);
// Retrieve the conversations.
$result = core_message_external::data_for_messagearea_conversations($user1->id);
@@ -1566,17 +1567,17 @@ class core_message_externallib_testcase extends externallib_advanced_testcase {
$this->assertTrue($message1['ismessaging']);
$this->assertTrue($message1['sentfromcurrentuser']);
$this->assertEquals('Dope.', $message1['lastmessage']);
$this->assertNull($message1['messageid']);
$this->assertEquals($messageid3, $message1['messageid']);
$this->assertNull($message1['isonline']);
$this->assertTrue($message1['isread']);
$this->assertFalse($message1['isread']);
$this->assertFalse($message1['isblocked']);
$this->assertEquals(0, $message1['unreadcount']);
$this->assertEquals(1, $message1['unreadcount']);
$this->assertEquals($user3->id, $message2['userid']);
$this->assertTrue($message2['ismessaging']);
$this->assertFalse($message2['sentfromcurrentuser']);
$this->assertEquals('Cool.', $message2['lastmessage']);
$this->assertNull($message2['messageid']);
$this->assertEquals($messageid2, $message2['messageid']);
$this->assertNull($message2['isonline']);
$this->assertFalse($message2['isread']);
$this->assertFalse($message2['isblocked']);
@@ -1586,7 +1587,7 @@ class core_message_externallib_testcase extends externallib_advanced_testcase {
$this->assertTrue($message3['ismessaging']);
$this->assertFalse($message3['sentfromcurrentuser']);
$this->assertEquals('Word.', $message3['lastmessage']);
$this->assertNull($message3['messageid']);
$this->assertEquals($messageid1, $message3['messageid']);
$this->assertNull($message3['isonline']);
$this->assertFalse($message3['isread']);
$this->assertFalse($message3['isblocked']);
@@ -1613,16 +1614,16 @@ class core_message_externallib_testcase extends externallib_advanced_testcase {
$this->send_message($user1, $user2, 'Yo!', 0, $time);
$this->send_message($user2, $user1, 'Sup mang?', 0, $time + 1);
$this->send_message($user1, $user2, 'Writing PHPUnit tests!', 0, $time + 2);
$this->send_message($user2, $user1, 'Word.', 0, $time + 3);
$messageid1 = $this->send_message($user2, $user1, 'Word.', 0, $time + 3);
$this->send_message($user1, $user3, 'Booyah', 0, $time + 4);
$this->send_message($user3, $user1, 'Whaaat?', 0, $time + 5);
$this->send_message($user1, $user3, 'Nothing.', 0, $time + 6);
$this->send_message($user3, $user1, 'Cool.', 0, $time + 7);
$messageid2 = $this->send_message($user3, $user1, 'Cool.', 0, $time + 7);
$this->send_message($user1, $user4, 'Hey mate, you see the new messaging UI in Moodle?', 0, $time + 8);
$this->send_message($user4, $user1, 'Yah brah, it\'s pretty rad.', 0, $time + 9);
$this->send_message($user1, $user4, 'Dope.', 0, $time + 10);
$messageid3 = $this->send_message($user1, $user4, 'Dope.', 0, $time + 10);
// Retrieve the conversations.
$result = core_message_external::data_for_messagearea_conversations($user1->id);
@@ -1643,17 +1644,17 @@ class core_message_externallib_testcase extends externallib_advanced_testcase {
$this->assertTrue($message1['ismessaging']);
$this->assertTrue($message1['sentfromcurrentuser']);
$this->assertEquals('Dope.', $message1['lastmessage']);
$this->assertNull($message1['messageid']);
$this->assertEquals($messageid3, $message1['messageid']);
$this->assertFalse($message1['isonline']);
$this->assertTrue($message1['isread']);
$this->assertFalse($message1['isread']);
$this->assertFalse($message1['isblocked']);
$this->assertEquals(0, $message1['unreadcount']);
$this->assertEquals(1, $message1['unreadcount']);
$this->assertEquals($user3->id, $message2['userid']);
$this->assertTrue($message2['ismessaging']);
$this->assertFalse($message2['sentfromcurrentuser']);
$this->assertEquals('Cool.', $message2['lastmessage']);
$this->assertNull($message2['messageid']);
$this->assertEquals($messageid2, $message2['messageid']);
$this->assertFalse($message2['isonline']);
$this->assertFalse($message2['isread']);
$this->assertFalse($message2['isblocked']);
@@ -1663,7 +1664,7 @@ class core_message_externallib_testcase extends externallib_advanced_testcase {
$this->assertTrue($message3['ismessaging']);
$this->assertFalse($message3['sentfromcurrentuser']);
$this->assertEquals('Word.', $message3['lastmessage']);
$this->assertNull($message3['messageid']);
$this->assertEquals($messageid1, $message3['messageid']);
$this->assertFalse($message3['isonline']);
$this->assertFalse($message3['isread']);
$this->assertFalse($message3['isblocked']);
+1
View File
@@ -605,6 +605,7 @@ class core_message_messagelib_testcase extends advanced_testcase {
// Get the recent conversations for the specified user.
$user = $users[$username];
$conversations = message_get_recent_conversations($user);
$this->assertDebuggingCalled();
foreach ($data as $expectation) {
$otheruser = $users[$expectation['with']];
$conversation = $conversations[$expectation['messageposition']];
+1 -1
View File
@@ -29,7 +29,7 @@
defined('MOODLE_INTERNAL') || die();
$version = 2017020200.01; // YYYYMMDD = weekly release date of this DEV branch.
$version = 2017020700.00; // YYYYMMDD = weekly release date of this DEV branch.
// RR = release increments - 00 in DEV branches.
// .XX = incremental changes.