[MERGE] mail, various: improve post and notification API and performances

Purpose of this merge is to clean and improve performances of post and
notification mechanism contained in mail application.

In this merge we provide

* centralization of notification code inside mail.thread instead of being
dispatched in mail.thread, mail.message and res.partner;
* simplification of call chain, removal of unnecessary methods;
* better propagation of messages value through the various calls in order
to use given value and avoid browse and cache miss adding queries;
* optimization of attachments management in post;
* cleaning of message_post now limited to records and simplified in its
management of parameter;
* cleaning and simplification of recipients grouping for email notification;
* cleaning of message_notify;
* various code cleaning and tweaking in order to reduce query count;

Performance tests indicate number of queries did fall, especially in
use cases involving management of followers and attachments. See sub
commits for more details. This merge is linked to task ID 1943901.

closes odoo/odoo#32404

Signed-off-by: Thibault Delavallee (tde) <tde@openerp.com>
This commit is contained in:
Robot Odoo
2019-05-29 23:27:53 +02:00
committed by GitHub
48 changed files with 1372 additions and 1142 deletions
+4 -5
View File
@@ -1060,17 +1060,16 @@ class AccountInvoice(models.Model):
return self.filtered(lambda inv: inv.state != 'cancel').action_cancel()
@api.multi
def _notify_get_groups(self, message, groups):
def _notify_get_groups(self):
""" Give access button to users and portal customer as portal is integrated
in account. Customer and portal group have probably no right to see
the document so they don't have the access button. """
groups = super(AccountInvoice, self)._notify_get_groups(message, groups)
groups = super(AccountInvoice, self)._notify_get_groups()
if self.state not in ('draft', 'cancel'):
for group_name, group_method, group_data in groups:
if group_name in ('customer', 'portal'):
continue
group_data['has_button_access'] = True
if group_name not in ('customer', 'portal'):
group_data['has_button_access'] = True
return groups
@@ -252,12 +252,13 @@ class AccountInvoice(models.Model):
# Handle both _Attachment namedtuple in mail.thread or ir.attachment.
return hasattr(attachment, 'content') and getattr(attachment, 'content') or base64.b64decode(attachment.datas)
filename = _get_attachment_filename(attachment)
content = _get_attachment_content(attachment)
# Check if the attachment is a pdf.
if not filename.endswith('.pdf'):
return
content = _get_attachment_content(attachment)
with io.BytesIO(content) as buffer:
try:
reader = PdfFileReader(buffer)
+4 -2
View File
@@ -227,7 +227,8 @@ class Attendee(models.Model):
""" Marks event invitation as Accepted. """
result = self.write({'state': 'accepted'})
for attendee in self:
attendee.event_id.message_post(body=_("%s has accepted invitation") % (attendee.common_name), subtype="calendar.subtype_invitation")
if attendee.event_id:
attendee.event_id.message_post(body=_("%s has accepted invitation") % (attendee.common_name), subtype="calendar.subtype_invitation")
return result
@api.multi
@@ -235,7 +236,8 @@ class Attendee(models.Model):
""" Marks event invitation as Declined. """
res = self.write({'state': 'declined'})
for attendee in self:
attendee.event_id.message_post(body=_("%s has declined invitation") % (attendee.common_name), subtype="calendar.subtype_invitation")
if attendee.event_id:
attendee.event_id.message_post(body=_("%s has declined invitation") % (attendee.common_name), subtype="calendar.subtype_invitation")
return res
+4 -4
View File
@@ -1114,10 +1114,10 @@ class Lead(models.Model):
return super(Lead, self)._track_subtype(init_values)
@api.multi
def _notify_get_groups(self, message, groups):
def _notify_get_groups(self):
""" Handle salesman recipients that can convert leads into opportunities
and set opportunities as won / lost. """
groups = super(Lead, self)._notify_get_groups(message, groups)
groups = super(Lead, self)._notify_get_groups()
self.ensure_one()
if self.type == 'lead':
@@ -1211,7 +1211,7 @@ class Lead(models.Model):
defaults['company_id'] = self.env['crm.team'].browse(defaults['team_id']).company_id.id
return super(Lead, self).message_new(msg_dict, custom_values=defaults)
def _message_post_after_hook(self, message, *args, **kwargs):
def _message_post_after_hook(self, message, msg_vals):
if self.email_from and not self.partner_id:
# we consider that posting a message with a specified recipient (not a follower, a specific one)
# on a document without customer means that it was created through the chatter using
@@ -1222,7 +1222,7 @@ class Lead(models.Model):
('partner_id', '=', False),
('email_from', '=', new_partner.email),
('stage_id.fold', '=', False)]).write({'partner_id': new_partner.id})
return super(Lead, self)._message_post_after_hook(message, *args, **kwargs)
return super(Lead, self)._message_post_after_hook(message, msg_vals)
@api.multi
def _message_partner_info_from_emails(self, emails, link_mail=False):
+2 -2
View File
@@ -503,7 +503,7 @@ class EventRegistration(models.Model):
'email_cc': False}
for r in self}
def _message_post_after_hook(self, message, *args, **kwargs):
def _message_post_after_hook(self, message, msg_vals):
if self.email and not self.partner_id:
# we consider that posting a message with a specified recipient (not a follower, a specific one)
# on a document without customer means that it was created through the chatter using
@@ -515,7 +515,7 @@ class EventRegistration(models.Model):
('email', '=', new_partner.email),
('state', 'not in', ['cancel']),
]).write({'partner_id': new_partner.id})
return super(EventRegistration, self)._message_post_after_hook(message, *args, **kwargs)
return super(EventRegistration, self)._message_post_after_hook(message, msg_vals)
@api.multi
def action_send_badge_email(self):
+6 -6
View File
@@ -567,7 +567,7 @@ class Challenge(models.Model):
body=body_html,
partner_ids=challenge.mapped('user_ids.partner_id.id'),
subtype='mail.mt_comment',
notif_layout='mail.mail_notification_light',
email_layout_xmlid='mail.mail_notification_light',
)
if challenge.report_message_group_id:
challenge.report_message_group_id.message_post(
@@ -586,18 +586,18 @@ class Challenge(models.Model):
'gamification.challenge',
challenge.id)
# send message only to users, not on the challenge
self.env['gamification.challenge'].message_post(
# notify message only to users, do not post on the challenge
challenge.message_notify(
body=body_html,
partner_ids=[(4, user.partner_id.id)],
partner_ids=[user.partner_id.id],
subtype='mail.mt_comment',
notif_layout='mail.mail_notification_light',
email_layout_xmlid='mail.mail_notification_light',
)
if challenge.report_message_group_id:
challenge.report_message_group_id.message_post(
body=body_html,
subtype='mail.mt_comment',
notif_layout='mail.mail_notification_light',
email_layout_xmlid='mail.mail_notification_light',
)
return challenge.write({'last_report_date': fields.Date.today()})
+2 -3
View File
@@ -222,12 +222,11 @@ class Goal(models.Model):
.get_email_template(self.id)
body_html = self.env['mail.template'].with_context(template._context)\
._render_template(template.body_html, 'gamification.goal', self.id)
# TDE NOTE: should be a notification (see with xdo)
self.env['mail.thread'].message_post(
self.message_notify(
body=body_html,
partner_ids=[self.user_id.partner_id.id],
subtype='mail.mt_comment',
notif_layout='mail.mail_notification_light',
email_layout_xmlid='mail.mail_notification_light',
)
return {'to_update': True}
+3 -3
View File
@@ -280,7 +280,7 @@ class Employee(models.Model):
tools.image_resize_images(vals)
employee = super(Employee, self).create(vals)
url = '/web#%s' % url_encode({'action': 'hr.plan_wizard_action', 'active_id': employee.id, 'active_model': 'hr.employee'})
employee._message_log(_('<b>Congratulations !</b> May I recommand you to setup an <a href="%s">onboarding plan ?</a>') % (url))
employee._message_log(body=_('<b>Congratulations !</b> May I recommand you to setup an <a href="%s">onboarding plan ?</a>') % (url))
if employee.department_id:
self.env['mail.channel'].sudo().search([
('subscription_department_ids', 'in', employee.department_id.id)
@@ -363,8 +363,8 @@ class Employee(models.Model):
self = self.sudo(real_user)
return self
def _message_log(self, *args, **kwargs):
return super(Employee, self._post_author())._message_log(*args, **kwargs)
def _message_log(self, **kwargs):
return super(Employee, self._post_author())._message_log(**kwargs)
@api.multi
@api.returns('mail.message', lambda value: value.id)
+1 -1
View File
@@ -532,7 +532,7 @@ class HrExpense(models.Model):
subject='Re: %s' % msg_dict.get('subject', ''),
body=body,
subtype_id=self.env.ref('mail.mt_note').id,
notif_layout='mail.mail_notification_light',
email_layout_xmlid='mail.mail_notification_light',
)
else:
self.env['mail.mail'].create({
+2 -2
View File
@@ -851,10 +851,10 @@ class HolidaysRequest(models.Model):
return super(HolidaysRequest, self)._track_subtype(init_values)
@api.multi
def _notify_get_groups(self, message, groups):
def _notify_get_groups(self):
""" Handle HR users and officers recipients that can validate or refuse holidays
directly from email. """
groups = super(HolidaysRequest, self)._notify_get_groups(message, groups)
groups = super(HolidaysRequest, self)._notify_get_groups()
self.ensure_one()
hr_actions = []
@@ -611,10 +611,10 @@ class HolidaysAllocation(models.Model):
return super(HolidaysAllocation, self)._track_subtype(init_values)
@api.multi
def _notify_get_groups(self, message, groups):
def _notify_get_groups(self):
""" Handle HR users and officers recipients that can validate or refuse holidays
directly from email. """
groups = super(HolidaysAllocation, self)._notify_get_groups(message, groups)
groups = super(HolidaysAllocation, self)._notify_get_groups()
self.ensure_one()
hr_actions = []
@@ -353,7 +353,7 @@ class Applicant(models.Model):
res['stage_id'] = (applicant.stage_id.template_id, {
'auto_delete_message': True,
'subtype_id': self.env['ir.model.data'].xmlid_to_res_id('mail.mt_note'),
'notif_layout': 'mail.mail_notification_light'
'email_layout_xmlid': 'mail.mail_notification_light'
})
return res
@@ -414,7 +414,7 @@ class Applicant(models.Model):
defaults.update(custom_values)
return super(Applicant, self).message_new(msg, custom_values=defaults)
def _message_post_after_hook(self, message, *args, **kwargs):
def _message_post_after_hook(self, message, msg_vals):
if self.email_from and not self.partner_id:
# we consider that posting a message with a specified recipient (not a follower, a specific one)
# on a document without customer means that it was created through the chatter using
@@ -425,7 +425,7 @@ class Applicant(models.Model):
('partner_id', '=', False),
('email_from', '=', new_partner.email),
('stage_id.fold', '=', False)]).write({'partner_id': new_partner.id})
return super(Applicant, self)._message_post_after_hook(message, *args, **kwargs)
return super(Applicant, self)._message_post_after_hook(message, msg_vals)
@api.multi
def create_employee_from_applicant(self):
+2 -2
View File
@@ -48,7 +48,7 @@ class MailChannel(models.Model):
record.is_chat = True
@api.multi
def _channel_message_notifications(self, message):
def _channel_message_notifications(self, message, message_format=False):
""" When a anonymous user create a mail.channel, the operator is not notify (to avoid massive polling when
clicking on livechat button). So when the anonymous person is sending its FIRST message, the channel header
should be added to the notification, since the user cannot be listining to the channel.
@@ -56,7 +56,7 @@ class MailChannel(models.Model):
livechat_channels = self.filtered(lambda x: x.channel_type == 'livechat')
other_channels = self.filtered(lambda x: x.channel_type != 'livechat')
notifications = super(MailChannel, livechat_channels)._channel_message_notifications(message.with_context(im_livechat_use_username=True)) + \
super(MailChannel, other_channels)._channel_message_notifications(message)
super(MailChannel, other_channels)._channel_message_notifications(message, message_format)
for channel in self:
# add uuid for private livechat channels to allow anonymous to listen
if channel.channel_type == 'livechat' and channel.public == 'private':
+10 -8
View File
@@ -386,14 +386,16 @@ class MailActivity(models.Model):
engine='ir.qweb',
minimal_qcontext=True
)
self.env['mail.thread'].message_notify(
partner_ids=activity.user_id.partner_id.ids,
body=body,
subject=_('%s: %s assigned to you') % (activity.res_name, activity.summary or activity.activity_type_id.name),
record_name=activity.res_name,
model_description=model_description,
notif_layout='mail.mail_notification_light'
)
record = self.env[activity.res_model].browse(activity.res_id)
if activity.user_id:
record.message_notify(
partner_ids=activity.user_id.partner_id.ids,
body=body,
subject=_('%s: %s assigned to you') % (activity.res_name, activity.summary or activity.activity_type_id.name),
record_name=activity.res_name,
model_description=model_description,
email_layout_xmlid='mail.mail_notification_light',
)
@api.multi
def action_done(self):
+24 -31
View File
@@ -301,28 +301,24 @@ class Channel(models.Model):
if not self.email_send:
notification = _('<div class="o_mail_notification">left <a href="#" class="o_channel_redirect" data-oe-id="%s">#%s</a></div>') % (self.id, self.name,)
# post 'channel left' message as root since the partner just unsubscribed from the channel
self.sudo().message_post(body=notification, message_type="notification", subtype="mail.mt_comment", author_id=partner.id)
self.sudo().message_post(body=notification, subtype="mail.mt_comment", author_id=partner.id)
return result
@api.multi
def _notify_get_groups(self, message, groups):
def _notify_get_groups(self):
""" All recipients of a message on a channel are considered as partners.
This means they will receive a minimal email, without a link to access
in the backend. Mailing lists should indeed send minimal emails to avoid
the noise. """
groups = super(Channel, self)._notify_get_groups(message, groups)
groups = super(Channel, self)._notify_get_groups()
for (index, (group_name, group_func, group_data)) in enumerate(groups):
if group_name != 'customer':
groups[index] = (group_name, lambda partner: False, group_data)
return groups
@api.multi
def _notify_specific_email_values(self, message):
res = super(Channel, self)._notify_specific_email_values(message)
try:
headers = safe_eval(res.get('headers', dict()))
except Exception:
headers = {}
def _notify_email_header_dict(self):
headers = super(Channel, self)._notify_email_header_dict()
headers['Precedence'] = 'list'
# avoid out-of-office replies from MS Exchange
# http://blogs.technet.com/b/exchange/archive/2006/10/06/3395024.aspx
@@ -334,8 +330,7 @@ class Channel(models.Model):
# X-Forge-To: will replace To: after SMTP envelope is determined by ir.mail.server
list_to = '"%s" <%s@%s>' % (self.name, self.alias_name, self.alias_domain)
headers['X-Forge-To'] = list_to
res['headers'] = repr(headers)
return res
return headers
@api.multi
def _message_receive_bounce(self, email, partner, mail_id=None):
@@ -346,16 +341,16 @@ class Channel(models.Model):
return super(Channel, self)._message_receive_bounce(email, partner, mail_id=mail_id)
@api.multi
def _notify_email_recipients(self, message, recipient_ids):
def _notify_email_recipient_values(self, recipient_ids):
# Excluded Blacklisted
whitelist = self.env['res.partner'].sudo().search([('id', 'in', recipient_ids)]).filtered(lambda p: not p.is_blacklisted)
whitelist = self.env['res.partner'].sudo().browse(recipient_ids).filtered(lambda p: not p.is_blacklisted)
# real mailing list: multiple recipients (hidden by X-Forge-To)
if self.alias_domain and self.alias_name:
return {
'email_to': ','.join(formataddr((partner.name, partner.email_normalized)) for partner in whitelist if partner.email_normalized),
'recipient_ids': [],
}
return super(Channel, self)._notify_email_recipients(message, whitelist.ids)
return super(Channel, self)._notify_email_recipient_values(whitelist.ids)
def _extract_moderation_values(self, message_type, **kwargs):
""" This method is used to compute moderation status before the creation
@@ -511,33 +506,31 @@ class Channel(models.Model):
notifications.append([(self._cr.dbname, 'res.partner', partner.id), channel_info])
return notifications
@api.multi
def _notify(self, message):
""" Broadcast the given message on the current channels.
Send the message on the Bus Channel (uuid for public mail.channel, and partner private bus channel (the tuple)).
A partner will receive only on message on its bus channel, even if this message belongs to multiple mail channel. Then 'channel_ids' field
of the received message indicates on wich mail channel the message should be displayed.
:param : mail.message to broadcast
"""
if not self:
return
message.ensure_one()
notifications = self._channel_message_notifications(message)
self.env['bus.bus'].sendmany(notifications)
def _notify_thread(self, message, msg_vals=False, model_description=False, mail_auto_delete=True):
# When posting a message on a mail channel, manage moderation and postpone notify users
if not msg_vals or msg_vals.get('moderation_status') != 'pending_moderation':
super(Channel, self)._notify_thread(
message,
msg_vals=msg_vals,
model_description=model_description,
mail_auto_delete=mail_auto_delete,
)
else:
message._notify_pending_by_chat()
@api.multi
def _channel_message_notifications(self, message):
def _channel_message_notifications(self, message, message_format=False):
""" Generate the bus notifications for the given message
:param message : the mail.message to sent
:returns list of bus notifications (tuple (bus_channe, message_content))
"""
message_values = message.message_format()[0]
message_format = message_format or message.message_format()[0]
notifications = []
for channel in self:
notifications.append([(self._cr.dbname, 'mail.channel', channel.id), dict(message_values)])
notifications.append([(self._cr.dbname, 'mail.channel', channel.id), dict(message_format)])
# add uuid to allow anonymous to listen
if channel.public == 'public':
notifications.append([channel.uuid, dict(message_values)])
notifications.append([channel.uuid, dict(message_format)])
return notifications
@api.model
+2 -2
View File
@@ -168,7 +168,7 @@ class MailMail(models.Model):
'failure_type': failure_type,
'failure_reason': failure_reason,
})
messages = notifications.mapped('mail_message_id').filtered(lambda m: m.res_id and m.model)
messages = notifications.mapped('mail_message_id').filtered(lambda m: m.is_thread_message())
messages._notify_failure_update() # notify user that we have a failure
(notifications - failed).sudo().write({
'email_status': 'sent',
@@ -312,7 +312,7 @@ class MailMail(models.Model):
bounce_alias = ICP.get_param("mail.bounce.alias")
catchall_domain = ICP.get_param("mail.catchall.domain")
if bounce_alias and catchall_domain:
if mail.model and mail.res_id:
if mail.mail_message_id.is_thread_message():
headers['Return-Path'] = '%s+%d-%s-%d@%s' % (bounce_alias, mail.id, mail.model, mail.res_id, catchall_domain)
else:
headers['Return-Path'] = '%s+%d@%s' % (bounce_alias, mail.id, catchall_domain)
+156 -195
View File
@@ -59,7 +59,8 @@ class Message(models.Model):
message_type = fields.Selection([
('email', 'Email'),
('comment', 'Comment'),
('notification', 'System notification')],
('notification', 'System notification'),
('user_notification', 'User Specific Notification')],
'Type', required=True, default='email',
help="Message type: email for email message, notification for system "
"message, comment for other messages such as user replies",
@@ -79,8 +80,7 @@ class Message(models.Model):
author_avatar = fields.Binary("Author's avatar", related='author_id.image_small', readonly=False)
# recipients: include inactive partners (they may have been archived after
# the message was sent, but they should remain visible in the relation)
partner_ids = fields.Many2many('res.partner', string='Recipients',
context={'active_test': False})
partner_ids = fields.Many2many('res.partner', string='Recipients', context={'active_test': False})
needaction_partner_ids = fields.Many2many(
'res.partner', 'mail_message_res_partner_needaction_rel', string='Partners with Need Action',
context={'active_test': False})
@@ -124,7 +124,7 @@ class Message(models.Model):
moderator_id = fields.Many2one('res.users', string="Moderated By", index=True)
need_moderation = fields.Boolean('Need moderation', compute='_compute_need_moderation', search='_search_need_moderation')
#keep notification layout informations to be able to generate mail again
layout = fields.Char('Layout', copy=False) # xml id of layout
email_layout_xmlid = fields.Char('Layout', copy=False, oldname='layout') # xml id of layout
add_sign = fields.Boolean(default=True)
@api.multi
@@ -393,7 +393,8 @@ class Message(models.Model):
customer_email_data.append((partner_tree[notification.res_partner_id.id][0], partner_tree[notification.res_partner_id.id][1], notification.email_status))
has_access_to_model = message.model and self.env[message.model].check_access_rights('read', raise_exception=False)
main_attachment = has_access_to_model and message.res_id and self.env[message.model].search([('id', '=',message.res_id)]) and getattr(self.env[message.model].browse(message.res_id), 'message_main_attachment_id')
if message.attachment_ids and message.res_id and issubclass(self.pool[message.model], self.pool['mail.thread']) and has_access_to_model:
main_attachment = self.env[message.model].browse(message.res_id).message_main_attachment_id
attachment_ids = []
for attachment in message.attachment_ids:
if attachment.id in attachments_tree:
@@ -417,7 +418,13 @@ class Message(models.Model):
@api.multi
def message_fetch_failed(self):
messages = self.search([('has_error', '=', True), ('author_id.id', '=', self.env.user.partner_id.id), ('res_id', '!=', 0), ('model', '!=', False)])
messages = self.search([
('has_error', '=', True),
('author_id.id', '=', self.env.user.partner_id.id),
('res_id', '!=', 0),
('model', '!=', False),
('message_type', '!=', 'user_notification')
])
return messages._format_mail_failures()
@api.model
@@ -515,7 +522,7 @@ class Message(models.Model):
message['needaction_partner_ids'] = notif_dict.get(message['id'], dict()).get('partner_id', [])
message['is_note'] = message['subtype_id'] and subtypes_dict[message['subtype_id'][0]]['id'] == note_id
message['is_discussion'] = message['subtype_id'] and subtypes_dict[message['subtype_id'][0]]['id'] == com_id
message['is_notification'] = message['is_note'] and not message['model'] and not message['res_id']
message['is_notification'] = message['message_type'] == 'user_notification'
message['subtype_description'] = message['subtype_id'] and subtypes_dict[message['subtype_id'][0]]['description']
if message['model'] and self.env[message['model']]._original_module:
message['module_icon'] = modules.module.get_module_icon(self.env[message['model']]._original_module)
@@ -583,6 +590,19 @@ class Message(models.Model):
if not self._cr.fetchone():
self._cr.execute("""CREATE INDEX mail_message_model_res_id_idx ON mail_message (model, res_id)""")
@api.multi
def is_thread_message(self, vals=None):
if vals:
res_id = vals.get('res_id')
model = vals.get('model')
message_type = vals.get('message_type')
else:
self.ensure_one()
res_id = self.res_id
model = self.model
message_type = self.message_type
return res_id and model and message_type != 'user_notification'
@api.model
def _find_allowed_model_wise(self, doc_model, doc_dict):
doc_ids = list(doc_dict)
@@ -640,7 +660,7 @@ class Message(models.Model):
super(Message, self.sudo(access_rights_uid or self._uid)).check_access_rights('read')
self._cr.execute("""
SELECT DISTINCT m.id, m.model, m.res_id, m.author_id,
SELECT DISTINCT m.id, m.model, m.res_id, m.author_id, m.message_type,
COALESCE(partner_rel.res_partner_id, needaction_rel.res_partner_id),
channel_partner.channel_id as channel_id
FROM "%s" m
@@ -656,14 +676,14 @@ class Message(models.Model):
ON channel_partner.channel_id = channel.id AND channel_partner.partner_id = %%(pid)s
WHERE m.id = ANY (%%(ids)s)""" % self._table, dict(pid=pid, ids=ids))
for id, rmod, rid, author_id, partner_id, channel_id in self._cr.fetchall():
for id, rmod, rid, author_id, message_type, partner_id, channel_id in self._cr.fetchall():
if author_id == pid:
author_ids.add(id)
elif partner_id == pid:
partner_ids.add(id)
elif channel_id:
channel_ids.add(id)
elif rmod and rid:
elif rmod and rid and message_type != 'user_notification':
model_ids.setdefault(rmod, {}).setdefault(rid, set()).add(id)
allowed_ids = self._find_allowed_doc_ids(model_ids)
@@ -733,13 +753,14 @@ class Message(models.Model):
(self._description, operation))
# Read mail_message.ids to have their values
message_values = dict((res_id, {}) for res_id in self.ids)
message_values = dict((message_id, {}) for message_id in self.ids)
if operation == 'read':
self._cr.execute("""
SELECT DISTINCT m.id, m.model, m.res_id, m.author_id, m.parent_id,
COALESCE(partner_rel.res_partner_id, needaction_rel.res_partner_id),
channel_partner.channel_id as channel_id, m.moderation_status
channel_partner.channel_id as channel_id, m.moderation_status,
m.message_type as message_type
FROM "%s" m
LEFT JOIN "mail_message_res_partner_rel" partner_rel
ON partner_rel.mail_message_id = m.id AND partner_rel.res_partner_id = %%(pid)s
@@ -752,7 +773,7 @@ class Message(models.Model):
LEFT JOIN "mail_channel_partner" channel_partner
ON channel_partner.channel_id = channel.id AND channel_partner.partner_id = %%(pid)s
WHERE m.id = ANY (%%(ids)s)""" % self._table, dict(pid=self.env.user.partner_id.id, ids=self.ids))
for mid, rmod, rid, author_id, parent_id, partner_id, channel_id, moderation_status in self._cr.fetchall():
for mid, rmod, rid, author_id, parent_id, partner_id, channel_id, moderation_status, message_type in self._cr.fetchall():
message_values[mid] = {
'model': rmod,
'res_id': rid,
@@ -760,13 +781,15 @@ class Message(models.Model):
'parent_id': parent_id,
'moderation_status': moderation_status,
'moderator_id': False,
'notified': any((message_values[mid].get('notified'), partner_id, channel_id))
'notified': any((message_values[mid].get('notified'), partner_id, channel_id)),
'message_type': message_type,
}
elif operation == 'write':
self._cr.execute("""
SELECT DISTINCT m.id, m.model, m.res_id, m.author_id, m.parent_id, m.moderation_status,
COALESCE(partner_rel.res_partner_id, needaction_rel.res_partner_id),
channel_partner.channel_id as channel_id, channel_moderator_rel.res_users_id as moderator_id
channel_partner.channel_id as channel_id, channel_moderator_rel.res_users_id as moderator_id,
m.message_type as message_type
FROM "%s" m
LEFT JOIN "mail_message_res_partner_rel" partner_rel
ON partner_rel.mail_message_id = m.id AND partner_rel.res_partner_id = %%(pid)s
@@ -783,7 +806,7 @@ class Message(models.Model):
LEFT JOIN "mail_channel_moderator_rel" channel_moderator_rel
ON channel_moderator_rel.mail_channel_id = moderated_channel.id AND channel_moderator_rel.res_users_id = %%(uid)s
WHERE m.id = ANY (%%(ids)s)""" % self._table, dict(pid=self.env.user.partner_id.id, uid=self.env.user.id, ids=self.ids))
for mid, rmod, rid, author_id, parent_id, moderation_status, partner_id, channel_id, moderator_id in self._cr.fetchall():
for mid, rmod, rid, author_id, parent_id, moderation_status, partner_id, channel_id, moderator_id, message_type in self._cr.fetchall():
message_values[mid] = {
'model': rmod,
'res_id': rid,
@@ -791,35 +814,38 @@ class Message(models.Model):
'parent_id': parent_id,
'moderation_status': moderation_status,
'moderator_id': moderator_id,
'notified': any((message_values[mid].get('notified'), partner_id, channel_id))
'notified': any((message_values[mid].get('notified'), partner_id, channel_id)),
'message_type': message_type,
}
elif operation == 'create':
self._cr.execute("""SELECT DISTINCT id, model, res_id, author_id, parent_id, moderation_status FROM "%s" WHERE id = ANY (%%s)""" % self._table, (self.ids,))
for mid, rmod, rid, author_id, parent_id, moderation_status in self._cr.fetchall():
self._cr.execute("""SELECT DISTINCT id, model, res_id, author_id, parent_id, moderation_status, message_type FROM "%s" WHERE id = ANY (%%s)""" % self._table, (self.ids,))
for mid, rmod, rid, author_id, parent_id, moderation_status, message_type in self._cr.fetchall():
message_values[mid] = {
'model': rmod,
'res_id': rid,
'author_id': author_id,
'parent_id': parent_id,
'moderation_status': moderation_status,
'moderator_id': False
'moderator_id': False,
'message_type': message_type,
}
else: # unlink
self._cr.execute("""SELECT DISTINCT m.id, m.model, m.res_id, m.author_id, m.parent_id, m.moderation_status, channel_moderator_rel.res_users_id as moderator_id
self._cr.execute("""SELECT DISTINCT m.id, m.model, m.res_id, m.author_id, m.parent_id, m.moderation_status, channel_moderator_rel.res_users_id as moderator_id, m.message_type as message_type
FROM "%s" m
LEFT JOIN "mail_channel" moderated_channel
ON m.moderation_status = 'pending_moderation' AND m.res_id = moderated_channel.id
LEFT JOIN "mail_channel_moderator_rel" channel_moderator_rel
ON channel_moderator_rel.mail_channel_id = moderated_channel.id AND channel_moderator_rel.res_users_id = (%%s)
WHERE m.id = ANY (%%s)""" % self._table, (self.env.user.id, self.ids,))
for mid, rmod, rid, author_id, parent_id, moderation_status, moderator_id in self._cr.fetchall():
for mid, rmod, rid, author_id, parent_id, moderation_status, moderator_id, message_type in self._cr.fetchall():
message_values[mid] = {
'model': rmod,
'res_id': rid,
'author_id': author_id,
'parent_id': parent_id,
'moderation_status': moderation_status,
'moderator_id': moderator_id
'moderator_id': moderator_id,
'message_type': message_type,
}
# Author condition (READ, WRITE, CREATE (private))
@@ -832,7 +858,56 @@ class Message(models.Model):
if message.get('moderation_status') != 'pending_moderation' and message.get('author_id') == self.env.user.partner_id.id]
elif operation == 'create':
author_ids = [mid for mid, message in message_values.items()
if not message.get('model') and not message.get('res_id')]
if not self.is_thread_message(message)]
# Moderator condition: allow to WRITE, UNLINK if moderator of a pending message
moderator_ids = []
if operation in ['write', 'unlink']:
moderator_ids = [mid for mid, message in message_values.items() if message.get('moderator_id')]
messages_to_check = self.ids
messages_to_check = set(messages_to_check).difference(set(author_ids), set(moderator_ids))
if not messages_to_check:
return
# Recipients condition, for read and write (partner_ids)
# keep on top, usefull for systray notifications
notified_ids = []
model_record_ids = _generate_model_record_ids(message_values, messages_to_check)
if operation in ['read', 'write']:
notified_ids = [mid for mid, message in message_values.items() if message.get('notified')]
messages_to_check = set(messages_to_check).difference(set(notified_ids))
if not messages_to_check:
return
# CRUD: Access rights related to the document
document_related_ids = []
document_related_candidate_ids = [mid for mid, message in message_values.items()
if (message.get('model') and message.get('res_id') and
message.get('message_type') != 'user_notification' and
(message.get('moderation_status') != 'pending_moderation' or operation not in ['write', 'unlink']))]
model_record_ids = _generate_model_record_ids(message_values, document_related_candidate_ids)
for model, doc_ids in model_record_ids.items():
DocumentModel = self.env[model]
if hasattr(DocumentModel, 'get_mail_message_access'):
check_operation = DocumentModel.get_mail_message_access(doc_ids, operation) ## why not giving model here?
else:
check_operation = self.env['mail.thread'].get_mail_message_access(doc_ids, operation, model_name=model)
records = DocumentModel.browse(doc_ids)
records.check_access_rights(check_operation)
mids = records.browse(doc_ids)._filter_access_rules(check_operation)
document_related_ids += [
mid for mid, message in message_values.items()
if (message.get('model') == model and
message.get('res_id') in mids.ids and
message.get('message_type') != 'user_notification' and
(message.get('moderation_status') != 'pending_moderation' or
operation not in ['write', 'unlink']))]
messages_to_check = messages_to_check.difference(set(document_related_ids))
if not messages_to_check:
return
# Parent condition, for create (check for received notifications for the created message parent)
notified_ids = []
@@ -854,17 +929,12 @@ class Message(models.Model):
notified_ids += [mid for mid, message in message_values.items()
if message.get('parent_id') in not_parent_ids]
# Moderator condition: allow to WRITE, UNLINK if moderator of a pending message
moderator_ids = []
if operation in ['write', 'unlink']:
moderator_ids = [mid for mid, message in message_values.items() if message.get('moderator_id')]
messages_to_check = messages_to_check.difference(set(notified_ids))
if not messages_to_check:
return
# Recipients condition, for read and write (partner_ids) and create (message_follower_ids)
other_ids = set(self.ids).difference(set(author_ids), set(notified_ids), set(moderator_ids))
model_record_ids = _generate_model_record_ids(message_values, other_ids)
if operation in ['read', 'write']:
notified_ids = [mid for mid, message in message_values.items() if message.get('notified')]
elif operation == 'create':
# Recipients condition for create (message_follower_ids)
if operation == 'create':
for doc_model, doc_ids in model_record_ids.items():
followers = self.env['mail.followers'].sudo().search([
('res_model', '=', doc_model),
@@ -873,30 +943,16 @@ class Message(models.Model):
])
fol_mids = [follower.res_id for follower in followers]
notified_ids += [mid for mid, message in message_values.items()
if message.get('model') == doc_model and message.get('res_id') in fol_mids]
if message.get('model') == doc_model and
message.get('res_id') in fol_mids and
message.get('message_type') != 'user_notification'
]
# CRUD: Access rights related to the document
other_ids = other_ids.difference(set(notified_ids))
model_record_ids = _generate_model_record_ids(message_values, other_ids)
document_related_ids = []
for model, doc_ids in model_record_ids.items():
DocumentModel = self.env[model]
mids = DocumentModel.browse(doc_ids).exists()
if hasattr(DocumentModel, 'check_mail_message_access'):
DocumentModel.check_mail_message_access(mids.ids, operation) # ?? mids ?
else:
self.env['mail.thread'].check_mail_message_access(mids.ids, operation, model_name=model)
if operation in ['write', 'unlink']:
document_related_ids += [mid for mid, message in message_values.items()
if message.get('model') == model and message.get('res_id') in mids.ids and
message.get('moderation_status') != 'pending_moderation']
else:
document_related_ids += [mid for mid, message in message_values.items()
if message.get('model') == model and message.get('res_id') in mids.ids]
messages_to_check = messages_to_check.difference(set(notified_ids))
if not messages_to_check:
return
# Calculate remaining ids: if not void, raise an error
other_ids = other_ids.difference(set(document_related_ids))
if not (other_ids and self.browse(other_ids).exists()):
if not self.browse(messages_to_check).exists():
return
raise AccessError(
_('The requested operation cannot be completed due to security restrictions. Please contact your system administrator.\n\n(Document type: %s, Operation: %s)') %
@@ -915,32 +971,41 @@ class Message(models.Model):
@api.model
def _get_reply_to(self, values):
""" Return a specific reply_to for the document """
model, res_id, email_from = values.get('model', self._context.get('default_model')), values.get('res_id', self._context.get('default_res_id')), values.get('email_from') # ctx values / defualt_get res ?
records = self.env[model].browse([res_id]) if model and res_id else None
return self.env['mail.thread']._notify_get_reply_to_on_records(default=email_from, records=records)[res_id or False]
model = values.get('model', self._context.get('default_model'))
res_id = values.get('res_id', self._context.get('default_res_id'))
email_from = values.get('email_from')
message_type = values.get('message_type')
records = None
if self.is_thread_message({'model': model, 'res_id': res_id, 'message_type': message_type}):
records = self.env[model].browse([res_id])
else:
res_id = False
return self.env['mail.thread']._notify_get_reply_to_on_records(default=email_from, records=records)[res_id]
@api.model
def _get_message_id(self, values):
if values.get('no_auto_thread', False) is True:
message_id = tools.generate_tracking_message_id('reply_to')
elif values.get('res_id') and values.get('model'):
elif self.is_thread_message(values):
message_id = tools.generate_tracking_message_id('%(res_id)s-%(model)s' % values)
else:
message_id = tools.generate_tracking_message_id('private')
return message_id
@api.multi
def _invalidate_documents(self):
def _invalidate_documents(self, model=None, res_id=None):
""" Invalidate the cache of the documents followed by ``self``. """
for record in self:
if record.model and record.res_id and 'message_ids' in self.env[record.model]:
self.env[record.model].invalidate_cache(fnames=[
model = model or record.model
res_id = res_id or record.res_id
if 'message_ids' in self.env[model]:
self.env[model].invalidate_cache(fnames=[
'message_ids',
'message_unread',
'message_unread_counter',
'message_needaction',
'message_needaction_counter',
], ids=[record.res_id])
], ids=[res_id])
@api.model
def create(self, values):
@@ -958,8 +1023,7 @@ class Message(models.Model):
values['record_name'] = self._get_record_name(values)
if 'attachment_ids' not in values:
values.setdefault('attachment_ids', [])
values['attachment_ids'] = []
# extract base64 images
if 'body' in values:
Attachments = self.env['ir.attachment']
@@ -985,8 +1049,21 @@ class Message(models.Model):
tracking_values_cmd = values.pop('tracking_value_ids', False)
message = super(Message, self).create(values)
# check attachement access
if values.get('attachment_ids'):
message.attachment_ids.check(mode='read')
attachment_ids = []
if all(isinstance(command, int) or command[0] in (4, 6) for command in values.get('attachment_ids')):
attachment_ids = []
for command in values.get('attachment_ids'):
if isinstance(command, int):
attachment_ids += [command]
elif command[0] == 6:
attachment_ids += command[2]
else: # command[0] == 4:
attachment_ids += [command[1]]
else:
attachment_ids = message.mapped('attachment_ids').ids # fallback on read if any unknow command
self.env['ir.attachment'].browse(attachment_ids).check(mode='read')
if tracking_values_cmd:
vals_lst = [dict(cmd[2], mail_message_id=message.id) for cmd in tracking_values_cmd if len(cmd) == 3 and cmd[0] == 0]
@@ -996,8 +1073,8 @@ class Message(models.Model):
if other_cmd:
message.sudo().write({'tracking_value_ids': tracking_values_cmd})
if values.get('model') and values.get('res_id'):
message._invalidate_documents()
if message.is_thread_message(values):
message._invalidate_documents(values.get('model'), values.get('res_id'))
return message
@@ -1010,13 +1087,14 @@ class Message(models.Model):
@api.multi
def write(self, vals):
if 'model' in vals or 'res_id' in vals:
record_changed = 'model' in vals or 'res_id' in vals
if record_changed or 'message_type' in vals:
self._invalidate_documents()
res = super(Message, self).write(vals)
if vals.get('attachment_ids'):
for mail in self:
mail.attachment_ids.check(mode='read')
if 'notification_ids' in vals or 'model' in vals or 'res_id' in vals:
if 'notification_ids' in vals or record_changed:
self._invalidate_documents()
return res
@@ -1030,129 +1108,11 @@ class Message(models.Model):
self.mapped('attachment_ids').filtered(
lambda attach: attach.res_model == self._name and (attach.res_id in self.ids or attach.res_id == 0)
).unlink()
self._invalidate_documents()
for elem in self:
if elem.is_thread_message():
elem._invalidate_documents()
return super(Message, self).unlink()
#------------------------------------------------------
# Messaging API
#------------------------------------------------------
@api.multi
def _notify(self, record, msg_vals, force_send=False, send_after_commit=True, model_description=False, mail_auto_delete=True):
""" Main notification method. This method basically does two things
* call ``_notify_compute_recipients`` that computes recipients to
notify based on message record or message creation values if given
(to optimize performance if we already have data computed);
* call ``_notify_recipients`` that performs the notification process;
:param record: record on which the message is posted, if any;
:param msg_vals: dictionary of values used to create the message. If given
it is used instead of accessing ``self`` to lesen query count in some
simple cases where no notification is actually required;
:param force_send: tells whether to send notification emails within the
current transaction or to use the email queue;
:param send_after_commit: if force_send, tells whether to send emails after
the transaction has been committed using a post-commit hook;
:param model_description: optional data used in notification process (see
notification templates);
:param mail_auto_delete: delete notification emails once sent;
"""
msg_vals = msg_vals if msg_vals else {}
rdata = self._notify_compute_recipients(record, msg_vals)
return self._notify_recipients(
rdata, record, msg_vals,
force_send=force_send, send_after_commit=send_after_commit,
model_description=model_description, mail_auto_delete=mail_auto_delete)
@api.multi
def _notify_compute_recipients(self, record, msg_vals):
""" Compute recipients to notify based on subtype and followers. This
method returns data structured as expected for ``_notify_recipients``. """
msg_sudo = self.sudo()
pids = [x[1] for x in msg_vals.get('partner_ids')] if 'partner_ids' in msg_vals else msg_sudo.partner_ids.ids
cids = [x[1] for x in msg_vals.get('channel_ids')] if 'channel_ids' in msg_vals else msg_sudo.channel_ids.ids
subtype_id = msg_vals.get('subtype_id') if 'subtype_id' in msg_vals else msg_sudo.subtype_id.id
recipient_data = {
'partners': [],
'channels': [],
}
res = self.env['mail.followers']._get_recipient_data(record, subtype_id, pids, cids)
author_id = msg_vals.get('author_id') or self.author_id.id if res else False
for pid, cid, active, pshare, ctype, notif, groups in res:
if pid and pid == author_id and not self.env.context.get('mail_notify_author'): # do not notify the author of its own messages
continue
if pid:
if active is False:
# avoid to notify inactive partner by email (odoobot)
continue
pdata = {'id': pid, 'active': active, 'share': pshare, 'groups': groups}
if notif == 'inbox':
recipient_data['partners'].append(dict(pdata, notif=notif, type='user'))
else:
if not pshare and notif: # has an user and is not shared, is therefore user
recipient_data['partners'].append(dict(pdata, notif='email', type='user'))
elif pshare and notif: # has an user but is shared, is therefore portal
recipient_data['partners'].append(dict(pdata, notif='email', type='portal'))
else: # has no user, is therefore customer
recipient_data['partners'].append(dict(pdata, notif='email', type='customer'))
elif cid:
recipient_data['channels'].append({'id': cid, 'notif': notif, 'type': ctype})
return recipient_data
@api.multi
def _notify_recipients(self, rdata, record, msg_vals,
force_send=False, send_after_commit=True,
model_description=False, mail_auto_delete=True):
""" Main method implementing the notification process.
:param rdata: dict containing recipients data: {
'partners': list of dict containing partner data: id, share status (boolean),
notification type ('email' or 'inbox'), type (main classification group used
in notification process), groups (user groups)
'channels': list of dict containing channel data: id, notification type
('email' for mailing list otherwise 'inbox'), type (channel_type)
}
"""
self.ensure_one()
email_cids = [r['id'] for r in rdata['channels'] if r['notif'] == 'email']
inbox_pids = [r['id'] for r in rdata['partners'] if r['notif'] == 'inbox']
message_values = {}
if rdata['channels']:
message_values['channel_ids'] = [(6, 0, [r['id'] for r in rdata['channels']])]
if rdata['partners']:
message_values['needaction_partner_ids'] = [(6, 0, [r['id'] for r in rdata['partners']])]
if message_values and record and hasattr(record, '_notify_customize_recipients'):
message_values.update(record._notify_customize_recipients(self, message_values, rdata))
if message_values:
self.write(message_values)
# notify partners and channels
if email_cids:
new_pids = self.env['res.partner'].sudo().search([
('id', 'not in', [r['id'] for r in rdata['partners']]),
('channel_ids', 'in', email_cids),
('email', 'not in', [self.author_id.email, self.email_from]),
])
for partner in new_pids:
rdata['partners'].append({'id': partner.id, 'share': True, 'notif': 'email', 'type': 'customer', 'groups': []})
partner_email_rdata = [r for r in rdata['partners'] if r['notif'] == 'email']
if partner_email_rdata:
self.env['res.partner']._notify(self, partner_email_rdata, record, force_send=force_send, send_after_commit=send_after_commit, model_description=model_description, mail_auto_delete=mail_auto_delete)
if inbox_pids:
self.env['res.partner'].browse(inbox_pids)._notify_by_chat(self)
if rdata['channels']:
self.env['mail.channel'].sudo().browse([r['id'] for r in rdata['channels']])._notify(self)
return True
# --------------------------------------------------
# Moderation
# --------------------------------------------------
@@ -1214,8 +1174,9 @@ class Message(models.Model):
})
# proceed with notification process to send notification emails and Inbox messages
for message in self:
record = self.env[message.model].browse(message.res_id) if message.model and message.res_id else None
message._notify(record, {})
if message.is_thread_message(): # note, since we will only intercept _notify_thread for message posted on channel,
# message will always be a thread_message. This check should always be true.
self.env[message.model].browse(message.res_id)._notify_thread(message)
@api.multi
def _moderate_send_reject_email(self, subject, comment):
@@ -1312,7 +1273,7 @@ class Message(models.Model):
MailThread = self.env['mail.thread'].with_context(mail_notify_author=True)
for moderator in moderators_to_notify:
MailThread.message_notify(
moderator.partner_id.ids,
partner_ids=moderator.partner_id.ids,
subject=_('Message are pending moderation'), # tocheck: target language
body=template.render({'record': moderator.partner_id}, engine='ir.qweb', minimal_qcontext=True),
email_from=moderator.company_id.catchall or moderator.company_id.email,
File diff suppressed because it is too large Load Diff
+15
View File
@@ -17,3 +17,18 @@ class BaseModel(models.AbstractModel):
activity_box = E.div(field, {'t-name': "activity-box"})
templates = E.templates(activity_box)
return E.activity(templates, string=self._description)
@api.multi
def _notify_email_headers(self):
"""
Generate the email headers based on record
"""
if not self:
return {}
self.ensure_one()
return repr(self._notify_email_header_dict())
def _notify_email_header_dict(self):
return {
'X-Odoo-Objects': "%s-%s" % (self._name, self.id),
}
+2 -174
View File
@@ -2,11 +2,8 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
import logging
import threading
from odoo.tools.misc import split_every
from odoo import _, api, fields, models, registry, SUPERUSER_ID
from odoo import _, api, fields, models
from odoo.addons.bus.models.bus_presence import AWAY_TIMER
from odoo.addons.bus.models.bus_presence import DISCONNECTION_TIMER
from odoo.osv import expression
@@ -40,176 +37,6 @@ class Partner(models.Model):
'email_cc': False}
for r in self}
@api.model
def _notify_prepare_template_context(self, message, record, model_description=False, mail_auto_delete=True):
# compute send user and its related signature
signature = ''
if message.author_id and message.author_id.user_ids:
user = message.author_id.user_ids[0]
if message.add_sign:
signature = user.signature
else:
user = self.env.user
if message.add_sign:
signature = "<p>-- <br/>%s</p>" % message.author_id.name
company = record.company_id.sudo() if record and 'company_id' in record else user.company_id
if company.website:
website_url = 'http://%s' % company.website if not company.website.lower().startswith(('http:', 'https:')) else company.website
else:
website_url = False
# Retrieve the language in which the template was rendered, in order to render the custom
# layout in the same language.
lang = self.env.context.get('lang')
if {'default_template_id', 'default_model', 'default_res_id'} <= self.env.context.keys():
template = self.env['mail.template'].browse(self.env.context['default_template_id'])
if template and template.lang:
lang = template._render_template(template.lang, self.env.context['default_model'], self.env.context['default_res_id'])
if not model_description and message.model:
model_description = self.env['ir.model'].with_context(lang=lang)._get(message.model).display_name
tracking = []
for tracking_value in self.env['mail.tracking.value'].sudo().search([('mail_message_id', '=', message.id)]):
groups = tracking_value.field_groups
if not groups or self.user_has_groups(groups):
tracking.append((tracking_value.field_desc,
tracking_value.get_old_display_value()[0],
tracking_value.get_new_display_value()[0]))
is_discussion = message.subtype_id.id == self.env['ir.model.data'].xmlid_to_res_id('mail.mt_comment')
return {
'message': message,
'signature': signature,
'website_url': website_url,
'company': company,
'model_description': model_description,
'record': record,
'record_name': message.record_name,
'tracking_values': tracking,
'is_discussion': is_discussion,
'subtype': message.subtype_id,
'lang': lang,
}
@api.model
def _notify(self, message, rdata, record, force_send=False, send_after_commit=True, model_description=False, mail_auto_delete=True):
""" Method to send email linked to notified messages. The recipients are
the recordset on which this method is called.
:param message: mail.message record to notify;
:param rdata: recipient data (see mail.message _notify);
:param record: optional record on which the message was posted;
:param force_send: tells whether to send notification emails within the
current transaction or to use the email queue;
:param send_after_commit: if force_send, tells whether to send emails after
the transaction has been committed using a post-commit hook;
:param model_description: optional data used in notification process (see
notification templates);
:param mail_auto_delete: delete notification emails once sent;
"""
if not rdata:
return True
base_template_ctx = self._notify_prepare_template_context(message, record, model_description=model_description)
template_xmlid = message.layout if message.layout else 'mail.message_notification_email'
try:
base_template = self.env.ref(template_xmlid, raise_if_not_found=True).with_context(lang=base_template_ctx['lang'])
except ValueError:
_logger.warning('QWeb template %s not found when sending notification emails. Sending without layouting.' % (template_xmlid))
base_template = False
# prepare notification mail values
base_mail_values = {
'mail_message_id': message.id,
'mail_server_id': message.mail_server_id.id,
'auto_delete': mail_auto_delete,
'references': message.parent_id.message_id if message.parent_id else False
}
if record:
base_mail_values.update(self.env['mail.thread']._notify_specific_email_values_on_records(message, records=record))
# classify recipients: actions / no action
recipients = self.env['mail.thread']._notify_classify_recipients_on_records(message, rdata, records=record)
Mail = self.env['mail.mail'].sudo()
emails = self.env['mail.mail'].sudo()
email_pids = set()
recipients_nbr, recipients_max = 0, 50
for group_tpl_values in [group for group in recipients.values() if group['recipients']]:
# generate notification email content
template_ctx = {**base_template_ctx, **group_tpl_values}
mail_body = base_template.render(template_ctx, engine='ir.qweb', minimal_qcontext=True)
mail_body = self.env['mail.thread']._replace_local_links(mail_body)
mail_subject = message.subject or (message.record_name and 'Re: %s' % message.record_name)
# send email
for email_chunk in split_every(50, group_tpl_values['recipients']):
recipient_values = self.env['mail.thread']._notify_email_recipients_on_records(message, email_chunk, records=record)
create_values = {
'body_html': mail_body,
'subject': mail_subject,
}
create_values.update(base_mail_values)
create_values.update(recipient_values)
recipient_ids = [r[1] for r in create_values.get('recipient_ids', [])]
email = Mail.create(create_values)
if email and recipient_ids:
notifications = self.env['mail.notification'].sudo().search([
('mail_message_id', '=', email.mail_message_id.id),
('res_partner_id', 'in', list(recipient_ids))
])
notifications.write({
'is_email': True,
'mail_id': email.id,
'is_read': True, # handle by email discards Inbox notification
'email_status': 'ready',
})
emails |= email
email_pids.update(recipient_ids)
# NOTE:
# 1. for more than 50 followers, use the queue system
# 2. do not send emails immediately if the registry is not loaded,
# to prevent sending email during a simple update of the database
# using the command-line.
test_mode = getattr(threading.currentThread(), 'testing', False)
if force_send and len(emails) < recipients_max and \
(not self.pool._init or test_mode):
email_ids = emails.ids
dbname = self.env.cr.dbname
_context = self._context
def send_notifications():
db_registry = registry(dbname)
with api.Environment.manage(), db_registry.cursor() as cr:
env = api.Environment(cr, SUPERUSER_ID, _context)
env['mail.mail'].browse(email_ids).send()
# unless asked specifically, send emails after the transaction to
# avoid side effects due to emails being sent while the transaction fails
if not test_mode and send_after_commit:
self._cr.after('commit', send_notifications)
else:
emails.send()
return True
@api.multi
def _notify_by_chat(self, message):
""" Broadcast the message to all the partner since """
if not self:
return
message_values = message.message_format()[0]
notifications = []
for partner in self:
notifications.append([(self._cr.dbname, 'ir.needaction', partner.id), dict(message_values)])
self.env['bus.bus'].sendmany(notifications)
@api.model
def get_needaction_count(self):
""" compute the number of needaction of the current user """
@@ -294,3 +121,4 @@ class Partner(models.Model):
return self.env.cr.dictfetchall()
else:
return {}
@@ -368,7 +368,7 @@ var Message = AbstractMessage.extend(Mixins.EventDispatcherMixin, ServicesMixin
* @return {boolean}
*/
isLinkedToDocumentThread: function () {
return !!(this._documentModel !== 'mail.channel' && this._documentID);
return !!(this._documentModel !== 'mail.channel' && this._documentID && this._type !== 'user_notification');
},
/**
* State whether the current user is the author of this message
+8 -6
View File
@@ -72,11 +72,13 @@ class Invite(models.TransientModel):
'no_auto_thread': True,
'add_sign': True,
})
self.env['res.partner'].with_context(auto_delete=True)._notify(
message,
[{'id': pid, 'share': True, 'notif': 'email', 'type': 'customer', 'groups': []} for pid in new_partners.ids],
document,
force_send=True,
send_after_commit=False)
partners_data = [{
'id': pid,
'share': True,
'notif': 'email',
'type': 'customer',
'groups': []
} for pid in new_partners.ids]
document._notify_record_by_email(message, partners_data, send_after_commit=False)
message.unlink()
return {'type': 'ir.actions.act_window_close'}
+15 -10
View File
@@ -184,9 +184,9 @@ class MailComposer(models.TransientModel):
result['model'] = parent.model
if not values.get('res_id'):
result['res_id'] = parent.res_id
partner_ids = values.get('partner_ids', list()) + [(4, id) for id in parent.partner_ids.ids]
partner_ids = values.get('partner_ids', list()) + parent.partner_ids.ids
if self._context.get('is_private') and parent.author_id: # check message is private then add author also in partner list.
partner_ids += [(4, parent.author_id.id)]
partner_ids += [parent.author_id.id]
result['partner_ids'] = partner_ids
elif values.get('model') and values.get('res_id'):
doc_name_get = self.env[values.get('model')].browse(values.get('res_id')).name_get()
@@ -274,14 +274,18 @@ class MailComposer(models.TransientModel):
post_params = dict(
message_type=wizard.message_type,
subtype_id=subtype_id,
notif_layout=notif_layout,
email_layout_xmlid=notif_layout,
add_sign=not bool(wizard.template_id),
mail_auto_delete=wizard.template_id.auto_delete if wizard.template_id else False,
model_description=model_description,
**mail_values)
if ActiveModel._name == 'mail.thread' and wizard.model:
post_params['model'] = wizard.model
ActiveModel.browse(res_id).message_post(**post_params)
model_description=model_description)
post_params.update(mail_values)
if ActiveModel._name == 'mail.thread':
if wizard.model:
post_params['model'] = wizard.model
post_params['res_id'] = res_id
ActiveModel.message_notify(**post_params)
else:
ActiveModel.browse(res_id).message_post(**post_params)
if wizard.composition_mode == 'mass_mail':
batch_mails.send(auto_commit=auto_commit)
@@ -332,7 +336,8 @@ class MailComposer(models.TransientModel):
# mass mailing: rendering override wizard static values
if mass_mail_mode and self.model:
mail_values.update(self.env['mail.thread']._notify_specific_email_values_on_records(False, records=self.env[self.model].browse(res_id)))
record = self.env[self.model].browse(res_id)
mail_values['headers'] = record._notify_email_headers()
# keep a copy unless specifically requested, reset record name (avoid browsing records)
mail_values.update(notification=not self.auto_delete_message, model=self.model, res_id=res_id, record_name=False)
# auto deletion of mail_mail
@@ -362,7 +367,7 @@ class MailComposer(models.TransientModel):
mail_values.pop('attachments', []),
attachment_ids,
{'model': 'mail.message', 'res_id': 0}
)
)['attachment_ids']
# Filter out the blacklisted records by setting the mail state to cancel -> Used for Mass Mailing stats
if res_id in blacklisted_rec_ids:
mail_values['state'] = 'cancel'
+6 -12
View File
@@ -63,26 +63,20 @@ class MailResendMessage(models.TransientModel):
notif_to_cancel.sudo().write({'email_status': 'canceled'})
if to_send:
message = wizard.mail_message_id
record = self.env[message.model].browse(message.res_id) if message.model and message.res_id else None
record = self.env[message.model].browse(message.res_id) if message.is_thread_message() else self.env['mail.thread']
rdata = []
email_partners_data = []
for pid, cid, active, pshare, ctype, notif, groups in self.env['mail.followers']._get_recipient_data(None, False, pids=to_send.ids):
if pid and notif == 'email' or not notif:
pdata = {'id': pid, 'share': pshare, 'active': active, 'notif': 'email', 'groups': groups or []}
if not pshare and notif: # has an user and is not shared, is therefore user
rdata.append(dict(pdata, type='user'))
email_partners_data.append(dict(pdata, type='user'))
elif pshare and notif: # has an user and is shared, is therefore portal
rdata.append(dict(pdata, type='portal'))
email_partners_data.append(dict(pdata, type='portal'))
else: # has no user, is therefore customer
rdata.append(dict(pdata, type='customer'))
email_partners_data.append(dict(pdata, type='customer'))
self.env['res.partner']._notify(
message,
rdata,
record,
force_send=True,
send_after_commit=False
)
record._notify_record_by_email(message, email_partners_data, send_after_commit=False)
self.mail_message_id._notify_failure_update()
return {'type': 'ir.actions.act_window_close'}
+1 -1
View File
@@ -214,7 +214,7 @@ class MailBot(models.AbstractModel):
def _is_bot_pinged(self, values):
odoobot_id = self.env['ir.model.data'].xmlid_to_res_id("base.partner_root")
return (4, odoobot_id) in values.get('partner_ids', [])
return odoobot_id in values.get('partner_ids', [])
def _is_bot_in_private_channel(self, record):
odoobot_id = self.env['ir.model.data'].xmlid_to_res_id("base.partner_root")
+3 -3
View File
@@ -7,6 +7,6 @@ from odoo import models
class MailThread(models.AbstractModel):
_inherit = 'mail.thread'
def _message_post_after_hook(self, message, values, model_description=False, mail_auto_delete=True):
self.env['mail.bot']._apply_logic(self, values)
return super(MailThread, self)._message_post_after_hook(message, values, model_description=model_description, mail_auto_delete=mail_auto_delete)
def _message_post_after_hook(self, message, msg_vals):
self.env['mail.bot']._apply_logic(self, msg_vals)
return super(MailThread, self)._message_post_after_hook(message, msg_vals)
+1 -1
View File
@@ -20,7 +20,7 @@ class MassMailController(http.Controller):
def _log_blacklist_action(self, blacklist_entry, mailing_id, description):
mailing = request.env['mail.mass_mailing'].sudo().browse(mailing_id)
model_display = mailing.mailing_model_id.display_name
blacklist_entry._message_log(description + " ({})".format(model_display))
blacklist_entry._message_log(body=description + " ({})".format(model_display))
@http.route(['/unsubscribe_from_list'], type='http', website=True, multilang=False, auth='public')
def unsubscribe_placeholder_link(self, **post):
+1 -1
View File
@@ -73,4 +73,4 @@ class MailThread(models.AbstractModel):
if max(stats) > min(stats) + datetime.timedelta(weeks=1):
blacklist_rec = self.env['mail.blacklist'].sudo()._add(email)
blacklist_rec._message_log(
'This email has been automatically blacklisted because of too much bounced.')
body='This email has been automatically blacklisted because of too much bounced.')
+7 -5
View File
@@ -81,11 +81,13 @@ def _message_post_helper(res_model, res_id, message, token='', nosubscribe=True,
kw.pop('attachment_ids', None)
kw.pop('hash', None)
kw.pop('pid', None)
return record.with_context(mail_create_nosubscribe=nosubscribe).message_post(body=message,
message_type=kw.pop('message_type', "comment"),
subtype=kw.pop('subtype', "mt_comment"),
author_id=author_id,
**kw)
return record.with_context(mail_create_nosubscribe=nosubscribe).message_post(
body=message,
message_type=kw.pop('message_type', "comment"),
subtype=kw.pop('subtype', "mt_comment"),
author_id=author_id,
**kw
)
class PortalChatter(http.Controller):
+3 -4
View File
@@ -72,10 +72,10 @@ class PortalMixin(models.AbstractModel):
return '%s?%s' % ('/mail/view' if redirect else self.access_url, url_encode(params))
@api.multi
def _notify_get_groups(self, message, groups):
def _notify_get_groups(self):
access_token = self._portal_ensure_token()
customer = self['partner_id']
groups = super(PortalMixin, self)._notify_get_groups()
if access_token and customer:
additional_params = {
'access_token': self.access_token,
@@ -88,13 +88,12 @@ class PortalMixin(models.AbstractModel):
'has_button_access': False,
'button_access': {
'url': access_link,
'title': _('View %s') % self.env['ir.model']._get(message.model).display_name,
},
})
]
else:
new_group = []
return super(PortalMixin, self)._notify_get_groups(message, new_group + groups)
return new_group + groups
@api.multi
def get_access_action(self, access_uid=None):
+2 -2
View File
@@ -58,7 +58,7 @@ class PortalShare(models.TransientModel):
'share_link': share_link},
subject=_("You are invited to access %s" % active_record.display_name),
subtype_id=note.id,
notif_layout='mail.mail_notification_light',
email_layout_xmlid='mail.mail_notification_light',
partner_ids=[(6, 0, partner.ids)])
# when partner not user send individual mail with signup token
for partner in self.partner_ids - partner_ids:
@@ -70,6 +70,6 @@ class PortalShare(models.TransientModel):
'share_link': share_link},
subject=_("You are invited to access %s" % active_record.display_name),
subtype_id=note.id,
notif_layout='mail.mail_notification_light',
email_layout_xmlid='mail.mail_notification_light',
partner_ids=[(6, 0, partner.ids)])
return {'type': 'ir.actions.act_window_close'}
+10 -16
View File
@@ -792,7 +792,7 @@ class Task(models.Model):
res['stage_id'] = (test_task.stage_id.mail_template_id, {
'auto_delete_message': True,
'subtype_id': self.env['ir.model.data'].xmlid_to_res_id('mail.mt_note'),
'notif_layout': 'mail.mail_notification_light'
'email_layout_xmlid': 'mail.mail_notification_light'
})
return res
@@ -812,12 +812,12 @@ class Task(models.Model):
return super(Task, self)._track_subtype(init_values)
@api.multi
def _notify_get_groups(self, message, groups):
def _notify_get_groups(self):
""" Handle project users and managers recipients that can assign
tasks and create new one directly from notification emails. Also give
access button to portal users and portal customers. If they are notified
they should probably have access to the document. """
groups = super(Task, self)._notify_get_groups(message, groups)
groups = super(Task, self)._notify_get_groups()
self.ensure_one()
@@ -836,9 +836,8 @@ class Task(models.Model):
groups = [new_group] + groups
for group_name, group_method, group_data in groups:
if group_name == 'customer':
continue
group_data['has_button_access'] = True
if group_name != 'customer':
group_data['has_button_access'] = True
return groups
@@ -907,22 +906,17 @@ class Task(models.Model):
return recipients
@api.multi
def _notify_specific_email_values(self, message):
res = super(Task, self)._notify_specific_email_values(message)
try:
headers = safe_eval(res.get('headers', dict()))
except Exception:
headers = {}
def _notify_email_header_dict(self):
headers = super(Task, self)._notify_email_header_dict()
if self.project_id:
current_objects = [h for h in headers.get('X-Odoo-Objects', '').split(',') if h]
current_objects.insert(0, 'project.project-%s, ' % self.project_id.id)
headers['X-Odoo-Objects'] = ','.join(current_objects)
if self.tag_ids:
headers['X-Odoo-Tags'] = ','.join(self.tag_ids.mapped('name'))
res['headers'] = repr(headers)
return res
return headers
def _message_post_after_hook(self, message, *args, **kwargs):
def _message_post_after_hook(self, message, msg_vals):
if self.email_from and not self.partner_id:
# we consider that posting a message with a specified recipient (not a follower, a specific one)
# on a document without customer means that it was created through the chatter using
@@ -933,7 +927,7 @@ class Task(models.Model):
('partner_id', '=', False),
('email_from', '=', new_partner.email),
('stage_id.fold', '=', False)]).write({'partner_id': new_partner.id})
return super(Task, self)._message_post_after_hook(message, *args, **kwargs)
return super(Task, self)._message_post_after_hook(message, msg_vals)
def action_assign_to_me(self):
self.write({'user_id': self.env.user.id})
+2 -2
View File
@@ -155,12 +155,12 @@ class RatingMixin(models.AbstractModel):
if subtype_id is False:
subtype_id = self.env['ir.model.data'].xmlid_to_res_id('mail.mt_note')
if force_send:
self = self.with_context(mail_notify_force_send=True)
self = self.with_context(mail_notify_force_send=True) # default value is True, should be set to false if not?
for record in self:
record.message_post_with_template(
template.id,
composition_mode=composition_mode,
notif_layout=notif_layout if notif_layout is not None else 'mail.mail_notification_light',
email_layout_xmlid=notif_layout if notif_layout is not None else 'mail.mail_notification_light',
subtype_id=subtype_id
)
+1 -1
View File
@@ -106,7 +106,7 @@ class PaymentTransaction(models.Model):
for trans in self.filtered(lambda t: t.sale_order_ids):
trans = trans.with_context(ctx_company)
for invoice in trans.invoice_ids:
invoice.message_post_with_template(int(default_template), notif_layout="mail.mail_notification_paynow")
invoice.message_post_with_template(int(default_template), email_layout_xmlid="mail.mail_notification_paynow")
return res
@api.multi
+5 -6
View File
@@ -667,7 +667,7 @@ class SaleOrder(models.Model):
template_id = self._find_mail_template(force_confirmation_template=True)
if template_id:
for order in self:
order.with_context(force_send=True).message_post_with_template(template_id, composition_mode='comment', notif_layout="mail.mail_notification_paynow")
order.with_context(force_send=True).message_post_with_template(template_id, composition_mode='comment', email_layout_xmlid="mail.mail_notification_paynow")
@api.multi
def action_done(self):
@@ -782,18 +782,17 @@ class SaleOrder(models.Model):
return (self.state == 'sent' or (self.state == 'draft' and include_draft)) and not self.is_expired and self.require_payment and transaction.state != 'done' and self.amount_total
@api.multi
def _notify_get_groups(self, message, groups):
def _notify_get_groups(self):
""" Give access button to users and portal customer as portal is integrated
in sale. Customer and portal group have probably no right to see
the document so they don't have the access button. """
groups = super(SaleOrder, self)._notify_get_groups(message, groups)
groups = super(SaleOrder, self)._notify_get_groups()
self.ensure_one()
if self.state not in ('draft', 'cancel'):
for group_name, group_method, group_data in groups:
if group_name in ('customer', 'portal'):
continue
group_data['has_button_access'] = True
if group_name not in ('customer', 'portal'):
group_data['has_button_access'] = True
return groups
+1 -1
View File
@@ -246,7 +246,7 @@ class SaleOrder(models.Model):
self.message_post_with_template(
template.id, composition_mode='comment',
model='sale.coupon', res_id=coupon.id,
notif_layout='mail.mail_notification_light',
email_layout_xmlid='mail.mail_notification_light',
)
def _get_applicable_programs(self):
+2 -9
View File
@@ -17,24 +17,17 @@ class TestChatterTweaks(BaseFunctionalTest, TestRecipients):
def test_post_no_subscribe_recipients(self):
original = self.test_record.message_follower_ids
self.test_record.sudo(self.user_employee).with_context({'mail_create_nosubscribe': True}).message_post(
body='Test Body', message_type='comment', subtype='mt_comment', partner_ids=[(4, self.partner_1.id), (4, self.partner_2.id)])
body='Test Body', message_type='comment', subtype='mt_comment', partner_ids=[self.partner_1.id, self.partner_2.id])
self.assertEqual(self.test_record.message_follower_ids.mapped('partner_id'), original.mapped('partner_id'))
self.assertEqual(self.test_record.message_follower_ids.mapped('channel_id'), original.mapped('channel_id'))
def test_post_subscribe_recipients(self):
original = self.test_record.message_follower_ids
self.test_record.sudo(self.user_employee).with_context({'mail_create_nosubscribe': True, 'mail_post_autofollow': True}).message_post(
body='Test Body', message_type='comment', subtype='mt_comment', partner_ids=[(4, self.partner_1.id), (4, self.partner_2.id)])
body='Test Body', message_type='comment', subtype='mt_comment', partner_ids=[self.partner_1.id, self.partner_2.id])
self.assertEqual(self.test_record.message_follower_ids.mapped('partner_id'), original.mapped('partner_id') | self.partner_1 | self.partner_2)
self.assertEqual(self.test_record.message_follower_ids.mapped('channel_id'), original.mapped('channel_id'))
def test_post_subscribe_recipients_partial(self):
original = self.test_record.message_follower_ids
self.test_record.sudo(self.user_employee).with_context({'mail_create_nosubscribe': True, 'mail_post_autofollow': True, 'mail_post_autofollow_partner_ids': [self.partner_2.id]}).message_post(
body='Test Body', message_type='comment', subtype='mt_comment', partner_ids=[(4, self.partner_1.id), (4, self.partner_2.id)])
self.assertEqual(self.test_record.message_follower_ids.mapped('partner_id'), original.mapped('partner_id') | self.partner_2)
self.assertEqual(self.test_record.message_follower_ids.mapped('channel_id'), original.mapped('channel_id'))
def test_chatter_mail_create_nolog(self):
""" Test disable of automatic chatter message at create """
rec = self.env['mail.test.simple'].sudo(self.user_employee).with_context({'mail_create_nolog': True}).create({'name': 'Test'})
@@ -595,45 +595,3 @@ class TestMailgateway(BaseFunctionalTest, MockEmails):
self.assertEqual(len(record), 1)
self.assertEqual(record.name, 'Spammy')
self.assertEqual(record._name, 'mail.test.gateway')
@mute_logger('odoo.addons.mail.models.mail_thread', 'odoo.models', 'odoo.addons.mail.models.mail_mail')
def test_private_discussion(self):
""" Testing private discussion between partners. """
self.partner_2 = self.env['res.partner'].with_context(BaseFunctionalTest._test_context).create({
'name': 'Admin',
'email': 'admin@agrolait.com',
})
msg1_pids = [self.partner_1.id, self.partner_2.id]
# Do: Raoul writes to Bert and Administrator, with a model specific by parameter that should not be taken into account
msg1 = self.env['mail.thread'].sudo(self.user_employee).message_post(partner_ids=msg1_pids, subtype='mail.mt_comment', model='mail.test')
# Test: message recipients
msg = self.env['mail.message'].browse(msg1.id)
self.assertEqual(msg.partner_ids, self.partner_2 | self.partner_1,
'message_post: private discussion: incorrect recipients')
self.assertEqual(msg.model, False,
'message_post: private discussion: parameter model not correctly ignored when having no res_id')
# Test: message-id
self.assertIn('openerp-private', msg.message_id.split('@')[0], 'message_post: private discussion: message-id should contain the private keyword')
# Do: Bert replies through mailgateway (is a customer)
self.format_and_process(MAIL_TEMPLATE, 'valid.lelitre@agrolait.com', 'not_important@mydomain.com', extra='In-Reply-To: %s' % msg.message_id)
# Test: last mail_message created
msg2 = self.env['mail.message'].search([], limit=1)
# Test: message recipients
self.assertEqual(msg2.author_id, self.partner_1,
'message_post: private discussion: wrong author through mailgateway based on email')
self.assertEqual(msg2.partner_ids, self.user_employee.partner_id | self.partner_2,
'message_post: private discussion: incorrect recipients when replying')
# Do: Bert replies through chatter (is a customer)
msg3 = self.env['mail.thread'].message_post(author_id=self.partner_1.id, parent_id=msg1.id, subtype='mail.mt_comment')
# Test: message recipients
msg = self.env['mail.message'].browse(msg3.id)
self.assertEqual(msg.partner_ids, self.user_employee.partner_id | self.partner_2,
'message_post: private discussion: incorrect recipients when replying')
self.assertEqual(msg.needaction_partner_ids, self.user_employee.partner_id | self.partner_2,
'message_post: private discussion: incorrect notified recipients when replying')
@@ -205,17 +205,20 @@ class TestMessagePost(BaseFunctionalTest, MockEmails, TestRecipients):
def test_post_notify(self):
self.user_employee.write({'notification_type': 'inbox'})
new_notification = self.env['mail.thread'].message_notify(
new_notification = self.test_record.message_notify(
subject='This should be a subject',
body='<p>You have received a notification</p>',
partner_ids=[(4, self.partner_1.id), (4, self.user_employee.partner_id.id)],
partner_ids=[self.partner_1.id, self.user_employee.partner_id.id],
)
self.assertEqual(new_notification.subtype_id, self.env.ref('mail.mt_note'))
self.assertEqual(new_notification.message_type, 'user_notification')
self.assertEqual(new_notification.body, '<p>You have received a notification</p>')
self.assertEqual(new_notification.author_id, self.env.user.partner_id)
self.assertEqual(new_notification.email_from, formataddr((self.env.user.name, self.env.user.email)))
self.assertEqual(new_notification.needaction_partner_ids, self.partner_1 | self.user_employee.partner_id)
self.assertNotIn(new_notification, self.test_record.message_ids)
# todo xdo add test message_notify on thread with followers and stuff
class TestComposer(BaseFunctionalTest, MockEmails, TestRecipients):
+2 -2
View File
@@ -163,8 +163,8 @@ class TestTracking(common.BaseFunctionalTest, common.MockEmails):
self.record._fields['email_from'].groups = 'base.group_erp_manager' # patch the group attribute
self.record.sudo().write({'email_from': 'X'})
msg_emp = self.partner_employee.sudo(self.user_employee)._notify_prepare_template_context(self.record.message_ids, self.record)
msg_admin = self.partner_admin.sudo(self.user_admin)._notify_prepare_template_context(self.record.message_ids, self.record)
msg_emp = self.record.sudo(self.user_employee)._notify_prepare_template_context(self.record.message_ids, {})
msg_admin = self.record.sudo(self.user_admin)._notify_prepare_template_context(self.record.message_ids, {})
self.assertFalse(msg_emp.get('tracking_values'), "should not have protected tracking values")
self.assertTrue(msg_admin.get('tracking_values'), "should have protected tracking values")
+179 -23
View File
@@ -1,5 +1,6 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
import base64
from email.utils import formataddr
@@ -82,7 +83,7 @@ class TestMailPerformance(TransactionCase):
'partner_id': self.env.ref('base.res_partner_12').id,
})
with self.assertQueryCount(__system__=5, demo=5): # test_mail only: 5 - 5
with self.assertQueryCount(__system__=4, demo=4): # test_mail only: 4 - 4
record.track = 'X'
@users('__system__', 'demo')
@@ -98,13 +99,13 @@ class TestMailPerformance(TransactionCase):
@warmup
def test_create_mail_with_tracking(self):
""" Create records inheriting from 'mail.thread' (with field tracking). """
with self.assertQueryCount(__system__=10, demo=10): # test_mail only: 10 - 10
with self.assertQueryCount(__system__=9, demo=9): # test_mail only: 9 - 9
self.env['test_performance.mail'].create({'name': 'X'})
@users('__system__', 'emp')
@warmup
def test_create_mail_simple(self):
with self.assertQueryCount(__system__=8, emp=8): # test_mail only: 8 - 8
with self.assertQueryCount(__system__=7, emp=7): # test_mail only: 7 - 7
self.env['mail.test.simple'].create({'name': 'Test'})
@users('__system__', 'emp')
@@ -160,7 +161,7 @@ class TestAdvMailPerformance(TransactionCase):
def test_adv_activity(self):
model = self.env['mail.test.activity']
with self.assertQueryCount(__system__=9, emp=8): # test_mail only: 9 - 8
with self.assertQueryCount(__system__=8, emp=7): # test_mail only: 8 - 7
model.create({'name': 'Test'})
@users('__system__', 'emp')
@@ -172,7 +173,7 @@ class TestAdvMailPerformance(TransactionCase):
'default_res_model': 'mail.test.activity',
})
with self.assertQueryCount(__system__=10, emp=17): # com runbot: 10 - 16 // test_mail only: 10 - 15
with self.assertQueryCount(__system__=10, emp=13): # com runbot: 10 - 13 // test_mail only: 10 - 13
activity = MailActivity.create({
'summary': 'Test Activity',
'res_id': record.id,
@@ -182,7 +183,7 @@ class TestAdvMailPerformance(TransactionCase):
#voip module read activity_type during create leading to one less query in enterprise on action_feedback
category = activity.activity_type_id.category
with self.assertQueryCount(__system__=24, emp=41): # com runbot: 24 - 41 // test_mail only: 24 - 39
with self.assertQueryCount(__system__=24, emp=31): # com runbot: 24 - 31 // test_mail only: 24 - 31
activity.action_feedback(feedback='Zizisse Done !')
@users('__system__', 'emp')
@@ -191,7 +192,7 @@ class TestAdvMailPerformance(TransactionCase):
def test_adv_activity_mixin(self):
record = self.env['mail.test.activity'].create({'name': 'Test'})
with self.assertQueryCount(__system__=10, emp=17): # com runbot: 10 - 16 // test_mail only: 10 - 15
with self.assertQueryCount(__system__=10, emp=14): # com runbot: 10 - 14 // test_mail only: 10 - 14
activity = record.action_start('Test Start')
#read activity_type to normalize cache between enterprise and community
#voip module read activity_type during create leading to one less query in enterprise on action_close
@@ -199,7 +200,7 @@ class TestAdvMailPerformance(TransactionCase):
record.write({'name': 'Dupe write'})
with self.assertQueryCount(__system__=26, emp=42): # com runbot: 26 - 42 // test_mail only: 26 - 40
with self.assertQueryCount(__system__=26, emp=31): # com runbot: 26 - 31 // test_mail only: 26 - 31
record.action_close('Dupe feedback')
self.assertEqual(record.activity_ids, self.env['mail.activity'])
@@ -210,7 +211,7 @@ class TestAdvMailPerformance(TransactionCase):
def test_message_assignation_email(self):
self.user_test.write({'notification_type': 'email'})
record = self.env['mail.test.track'].create({'name': 'Test'})
with self.assertQueryCount(__system__=61, emp=76): # com runbot: 61 - 76 // test_mail only: 61 - 71
with self.assertQueryCount(__system__=56, emp=59): # com runbot: 56 - 59 // test_mail only: 56 - 59
record.write({
'user_id': self.user_test.id,
})
@@ -219,7 +220,7 @@ class TestAdvMailPerformance(TransactionCase):
@warmup
def test_message_assignation_inbox(self):
record = self.env['mail.test.track'].create({'name': 'Test'})
with self.assertQueryCount(__system__=37, emp=44): # test_mail only: 37 - 44
with self.assertQueryCount(__system__=34, emp=40): # test_mail only: 34 - 40
record.write({
'user_id': self.user_test.id,
})
@@ -239,7 +240,7 @@ class TestAdvMailPerformance(TransactionCase):
def test_message_log_with_post(self):
record = self.env['mail.test.simple'].create({'name': 'Test'})
with self.assertQueryCount(__system__=7, emp=13): # test_mail only: 7 - 13
with self.assertQueryCount(__system__=6, emp=7): # test_mail only: 6 - 7
record.message_post(
body='<p>Test message_post as log</p>',
subtype='mail.mt_note',
@@ -250,7 +251,7 @@ class TestAdvMailPerformance(TransactionCase):
def test_message_post_no_notification(self):
record = self.env['mail.test.simple'].create({'name': 'Test'})
with self.assertQueryCount(__system__=7, emp=13): # test_mail only: 7 - 13
with self.assertQueryCount(__system__=6, emp=7): # test_mail only: 6 - 7
record.message_post(
body='<p>Test Post Performances basic</p>',
partner_ids=[],
@@ -263,7 +264,7 @@ class TestAdvMailPerformance(TransactionCase):
def test_message_post_one_email_notification(self):
record = self.env['mail.test.simple'].create({'name': 'Test'})
with self.assertQueryCount(__system__=52, emp=72): # com runbot: 49 - 69 // test_mail only: 52 - 69
with self.assertQueryCount(__system__=47, emp=51): # com runbot: 47 - 51 // test_mail only: 47 - 51
record.message_post(
body='<p>Test Post Performances with an email ping</p>',
partner_ids=self.customer.ids,
@@ -275,7 +276,7 @@ class TestAdvMailPerformance(TransactionCase):
def test_message_post_one_inbox_notification(self):
record = self.env['mail.test.simple'].create({'name': 'Test'})
with self.assertQueryCount(__system__=32, emp=44): # com runbot 31 - 43 // test_mail only: 32 - 44
with self.assertQueryCount(__system__=30, emp=36): # com runbot 30 - 36 // test_mail only: 30 - 36
record.message_post(
body='<p>Test Post Performances with an inbox ping</p>',
partner_ids=self.user_test.partner_id.ids,
@@ -385,8 +386,7 @@ class TestHeavyMailPerformance(TransactionCase):
'recipient_ids': [(4, pid) for pid in self.partners.ids],
})
mail_ids = mail.ids
with self.assertQueryCount(__system__=14, emp=21): # test_mail only: 14 - 21
with self.assertQueryCount(__system__=16, emp=22): # test_mail only: 16 - 22
self.env['mail.mail'].browse(mail_ids).send()
self.assertEqual(mail.body_html, '<p>Test</p>')
@@ -399,7 +399,7 @@ class TestHeavyMailPerformance(TransactionCase):
self.umbrella.message_subscribe(self.user_portal.partner_id.ids)
record = self.umbrella.sudo(self.env.user)
with self.assertQueryCount(__system__=85, emp=108): # com runbot: 82 - 105 // test_mail only: 85 - 105
with self.assertQueryCount(__system__=78, emp=82): # com runbot: 78 - 82 // test_mail only: 78 - 82
record.message_post(
body='<p>Test Post Performances</p>',
message_type='comment',
@@ -416,7 +416,7 @@ class TestHeavyMailPerformance(TransactionCase):
record = self.umbrella.sudo(self.env.user)
template_id = self.env.ref('test_mail.mail_test_tpl').id
with self.assertQueryCount(__system__=102, emp=126): # com runbot: 99 - 123 // test_mail only: 102 - 123
with self.assertQueryCount(__system__=95, emp=101): # com runbot: 95 - 101 // test_mail only: 95 - 101
record.message_post_with_template(template_id, message_type='comment', composition_mode='comment')
self.assertEqual(record.message_ids[0].body, '<p>Adding stuff on %s</p>' % record.name)
@@ -485,7 +485,7 @@ class TestHeavyMailPerformance(TransactionCase):
'user_id': self.env.uid,
})
self.assertEqual(rec.message_partner_ids, self.partners | self.env.user.partner_id)
with self.assertQueryCount(__system__=60, emp=75): # com runbot: 60 - 75 // test_mail only: 60 - 72
with self.assertQueryCount(__system__=55, emp=58): # com runbot: 55 - 58 // test_mail only: 55 - 58
rec.write({'user_id': self.user_portal.id})
self.assertEqual(rec.message_partner_ids, self.partners | self.env.user.partner_id | self.user_portal.partner_id)
# write tracking message
@@ -505,7 +505,7 @@ class TestHeavyMailPerformance(TransactionCase):
customer_id = self.customer.id
user_id = self.user_portal.id
with self.assertQueryCount(__system__=155, emp=172): # com runbot: 155 - 172 // test_mail only: 154 - 168
with self.assertQueryCount(__system__=147, emp=155): # com runbot: 147 - 155 // test_mail only: 147 - 155
rec = self.env['mail.test.full'].create({
'name': 'Test',
'umbrella_id': umbrella_id,
@@ -532,7 +532,7 @@ class TestHeavyMailPerformance(TransactionCase):
})
self.assertEqual(rec.message_partner_ids, self.user_portal.partner_id | self.env.user.partner_id)
self.assertEqual(len(rec.message_ids), 1)
with self.assertQueryCount(__system__=99, emp=122): # com runbot: 99 - 122 // test_mail only: 99 - 119
with self.assertQueryCount(__system__=96, emp=101): # com runbot: 96 - 101 // test_mail only: 96 - 101
rec.write({
'name': 'Test2',
'umbrella_id': self.umbrella.id,
@@ -568,7 +568,7 @@ class TestHeavyMailPerformance(TransactionCase):
})
self.assertEqual(rec.message_partner_ids, self.user_portal.partner_id | self.env.user.partner_id)
with self.assertQueryCount(__system__=105, emp=127): # test_mail only: 104 - 123
with self.assertQueryCount(__system__=101, emp=106): # test_mail only: 101 - 106
rec.write({
'name': 'Test2',
'umbrella_id': umbrella_id,
@@ -600,7 +600,7 @@ class TestHeavyMailPerformance(TransactionCase):
})
self.assertEqual(rec.message_partner_ids, self.partners | self.env.user.partner_id | self.user_portal.partner_id)
with self.assertQueryCount(__system__=52, emp=66): # test_mail only: 51 - 66
with self.assertQueryCount(__system__=50, emp=62): # test_mail only: 50 - 62
rec.write({
'name': 'Test2',
'customer_id': customer_id,
@@ -617,3 +617,159 @@ class TestHeavyMailPerformance(TransactionCase):
self.assertEqual(rec.message_ids[2].subtype_id, self.env.ref('test_mail.st_mail_test_full_umbrella_upd'))
self.assertEqual(rec.message_ids[2].needaction_partner_ids, self.partners | self.user_portal.partner_id)
self.assertEqual(len(rec.message_ids), 3)
@tagged('mail_performance')
class TestMailPerformancePost(TransactionCase):
def setUp(self):
super(TestMailPerformancePost, self).setUp()
self._quick_create_ctx = {
'no_reset_password': True,
'mail_create_nolog': True,
'mail_create_nosubscribe': True,
'mail_notrack': True,
}
self.user_employee = self.env['res.users'].with_context(self._quick_create_ctx).create({
'name': 'Ernest Employee',
'login': 'emp',
'email': 'e.e@example.com',
'signature': '--\nErnest',
'notification_type': 'inbox',
'groups_id': [(6, 0, [self.env.ref('base.group_user').id])],
})
# record
self.customer = self.env['res.partner'].with_context(self._quick_create_ctx).create({
'name': 'customer',
'email': 'customer@example.com',
})
self.record = self.env['mail.test'].with_context(mail_create_nosubscribe=True).create({
'name': 'Test record',
'customer_id': self.customer.id,
'alias_name': 'test-alias',
})
# followers
self.user_follower_email = self.env['res.users'].with_context(self._quick_create_ctx).create({
'name': 'user_follower_email',
'login': 'user_follower_email',
'email': 'user_follower_email@example.com',
'notification_type': 'email',
'groups_id': [(6, 0, [self.env.ref('base.group_user').id])],
})
self.user_follower_inbox = self.env['res.users'].with_context(self._quick_create_ctx).create({
'name': 'user_follower_inbox',
'login': 'user_follower_inbox',
'email': 'user_follower_inbox@example.com',
'notification_type': 'inbox',
'groups_id': [(6, 0, [self.env.ref('base.group_user').id])],
})
self.partner_follower = self.env['res.partner'].with_context(self._quick_create_ctx).create({
'name': 'partner_follower',
'email': 'partner_follower@example.com',
})
self.record.message_subscribe([
self.partner_follower.id,
self.user_follower_inbox.partner_id.id,
self.user_follower_email.partner_id.id
])
# partner_ids
self.user_inbox = self.env['res.users'].with_context(self._quick_create_ctx).create({
'name': 'user_inbox',
'login': 'user_inbox',
'email': 'user_inbox@example.com',
'notification_type': 'inbox',
'groups_id': [(6, 0, [self.env.ref('base.group_user').id])],
})
self.user_email = self.env['res.users'].with_context(self._quick_create_ctx).create({
'name': 'user_email',
'login': 'user_email',
'email': 'user_email@example.com',
'notification_type': 'email',
'groups_id': [(6, 0, [self.env.ref('base.group_user').id])],
})
self.partner = self.env['res.partner'].with_context(self._quick_create_ctx).create({
'name': 'partner',
'email': 'partner@example.com',
})
# channels user/partner
self.partner_channel_inbox = self.env['res.partner'].with_context(self._quick_create_ctx).create({
'name': 'partner_channel_inbox',
'email': 'partner_channel_inbox@example.com',
})
self.partner_channel_email = self.env['res.partner'].with_context(self._quick_create_ctx).create({
'name': 'partner_channel_email',
'email': 'partner_channel_email@example.com',
})
self.user_channel_inbox = self.env['res.users'].with_context(self._quick_create_ctx).create({
'name': 'user_channel_inbox',
'login': 'user_channel_inbox',
'email': 'user_channel_inbox@example.com',
'notification_type': 'inbox',
'groups_id': [(6, 0, [self.env.ref('base.group_user').id])],
})
self.user_channel_email = self.env['res.users'].with_context(self._quick_create_ctx).create({
'name': 'user_channel_email',
'login': 'user_channel_email',
'email': 'user_channel_email@example.com',
'notification_type': 'inbox',
'groups_id': [(6, 0, [self.env.ref('base.group_user').id])],
})
# channels
self.channel_inbox = self.env['mail.channel'].with_context(self._quick_create_ctx).create({
'name': 'channel_inbox',
'channel_partner_ids': [(4, self.partner_channel_inbox.id), (4, self.user_channel_inbox.partner_id.id)]
})
self.channel_email = self.env['mail.channel'].with_context(self._quick_create_ctx).create({
'name': 'channel_email',
'email_send': True,
'channel_partner_ids': [(4, self.partner_channel_email.id), (4, self.user_channel_email.partner_id.id)]
})
self.vals = [{
'datas': base64.b64encode(bytes("attachement content %s" % i, 'utf-8')),
'name': 'attach %s' % i,
'datas_fname': 'fileText_test%s.txt' % i,
'mimetype': 'text/plain',
'res_model': 'mail.compose.message',
'res_id': 0,
} for i in range(3)]
self.patch(self.env.registry, 'ready', True)
@mute_logger('odoo.tests', 'odoo.addons.mail.models.mail_mail', 'odoo.models.unlink')
@users('emp')
@warmup
def test_complete_message_post(self):
# aims to cover as much features of message_post as possible
partner_ids = [self.user_inbox.partner_id.id, self.user_email.partner_id.id, self.partner.id]
channel_ids = [self.channel_inbox.id, self.channel_email.id]
record = self.record.sudo(self.env.user)
attachements = [ # not linear on number of attachements
('attach tuple 1', "attachement tupple content 1"),
('attach tuple 2', "attachement tupple content 2", {'cid': 'cid1'}),
('attach tuple 3', "attachement tupple content 3", {'cid': 'cid2'}),
]
self.attachements = self.env['ir.attachment'].sudo(self.env.user).create(self.vals) #-> 163-> 165 query
attachement_ids = self.attachements.ids
with self.assertQueryCount(emp=163): # com runbot 156 // test_mail only: 133
self.cr.sql_log = self.warm and self.cr.sql_log_count
record.with_context({}).message_post(
body='<p>Test body <img src="cid:cid1"> <img src="cid:cid2"></p>',
subject='Test Subject',
message_type='notification',
subtype=None,
partner_ids=partner_ids,
channel_ids=channel_ids,
parent_id=False,
attachments=attachements,
attachment_ids=attachement_ids,
add_sign=True,
model_description=False,
mail_auto_delete=True
)
self.cr.sql_log = False
self.assertTrue(record.message_ids[0].body.startswith('<p>Test body <img src="/web/image/'))
self.assertEqual(self.attachements.mapped('res_model'), [record._name for i in range(3)])
self.assertEqual(self.attachements.mapped('res_id'), [record.id for i in range(3)])
# self.assertEqual(record.message_ids[0].needaction_partner_ids, [])
+4 -4
View File
@@ -248,9 +248,9 @@ class BlogPost(models.Model):
}
@api.multi
def _notify_get_groups(self, message, groups):
def _notify_get_groups(self):
""" Add access button to everyone if the document is published. """
groups = super(BlogPost, self)._notify_get_groups(message, groups)
groups = super(BlogPost, self)._notify_get_groups()
if self.website_published:
for group_name, group_method, group_data in groups:
@@ -259,14 +259,14 @@ class BlogPost(models.Model):
return groups
@api.multi
def _notify_customize_recipients(self, message, msg_vals, recipients_vals):
def _notify_customize_recipients(self, message, msg_vals):
""" Override to avoid keeping all notified recipients of a comment.
We avoid tracking needaction on post comments. Only emails should be
sufficient. """
msg_type = msg_vals.get('message_type') or message.message_type
if msg_type == 'comment':
return {'needaction_partner_ids': []}
return {}
return super(BlogPost, self)._notify_customize_recipients(message, msg_vals)
def _default_website_meta(self):
res = super(BlogPost, self)._default_website_meta()
@@ -183,7 +183,7 @@ class CrmLead(models.Model):
if comment:
message += '<p>%s</p>' % comment
for lead in self:
lead.message_post(body=message, subtype="mail.mt_note")
lead.message_post(body=message)
lead.sudo().convert_opportunity(lead.partner_id.id) # sudo required to convert partner data
@api.multi
@@ -197,7 +197,7 @@ class CrmLead(models.Model):
self.message_unsubscribe(partner_ids=partner_ids.ids)
if comment:
message += '<p>%s</p>' % comment
self.message_post(body=message, subtype="mail.mt_note")
self.message_post(body=message)
values = {
'partner_assigned_id': False,
}
@@ -142,7 +142,7 @@ class Track(models.Model):
'composition_mode': 'comment',
'auto_delete_message': True,
'subtype_id': self.env['ir.model.data'].xmlid_to_res_id('mail.mt_note'),
'notif_layout': 'mail.mail_notification_light'
'email_layout_xmlid': 'mail.mail_notification_light'
})
return res
@@ -163,7 +163,7 @@ class Track(models.Model):
track._message_add_suggested_recipient(recipients, email=track.partner_email, reason=_('Speaker Email'))
return recipients
def _message_post_after_hook(self, message, *args, **kwargs):
def _message_post_after_hook(self, message, msg_vals):
if self.partner_email and not self.partner_id:
# we consider that posting a message with a specified recipient (not a follower, a specific one)
# on a document without customer means that it was created through the chatter using
@@ -175,7 +175,7 @@ class Track(models.Model):
('partner_email', '=', new_partner.email),
('stage_id.is_cancel', '=', False),
]).write({'partner_id': new_partner.id})
return super(Track, self)._message_post_after_hook(message, *args, **kwargs)
return super(Track, self)._message_post_after_hook(message, msg_vals)
@api.multi
def open_track_speakers_list(self):
+8 -8
View File
@@ -422,12 +422,13 @@ class Post(models.Model):
return post
@api.model
def check_mail_message_access(self, res_ids, operation, model_name=None):
def get_mail_message_access(self, res_ids, operation, model_name=None):
# XDO FIXME: to be correctly fixed with new get_mail_message_access and filter access rule
if operation in ('write', 'unlink') and (not model_name or model_name == 'forum.post'):
# Make sure only author or moderator can edit/delete messages
if any(not post.can_edit for post in self.browse(res_ids)):
raise KarmaError('Not enough karma to edit a post.')
return super(Post, self).check_mail_message_access(res_ids, operation, model_name=model_name)
return super(Post, self).get_mail_message_access(res_ids, operation, model_name=model_name)
@api.multi
def write(self, vals):
@@ -771,9 +772,9 @@ class Post(models.Model):
}
@api.multi
def _notify_get_groups(self, message, groups):
def _notify_get_groups(self):
""" Add access button to everyone if the document is active. """
groups = super(Post, self)._notify_get_groups(message, groups)
groups = super(Post, self)._notify_get_groups()
if self.state == 'active':
for group_name, group_method, group_data in groups:
@@ -784,7 +785,6 @@ class Post(models.Model):
@api.multi
@api.returns('mail.message', lambda value: value.id)
def message_post(self, message_type='notification', **kwargs):
question_followers = self.env['res.partner']
if self.ids and message_type == 'comment': # user comments have a restriction on karma
# add followers of comments on the parent post
if self.parent_id:
@@ -795,7 +795,7 @@ class Post(models.Model):
('res_id', '=', self.parent_id.id),
('partner_id', '!=', False),
]).filtered(lambda fol: comment_subtype in fol.subtype_ids).mapped('partner_id')
partner_ids += [(4, partner.id) for partner in question_followers]
partner_ids += question_followers.ids
kwargs['partner_ids'] = partner_ids
self.ensure_one()
@@ -806,14 +806,14 @@ class Post(models.Model):
return super(Post, self).message_post(message_type=message_type, **kwargs)
@api.multi
def _notify_customize_recipients(self, message, msg_vals, recipients_vals):
def _notify_customize_recipients(self, message, msg_vals):
""" Override to avoid keeping all notified recipients of a comment.
We avoid tracking needaction on post comments. Only emails should be
sufficient. """
msg_type = msg_vals.get('message_type') or message.message_type
if msg_type == 'comment':
return {'needaction_partner_ids': [], 'partner_ids': []}
return {}
return super(Post, self)._notify_customize_recipients(message, msg_vals)
class PostReason(models.Model):
@@ -14,18 +14,13 @@ class MailGroup(models.Model):
_inherit = 'mail.channel'
@api.multi
def _notify_specific_email_values(self, message):
res = super(MailGroup, self)._notify_specific_email_values(message)
try:
headers = safe_eval(res.get('headers', dict()))
except Exception:
headers = {}
def _notify_email_header_dict(self):
headers = super(MailGroup, self)._notify_email_header_dict()
base_url = self.env['ir.config_parameter'].sudo().get_param('web.base.url')
headers['List-Archive'] = '<%s/groups/%s>' % (base_url, slug(self)),
headers['List-Subscribe'] = '<%s/groups>' % (base_url),
headers['List-Unsubscribe'] = '<%s/groups?unsubscribe>' % (base_url,),
res['headers'] = repr(headers)
return res
return headers
@api.multi
def _send_confirmation_email(self, partner_ids, unsubscribe=False):
+3 -3
View File
@@ -337,9 +337,9 @@ class Slide(models.Model):
return super(Slide, self).get_access_action(access_uid)
@api.multi
def _notify_get_groups(self, message, groups):
def _notify_get_groups(self):
""" Add access button to everyone if the document is active. """
groups = super(Slide, self)._notify_get_groups(message, groups)
groups = super(Slide, self)._notify_get_groups()
if self.website_published:
for group_name, group_method, group_data in groups:
@@ -361,7 +361,7 @@ class Slide(models.Model):
subject=subject,
body=html_body,
subtype='website_slides.mt_channel_slide_published',
notif_layout='mail.mail_notification_light',
email_layout_xmlid='mail.mail_notification_light',
)
return True
+19 -4
View File
@@ -11,7 +11,7 @@ from collections import defaultdict
import uuid
from odoo import api, fields, models, tools, SUPERUSER_ID, _
from odoo.exceptions import AccessError, ValidationError
from odoo.exceptions import AccessError, ValidationError, MissingError
from odoo.tools import config, human_size, ustr, html_escape
from odoo.tools.mimetypes import guess_mimetype
@@ -313,6 +313,8 @@ class IrAttachment(models.Model):
def _check_serving_attachments(self):
# restrict writing on attachments that could be served by the
# ir.http's dispatch exception handling
# XDO note: this should be done in check(write), constraints for access rights?
# XDO note: if read on sudo, read twice, one for constraints, one for _inverse_datas as user
if self.env.user._is_admin():
return
if self.type == 'binary' and self.url:
@@ -326,13 +328,15 @@ class IrAttachment(models.Model):
In the 'document' module, it is overriden to relax this hard rule, since
more complex ones apply there.
"""
if self.env.user._is_superuser():
return True
# collect the records to check (by model)
model_ids = defaultdict(set) # {model_name: set(ids)}
require_employee = False
if self:
self._cr.execute('SELECT res_model, res_id, create_uid, public, res_field FROM ir_attachment WHERE id IN %s', [tuple(self.ids)])
for res_model, res_id, create_uid, public, res_field in self._cr.fetchall():
if self.env.user.id != SUPERUSER_ID and not self.env.user._is_system() and res_field:
if not self.env.user._is_system() and res_field:
raise AccessError(_("Sorry, you are not allowed to access this document."))
if public and mode == 'read':
continue
@@ -488,12 +492,20 @@ class IrAttachment(models.Model):
@api.model_create_multi
def create(self, vals_list):
record_tuple_set = set()
for values in vals_list:
# remove computed field depending of datas
for field in ('file_size', 'checksum'):
values.pop(field, False)
values = self._check_contents(values)
self.browse().check('write', values=values)
# 'check()' only uses res_model and res_id from values, and make an exists.
# We can group the values by model, res_id to make only one query when
# creating multiple attachments on a single record.
record_tuple = (values.get('res_model'), values.get('res_id'))
record_tuple_set.add(record_tuple)
for record_tuple in record_tuple_set:
(res_model, res_id) = record_tuple
self.check('write', values={'res_model':res_model, 'res_id':res_id})
return super(IrAttachment, self).create(vals_list)
@api.multi
@@ -504,10 +516,13 @@ class IrAttachment(models.Model):
def generate_access_token(self):
if self.access_token:
return self.access_token
access_token = str(uuid.uuid4())
access_token = self._generate_access_token()
self.write({'access_token': access_token})
return access_token
def _generate_access_token(self):
return str(uuid.uuid4())
@api.model
def action_get(self):
return self.env['ir.actions.act_window'].for_xml_id('base', 'action_attachment')