Browse Source

[MIG] mail_tracking

* Improve tests
* Show trackings even if partner removed
* Disable CSRF protection to webhooks controllers
pull/87/head
Antonio Espinosa 8 years ago
committed by Pedro M. Baeza
parent
commit
0f95a45830
  1. 2
      mail_tracking/README.rst
  2. 2
      mail_tracking/__openerp__.py
  3. 4
      mail_tracking/controllers/main.py
  4. 27
      mail_tracking/models/mail_mail.py
  5. 51
      mail_tracking/models/mail_message.py
  6. 9
      mail_tracking/models/mail_tracking_email.py
  7. 8
      mail_tracking/static/src/css/mail_tracking.css
  8. 152
      mail_tracking/static/src/js/mail_tracking.js
  9. 42
      mail_tracking/static/src/xml/mail_tracking.xml
  10. 78
      mail_tracking/tests/test_mail_tracking.py
  11. 6
      mail_tracking/views/assets.xml
  12. 6
      mail_tracking/views/mail_tracking_email_view.xml
  13. 6
      mail_tracking/views/mail_tracking_event_view.xml
  14. 8
      mail_tracking/views/res_partner_view.xml

2
mail_tracking/README.rst

@ -63,7 +63,7 @@ These are all available status icons:
.. image:: https://odoo-community.org/website/image/ir.attachment/5784_f2813bd/datas
:alt: Try me on Runbot
:target: https://runbot.odoo-community.org/runbot/205/8.0
:target: https://runbot.odoo-community.org/runbot/205/9.0
If you want to see all tracking emails and events you can go to

2
mail_tracking/__openerp__.py

@ -5,7 +5,7 @@
{
"name": "Email tracking",
"summary": "Email tracking system for all mails sent",
"version": "8.0.2.0.1",
"version": "9.0.1.0.0",
"category": "Social Network",
"website": "http://www.tecnativa.com",
"author": "Tecnativa, "

4
mail_tracking/controllers/main.py

@ -36,7 +36,7 @@ class MailTrackingController(http.Controller):
}
@http.route('/mail/tracking/all/<string:db>',
type='http', auth='none')
type='http', auth='none', csrf=False)
def mail_tracking_all(self, db, **kw):
env = _env_get(db)
if not env:
@ -49,7 +49,7 @@ class MailTrackingController(http.Controller):
return response
@http.route('/mail/tracking/event/<string:db>/<string:event_type>',
type='http', auth='none')
type='http', auth='none', csrf=False)
def mail_tracking_event(self, db, event_type, **kw):
env = _env_get(db)
if not env:

27
mail_tracking/models/mail_mail.py

@ -12,8 +12,7 @@ from openerp import models, api, fields
class MailMail(models.Model):
_inherit = 'mail.mail'
@api.model
def _tracking_email_prepare(self, mail, partner, email):
def _tracking_email_prepare(self, partner, email):
ts = time.time()
dt = datetime.utcfromtimestamp(ts)
email_to_list = email.get('email_to', [])
@ -22,22 +21,16 @@ class MailMail(models.Model):
'name': email.get('subject', False),
'timestamp': '%.6f' % ts,
'time': fields.Datetime.to_string(dt),
'mail_id': mail.id if mail else False,
'mail_message_id': mail.mail_message_id.id if mail else False,
'mail_id': self.id,
'mail_message_id': self.mail_message_id.id,
'partner_id': partner.id if partner else False,
'recipient': email_to,
'sender': mail.email_from,
'sender': self.email_from,
}
@api.model
def send_get_email_dict(self, mail, partner=None):
email = super(MailMail, self).send_get_email_dict(
mail, partner=partner)
m_tracking = self.env['mail.tracking.email']
tracking_email = False
if mail:
vals = self._tracking_email_prepare(mail, partner, email)
tracking_email = m_tracking.sudo().create(vals)
if tracking_email:
email = tracking_email.tracking_img_add(email)
return email
@api.multi
def send_get_email_dict(self, partner=None):
email = super(MailMail, self).send_get_email_dict(partner=partner)
vals = self._tracking_email_prepare(partner, email)
tracking_email = self.env['mail.tracking.email'].sudo().create(vals)
return tracking_email.tracking_img_add(email)

51
mail_tracking/models/mail_message.py

@ -3,8 +3,6 @@
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
from openerp import models, api
import logging
_logger = logging.getLogger(__name__)
class MailMessage(models.Model):
@ -33,22 +31,49 @@ class MailMessage(models.Model):
status = tracking_status_map.get(tracking_email_status, 'unknown')
return status
@api.multi
def tracking_status(self):
res = {}
for message in self:
partner_trackings = []
partners_already = self.env['res.partner']
partners = self.env['res.partner']
trackings = self.env['mail.tracking.email'].search([
('mail_message_id', '=', message.id),
])
# Search all trackings for this message
for tracking in trackings:
status = self._partner_tracking_status_get(tracking)
recipient = (
tracking.partner_id.display_name or tracking.recipient)
partner_trackings.append((
status, tracking.id, recipient, tracking.partner_id.id))
if tracking.partner_id:
partners_already |= tracking.partner_id
# Search all recipients for this message
if message.partner_ids:
partners |= message.partner_ids
if message.needaction_partner_ids:
partners |= message.needaction_partner_ids
# Remove recipients already included
partners -= partners_already
for partner in partners:
# If there is partners not included, then status is 'unknown'
partner_trackings.append((
'unknown', False, partner.display_name, partner.id))
res[message.id] = partner_trackings
return res
@api.model
def _message_read_dict_postprocess(self, messages, message_tree):
res = super(MailMessage, self)._message_read_dict_postprocess(
messages, message_tree)
mail_message_ids = {m.get('id') for m in messages if m.get('id')}
mail_messages = self.browse(mail_message_ids)
partner_trackings = mail_messages.tracking_status()
for message_dict in messages:
mail_message_id = message_dict.get('id', False)
if mail_message_id:
partner_trackings = {}
for partner in message_dict.get('partner_ids', []):
partner_id = partner[0]
tracking_email = self.env['mail.tracking.email'].search([
('mail_message_id', '=', mail_message_id),
('partner_id', '=', partner_id),
], limit=1)
status = self._partner_tracking_status_get(tracking_email)
partner_trackings[str(partner_id)] = (
status, tracking_email.id)
message_dict['partner_trackings'] = partner_trackings
message_dict['partner_trackings'] = \
partner_trackings[mail_message_id]
return res

9
mail_tracking/models/mail_tracking_email.py

@ -167,7 +167,7 @@ class MailTrackingEmail(models.Model):
@api.depends('name', 'recipient')
def _compute_display_name(self):
for email in self:
parts = [email.name]
parts = [email.name or '']
if email.recipient:
parts.append(email.recipient)
email.display_name = ' - '.join(parts)
@ -225,13 +225,14 @@ class MailTrackingEmail(models.Model):
def _message_partners_check(self, message, message_id):
mail_message = self.mail_message_id
partners = mail_message.notified_partner_ids | mail_message.partner_ids
partners = (
mail_message.needaction_partner_ids | mail_message.partner_ids)
if (self.partner_id and self.partner_id not in partners):
# If mail_message haven't tracking partner, then
# add it in order to see his trackking status in chatter
# add it in order to see his tracking status in chatter
if mail_message.subtype_id:
mail_message.sudo().write({
'notified_partner_ids': [(4, self.partner_id.id)],
'needaction_partner_ids': [(4, self.partner_id.id)],
})
else:
mail_message.sudo().write({

8
mail_tracking/static/src/css/mail_tracking.css

@ -11,3 +11,11 @@
.mail_tracking span.mail_tracking_opened {
color: #a34a8b;
}
.o_mail_thread .o_thread_message .o_thread_message_core .o_mail_info {
margin: 0;
}
.o_mail_thread .o_thread_message .o_thread_message_core .o_mail_tracking {
margin: 0 0 2px 0;
}

152
mail_tracking/static/src/js/mail_tracking.js

@ -1,63 +1,103 @@
/* © 2016 Antonio Espinosa - <antonio.espinosa@tecnativa.com>
License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). */
(function ($, window, document) {
'use strict';
odoo.define('mail_tracking.partner_tracking', function(require){
"use strict";
openerp.mail_tracking = function (instance) {
var _t = instance.web._t,
_lt = instance.web._lt;
var QWeb = instance.web.qweb;
var mail_orig = instance.mail;
var mail_inherit = function() {
instance.mail.MessageCommon.include({
init: function (parent, datasets, options) {
this._super(parent, datasets, options);
this.partner_trackings = datasets.partner_trackings || [];
}
});
instance.mail.ThreadMessage.include({
bind_events: function () {
this._super();
this.$('.oe_mail_action_tracking').on('click', this.on_tracking_status_clicked);
},
on_tracking_status_clicked: function (event) {
event.preventDefault();
var tracking_email_id = $(event.delegateTarget).data('tracking');
var state = {
'model': 'mail.tracking.email',
'id': tracking_email_id,
'title': _t("Message tracking"),
};
instance.webclient.action_manager.do_push_state(state);
console.log('tracking_email_id = ' + tracking_email_id);
var action = {
type:'ir.actions.act_window',
view_type: 'form',
view_mode: 'form',
res_model: 'mail.tracking.email',
views: [[false, 'form']],
target: 'new',
res_id: tracking_email_id,
};
this.do_action(action);
}
});
var $ = require('$');
var core = require('web.core');
var session = require('web.session');
var Model = require('web.Model');
var ActionManager = require('web.ActionManager');
var chat_manager = require('mail.chat_manager');
var ChatThread = require('mail.ChatThread');
var Chatter = require('mail.Chatter');
var _t = core._t;
var MessageModel = new Model('mail.message', session.context);
// chat_manager is a simple dictionary, not an OdooClass
chat_manager._make_message_super = chat_manager.make_message;
chat_manager.make_message = function(data) {
var msg = this._make_message_super(data);
msg.partner_trackings = data.partner_trackings || [];
return msg;
};
ChatThread.include({
on_tracking_partner_click: function (event) {
var partner_id = $(event.currentTarget).data('partner');
var state = {
'model': 'res.partner',
'id': partner_id,
'title': _t("Tracking partner"),
};
event.preventDefault();
this.action_manager.do_push_state(state);
var action = {
type:'ir.actions.act_window',
view_type: 'form',
view_mode: 'form',
res_model: 'res.partner',
views: [[false, 'form']],
target: 'current',
res_id: partner_id,
};
this.do_action(action);
},
on_tracking_status_click: function (event) {
var tracking_email_id = $(event.currentTarget).data('tracking');
var state = {
'model': 'mail.tracking.email',
'id': tracking_email_id,
'title': _t("Message tracking"),
};
event.preventDefault();
this.action_manager.do_push_state(state);
var action = {
type:'ir.actions.act_window',
view_type: 'form',
view_mode: 'form',
res_model: 'mail.tracking.email',
views: [[false, 'form']],
target: 'new',
res_id: tracking_email_id,
};
this.do_action(action);
},
bind_events: function () {
this.$el.on('click', '.o_mail_action_tracking_partner',
this.on_tracking_partner_click);
this.$el.on('click', '.o_mail_action_tracking_status',
this.on_tracking_status_click);
},
init: function (parent, options) {
this._super.apply(this, arguments);
this.action_manager = this.findAncestor(function(ancestor){
return ancestor instanceof ActionManager;
});
},
start: function () {
this._super();
this.bind_events();
},
render: function(messages, options) {
var self = this, render_super = this._super,
msgs = {},
msg_ids = [];
// Update trackings (async) each time we re-render thread
_.each(messages, function (message) {
msgs[message.id] = message;
msg_ids.push(message.id);
});
MessageModel.call('tracking_status', [msg_ids]).then(function (trackings) {
_.each(trackings, function (tracking, id) {
msgs[id].partner_trackings = tracking;
});
render_super.apply(self, [messages, options]);
});
},
// Tricky way to guarantee that this module is loaded always
// after mail module.
// When --load=web,mail_tracking is specified in init script, then
// web and mail_tracking are the first modules to load in JS
if (instance.mail.MessageCommon === undefined) {
instance.mail = function(instance) {
instance.mail = mail_orig;
instance.mail(instance, instance.mail);
mail_inherit();
};
} else {
mail_inherit();
}
};
});
}(window.jQuery, window, document));
}); // odoo.define

42
mail_tracking/static/src/xml/mail_tracking.xml

@ -40,21 +40,33 @@
</t>
</t>
<t t-extend="mail.thread.message">
<t t-jquery="span[t-attf-class='oe_partner_follower']" t-operation="append">
<t t-set='tracking' t-value='widget.partner_trackings[partner[0]]'/>
<t t-if="tracking[1]">
<span class="mail_tracking oe_mail_action_tracking"
t-att-data-tracking="tracking[1]"
t-attf-title="Status: #{tracking[0]}">
<t t-call="mail.tracking.status"/>
</span>
</t>
<t t-if="!tracking[1]">
<span class="mail_tracking" t-attf-title="Status: #{tracking[0]}">
<t t-call="mail.tracking.status"/>
</span>
</t>
<t t-extend="mail.ChatThread.Message">
<t t-jquery="p[class='o_mail_info']" t-operation="after">
<p class="o_mail_tracking">
<strong>To:</strong>
<t t-set="first_tracking" t-value="true"/>
<t t-foreach="message.partner_trackings" t-as="tracking">
<t t-if="!first_tracking">
-
</t>
<t t-if="tracking[3]">
<a class="o_mail_action_tracking_partner"
t-att-data-partner="tracking[3]"
t-attf-href="#model=res.partner&amp;id=#{tracking[3]}">
<t t-esc="tracking[2]"/>
</a>
</t>
<t t-if="!tracking[3]">
<span><t t-esc="tracking[2]"/></span>
</t>
<span class="mail_tracking o_mail_action_tracking_status"
t-att-data-tracking="tracking[1]"
t-attf-title="Status: #{tracking[0]}">
<t t-call="mail.tracking.status"/>
</span>
<t t-set="first_tracking" t-value="false"/>
</t>
</p>
</t>
</t>

78
mail_tracking/tests/test_mail_tracking.py

@ -65,19 +65,20 @@ class TestMailTracking(TransactionCase):
self.assertTrue(tracking_email)
self.assertEqual(tracking_email.state, 'sent')
# message_dict read by web interface
message_dict = self.env['mail.message'].message_read(message.id)
# First item is message content
self.assertTrue(len(message_dict) > 0)
message_dict = message_dict[0]
message_dict = message.message_read()
# First item in threads is message content
message_dict = message_dict['threads'][0][0]
self.assertTrue(len(message_dict['partner_ids']) > 0)
# First partner is recipient
partner_id = message_dict['partner_ids'][0][0]
self.assertEqual(partner_id, self.recipient.id)
status = message_dict['partner_trackings'][str(partner_id)]
status = message_dict['partner_trackings'][0]
# Tracking status must be sent and
# mail tracking must be the one search before
self.assertEqual(status[0], 'sent')
self.assertEqual(status[1], tracking_email.id)
self.assertEqual(status[2], self.recipient.display_name)
self.assertEqual(status[3], self.recipient.id)
# And now open the email
metadata = {
'ip': '127.0.0.1',
@ -88,11 +89,11 @@ class TestMailTracking(TransactionCase):
tracking_email.event_create('open', metadata)
self.assertEqual(tracking_email.state, 'opened')
def mail_send(self):
def mail_send(self, recipient):
mail = self.env['mail.mail'].create({
'subject': 'Test subject',
'email_from': 'from@domain.com',
'email_to': 'to@domain.com',
'email_to': recipient,
'body_html': '<p>This is a test message</p>',
})
mail.send()
@ -106,7 +107,7 @@ class TestMailTracking(TransactionCase):
controller = MailTrackingController()
db = self.env.cr.dbname
image = base64.decodestring(BLANK)
mail, tracking = self.mail_send()
mail, tracking = self.mail_send(self.recipient.email)
self.assertEqual(mail.email_to, tracking.recipient)
self.assertEqual(mail.email_from, tracking.sender)
with mock.patch(mock_request) as mock_func:
@ -115,7 +116,7 @@ class TestMailTracking(TransactionCase):
self.assertEqual(image, res.response[0])
def test_concurrent_open(self):
mail, tracking = self.mail_send()
mail, tracking = self.mail_send(self.recipient.email)
ts = time.time()
metadata = {
'ip': '127.0.0.1',
@ -146,7 +147,7 @@ class TestMailTracking(TransactionCase):
self.assertEqual(len(opens), 2)
def test_concurrent_click(self):
mail, tracking = self.mail_send()
mail, tracking = self.mail_send(self.recipient.email)
ts = time.time()
metadata = {
'ip': '127.0.0.1',
@ -188,11 +189,66 @@ class TestMailTracking(TransactionCase):
def test_smtp_error(self):
with mock.patch(mock_send_email) as mock_func:
mock_func.side_effect = Warning('Test error')
mail, tracking = self.mail_send()
mail, tracking = self.mail_send(self.recipient.email)
self.assertEqual('error', tracking.state)
self.assertEqual('Warning', tracking.error_type)
self.assertEqual('Test error', tracking.error_description)
def test_partner_email_change(self):
mail, tracking = self.mail_send(self.recipient.email)
tracking.event_create('open', {})
orig_score = self.recipient.email_score
orig_email = self.recipient.email
self.recipient.email = orig_email + '2'
self.assertEqual(50.0, self.recipient.email_score)
self.recipient.email = orig_email
self.assertEqual(orig_score, self.recipient.email_score)
def test_process_hard_bounce(self):
mail, tracking = self.mail_send(self.recipient.email)
tracking.event_create('hard_bounce', {})
self.assertEqual('bounced', tracking.state)
def test_process_soft_bounce(self):
mail, tracking = self.mail_send(self.recipient.email)
tracking.event_create('soft_bounce', {})
self.assertEqual('soft-bounced', tracking.state)
def test_process_delivered(self):
mail, tracking = self.mail_send(self.recipient.email)
tracking.event_create('delivered', {})
self.assertEqual('delivered', tracking.state)
def test_process_deferral(self):
mail, tracking = self.mail_send(self.recipient.email)
tracking.event_create('deferral', {})
self.assertEqual('deferred', tracking.state)
def test_process_spam(self):
mail, tracking = self.mail_send(self.recipient.email)
tracking.event_create('spam', {})
self.assertEqual('spam', tracking.state)
def test_process_unsub(self):
mail, tracking = self.mail_send(self.recipient.email)
tracking.event_create('unsub', {})
self.assertEqual('unsub', tracking.state)
def test_process_reject(self):
mail, tracking = self.mail_send(self.recipient.email)
tracking.event_create('reject', {})
self.assertEqual('rejected', tracking.state)
def test_process_open(self):
mail, tracking = self.mail_send(self.recipient.email)
tracking.event_create('open', {})
self.assertEqual('opened', tracking.state)
def test_process_click(self):
mail, tracking = self.mail_send(self.recipient.email)
tracking.event_create('click', {})
self.assertEqual('opened', tracking.state)
def test_db(self):
db = self.env.cr.dbname
controller = MailTrackingController()

6
mail_tracking/views/assets.xml

@ -1,8 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- © 2016 Antonio Espinosa - <antonio.espinosa@tecnativa.com>
License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). -->
<openerp>
<data>
<odoo>
<template id="assets_backend"
name="mail_tracking assets"
@ -15,5 +14,4 @@
</xpath>
</template>
</data>
</openerp>
</odoo>

6
mail_tracking/views/mail_tracking_email_view.xml

@ -1,8 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- © 2016 Antonio Espinosa - <antonio.espinosa@tecnativa.com>
License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). -->
<openerp>
<data>
<odoo>
<record model="ir.ui.view" id="view_mail_tracking_email_form">
<field name="name">mail.tracking.email.form</field>
@ -118,5 +117,4 @@
parent="base.menu_email"
action="action_view_mail_tracking_email"/>
</data>
</openerp>
</odoo>

6
mail_tracking/views/mail_tracking_event_view.xml

@ -1,8 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- © 2016 Antonio Espinosa - <antonio.espinosa@tecnativa.com>
License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). -->
<openerp>
<data>
<odoo>
<record model="ir.ui.view" id="view_mail_tracking_event_form">
<field name="name">mail.tracking.event.form</field>
@ -121,5 +120,4 @@
action="action_view_mail_tracking_event"/>
</data>
</openerp>
</odoo>

8
mail_tracking/views/res_partner_view.xml

@ -1,15 +1,14 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- © 2016 Antonio Espinosa - <antonio.espinosa@tecnativa.com>
License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). -->
<openerp>
<data>
<odoo>
<record model="ir.ui.view" id="view_partner_form">
<field name="name">Partner Form with tracking emails</field>
<field name="model">res.partner</field>
<field name="inherit_id" ref="base.view_partner_form"/>
<field name="arch" type="xml">
<div class="oe_right oe_button_box" position="inside">
<div name="button_box" position="inside">
<button name="%(mail_tracking.action_view_mail_tracking_email)d"
context="{'search_default_recipient': email,
'default_recipient': email}"
@ -29,5 +28,4 @@
</field>
</record>
</data>
</openerp>
</odoo>
Loading…
Cancel
Save