diff --git a/mail_tracking/README.rst b/mail_tracking/README.rst index f4ff09fc5..b8ad2e93b 100644 --- a/mail_tracking/README.rst +++ b/mail_tracking/README.rst @@ -52,6 +52,7 @@ status icon will appear just right to name of notified partner. These are all available status icons: +<<<<<<< HEAD .. |sent| image:: mail_tracking/static/src/img/sent.png :width: 10px @@ -68,9 +69,27 @@ These are all available status icons: :width: 10px .. |unknown| image:: mail_tracking/static/src/img/unknown.png +======= +.. |sent| image:: https://raw.githubusercontent.com/OCA/social/11.0/mail_tracking/static/src/img/sent.png :width: 10px -.. |cc| image:: static/src/img/cc.png +.. |delivered| image:: https://raw.githubusercontent.com/OCA/social/11.0/mail_tracking/static/src/img/delivered.png + :width: 15px + +.. |opened| image:: https://raw.githubusercontent.com/OCA/social/11.0/mail_tracking/static/src/img/opened.png + :width: 15px + +.. |error| image:: https://raw.githubusercontent.com/OCA/social/11.0/mail_tracking/static/src/img/error.png + :width: 10px + +.. |waiting| image:: https://raw.githubusercontent.com/OCA/social/11.0/mail_tracking/static/src/img/waiting.png + :width: 10px + +.. |unknown| image:: https://raw.githubusercontent.com/OCA/social/11.0/mail_tracking/static/src/img/unknown.png +>>>>>>> 75b9662... [IMP] mail_tracking: Failed Messages (Discuss & View) + :width: 10px + +.. |cc| image:: https://raw.githubusercontent.com/OCA/social/11.0/mail_tracking/static/src/img/cc.png :width: 10px |unknown| **Unknown**: No email tracking info available. Maybe this notified partner has 'Receive Inbox Notifications by Email' == 'Never' @@ -88,10 +107,33 @@ These are all available status icons: |cc| **Cc**: It's a Carbon-Copy recipient. Can't know the status so is 'Unknown' +<<<<<<< HEAD If you want to see all tracking emails and events you can go to * Settings > Technical > Email > Tracking emails * Settings > Technical > Email > Tracking events +======= +When the message generates and 'error' status, it will apear on discuss 'Failed' +channel. Any view that uses 'mail_thread' widget can show the failed messages +too. + +* Discuss + + .. image:: https://raw.githubusercontent.com/OCA/social/11.0/mail_tracking/static/img/failed_message_discuss.png + +* Chatter + + .. image:: https://raw.githubusercontent.com/OCA/social/11.0/mail_tracking/static/img/failed_message_widget.png + +Known issues / Roadmap +====================== + +* Handle message updates on discuss 'channel_failed' instead of showing the + 'outdated' message. +* Adapt chat_manager changes in v12 +* Adapt discuss changes in v12 +* Add pivot for tracking events and mail trackings +>>>>>>> 75b9662... [IMP] mail_tracking: Failed Messages (Discuss & View) Bug Tracker =========== diff --git a/mail_tracking/__manifest__.py b/mail_tracking/__manifest__.py index 6c4b34265..3a5b3e41d 100644 --- a/mail_tracking/__manifest__.py +++ b/mail_tracking/__manifest__.py @@ -26,10 +26,17 @@ "views/assets.xml", "views/mail_tracking_email_view.xml", "views/mail_tracking_event_view.xml", + "views/mail_message_view.xml", "views/res_partner_view.xml", + "wizard/mail_compose_message_view.xml", ], "qweb": [ "static/src/xml/mail_tracking.xml", + "static/src/xml/failed_message.xml", + "static/src/xml/client_action.xml", + ], + 'demo': [ + 'demo/demo.xml', ], "pre_init_hook": "pre_init_hook", } diff --git a/mail_tracking/controllers/main.py b/mail_tracking/controllers/main.py index a12d25ccf..2f53ecbb7 100644 --- a/mail_tracking/controllers/main.py +++ b/mail_tracking/controllers/main.py @@ -3,7 +3,9 @@ import werkzeug from psycopg2 import OperationalError -from odoo import api, http, registry, SUPERUSER_ID +from odoo import api, http, registry, SUPERUSER_ID, _ +from odoo.addons.mail.controllers.main import MailController +from odoo.http import request import logging import base64 _logger = logging.getLogger(__name__) @@ -37,7 +39,7 @@ def _env_get(db, callback, tracking_id, event_type, **kw): return res -class MailTrackingController(http.Controller): +class MailTrackingController(MailController): def _request_metadata(self): request = http.request.httprequest @@ -85,3 +87,22 @@ class MailTrackingController(http.Controller): response.mimetype = 'image/gif' response.data = base64.b64decode(BLANK) return response + + @http.route() + def mail_client_action(self): + values = super().mail_client_action() + values['channel_slots']['channel_channel'].append({ + 'id': 'channel_failed', + 'name': _("Failed"), + 'uuid': None, + 'state': 'open', + 'is_minimized': False, + 'channel_type': 'static', + 'public': False, + 'mass_mailing': None, + 'group_based_subscription': None, + }) + values.update({ + 'failed_counter': request.env['mail.message'].get_failed_count(), + }) + return values diff --git a/mail_tracking/demo/demo.xml b/mail_tracking/demo/demo.xml new file mode 100644 index 000000000..e3bfd311f --- /dev/null +++ b/mail_tracking/demo/demo.xml @@ -0,0 +1,54 @@ + + + + + + + res.partner + + comment + + 1 + This is a failed message

]]>
+ res1@yourcompany.example.com + + + Failed Message +
+ + + Failed Message + + + res1@yourcompany.example.com + demo@yourcompany.example.com + error + + + + + + res.partner + + comment + + 1 + This is another failed message

]]>
+ res10@yourcompany.example.com + + + Failed Message +
+ + + Failed Message + + + res10@yourcompany.example.com + demo@yourcompany.example.com + error + + + +
+
diff --git a/mail_tracking/models/__init__.py b/mail_tracking/models/__init__.py index 42736005c..aab92b3d9 100644 --- a/mail_tracking/models/__init__.py +++ b/mail_tracking/models/__init__.py @@ -6,5 +6,6 @@ from . import mail_mail from . import mail_message from . import mail_tracking_email from . import mail_tracking_event +from . import mail_composer from . import res_partner from . import mail_thread diff --git a/mail_tracking/models/mail_composer.py b/mail_tracking/models/mail_composer.py new file mode 100644 index 000000000..f4f847e38 --- /dev/null +++ b/mail_tracking/models/mail_composer.py @@ -0,0 +1,30 @@ +# Copyright 2019 Alexandre Díaz +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). + +from odoo import models, fields, api + + +class MailComposer(models.TransientModel): + _inherit = 'mail.compose.message' + + hide_followers = fields.Boolean(string="Hide follower message", + default=False) + + @api.multi + def send_mail(self, auto_commit=False): + """ This method marks as reviewed the message when using the 'Retry' + option in the mail_failed_message widget""" + message = self.env['mail.message'].browse( + self._context.get('message_id')) + if message.exists(): + message.mail_tracking_needs_action = False + return super().send_mail(auto_commit=auto_commit) + + @api.model + def get_record_data(self, values): + values = super(MailComposer, self).get_record_data(values) + if self._context.get('default_hide_followers', False): + values['partner_ids'] = [ + (6, 0, self._context.get('default_partner_ids', list())) + ] + return values diff --git a/mail_tracking/models/mail_message.py b/mail_tracking/models/mail_message.py index 7492021e6..b71ecd17c 100644 --- a/mail_tracking/models/mail_message.py +++ b/mail_tracking/models/mail_message.py @@ -12,6 +12,20 @@ class MailMessage(models.Model): # Recipients email_cc = fields.Char("Cc", help='Additional recipients that receive a ' '"Carbon Copy" of the e-mail') + mail_tracking_ids = fields.One2many( + comodel_name='mail.tracking.email', + inverse_name='mail_message_id', + string="Mail Trackings", + ) + mail_tracking_needs_action = fields.Boolean( + help="The message tracking will be considered" + " to filter tracking issues", + default=False, + ) + + @api.model + def get_failed_states(self): + return {'error', 'rejected', 'spam', 'bounced', 'soft-bounced'} def _tracking_status_map_get(self): return { @@ -95,6 +109,17 @@ class MailMessage(models.Model): }) return res + @api.multi + def _get_failed_message(self): + res = {} + for message in self: + res.update({ + message.id: message.mail_tracking_needs_action + and bool(message.mail_tracking_ids.filtered( + lambda x: x.state in self.get_failed_states())) + }) + return res + @api.model def _message_read_dict_postprocess(self, messages, message_tree): res = super(MailMessage, self)._message_read_dict_postprocess( @@ -103,11 +128,77 @@ class MailMessage(models.Model): mail_messages = self.browse(mail_message_ids) partner_trackings = mail_messages.tracking_status() email_cc = mail_messages._get_email_cc() + failed_message = mail_messages._get_failed_message() for message_dict in messages: mail_message_id = message_dict.get('id', False) if mail_message_id: message_dict.update({ 'partner_trackings': partner_trackings[mail_message_id], 'email_cc': email_cc[mail_message_id], + 'failed_message': failed_message[mail_message_id], }) return res + + @api.model + def _prepare_dict_failed_message(self, message): + failed_trackings = message.mail_tracking_ids.filtered( + lambda x: x.state in self.get_failed_states()) + failed_partners = failed_trackings.mapped('partner_id') + failed_recipients = failed_partners.name_get() + return { + 'id': message.id, + 'date': message.date, + 'author_id': message.author_id.name_get()[0], + 'body': message.body, + 'failed_recipients': failed_recipients, + } + + @api.multi + def get_failed_messages(self): + return [self._prepare_dict_failed_message(msg) for msg in self] + + @api.multi + def toggle_tracking_status(self): + """Toggle message tracking action needed to ignore them in the tracking + issues filter""" + self.mail_tracking_needs_action = not self.mail_tracking_needs_action + return self.mail_tracking_needs_action + + def _get_failed_message_domain(self): + return [ + ('mail_tracking_ids.state', 'in', list(self.get_failed_states())), + ('mail_tracking_needs_action', '=', True) + ] + + @api.model + def get_failed_count(self): + """ Gets the number of failed messages """ + return self.search_count(self._get_failed_message_domain()) + + @api.model + def message_fetch(self, domain, limit=20): + # HACK: Because can't modify the domain in discuss JS to search the + # failed messages we force the change here to clean it of + # not valid criterias + if self.env.context.get('filter_failed_message'): + domain = self._get_failed_message_domain() + return super().message_fetch(domain, limit=limit) + + @api.multi + def _notify(self, force_send=False, send_after_commit=True, + user_signature=True): + self_sudo = self.sudo() + hide_followers = self_sudo._context.get('default_hide_followers', + False) + if hide_followers: + # HACK: Because Odoo uses subtype to found message followers + # whe modify it to False to avoid include them. + orig_subtype_id = self_sudo.subtype_id + self_sudo.subtype_id = False + res = super()._notify(force_send=force_send, + send_after_commit=send_after_commit, + user_signature=user_signature) + if hide_followers: + self_sudo.subtype_id = orig_subtype_id + + return res diff --git a/mail_tracking/models/mail_thread.py b/mail_tracking/models/mail_thread.py index 186b534d0..920fa172b 100644 --- a/mail_tracking/models/mail_thread.py +++ b/mail_tracking/models/mail_thread.py @@ -1,14 +1,22 @@ # Copyright 2019 Alexandre Díaz # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). -from odoo import models, api, _ +from odoo import fields, models, api, _ from email.utils import getaddresses from odoo.tools import email_split_and_format +from lxml import etree class MailThread(models.AbstractModel): _inherit = "mail.thread" + failed_message_ids = fields.One2many( + 'mail.message', 'res_id', string='Failed Messages', + domain=lambda self: + [('model', '=', self._name)] + + self.env['mail.message']._get_failed_message_domain(), + auto_join=True) + @api.multi @api.returns('self', lambda value: value.id) def message_post(self, *args, **kwargs): @@ -22,6 +30,8 @@ class MailThread(models.AbstractModel): @api.multi def message_get_suggested_recipients(self): + """Adds email Cc recipients as suggested recipients. + If the recipient have an res.partner uses it.""" res = super().message_get_suggested_recipients() ResPartnerObj = self.env['res.partner'] email_cc_formated_list = [] @@ -42,3 +52,52 @@ class MailThread(models.AbstractModel): record._message_add_suggested_recipient( res, partner=partner, reason=_('Cc')) return res + + @api.model + def fields_view_get(self, view_id=None, view_type='form', toolbar=False, + submenu=False): + """Add filters for failed messages. + + These filters will show up on any form or search views of any + model inheriting from ``mail.thread``. + """ + res = super().fields_view_get( + view_id=view_id, view_type=view_type, toolbar=toolbar, + submenu=submenu) + if view_type != 'search' and view_type != 'form': + return res + doc = etree.XML(res['arch']) + if view_type == 'search': + # Modify view to add new filter element + nodes = doc.xpath("//search") + if nodes: + # Create filter element + new_filter = etree.Element( + 'filter', { + 'string': _('Failed sent messages'), + 'name': "failed_message_ids", + 'domain': str([ + ['failed_message_ids.mail_tracking_ids.state', + 'in', + list( + self.env['mail.message'].get_failed_states() + )], + ['failed_message_ids.mail_tracking_needs_action', + '=', True] + ]) + }) + nodes[0].append(etree.Element('separator')) + nodes[0].append(new_filter) + elif view_type == 'form': + # Modify view to add new field element + nodes = doc.xpath( + "//field[@name='message_ids' and @widget='mail_thread']") + if nodes: + # Create field + field_failed_messages = etree.Element('field', { + 'name': 'failed_message_ids', + 'widget': 'mail_failed_message', + }) + nodes[0].addprevious(field_failed_messages) + res['arch'] = etree.tostring(doc, encoding='unicode') + return res diff --git a/mail_tracking/models/mail_tracking_email.py b/mail_tracking/models/mail_tracking_email.py index 3a2e9b35b..f7bdcd8d7 100644 --- a/mail_tracking/models/mail_tracking_email.py +++ b/mail_tracking/models/mail_tracking_email.py @@ -92,13 +92,21 @@ class MailTrackingEmail(models.Model): string="Tracking events", comodel_name='mail.tracking.event', inverse_name='tracking_email_id', readonly=True) + @api.multi + def write(self, vals): + if vals.get('state') in self.env['mail.message'].get_failed_states(): + self.mapped('mail_message_id').write({ + 'mail_tracking_needs_action': True, + }) + super().write(vals) + @api.model def email_is_bounced(self, email): if not email: return False res = self._email_last_tracking_state(email) - return res and res[0].get('state', '') in ['rejected', 'error', - 'spam', 'bounced'] + return res and res[0].get('state', '') in {'rejected', 'error', + 'spam', 'bounced'} @api.model def _email_last_tracking_state(self, email): diff --git a/mail_tracking/readme/ROADMAP.rst b/mail_tracking/readme/ROADMAP.rst new file mode 100644 index 000000000..dcb039c01 --- /dev/null +++ b/mail_tracking/readme/ROADMAP.rst @@ -0,0 +1,5 @@ +* Handle message updates on discuss 'channel_failed' instead of showing the + 'outdated' message. +* Adapt chat_manager changes in v12 +* Adapt discuss changes in v12 +* Add pivot for tracking events and mail trackings diff --git a/mail_tracking/readme/USAGE.rst b/mail_tracking/readme/USAGE.rst index 4bf4a0745..665c3ac1c 100644 --- a/mail_tracking/readme/USAGE.rst +++ b/mail_tracking/readme/USAGE.rst @@ -4,25 +4,25 @@ status icon will appear just right to name of notified partner. These are all available status icons: -.. |sent| image:: mail_tracking/static/src/img/sent.png +.. |sent| image:: ../static/src/img/sent.png :width: 10px -.. |delivered| image:: mail_tracking/static/src/img/delivered.png +.. |delivered| image:: ../static/src/img/delivered.png :width: 15px -.. |opened| image:: mail_tracking/static/src/img/opened.png +.. |opened| image:: ../static/src/img/opened.png :width: 15px -.. |error| image:: mail_tracking/static/src/img/error.png +.. |error| image:: ../static/src/img/error.png :width: 10px -.. |waiting| image:: mail_tracking/static/src/img/waiting.png +.. |waiting| image:: ../static/src/img/waiting.png :width: 10px -.. |unknown| image:: mail_tracking/static/src/img/unknown.png +.. |unknown| image:: ../static/src/img/unknown.png :width: 10px -.. |cc| image:: static/src/img/cc.png +.. |cc| image:: ../static/src/img/cc.png :width: 10px |unknown| **Unknown**: No email tracking info available. Maybe this notified partner has 'Receive Inbox Notifications by Email' == 'Never' @@ -44,3 +44,15 @@ If you want to see all tracking emails and events you can go to * Settings > Technical > Email > Tracking emails * Settings > Technical > Email > Tracking events + +When the message generates and 'error' status, it will apear on discuss 'Failed' +channel. Any view that uses 'mail_thread' widget can show the failed messages +too. + +* Discuss + + .. image:: ../static/img/failed_message_discuss.png + +* Chatter + + .. image:: ../static/img/failed_message_widget.png diff --git a/mail_tracking/static/description/index.html b/mail_tracking/static/description/index.html index c1e967d9c..72874c031 100644 --- a/mail_tracking/static/description/index.html +++ b/mail_tracking/static/description/index.html @@ -376,6 +376,7 @@ right to his name.

@@ -403,6 +412,7 @@ For example, --load=web,mail_trac form, then an email tracking is created for each email notification. Then a status icon will appear just right to name of notified partner.

These are all available status icons:

+<<<<<<< HEAD

unknown Unknown: No email tracking info available. Maybe this notified partner has ‘Receive Inbox Notifications by Email’ == ‘Never’

waiting Waiting: Waiting to be sent

error Error: Error while sending

@@ -414,10 +424,39 @@ status icon will appear just right to name of notified partner.