+
To:
diff --git a/addons/mail/static/src/xml/user_menu.xml b/addons/mail/static/src/xml/user_menu.xml
new file mode 100644
index 00000000000..b5186c3b056
--- /dev/null
+++ b/addons/mail/static/src/xml/user_menu.xml
@@ -0,0 +1,11 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/addons/mail/static/tests/chatter_tests.js b/addons/mail/static/tests/chatter_tests.js
index e75157d3ad7..7aaa35ac4e3 100644
--- a/addons/mail/static/tests/chatter_tests.js
+++ b/addons/mail/static/tests/chatter_tests.js
@@ -27,6 +27,18 @@ QUnit.module('Chatter', {
this.services = mailTestUtils.getMailServices();
this.data = {
+ 'res.partner': {
+ fields: {
+ im_status: {
+ string: "im_status",
+ type: "char",
+ }
+ },
+ records: [{
+ id: 1,
+ im_status: 'online',
+ }]
+ },
partner: {
fields: {
display_name: { string: "Displayed name", type: "char" },
@@ -772,7 +784,7 @@ QUnit.test('chatter: post, receive and star messages', function (assert) {
}
if (args.method === 'get_mention_suggestions') {
getSuggestionsDef.resolve();
- return $.when([{email: "test@odoo.com", id: 1, name: "Test User"}]);
+ return $.when([[{email: "test@odoo.com", id: 1, name: "Test User"}], []]);
}
if (args.method === 'message_post') {
var lastMessageData = _.max(this.data['mail.message'].records, function (messageData) {
@@ -1308,7 +1320,7 @@ QUnit.test('chatter: discard changes on message post with post_refresh "recipien
}
if (args.method === 'get_mention_suggestions') {
getSuggestionsDef.resolve();
- return $.when([{email: "me@odoo.com", id: 42, name: "Me"}]);
+ return $.when([[{email: "me@odoo.com", id: 42, name: "Me"}], []]);
}
if (args.method === 'message_format') {
var requested_msgs = _.filter(messages, function (msg) {
diff --git a/addons/mail/static/tests/discuss_tests.js b/addons/mail/static/tests/discuss_tests.js
index 5f903898255..4a8dc9c56a3 100644
--- a/addons/mail/static/tests/discuss_tests.js
+++ b/addons/mail/static/tests/discuss_tests.js
@@ -61,6 +61,18 @@ QUnit.module('Discuss', {
},
},
},
+ 'res.partner': {
+ fields: {
+ im_status: {
+ string: "status",
+ type: 'char',
+ },
+ },
+ records: [{
+ id: 1,
+ im_status: 'online',
+ }]
+ },
};
this.services = mailTestUtils.getMailServices();
},
diff --git a/addons/mail/static/tests/helpers/test_utils.js b/addons/mail/static/tests/helpers/test_utils.js
index cf63c281c58..063e3a9eb03 100644
--- a/addons/mail/static/tests/helpers/test_utils.js
+++ b/addons/mail/static/tests/helpers/test_utils.js
@@ -5,6 +5,7 @@ var BusService = require('bus.BusService');
var Discuss = require('mail.Discuss');
var MailService = require('mail.Service');
+var mailUtils = require('mail.utils');
var AbstractStorageService = require('web.AbstractStorageService');
var Class = require('web.Class');
@@ -79,6 +80,111 @@ var MockMailService = Class.extend({
},
});
+/**
+ * Patch all the mailUtils.clearTimeout and mailUtils.setTimeout.
+ *
+ * @return {Object} helper functions, including unpatch and time management tools.
+ */
+var patchMailTimeouts = function () {
+ var currentTime = 0;
+ var timeouts = {};
+ var countTimeout = 0;
+
+ mailUtils.clearTimeout = function (id) {
+ delete timeouts[id];
+ };
+
+ mailUtils.setTimeout = function (func, duration) {
+ duration = duration || 0;
+ var executeTime = currentTime + duration;
+ countTimeout++;
+ timeouts[countTimeout] = {
+ executeTime: executeTime,
+ func: func
+ };
+ return countTimeout;
+ };
+ /**
+ * @return {integer|boolean} id of the next timeout in queue, false if queue is empty
+ */
+ function getNextTimeoutId() {
+ var minKey = false;
+ _.each(timeouts, function (value, key) {
+ if (minKey === false) {
+ minKey = Number(key);
+ return;
+ }
+ var minTime = timeouts[minKey].executeTime;
+ if (value.executeTime < minTime || (value.executeTime === minTime && key < minKey)) {
+ minKey = Number(key);
+ }
+ });
+ return minKey;
+ }
+
+ /**
+ * @return {integer|boolean} delay (time interval) before the next timeout in queue is executed.
+ * Useful to know how much time to advance to execute next timer.
+ */
+ function getNextTimeoutDelay() {
+ var next = getNextTimeoutId();
+ if (next === false) {
+ return false;
+ }
+ return timeouts[next].executeTime - currentTime;
+ }
+
+ /**
+ * Set the current time to given time
+ *
+ * @param {integer} time
+ */
+ function setTime(time) {
+ var next = getNextTimeoutId();
+ if (next !== false && timeouts[next].executeTime <= time) {
+ currentTime = timeouts[next].executeTime;
+ var func = timeouts[next].func;
+ // watch out setTimeout inside setTimeout (recursive)
+ delete timeouts[next];
+ func();
+ setTime(time);
+ }
+ else {
+ currentTime = time;
+ }
+ }
+
+ /**
+ * Add the given time to current time
+ *
+ * @param {integer} time
+ */
+ function addTime(time) {
+ setTime(currentTime + time);
+ }
+
+ /**
+ * Set time to the max time in queue and execute all timeouts before this time.
+ */
+ function runPendingTimeouts() {
+ var maxTimeInQueue = 0;
+ _.each(timeouts, function (value, key) {
+ if (value.executeTime > maxTimeInQueue) {
+ maxTimeInQueue = value.executeTime;
+ }
+ });
+ setTime(maxTimeInQueue);
+ }
+
+ return {
+ addTime: addTime,
+ getNextTimeoutDelay:getNextTimeoutDelay,
+ runPendingTimeouts: runPendingTimeouts,
+ setTime: setTime,
+ };
+};
+
+
/**
* Returns the list of mail services required by the mail components: a
* mail_service, and its two dependencies bus_service and local_storage.
@@ -87,6 +193,7 @@ var MockMailService = Class.extend({
* and local_storage, in that order
*/
function getMailServices() {
+ patchMailTimeouts();
return new MockMailService().getServices();
}
@@ -94,6 +201,7 @@ return {
MockMailService: MockMailService,
createDiscuss: createDiscuss,
getMailServices: getMailServices,
+ patchMailTimeouts: patchMailTimeouts,
};
});
diff --git a/addons/mail/static/tests/mail_status_manager_tests.js b/addons/mail/static/tests/mail_status_manager_tests.js
new file mode 100644
index 00000000000..a1ae329c9dc
--- /dev/null
+++ b/addons/mail/static/tests/mail_status_manager_tests.js
@@ -0,0 +1,177 @@
+odoo.define('mail.mailStatusServiceTests', function (require) {
+"use strict";
+
+var mailTestUtils = require('mail.testUtils');
+
+var core = require('web.core');
+var testUtils = require('web.test_utils');
+var Widget = require('web.Widget');
+
+var QWeb = core.qweb;
+
+QUnit.module('mail', {}, function () {
+QUnit.module('service', {}, function () {
+QUnit.module('Status manager', {
+ beforeEach: function () {
+ this.services = mailTestUtils.getMailServices(this);
+ this.timeoutMock = mailTestUtils.patchMailTimeouts();
+ },
+});
+QUnit.test('simple set im_status', function (assert) {
+ assert.expect(1);
+ var parent = testUtils.createParent({
+ services: this.services,
+ mockRPC: function (route, args) {
+ if (route === '/mail/init_messaging') {
+ return this._super.apply(this, arguments);
+ }
+ throw new Error(_.str.sprintf('No rpc call should be performed: %s, %s \n %s', args.model, args.method, route));
+ },
+ });
+ parent.call('mail_service', 'updateImStatus', [{
+ id: 1,
+ im_status: 'online',
+ }]);
+ assert.strictEqual(parent.call('mail_service', 'getImStatus', { partnerID: 1 }), 'online');
+
+ this.timeoutMock.runPendingTimeouts();
+ parent.destroy();
+});
+
+QUnit.test('multi get_im_status', function (assert) {
+ assert.expect(9);
+ var readCount = 0;
+ var parent = testUtils.createParent({
+ //data: this.data,
+ services: this.services,
+ mockRPC: function (route, args) {
+ if (route === '/mail/init_messaging') {
+ return this._super.apply(this, arguments);
+ }
+ if (args.method === 'read' && args.model === 'res.partner') {
+ assert.deepEqual(args.args[0], [2,3]);
+ assert.deepEqual(args.args[1], ['id', 'im_status']);
+ readCount++;
+ return $.when([
+ {id: 2, im_status: 'away'},
+ {id: 3, im_status: 'im_partner'}
+ ]);
+ }
+ throw new Error(_.str.sprintf('No rpc call should be performed: %s, %s \n %s', args.model, args.method, route));
+ },
+ });
+ parent.call('mail_service', 'updateImStatus', [{
+ id: 1,
+ im_status: 'online',
+ }]);
+ assert.strictEqual(parent.call('mail_service', 'getImStatus', { partnerID: 1 }), 'online');
+ assert.strictEqual(parent.call('mail_service', 'getImStatus', { partnerID: 2 }), undefined);
+ assert.strictEqual(parent.call('mail_service', 'getImStatus', { partnerID: 3 }), undefined);
+
+ this.timeoutMock.runPendingTimeouts();
+ assert.strictEqual(parent.call('mail_service', 'getImStatus', { partnerID: 1 }), 'online');
+ assert.strictEqual(parent.call('mail_service', 'getImStatus', { partnerID: 2 }), 'away');
+ assert.strictEqual(parent.call('mail_service', 'getImStatus', { partnerID: 3 }), 'im_partner');
+
+ this.timeoutMock.runPendingTimeouts();
+ assert.strictEqual(readCount, 1, 'Only one read on partner should have been performed');
+
+ parent.destroy();
+});
+
+QUnit.test('update loop', function (assert) {
+ assert.expect(12);
+ var readCount = 0;
+ var parent = testUtils.createParent({
+ services: this.services,
+ mockRPC: function (route, args) {
+ if (route === '/mail/init_messaging') {
+ return this._super.apply(this, arguments);
+ }
+ if (args.method === 'read' && args.model === 'res.partner') {
+ assert.deepEqual(args.args[0], [1, 2]);
+ assert.deepEqual(args.args[1], ['id', 'im_status']);
+ readCount++;
+ return $.when([
+ {"id": 1, "im_status": "online"},
+ {"id": 2, "im_status": "away"}
+ ]);
+ }
+ throw new Error(_.str.sprintf('No rpc call should be performed: %s, %s \n %s', args.model, args.method, route));
+ },
+ });
+ // set initial status
+ parent.call('mail_service', 'updateImStatus', [
+ { id: 1, im_status: 'offline' },
+ { id: 2, im_status: 'offline' },
+ { id: 3, im_status: 'im_partner' }, //shouldn't be updated !!!!
+ ]);
+ //_updateImStatusLoop should be running at one second per iteration, lets make a minute pass.
+ assert.strictEqual(readCount, 0);
+ this.timeoutMock.addTime(50*1000);
+ assert.strictEqual(readCount, 0);
+ this.timeoutMock.addTime(1000);
+ assert.strictEqual(readCount, 1, 'one call should have been made after 50 seconds' );
+ assert.strictEqual(parent.call('mail_service', 'getImStatus', { partnerID: 1 }), 'online');
+ assert.strictEqual(parent.call('mail_service', 'getImStatus', { partnerID: 2 }), 'away');
+
+ //simulate change of focus
+ //original listener: $(window).on("blur", this._onWindowFocusChange.bind(this, false); + unload, ...
+ parent.call('mail_service', '_onWindowFocusChange', false); // remove focus from tab
+
+ this.timeoutMock.addTime(5*60*1000); // x minutes without focus, no rpc should be done during this time
+ assert.strictEqual(readCount, 1, 'No more call should have been performed');
+ //simulate change of focus
+ //original listener: $(window).on("focus", this._onWindowFocusChange.bind(this, true);
+ parent.call('mail_service', '_onWindowFocusChange', true); // give focus to tab
+ var nextUpdateDelay = this.timeoutMock.getNextTimeoutDelay();
+ assert.strictEqual(nextUpdateDelay, 1000, "next update should be done in maximum one second");
+ this.timeoutMock.addTime(nextUpdateDelay); // one second should be enough
+ assert.strictEqual(readCount, 2, 'One more call should have been done once tab focused');
+ this.timeoutMock.runPendingTimeouts();
+ parent.destroy();
+});
+
+QUnit.test('update status', function (assert) {
+ // the current solution to look for updatable im_status in dom is not perfect, but waiting for
+ // ability to include widgets in views, this is the most simple solution
+ assert.expect(2);
+ var StatusWidget = Widget.extend({
+ start: function () {
+ this.render();
+ this._super.apply(this, arguments);
+ },
+ render: function () {
+ var status = QWeb.render('mail.UserStatus', {
+ status: 'online',
+ partnerID: 1,
+ });
+ this.$el.html(status);
+ }
+ });
+ var statusWidget = new StatusWidget();
+ testUtils.mock.addMockEnvironment(statusWidget, {
+ services: this.services,
+ mockRPC: function (route, args) {
+ if (route === '/mail/init_messaging') {
+ return this._super.apply(this, arguments);
+ }
+ throw new Error(_.str.sprintf('No rpc call should be performed: %s, %s \n %s', args.model, args.method, route));
+ },
+ });
+ statusWidget.appendTo($('#qunit-fixture'));
+ //Render unknow im_status:
+ // set initial status
+ assert.ok(statusWidget.$('.o_updatable_im_status i').hasClass('o_user_online'));
+
+ statusWidget.call('mail_service', 'updateImStatus', [
+ { id: 1, im_status: 'offline' },
+ ]);
+ assert.notOk(statusWidget.$('.o_updatable_im_status i').hasClass('o_user_online'));
+ this.timeoutMock.runPendingTimeouts();
+ statusWidget.destroy();
+});
+
+});
+});
+});
diff --git a/addons/mail/static/tests/thread_window/basic_thread_window_tests.js b/addons/mail/static/tests/thread_window/basic_thread_window_tests.js
index 3fc942b506f..5a0bc62f880 100644
--- a/addons/mail/static/tests/thread_window/basic_thread_window_tests.js
+++ b/addons/mail/static/tests/thread_window/basic_thread_window_tests.js
@@ -632,6 +632,69 @@ QUnit.test('do not auto-focus chat window on receiving new message from new DM',
parent.destroy();
});
+QUnit.test('out-of-office status in thread window', function (assert) {
+ assert.expect(1);
+ this.data = {
+ 'mail.message': {
+ fields: {},
+ records: [],
+ },
+ initMessaging: {
+ channel_slots: {
+ channel_channel: [{
+ id: 1,
+ name: "DM",
+ channel_type: "chat",
+ message_unread_counter: 0,
+ direct_partner: [{ id: 666, name: 'DemoUser1', im_status: 'online', out_of_office_message: 'Please don\'t disturb'}],
+ }],
+ },
+ },
+ };
+ var parent = this.createParent({
+ data: this.data,
+ services: this.services,
+ });
+ // detach channel 1, so that it opens corresponding thread window.
+ parent.call('mail_service', 'getChannel', 1).detach();
+
+ var $threadWindow = $('.o_thread_window');
+ assert.containsOnce($threadWindow, '.o_out_of_office_text');
+
+ parent.destroy();
+});
+
+QUnit.test('no out-of-office status in thread window', function (assert) {
+ assert.expect(1);
+ this.data = {
+ 'mail.message': {
+ fields: {},
+ records: [],
+ },
+ initMessaging: {
+ channel_slots: {
+ channel_channel: [{
+ id: 1,
+ name: "DM",
+ channel_type: "chat",
+ message_unread_counter: 0,
+ direct_partner: [{ id: 666, name: 'DemoUser1', im_status: 'online'}],
+ }],
+ },
+ },
+ };
+ var parent = this.createParent({
+ data: this.data,
+ services: this.services,
+ });
+ // detach channel 1, so that it opens corresponding thread window.
+ parent.call('mail_service', 'getChannel', 1).detach();
+
+ var $threadWindow = $('.o_thread_window');
+ assert.containsNone($threadWindow, '.o_out_of_office_text');
+ parent.destroy();
+});
+
});
});
});
diff --git a/addons/mail/views/mail_templates.xml b/addons/mail/views/mail_templates.xml
index e0175e77195..ea742a29fda 100644
--- a/addons/mail/views/mail_templates.xml
+++ b/addons/mail/views/mail_templates.xml
@@ -14,6 +14,7 @@
+
@@ -65,6 +66,7 @@
+
@@ -129,8 +131,10 @@
-
+
+
+
diff --git a/addons/mail/views/res_users_views.xml b/addons/mail/views/res_users_views.xml
index d133fb0424b..8f4a376f0d9 100644
--- a/addons/mail/views/res_users_views.xml
+++ b/addons/mail/views/res_users_views.xml
@@ -12,6 +12,11 @@
+
+
+
+
+
@@ -33,6 +38,5 @@
-
diff --git a/addons/mail_bot/models/res_partner.py b/addons/mail_bot/models/res_partner.py
index c715adccc49..2a6ec5eca55 100644
--- a/addons/mail_bot/models/res_partner.py
+++ b/addons/mail_bot/models/res_partner.py
@@ -2,6 +2,7 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import api, models
+
class Partner(models.Model):
_inherit = 'res.partner'
diff --git a/addons/mail_bot/views/discuss.xml b/addons/mail_bot/views/discuss.xml
index e7f0650c867..95526e6b11a 100644
--- a/addons/mail_bot/views/discuss.xml
+++ b/addons/mail_bot/views/discuss.xml
@@ -1,10 +1,5 @@
-
-
-
-
-
diff --git a/addons/test_mail/tests/test_mail_channel.py b/addons/test_mail/tests/test_mail_channel.py
index 79ab747fed5..c95f562332d 100644
--- a/addons/test_mail/tests/test_mail_channel.py
+++ b/addons/test_mail/tests/test_mail_channel.py
@@ -196,6 +196,19 @@ class TestChannelFeatures(common.BaseFunctionalTest, common.MockEmails):
email['email_to'][0],
[formataddr((self.user_employee.name, self.user_employee.email)), formataddr((self.test_partner.name, self.test_partner.email))])
+ @mute_logger('odoo.addons.mail.models.mail_mail')
+ def test_channel_out_of_office(self):
+ self.user_employee.out_of_office_message = 'Out'
+ test_chat = self.env['mail.channel'].with_context(common.BaseFunctionalTest._test_context).create({
+ 'channel_partner_ids': [(4, self.user_employee.partner_id.id), (4, self.user_admin.partner_id.id)],
+ 'public': 'private',
+ 'channel_type': 'chat',
+ 'email_send': False,
+ 'name': 'test'
+ })
+ infos = test_chat.sudo(self.user_admin).channel_info()
+ self.assertEqual(infos[0]['direct_partner'][0]['out_of_office_message'], 'Out')
+
@tagged('moderation')
class TestChannelModeration(common.Moderation):
diff --git a/odoo/addons/base/views/res_users_views.xml b/odoo/addons/base/views/res_users_views.xml
index f75ba79e3ef..ae30ce89dff 100644
--- a/odoo/addons/base/views/res_users_views.xml
+++ b/odoo/addons/base/views/res_users_views.xml
@@ -354,7 +354,7 @@
-
+