##############################################################################
import base64
+import datetime
import dateutil
import email
import logging
import pytz
+import re
import time
-from openerp import tools
import xmlrpclib
-
-from mako.template import Template as MakoTemplate
-
from email.message import Message
-from mail_message import decode
+
+from openerp import tools
from openerp import SUPERUSER_ID
+from openerp.addons.mail.mail_message import decode
from openerp.osv import fields, osv
-from openerp.osv.orm import browse_record
from openerp.tools.safe_eval import safe_eval as eval
-from tools.translate import _
_logger = logging.getLogger(__name__)
def decode_header(message, header, separator=' '):
- return separator.join(map(decode, message.get_all(header, [])))
+ return separator.join(map(decode, filter(None, message.get_all(header, []))))
class mail_thread(osv.AbstractModel):
_description = 'Email Thread'
_mail_flat_thread = True
- _TRACK_TEMPLATE = """
- %if message_description:
- <span>${message_description}</span>
- %endif
- <ul>
- %for name, change in tracked_values.items():
- <li><span>${name}</span>:
- %if change.get('old_value'):
- ${change.get('old_value')} ->
- %endif
- ${change.get('new_value')}</li>
- %endfor
- </ul>
- """
+ # Automatic logging system if mail installed
+ # _track = {
+ # 'field': {
+ # 'module.subtype_xml': lambda self, cr, uid, obj, context=None: obj[state] == done,
+ # 'module.subtype_xml2': lambda self, cr, uid, obj, context=None: obj[state] != done,
+ # },
+ # 'field2': {
+ # ...
+ # },
+ # }
+ # where
+ # :param string field: field name
+ # :param module.subtype_xml: xml_id of a mail.message.subtype (i.e. mail.mt_comment)
+ # :param obj: is a browse_record
+ # :param function lambda: returns whether the tracking should record using this subtype
+ _track = {}
def _get_message_data(self, cr, uid, ids, name, args, context=None):
""" Computes:
#------------------------------------------------------
def create(self, cr, uid, values, context=None):
- """ Override to subscribe the current user. """
+ """ Chatter override :
+ - subscribe uid
+ - subscribe followers of parent
+ - log a creation message
+ """
if context is None:
context = {}
thread_id = super(mail_thread, self).create(cr, uid, values, context=context)
# subscribe uid unless asked not to
- if not context.get('mail_nosubscribe'):
+ if not context.get('mail_create_nosubscribe'):
self.message_subscribe_users(cr, uid, [thread_id], [uid], context=context)
+ self.message_auto_subscribe(cr, uid, [thread_id], values.keys(), context=context)
- # automatic logging
- # self.message_post(cr, uid, thread_id, body='Document <b>created</b>.', context=context)
-
+ # automatic logging unless asked not to (mainly for various testing purpose)
+ if not context.get('mail_create_nolog'):
+ self.message_post(cr, uid, thread_id, body='Document created', context=context)
return thread_id
def write(self, cr, uid, ids, values, context=None):
+ if isinstance(ids, (int, long)):
+ ids = [ids]
+ # Track initial values of tracked fields
tracked_fields = self._get_tracked_fields(cr, uid, values.keys(), context=context)
- to_log = [name for name in values.keys() if name in tracked_fields]
- if to_log:
- initial = self.read(cr, uid, ids, [name for name, info in tracked_fields.items()], context=context)
+ if tracked_fields:
+ initial = self.read(cr, uid, ids, tracked_fields.keys(), context=context)
initial_values = dict((item['id'], item) for item in initial)
+ # Perform write, update followers
result = super(mail_thread, self).write(cr, uid, ids, values, context=context)
+ self.message_auto_subscribe(cr, uid, ids, values.keys(), context=context)
- if to_log:
- self.message_track(cr, uid, ids, values.keys(), initial_values, context=context)
+ # Perform the tracking
+ if tracked_fields:
+ self.message_track(cr, uid, ids, tracked_fields, initial_values, context=context)
return result
def unlink(self, cr, uid, ids, context=None):
:return list: a list of (field_name, column_info obj), containing
always tracked fields and modified on_change fields
"""
- return dict((name, column_info)
- for name, column_info in self._all_columns.items()
- if getattr(column_info.column, '_track_visibility', False) == 2
- or (getattr(column_info.column, '_track_visibility', False) == 1 and name in updated_fields))
-
- def message_track(self, cr, uid, ids, updated_fields, initial_values, log_message='', context=None):
- """
- :param list updated_fields: modified field names
- """
- translation_obj = self.pool.get('ir.translation')
-
- def format_false_value(field_obj):
- if field_obj._type == 'boolean':
- return False
- return field_obj._symbol_set[1](False)
-
- def convert_for_comparison(value, field_obj):
- if not value:
- return format_false_value(field_obj)
- if isinstance(value, browse_record): # compare browse record on id only
- return value.id
- if isinstance(value, tuple) and len(value) == 2: # name_get result
- return value[0]
- return value
-
- def convert_for_display(value, field_obj):
+ lst = []
+ for name, column_info in self._all_columns.items():
+ visibility = getattr(column_info.column, 'track_visibility', False)
+ if visibility == 'always' or (visibility == 'onchange' and name in updated_fields) or name in self._track:
+ lst.append(name)
+ if not lst:
+ return lst
+ return self.fields_get(cr, uid, lst, context=context)
+
+ def message_track(self, cr, uid, ids, tracked_fields, initial_values, context=None):
+
+ def convert_for_display(value, col_info):
+ if not value and col_info['type'] == 'boolean':
+ return 'False'
if not value:
- return format_false_value(field_obj)
- if field_obj._type == 'many2one':
- if isinstance(value, tuple) and len(value) == 2: # already name_get result
- return value[1]
- if not isinstance(value, browse_record): # value should be an ID
- value = self.pool.get(field_obj._obj).browse(cr, SUPERUSER_ID, value, context=None)
- return value.name_get()[0][1]
- if field_obj._type == 'selection': # CHS/TDE TODO: translated value ?
- select_value = filter(lambda item: item[0] == value, field_obj.selection)
- return select_value[0][1]
+ return ''
+ if col_info['type'] == 'many2one':
+ return value[1]
+ if col_info['type'] == 'selection':
+ return dict(col_info['selection'])[value]
return value
- def translate_field(column_info):
- model = column_info.parent_model or self._name
- return translation_obj._get_source(cr, uid, '{0},{1}'.format(model, column_info.name), 'field', context.get('lang'), column_info.column.string)
-
- tracked_fields = self._get_tracked_fields(cr, uid, updated_fields, context=context)
- to_log = [name for name in updated_fields if name in tracked_fields]
- if not to_log:
+ def format_message(message_description, tracked_values):
+ message = ''
+ if message_description:
+ message = '<span>%s</span>' % message_description
+ for name, change in tracked_values.items():
+ message += '<div> • <b>%s</b>: ' % change.get('col_info')
+ if change.get('old_value'):
+ message += '%s → ' % change.get('old_value')
+ message += '%s</div>' % change.get('new_value')
+ return message
+
+ if not tracked_fields:
return True
- # browse with SUPERUSER_ID to avoid rights issues (i.e. tracking res.partner relational field -> name_get result should always be visible)
- for record in self.browse(cr, SUPERUSER_ID, ids, context=context):
+ for record in self.read(cr, uid, ids, tracked_fields.keys(), context=context):
+ initial = initial_values[record['id']]
+ changes = []
tracked_values = {}
- default_log = True
- changes_found = False
- initial = initial_values[record.id]
# generate tracked_values data structure: {'col_name': {col_info, new_value, old_value}}
for col_name, col_info in tracked_fields.items():
- old_value = convert_for_comparison(initial[col_name], col_info.column)
- new_value = convert_for_comparison(record[col_name], col_info.column)
- if old_value == new_value and col_info.column._track_visibility == 2:
- tracked_values[col_name] = dict(col_info=col_info, new_value=convert_for_display(record[col_name], col_info.column))
- elif old_value != new_value:
- tracked_values[col_name] = dict(col_info=col_info, old_value=convert_for_display(initial[col_name], col_info.column), new_value=convert_for_display(record[col_name], col_info.column))
- changes_found = True
- if not changes_found:
+ if record[col_name] == initial[col_name] and getattr(self._all_columns[col_name].column, 'track_visibility', None) == 'always':
+ tracked_values[col_name] = dict(col_info=col_info['string'],
+ new_value=convert_for_display(record[col_name], col_info))
+ elif record[col_name] != initial[col_name]:
+ if getattr(self._all_columns[col_name].column, 'track_visibility', None) in ['always', 'onchange']:
+ tracked_values[col_name] = dict(col_info=col_info['string'],
+ old_value=convert_for_display(initial[col_name], col_info),
+ new_value=convert_for_display(record[col_name], col_info))
+ if col_name in tracked_fields:
+ changes.append(col_name)
+ if not changes:
continue
# find subtypes and post messages or log if no subtype found
- subtypes = set([subtype for field, track_info in self._track.items() if field in to_log
- for subtype, method in track_info.items() if method(self, cr, uid, record, context)])
- for subtype in subtypes:
- subtype_data = subtype.split('.')
- subtype_ref = self.pool.get('ir.model.data').get_object_reference(cr, uid, subtype_data[0], subtype_data[1])
- if not subtype_ref:
+ subtypes = []
+ for field, track_info in self._track.items():
+ if field not in changes:
continue
- subtype_rec = self.pool.get('mail.message.subtype').browse(cr, uid, subtype_ref[1], context=context)
- message = MakoTemplate(self._TRACK_TEMPLATE).render_unicode(message_description=subtype_rec.description, tracked_values=tracked_values)
- self.message_post(cr, uid, record.id, body=message, subtype=subtype, context=context)
- default_log = False
- if default_log:
- message = MakoTemplate(self._TRACK_TEMPLATE).render_unicode(message_description=log_message, tracked_values=tracked_values)
- self.message_post(cr, uid, record.id, body=message, context=context)
+ for subtype, method in track_info.items():
+ if method(self, cr, uid, record, context):
+ subtypes.append(subtype)
+ posted = False
+ for subtype in subtypes:
+ try:
+ subtype_rec = self.pool.get('ir.model.data').get_object(cr, uid, subtype.split('.')[0], subtype.split('.')[1])
+ except ValueError, e:
+ _logger.debug('subtype %s not found, giving error "%s"' % (subtype, e))
+ continue
+ message = format_message(subtype_rec.description if subtype_rec.description else subtype_rec.name, tracked_values)
+ self.message_post(cr, uid, record['id'], body=message, subtype=subtype, context=context)
+ posted = True
+ if not posted:
+ message = format_message('', tracked_values)
+ self.message_post(cr, uid, record['id'], body=message, context=context)
return True
#------------------------------------------------------
return []
#------------------------------------------------------
+ # Email specific
+ #------------------------------------------------------
+
+ def message_get_reply_to(self, cr, uid, ids, context=None):
+ if not self._inherits.get('mail.alias'):
+ return [False for id in ids]
+ return ["%s@%s" % (record['alias_name'], record['alias_domain'])
+ if record.get('alias_domain') and record.get('alias_name')
+ else False
+ for record in self.read(cr, uid, ids, ['alias_name', 'alias_domain'], context=context)]
+
+ #------------------------------------------------------
# Mail gateway
#------------------------------------------------------
""" Find partners related to some header fields of the message. """
s = ', '.join([decode(message.get(h)) for h in header_fields if message.get(h)])
return [partner_id for email in tools.email_split(s)
- for partner_id in self.pool.get('res.partner').search(cr, uid, [('email', 'ilike', email)], context=context)]
+ for partner_id in self.pool.get('res.partner').search(cr, uid, [('email', 'ilike', email)], limit=1, context=context)]
def _message_find_user_id(self, cr, uid, message, context=None):
from_local_part = tools.email_split(decode(message.get('From')))[0]
message_id, model, thread_id, custom_values, uid)
return [(model, thread_id, custom_values, uid)]
- # Verify this is a reply to a private message
- message_ids = self.pool.get('mail.message').search(cr, uid, [('message_id', '=', in_reply_to)], limit=1, context=context)
- if message_ids:
- message = self.pool.get('mail.message').browse(cr, uid, message_ids[0], context=context)
- _logger.debug('Routing mail with Message-Id %s: direct reply to a private message: %s, custom_values: %s, uid: %s',
- message_id, message.id, custom_values, uid)
- return [(message.model, message.res_id, custom_values, uid)]
+ # Verify whether this is a reply to a private message
+ if in_reply_to:
+ message_ids = self.pool.get('mail.message').search(cr, uid, [('message_id', '=', in_reply_to)], limit=1, context=context)
+ if message_ids:
+ message = self.pool.get('mail.message').browse(cr, uid, message_ids[0], context=context)
+ _logger.debug('Routing mail with Message-Id %s: direct reply to a private message: %s, custom_values: %s, uid: %s',
+ message_id, message.id, custom_values, uid)
+ return [(message.model, message.res_id, custom_values, uid)]
# 2. Look for a matching mail.alias entry
# Delivered-To is a safe bet in most modern MTAs, but we have to fallback on To + Cc values
# for all the odd MTAs out there, as there is no standard header for the envelope's `rcpt_to` value.
- rcpt_tos = decode_header(message, 'Delivered-To') or \
- ','.join([decode_header(message, 'To'),
+ rcpt_tos = \
+ ','.join([decode_header(message, 'Delivered-To'),
+ decode_header(message, 'To'),
decode_header(message, 'Cc'),
decode_header(message, 'Resent-To'),
decode_header(message, 'Resent-Cc')])
for alias in mail_alias.browse(cr, uid, alias_ids, context=context):
user_id = alias.alias_user_id.id
if not user_id:
- user_id = self._message_find_user_id(cr, uid, message, context=context)
+ # TDE note: this could cause crashes, because no clue that the user
+ # that send the email has the right to create or modify a new document
+ # Fallback on user_id = uid
+ # Note: recognized partners will be added as followers anyway
+ # user_id = self._message_find_user_id(cr, uid, message, context=context)
+ user_id = uid
+ _logger.debug('No matching user_id for the alias %s', alias.alias_name)
routes.append((alias.alias_model_id.model, alias.alias_force_thread_id, \
eval(alias.alias_defaults), user_id))
_logger.debug('Routing mail with Message-Id %s: direct alias match: %r', message_id, routes)
msg = self.message_parse(cr, uid, msg_txt, save_original=save_original, context=context)
if strip_attachments:
msg.pop('attachments', None)
+
+ # postpone setting msg.partner_ids after message_post, to avoid double notifications
+ partner_ids = msg.pop('partner_ids', [])
+
thread_id = False
for model, thread_id, custom_values, user_id in routes:
if self._name != model:
assert thread_id and hasattr(model_pool, 'message_update') or hasattr(model_pool, 'message_new'), \
"Undeliverable mail with Message-Id %s, model %s does not accept incoming emails" % \
(msg['message_id'], model)
+
+ # disabled subscriptions during message_new/update to avoid having the system user running the
+ # email gateway become a follower of all inbound messages
+ nosub_ctx = dict(context, mail_create_nosubscribe=True)
if thread_id and hasattr(model_pool, 'message_update'):
- model_pool.message_update(cr, user_id, [thread_id], msg, context=context)
+ model_pool.message_update(cr, user_id, [thread_id], msg, context=nosub_ctx)
else:
- thread_id = model_pool.message_new(cr, user_id, msg, custom_values, context=context)
+ thread_id = model_pool.message_new(cr, user_id, msg, custom_values, context=nosub_ctx)
else:
assert thread_id == 0, "Posting a message without model should be with a null res_id, to create a private message."
model_pool = self.pool.get('mail.thread')
- model_pool.message_post_user_api(cr, uid, [thread_id], context=context, content_subtype='html', **msg)
+ new_msg_id = model_pool.message_post_user_api(cr, uid, [thread_id], context=context, content_subtype='html', **msg)
+
+ # when posting an incoming email to a document: subscribe the author, if a partner, as follower
+ if model and thread_id and msg.get('author_id'):
+ model_pool.message_subscribe(cr, uid, [thread_id], [msg.get('author_id')], context=context)
+
+ if partner_ids:
+ # postponed after message_post, because this is an external message and we don't want to create
+ # duplicate emails due to notifications
+ self.pool.get('mail.message').write(cr, uid, [new_msg_id], {'partner_ids': partner_ids}, context=context)
+
return thread_id
def message_new(self, cr, uid, msg_dict, custom_values=None, context=None):
"""
if context is None:
context = {}
+ data = {}
+ if isinstance(custom_values, dict):
+ data = custom_values.copy()
model = context.get('thread_model') or self._name
model_pool = self.pool.get(model)
fields = model_pool.fields_get(cr, uid, context=context)
- data = model_pool.default_get(cr, uid, fields, context=context)
if 'name' in fields and not data.get('name'):
data['name'] = msg_dict.get('subject', '')
- if custom_values and isinstance(custom_values, dict):
- data.update(custom_values)
res_id = model_pool.create(cr, uid, data, context=context)
return res_id
alternative = (message.get_content_type() == 'multipart/alternative')
for part in message.walk():
if part.get_content_maintype() == 'multipart':
- continue # skip container
- filename = part.get_filename() # None if normal part
- encoding = part.get_content_charset() # None if attachment
+ continue # skip container
+ filename = part.get_filename() # None if normal part
+ encoding = part.get_content_charset() # None if attachment
# 1) Explicit Attachments -> attachments
if filename or part.get('content-disposition', '').strip().startswith('attachment'):
attachments.append((filename or 'attachment', part.get_payload(decode=True)))
msg_dict['author_id'] = author_ids[0]
else:
msg_dict['email_from'] = message.get('from')
- partner_ids = self._message_find_partners(cr, uid, message, ['From', 'To', 'Cc'], context=context)
+ partner_ids = self._message_find_partners(cr, uid, message, ['To', 'Cc'], context=context)
msg_dict['partner_ids'] = [(4, partner_id) for partner_id in partner_ids]
if 'Date' in message:
- date_hdr = decode(message.get('Date'))
- # convert from email timezone to server timezone
- date_server_datetime = dateutil.parser.parse(date_hdr).astimezone(pytz.timezone(tools.get_server_timezone()))
- date_server_datetime_str = date_server_datetime.strftime(tools.DEFAULT_SERVER_DATETIME_FORMAT)
- msg_dict['date'] = date_server_datetime_str
+ try:
+ date_hdr = decode(message.get('Date'))
+ parsed_date = dateutil.parser.parse(date_hdr, fuzzy=True)
+ if parsed_date.utcoffset() is None:
+ # naive datetime, so we arbitrarily decide to make it
+ # UTC, there's no better choice. Should not happen,
+ # as RFC2822 requires timezone offset in Date headers.
+ stored_date = parsed_date.replace(tzinfo=pytz.utc)
+ else:
+ stored_date = parsed_date.astimezone(pytz.utc)
+ except Exception:
+ _logger.warning('Failed to parse Date header %r in incoming mail '
+ 'with message-id %r, assuming current date/time.',
+ message.get('Date'), message_id)
+ stored_date = datetime.datetime.now()
+ msg_dict['date'] = stored_date.strftime(tools.DEFAULT_SERVER_DATETIME_FORMAT)
if 'In-Reply-To' in message:
parent_ids = self.pool.get('mail.message').search(cr, uid, [('message_id', '=', decode(message['In-Reply-To']))])
"now deprecated res.log.")
self.message_post(cr, uid, [id], message, context=context)
+ def message_create_partners_from_emails(self, cr, uid, emails, context=None):
+ """ Convert a list of emails into a list partner_ids and a list
+ new_partner_ids. The return value is non conventional because
+ it is meant to be used by the mail widget.
+
+ :return dict: partner_ids and new_partner_ids
+ """
+ partner_obj = self.pool.get('res.partner')
+ mail_message_obj = self.pool.get('mail.message')
+
+ partner_ids = []
+ new_partner_ids = []
+ for email in emails:
+ m = re.search(r"((.+?)\s*<)?([^<>]+@[^<>]+)>?", email, re.IGNORECASE | re.DOTALL)
+ name = m.group(2) or m.group(0)
+ email = m.group(3)
+ ids = partner_obj.search(cr, SUPERUSER_ID, [('email', '=', email)], context=context)
+ if ids:
+ partner_ids.append(ids[0])
+ partner_id = ids[0]
+ else:
+ partner_id = partner_obj.create(cr, uid, {
+ 'name': name or email,
+ 'email': email,
+ }, context=context)
+ new_partner_ids.append(partner_id)
+
+ # link mail with this from mail to the new partner id
+ message_ids = mail_message_obj.search(cr, SUPERUSER_ID, ['|', ('email_from', '=', email), ('email_from', 'ilike', '<%s>' % email), ('author_id', '=', False)], context=context)
+ if message_ids:
+ mail_message_obj.write(cr, SUPERUSER_ID, message_ids, {'email_from': None, 'author_id': partner_id}, context=context)
+ return {
+ 'partner_ids': partner_ids,
+ 'new_partner_ids': new_partner_ids,
+ }
+
def message_post(self, cr, uid, thread_id, body='', subject=None, type='notification',
subtype=None, parent_id=False, attachments=None, context=None, **kwargs):
""" Post a new message in an existing thread, returning the new
mail_message = self.pool.get('mail.message')
model = context.get('thread_model', self._name) if thread_id else False
- attachment_ids = []
+ attachment_ids = kwargs.pop('attachment_ids', [])
for name, content in attachments:
if isinstance(content, unicode):
content = content.encode('utf-8')
return mail_message.create(cr, uid, values, context=context)
- def message_post_user_api(self, cr, uid, thread_id, body='', subject=False, parent_id=False,
- attachment_ids=None, context=None, content_subtype='plaintext', **kwargs):
+ def message_post_user_api(self, cr, uid, thread_id, body='', parent_id=False,
+ attachment_ids=None, content_subtype='plaintext',
+ context=None, **kwargs):
""" Wrapper on message_post, used for user input :
- mail gateway
- quick reply in Chatter (refer to mail.js), not
- attachment_ids: supposed not attached to any document; attach them
to the related document. Should only be set by Chatter.
"""
+ mail_message_obj = self.pool.get('mail.message')
ir_attachment = self.pool.get('ir.attachment')
- mail_message = self.pool.get('mail.message')
- # 1. Pre-processing: body, partner_ids, type and subtype
- if content_subtype == 'plaintext':
- body = tools.plaintext2html(body)
-
- partner_ids = kwargs.pop('partner_ids', [])
- if parent_id:
- parent_message = self.pool.get('mail.message').browse(cr, uid, parent_id, context=context)
- partner_ids += [(4, partner.id) for partner in parent_message.partner_ids]
- # TDE FIXME HACK: mail.thread -> private message
- if self._name == 'mail.thread' and parent_message.author_id.id:
- partner_ids.append((4, parent_message.author_id.id))
+ # 1.A.1: add recipients of parent message (# TDE FIXME HACK: mail.thread -> private message)
+ partner_ids = set([])
+ if parent_id and self._name == 'mail.thread':
+ parent_message = mail_message_obj.browse(cr, uid, parent_id, context=context)
+ partner_ids |= set([(4, partner.id) for partner in parent_message.partner_ids])
+ if parent_message.author_id.id:
+ partner_ids.add((4, parent_message.author_id.id))
+
+ # 1.A.2: add specified recipients
+ param_partner_ids = set()
+ for item in kwargs.pop('partner_ids', []):
+ if isinstance(item, (list)):
+ param_partner_ids.add((item[0], item[1]))
+ elif isinstance(item, (int, long)):
+ param_partner_ids.add((4, item))
+ else:
+ param_partner_ids.add(item)
+ partner_ids |= param_partner_ids
- message_type = kwargs.pop('type', 'comment')
- message_subtype = kwargs.pop('subtype', 'mail.mt_comment')
+ # 1.A.3: add parameters recipients as follower
+ # TDE FIXME in 7.1: should check whether this comes from email_list or partner_ids
+ if param_partner_ids and self._name != 'mail.thread':
+ self.message_subscribe(cr, uid, [thread_id], [pid[1] for pid in param_partner_ids], context=context)
- # 2. Post message
- new_message_id = self.message_post(cr, uid, thread_id=thread_id, body=body, subject=subject, type=message_type,
- subtype=message_subtype, parent_id=parent_id, context=context, partner_ids=partner_ids, **kwargs)
+ # 1.B: handle body, message_type and message_subtype
+ if content_subtype == 'plaintext':
+ body = tools.plaintext2html(body)
+ msg_type = kwargs.pop('type', 'comment')
+ msg_subtype = kwargs.pop('subtype', 'mail.mt_comment')
- # 3. Post-processing
+ # 2. Pre-processing: attachments
# HACK TDE FIXME: Chatter: attachments linked to the document (not done JS-side), load the message
if attachment_ids:
# TDE FIXME (?): when posting a private message, we use mail.thread as a model
if filtered_attachment_ids:
if thread_id and model:
ir_attachment.write(cr, SUPERUSER_ID, attachment_ids, {'res_model': model, 'res_id': thread_id}, context=context)
- mail_message.write(cr, SUPERUSER_ID, [new_message_id], {'attachment_ids': [(6, 0, [pid for pid in attachment_ids])]}, context=context)
+ else:
+ attachment_ids = []
+ attachment_ids = [(4, id) for id in attachment_ids]
- return new_message_id
+ # 3. Post message
+ return self.message_post(cr, uid, thread_id=thread_id, body=body,
+ type=msg_type, subtype=msg_subtype, parent_id=parent_id,
+ attachment_ids=attachment_ids, partner_ids=list(partner_ids), context=context, **kwargs)
#------------------------------------------------------
# Followers API
def message_subscribe(self, cr, uid, ids, partner_ids, subtype_ids=None, context=None):
""" Add partners to the records followers. """
- self.check_access_rights(cr, uid, 'read')
+ user_pid = self.pool.get('res.users').read(cr, uid, uid, ['partner_id'], context=context)['partner_id'][0]
+ if set(partner_ids) == set([user_pid]):
+ self.check_access_rights(cr, uid, 'read')
+ else:
+ self.check_access_rights(cr, uid, 'write')
+
self.write(cr, SUPERUSER_ID, ids, {'message_follower_ids': [(4, pid) for pid in partner_ids]}, context=context)
# if subtypes are not specified (and not set to a void list), fetch default ones
if subtype_ids is None:
def message_unsubscribe(self, cr, uid, ids, partner_ids, context=None):
""" Remove partners from the records followers. """
- self.check_access_rights(cr, uid, 'read')
+ user_pid = self.pool.get('res.users').read(cr, uid, uid, ['partner_id'], context=context)['partner_id'][0]
+ if set(partner_ids) == set([user_pid]):
+ self.check_access_rights(cr, uid, 'read')
+ else:
+ self.check_access_rights(cr, uid, 'write')
return self.write(cr, SUPERUSER_ID, ids, {'message_follower_ids': [(3, pid) for pid in partner_ids]}, context=context)
- def _subscribe_followers_subtype(self, cr, uid, ids, res_id, model, context=None):
- """ TDE note: not the best way to do this, we could override _get_followers
- of task, and perform a better mapping of subtypes than a mapping
- based on names.
- However we will keep this implementation, maybe to be refactored
- in 7.1 of future versions. """
+ def _message_get_auto_subscribe_fields(self, cr, uid, updated_fields, auto_follow_fields=['user_id'], context=None):
+ """ Returns the list of relational fields linking to res.users that should
+ trigger an auto subscribe. The default list checks for the fields
+ - called 'user_id'
+ - linking to res.users
+ - with track_visibility set
+ In OpenERP V7, this is sufficent for all major addon such as opportunity,
+ project, issue, recruitment, sale.
+ Override this method if a custom behavior is needed about fields
+ that automatically subscribe users.
+ """
+ user_field_lst = []
+ for name, column_info in self._all_columns.items():
+ if name in auto_follow_fields and name in updated_fields and getattr(column_info.column, 'track_visibility', False) and column_info.column._obj == 'res.users':
+ user_field_lst.append(name)
+ return user_field_lst
+
+ def message_auto_subscribe(self, cr, uid, ids, updated_fields, context=None):
+ """
+ 1. fetch project subtype related to task (parent_id.res_model = 'project.task')
+ 2. for each project subtype: subscribe the follower to the task
+ """
subtype_obj = self.pool.get('mail.message.subtype')
follower_obj = self.pool.get('mail.followers')
- # create mapping
- subtype_ids = subtype_obj.search(cr, uid, ['|', ('res_model', '=', False), ('res_model', '=', self._name)], context=context)
- subtypes = subtype_obj.browse(cr, uid, subtype_ids, context=context)
- # fetch subscriptions
- follower_ids = follower_obj.search(cr, uid, [('res_model', '=', model), ('res_id', '=', res_id)], context=context)
- # copy followers
- for follower in follower_obj.browse(cr, uid, follower_ids, context=context):
- if not follower.subtype_ids:
- continue
- subtype_names = [follower_subtype.name for follower_subtype in follower.subtype_ids]
- subtype_ids = [subtype.id for subtype in subtypes if subtype.name in subtype_names]
- self.message_subscribe(cr, uid, ids, [follower.partner_id.id],
- subtype_ids=subtype_ids, context=context)
+
+ # fetch auto_follow_fields
+ user_field_lst = self._message_get_auto_subscribe_fields(cr, uid, updated_fields, context=context)
+
+ # fetch related record subtypes
+ related_subtype_ids = subtype_obj.search(cr, uid, ['|', ('res_model', '=', False), ('parent_id.res_model', '=', self._name)], context=context)
+ subtypes = subtype_obj.browse(cr, uid, related_subtype_ids, context=context)
+ default_subtypes = [subtype for subtype in subtypes if subtype.res_model == False]
+ related_subtypes = [subtype for subtype in subtypes if subtype.res_model != False]
+ relation_fields = set([subtype.relation_field for subtype in subtypes if subtype.relation_field != False])
+ if (not related_subtypes or not any(relation in updated_fields for relation in relation_fields)) and not user_field_lst:
+ return True
+
+ for record in self.browse(cr, uid, ids, context=context):
+ new_followers = dict()
+ parent_res_id = False
+ parent_model = False
+ for subtype in related_subtypes:
+ if not subtype.relation_field or not subtype.parent_id:
+ continue
+ if not subtype.relation_field in self._columns or not getattr(record, subtype.relation_field, False):
+ continue
+ parent_res_id = getattr(record, subtype.relation_field).id
+ parent_model = subtype.res_model
+ follower_ids = follower_obj.search(cr, SUPERUSER_ID, [
+ ('res_model', '=', parent_model),
+ ('res_id', '=', parent_res_id),
+ ('subtype_ids', 'in', [subtype.id])
+ ], context=context)
+ for follower in follower_obj.browse(cr, SUPERUSER_ID, follower_ids, context=context):
+ new_followers.setdefault(follower.partner_id.id, set()).add(subtype.parent_id.id)
+
+ if parent_res_id and parent_model:
+ for subtype in default_subtypes:
+ follower_ids = follower_obj.search(cr, SUPERUSER_ID, [
+ ('res_model', '=', parent_model),
+ ('res_id', '=', parent_res_id),
+ ('subtype_ids', 'in', [subtype.id])
+ ], context=context)
+ for follower in follower_obj.browse(cr, SUPERUSER_ID, follower_ids, context=context):
+ new_followers.setdefault(follower.partner_id.id, set()).add(subtype.id)
+
+ # add followers coming from res.users relational fields that are tracked
+ user_ids = [getattr(record, name).id for name in user_field_lst if getattr(record, name)]
+ for partner_id in [user.partner_id.id for user in self.pool.get('res.users').browse(cr, SUPERUSER_ID, user_ids, context=context)]:
+ new_followers.setdefault(partner_id, None)
+
+ for pid, subtypes in new_followers.items():
+ subtypes = list(subtypes) if subtypes is not None else None
+ self.message_subscribe(cr, uid, [record.id], [pid], subtypes, context=context)
+ return True
#------------------------------------------------------
# Thread state