Browse Source

Merge pull request #108 from dbo-odoo/10.0-mig-mail_tracking

[MIG] mail_tracking: Migrated to 10.0
pull/147/head
Pedro M. Baeza 8 years ago
committed by GitHub
parent
commit
d42d5a96f6
  1. 2
      mail_tracking/README.rst
  2. 5
      mail_tracking/__manifest__.py
  3. 71
      mail_tracking/controllers/main.py
  4. 12
      mail_tracking/data/tracking_data.xml
  5. 2
      mail_tracking/models/ir_mail_server.py
  6. 5
      mail_tracking/models/mail_mail.py
  7. 7
      mail_tracking/models/mail_message.py
  8. 139
      mail_tracking/models/mail_tracking_email.py
  9. 7
      mail_tracking/models/mail_tracking_event.py
  10. 51
      mail_tracking/models/res_partner.py
  11. 2
      mail_tracking/security/ir.model.access.csv
  12. 16
      mail_tracking/security/mail_tracking_email_security.xml
  13. 3
      mail_tracking/static/src/js/mail_tracking.js
  14. 64
      mail_tracking/tests/test_mail_tracking.py
  15. 6
      mail_tracking/views/assets.xml
  16. 15
      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 .. image:: https://odoo-community.org/website/image/ir.attachment/5784_f2813bd/datas
:alt: Try me on Runbot :alt: Try me on Runbot
:target: https://runbot.odoo-community.org/runbot/205/9.0
:target: https://runbot.odoo-community.org/runbot/205/10.0
If you want to see all tracking emails and events you can go to If you want to see all tracking emails and events you can go to

5
mail_tracking/__manifest__.py

@ -5,20 +5,21 @@
{ {
"name": "Email tracking", "name": "Email tracking",
"summary": "Email tracking system for all mails sent", "summary": "Email tracking system for all mails sent",
"version": "9.0.1.0.0",
"version": "10.0.1.0.0",
"category": "Social Network", "category": "Social Network",
"website": "http://www.tecnativa.com", "website": "http://www.tecnativa.com",
"author": "Tecnativa, " "author": "Tecnativa, "
"Odoo Community Association (OCA)", "Odoo Community Association (OCA)",
"license": "AGPL-3", "license": "AGPL-3",
"application": False, "application": False,
'installable': False,
'installable': True,
"depends": [ "depends": [
"decimal_precision", "decimal_precision",
"mail", "mail",
], ],
"data": [ "data": [
"data/tracking_data.xml", "data/tracking_data.xml",
"security/mail_tracking_email_security.xml",
"security/ir.model.access.csv", "security/ir.model.access.csv",
"views/assets.xml", "views/assets.xml",
"views/mail_tracking_email_view.xml", "views/mail_tracking_email_view.xml",

71
mail_tracking/controllers/main.py

@ -4,15 +4,20 @@
import werkzeug import werkzeug
from psycopg2 import OperationalError from psycopg2 import OperationalError
from openerp import api, http, registry, SUPERUSER_ID
from odoo import api, http, registry, SUPERUSER_ID
import logging import logging
_logger = logging.getLogger(__name__) _logger = logging.getLogger(__name__)
BLANK = 'R0lGODlhAQABAIAAANvf7wAAACH5BAEAAAAALAAAAAABAAEAAAICRAEAOw==' BLANK = 'R0lGODlhAQABAIAAANvf7wAAACH5BAEAAAAALAAAAAABAAEAAAICRAEAOw=='
def _env_get(db):
def _env_get(db, callback, tracking_id, event_type, **kw):
res = 'NOT FOUND'
reg = False reg = False
current = http.request.db and db == http.request.db
env = current and http.request.env
if not env:
with api.Environment.manage():
try: try:
reg = registry(db) reg = registry(db)
except OperationalError: except OperationalError:
@ -20,8 +25,16 @@ def _env_get(db):
except: # pragma: no cover except: # pragma: no cover
_logger.warning("Selected BD '%s' connection error", db) _logger.warning("Selected BD '%s' connection error", db)
if reg: if reg:
return api.Environment(reg.cursor(), SUPERUSER_ID, {})
return False
_logger.info("New environment for database '%s'", db)
with reg.cursor() as new_cr:
new_env = api.Environment(new_cr, SUPERUSER_ID, {})
res = callback(new_env, tracking_id, event_type, **kw)
new_env.cr.commit()
else:
# make sudo when reusing environment
env = env(user=SUPERUSER_ID)
res = callback(env, tracking_id, event_type, **kw)
return res
class MailTrackingController(http.Controller): class MailTrackingController(http.Controller):
@ -35,49 +48,37 @@ class MailTrackingController(http.Controller):
'ua_family': request.user_agent.browser or False, 'ua_family': request.user_agent.browser or False,
} }
def _tracking_open(self, env, tracking_id, event_type, **kw):
tracking_email = env['mail.tracking.email'].search([
('id', '=', tracking_id),
])
if tracking_email:
metadata = self._request_metadata()
tracking_email.event_create('open', metadata)
else:
_logger.warning(
"MailTracking email '%s' not found", tracking_id)
def _tracking_event(self, env, tracking_id, event_type, **kw):
metadata = self._request_metadata()
return env['mail.tracking.email'].event_process(
http.request, kw, metadata, event_type=event_type)
@http.route('/mail/tracking/all/<string:db>', @http.route('/mail/tracking/all/<string:db>',
type='http', auth='none', csrf=False) type='http', auth='none', csrf=False)
def mail_tracking_all(self, db, **kw): def mail_tracking_all(self, db, **kw):
env = _env_get(db)
if not env:
return 'NOT FOUND'
metadata = self._request_metadata()
response = env['mail.tracking.email'].event_process(
http.request, kw, metadata)
env.cr.commit()
env.cr.close()
return response
return _env_get(db, self._tracking_event, None, None, **kw)
@http.route('/mail/tracking/event/<string:db>/<string:event_type>', @http.route('/mail/tracking/event/<string:db>/<string:event_type>',
type='http', auth='none', csrf=False) type='http', auth='none', csrf=False)
def mail_tracking_event(self, db, event_type, **kw): def mail_tracking_event(self, db, event_type, **kw):
env = _env_get(db)
if not env:
return 'NOT FOUND'
metadata = self._request_metadata()
response = env['mail.tracking.email'].event_process(
http.request, kw, metadata, event_type=event_type)
env.cr.commit()
env.cr.close()
return response
return _env_get(db, self._tracking_event, None, event_type, **kw)
@http.route('/mail/tracking/open/<string:db>' @http.route('/mail/tracking/open/<string:db>'
'/<int:tracking_email_id>/blank.gif', '/<int:tracking_email_id>/blank.gif',
type='http', auth='none') type='http', auth='none')
def mail_tracking_open(self, db, tracking_email_id, **kw): def mail_tracking_open(self, db, tracking_email_id, **kw):
env = _env_get(db)
if env:
tracking_email = env['mail.tracking.email'].search([
('id', '=', tracking_email_id),
])
if tracking_email:
metadata = self._request_metadata()
tracking_email.event_create('open', metadata)
else:
_logger.warning(
"MailTracking email '%s' not found", tracking_email_id)
env.cr.commit()
env.cr.close()
_env_get(db, self._tracking_open, tracking_email_id, None, **kw)
# Always return GIF blank image # Always return GIF blank image
response = werkzeug.wrappers.Response() response = werkzeug.wrappers.Response()

12
mail_tracking/data/tracking_data.xml

@ -1,13 +1,9 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<!-- © 2016 Antonio Espinosa - <antonio.espinosa@tecnativa.com> <!-- © 2016 Antonio Espinosa - <antonio.espinosa@tecnativa.com>
License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). --> License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). -->
<openerp>
<data>
<record forcecreate="True" id="decimal_tracking_timestamp" model="decimal.precision">
<odoo>
<record forcecreate="True" id="decimal_tracking_timestamp" model="decimal.precision">
<field name="name">MailTracking Timestamp</field> <field name="name">MailTracking Timestamp</field>
<field name="digits">6</field> <field name="digits">6</field>
</record>
</data>
</openerp>
</record>
</odoo>

2
mail_tracking/models/ir_mail_server.py

@ -4,7 +4,7 @@
import re import re
import threading import threading
from openerp import models, api, tools
from odoo import models, api, tools
class IrMailServer(models.Model): class IrMailServer(models.Model):

5
mail_tracking/models/mail_mail.py

@ -6,7 +6,7 @@ import time
from datetime import datetime from datetime import datetime
from email.utils import COMMASPACE from email.utils import COMMASPACE
from openerp import models, api, fields
from odoo import models, fields
class MailMail(models.Model): class MailMail(models.Model):
@ -18,7 +18,7 @@ class MailMail(models.Model):
email_to_list = email.get('email_to', []) email_to_list = email.get('email_to', [])
email_to = COMMASPACE.join(email_to_list) email_to = COMMASPACE.join(email_to_list)
return { return {
'name': email.get('subject', False),
'name': self.subject,
'timestamp': '%.6f' % ts, 'timestamp': '%.6f' % ts,
'time': fields.Datetime.to_string(dt), 'time': fields.Datetime.to_string(dt),
'mail_id': self.id, 'mail_id': self.id,
@ -28,7 +28,6 @@ class MailMail(models.Model):
'sender': self.email_from, 'sender': self.email_from,
} }
@api.multi
def send_get_email_dict(self, partner=None): def send_get_email_dict(self, partner=None):
email = super(MailMail, self).send_get_email_dict(partner=partner) email = super(MailMail, self).send_get_email_dict(partner=partner)
vals = self._tracking_email_prepare(partner, email) vals = self._tracking_email_prepare(partner, email)

7
mail_tracking/models/mail_message.py

@ -2,7 +2,7 @@
# © 2016 Antonio Espinosa - <antonio.espinosa@tecnativa.com> # © 2016 Antonio Espinosa - <antonio.espinosa@tecnativa.com>
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
from openerp import models, api
from odoo import models, api
class MailMessage(models.Model): class MailMessage(models.Model):
@ -31,7 +31,6 @@ class MailMessage(models.Model):
status = tracking_status_map.get(tracking_email_status, 'unknown') status = tracking_status_map.get(tracking_email_status, 'unknown')
return status return status
@api.multi
def tracking_status(self): def tracking_status(self):
res = {} res = {}
for message in self: for message in self:
@ -45,7 +44,7 @@ class MailMessage(models.Model):
for tracking in trackings: for tracking in trackings:
status = self._partner_tracking_status_get(tracking) status = self._partner_tracking_status_get(tracking)
recipient = ( recipient = (
tracking.partner_id.display_name or tracking.recipient)
tracking.partner_id.name or tracking.recipient)
partner_trackings.append(( partner_trackings.append((
status, tracking.id, recipient, tracking.partner_id.id)) status, tracking.id, recipient, tracking.partner_id.id))
if tracking.partner_id: if tracking.partner_id:
@ -60,7 +59,7 @@ class MailMessage(models.Model):
for partner in partners: for partner in partners:
# If there is partners not included, then status is 'unknown' # If there is partners not included, then status is 'unknown'
partner_trackings.append(( partner_trackings.append((
'unknown', False, partner.display_name, partner.id))
'unknown', False, partner.name, partner.id))
res[message.id] = partner_trackings res[message.id] = partner_trackings
return res return res

139
mail_tracking/models/mail_tracking_email.py

@ -8,8 +8,8 @@ import time
import re import re
from datetime import datetime from datetime import datetime
from openerp import models, api, fields, tools
import openerp.addons.decimal_precision as dp
from odoo import models, api, fields, tools
import odoo.addons.decimal_precision as dp
_logger = logging.getLogger(__name__) _logger = logging.getLogger(__name__)
@ -23,14 +23,19 @@ class MailTrackingEmail(models.Model):
_rec_name = 'display_name' _rec_name = 'display_name'
_description = 'MailTracking email' _description = 'MailTracking email'
# This table is going to grow fast and to infinite, so we index:
# - name: Search in tree view
# - time: default order fields
# - recipient_address: Used for email_store calculation (non-store)
# - state: Search and group_by in tree view
name = fields.Char(string="Subject", readonly=True, index=True) name = fields.Char(string="Subject", readonly=True, index=True)
display_name = fields.Char( display_name = fields.Char(
string="Display name", readonly=True, store=True, string="Display name", readonly=True, store=True,
compute="_compute_display_name")
compute="_compute_tracking_display_name")
timestamp = fields.Float( timestamp = fields.Float(
string='UTC timestamp', readonly=True, string='UTC timestamp', readonly=True,
digits=dp.get_precision('MailTracking Timestamp')) digits=dp.get_precision('MailTracking Timestamp'))
time = fields.Datetime(string="Time", readonly=True)
time = fields.Datetime(string="Time", readonly=True, index=True)
date = fields.Date( date = fields.Date(
string="Date", readonly=True, compute="_compute_date", store=True) string="Date", readonly=True, compute="_compute_date", store=True)
mail_message_id = fields.Many2one( mail_message_id = fields.Many2one(
@ -42,7 +47,7 @@ class MailTrackingEmail(models.Model):
recipient = fields.Char(string='Recipient email', readonly=True) recipient = fields.Char(string='Recipient email', readonly=True)
recipient_address = fields.Char( recipient_address = fields.Char(
string='Recipient email address', readonly=True, store=True, string='Recipient email address', readonly=True, store=True,
compute='_compute_recipient_address')
compute='_compute_recipient_address', index=True)
sender = fields.Char(string='Sender email', readonly=True) sender = fields.Char(string='Sender email', readonly=True)
state = fields.Selection([ state = fields.Selection([
('error', 'Error'), ('error', 'Error'),
@ -88,72 +93,56 @@ class MailTrackingEmail(models.Model):
inverse_name='tracking_email_id', readonly=True) inverse_name='tracking_email_id', readonly=True)
@api.model @api.model
def tracking_ids_recalculate(self, model, email_field, tracking_field,
email, new_tracking=None):
objects = self.env[model].search([
(email_field, '=ilike', email),
])
for obj in objects:
trackings = obj[tracking_field]
if new_tracking:
trackings |= new_tracking
trackings = trackings._email_score_tracking_filter()
if set(obj[tracking_field].ids) != set(trackings.ids):
if trackings:
obj.write({
tracking_field: [(6, False, trackings.ids)]
})
else:
obj.write({
tracking_field: [(5, False, False)]
})
return objects
def _email_score_tracking_filter(self, domain, order='time desc',
limit=10):
"""Default tracking search. Ready to be inherited."""
return self.search(domain, limit=limit, order=order)
@api.model @api.model
def _tracking_ids_to_write(self, email):
trackings = self.env['mail.tracking.email'].search([
('recipient_address', '=ilike', email)
])
trackings = trackings._email_score_tracking_filter()
if trackings:
return [(6, False, trackings.ids)]
else:
return [(5, False, False)]
@api.multi
def _email_score_tracking_filter(self):
"""Default email score filter for tracking emails"""
# Consider only last 10 tracking emails
return self.sorted(key=lambda r: r.time, reverse=True)[:10]
def email_is_bounced(self, email):
return len(self._email_score_tracking_filter([
('recipient_address', '=ilike', email),
('state', 'in', ('error', 'rejected', 'spam', 'bounced')),
])) > 0
@api.model @api.model
def email_score_from_email(self, email): def email_score_from_email(self, email):
trackings = self.env['mail.tracking.email'].search([
return self._email_score_tracking_filter([
('recipient_address', '=ilike', email) ('recipient_address', '=ilike', email)
])
return trackings.email_score()
]).email_score()
@api.model
def _email_score_weights(self):
"""Default email score weights. Ready to be inherited"""
return {
'error': -50.0,
'rejected': -25.0,
'spam': -25.0,
'bounced': -25.0,
'soft-bounced': -10.0,
'unsub': -10.0,
'delivered': 1.0,
'opened': 5.0,
}
@api.multi
def email_score(self): def email_score(self):
"""Default email score algorimth"""
"""Default email score algorimth. Ready to be inherited
Must return a value beetwen 0.0 and 100.0
- Bad reputation: Value between 0 and 50.0
- Unknown reputation: Value 50.0
- Good reputation: Value between 50.0 and 100.0
"""
weights = self._email_score_weights()
score = 50.0 score = 50.0
trackings = self._email_score_tracking_filter()
for tracking in trackings:
if tracking.state in ('error',):
score -= 50.0
elif tracking.state in ('rejected', 'spam', 'bounced'):
score -= 25.0
elif tracking.state in ('soft-bounced', 'unsub'):
score -= 10.0
elif tracking.state in ('delivered',):
score += 5.0
elif tracking.state in ('opened',):
score += 10.0
for tracking in self:
score += weights.get(tracking.state, 0.0)
if score > 100.0: if score > 100.0:
score = 100.0 score = 100.0
elif score < 0.0:
score = 0.0
return score return score
@api.multi
@api.depends('recipient') @api.depends('recipient')
def _compute_recipient_address(self): def _compute_recipient_address(self):
for email in self: for email in self:
@ -163,32 +152,24 @@ class MailTrackingEmail(models.Model):
else: else:
email.recipient_address = email.recipient email.recipient_address = email.recipient
@api.multi
@api.depends('name', 'recipient') @api.depends('name', 'recipient')
def _compute_display_name(self):
def _compute_tracking_display_name(self):
for email in self: for email in self:
parts = [email.name or ''] parts = [email.name or '']
if email.recipient: if email.recipient:
parts.append(email.recipient) parts.append(email.recipient)
email.display_name = ' - '.join(parts) email.display_name = ' - '.join(parts)
@api.multi
@api.depends('time') @api.depends('time')
def _compute_date(self): def _compute_date(self):
for email in self: for email in self:
email.date = fields.Date.to_string( email.date = fields.Date.to_string(
fields.Date.from_string(email.time)) fields.Date.from_string(email.time))
@api.model
def create(self, vals):
tracking = super(MailTrackingEmail, self).create(vals)
self.tracking_ids_recalculate(
'res.partner', 'email', 'tracking_email_ids',
tracking.recipient_address, new_tracking=tracking)
return tracking
def _get_mail_tracking_img(self): def _get_mail_tracking_img(self):
base_url = self.env['ir.config_parameter'].get_param('web.base.url')
m_config = self.env['ir.config_parameter']
base_url = (m_config.get_param('mail_tracking.base.url') or
m_config.get_param('web.base.url'))
path_url = ( path_url = (
'mail/tracking/open/%(db)s/%(tracking_email_id)s/blank.gif' % { 'mail/tracking/open/%(db)s/%(tracking_email_id)s/blank.gif' % {
'db': self.env.cr.dbname, 'db': self.env.cr.dbname,
@ -202,7 +183,12 @@ class MailTrackingEmail(models.Model):
'tracking_email_id': self.id, 'tracking_email_id': self.id,
}) })
@api.multi
def _partners_email_bounced_set(self, reason):
for tracking_email in self:
self.env['res.partner'].search([
('email', '=ilike', tracking_email.recipient_address)
]).email_bounced_set(tracking_email, reason)
def smtp_error(self, mail_server, smtp_server, exception): def smtp_error(self, mail_server, smtp_server, exception):
self.sudo().write({ self.sudo().write({
'error_smtp_server': tools.ustr(smtp_server), 'error_smtp_server': tools.ustr(smtp_server),
@ -210,9 +196,9 @@ class MailTrackingEmail(models.Model):
'error_description': tools.ustr(exception), 'error_description': tools.ustr(exception),
'state': 'error', 'state': 'error',
}) })
self.sudo()._partners_email_bounced_set('error')
return True return True
@api.multi
def tracking_img_add(self, email): def tracking_img_add(self, email):
self.ensure_one() self.ensure_one()
tracking_url = self._get_mail_tracking_img() tracking_url = self._get_mail_tracking_img()
@ -240,7 +226,6 @@ class MailTrackingEmail(models.Model):
}) })
return True return True
@api.multi
def _tracking_sent_prepare(self, mail_server, smtp_server, message, def _tracking_sent_prepare(self, mail_server, smtp_server, message,
message_id): message_id):
self.ensure_one() self.ensure_one()
@ -286,7 +271,6 @@ class MailTrackingEmail(models.Model):
concurrent_event_ids = m_event.search(domain) concurrent_event_ids = m_event.search(domain)
return concurrent_event_ids return concurrent_event_ids
@api.multi
def event_create(self, event_type, metadata): def event_create(self, event_type, metadata):
event_ids = self.env['mail.tracking.event'] event_ids = self.env['mail.tracking.event']
for tracking_email in self: for tracking_email in self:
@ -295,13 +279,10 @@ class MailTrackingEmail(models.Model):
vals = tracking_email._event_prepare(event_type, metadata) vals = tracking_email._event_prepare(event_type, metadata)
if vals: if vals:
event_ids += event_ids.sudo().create(vals) event_ids += event_ids.sudo().create(vals)
partners = self.tracking_ids_recalculate(
'res.partner', 'email', 'tracking_email_ids',
tracking_email.recipient_address)
if partners:
partners.email_score_calculate()
else: else:
_logger.debug("Concurrent event '%s' discarded", event_type) _logger.debug("Concurrent event '%s' discarded", event_type)
if event_type in {'hard_bounce', 'spam', 'reject'}:
self.sudo()._partners_email_bounced_set(event_type)
return event_ids return event_ids
@api.model @api.model

7
mail_tracking/models/mail_tracking_event.py

@ -5,8 +5,8 @@
import time import time
from datetime import datetime from datetime import datetime
from openerp import models, api, fields
import openerp.addons.decimal_precision as dp
from odoo import models, api, fields
import odoo.addons.decimal_precision as dp
class MailTrackingEvent(models.Model): class MailTrackingEvent(models.Model):
@ -23,7 +23,7 @@ class MailTrackingEvent(models.Model):
date = fields.Date( date = fields.Date(
string="Date", readonly=True, compute="_compute_date", store=True) string="Date", readonly=True, compute="_compute_date", store=True)
tracking_email_id = fields.Many2one( tracking_email_id = fields.Many2one(
string='Message', readonly=True,
string='Message', readonly=True, required=True, ondelete='cascade',
comodel_name='mail.tracking.email') comodel_name='mail.tracking.email')
event_type = fields.Selection(string='Event type', selection=[ event_type = fields.Selection(string='Event type', selection=[
('sent', 'Sent'), ('sent', 'Sent'),
@ -51,7 +51,6 @@ class MailTrackingEvent(models.Model):
error_description = fields.Char(string='Error description', readonly=True) error_description = fields.Char(string='Error description', readonly=True)
error_details = fields.Text(string='Error details', readonly=True) error_details = fields.Text(string='Error details', readonly=True)
@api.multi
@api.depends('time') @api.depends('time')
def _compute_date(self): def _compute_date(self):
for email in self: for email in self:

51
mail_tracking/models/res_partner.py

@ -2,45 +2,46 @@
# © 2016 Antonio Espinosa - <antonio.espinosa@tecnativa.com> # © 2016 Antonio Espinosa - <antonio.espinosa@tecnativa.com>
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
from openerp import models, api, fields
from odoo import models, api, fields
class ResPartner(models.Model): class ResPartner(models.Model):
_inherit = 'res.partner' _inherit = 'res.partner'
tracking_email_ids = fields.Many2many(
string="Tracking emails", comodel_name="mail.tracking.email",
readonly=True)
# tracking_emails_count and email_score are non-store fields in order
# to improve performance
# email_bounced is store=True and index=True field in order to filter
# in tree view for processing bounces easier
tracking_emails_count = fields.Integer( tracking_emails_count = fields.Integer(
string="Tracking emails count", store=True, readonly=True,
compute="_compute_tracking_emails_count")
email_score = fields.Float(
string="Email score", readonly=True, default=50.0)
compute='_compute_tracking_emails_count', readonly=True)
email_bounced = fields.Boolean(index=True)
email_score = fields.Float(compute='_compute_email_score', readonly=True)
@api.multi
def email_score_calculate(self):
# This is not a compute method because is causing a inter-block
# in mail_tracking_email PostgreSQL table
# We suspect that tracking_email write to state field block that
# table and then inside write ORM try to read from DB
# tracking_email_ids because it's not in cache.
# PostgreSQL blocks read because we have not committed yet the write
for partner in self:
partner.email_score = partner.tracking_email_ids.email_score()
@api.depends('email')
def _compute_email_score(self):
for partner in self.filtered('email'):
partner.email_score = self.env['mail.tracking.email'].\
email_score_from_email(partner.email)
@api.one
@api.depends('tracking_email_ids')
@api.multi
@api.depends('email')
def _compute_tracking_emails_count(self): def _compute_tracking_emails_count(self):
self.tracking_emails_count = self.env['mail.tracking.email'].\
for partner in self:
partner.tracking_emails_count = self.env['mail.tracking.email'].\
search_count([ search_count([
('recipient_address', '=ilike', self.email)
('recipient_address', '=ilike', partner.email)
]) ])
@api.multi @api.multi
def email_bounced_set(self, tracking_email, reason):
"""Inherit this method to make any other actions to partners"""
partners = self.filtered(lambda r: not r.email_bounced)
return partners.write({'email_bounced': True})
def write(self, vals): def write(self, vals):
email = vals.get('email') email = vals.get('email')
if email is not None: if email is not None:
m_track = self.env['mail.tracking.email']
vals['tracking_email_ids'] = m_track._tracking_ids_to_write(email)
vals['email_score'] = m_track.email_score_from_email(email)
vals['email_bounced'] = (
bool(email) and
self.env['mail.tracking.email'].email_is_bounced(email))
return super(ResPartner, self).write(vals) return super(ResPartner, self).write(vals)

2
mail_tracking/security/ir.model.access.csv

@ -1,4 +1,6 @@
"id","name","model_id:id","group_id:id","perm_read","perm_write","perm_create","perm_unlink" "id","name","model_id:id","group_id:id","perm_read","perm_write","perm_create","perm_unlink"
"access_mail_tracking_email_group_public","mail_tracking_email group_public","model_mail_tracking_email","base.group_public",1,0,0,0
"access_mail_tracking_email_group_portal","mail_tracking_email group_portal","model_mail_tracking_email","base.group_portal",1,0,0,0
"access_mail_tracking_email_group_user","mail_tracking_email group_user","model_mail_tracking_email","base.group_user",1,0,0,0 "access_mail_tracking_email_group_user","mail_tracking_email group_user","model_mail_tracking_email","base.group_user",1,0,0,0
"access_mail_tracking_event_group_user","mail_tracking_event group_user","model_mail_tracking_event","base.group_user",1,0,0,0 "access_mail_tracking_event_group_user","mail_tracking_event group_user","model_mail_tracking_event","base.group_user",1,0,0,0
"access_mail_tracking_email_group_system","mail_tracking_email group_system","model_mail_tracking_email","base.group_system",1,1,1,1 "access_mail_tracking_email_group_system","mail_tracking_email group_system","model_mail_tracking_email","base.group_system",1,1,1,1

16
mail_tracking/security/mail_tracking_email_security.xml

@ -0,0 +1,16 @@
<?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). -->
<odoo>
<record model="ir.rule" id="mail_tracking_email_portal_public_rule">
<field name="name">mail_tracking_email: portal/public: read access on my email trackings</field>
<field name="model_id" ref="model_mail_tracking_email"/>
<field name="domain_force">[('partner_id', '=', user.partner_id.id)]</field>
<field name="groups" eval="[(4, ref('base.group_portal')), (4, ref('base.group_public'))]"/>
<field name="perm_create" eval="False"/>
<field name="perm_unlink" eval="False"/>
<field name="perm_write" eval="False"/>
</record>
</odoo>

3
mail_tracking/static/src/js/mail_tracking.js

@ -4,7 +4,6 @@
odoo.define('mail_tracking.partner_tracking', function(require){ odoo.define('mail_tracking.partner_tracking', function(require){
"use strict"; "use strict";
var $ = require('$');
var core = require('web.core'); var core = require('web.core');
var session = require('web.session'); var session = require('web.session');
var Model = require('web.Model'); var Model = require('web.Model');
@ -26,7 +25,7 @@ chat_manager.make_message = function(data) {
ChatThread.include({ ChatThread.include({
on_tracking_partner_click: function (event) { on_tracking_partner_click: function (event) {
var partner_id = $(event.currentTarget).data('partner');
var partner_id = this.$el.find(event.currentTarget).data('partner');
var state = { var state = {
'model': 'res.partner', 'model': 'res.partner',
'id': partner_id, 'id': partner_id,

64
mail_tracking/tests/test_mail_tracking.py

@ -5,12 +5,12 @@
import mock import mock
import base64 import base64
import time import time
from openerp.tests.common import TransactionCase
from odoo import http
from odoo.tests.common import TransactionCase
from ..controllers.main import MailTrackingController, BLANK from ..controllers.main import MailTrackingController, BLANK
mock_request = 'openerp.http.request'
mock_send_email = ('openerp.addons.base.ir.ir_mail_server.'
'ir_mail_server.send_email')
mock_send_email = ('odoo.addons.base.ir.ir_mail_server.'
'IrMailServer.send_email')
class FakeUserAgent(object): class FakeUserAgent(object):
@ -22,11 +22,9 @@ class FakeUserAgent(object):
return 'Test suite' return 'Test suite'
# One test case per method
class TestMailTracking(TransactionCase): class TestMailTracking(TransactionCase):
# Use case : Prepare some data for current test case
def setUp(self):
super(TestMailTracking, self).setUp()
def setUp(self, *args, **kwargs):
super(TestMailTracking, self).setUp(*args, **kwargs)
self.sender = self.env['res.partner'].create({ self.sender = self.env['res.partner'].create({
'name': 'Test sender', 'name': 'Test sender',
'email': 'sender@example.com', 'email': 'sender@example.com',
@ -37,12 +35,22 @@ class TestMailTracking(TransactionCase):
'email': 'recipient@example.com', 'email': 'recipient@example.com',
'notify_email': 'always', 'notify_email': 'always',
}) })
self.request = {
self.last_request = http.request
http.request = type('obj', (object,), {
'db': self.env.cr.dbname,
'env': self.env,
'endpoint': type('obj', (object,), {
'routing': [],
}),
'httprequest': type('obj', (object,), { 'httprequest': type('obj', (object,), {
'remote_addr': '123.123.123.123', 'remote_addr': '123.123.123.123',
'user_agent': FakeUserAgent(), 'user_agent': FakeUserAgent(),
}), }),
}
})
def tearDown(self, *args, **kwargs):
http.request = self.last_request
return super(TestMailTracking, self).tearDown(*args, **kwargs)
def test_message_post(self): def test_message_post(self):
# This message will generate a notification for recipient # This message will generate a notification for recipient
@ -50,7 +58,7 @@ class TestMailTracking(TransactionCase):
'subject': 'Message test', 'subject': 'Message test',
'author_id': self.sender.id, 'author_id': self.sender.id,
'email_from': self.sender.email, 'email_from': self.sender.email,
'type': 'comment',
'message_type': 'comment',
'model': 'res.partner', 'model': 'res.partner',
'res_id': self.recipient.id, 'res_id': self.recipient.id,
'partner_ids': [(4, self.recipient.id)], 'partner_ids': [(4, self.recipient.id)],
@ -65,9 +73,7 @@ class TestMailTracking(TransactionCase):
self.assertTrue(tracking_email) self.assertTrue(tracking_email)
self.assertEqual(tracking_email.state, 'sent') self.assertEqual(tracking_email.state, 'sent')
# message_dict read by web interface # message_dict read by web interface
message_dict = message.message_read()
# First item in threads is message content
message_dict = message_dict['threads'][0][0]
message_dict = message.message_format()[0]
self.assertTrue(len(message_dict['partner_ids']) > 0) self.assertTrue(len(message_dict['partner_ids']) > 0)
# First partner is recipient # First partner is recipient
partner_id = message_dict['partner_ids'][0][0] partner_id = message_dict['partner_ids'][0][0]
@ -110,10 +116,15 @@ class TestMailTracking(TransactionCase):
mail, tracking = self.mail_send(self.recipient.email) mail, tracking = self.mail_send(self.recipient.email)
self.assertEqual(mail.email_to, tracking.recipient) self.assertEqual(mail.email_to, tracking.recipient)
self.assertEqual(mail.email_from, tracking.sender) self.assertEqual(mail.email_from, tracking.sender)
with mock.patch(mock_request) as mock_func:
mock_func.return_value = type('obj', (object,), self.request)
res = controller.mail_tracking_open(db, tracking.id) res = controller.mail_tracking_open(db, tracking.id)
self.assertEqual(image, res.response[0]) self.assertEqual(image, res.response[0])
# Two events: sent and open
self.assertEqual(2, len(tracking.tracking_event_ids))
# Fake event: tracking_email_id = False
res = controller.mail_tracking_open(db, False)
self.assertEqual(image, res.response[0])
# Two events again because no tracking_email_id found for False
self.assertEqual(2, len(tracking.tracking_event_ids))
def test_concurrent_open(self): def test_concurrent_open(self):
mail, tracking = self.mail_send(self.recipient.email) mail, tracking = self.mail_send(self.recipient.email)
@ -193,31 +204,38 @@ class TestMailTracking(TransactionCase):
self.assertEqual('error', tracking.state) self.assertEqual('error', tracking.state)
self.assertEqual('Warning', tracking.error_type) self.assertEqual('Warning', tracking.error_type)
self.assertEqual('Test error', tracking.error_description) self.assertEqual('Test error', tracking.error_description)
self.assertTrue(self.recipient.email_bounced)
def test_partner_email_change(self): def test_partner_email_change(self):
mail, tracking = self.mail_send(self.recipient.email) mail, tracking = self.mail_send(self.recipient.email)
tracking.event_create('open', {}) tracking.event_create('open', {})
orig_score = self.recipient.email_score orig_score = self.recipient.email_score
orig_count = self.recipient.tracking_emails_count
orig_email = self.recipient.email orig_email = self.recipient.email
self.recipient.email = orig_email + '2' self.recipient.email = orig_email + '2'
self.assertEqual(50.0, self.recipient.email_score) self.assertEqual(50.0, self.recipient.email_score)
self.assertEqual(0, self.recipient.tracking_emails_count)
self.recipient.email = orig_email self.recipient.email = orig_email
self.assertEqual(orig_score, self.recipient.email_score) self.assertEqual(orig_score, self.recipient.email_score)
self.assertEqual(orig_count, self.recipient.tracking_emails_count)
def test_process_hard_bounce(self): def test_process_hard_bounce(self):
mail, tracking = self.mail_send(self.recipient.email) mail, tracking = self.mail_send(self.recipient.email)
tracking.event_create('hard_bounce', {}) tracking.event_create('hard_bounce', {})
self.assertEqual('bounced', tracking.state) self.assertEqual('bounced', tracking.state)
self.assertTrue(self.recipient.email_score < 50.0)
def test_process_soft_bounce(self): def test_process_soft_bounce(self):
mail, tracking = self.mail_send(self.recipient.email) mail, tracking = self.mail_send(self.recipient.email)
tracking.event_create('soft_bounce', {}) tracking.event_create('soft_bounce', {})
self.assertEqual('soft-bounced', tracking.state) self.assertEqual('soft-bounced', tracking.state)
self.assertTrue(self.recipient.email_score < 50.0)
def test_process_delivered(self): def test_process_delivered(self):
mail, tracking = self.mail_send(self.recipient.email) mail, tracking = self.mail_send(self.recipient.email)
tracking.event_create('delivered', {}) tracking.event_create('delivered', {})
self.assertEqual('delivered', tracking.state) self.assertEqual('delivered', tracking.state)
self.assertTrue(self.recipient.email_score > 50.0)
def test_process_deferral(self): def test_process_deferral(self):
mail, tracking = self.mail_send(self.recipient.email) mail, tracking = self.mail_send(self.recipient.email)
@ -228,32 +246,42 @@ class TestMailTracking(TransactionCase):
mail, tracking = self.mail_send(self.recipient.email) mail, tracking = self.mail_send(self.recipient.email)
tracking.event_create('spam', {}) tracking.event_create('spam', {})
self.assertEqual('spam', tracking.state) self.assertEqual('spam', tracking.state)
self.assertTrue(self.recipient.email_score < 50.0)
def test_process_unsub(self): def test_process_unsub(self):
mail, tracking = self.mail_send(self.recipient.email) mail, tracking = self.mail_send(self.recipient.email)
tracking.event_create('unsub', {}) tracking.event_create('unsub', {})
self.assertEqual('unsub', tracking.state) self.assertEqual('unsub', tracking.state)
self.assertTrue(self.recipient.email_score < 50.0)
def test_process_reject(self): def test_process_reject(self):
mail, tracking = self.mail_send(self.recipient.email) mail, tracking = self.mail_send(self.recipient.email)
tracking.event_create('reject', {}) tracking.event_create('reject', {})
self.assertEqual('rejected', tracking.state) self.assertEqual('rejected', tracking.state)
self.assertTrue(self.recipient.email_score < 50.0)
def test_process_open(self): def test_process_open(self):
mail, tracking = self.mail_send(self.recipient.email) mail, tracking = self.mail_send(self.recipient.email)
tracking.event_create('open', {}) tracking.event_create('open', {})
self.assertEqual('opened', tracking.state) self.assertEqual('opened', tracking.state)
self.assertTrue(self.recipient.email_score > 50.0)
def test_process_click(self): def test_process_click(self):
mail, tracking = self.mail_send(self.recipient.email) mail, tracking = self.mail_send(self.recipient.email)
tracking.event_create('click', {}) tracking.event_create('click', {})
self.assertEqual('opened', tracking.state) self.assertEqual('opened', tracking.state)
self.assertTrue(self.recipient.email_score > 50.0)
def test_process_several_bounce(self):
for i in range(1, 10):
mail, tracking = self.mail_send(self.recipient.email)
tracking.event_create('hard_bounce', {})
self.assertEqual('bounced', tracking.state)
self.assertEqual(0.0, self.recipient.email_score)
def test_db(self): def test_db(self):
db = self.env.cr.dbname db = self.env.cr.dbname
controller = MailTrackingController() controller = MailTrackingController()
with mock.patch(mock_request) as mock_func:
mock_func.return_value = type('obj', (object,), self.request)
not_found = controller.mail_tracking_all('not_found_db') not_found = controller.mail_tracking_all('not_found_db')
self.assertEqual('NOT FOUND', not_found.response[0]) self.assertEqual('NOT FOUND', not_found.response[0])
none = controller.mail_tracking_all(db) none = controller.mail_tracking_all(db)

6
mail_tracking/views/assets.xml

@ -2,8 +2,7 @@
<!-- © 2016 Antonio Espinosa - <antonio.espinosa@tecnativa.com> <!-- © 2016 Antonio Espinosa - <antonio.espinosa@tecnativa.com>
License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). --> License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). -->
<odoo> <odoo>
<template id="assets_backend"
<template id="assets_backend"
name="mail_tracking assets" name="mail_tracking assets"
inherit_id="web.assets_backend"> inherit_id="web.assets_backend">
<xpath expr="." position="inside"> <xpath expr="." position="inside">
@ -12,6 +11,5 @@
<script type="text/javascript" <script type="text/javascript"
src="/mail_tracking/static/src/js/mail_tracking.js"/> src="/mail_tracking/static/src/js/mail_tracking.js"/>
</xpath> </xpath>
</template>
</template>
</odoo> </odoo>

15
mail_tracking/views/res_partner_view.xml

@ -24,7 +24,22 @@
<field name="email" position="after"> <field name="email" position="after">
<field name="email_score" widget="progressbar" <field name="email_score" widget="progressbar"
attrs="{'invisible': [('email', '=', False)]}"/> attrs="{'invisible': [('email', '=', False)]}"/>
<field name="email_bounced"
attrs="{'invisible': [('email', '=', False)]}"/>
</field>
</field> </field>
</record>
<record model="ir.ui.view" id="view_res_partner_filter">
<field name="name">Filter bounced partners</field>
<field name="model">res.partner</field>
<field name="inherit_id" ref="base.view_res_partner_filter"/>
<field name="arch" type="xml">
<filter name="type_company" position="after">
<separator/>
<filter string="Email bounced" name="email_bounced"
domain="[('email', '!=' , False), ('email_bounced', '=', True)]"/>
</filter>
</field> </field>
</record> </record>

Loading…
Cancel
Save