Notify subscribed users of new message in chat

This commit is contained in:
Nicolas Évrard committed 2025-12-02 18:11:10 +01:00
1 parent 48dcd6ffdd
commit fa19c6226f
7 files changed
+174 -13

No files matched your search

+16 -2
View File
@@ -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
+22 -3
View File
@@ -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):
+47
View File
@@ -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)
+9 -8
View File
@@ -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')
+15
View File
@@ -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')
+3
View File
@@ -28,5 +28,8 @@ this repository contains the full copyright notices and license terms. -->
<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>
+62
View File
@@ -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,
})