From 41bc4430aa45dce54b8388f74e6f624196755ae4 Mon Sep 17 00:00:00 2001 From: ommo73 Date: Tue, 19 Mar 2019 19:08:21 +0500 Subject: [PATCH] :zap: mail_private New: added ability to select channels for private message sending --- mail_private/__init__.py | 1 + mail_private/__manifest__.py | 10 +- mail_private/doc/changelog.rst | 6 + mail_private/full_composer_wizard.xml | 4 + mail_private/models.py | 223 ++++++++++++++++++- mail_private/static/src/js/mail_private.js | 133 ++++++++++- mail_private/static/src/xml/mail_private.xml | 16 ++ mail_private/template.xml | 2 + 8 files changed, 389 insertions(+), 6 deletions(-) diff --git a/mail_private/__init__.py b/mail_private/__init__.py index cde864ba..aa407583 100644 --- a/mail_private/__init__.py +++ b/mail_private/__init__.py @@ -1,3 +1,4 @@ # -*- coding: utf-8 -*- +# License LGPL-3.0 (https://www.gnu.org/licenses/lgpl.html) from . import models diff --git a/mail_private/__manifest__.py b/mail_private/__manifest__.py index 6c0e4211..36720c37 100644 --- a/mail_private/__manifest__.py +++ b/mail_private/__manifest__.py @@ -1,10 +1,18 @@ # -*- coding: utf-8 -*- +# Copyright 2016 x620 +# Copyright 2016 Ilmir Karamov +# Copyright 2016 Ivan Yelizariev +# Copyright 2017 Artyom Losev +# Copyright 2018 Ruslan Ronzhin +# Copyright 2018 Kolushov Alexandr +# Copyright 2019 Artem Rafailov +# License LGPL-3.0 (https://www.gnu.org/licenses/lgpl.html). { "name": """Internal Messaging""", "summary": """Send private messages to specified recipients, regardless of who are in followers list.""", "category": "Discuss", "images": ['images/mail_private_image.png'], - "version": "10.0.1.0.1", + "version": "10.0.1.1.0", "application": False, "author": "IT-Projects LLC, Pavel Romanchenko", diff --git a/mail_private/doc/changelog.rst b/mail_private/doc/changelog.rst index f3b7c081..01b747ca 100644 --- a/mail_private/doc/changelog.rst +++ b/mail_private/doc/changelog.rst @@ -1,3 +1,9 @@ +`1.1.0` +------- + +- **New**: added ability to select channels for private message sending. +- **New**: internal users are flagged automatically. + `1.0.1` ------- diff --git a/mail_private/full_composer_wizard.xml b/mail_private/full_composer_wizard.xml index 9f57a207..46daeb59 100644 --- a/mail_private/full_composer_wizard.xml +++ b/mail_private/full_composer_wizard.xml @@ -1,4 +1,8 @@ + + diff --git a/mail_private/models.py b/mail_private/models.py index 1f69756d..303e7cc6 100644 --- a/mail_private/models.py +++ b/mail_private/models.py @@ -1,5 +1,11 @@ # -*- coding: utf-8 -*- -from odoo import models, fields, api +# Copyright 2016 x620 +# Copyright 2016 manawi +# Copyright 2017 Artyom Losev +# Copyright 2019 Artem Rafailov +# License LGPL-3.0 (https://www.gnu.org/licenses/lgpl.html). + +from odoo import models, fields, api, exceptions, _, tools class MailComposeMessage(models.TransientModel): @@ -7,8 +13,223 @@ class MailComposeMessage(models.TransientModel): is_private = fields.Boolean(string='Send Internal Message') + def get_internal_users_ids(self): + internal_users_ids = self.env['res.users'].search([('share', '=', False)]).ids + return internal_users_ids + @api.multi def send_mail(self, auto_commit=False): for w in self: w.is_log = True if w.is_private else w.is_log super(MailComposeMessage, self).send_mail(auto_commit=False) + + +class MailMessage(models.Model): + _inherit = 'mail.message' + + @api.multi + def _notify(self, force_send=False, send_after_commit=True, user_signature=True): + self_sudo = self.sudo() + if 'is_private' not in self_sudo._context or not self_sudo._context['is_private']: + super(MailMessage, self)._notify(force_send, send_after_commit, user_signature) + else: + self._notify_mail_private(force_send, send_after_commit, user_signature) + + @api.multi + def _notify_mail_private(self, force_send=False, send_after_commit=True, user_signature=True): + """ The method was partially copied from Odoo. + In the current method, the way of getting channels for a private message is changed. + """ + # have a sudoed copy to manipulate partners (public can go here with + # website modules like forum / blog / ... + + # TDE CHECK: add partners / channels as arguments to be able to notify a message with / without computation ?? + self.ensure_one() # tde: not sure, just for testinh, will see + partners = self.env['res.partner'] | self.partner_ids + channels = self.env['mail.channel'] | self.channel_ids + + # update message, with maybe custom values + message_values = { + 'channel_ids': [(6, 0, channels.ids)], + 'needaction_partner_ids': [(6, 0, partners.ids)] + } + if self.model and self.res_id and hasattr(self.env[self.model], 'message_get_message_notify_values'): + message_values.update( + self.env[self.model].browse(self.res_id).message_get_message_notify_values(self, message_values)) + self.write(message_values) + + # notify partners and channels + partners._notify(self, force_send=force_send, send_after_commit=send_after_commit, + user_signature=user_signature) + channels._notify(self) + + # Discard cache, because child / parent allow reading and therefore + # change access rights. + if self.parent_id: + self.parent_id.invalidate_cache() + + return True + + +class MailThread(models.AbstractModel): + _inherit = 'mail.thread' + + @api.multi + @api.returns('self', lambda value: value.id) + def message_post(self, body='', subject=None, message_type='notification', + subtype=None, parent_id=False, attachments=None, + content_subtype='html', **kwargs): + """ Post a new message in an existing thread, returning the new + mail.message ID. + :param int thread_id: thread ID to post into, or list with one ID; + if False/0, mail.message model will also be set as False + :param str body: body of the message, usually raw HTML that will + be sanitized + :param str type: see mail_message.type field + :param str content_subtype:: if plaintext: convert body into html + :param int parent_id: handle reply to a previous message by adding the + parent partners to the message in case of private discussion + :param tuple(str,str) attachments or list id: list of attachment tuples in the form + ``(name,content)``, where content is NOT base64 encoded + Extra keyword arguments will be used as default column values for the + new mail.message record. Special cases: + - attachment_ids: supposed not attached to any document; attach them + to the related document. Should only be set by Chatter. + :return int: ID of newly created mail.message + """ + if attachments is None: + attachments = {} + if self.ids and not self.ensure_one(): + raise exceptions.Warning( + _('Invalid record set: should be called as model (without records) or on single-record recordset')) + + # if we're processing a message directly coming from the gateway, the destination model was + # set in the context. + model = False + if self.ids: + self.ensure_one() + model = self._context.get('thread_model', False) if self._name == 'mail.thread' else self._name + if model and model != self._name and hasattr(self.env[model], 'message_post'): + RecordModel = self.env[model].with_context(thread_model=None) # TDE: was removing the key ? + return RecordModel.browse(self.ids).message_post( + body=body, subject=subject, message_type=message_type, + subtype=subtype, parent_id=parent_id, attachments=attachments, + content_subtype=content_subtype, **kwargs) + + # 0: Find the message's author, because we need it for private discussion + author_id = kwargs.get('author_id') + if author_id is None: # keep False values + author_id = self.env['mail.message']._get_default_author().id + + # 1: Handle content subtype: if plaintext, converto into HTML + if content_subtype == 'plaintext': + body = tools.plaintext2html(body) + + # 2: Private message: add recipients (recipients and author of parent message) - current author + # + legacy-code management (! we manage only 4 and 6 commands) + partner_ids = set() + kwargs_partner_ids = kwargs.pop('partner_ids', []) + for partner_id in kwargs_partner_ids: + if isinstance(partner_id, (list, tuple)) and partner_id[0] == 4 and len(partner_id) == 2: + partner_ids.add(partner_id[1]) + if isinstance(partner_id, (list, tuple)) and partner_id[0] == 6 and len(partner_id) == 3: + partner_ids |= set(partner_id[2]) + elif isinstance(partner_id, (int, long)): + partner_ids.add(partner_id) + else: + pass # we do not manage anything else + if parent_id and not model: + parent_message = self.env['mail.message'].browse(parent_id) + private_followers = set([partner.id for partner in parent_message.partner_ids]) + if parent_message.author_id: + private_followers.add(parent_message.author_id.id) + private_followers -= set([author_id]) + partner_ids |= private_followers + + # 4: mail.message.subtype + subtype_id = kwargs.get('subtype_id', False) + if not subtype_id: + subtype = subtype or 'mt_note' + if '.' not in subtype: + subtype = 'mail.%s' % subtype + subtype_id = self.env['ir.model.data'].xmlid_to_res_id(subtype) + + # automatically subscribe recipients if asked to + if self._context.get('mail_post_autofollow') and self.ids and partner_ids: + partner_to_subscribe = partner_ids + if self._context.get('mail_post_autofollow_partner_ids'): + partner_to_subscribe = filter( + lambda item: item in self._context.get('mail_post_autofollow_partner_ids'), partner_ids) + self.message_subscribe(list(partner_to_subscribe), force=False) + + # _mail_flat_thread: automatically set free messages to the first posted message + MailMessage = self.env['mail.message'] + if self._mail_flat_thread and model and not parent_id and self.ids: + messages = MailMessage.search( + ['&', ('res_id', '=', self.ids[0]), ('model', '=', model), ('message_type', '=', 'email')], + order="id ASC", limit=1) + if not messages: + messages = MailMessage.search(['&', ('res_id', '=', self.ids[0]), ('model', '=', model)], + order="id ASC", limit=1) + parent_id = messages and messages[0].id or False + # we want to set a parent: force to set the parent_id to the oldest ancestor, to avoid having more than 1 level of thread + elif parent_id: + messages = MailMessage.sudo().search([('id', '=', parent_id), ('parent_id', '!=', False)], limit=1) + # avoid loops when finding ancestors + processed_list = [] + if messages: + message = messages[0] + while (message.parent_id and message.parent_id.id not in processed_list): + processed_list.append(message.parent_id.id) + message = message.parent_id + parent_id = message.id + values = kwargs + if 'channel_ids' in values: + values.update({ + 'author_id': author_id, + 'model': model, + 'res_id': model and self.ids[0] or False, + 'body': body, + 'subject': subject or False, + 'message_type': message_type, + 'parent_id': parent_id, + 'subtype_id': subtype_id, + 'partner_ids': [(4, pid) for pid in partner_ids], + 'channel_ids': [(4, pid) for pid in kwargs['channel_ids']] + }) + else: + values.update({ + 'author_id': author_id, + 'model': model, + 'res_id': model and self.ids[0] or False, + 'body': body, + 'subject': subject or False, + 'message_type': message_type, + 'parent_id': parent_id, + 'subtype_id': subtype_id, + 'partner_ids': [(4, pid) for pid in partner_ids], + }) + + # 3. Attachments + # - HACK TDE FIXME: Chatter: attachments linked to the document (not done JS-side), load the message + attachment_ids = self._message_post_process_attachments(attachments, kwargs.pop('attachment_ids', []), values) + values['attachment_ids'] = attachment_ids + + # Avoid warnings about non-existing fields + for x in ('from', 'to', 'cc'): + values.pop(x, None) + + # Post the message + new_message = MailMessage.create(values) + # Post-process: subscribe author, update message_last_post + # Note: the message_last_post mechanism is no longer used. This + # will be removed in a later version. + if (self._context.get('mail_save_message_last_post') and model and model != 'mail.thread' and self.ids and subtype_id): + subtype_rec = self.env['mail.message.subtype'].sudo().browse(subtype_id) + if not subtype_rec.internal: + # done with SUPERUSER_ID, because on some models users can post only with read access, not necessarily write access + self.sudo().write({'message_last_post': fields.Datetime.now()}) + if new_message.author_id and model and self.ids and message_type != 'notification' and not self._context.get( + 'mail_create_nosubscribe'): + self.message_subscribe([new_message.author_id.id], force=False) + return new_message diff --git a/mail_private/static/src/js/mail_private.js b/mail_private/static/src/js/mail_private.js index d311b3cd..f66e3d16 100644 --- a/mail_private/static/src/js/mail_private.js +++ b/mail_private/static/src/js/mail_private.js @@ -1,3 +1,9 @@ +/* Copyright 2016 x620 + Copyright 2016 Ivan Yelizariev + Copyright 2016 manawi + Copyright 2017 Artyom Losev + Copyright 2019 Artem Rafailov + License LGPL-3.0 (https://www.gnu.org/licenses/lgpl.html). */ odoo.define('mail_private', function (require) { 'use strict'; @@ -33,14 +39,21 @@ Chatter.include({ }).fail(function () { // todo: display notification }); - }, + }, on_open_composer_private_message: function (event) { var self = this; this.private = true; + + this.get_recipients_for_internal_message().then(function (data) { self.recipients_for_internal_message = data; - self.open_composer({is_private: true}); + return self.get_channels_for_internal_message(); + }).then(function (data) { + self.channels_for_internal_message = data; + self.get_internal_users_ids().then(function(res_ids){ + self.open_composer({is_private: true, internal_ids: res_ids}); + }); }); }, @@ -52,13 +65,14 @@ Chatter.include({ open_composer: function (options) { var self = this; this._super.apply(this, arguments); - if (options && options.is_private) { + if (options && options.is_private) { + self.internal_users_ids = options.internal_ids this.composer.options.is_private = options.is_private; _.each(self.recipients_for_internal_message, function (partner) { self.composer.suggested_partners.push({ - checked: (partner.user_ids.length > 0), + checked: (self.internal_users_ids.indexOf(partner.user_ids[0]) !== -1), partner_id: partner.id, full_name: partner.name, name: partner.name, @@ -68,7 +82,17 @@ Chatter.include({ :'Follower' }); }); + + _.each(self.channels_for_internal_message, function (channel) { + self.composer.suggested_channels.push({ + checked: true, + channel_id: channel.id, + full_name: channel.name, + name: ('# ' + channel.name), + }); + }); } + }, get_recipients_for_internal_message: function () { @@ -101,6 +125,46 @@ Chatter.include({ }); }); }); + }, + + get_channels_for_internal_message: function () { + var self = this; + self.result = {}; + return new Model(this.context.default_model).query( + ['message_follower_ids', 'partner_id']).filter( + [['id', '=', self.context.default_res_id]]).all() + .then(function (thread) { + var follower_ids = thread[0].message_follower_ids; + self.result[self.context.default_res_id] = []; + self.customer = thread[0].partner_id; + + // Fetch partner ids + return new Model('mail.followers').call( + 'read', [follower_ids, ['channel_id']]).then(function (res_partners) { + // Filter result and push to array + var res_partners_filtered = _.map(res_partners, function (partner) { + if (partner.channel_id[0]) { + return partner.channel_id[0]; + } + }).filter(function (partner) { + return typeof partner !== 'undefined'; + }); + + return new Model('mail.channel').call( + 'read', [res_partners_filtered, ['name', 'id']] + ).then(function (recipients) { + return recipients; + }); + }); + }); + }, + + get_internal_users_ids: function () { + var ResUser = new Model('mail.compose.message'); + this.users_ids = ResUser.call('get_internal_users_ids', [[]]).then( function (users_ids) { + return users_ids; + }); + return this.users_ids; } }); @@ -108,6 +172,7 @@ MailComposer.include({ init: function (parent, dataset, options) { this._super(parent, dataset, options); this.events['click .oe_composer_uncheck'] = 'on_uncheck_recipients'; + this.suggested_channels = []; }, @@ -115,6 +180,52 @@ MailComposer.include({ this.$('.o_composer_suggested_partners input:checked').each(function() { $(this).prop('checked', false); }); + this.$('.o_composer_suggested_channels input:checked').each(function() { + $(this).prop('checked', false); + }); + }, + + preprocess_message: function () { + var self = this; + var def = $.Deferred(); + this._super().then(function (message) { + message = _.extend(message, { + subtype: 'mail.mt_comment', + message_type: 'comment', + content_subtype: 'html', + context: self.context, + }); + + // Subtype + if (self.options.is_log) { + message.subtype = 'mail.mt_note'; + } + + // for module mail_private + if (self.options.is_private) { + message.context.is_private = true; + message.context.channel_ids = self.get_checked_channels_ids(); + message.channel_ids = self.get_checked_channels_ids(); + } + + // Partner_ids + if (self.options.is_log) { + def.resolve(message); + } else { + var checked_suggested_partners = self.get_checked_suggested_partners(); + self.check_suggested_partners(checked_suggested_partners).done(function (partner_ids) { + message.partner_ids = (message.partner_ids || []).concat(partner_ids); + // update context + message.context = _.defaults({}, message.context, { + mail_post_autofollow: true, + }); + def.resolve(message); + }); + } + + }); + + return def; }, on_open_full_composer: function() { @@ -177,6 +288,20 @@ MailComposer.include({ return checked_partners; }, + get_checked_channels_ids: function () { + var self = this; + var checked_channels = []; + this.$('.o_composer_suggested_channels input:checked').each(function() { + var full_name = $(this).data('fullname'); + checked_channels = checked_channels.concat(_.filter(self.suggested_channels, function(item) { + if (full_name === item.full_name) { + checked_channels.push(item.channel_id); + } + })); + }); + return checked_channels; + }, + }); }); diff --git a/mail_private/static/src/xml/mail_private.xml b/mail_private/static/src/xml/mail_private.xml index 5607c9e9..cebfd85c 100644 --- a/mail_private/static/src/xml/mail_private.xml +++ b/mail_private/static/src/xml/mail_private.xml @@ -1,4 +1,10 @@ +