[MERGE] [IMP] mail: improved management of links in notification emails.

A new server action is defined that handles the redirection. If the user has access to the document, it opens the form view of the document. A method is called that allows to define the form view to open in some specific models (such as lead/opportunities). If the user has no access to the document, it opens the inbox, filtered on the discussion. The inbox opened is different for employees and portal users.

This server action is called by the link included in notification emails. This way the redirection Inbox/document is done dynamically at login, and not statically when generating the email.

Added tests.

bzr revid: tde@openerp.com-20130522124930-21tgj52v83hw8cj0
This commit is contained in:
Thibault Delavallée
2013-05-22 14:49:30 +02:00
14 changed files with 329 additions and 40 deletions
+20
View File
@@ -635,6 +635,26 @@ class account_invoice(osv.osv):
self.create_workflow(cr, uid, ids)
return True
# ----------------------------------------
# Mail related methods
# ----------------------------------------
def _get_formview_action(self, cr, uid, id, context=None):
""" Update form view id of action to open the invoice """
action = super(account_invoice, self)._get_formview_action(cr, uid, id, context=context)
obj = self.browse(cr, uid, id, context=context)
if obj.type == 'in_invoice':
model, view_id = self.pool.get('ir.model.data').get_object_reference(cr, uid, 'account', 'invoice_supplier_form')
action.update({
'views': [(view_id, 'form')],
})
else:
model, view_id = self.pool.get('ir.model.data').get_object_reference(cr, uid, 'account', 'invoice_form')
action.update({
'views': [(view_id, 'form')],
})
return action
# Workflow stuff
#################
+10
View File
@@ -983,6 +983,16 @@ class crm_lead(base_stage, format_address, osv.osv):
return [lead.section_id.message_get_reply_to()[0] if lead.section_id else False
for lead in self.browse(cr, SUPERUSER_ID, ids, context=context)]
def _get_formview_action(self, cr, uid, id, context=None):
action = super(crm_lead, self)._get_formview_action(cr, uid, id, context=context)
obj = self.browse(cr, uid, id, context=context)
if obj.type == 'opportunity':
model, view_id = self.pool.get('ir.model.data').get_object_reference(cr, uid, 'crm', 'crm_case_form_view_oppor')
action.update({
'views': [(view_id, 'form')],
})
return action
def message_get_suggested_recipients(self, cr, uid, ids, context=None):
recipients = super(crm_lead, self).message_get_suggested_recipients(cr, uid, ids, context=context)
try:
+34 -30
View File
@@ -28,7 +28,6 @@ from urlparse import urljoin
from openerp import tools
from openerp import SUPERUSER_ID
from openerp.osv import fields, osv
from openerp.osv.orm import except_orm
from openerp.tools.translate import _
_logger = logging.getLogger(__name__)
@@ -205,6 +204,36 @@ class mail_mail(osv.Model):
self.unlink(cr, SUPERUSER_ID, [mail.id], context=context)
return True
#------------------------------------------------------
# mail_mail formatting, tools and send mechanism
#------------------------------------------------------
# TODO in 8.0(+): maybe factorize this to enable in modules link generation
# independently of mail_mail model
# TODO in 8.0(+): factorize doc name sanitized and 'Followers of ...' formatting
# because it begins to appear everywhere
def _get_partner_access_link(self, cr, uid, mail, partner=None, context=None):
""" Generate URLs for links in mails:
- partner is an user and has read access to the document: direct link to document with model, res_id
"""
if partner and partner.user_ids:
base_url = self.pool.get('ir.config_parameter').get_param(cr, uid, 'web.base.url')
# the parameters to encode for the query and fragment part of url
query = {'db': cr.dbname}
fragment = {
'login': partner.user_ids[0].login,
'action': 'mail.action_mail_redirect',
}
if mail.notification:
fragment.update({
'message_id': mail.mail_message_id.id,
})
url = urljoin(base_url, "?%s#%s" % (urlencode(query), urlencode(fragment)))
return _("""<small>Access your messages and documents <a style='color:inherit' href="%s">in OpenERP</a></small>""") % url
else:
return None
def send_get_mail_subject(self, cr, uid, mail, force=False, partner=None, context=None):
""" If subject is void and record_name defined: '<Author> posted on <Resource>'
@@ -218,32 +247,6 @@ class mail_mail(osv.Model):
return 'Re: %s' % (mail.parent_id.subject)
return mail.subject
def send_get_mail_body_footer(self, cr, uid, mail, partner=None, context=None):
""" Return a specific footer for the ir_email body. The main purpose of this method
is to be inherited by Portal, to add modify the link for signing in, in
each notification email a partner receives.
"""
body_footer = ""
# partner is a user, link to a related document (incentive to install portal)
if partner and partner.user_ids and mail.model and mail.res_id \
and self.check_access_rights(cr, partner.user_ids[0].id, 'read', raise_exception=False):
related_user = partner.user_ids[0]
try:
self.pool[mail.model].check_access_rule(cr, related_user.id, [mail.res_id], 'read', context=context)
base_url = self.pool.get('ir.config_parameter').get_param(cr, uid, 'web.base.url')
# the parameters to encode for the query and fragment part of url
query = {'db': cr.dbname}
fragment = {
'login': related_user.login,
'model': mail.model,
'id': mail.res_id,
}
url = urljoin(base_url, "?%s#%s" % (urlencode(query), urlencode(fragment)))
body_footer = _("""<small>Access this document <a style='color:inherit' href="%s">directly in OpenERP</a></small>""") % url
except except_orm, e:
pass
return body_footer
def send_get_mail_body(self, cr, uid, mail, partner=None, context=None):
""" Return a specific ir_email body. The main purpose of this method
is to be inherited to add custom content depending on some module.
@@ -253,9 +256,10 @@ class mail_mail(osv.Model):
"""
body = mail.body_html
# add footer
body_footer = self.send_get_mail_body_footer(cr, uid, mail, partner=partner, context=context)
body = tools.append_content_to_html(body, body_footer, plaintext=False, container_tag='div')
# generate footer
link = self._get_partner_access_link(cr, uid, mail, partner, context=context)
if link:
body = tools.append_content_to_html(body, link, plaintext=False, container_tag='div')
return body
def send_get_email_dict(self, cr, uid, mail, partner=None, context=None):
+3
View File
@@ -59,6 +59,9 @@
<field name="type"/>
<field name="author_id"/>
<field name="partner_ids"/>
<field name="model"/>
<field name="res_id"/>
<field name="parent_id"/>
<filter string="To Read"
name="message_unread" help="Show messages to read"
domain="[('to_read', '=', True)]"/>
+64
View File
@@ -449,6 +449,70 @@ class mail_thread(osv.AbstractModel):
ir_attachment_obj.unlink(cr, uid, attach_ids, context=context)
return True
def _get_formview_action(self, cr, uid, id, model=None, context=None):
""" Return an action to open the document. This method is meant to be
overridden in addons that want to give specific view ids for example.
:param int id: id of the document to open
:param string model: specific model that overrides self._name
"""
return {
'type': 'ir.actions.act_window',
'res_model': model or self._name,
'view_type': 'form',
'view_mode': 'form',
'views': [(False, 'form')],
'target': 'current',
'res_id': id,
}
def _get_inbox_action_xml_id(self, cr, uid, context=None):
""" When redirecting towards the Inbox, choose which action xml_id has
to be fetched. This method is meant to be inherited, at least in portal
because portal users have a different Inbox action than classic users. """
return ('mail', 'action_mail_inbox_feeds')
def message_redirect_action(self, cr, uid, context=None):
""" For a given message, return an action that either
- opens the form view of the related document if model, res_id, and
read access to the document
- opens the Inbox with a default search on the conversation if model,
res_id
- opens the Inbox with context propagated
"""
if context is None:
context = {}
# default action is the Inbox action
self.pool.get('res.users').browse(cr, SUPERUSER_ID, uid, context=context)
act_model, act_id = self.pool.get('ir.model.data').get_object_reference(cr, uid, *self._get_inbox_action_xml_id(cr, uid, context=context))
action = self.pool.get(act_model).read(cr, uid, act_id, [])
# if msg_id specified: try to redirect to the document or fallback on the Inbox
msg_id = context.get('params', {}).get('message_id')
if not msg_id:
return action
msg = self.pool.get('mail.message').browse(cr, uid, msg_id, context=context)
if msg.model and msg.res_id:
action.update({
'context': {
'search_default_model': msg.model,
'search_default_res_id': msg.res_id,
}
})
if self.pool.get(msg.model).check_access_rights(cr, uid, 'read', raise_exception=False):
try:
model_obj = self.pool.get(msg.model)
model_obj.check_access_rule(cr, uid, [msg.res_id], 'read', context=context)
if not hasattr(model_obj, '_get_formview_action'):
action = self.pool.get('mail.thread')._get_formview_action(cr, uid, msg.res_id, model=msg.model, context=context)
else:
action = model_obj._get_formview_action(cr, uid, msg.res_id, context=context)
except (osv.except_osv, orm.except_orm):
pass
return action
#------------------------------------------------------
# Email specific
#------------------------------------------------------
+9
View File
@@ -111,6 +111,15 @@
</field>
</record>
<record id="action_mail_redirect" model="ir.actions.server">
<field name="name">Mail Redirection (Document / Inbox)</field>
<field name="condition">True</field>
<field name="model_id" ref="model_mail_thread"/>
<field name="code">action = pool.get('mail.thread').message_redirect_action(cr, uid, context)</field>
<field name="state">code</field>
<field name="type">ir.actions.server</field>
</record>
<!-- MENU -->
<!-- Top menu item -->
+7
View File
@@ -1866,7 +1866,14 @@ openerp.mail = function (session) {
this.domain = this.action.params.domain || this.action.domain || [];
this.context = _.extend(this.action.params.context || {}, this.action.context || {});
// filter some parameters that we will propagate as search_default
this.defaults = {};
for (var key in this.action.context.params) {
if (_.indexOf(['model', 'res_id'], key) == -1) {
continue;
}
this.context['search_default_' + key] = this.action.context.params[key];
}
for (var key in this.context) {
if (key.match(/^search_default_/)) {
this.defaults[key.replace(/^search_default_/, '')] = this.context[key];
+62 -2
View File
@@ -19,12 +19,15 @@
#
##############################################################################
from openerp.addons.mail.mail_mail import mail_mail
from openerp.addons.mail.mail_thread import mail_thread
from openerp.addons.mail.tests.test_mail_base import TestMailBase
from openerp.tools import mute_logger
from openerp.tools.mail import html_sanitize
class test_mail(TestMailBase):
def test_000_alias_setup(self):
""" Test basic mail.alias setup works, before trying to use them for routing """
cr, uid = self.cr, self.uid
@@ -43,7 +46,6 @@ class test_mail(TestMailBase):
self.user_barty = self.res_users.browse(cr, uid, self.user_barty_id)
self.assertEquals(self.user_barty.alias_name, 'b4r+_-_r3wl-', 'Disallowed chars should be replaced by hyphens')
def test_00_followers_function_field(self):
""" Tests designed for the many2many function field 'follower_ids'.
We will test to perform writes using the many2many commands 0, 3, 4,
@@ -226,6 +228,64 @@ class test_mail(TestMailBase):
self.assertNotIn('First answer, should not be displayed', result, 'Old answer should not be in quote.')
self.assertNotIn('My answer I am propagating', result, 'Thread header content should be in quote.')
def test_11_notification_url(self):
""" Tests designed to test the URL added in notification emails. """
cr, uid, group_pigs = self.cr, self.uid, self.group_pigs
# Partner data
partner_raoul = self.res_partner.browse(cr, uid, self.partner_raoul_id)
partner_bert_id = self.res_partner.create(cr, uid, {'name': 'bert'})
partner_bert = self.res_partner.browse(cr, uid, partner_bert_id)
# Mail data
mail_mail_id = self.mail_mail.create(cr, uid, {'state': 'exception'})
mail = self.mail_mail.browse(cr, uid, mail_mail_id)
# Test: link for nobody -> None
url = mail_mail._get_partner_access_link(self.mail_mail, cr, uid, mail)
self.assertEqual(url, None,
'notification email: mails not send to a specific partner should not have any URL')
# Test: link for partner -> None
url = mail_mail._get_partner_access_link(self.mail_mail, cr, uid, mail, partner=partner_bert)
self.assertEqual(url, None,
'notification email: mails send to a not-user partner should not have any URL')
# Test: link for user -> signin
url = mail_mail._get_partner_access_link(self.mail_mail, cr, uid, mail, partner=partner_raoul)
self.assertIn('action=mail.action_mail_redirect', url,
'notification email: link should contain the redirect action')
self.assertIn('login=%s' % partner_raoul.user_ids[0].login, url,
'notification email: link should contain the user login')
@mute_logger('openerp.addons.mail.mail_thread', 'openerp.osv.orm')
def test_12_inbox_redirection(self):
""" Tests designed to test the inbox redirection of emails notification URLs. """
cr, uid, user_admin, group_pigs = self.cr, self.uid, self.user_admin, self.group_pigs
model, act_id = self.ir_model_data.get_object_reference(cr, uid, 'mail', 'action_mail_inbox_feeds')
# Data: post a message on pigs
msg_id = self.group_pigs.message_post(body='My body', partner_ids=[self.partner_bert_id], type='comment', subtype='mail.mt_comment')
# No specific parameters -> should redirect to Inbox
action = mail_thread.message_redirect_action(self.mail_thread, cr, self.user_raoul_id, {'params': {}})
self.assertEqual(action.get('type'), 'ir.actions.client',
'URL redirection: action without parameters should redirect to client action Inbox')
self.assertEqual(action.get('id'), act_id,
'URL redirection: action without parameters should redirect to client action Inbox')
# Bert has read access to Pigs -> should redirect to form view of Pigs
action = mail_thread.message_redirect_action(self.mail_thread, cr, self.user_raoul_id, {'params': {'message_id': msg_id}})
self.assertEqual(action.get('type'), 'ir.actions.act_window',
'URL redirection: action with message_id for read-accredited user should redirect to Pigs')
self.assertEqual(action.get('res_id'), group_pigs.id,
'URL redirection: action with message_id for read-accredited user should redirect to Pigs')
# Bert has no read access to Pigs -> should redirect to Inbox
action = mail_thread.message_redirect_action(self.mail_thread, cr, self.user_bert_id, {'params': {'message_id': msg_id}})
self.assertEqual(action.get('type'), 'ir.actions.client',
'URL redirection: action without parameters should redirect to client action Inbox')
self.assertEqual(action.get('id'), act_id,
'URL redirection: action without parameters should redirect to client action Inbox')
def test_20_message_post(self):
""" Tests designed for message_post. """
cr, uid, user_raoul, group_pigs = self.cr, self.uid, self.user_raoul, self.group_pigs
+3
View File
@@ -20,6 +20,7 @@
##############################################################################
from openerp.addons.mail.tests.test_mail_base import TestMailBase
from openerp.tools import mute_logger
MAIL_TEMPLATE = """Return-Path: <whatever-2a840@postmaster.twitter.com>
To: {to}
@@ -223,6 +224,7 @@ class TestMailgateway(TestMailBase):
self.assertEqual(mail.reply_to, 'someone@example.com',
'mail_mail: reply_to should equal the rpely_to given to create')
@mute_logger('openerp.addons.mail.mail_thread', 'openerp.osv.orm')
def test_10_message_process(self):
""" Testing incoming emails processing. """
cr, uid, user_raoul = self.cr, self.uid, self.user_raoul
@@ -465,6 +467,7 @@ class TestMailgateway(TestMailBase):
self.assertEqual(msg.body, '<pre>\nPlease call me as soon as possible this afternoon!\n\n--\nSylvie\n</pre>',
'message_process: plaintext incoming email incorrectly parsed')
@mute_logger('openerp.addons.mail.mail_thread', 'openerp.osv.orm')
def test_20_thread_parent_resolution(self):
""" Testing parent/child relationships are correctly established when processing incoming mails """
cr, uid = self.cr, self.uid
+1
View File
@@ -20,6 +20,7 @@
##############################################################################
import portal
import mail_thread
import mail_mail
import mail_message
import wizard
+5 -6
View File
@@ -28,11 +28,10 @@ class mail_mail(osv.Model):
""" Update of mail_mail class, to add the signin URL to notifications. """
_inherit = 'mail.mail'
def send_get_mail_body_footer(self, cr, uid, mail, partner=None, context=None):
""" add a signin link inside the body of a mail.mail
:param mail: mail.mail browse_record
:param partner: browse_record of the specific recipient partner
:return: the resulting body_html
def _get_partner_access_link(self, cr, uid, mail, partner=None, context=None):
""" Generate URLs for links in mails:
- partner is not an user: signup_url
- partner is an user: fallback on classic URL
"""
if context is None:
context = {}
@@ -44,4 +43,4 @@ class mail_mail(osv.Model):
context=contex_signup)[partner.id]
return _("""<small>Access your messages and documents through <a style='color:inherit' href="%s">our Customer Portal</a></small>""") % signup_url
else:
return super(mail_mail, self).send_get_mail_body_footer(cr, uid, mail, partner=partner, context=context)
return super(mail_mail, self)._get_partner_access_link(cr, uid, mail, partner=partner, context=context)
+43
View File
@@ -0,0 +1,43 @@
# -*- coding: utf-8 -*-
##############################################################################
#
# OpenERP, Open Source Management Solution
# Copyright (C) 2013-TODAY OpenERP S.A (<http://www.openerp.com>).
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
##############################################################################
from openerp import SUPERUSER_ID
from openerp.osv import osv
class mail_thread(osv.AbstractModel):
""" Update of mail_mail class, to add the signin URL to notifications. """
_inherit = 'mail.thread'
def _get_inbox_action_xml_id(self, cr, uid, context=None):
""" For a given message, return an action that either
- opens the form view of the related document if model, res_id, and
read access to the document
- opens the Inbox with a default search on the conversation if model,
res_id
- opens the Inbox with context propagated
"""
cur_user = self.pool.get('res.users').browse(cr, SUPERUSER_ID, uid, context=context)
# if uid is a portal user -> action is different
if any(group.is_portal for group in cur_user.groups_id):
return ('portal', 'action_mail_inbox_feeds_portal')
else:
return super(mail_thread, self)._get_inbox_action_xml_id(cr, uid, context=context)
+1 -1
View File
@@ -3,7 +3,7 @@ access_mail_message_portal,mail.message.portal,mail.model_mail_message,group_por
access_mail_mail_portal,mail.mail.portal,mail.model_mail_mail,group_portal,1,1,1,0
access_mail_notification_portal,mail.notification.portal,mail.model_mail_notification,group_portal,1,1,1,0
access_mail_followers_portal,mail.followers.portal,mail.model_mail_followers,group_portal,1,1,0,0
access_res_partner,res.partner,base.model_res_partner,portal.group_portal,1,0,0,0
access_res_partner_portal,res.partner.portal,base.model_res_partner,portal.group_portal,1,0,0,0
access_acquirer,portal.payment.acquirer,portal.model_portal_payment_acquirer,,1,0,0,0
access_acquirer_all,portal.payment.acquirer,portal.model_portal_payment_acquirer,base.group_system,1,1,1,1
access_ir_attachment_group_portal,ir.attachment group_portal,base.model_ir_attachment,portal.group_portal,1,0,1,0
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
3 access_mail_mail_portal mail.mail.portal mail.model_mail_mail group_portal 1 1 1 0
4 access_mail_notification_portal mail.notification.portal mail.model_mail_notification group_portal 1 1 1 0
5 access_mail_followers_portal mail.followers.portal mail.model_mail_followers group_portal 1 1 0 0
6 access_res_partner access_res_partner_portal res.partner res.partner.portal base.model_res_partner portal.group_portal 1 0 0 0
7 access_acquirer portal.payment.acquirer portal.model_portal_payment_acquirer 1 0 0 0
8 access_acquirer_all portal.payment.acquirer portal.model_portal_payment_acquirer base.group_system 1 1 1 1
9 access_ir_attachment_group_portal ir.attachment group_portal base.model_ir_attachment portal.group_portal 1 0 1 0
+67 -1
View File
@@ -133,7 +133,73 @@ class test_portal(TestMailBase):
self.assertTrue(partner_carine.signup_url in sent_email.get('body'),
'invite: body of invitation email does not contain signup url')
def test_20_message_read(self):
def test_20_notification_url(self):
""" Tests designed to test the URL added in notification emails. """
cr, uid, group_pigs = self.cr, self.uid, self.group_pigs
# Partner data
partner_raoul = self.res_partner.browse(cr, uid, self.partner_raoul_id)
partner_bert_id = self.res_partner.create(cr, uid, {'name': 'bert'})
partner_bert = self.res_partner.browse(cr, uid, partner_bert_id)
# Mail data
mail_mail_id = self.mail_mail.create(cr, uid, {'state': 'exception'})
mail = self.mail_mail.browse(cr, uid, mail_mail_id)
# Test: link for nobody -> None
url = self.mail_mail._get_partner_access_link(cr, uid, mail)
self.assertEqual(url, None,
'notification email: mails not send to a specific partner should not have any URL')
# Test: link for partner -> signup URL
url = self.mail_mail._get_partner_access_link(cr, uid, mail, partner=partner_bert)
self.assertIn(partner_bert.signup_url, url,
'notification email: mails send to a not-user partner should contain the signup URL')
# Test: link for user -> signin
url = self.mail_mail._get_partner_access_link(cr, uid, mail, partner=partner_raoul)
self.assertIn('action=mail.action_mail_redirect', url,
'notification email: link should contain the redirect action')
self.assertIn('login=%s' % partner_raoul.user_ids[0].login, url,
'notification email: link should contain the user login')
@mute_logger('openerp.addons.mail.mail_thread', 'openerp.osv.orm')
def test_21_inbox_redirection(self):
""" Tests designed to test the inbox redirection of emails notification URLs. """
cr, uid, user_admin, group_pigs = self.cr, self.uid, self.user_admin, self.group_pigs
model, act_id = self.ir_model_data.get_object_reference(cr, uid, 'mail', 'action_mail_inbox_feeds')
model, port_act_id = self.ir_model_data.get_object_reference(cr, uid, 'portal', 'action_mail_inbox_feeds_portal')
# Data: post a message on pigs
msg_id = self.group_pigs.message_post(body='My body', partner_ids=[self.partner_bert_id, self.partner_chell_id], type='comment', subtype='mail.mt_comment')
# No specific parameters -> should redirect to Inbox
action = self.mail_thread.message_redirect_action(cr, self.user_raoul_id, {'params': {}})
self.assertEqual(action.get('type'), 'ir.actions.client',
'URL redirection: action without parameters should redirect to client action Inbox')
self.assertEqual(action.get('id'), act_id,
'URL redirection: action without parameters should redirect to client action Inbox')
# Bert has read access to Pigs -> should redirect to form view of Pigs
action = self.mail_thread.message_redirect_action(cr, self.user_raoul_id, {'params': {'message_id': msg_id}})
self.assertEqual(action.get('type'), 'ir.actions.act_window',
'URL redirection: action with message_id for read-accredited user should redirect to Pigs')
self.assertEqual(action.get('res_id'), group_pigs.id,
'URL redirection: action with message_id for read-accredited user should redirect to Pigs')
# Bert has no read access to Pigs -> should redirect to Inbox
action = self.mail_thread.message_redirect_action(cr, self.user_bert_id, {'params': {'message_id': msg_id}})
self.assertEqual(action.get('type'), 'ir.actions.client',
'URL redirection: action without parameters should redirect to client action Inbox')
self.assertEqual(action.get('id'), act_id,
'URL redirection: action without parameters should redirect to client action Inbox')
# Chell has no read access to pigs -> should redirect to Portal Inbox
action = self.mail_thread.message_redirect_action(cr, self.user_chell_id, {'params': {'message_id': msg_id}})
self.assertEqual(action.get('type'), 'ir.actions.client',
'URL redirection: action without parameters should redirect to client action Inbox')
self.assertEqual(action.get('id'), port_act_id,
'URL redirection: action without parameters should redirect to client action Inbox')
def test_30_message_read(self):
cr, uid, group_port_id = self.cr, self.uid, self.group_port_id
# Data: custom subtypes