summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorRobot Odoo <xmo+roboto@odoo.com>2019-02-11 12:57:48 +0100
committerGitHub <noreply@github.com>2019-02-11 12:57:48 +0100
commit47bb7ef7968cb3c5c11f767df06fcf09333a2b13 (patch)
tree1e86b27e45fe2b5803294e886227495fdd68a96b
parent768844040f887868759309ad101fe50191fc949a (diff)
parent975164473ccd51c00eee5917dddbd8be71b5aefc (diff)
[MERGE] mail, hr_holidays, *: add out of office status
*: base, bus, mail_bot, test_mail Before this task, it was not easy to tell when a user is not available (or is on holiday) from conversations, such as from chat windows. With this task, a user can now tell that he is out-of-office from the user menu preferences, by defining an an out-of-office message. This information will be displayed to other users from chat conversations and mention suggestions (i.e. with `@`). Also, the computation of the user `im_status` has been changed when he has an active leave: an out-of-office message can be set on the leave, and this will be displayed on conversations instead of the user's out-of-office message from the user menu preferences. Task: 1856205 closes odoo/odoo#29974
-rw-r--r--addons/bus/models/bus.py13
-rw-r--r--addons/bus/models/res_partner.py46
-rw-r--r--addons/bus/static/src/js/longpolling_bus.js6
-rw-r--r--addons/hr_holidays/models/__init__.py5
-rw-r--r--addons/hr_holidays/models/hr_leave.py10
-rw-r--r--addons/hr_holidays/models/mail_channel.py29
-rw-r--r--addons/hr_holidays/models/res_partner.py22
-rw-r--r--addons/hr_holidays/models/res_users.py29
-rw-r--r--addons/hr_holidays/static/src/xml/discuss.xml9
-rw-r--r--addons/hr_holidays/tests/__init__.py1
-rw-r--r--addons/hr_holidays/tests/test_out_of_office.py96
-rw-r--r--addons/hr_holidays/views/hr_leave_views.xml2
-rw-r--r--addons/mail/__manifest__.py1
-rw-r--r--addons/mail/models/__init__.py1
-rw-r--r--addons/mail/models/ir_http.py15
-rw-r--r--addons/mail/models/mail_channel.py75
-rw-r--r--addons/mail/models/res_partner.py37
-rw-r--r--addons/mail/models/res_users.py5
-rw-r--r--addons/mail/static/src/js/composers/basic_composer.js10
-rw-r--r--addons/mail/static/src/js/discuss.js9
-rw-r--r--addons/mail/static/src/js/models/messages/abstract_message.js8
-rw-r--r--addons/mail/static/src/js/models/messages/message.js10
-rw-r--r--addons/mail/static/src/js/models/threads/abstract_thread.js16
-rw-r--r--addons/mail/static/src/js/models/threads/dm_chat.js49
-rw-r--r--addons/mail/static/src/js/services/mail_manager.js8
-rw-r--r--addons/mail/static/src/js/services/mail_notification_manager.js18
-rw-r--r--addons/mail/static/src/js/services/mail_service.js1
-rw-r--r--addons/mail/static/src/js/services/mail_status_manager.js197
-rw-r--r--addons/mail/static/src/js/services/mail_window_manager.js41
-rw-r--r--addons/mail/static/src/js/thread_windows/abstract_thread_window.js22
-rw-r--r--addons/mail/static/src/js/thread_windows/thread_window.js24
-rw-r--r--addons/mail/static/src/js/user_menu.js22
-rw-r--r--addons/mail/static/src/js/utils.js10
-rw-r--r--addons/mail/static/src/scss/abstract_thread_window.scss20
-rw-r--r--addons/mail/static/src/scss/composer.scss3
-rw-r--r--addons/mail/static/src/scss/discuss.scss5
-rw-r--r--addons/mail/static/src/scss/thread.scss13
-rw-r--r--addons/mail/static/src/xml/composer.xml4
-rw-r--r--addons/mail/static/src/xml/discuss.xml15
-rw-r--r--addons/mail/static/src/xml/thread.xml18
-rw-r--r--addons/mail/static/src/xml/thread_window.xml7
-rw-r--r--addons/mail/static/src/xml/user_menu.xml11
-rw-r--r--addons/mail/static/tests/chatter_tests.js16
-rw-r--r--addons/mail/static/tests/discuss_tests.js12
-rw-r--r--addons/mail/static/tests/helpers/test_utils.js108
-rw-r--r--addons/mail/static/tests/mail_status_manager_tests.js177
-rw-r--r--addons/mail/static/tests/thread_window/basic_thread_window_tests.js63
-rw-r--r--addons/mail/views/mail_templates.xml6
-rw-r--r--addons/mail/views/res_users_views.xml6
-rw-r--r--addons/mail_bot/models/res_partner.py1
-rw-r--r--addons/mail_bot/views/discuss.xml5
-rw-r--r--addons/test_mail/tests/test_mail_channel.py13
-rw-r--r--odoo/addons/base/views/res_users_views.xml2
53 files changed, 1193 insertions, 159 deletions
diff --git a/addons/bus/models/bus.py b/addons/bus/models/bus.py
index 7a7f8b497db..8187e2d43e6 100644
--- a/addons/bus/models/bus.py
+++ b/addons/bus/models/bus.py
@@ -69,7 +69,7 @@ class ImBus(models.Model):
self.sendmany([[channel, message]])
@api.model
- def poll(self, channels, last=0, options=None, force_status=False):
+ def poll(self, channels, last=0, options=None):
if options is None:
options = {}
# first poll return the notification in the 'buffer'
@@ -89,15 +89,6 @@ class ImBus(models.Model):
'channel': json.loads(notif['channel']),
'message': json.loads(notif['message']),
})
-
- if result or force_status:
- partner_ids = options.get('bus_presence_partner_ids')
- if partner_ids:
- partners = self.env['res.partner'].browse(partner_ids)
- result += [{
- 'id': -1,
- 'channel': (self._cr.dbname, 'bus.presence'),
- 'message': {'id': r.id, 'im_status': r.im_status}} for r in partners]
return result
@@ -145,7 +136,7 @@ class ImDispatch(object):
event.wait(timeout=timeout)
with registry.cursor() as cr:
env = api.Environment(cr, SUPERUSER_ID, {})
- notifications = env['bus.bus'].poll(channels, last, options, force_status=True)
+ notifications = env['bus.bus'].poll(channels, last, options)
except Exception:
# timeout
pass
diff --git a/addons/bus/models/res_partner.py b/addons/bus/models/res_partner.py
index a476e0c08dc..3f67d2d1c9f 100644
--- a/addons/bus/models/res_partner.py
+++ b/addons/bus/models/res_partner.py
@@ -15,50 +15,16 @@ class ResPartner(models.Model):
self.env.cr.execute("""
SELECT
U.partner_id as id,
- CASE WHEN age(now() AT TIME ZONE 'UTC', max(B.last_poll)) > interval %s THEN 'offline'
- WHEN age(now() AT TIME ZONE 'UTC', max(B.last_presence)) > interval %s THEN 'away'
- ELSE 'online'
+ CASE WHEN max(B.last_poll) IS NULL THEN 'offline'
+ WHEN age(now() AT TIME ZONE 'UTC', max(B.last_poll)) > interval %s THEN 'offline'
+ WHEN age(now() AT TIME ZONE 'UTC', max(B.last_presence)) > interval %s THEN 'away'
+ ELSE 'online'
END as status
FROM bus_presence B
- JOIN res_users U ON B.user_id = U.id
+ RIGHT JOIN res_users U ON B.user_id = U.id
WHERE U.partner_id IN %s AND U.active = 't'
GROUP BY U.partner_id
""", ("%s seconds" % DISCONNECTION_TIMER, "%s seconds" % AWAY_TIMER, tuple(self.ids)))
res = dict(((status['id'], status['status']) for status in self.env.cr.dictfetchall()))
for partner in self:
- partner.im_status = res.get(partner.id, 'offline')
-
- @api.model
- def im_search(self, name, limit=20):
- """ Search partner with a name and return its id, name and im_status.
- Note : the user must be logged
- :param name : the partner name to search
- :param limit : the limit of result to return
- """
- # This method is supposed to be used only in the context of channel creation or
- # extension via an invite. As both of these actions require the 'create' access
- # right, we check this specific ACL.
- if self.env['mail.channel'].check_access_rights('create', raise_exception=False):
- name = '%' + name + '%'
- excluded_partner_ids = [self.env.user.partner_id.id]
- self.env.cr.execute("""
- SELECT
- U.id as user_id,
- P.id as id,
- P.name as name,
- CASE WHEN B.last_poll IS NULL THEN 'offline'
- WHEN age(now() AT TIME ZONE 'UTC', B.last_poll) > interval %s THEN 'offline'
- WHEN age(now() AT TIME ZONE 'UTC', B.last_presence) > interval %s THEN 'away'
- ELSE 'online'
- END as im_status
- FROM res_users U
- JOIN res_partner P ON P.id = U.partner_id
- LEFT JOIN bus_presence B ON B.user_id = U.id
- WHERE P.name ILIKE %s
- AND P.id NOT IN %s
- AND U.active = 't'
- LIMIT %s
- """, ("%s seconds" % DISCONNECTION_TIMER, "%s seconds" % AWAY_TIMER, name, tuple(excluded_partner_ids), limit))
- return self.env.cr.dictfetchall()
- else:
- return {}
+ partner.im_status = res.get(partner.id, 'im_partner') # if not found, it is a partner, useful to avoid to refresh status in js
diff --git a/addons/bus/static/src/js/longpolling_bus.js b/addons/bus/static/src/js/longpolling_bus.js
index 4764e2e52b0..a62f4897391 100644
--- a/addons/bus/static/src/js/longpolling_bus.js
+++ b/addons/bus/static/src/js/longpolling_bus.js
@@ -37,7 +37,6 @@ var LongpollingBus = Bus.extend(ServicesMixin, {
// bus presence
this._lastPresenceTime = new Date().getTime();
- this._lastPartnersPresenceCheck = this._lastPresenceTime;
$(window).on("focus." + this._id, this._onFocusChange.bind(this, {focus: true}));
$(window).on("blur." + this._id, this._onFocusChange.bind(this, {focus: false}));
$(window).on("unload." + this._id, this._onFocusChange.bind(this, {focus: false}));
@@ -177,11 +176,6 @@ var LongpollingBus = Bus.extend(ServicesMixin, {
var options = _.extend({}, this._options, {
bus_inactivity: now - this._getLastPresence(),
});
- if (this._lastPartnersPresenceCheck + this.PARTNERS_PRESENCE_CHECK_PERIOD > now) {
- options = _.omit(options, 'bus_presence_partner_ids');
- } else {
- this._lastPartnersPresenceCheck = now;
- }
var data = {channels: this._channels, last: this._lastNotificationID, options: options};
// The backend has a maximum cycle time of 50 seconds so give +10 seconds
this._pollRpc = this._rpc({route: this.POLL_ROUTE, params: data}, {shadow : true, timeout: 60000});
diff --git a/addons/hr_holidays/models/__init__.py b/addons/hr_holidays/models/__init__.py
index d391276412b..2da28fe24fa 100644
--- a/addons/hr_holidays/models/__init__.py
+++ b/addons/hr_holidays/models/__init__.py
@@ -6,4 +6,7 @@ from . import hr
from . import hr_leave
from . import hr_leave_allocation
from . import hr_leave_type
-from . import mail_message_subtype \ No newline at end of file
+from . import mail_channel
+from . import mail_message_subtype
+from . import res_partner
+from . import res_users
diff --git a/addons/hr_holidays/models/hr_leave.py b/addons/hr_holidays/models/hr_leave.py
index c475dec01c7..5536a80f1b8 100644
--- a/addons/hr_holidays/models/hr_leave.py
+++ b/addons/hr_holidays/models/hr_leave.py
@@ -9,7 +9,7 @@ import math
from datetime import datetime
from pytz import timezone, UTC
-from odoo import api, fields, models
+from odoo import api, fields, models, tools
from odoo.addons.resource.models.resource import float_to_time, HOURS_PER_DAY
from odoo.exceptions import AccessError, UserError, ValidationError
from odoo.tools import float_compare
@@ -128,6 +128,7 @@ class HolidaysRequest(models.Model):
'hr.department', string='Department', readonly=True,
states={'draft': [('readonly', False)], 'confirm': [('readonly', False)]})
notes = fields.Text('Reasons', readonly=True, states={'draft': [('readonly', False)], 'confirm': [('readonly', False)]})
+ out_of_office_message = fields.Char(string='Out of Office Message')
# duration
date_from = fields.Datetime(
'Start Date', readonly=True, index=True, copy=False, required=True,
@@ -250,6 +251,13 @@ class HolidaysRequest(models.Model):
('duration_check', "CHECK ( number_of_days >= 0 )", "If you want to change the number of days you should use the 'period' mode"),
]
+ @api.model_cr_context
+ def _auto_init(self):
+ res = super(HolidaysRequest, self)._auto_init()
+ tools.create_index(self._cr, 'hr_leave_date_to_date_from_index',
+ self._table, ['date_to', 'date_from'])
+ return res
+
@api.onchange('holiday_status_id')
def _onchange_holiday_status_id(self):
self.request_unit_half = False
diff --git a/addons/hr_holidays/models/mail_channel.py b/addons/hr_holidays/models/mail_channel.py
new file mode 100644
index 00000000000..3d5992dd856
--- /dev/null
+++ b/addons/hr_holidays/models/mail_channel.py
@@ -0,0 +1,29 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+from odoo import api, fields, models
+
+
+class Channel(models.Model):
+ _inherit = 'mail.channel'
+
+ @api.multi
+ def partner_info(self, all_partners, direct_partners):
+ partner_infos = super(Channel, self).partner_info(all_partners, direct_partners)
+ # only search for leave out_of_office_message if im_status is on leave
+ partners_on_leave = [partner_id for partner_id in direct_partners.ids if 'leave' in partner_infos[partner_id]['im_status']]
+ if partners_on_leave:
+ now = fields.Datetime.now()
+ self.env.cr.execute('''SELECT res_users.partner_id as partner_id, hr_leave.out_of_office_message as out_of_office_message, hr_leave.date_to as date_to
+ FROM res_users
+ JOIN hr_leave ON hr_leave.user_id = res_users.id
+ AND hr_leave.state not in ('cancel', 'refuse')
+ AND res_users.active = 't'
+ AND hr_leave.date_from <= %s
+ AND hr_leave.date_to >= %s
+ AND res_users.partner_id in %s''', (now, now, tuple(partners_on_leave)))
+ out_of_office_infos = dict(((res['partner_id'], res) for res in self.env.cr.dictfetchall()))
+ for partner_id, out_of_office_info in out_of_office_infos.items():
+ partner_infos[partner_id]['out_of_office_date_end'] = out_of_office_info['date_to']
+ partner_infos[partner_id]['out_of_office_message'] = out_of_office_info['out_of_office_message']
+ return partner_infos
diff --git a/addons/hr_holidays/models/res_partner.py b/addons/hr_holidays/models/res_partner.py
new file mode 100644
index 00000000000..dde82f5cc9b
--- /dev/null
+++ b/addons/hr_holidays/models/res_partner.py
@@ -0,0 +1,22 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+from odoo import api, models
+
+
+class ResPartner(models.Model):
+ _inherit = 'res.partner'
+
+ def _compute_im_status(self):
+ super(ResPartner, self)._compute_im_status()
+ absent_now = self._get_on_leave_ids()
+ for partner in self:
+ if partner.id in absent_now:
+ if partner.im_status == 'online':
+ partner.im_status = 'leave_online'
+ else:
+ partner.im_status = 'leave_offline'
+
+ @api.model
+ def _get_on_leave_ids(self):
+ return self.env['res.users']._get_on_leave_ids(partner=True)
diff --git a/addons/hr_holidays/models/res_users.py b/addons/hr_holidays/models/res_users.py
new file mode 100644
index 00000000000..3df8f5175f5
--- /dev/null
+++ b/addons/hr_holidays/models/res_users.py
@@ -0,0 +1,29 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+from odoo import api, fields, models
+
+
+class ResUsers(models.Model):
+ _inherit = "res.users"
+
+ def _compute_im_status(self):
+ super(ResUsers, self)._compute_im_status()
+ on_leave_user_ids = self._get_on_leave_ids()
+ for user in self:
+ if user.id in on_leave_user_ids:
+ if user.im_status == 'online':
+ user.im_status = 'leave_online'
+ else:
+ user.im_status = 'leave_offline'
+
+ @api.model
+ def _get_on_leave_ids(self, partner=False):
+ now = fields.Datetime.now()
+ field = 'partner_id' if partner else 'id'
+ self.env.cr.execute('''SELECT res_users.%s FROM res_users
+ JOIN hr_leave ON hr_leave.user_id = res_users.id
+ AND state not in ('cancel', 'refuse')
+ AND res_users.active = 't'
+ AND date_from <= %%s AND date_to >= %%s''' % field, (now, now))
+ return [r[0] for r in self.env.cr.fetchall()]
diff --git a/addons/hr_holidays/static/src/xml/discuss.xml b/addons/hr_holidays/static/src/xml/discuss.xml
new file mode 100644
index 00000000000..33128c54c3f
--- /dev/null
+++ b/addons/hr_holidays/static/src/xml/discuss.xml
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<templates xml:space="preserve">
+ <t t-extend="mail.UserStatus">
+ <t t-jquery="i:last" t-operation="after">
+ <i t-if="status == 'leave_online'" class="o_mail_user_status o_user_online fa fa-plane" title="Online" role="img" aria-label="User is online"/>
+ <i t-if="status == 'leave_offline'" class="o_mail_user_status o_user_idle fa fa-plane" title="Out of office" role="img" aria-label="User is out of office"/>
+ </t>
+ </t>
+</templates>
diff --git a/addons/hr_holidays/tests/__init__.py b/addons/hr_holidays/tests/__init__.py
index 9510f75b34b..67c8327e999 100644
--- a/addons/hr_holidays/tests/__init__.py
+++ b/addons/hr_holidays/tests/__init__.py
@@ -7,3 +7,4 @@ from . import test_hr_leave_type
from . import test_accrual_allocations
from . import test_change_department
from . import test_leave_requests
+from . import test_out_of_office
diff --git a/addons/hr_holidays/tests/test_out_of_office.py b/addons/hr_holidays/tests/test_out_of_office.py
new file mode 100644
index 00000000000..05e3e480cc0
--- /dev/null
+++ b/addons/hr_holidays/tests/test_out_of_office.py
@@ -0,0 +1,96 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+from datetime import datetime
+from dateutil.relativedelta import relativedelta
+
+from odoo.tests.common import tagged, users, warmup
+from odoo.addons.hr_holidays.tests.common import TestHrHolidaysBase
+
+
+@tagged('out_of_office')
+class TestOutOfOffice(TestHrHolidaysBase):
+
+ def test_leave_ooo(self):
+ leave_type = self.env['hr.leave.type'].create({
+ 'name': 'Legal Leaves',
+ 'time_type': 'leave',
+ 'allocation_type': 'no',
+ })
+ self.assertNotEqual(self.employee_hruser.user_id.im_status, 'leave_offline', 'user should not be on leave')
+ self.assertNotEqual(self.employee_hruser.user_id.partner_id.im_status, 'leave_offline', 'user should not be on leave')
+ leave_date_end = (datetime.today() + relativedelta(days=3))
+ leave = self.env['hr.leave'].create({
+ 'name': 'Christmas',
+ 'employee_id': self.employee_hruser.id,
+ 'holiday_status_id': leave_type.id,
+ 'date_from': (datetime.today() - relativedelta(days=1)),
+ 'date_to': leave_date_end,
+ 'out_of_office_message': 'contact tde in case of problems',
+ 'number_of_days': 4,
+ })
+ leave.action_approve()
+ self.assertEqual(self.employee_hruser.user_id.im_status, 'leave_offline', 'user should be out (leave_offline)')
+ self.assertEqual(self.employee_hruser.user_id.partner_id.im_status, 'leave_offline', 'user should be out (leave_offline)')
+
+ partner = self.employee_hruser.user_id.partner_id
+ partner2 = self.user_employee.partner_id
+
+ channel = self.env['mail.channel'].with_context({
+ 'mail_create_nolog': True,
+ 'mail_create_nosubscribe': True,
+ 'mail_channel_noautofollow': True,
+ }).create({
+ 'channel_partner_ids': [(4, partner.id), (4, partner2.id)],
+ 'public': 'private',
+ 'channel_type': 'chat',
+ 'email_send': False,
+ 'name': 'test'
+ })
+ infos = channel.sudo(self.user_employee).channel_info()
+ self.assertEqual(infos[0]['direct_partner'][0]['out_of_office_date_end'], leave_date_end)
+ self.assertEqual(infos[0]['direct_partner'][0]['out_of_office_message'], 'contact tde in case of problems')
+
+
+@tagged('out_of_office')
+class TestOutOfOfficePerformance(TestHrHolidaysBase):
+
+ def setUp(self):
+ super(TestOutOfOfficePerformance, self).setUp()
+ self.leave_type = self.env['hr.leave.type'].create({
+ 'name': 'Legal Leaves',
+ 'time_type': 'leave',
+ 'allocation_type': 'no',
+ })
+ self.leave_date_end = (datetime.today() + relativedelta(days=3))
+ self.leave = self.env['hr.leave'].create({
+ 'name': 'Christmas',
+ 'employee_id': self.employee_hruser_id,
+ 'holiday_status_id': self.leave_type.id,
+ 'date_from': (datetime.today() - relativedelta(days=1)),
+ 'date_to': (datetime.today() + relativedelta(days=3)),
+ 'out_of_office_message': 'contact tde in case of problems',
+ 'number_of_days': 4,
+ })
+
+ self.hr_user = self.employee_hruser.user_id
+ self.hr_partner = self.employee_hruser.user_id.partner_id
+ self.employer_partner = self.user_employee.partner_id
+
+ @users('__system__', 'demo')
+ @warmup
+ def test_leave_im_status_performance_partner_offline(self):
+ with self.assertQueryCount(__system__=2, demo=2):
+ self.assertEqual(self.employer_partner.im_status, 'offline')
+
+ @users('__system__', 'demo')
+ @warmup
+ def test_leave_im_status_performance_user_leave_offline(self):
+ with self.assertQueryCount(__system__=2, demo=2):
+ self.assertEqual(self.hr_user.im_status, 'leave_offline')
+
+ @users('__system__', 'demo')
+ @warmup
+ def test_leave_im_status_performance_partner_leave_offline(self):
+ with self.assertQueryCount(__system__=2, demo=2):
+ self.assertEqual(self.hr_partner.im_status, 'leave_offline')
diff --git a/addons/hr_holidays/views/hr_leave_views.xml b/addons/hr_holidays/views/hr_leave_views.xml
index f655906d0af..c38c2d9f7dd 100644
--- a/addons/hr_holidays/views/hr_leave_views.xml
+++ b/addons/hr_holidays/views/hr_leave_views.xml
@@ -246,7 +246,7 @@
</div>
<field name="name" attrs="{'readonly': [('state', 'not in', ('draft', 'confirm'))]}"/>
-
+ <field name="out_of_office_message"/>
</group>
<group name="col_right">
<field name="employee_id" invisible="1"/>
diff --git a/addons/mail/__manifest__.py b/addons/mail/__manifest__.py
index 81b201b98e9..80e5f0fff28 100644
--- a/addons/mail/__manifest__.py
+++ b/addons/mail/__manifest__.py
@@ -57,6 +57,7 @@
'static/src/xml/abstract_thread_window.xml',
'static/src/xml/thread_window.xml',
'static/src/xml/announcement.xml',
+ 'static/src/xml/user_menu.xml',
'static/src/xml/web_kanban_activity.xml',
],
}
diff --git a/addons/mail/models/__init__.py b/addons/mail/models/__init__.py
index c53d8689082..750446371ee 100644
--- a/addons/mail/models/__init__.py
+++ b/addons/mail/models/__init__.py
@@ -24,5 +24,6 @@ from . import ir_action_act_window
from . import ir_actions
from . import ir_attachment
from . import ir_autovacuum
+from . import ir_http
from . import ir_model
from . import ir_ui_view
diff --git a/addons/mail/models/ir_http.py b/addons/mail/models/ir_http.py
new file mode 100644
index 00000000000..b6aa21809a5
--- /dev/null
+++ b/addons/mail/models/ir_http.py
@@ -0,0 +1,15 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+from odoo import models
+from odoo.http import request
+
+
+class IrHttp(models.AbstractModel):
+ _inherit = 'ir.http'
+
+ def session_info(self):
+ user = request.env.user
+ result = super(IrHttp, self).session_info()
+ result['out_of_office_message'] = user.out_of_office_message
+ return result
diff --git a/addons/mail/models/mail_channel.py b/addons/mail/models/mail_channel.py
index bdc0f8b2160..3a83cd8f9bd 100644
--- a/addons/mail/models/mail_channel.py
+++ b/addons/mail/models/mail_channel.py
@@ -540,21 +540,47 @@ class Channel(models.Model):
notifications.append([channel.uuid, dict(message_values)])
return notifications
+ @api.model
+ def partner_info(self, all_partners, direct_partners):
+ """
+ Return the information needed by channel to display channel members
+ :param all_partners: list of res.parner():
+ :param direct_partners: list of res.parner():
+ :returns: a list of {'id', 'name', 'email'} for each partner and adds {im_status, out_of_office_message} for direct_partners.
+ :rtype : list(dict)
+ """
+ partner_infos = {partner['id']: partner for partner in all_partners.sudo().read(['id', 'name', 'email'])}
+ # add im _status and out_of_office_message for direct_partners
+ direct_partners_im_status = {partner['id']: partner for partner in direct_partners.sudo().read(['im_status'])}
+ partner_infos.update(direct_partners_im_status)
+ for user in self.env['res.users'].search([('partner_id', 'in', direct_partners.ids), ('out_of_office_message', '!=', False)]):
+ partner_infos[user.partner_id.id]['out_of_office_message'] = user.out_of_office_message
+ return partner_infos
+
@api.multi
- def channel_info(self, extra_info = False):
+ def channel_info(self, extra_info=False):
""" Get the informations header for the current channels
:returns a list of channels values
:rtype : list(dict)
"""
+ if not self:
+ return []
channel_infos = []
-
# all relations partner_channel on those channels
all_partner_channel = self.env['mail.channel.partner'].search([('channel_id', 'in', self.ids)])
# all partner infos on those channels
- partner_infos = all_partner_channel.mapped('partner_id').read(['id', 'name', 'email'])
+ channel_dict = {channel.id: channel for channel in self}
+ all_partners = all_partner_channel.mapped('partner_id')
+ direct_channel_partners = all_partner_channel.filtered(lambda pc: channel_dict[pc.channel_id.id].channel_type == 'chat')
+ direct_partners = direct_channel_partners.mapped('partner_id')
+ partner_infos = self.partner_info(all_partners, direct_partners)
+
+ # add last message preview (only used in mobile)
+ addPreview = self._context.get('isMobile', False)
+ if addPreview:
+ channel_previews = {channel_preview['id']: channel_preview for channel_preview in self.channel_fetch_preview()}
- # for each channel, build the information header and include the logged partner information
for channel in self:
info = {
'id': channel.id,
@@ -572,42 +598,39 @@ class Channel(models.Model):
}
if extra_info:
info['info'] = extra_info
- # add the partner for 'direct mesage' channel
- if channel.channel_type == 'chat':
- info['direct_partner'] = (channel.sudo()
- .with_context(active_test=False)
- .channel_partner_ids
- .filtered(lambda p: p.id != self.env.user.partner_id.id)
- .read(['id', 'name', 'im_status']))
# add last message preview (only used in mobile)
- if self._context.get('isMobile', False):
- last_message = channel.channel_fetch_preview()
- if last_message:
- info['last_message'] = last_message[0].get('last_message')
+ if addPreview:
+ if channel in channel_previews:
+ info['last_message'] = channel_previews[channel]
# listeners of the channel
channel_partners = all_partner_channel.filtered(lambda pc: channel.id == pc.channel_id.id)
# find the channel partner state, if logged user
- partner_channel = self.env['mail.channel.partner']
if self.env.user and self.env.user.partner_id:
- partner_channel = channel_partners.filtered(lambda pc: pc.partner_id.id == self.env.user.partner_id.id)
+ # add the partner for 'direct mesage' channel
+ if channel.channel_type == 'chat':
+ # direct_partner should be removed from channel info since we can find it from members and channel_type
+ # we keep it know to avoid change tests and javascript
+ direct_partner = direct_channel_partners.filtered(lambda pc: pc.partner_id.id != self.env.user.partner_id.id)
+ info['direct_partner'] = [partner_infos[direct_partner[0].partner_id.id]]
# add needaction and unread counter, since the user is logged
info['message_needaction_counter'] = channel.message_needaction_counter
info['message_unread_counter'] = channel.message_unread_counter
- # add user session state, if available and if user is logged
- if len(partner_channel.ids):
- partner_channel = partner_channel[0]
- info['state'] = partner_channel.fold_state or 'open'
- info['is_minimized'] = partner_channel.is_minimized
- info['seen_message_id'] = partner_channel.seen_message_id.id
- info['custom_channel_name'] = partner_channel.custom_channel_name
+ # add user session state, if available and if user is logged
+ partner_channel = channel_partners.filtered(lambda pc: pc.partner_id.id == self.env.user.partner_id.id)
+ if partner_channel:
+ partner_channel = partner_channel[0]
+ info['state'] = partner_channel.fold_state or 'open'
+ info['is_minimized'] = partner_channel.is_minimized
+ info['seen_message_id'] = partner_channel.seen_message_id.id
+ info['custom_channel_name'] = partner_channel.custom_channel_name
# add members infos
partner_ids = channel_partners.mapped('partner_id').ids
- info['members'] = [partner_info for partner_info in partner_infos if partner_info['id'] in partner_ids]
+ info['members'] = [partner_infos[partner] for partner in partner_ids]
info['seen_partners_info'] = [{
'partner_id': cp.partner_id.id,
'fetched_message_id': cp.fetched_message_id.id,
@@ -925,6 +948,8 @@ class Channel(models.Model):
@api.multi
def channel_fetch_preview(self):
""" Return the last message of the given channels """
+ if not self:
+ return []
self._cr.execute("""
SELECT mail_channel_id AS id, MAX(mail_message_id) AS message_id
FROM mail_message_mail_channel_rel
diff --git a/addons/mail/models/res_partner.py b/addons/mail/models/res_partner.py
index 04be083d8da..b6f65d79eec 100644
--- a/addons/mail/models/res_partner.py
+++ b/addons/mail/models/res_partner.py
@@ -7,6 +7,8 @@ import threading
from odoo.tools.misc import split_every
from odoo import _, api, fields, models, registry, SUPERUSER_ID
+from odoo.addons.bus.models.bus_presence import AWAY_TIMER
+from odoo.addons.bus.models.bus_presence import DISCONNECTION_TIMER
from odoo.osv import expression
_logger = logging.getLogger(__name__)
@@ -252,3 +254,38 @@ class Partner(models.Model):
partners = [p for p in partners if not len([u for u in users if u['id'] == p['id']])]
return [users, partners]
+
+ @api.model
+ def im_search(self, name, limit=20):
+ """ Search partner with a name and return its id, name and im_status.
+ Note : the user must be logged
+ :param name : the partner name to search
+ :param limit : the limit of result to return
+ """
+ # This method is supposed to be used only in the context of channel creation or
+ # extension via an invite. As both of these actions require the 'create' access
+ # right, we check this specific ACL.
+ if self.env['mail.channel'].check_access_rights('create', raise_exception=False):
+ name = '%' + name + '%'
+ excluded_partner_ids = [self.env.user.partner_id.id]
+ self.env.cr.execute("""
+ SELECT
+ U.id as user_id,
+ P.id as id,
+ P.name as name,
+ CASE WHEN B.last_poll IS NULL THEN 'offline'
+ WHEN age(now() AT TIME ZONE 'UTC', B.last_poll) > interval %s THEN 'offline'
+ WHEN age(now() AT TIME ZONE 'UTC', B.last_presence) > interval %s THEN 'away'
+ ELSE 'online'
+ END as im_status
+ FROM res_users U
+ JOIN res_partner P ON P.id = U.partner_id
+ LEFT JOIN bus_presence B ON B.user_id = U.id
+ WHERE P.name ILIKE %s
+ AND P.id NOT IN %s
+ AND U.active = 't'
+ LIMIT %s
+ """, ("%s seconds" % DISCONNECTION_TIMER, "%s seconds" % AWAY_TIMER, name, tuple(excluded_partner_ids), limit))
+ return self.env.cr.dictfetchall()
+ else:
+ return {}
diff --git a/addons/mail/models/res_users.py b/addons/mail/models/res_users.py
index 9e04c6372e9..6d03343a33d 100644
--- a/addons/mail/models/res_users.py
+++ b/addons/mail/models/res_users.py
@@ -38,6 +38,7 @@ class Users(models.Model):
moderation_channel_ids = fields.Many2many(
'mail.channel', 'mail_channel_moderator_rel',
string='Moderated channels')
+ out_of_office_message = fields.Char(string='Out of Office Message')
@api.depends('moderation_channel_ids.moderation', 'moderation_channel_ids.moderator_ids')
@api.multi
@@ -74,10 +75,10 @@ GROUP BY channel_moderator.res_users_id""", [tuple(self.ids)])
init_res = super(Users, self).__init__(pool, cr)
# duplicate list to avoid modifying the original reference
type(self).SELF_WRITEABLE_FIELDS = list(self.SELF_WRITEABLE_FIELDS)
- type(self).SELF_WRITEABLE_FIELDS.extend(['notification_type'])
+ type(self).SELF_WRITEABLE_FIELDS.extend(['notification_type', 'out_of_office_message'])
# duplicate list to avoid modifying the original reference
type(self).SELF_READABLE_FIELDS = list(self.SELF_READABLE_FIELDS)
- type(self).SELF_READABLE_FIELDS.extend(['notification_type'])
+ type(self).SELF_READABLE_FIELDS.extend(['notification_type', 'out_of_office_message'])
return init_res
@api.model
diff --git a/addons/mail/static/src/js/composers/basic_composer.js b/addons/mail/static/src/js/composers/basic_composer.js
index 8a045ac2d82..58e334808bf 100644
--- a/addons/mail/static/src/js/composers/basic_composer.js
+++ b/addons/mail/static/src/js/composers/basic_composer.js
@@ -371,7 +371,15 @@ var BasicComposer = Widget.extend({
{ limit: limit, search: search }
);
}
- return suggestions;
+ return $.when(suggestions).then(function (suggestions) {
+ //add im_status on suggestions
+ _.each(suggestions, function (suggestionsSet) {
+ _.each(suggestionsSet, function (suggestion) {
+ suggestion.im_status = self.call('mail_service', 'getImStatus', { partnerID: suggestion.id });
+ });
+ });
+ return suggestions;
+ });
});
},
/**
diff --git a/addons/mail/static/src/js/discuss.js b/addons/mail/static/src/js/discuss.js
index 808cd8338ad..073befc5fcd 100644
--- a/addons/mail/static/src/js/discuss.js
+++ b/addons/mail/static/src/js/discuss.js
@@ -56,7 +56,10 @@ var PartnerInviteDialog = Dialog.extend({
allowClear: true,
multiple: true,
formatResult: function (item) {
- var status = QWeb.render('mail.UserStatus', { status: item.im_status });
+ var status = QWeb.render('mail.UserStatus', {
+ status: self.call('mail_service', 'getImStatus', { partnerID: item.id }),
+ partnerID: item.id,
+ });
return $('<span>').text(item.text).prepend(status);
},
query: function (query) {
@@ -992,10 +995,10 @@ var Discuss = AbstractAction.extend({
.on('new_channel', this, this._onNewChannel)
.on('is_thread_bottom_visible', this, this._onIsThreadBottomVisible)
.on('unsubscribe_from_channel', this, this._onChannelLeft)
+ .on('updated_im_status', this, this._onUpdatedImStatus)
.on('update_needaction', this, this._onUpdateNeedaction)
.on('update_starred', this, this._onUpdateStarred)
.on('update_thread_unread_counter', this, this._onUpdateThreadUnreadCounter)
- .on('update_dm_presence', this, this._onUpdateDmPresence)
.on('activity_updated', this, this._onActivityUpdated)
.on('update_moderation_counter', this, this._onUpdateModerationCounter)
.on('update_typing_partners', this, this._onTypingPartnersUpdated)
@@ -1572,7 +1575,7 @@ var Discuss = AbstractAction.extend({
/**
* @private
*/
- _onUpdateDmPresence: function () {
+ _onUpdatedImStatus: function () {
this._throttledUpdateThreads();
},
/**
diff --git a/addons/mail/static/src/js/models/messages/abstract_message.js b/addons/mail/static/src/js/models/messages/abstract_message.js
index c1ae22b9708..bb2a19cc6ac 100644
--- a/addons/mail/static/src/js/models/messages/abstract_message.js
+++ b/addons/mail/static/src/js/models/messages/abstract_message.js
@@ -76,6 +76,14 @@ var AbstractMessage = Class.extend({
return this._serverAuthorID[0];
},
/**
+ * Threads do not have an im status by default
+ *
+ * @return {undefined}
+ */
+ getAuthorImStatus: function () {
+ return undefined;
+ },
+ /**
* Get the relative url of the avatar to display next to the message
*
* @abstract
diff --git a/addons/mail/static/src/js/models/messages/message.js b/addons/mail/static/src/js/models/messages/message.js
index dc98bb7195b..a414a11874b 100644
--- a/addons/mail/static/src/js/models/messages/message.js
+++ b/addons/mail/static/src/js/models/messages/message.js
@@ -66,6 +66,16 @@ var Message = AbstractMessage.extend(Mixins.EventDispatcherMixin, ServicesMixin
this._customerEmailData.push(data);
},
/**
+ * @override
+ * @return {string|undefined}
+ */
+ getAuthorImStatus: function () {
+ if (!this.hasAuthor()) {
+ return undefined;
+ }
+ return this.call('mail_service', 'getImStatus', { partnerID: this.getAuthorID() });
+ },
+ /**
* Get the name of the author of this message
* If there are no author, return "".
*
diff --git a/addons/mail/static/src/js/models/threads/abstract_thread.js b/addons/mail/static/src/js/models/threads/abstract_thread.js
index d6a37967c68..a515c4d2f8a 100644
--- a/addons/mail/static/src/js/models/threads/abstract_thread.js
+++ b/addons/mail/static/src/js/models/threads/abstract_thread.js
@@ -75,6 +75,22 @@ var AbstractThread = Class.extend(Mixins.EventDispatcherMixin, {
return this._name;
},
/**
+ * By default, threads do not have out-of-office feature
+ *
+ * @return {string|undefined}
+ */
+ getOutOfOfficeInfo: function () {
+ return undefined;
+ },
+ /**
+ * By default, threads do not have out-of-office feature
+ *
+ * @return {string|undefined}
+ */
+ getOutOfOfficeMessage: function () {
+ return undefined;
+ },
+ /**
* Get the status of the thread (e.g. 'online', 'offline', etc.)
*
* @returns {string}
diff --git a/addons/mail/static/src/js/models/threads/dm_chat.js b/addons/mail/static/src/js/models/threads/dm_chat.js
index 5e69dbeff8c..fb524515193 100644
--- a/addons/mail/static/src/js/models/threads/dm_chat.js
+++ b/addons/mail/static/src/js/models/threads/dm_chat.js
@@ -3,6 +3,10 @@ odoo.define('mail.model.DMChat', function (require) {
var TwoUserChannel = require('mail.model.TwoUserChannel');
+var core = require('web.core');
+
+var _t = core._t;
+
/**
* Any piece of code in JS that make use of DMs must ideally interact with
* such objects, instead of direct data from the server.
@@ -18,6 +22,8 @@ var DMChat = TwoUserChannel.extend({
* @param {integer} params.data.direct_partner[0].id
* @param {string} params.data.direct_partner[0].im_status
* @param {string} params.data.direct_partner[0].name
+ * @param {string} [params.data.direct_partner[0].out_of_office_message]
+ * @param {string} [params.data.direct_partner[0].out_of_office_date_end]
*/
init: function (params) {
this._super.apply(this, arguments);
@@ -26,8 +32,14 @@ var DMChat = TwoUserChannel.extend({
this._directPartnerID = data.direct_partner[0].id;
this._name = data.custom_channel_name || data.direct_partner[0].name;
- this._status = data.direct_partner[0].im_status;
+ this._outOfOfficeMessage = data.direct_partner[0].out_of_office_message;
+ this._outOfOfficeDateEnd = data.direct_partner[0].out_of_office_date_end;
this._type = 'dm_chat';
+
+ this.call('mail_service', 'updateImStatus', [{
+ id: this._directPartnerID,
+ im_status: data.direct_partner[0].im_status
+ }]);
},
//--------------------------------------------------------------------------
@@ -45,6 +57,34 @@ var DMChat = TwoUserChannel.extend({
return this._directPartnerID;
},
/**
+ * Get the out of office info
+ *
+ * @override {mail.model.AbstractThread}
+ * @returns {string|undefined}
+ */
+ getOutOfOfficeInfo: function () {
+ if (!this._outOfOfficeDateEnd) {
+ return undefined;
+ }
+ var currentDate = new Date();
+ var date = new Date(this._outOfOfficeDateEnd);
+ var options = { day: 'numeric', month: 'short' };
+ if (currentDate.getFullYear() !== date.getFullYear()) {
+ options.year = 'numeric';
+ }
+ var formattedDate = date.toLocaleDateString(window.navigator.language, options);
+ return _.str.sprintf(_t("Out of office until %s"), formattedDate);
+ },
+ /**
+ * Get the out of office message of the thread
+ *
+ * @override {mail.model.AbstractThread}
+ * @returns {string}
+ */
+ getOutOfOfficeMessage: function () {
+ return this._outOfOfficeMessage;
+ },
+ /**
* @override
*/
getPreview: function () {
@@ -53,10 +93,11 @@ var DMChat = TwoUserChannel.extend({
return result;
},
/**
- * @param {string} newStatus
+ * @override
+ * @return {string}
*/
- setStatus: function (newStatus) {
- this._status = newStatus;
+ getStatus: function () {
+ return this.call('mail_service', 'getImStatus', { partnerID: this._directPartnerID });
},
});
diff --git a/addons/mail/static/src/js/services/mail_manager.js b/addons/mail/static/src/js/services/mail_manager.js
index 777df1df4a6..204a88bb8e9 100644
--- a/addons/mail/static/src/js/services/mail_manager.js
+++ b/addons/mail/static/src/js/services/mail_manager.js
@@ -443,10 +443,6 @@ var MailManager = AbstractService.extend({
channel = this._makeChannel(data, options);
if (channel.getType() === 'dm_chat') {
this._pinnedDmPartners.push(channel.getDirectPartnerID());
- this.call('bus_service', 'updateOption',
- 'bus_presence_partner_ids',
- this._pinnedDmPartners
- );
}
this._threads.push(channel);
if (data.last_message) {
@@ -1041,10 +1037,6 @@ var MailManager = AbstractService.extend({
var index = this._pinnedDmPartners.indexOf(channel.getDirectPartnerID());
if (index > -1) {
this._pinnedDmPartners.splice(index, 1);
- this.call('bus_service', 'updateOption',
- 'bus_presence_partner_ids',
- this._pinnedDmPartners
- );
}
}
this._threads = _.without(this._threads, channel);
diff --git a/addons/mail/static/src/js/services/mail_notification_manager.js b/addons/mail/static/src/js/services/mail_notification_manager.js
index b59d7b91434..b77fa3e65fd 100644
--- a/addons/mail/static/src/js/services/mail_notification_manager.js
+++ b/addons/mail/static/src/js/services/mail_notification_manager.js
@@ -494,22 +494,6 @@ MailManager.include({
});
},
/**
- * On receiving an update on user status (e.g. becoming 'online', 'offline',
- * 'idle', etc.).
- *
- * @private
- * @param {Object} data partner infos
- * @param {integer} data.id partner ID
- * @param {string} data.im_status partner new 'im status' (e.g. 'online')
- */
- _handlePresenceNotification: function (data) {
- var dmChat = this.getDMChatFromPartnerID(data.id);
- if (dmChat) {
- dmChat.setStatus(data.im_status);
- this._mailBus.trigger('update_dm_presence', dmChat);
- }
- },
- /**
* @override
* @private
*/
@@ -569,8 +553,6 @@ MailManager.include({
});
} else if (model === 'res.partner') {
self._handlePartnerNotification(notif[1]);
- } else if (model === 'bus.presence') {
- self._handlePresenceNotification(notif[1]);
}
});
},
diff --git a/addons/mail/static/src/js/services/mail_service.js b/addons/mail/static/src/js/services/mail_service.js
index 4f6b0c29dfc..2256e696c2f 100644
--- a/addons/mail/static/src/js/services/mail_service.js
+++ b/addons/mail/static/src/js/services/mail_service.js
@@ -5,6 +5,7 @@ var core = require('web.core');
var MailManager = require('mail.Manager');
+require('mail.Manager.Status');
require('mail.Manager.Notification');
require('mail.Manager.Window');
require('mail.Manager.DocumentThread');
diff --git a/addons/mail/static/src/js/services/mail_status_manager.js b/addons/mail/static/src/js/services/mail_status_manager.js
new file mode 100644
index 00000000000..5ca151d47bf
--- /dev/null
+++ b/addons/mail/static/src/js/services/mail_status_manager.js
@@ -0,0 +1,197 @@
+odoo.define('mail.Manager.Status', function (require) {
+"use strict";
+
+var core = require('web.core');
+var MailManager = require('mail.Manager');
+var mailUtils = require('mail.utils');
+var QWeb = core.qweb;
+
+/**
+ * Mail Manager: IM Status
+ *
+ * This component handles im status of partners, which is useful for DM Chats,
+ * partner mention suggestions, and chatter messages that display the user icon.
+ */
+MailManager.include({
+ _UPDATE_INTERVAL: 50,
+
+ //--------------------------------------------------------------------------
+ // Public
+ //--------------------------------------------------------------------------
+
+ /**
+ * Returns the uin cache im status, and triggers an asynchronous throttled
+ * update of im_status cache for missings
+ *
+ * @param {Object} data
+ * @param {integer} data.partnerID
+ * @return {String}
+ */
+ getImStatus: function (data) {
+ var self = this;
+ var partnerID = data.partnerID;
+ if (!this._imStatus[partnerID]) {
+ // Add to list to call it in next bus update or _fetchMissingImStatus
+ this._imStatus[partnerID] = undefined;
+ // fetch after some time if no other getImStatus occurs
+ mailUtils.clearTimeout(this._fetchStatusTimeout);
+ this._fetchStatusTimeout = mailUtils.setTimeout(function () {
+ self._fetchMissingImStatus();
+ }, 500);
+ }
+ return this._imStatus[partnerID];
+ },
+ /**
+ * Update status manually, to avoid to do a rpc and an asynchronous update
+ * after getImStatus. Can be done by any caller knowing the last im_status
+ * state.
+ *
+ * @param {Object[]} statusList, A list of {id, im_status}
+ */
+ updateImStatus: function (statusList) {
+ var updatedIDs = [];
+ var self = this;
+ _.each(statusList, function (status) {
+ if (self._imStatus[status.id] === status.im_status) {
+ return;
+ }
+ updatedIDs.push(status.id);
+ self._imStatus[status.id] = status.im_status;
+ });
+ if (! _.isEmpty(updatedIDs)) {
+ this._mailBus.trigger('updated_im_status', updatedIDs); // useful for thread window header
+ this._renderImStatus(updatedIDs);
+ }
+ },
+
+ //--------------------------------------------------------------------------
+ // Private
+ //--------------------------------------------------------------------------
+
+ /**
+ * Fetch the list of im_status for partner with id in ids list and triggers
+ * an update.
+ *
+ * @private
+ * @param {Object} data
+ * @param {integer[]} data.partnerIDs
+ * @return {Deferred}
+ */
+ _fetchImStatus: function (data) {
+ var self = this;
+ var partnerIDs = data.partnerIDs;
+ if (_.isEmpty(partnerIDs)) {
+ return $.when();
+ }
+ return this._rpc({
+ model: 'res.partner',
+ method: 'read',
+ args: [partnerIDs, ['id', 'im_status']],
+ }).then( function (results) {
+ self.updateImStatus(results);
+ });
+ },
+ /**
+ * Fetch the list of im_status for partner with an unknown im_status and
+ * triggers an update.
+ *
+ * @private
+ * @return {Deferred}
+ */
+ _fetchMissingImStatus: function () {
+ var missing = [];
+ _.each(this._imStatus, function (value, key) {
+ if (value === undefined) {
+ missing.push(Number(key));
+ }
+ });
+ return this._fetchImStatus({ partnerIDs: missing });
+ },
+ /**
+ * @private
+ * @return {integer[]} a list of partner ids that needs update
+ */
+ _getImStatusToUpdate: function () {
+ var toUpdate = [];
+ _.each(this._imStatus, function (status, key) {
+ //filter on im_partner and bot: useless to update them, status won't change
+ if (['im_partner', 'bot'].indexOf(status) === -1) {
+ toUpdate.push(Number(key));
+ }
+ });
+ return toUpdate;
+ },
+ /**
+ * @override
+ * @private
+ */
+ _initializeInternalState: function () {
+ this._super.apply(this, arguments);
+ this._fetchStatusTimeout = undefined;
+ this._imStatus = {};
+ this._isTabFocused = true;
+ this._updateImStatusLoop();
+ },
+ /**
+ * @override
+ * @private
+ */
+ _listenOnBuses: function () {
+ this._super.apply(this, arguments);
+ $(window).on("focus", this._onWindowFocusChange.bind(this, true));
+ $(window).on("blur", this._onWindowFocusChange.bind(this, false));
+ $(window).on("unload", this._onWindowFocusChange.bind(this, false));
+ },
+ /**
+ * @private
+ * @param {integer[]} updatedIds
+ */
+ _renderImStatus: function (updatedIds) {
+ var self = this;
+ $('.o_updatable_im_status').each(function () {
+ var $this = $(this);
+ var partnerID = $this.data('partner-id');
+ if (partnerID !== undefined && updatedIds.indexOf(partnerID) !== -1) { // todo instead add id on o_updatable_im_status and select only concerned ones
+ var status = QWeb.render('mail.UserStatus', {
+ status: self.getImStatus({ partnerID: partnerID }),
+ partnerID: partnerID,
+ });
+ $this.replaceWith(status);
+ }
+ });
+ },
+ /**
+ * Once initialised, this loop will update the im_status of registered
+ * users.
+ *
+ * @private
+ * @param {integer} [counter=0] The recursion loop counter
+ */
+ _updateImStatusLoop: function (counter) {
+ var self = this;
+ if (!_.isNumber(counter)) {
+ counter = 0;
+ }
+ mailUtils.setTimeout(function () {
+ if (counter >= self._UPDATE_INTERVAL && self._isTabFocused) {
+ self._fetchImStatus({ partnerIDs: self._getImStatusToUpdate() });
+ counter = 0;
+ }
+ self._updateImStatusLoop(counter+1);
+ }, 1000);
+ },
+
+ //--------------------------------------------------------------------------
+ // Handlers
+ //--------------------------------------------------------------------------
+
+ /**
+ * @private
+ * @param {boolean} focused
+ */
+ _onWindowFocusChange: function (focused) {
+ this._isTabFocused = focused;
+ },
+});
+
+});
diff --git a/addons/mail/static/src/js/services/mail_window_manager.js b/addons/mail/static/src/js/services/mail_window_manager.js
index 4971e7f3f4b..e047c71ab89 100644
--- a/addons/mail/static/src/js/services/mail_window_manager.js
+++ b/addons/mail/static/src/js/services/mail_window_manager.js
@@ -334,8 +334,8 @@ MailManager.include({
.on('new_channel', this, this._onNewChannel)
.on('is_thread_bottom_visible', this, this._onIsThreadBottomVisible)
.on('unsubscribe_from_channel', this, this._onUnsubscribeFromChannel)
- .on('update_thread_unread_counter', this, this._onUpdateThreadUnreadCounter)
- .on('update_dm_presence', this, this._onUpdateDmPresence);
+ .on('updated_im_status', this, this._onUpdatedImStatus)
+ .on('update_thread_unread_counter', this, this._onUpdateThreadUnreadCounter);
core.bus.on('resize', this, _.debounce(this._repositionThreadWindows.bind(this), 100));
},
@@ -664,6 +664,28 @@ MailManager.include({
this._closeThreadWindow(channelID);
},
/**
+ * Called when there is a change of the im status of the partner.
+ * The header of the thread window should be updated accordingly,
+ * in order to display the correct new im status of this users.
+ *
+ * @private
+ * @param {integer} partnerID
+ */
+ _onUpdatedImStatus: function (partnerIDs) {
+ var self = this;
+ _.each(partnerIDs, function (partnerID) {
+ var thread = self.getDMChatFromPartnerID(partnerID);
+ if (! thread) {
+ return;
+ }
+ var threadWindow = self._getThreadWindow(thread.getID());
+ if (!threadWindow) {
+ return;
+ }
+ threadWindow.renderHeader();
+ });
+ },
+ /**
* Called when a thread has its unread counter that has changed.
* The unread counter on the thread windows should be updated.
*
@@ -691,21 +713,6 @@ MailManager.include({
}
},
/**
- * Called when there is a change of the im status of the user linked to
- * DMs. The header of the thread window should be updated accordingly,
- * in order to display the correct new im status of this users.
- *
- * @private
- * @param {mail.model.Thread} thread
- */
- _onUpdateDmPresence: function (thread) {
- _.each(this._threadWindows, function (threadWindow) {
- if (thread.getID() === threadWindow.getID()) {
- threadWindow.renderHeader();
- }
- });
- },
- /**
* Called when a message has been updated.
*
* @private
diff --git a/addons/mail/static/src/js/thread_windows/abstract_thread_window.js b/addons/mail/static/src/js/thread_windows/abstract_thread_window.js
index 3e4e2e16325..300bfb11a3e 100644
--- a/addons/mail/static/src/js/thread_windows/abstract_thread_window.js
+++ b/addons/mail/static/src/js/thread_windows/abstract_thread_window.js
@@ -148,6 +148,28 @@ var AbstractThreadWindow = Widget.extend({
return this._thread;
},
/**
+ *Get out of office info
+ *
+ * @returns {string|undefined}
+ */
+ getOutOfOfficeInfo: function () {
+ if (!this.hasThread()) {
+ return undefined;
+ }
+ return this._thread.getOutOfOfficeInfo();
+ },
+ /**
+ * Get out of office user text
+ *
+ * @returns {string|undefined}
+ */
+ getOutOfOfficeMessage: function () {
+ if (!this.hasThread()) {
+ return undefined;
+ }
+ return this._thread.getOutOfOfficeMessage();
+ },
+ /**
* Get the status of the thread, such as the im status of a DM chat
* ('online', 'offline', etc.). If this window has no thread, returns
* `undefined`.
diff --git a/addons/mail/static/src/js/thread_windows/thread_window.js b/addons/mail/static/src/js/thread_windows/thread_window.js
index 331228c91c8..41b0ef6d36c 100644
--- a/addons/mail/static/src/js/thread_windows/thread_window.js
+++ b/addons/mail/static/src/js/thread_windows/thread_window.js
@@ -21,6 +21,7 @@ var ThreadWindow = AbstractThreadWindow.extend({
'click .o_mail_thread': '_onThreadWindowFocus',
'click .o_thread_composer': '_onThreadWindowFocus',
'click .o_thread_window_expand': '_onClickExpand',
+ 'click .o_out_of_office_read_more_less_button': '_onClickOutOfOfficeReadMoreLess',
}),
/**
* Version of thread window that supports {mail.model.Thread}
@@ -80,6 +81,7 @@ var ThreadWindow = AbstractThreadWindow.extend({
self.$input = self.$('.o_composer_text_field');
});
}
+ this._updateOutOfOfficeReadMoreLessButton();
return $.when(superDef, composerDef);
},
@@ -250,6 +252,19 @@ var ThreadWindow = AbstractThreadWindow.extend({
})
.focus();
},
+ /**
+ * @private
+ */
+ _updateOutOfOfficeReadMoreLessButton: function () {
+ var $readMore = this.$('.o_out_of_office_text');
+ var isOverflowing = $readMore.prop('scrollWidth') > $readMore.width();
+ var isOverflowShown = !$readMore.hasClass('o_text_wrap');
+ if (isOverflowing || isOverflowShown) {
+ var $button = this.$('.o_out_of_office_read_more_less_button');
+ $button.show();
+ $button.text(isOverflowing ? _t('Read more') : _t('Read less'));
+ }
+ },
//--------------------------------------------------------------------------
// Handlers
@@ -289,6 +304,15 @@ var ThreadWindow = AbstractThreadWindow.extend({
}
}, 1000, true),
/**
+ * @private
+ * @param {MouseEvent} ev
+ */
+ _onClickOutOfOfficeReadMoreLess: function (ev) {
+ ev.preventDefault();
+ this.$('.o_out_of_office_text').toggleClass('o_text_wrap');
+ this._updateOutOfOfficeReadMoreLessButton();
+ },
+ /**
* @override
* @private
* @param {KeyboardEvent} ev
diff --git a/addons/mail/static/src/js/user_menu.js b/addons/mail/static/src/js/user_menu.js
new file mode 100644
index 00000000000..77235c1f166
--- /dev/null
+++ b/addons/mail/static/src/js/user_menu.js
@@ -0,0 +1,22 @@
+odoo.define('mail.UserMenu', function (require) {
+"use strict";
+
+/**
+ * This file includes the UserMenu widget defined in Community to add or
+ * override actions only available in Enterprise.
+ */
+
+var UserMenu = require('web.UserMenu');
+
+UserMenu.include({
+ /**
+ * @override
+ */
+ init: function () {
+ this._super.apply(this, arguments);
+ var session = this.getSession();
+ this.outOfOfficeMessage = session.out_of_office_message;
+ },
+});
+
+});
diff --git a/addons/mail/static/src/js/utils.js b/addons/mail/static/src/js/utils.js
index 1f31a6ead2f..14ac97def76 100644
--- a/addons/mail/static/src/js/utils.js
+++ b/addons/mail/static/src/js/utils.js
@@ -91,6 +91,14 @@ function timeFromNow(date) {
return date.fromNow();
}
+function o_clearTimeout(id) {
+ return clearTimeout(id);
+}
+
+function o_setTimeout(func, delay) {
+ return setTimeout(func, delay);
+}
+
return {
addLink: addLink,
getTextToHTML: getTextToHTML,
@@ -100,6 +108,8 @@ return {
parseEmail: parseEmail,
stripHTML: stripHTML,
timeFromNow: timeFromNow,
+ clearTimeout: o_clearTimeout,
+ setTimeout: o_setTimeout,
};
});
diff --git a/addons/mail/static/src/scss/abstract_thread_window.scss b/addons/mail/static/src/scss/abstract_thread_window.scss
index b148afa111f..ab6b46d4b9a 100644
--- a/addons/mail/static/src/scss/abstract_thread_window.scss
+++ b/addons/mail/static/src/scss/abstract_thread_window.scss
@@ -79,6 +79,26 @@ $o-mail-thread-window-zindex: $zindex-modal + 1 !default;
}
+ .o_out_of_office {
+ background-color: #9dd0e8;
+ padding:8px;
+
+ .o_out_of_office_info {
+ font-weight: bold;
+ }
+ .o_out_of_office_text {
+ word-wrap: break-word;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ }
+ .o_text_wrap {
+ white-space: nowrap;
+ }
+ .o_out_of_office_read_more_less_button {
+ display: none;
+ }
+ }
+
.o_mail_thread {
flex: 1 1 100%;
overflow: auto;
diff --git a/addons/mail/static/src/scss/composer.scss b/addons/mail/static/src/scss/composer.scss
index 096c527aa16..1f2fffcee60 100644
--- a/addons/mail/static/src/scss/composer.scss
+++ b/addons/mail/static/src/scss/composer.scss
@@ -55,6 +55,9 @@
font-style: italic;
color: gray;
}
+ .fa {
+ font-size: 0.8em;
+ }
}
textarea {
resize: none;
diff --git a/addons/mail/static/src/scss/discuss.scss b/addons/mail/static/src/scss/discuss.scss
index 570dc2dedd6..711180d3544 100644
--- a/addons/mail/static/src/scss/discuss.scss
+++ b/addons/mail/static/src/scss/discuss.scss
@@ -252,6 +252,11 @@
&.o_user_idle {
color: theme-color('warning');
}
+ &.fa-stack {
+ width: 1em;
+ height: 1em;
+ line-height: 1em;
+ }
}
diff --git a/addons/mail/static/src/scss/thread.scss b/addons/mail/static/src/scss/thread.scss
index 7c867896150..84a826922ff 100644
--- a/addons/mail/static/src/scss/thread.scss
+++ b/addons/mail/static/src/scss/thread.scss
@@ -68,6 +68,19 @@
margin-top: 2px;
text-align: center;
font-size: smaller;
+ .o_thread_message_sidebar_image {
+ position: relative;
+ height: $o-mail-thread-avatar-size;
+
+ .o_updatable_im_status {
+ width: $o-mail-thread-avatar-size;
+ }
+ .o_mail_user_status {
+ position: absolute;
+ bottom: 0;
+ right: 0;
+ }
+ }
@include media-breakpoint-down(sm) {
margin-top: 4px;
diff --git a/addons/mail/static/src/xml/composer.xml b/addons/mail/static/src/xml/composer.xml
index ff4680d7851..b9de0194e93 100644
--- a/addons/mail/static/src/xml/composer.xml
+++ b/addons/mail/static/src/xml/composer.xml
@@ -71,6 +71,10 @@
</t>
<t t-name="mail.MentionSuggestions.Partner" t-extend="mail.MentionSuggestions">
<t t-jquery=".o_mention_proposition" t-operation="append">
+ <t t-call="mail.UserStatus">
+ <t t-set="status" t-value="suggestion.im_status"/>
+ <t t-set="partnerID" t-value="suggestion.id"/>
+ </t>
<span class="o_mention_name"><t t-esc="suggestion.name"/></span>
<t t-if="suggestion.email">
<span class="o_mention_info">(<t t-esc="suggestion.email"/>)</span>
diff --git a/addons/mail/static/src/xml/discuss.xml b/addons/mail/static/src/xml/discuss.xml
index 98430c81ce1..10fb71a5110 100644
--- a/addons/mail/static/src/xml/discuss.xml
+++ b/addons/mail/static/src/xml/discuss.xml
@@ -154,6 +154,7 @@
<span t-if="displayStatus">
<t t-call="mail.UserStatus">
<t t-set="status" t-value="channel.getStatus()"/>
+ <t t-set="partnerID" t-value="channel.getDirectPartnerID()"/>
</t>
</span>
<span t-if="displayHash" class="o_mail_hash">#</span>
@@ -176,11 +177,14 @@
<!--
@param {string} status
+ @param {integer|undefined} [partnerID]
-->
<t t-name="mail.UserStatus">
- <i t-if="status == 'online'" class="o_mail_user_status o_user_online fa fa-circle" title="Online" role="img" aria-label="User is online"/>
- <i t-if="status == 'away'" class="o_mail_user_status o_user_idle fa fa-circle" title="Idle" role="img" aria-label="User is idle"/>
- <i t-if="status == 'offline'" class="o_mail_user_status fa fa-circle-o" title="Offline" role="img" aria-label="User is offline"/>
+ <span t-att-class="partnerID ? 'o_updatable_im_status' : ''" t-att-data-partner-id="partnerID">
+ <i t-if="status == 'online'" class="o_mail_user_status o_user_online fa fa-circle" title="Online" role="img" aria-label="User is online"/>
+ <i t-if="status == 'away'" class="o_mail_user_status o_user_idle fa fa-circle" title="Idle" role="img" aria-label="User is idle"/>
+ <i t-if="status == 'offline'" class="o_mail_user_status fa fa-circle" title="Offline" role="img" aria-label="User is offline"/>
+ </span>
</t>
<!--
@@ -300,8 +304,9 @@
t-att-data-preview-id="preview.id" t-att-data-document-id="preview.documentID" t-att-data-document-model="preview.documentModel" t-att-data-unread-counter="preview.unreadCounter" t-att-data-message-ids="preview.messageIDs">
<div t-attf-class="o_mail_preview_image #{preview.documentModel ? 'o_mail_preview_app' : ''}">
<img class="o_mail_preview_image" t-att-src="preview.imageSRC" alt="Preview"/>
- <i t-if="preview.status === 'online'" class="o_mail_user_status o_user_online fa fa-circle" title="Online" role="img" aria-label="User is online"/>
- <i t-if="preview.status === 'away'" class="o_mail_user_status o_user_idle fa fa-circle" title="Idle" role="img" aria-label="User is idle"/>
+ <t t-call="mail.UserStatus">
+ <t t-set="status" t-value="preview.status"/>
+ </t>
</div>
<div class="o_preview_info">
<div class="o_preview_title">
diff --git a/addons/mail/static/src/xml/thread.xml b/addons/mail/static/src/xml/thread.xml
index be73917a1b5..f6d6ad599ec 100644
--- a/addons/mail/static/src/xml/thread.xml
+++ b/addons/mail/static/src/xml/thread.xml
@@ -258,12 +258,18 @@
<div t-att-class="'o_thread_message ' + (message.getID() === options.selectedMessageID ? 'o_thread_selected_message ' : ' ') + (message.isDiscussion() or message.isNotification() ? ' o_mail_discussion ' : ' o_mail_not_discussion ')" t-att-data-message-id="message.getID()">
<div t-if="options.displayAvatars" class="o_thread_message_sidebar">
<t t-if="message.hasAuthor()">
- <img t-if="displayAuthorMessages[message.getID()]"
- alt=""
- t-att-src="message.getAvatarSource()"
- data-oe-model="res.partner"
- t-att-data-oe-id="message.shouldRedirectToAuthor() ? message.getAuthorID() : ''"
- t-attf-class="o_thread_message_avatar rounded-circle #{message.shouldRedirectToAuthor() ? 'o_mail_redirect' : ''}"/>
+ <div t-if="displayAuthorMessages[message.getID()]" class="o_thread_message_sidebar_image">
+ <img
+ alt=""
+ t-att-src="message.getAvatarSource()"
+ data-oe-model="res.partner"
+ t-att-data-oe-id="message.shouldRedirectToAuthor() ? message.getAuthorID() : ''"
+ t-attf-class="o_thread_message_avatar rounded-circle #{message.shouldRedirectToAuthor() ? 'o_mail_redirect' : ''}"/>
+ <t t-call="mail.UserStatus">
+ <t t-set="status" t-value="message.getAuthorImStatus()"/>
+ <t t-set="partnerID" t-value="message.getAuthorID()"/>
+ </t>
+ </div>
</t>
<t t-else="">
<img t-if="displayAuthorMessages[message.getID()]"
diff --git a/addons/mail/static/src/xml/thread_window.xml b/addons/mail/static/src/xml/thread_window.xml
index 4164bc0abc9..76a130d1ba2 100644
--- a/addons/mail/static/src/xml/thread_window.xml
+++ b/addons/mail/static/src/xml/thread_window.xml
@@ -6,6 +6,13 @@
-->
<t t-name="mail.ThreadWindow" t-extend="mail.AbstractThreadWindow">
<t t-jquery=".o_thread_window_header" t-operation="after">
+ <div class="o_out_of_office" t-if="widget.getOutOfOfficeInfo() or widget.getOutOfOfficeMessage()">
+ <div class="o_out_of_office_info" t-if="widget.getOutOfOfficeInfo()" t-esc="widget.getOutOfOfficeInfo()"/>
+ <div class="o_out_of_office_text o_text_wrap" t-if="widget.getOutOfOfficeMessage()">
+ <t t-esc="widget.getOutOfOfficeMessage()"/>
+ <a href="#" class="o_out_of_office_read_more_less_button">Read more</a>
+ </div>
+ </div>
<div t-if="!widget.hasThread()" class="o_thread_search_input">
<span> To: </span>
<input type="text" placeholder="User name"/>
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 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<templates xml:space="preserve">
+
+ <t t-extend="UserMenu">
+ <t t-jquery=".oe_topbar_name" t-operation="before">
+ <t t-if="widget.outOfOfficeMessage">
+ <i class="o_mail_user_status o_user_idle fa fa-minus-circle" title="You have set an out of office message."/>
+ </t>
+ </t>
+ </t>
+</templates>
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');
@@ -80,6 +81,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 @@
<!-- Services -->
<script type="text/javascript" src="/mail/static/src/js/services/mail_manager.js"></script>
<script type="text/javascript" src="/mail/static/src/js/services/mail_document_thread_manager.js"></script>
+ <script type="text/javascript" src="/mail/static/src/js/services/mail_status_manager.js"></script>
<script type="text/javascript" src="/mail/static/src/js/services/mail_notification_manager.js"></script>
<script type="text/javascript" src="/mail/static/src/js/services/mail_window_manager.js"></script>
<script type="text/javascript" src="/mail/static/src/js/services/mail_service.js"></script>
@@ -65,6 +66,7 @@
<script type="text/javascript" src="/mail/static/src/js/systray/systray_activity_menu.js"></script>
<script type="text/javascript" src="/mail/static/src/js/systray/systray_messaging_menu.js"></script>
<script type="text/javascript" src="/mail/static/src/js/tour.js"></script>
+ <script type="text/javascript" src="/mail/static/src/js/user_menu.js"></script>
<!-- tools -->
<script type="text/javascript" src="/mail/static/src/js/tools/debug_manager.js"></script>
<!-- utils -->
@@ -129,8 +131,10 @@
<script type="text/javascript" src="/mail/static/tests/models/cc_throttle_function_tests.js"></script>
<script type="text/javascript" src="/mail/static/tests/models/timer_tests.js"></script>
<script type="text/javascript" src="/mail/static/tests/models/timers_tests.js"></script>
- <!-- activity view -->
+ <!-- activity view -->
<script type="text/javascript" src="/mail/static/tests/activity_tests.js"></script>
+ <!-- services -->
+ <script type="text/javascript" src="/mail/static/tests/mail_status_manager_tests.js"></script>
</xpath>
</template>
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 @@
<field name="email" position="before">
<field name="notification_type" widget="radio" readonly="0"/>
</field>
+ <xpath expr="//group[@name='signature']" position="after">
+ <group string="Out of office" name="out of office">
+ <field name="out_of_office_message" readonly="0"/>
+ </group>
+ </xpath>
</data>
</field>
</record>
@@ -33,6 +38,5 @@
</data>
</field>
</record>
-
</data>
</odoo>
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 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates xml:space="preserve">
- <t t-extend="mail.Preview">
- <t t-jquery="div[t-attf-class*='o_mail_preview_image']" t-operation="append">
- <i t-if="preview.status == 'bot'" class="o_mail_user_status o_user_online fa fa-heart" title="Bot" role="img" aria-label="User is a bot"/>
- </t>
- </t>
<t t-extend="mail.UserStatus">
<t t-jquery="i:last" t-operation="after">
<i t-if="status == 'bot'" class="o_mail_user_status o_user_online fa fa-heart" title="Bot" role="img" aria-label="User is a bot"/>
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 @@
</group>
<group name="preference_contact"></group>
</group>
- <group>
+ <group name="signature">
<field name="signature" readonly="0"/>
</group>
<footer>