diff --git a/trytond/doc/ref/models.rst b/trytond/doc/ref/models.rst
index 03dd798186..d22a2fd918 100644
--- a/trytond/doc/ref/models.rst
+++ b/trytond/doc/ref/models.rst
@@ -1286,9 +1286,23 @@ ChatMixin
A mixin_ to activate a `model-ir.chat.channel` on any
:class:`~trytond.model.ModelStorage` record.
+Class methods:
+
+.. classmethod:: ChatMixin.chat_post(records, message_id[, audience[, n[, \*\*variables]]])
+
+ Posts a message to the `model-ir.chat.channel` of the ``records`` using the
+ XML ID of a `model-ir.message` formatted using
+ :func:`~trytond.i18n.gettext` or :func:`~trytond.i18n.ngettext` if ``n`` is
+ set.
+
+Instance methods:
+
+.. method:: ChatMixin.chat_language([audience])
+
+ Returns the language to use to translate the message posted by
+ :meth:`~ChatMixin.chat_post`.
+
.. _mixin: http://en.wikipedia.org/wiki/Mixin
.. _JSON: http://en.wikipedia.org/wiki/Json
.. _UNION: http://en.wikipedia.org/wiki/Union_(SQL)#UNION_operator
-
-
diff --git a/trytond/trytond/ir/chat.py b/trytond/trytond/ir/chat.py
index 3f49d8f847..93563da118 100644
--- a/trytond/trytond/ir/chat.py
+++ b/trytond/trytond/ir/chat.py
@@ -1,6 +1,8 @@
# This file is part of Tryton. The COPYRIGHT file at the top level of
# this repository contains the full copyright notices and license terms.
+import json
+
from sql import Null
from sql.conditionals import NullIf
@@ -10,6 +12,7 @@ from trytond.model import ChatMixin, Check, ModelSQL, Unique, fields
from trytond.model.exceptions import ValidationError
from trytond.pool import Pool
from trytond.rpc import RPC
+from trytond.tools import firstline
from trytond.tools.email_ import (
EmailNotValidError, normalize_email, validate_email)
from trytond.transaction import Transaction
@@ -114,7 +117,9 @@ class Channel(ModelSQL):
pool = Pool()
Message = pool.get('ir.chat.message')
User = pool.get('res.user')
- user = User(Transaction().user)
+ transaction = Transaction()
+ user = User(transaction.user)
+ ctx_user = User(transaction.context.get('user', user))
channel = cls._get_channel(resource)
message = Message(
channel=channel,
@@ -129,7 +134,8 @@ class Channel(ModelSQL):
'message': message.as_dict(),
})
for follower in channel.followers:
- follower.notify(message)
+ if follower.user != ctx_user:
+ follower.notify(message)
return message
@@ -190,6 +196,8 @@ class AuthorMixin:
@fields.depends('user', 'email')
def on_change_with_author(self, name=None):
if self.user:
+ if not self.user.id:
+ return
return self.user.name
elif self.email:
return self.email
@@ -253,7 +261,18 @@ class Follower(AuthorMixin, ModelSQL):
]))
def notify(self, message):
- pass
+ pool = Pool()
+ Notification = pool.get('res.notification')
+
+ if self.user:
+ Notification(
+ user=self.user,
+ label=message.author,
+ description=firstline(message.content),
+ icon='tryton-chat',
+ model=message.channel.resource.__name__,
+ records=json.dumps([message.channel.resource.id])
+ ).save()
class Message(AuthorMixin, ModelSQL):
diff --git a/trytond/trytond/model/chat.py b/trytond/trytond/model/chat.py
index 069f2de7c6..717060e6cc 100644
--- a/trytond/trytond/model/chat.py
+++ b/trytond/trytond/model/chat.py
@@ -1,6 +1,53 @@
# This file is part of Tryton. The COPYRIGHT file at the top level of
# this repository contains the full copyright notices and license terms.
+from collections import namedtuple
+from itertools import groupby
+
+from trytond.i18n import gettext, ngettext
+from trytond.pool import Pool
+from trytond.transaction import Transaction
+
+from .descriptors import dualmethod
+
+ChatMessage = namedtuple(
+ 'ChatMessage',
+ ['msg_id', 'n', 'variables', 'audience'])
+
class ChatMixin:
__slots__ = ()
+
+ def chat_language(self, audience='internal'):
+ return
+
+ @dualmethod
+ def chat_post(
+ cls, records, message_id,
+ audience='internal', n=None, **variables):
+ transaction = Transaction()
+ for language, recs in groupby(
+ records, key=lambda r: r.chat_language(audience)):
+ with transaction.set_context(language=language):
+ cls.__queue__._chat_dispatch(
+ records,
+ ChatMessage(
+ msg_id=message_id,
+ n=n,
+ variables=variables,
+ audience=audience,
+ ))
+
+ @classmethod
+ def _chat_dispatch(cls, records, message):
+ pool = Pool()
+ Channel = pool.get('ir.chat.channel')
+ transaction = Transaction()
+ message = ChatMessage(*message)
+ if message.n is None:
+ msg = gettext(message.msg_id, **message.variables)
+ else:
+ msg = ngettext(message.msg_id, message.n, **message.variables)
+ with transaction.set_user(0):
+ for record in records:
+ Channel.post(record, msg, audience=message.audience)
diff --git a/trytond/trytond/tests/__init__.py b/trytond/trytond/tests/__init__.py
index 9b1c6fb79d..9d9c582389 100644
--- a/trytond/trytond/tests/__init__.py
+++ b/trytond/trytond/tests/__init__.py
@@ -9,14 +9,14 @@ except ImportError:
def register():
from . import (
- access, copy_, export_data, field_binary, field_boolean, field_char,
- field_context, field_date, field_datetime, field_dict, field_float,
- field_fmany2one, field_function, field_integer, field_many2many,
- field_many2one, field_multiselection, field_numeric, field_one2many,
- field_one2one, field_reference, field_selection, field_text,
- field_time, field_timedelta, history, import_data, mixin, model,
- model_log, modelsql, modelstorage, modelview, mptt, multivalue, path,
- resource, rule, tree, trigger, wizard, workflow)
+ access, chat, copy_, export_data, field_binary, field_boolean,
+ field_char, field_context, field_date, field_datetime, field_dict,
+ field_float, field_fmany2one, field_function, field_integer,
+ field_many2many, field_many2one, field_multiselection, field_numeric,
+ field_one2many, field_one2one, field_reference, field_selection,
+ field_text, field_time, field_timedelta, history, import_data, mixin,
+ model, model_log, modelsql, modelstorage, modelview, mptt, multivalue,
+ path, resource, rule, tree, trigger, wizard, workflow)
access.register('tests')
copy_.register('tests')
@@ -53,6 +53,7 @@ def register():
model_log.register('tests')
mptt.register('tests')
multivalue.register('tests')
+ chat.register('tests')
path.register('tests')
resource.register('tests')
rule.register('tests')
diff --git a/trytond/trytond/tests/chat.py b/trytond/trytond/tests/chat.py
new file mode 100644
index 0000000000..88f3aaa433
--- /dev/null
+++ b/trytond/trytond/tests/chat.py
@@ -0,0 +1,15 @@
+# This file is part of Tryton. The COPYRIGHT file at the top level of
+# this repository contains the full copyright notices and license terms.
+
+from trytond.model import ChatMixin, ModelSQL
+from trytond.pool import Pool
+
+
+class ChatRoom(ChatMixin, ModelSQL):
+ __name__ = 'test.chat.room'
+
+
+def register(module):
+ Pool.register(
+ ChatRoom,
+ module=module, type_='model')
diff --git a/trytond/trytond/tests/message.xml b/trytond/trytond/tests/message.xml
index f255184da5..32aab77887 100644
--- a/trytond/trytond/tests/message.xml
+++ b/trytond/trytond/tests/message.xml
@@ -28,5 +28,8 @@ this repository contains the full copyright notices and license terms. -->
Constraint must be checked.
+
+ Chat Message: %(foo)s
+
diff --git a/trytond/trytond/tests/test_chat.py b/trytond/trytond/tests/test_chat.py
new file mode 100644
index 0000000000..ece37c9106
--- /dev/null
+++ b/trytond/trytond/tests/test_chat.py
@@ -0,0 +1,62 @@
+# This file is part of Tryton. The COPYRIGHT file at the top level of
+# this repository contains the full copyright notices and license terms.
+
+from trytond.pool import Pool
+from trytond.transaction import Transaction
+
+from .test_tryton import TestCase, activate_module, with_transaction
+
+
+class NotificationTestCase(TestCase):
+ "Test Notification"
+
+ @classmethod
+ def setUpClass(cls):
+ super().setUpClass()
+ activate_module('tests')
+
+ def run_tasks(self):
+ pool = Pool()
+ Queue = pool.get('ir.queue')
+ transaction = Transaction()
+ while transaction.tasks:
+ task = Queue(transaction.tasks.pop())
+ task.run()
+
+ @with_transaction()
+ def test_chat_post(self):
+ "Test posting on chat"
+ pool = Pool()
+ User = pool.get('res.user')
+ Room = pool.get('test.chat.room')
+ Notification = pool.get('res.notification')
+ Channel = pool.get('ir.chat.channel')
+ Message = pool.get('ir.chat.message')
+
+ alice, = User.create([{
+ 'name': "Alice",
+ 'login': 'alice',
+ }])
+ room = Room()
+ room.save()
+ Channel.subscribe(room, alice.login)
+ channel = Channel._get_channel(room)
+
+ room.chat_post('tests.msg_chat', foo="Bar", audience='public')
+ self.run_tasks()
+
+ message, = Message.search([('channel', '=', channel)])
+ self.assertEqual(message.audience, 'public')
+ self.assertEqual(message.content, "Chat Message: Bar")
+ with Transaction().set_user(alice.id):
+ notification, = Notification.get()
+ self.assertEqual(notification, {
+ 'id': notification['id'],
+ 'label': None,
+ 'description': "Chat Message: Bar",
+ 'icon': 'tryton-chat',
+ 'model': Room.__name__,
+ 'records': [room.id],
+ 'action': None,
+ 'unread': True,
+ })