details:   https://code.tryton.org/tryton/commit/481aed0501da
branch:    default
user:      Nicolas Évrard <[email protected]>
date:      Tue Dec 02 18:11:10 2025 +0100
description:
        Notify subscribed users of new message in chat
diffstat:

 trytond/doc/ref/models.rst         |  18 +++++++++-
 trytond/trytond/ir/chat.py         |  25 +++++++++++++-
 trytond/trytond/model/chat.py      |  47 ++++++++++++++++++++++++++++
 trytond/trytond/tests/__init__.py  |  17 +++++----
 trytond/trytond/tests/chat.py      |  15 +++++++++
 trytond/trytond/tests/message.xml  |   3 +
 trytond/trytond/tests/test_chat.py |  62 ++++++++++++++++++++++++++++++++++++++
 7 files changed, 174 insertions(+), 13 deletions(-)

diffs (287 lines):

diff -r 7e674916c6bc -r 481aed0501da trytond/doc/ref/models.rst
--- a/trytond/doc/ref/models.rst        Tue Dec 02 13:08:54 2025 +0100
+++ b/trytond/doc/ref/models.rst        Tue Dec 02 18:11:10 2025 +0100
@@ -1286,9 +1286,23 @@
 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 -r 7e674916c6bc -r 481aed0501da trytond/trytond/ir/chat.py
--- a/trytond/trytond/ir/chat.py        Tue Dec 02 13:08:54 2025 +0100
+++ b/trytond/trytond/ir/chat.py        Tue Dec 02 18:11:10 2025 +0100
@@ -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.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 @@
         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 @@
                 '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 @@
     @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 @@
                     ]))
 
     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 -r 7e674916c6bc -r 481aed0501da trytond/trytond/model/chat.py
--- a/trytond/trytond/model/chat.py     Tue Dec 02 13:08:54 2025 +0100
+++ b/trytond/trytond/model/chat.py     Tue Dec 02 18:11:10 2025 +0100
@@ -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 -r 7e674916c6bc -r 481aed0501da trytond/trytond/tests/__init__.py
--- a/trytond/trytond/tests/__init__.py Tue Dec 02 13:08:54 2025 +0100
+++ b/trytond/trytond/tests/__init__.py Tue Dec 02 18:11:10 2025 +0100
@@ -9,14 +9,14 @@
 
 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 @@
     model_log.register('tests')
     mptt.register('tests')
     multivalue.register('tests')
+    chat.register('tests')
     path.register('tests')
     resource.register('tests')
     rule.register('tests')
diff -r 7e674916c6bc -r 481aed0501da trytond/trytond/tests/chat.py
--- /dev/null   Thu Jan 01 00:00:00 1970 +0000
+++ b/trytond/trytond/tests/chat.py     Tue Dec 02 18:11:10 2025 +0100
@@ -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 -r 7e674916c6bc -r 481aed0501da trytond/trytond/tests/message.xml
--- a/trytond/trytond/tests/message.xml Tue Dec 02 13:08:54 2025 +0100
+++ b/trytond/trytond/tests/message.xml Tue Dec 02 18:11:10 2025 +0100
@@ -28,5 +28,8 @@
         <record model="ir.message" id="msg_binary_required_sql_constraint">
             <field name="text">Constraint must be checked.</field>
         </record>
+        <record model="ir.message" id="msg_chat">
+            <field name="text">Chat Message: %(foo)s</field>
+        </record>
     </data>
 </tryton>
diff -r 7e674916c6bc -r 481aed0501da trytond/trytond/tests/test_chat.py
--- /dev/null   Thu Jan 01 00:00:00 1970 +0000
+++ b/trytond/trytond/tests/test_chat.py        Tue Dec 02 18:11:10 2025 +0100
@@ -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,
+                    })

Reply via email to