diff options
| author | Thibault Delavallée <tde@openerp.com> | 2015-04-27 11:49:02 +0200 |
|---|---|---|
| committer | Thibault Delavallée <tde@openerp.com> | 2015-04-27 12:52:45 +0200 |
| commit | 4a975f1b7d719ebbf17e0d4194cf3fcc56e2f56f (patch) | |
| tree | 96a29b7f19adabe5f371b023116da887bb0ace03 /addons/mail/mail_message.py | |
| parent | 4b122ad41d873febe288ab1f8c6cdc078ca2a479 (diff) | |
[MOV] mail: moved files according to the new module organization
Diffstat (limited to 'addons/mail/mail_message.py')
| -rw-r--r-- | addons/mail/mail_message.py | 845 |
1 files changed, 0 insertions, 845 deletions
diff --git a/addons/mail/mail_message.py b/addons/mail/mail_message.py deleted file mode 100644 index 14c23e78862..00000000000 --- a/addons/mail/mail_message.py +++ /dev/null @@ -1,845 +0,0 @@ -# -*- coding: utf-8 -*- - -from email.header import decode_header -from email.utils import formataddr -import logging - -from openerp import _, api, fields, models, SUPERUSER_ID -from openerp import tools -from openerp.exceptions import UserError, AccessError - - -_logger = logging.getLogger(__name__) - - -def decode(text): - """Returns unicode() string conversion of the the given encoded smtp header text""" - # TDE proposal: move to tools ? - if text: - text = decode_header(text.replace('\r', '')) - return ''.join([tools.ustr(x[0], x[1]) for x in text]) - - -class Message(models.Model): - """ Messages model: system notification (replacing res.log notifications), - comments (OpenChatter discussion) and incoming emails. """ - _name = 'mail.message' - _description = 'Message' - _inherit = ['ir.needaction_mixin'] - _order = 'id desc' - _rec_name = 'record_name' - - _message_read_limit = 30 - _message_read_fields = ['id', 'parent_id', 'model', 'res_id', 'body', 'subject', 'date', 'to_read', 'email_from', - 'message_type', 'vote_user_ids', 'attachment_ids', 'author_id', 'partner_ids', 'record_name'] - _message_record_name_length = 18 - _message_read_more_limit = 1024 - - @api.model - def _get_default_from(self): - if self.env.user.alias_name and self.env.user.alias_domain: - return formataddr((self.env.user.name, '%s@%s' % (self.env.user.alias_name, self.env.user.alias_domain))) - elif self.env.user.email: - return formataddr((self.env.user.name, self.env.user.email)) - raise UserError(_("Unable to send email, please configure the sender's email address or alias.")) - - @api.model - def _get_default_author(self): - return self.env.user.partner_id.id - - message_type = fields.Selection([ - ('email', 'Email'), - ('comment', 'Comment'), - ('notification', 'System 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", - oldname='type') - email_from = fields.Char('From', default=_get_default_from, - help="Email address of the sender. This field is set when no matching partner is found for incoming emails.") - reply_to = fields.Char('Reply-To', help='Reply email address. Setting the reply_to bypasses the automatic thread creation.') - no_auto_thread = fields.Boolean('No threading for answers', help='Answers do not go in the original document\' discussion thread. This has an impact on the generated message-id.') - author_id = fields.Many2one( - 'res.partner', 'Author', select=1, - ondelete='set null', default=_get_default_author, - help="Author of the message. If not set, email_from may hold an email address that did not match any partner.") - author_avatar = fields.Binary("Author's avatar", related='author_id.image_small') - partner_ids = fields.Many2many('res.partner', string='Recipients') - notified_partner_ids = fields.Many2many( - 'res.partner', 'mail_notification', - 'message_id', 'partner_id', 'Notified partners', - help='Partners that have a notification pushing this message in their mailboxes') - attachment_ids = fields.Many2many( - 'ir.attachment', 'message_attachment_rel', - 'message_id', 'attachment_id', 'Attachments') - parent_id = fields.Many2one( - 'mail.message', 'Parent Message', select=True, - ondelete='set null', help="Initial thread message.") - child_ids = fields.One2many('mail.message', 'parent_id', 'Child Messages') - model = fields.Char('Related Document Model', select=1) - res_id = fields.Integer('Related Document ID', select=1) - record_name = fields.Char('Message Record Name', help="Name get of the related document.") - notification_ids = fields.One2many( - 'mail.notification', 'message_id', - string='Notifications', auto_join=True, - help='Technical field holding the message notifications. Use notified_partner_ids to access notified partners.') - subject = fields.Char('Subject') - date = fields.Datetime('Date', default=fields.Datetime.now) - message_id = fields.Char('Message-Id', help='Message unique identifier', select=1, readonly=1, copy=False) - body = fields.Html('Contents', default='', help='Automatically sanitized HTML contents') - to_read = fields.Boolean( - 'To read', compute='_get_to_read', search='_search_to_read', - help='Current user has an unread notification linked to this message') - starred = fields.Boolean( - 'Starred', compute='_get_starred', search='_search_starred', - help='Current user has a starred notification linked to this message') - subtype_id = fields.Many2one('mail.message.subtype', 'Subtype', ondelete='set null', select=1) - vote_user_ids = fields.Many2many( - 'res.users', 'mail_vote', 'message_id', 'user_id', string='Votes', - help='Users that voted for this message') - mail_server_id = fields.Many2one('ir.mail_server', 'Outgoing mail server', readonly=1) - - @api.depends('notification_ids') - def _get_to_read(self): - """ Compute if the message is unread by the current user. """ - partner_id = self.env.user.partner_id.id - notifications = self.env['mail.notification'].sudo().search([ - ('partner_id', '=', partner_id), - ('message_id', 'in', self.ids), - ('is_read', '=', False)]) - for message in self: - message.to_read = message in notifications.mapped('message_id') - - def _search_to_read(self, operator, operand): - """ Search for messages to read by the current user. Condition is - inversed because we search unread message on a is_read column. """ - return ['&', ('notification_ids.partner_id.user_ids', 'in', [self.env.uid]), ('notification_ids.is_read', operator, not operand)] - - @api.depends('notification_ids') - def _get_starred(self): - """ Compute if the message is starred by the current user. """ - partner_id = self.env.user.partner_id.id - notifications = self.env['mail.notification'].sudo().search([ - ('partner_id', '=', partner_id), - ('message_id', 'in', self.ids), - ('starred', '=', True)]) - for message in self: - message.starred = message in notifications.mapped('message_id') - - def _search_starred(self, operator, operand): - """ Search for starred messages by the current user.""" - return ['&', ('notification_ids.partner_id.user_ids', 'in', [self.env.uid]), ('notification_ids.starred', operator, operand)] - - @api.model - def _needaction_domain_get(self): - return [('to_read', '=', True)] - - #------------------------------------------------------ - # Vote/Like - #------------------------------------------------------ - - @api.multi - def vote_toggle(self): - ''' Toggles vote. Performed using read to avoid access rights issues. ''' - for message in self.sudo(): - new_has_voted = not (self._uid in message.vote_user_ids.ids) - if new_has_voted: - self.browse(message.id).write({'vote_user_ids': [(4, self._uid)]}) # tde: todo with user access rights - else: - self.browse(message.id).write({'vote_user_ids': [(3, self._uid)]}) # tde: todo with user access rights - return new_has_voted or False - - #------------------------------------------------------ - # download an attachment - #------------------------------------------------------ - - @api.multi - def download_attachment(self, attachment_id): - self.ensure_one() - if attachment_id in self.attachment_ids.ids: - attachment = self.env['ir.attachment'].sudo().browse(attachment_id) - if attachment.datas and attachment.datas_fname: - return { - 'base64': attachment.datas, - 'filename': attachment.datas_fname, - } - return False - - #------------------------------------------------------ - # Notification API - #------------------------------------------------------ - - @api.multi - def set_message_read(self, read, create_missing=True): - """ Set messages as (un)read. Technically, the notifications related - to uid are set to (un)read. If for some msg_ids there are missing - notifications (i.e. due to load more or thread parent fetching), - they are created. - - :param bool read: set notification as (un)read - :param bool create_missing: create notifications for missing entries - (i.e. when acting on displayed messages not notified) - - :return number of message mark as read - """ - notifications = self.env['mail.notification'].search([ - ('partner_id', '=', self.env.user.partner_id.id), - ('message_id', 'in', self.ids), - ('is_read', '=', not read)]) - notifications.write({'is_read': read}) - - # some messages do not have notifications: find which one, create notification, update read status - if len(notifications) < len(self) and create_missing: - for message in self - notifications.mapped('message_id'): - self.env['mail.notification'].create({'partner_id': self.env.user.partner_id.id, 'is_read': read, 'message_id': message.id}) - - return len(notifications) - - @api.multi - def set_message_starred(self, starred, create_missing=True): - """ Set messages as (un)starred. Technically, the notifications related - to uid are set to (un)starred. - - :param bool starred: set notification as (un)starred - :param bool create_missing: create notifications for missing entries - (i.e. when acting on displayed messages not notified) - """ - values = {'starred': starred} - if starred: - values['is_read'] = False - notifications = self.env['mail.notification'].search([ - ('partner_id', '=', self.env.user.partner_id.id), - ('message_id', 'in', self.ids), - ('starred', '=', not starred)]) - notifications.write(values) - - # some messages do not have notifications: find which one, create notification, update starred status - if len(notifications) < len(self) and create_missing: - values['partner_id'] = self.env.user.partner_id.id - for message in self - notifications.mapped('message_id'): - values['message_id'] = message.id - self.env['mail.notification'].create(values) - - return starred - - #------------------------------------------------------ - # Message loading for web interface - #------------------------------------------------------ - - @api.model - def _message_read_dict_postprocess(self, messages, message_tree): - """ Post-processing on values given by message_read. This method will - handle partners in batch to avoid doing numerous queries. - - :param list messages: list of message, as get_dict result - :param dict message_tree: {[msg.id]: msg browse record} - """ - pid = self.env.user.partner_id.id - - # 1. Aggregate partners (author_id and partner_ids) and attachments - partner_ids = set() - attachment_ids = set() - for key, message in message_tree.iteritems(): - if message.author_id: - partner_ids |= set([message.author_id.id]) - if message.subtype_id and message.notified_partner_ids: # take notified people of message with a subtype - partner_ids |= set([partner.id for partner in message.notified_partner_ids]) - elif not message.subtype_id and message.partner_ids: # take specified people of message without a subtype (log) - partner_ids |= set([partner.id for partner in message.partner_ids]) - if message.attachment_ids: - attachment_ids |= set([attachment.id for attachment in message.attachment_ids]) - # Read partners as SUPERUSER -> display the names like classic m2o even if no access - partners = self.env['res.partner'].sudo().browse(partner_ids).name_get() - partner_tree = dict((partner[0], partner) for partner in partners) - - # 2. Attachments as SUPERUSER, because could receive msg and attachments for doc uid cannot see - attachments = self.env['ir.attachment'].sudo().browse(attachment_ids).read(['id', 'datas_fname', 'name', 'file_type_icon']) - attachments_tree = dict((attachment['id'], { - 'id': attachment['id'], - 'filename': attachment['datas_fname'], - 'name': attachment['name'], - 'file_type_icon': attachment['file_type_icon'], - }) for attachment in attachments) - - # 3. Update message dictionaries - for message_dict in messages: - message_id = message_dict.get('id') - message = message_tree[message_id] - if message.author_id: - author = partner_tree[message.author_id.id] - else: - author = (0, message.email_from) - partner_ids = [] - if message.subtype_id: - partner_ids = [partner_tree[partner.id] for partner in message.notified_partner_ids - if partner.id in partner_tree] - else: - partner_ids = [partner_tree[partner.id] for partner in message.partner_ids - if partner.id in partner_tree] - attachment_ids = [] - for attachment in message.attachment_ids: - if attachment.id in attachments_tree: - attachment_ids.append(attachments_tree[attachment.id]) - message_dict.update({ - 'is_author': pid == author[0], - 'author_id': author, - 'partner_ids': partner_ids, - 'attachment_ids': attachment_ids, - 'user_pid': pid - }) - return True - - @api.multi - def _message_read_dict(self, parent_id=False): - """ Return a dict representation of the message. This representation is - used in the JS client code, to display the messages. Partners and - attachments related stuff will be done in post-processing in batch. - - :param dict message: mail.message browse record - """ - self.ensure_one() - # private message: no model, no res_id - is_private = False - if not self.model or not self.res_id: - is_private = True - # votes and favorites: res.users ids, no prefetching should be done - vote_nb = len(self.vote_user_ids) - has_voted = self._uid in [user.id for user in self.vote_user_ids] - - try: - if parent_id: - max_length = 300 - else: - max_length = 100 - body_short = tools.html_email_clean(self.body, remove=False, shorten=True, max_length=max_length) - - except Exception: - body_short = '<p><b>Encoding Error : </b><br/>Unable to convert this message (id: %s).</p>' % self.id - _logger.exception(Exception) - - return {'id': self.id, - 'type': self.message_type, - 'subtype': self.subtype_id.name if self.subtype_id else False, - 'body': self.body, - 'body_short': body_short, - 'model': self.model, - 'res_id': self.res_id, - 'record_name': self.record_name, - 'subject': self.subject, - 'date': self.date, - 'to_read': self.to_read, - 'parent_id': parent_id, - 'is_private': is_private, - 'author_id': False, - 'author_avatar': self.author_avatar, - 'is_author': False, - 'partner_ids': [], - 'vote_nb': vote_nb, - 'has_voted': has_voted, - 'is_favorite': self.starred, - 'attachment_ids': [], - } - - @api.model - def _message_read_add_expandables(self, messages, message_tree, parent_tree, - message_unload_ids=[], thread_level=0, domain=[], parent_id=False): - """ Create expandables for message_read, to load new messages. - 1. get the expandable for new threads - if display is flat (thread_level == 0): - fetch message_ids < min(already displayed ids), because we - want a flat display, ordered by id - else: - fetch message_ids that are not childs of already displayed - messages - 2. get the expandables for new messages inside threads if display - is not flat - for each thread header, search for its childs - for each hole in the child list based on message displayed, - create an expandable - - :param list messages: list of message structure for the Chatter - widget to which expandables are added - :param dict message_tree: dict [id]: browse record of this message - :param dict parent_tree: dict [parent_id]: [child_ids] - :param list message_unload_ids: list of message_ids we do not want - to load - :return bool: True - """ - def _get_expandable(domain, message_nb, parent_id, max_limit): - return { - 'domain': domain, - 'nb_messages': message_nb, - 'type': 'expandable', - 'parent_id': parent_id, - 'max_limit': max_limit, - } - - if not messages: - return True - message_ids = sorted(message_tree.keys()) - - # 1. get the expandable for new threads - if thread_level == 0: - exp_domain = domain + [('id', '<', min(message_unload_ids + message_ids))] - else: - exp_domain = domain + ['!', ('id', 'child_of', message_unload_ids + parent_tree.keys())] - more_count = self.search_count(exp_domain) - if more_count: - # inside a thread: prepend - if parent_id: - messages.insert(0, _get_expandable(exp_domain, -1, parent_id, True)) - # new threads: append - else: - messages.append(_get_expandable(exp_domain, -1, parent_id, True)) - - # 2. get the expandables for new messages inside threads if display is not flat - if thread_level == 0: - return True - for message_id in message_ids: - message = message_tree[message_id] - - # generate only for thread header messages (TDE note: parent_id may be False is uid cannot see parent_id, seems ok) - if message.parent_id: - continue - - # check there are message for expandable - child_ids = set([child.id for child in message.child_ids]) - set(message_unload_ids) - child_ids = sorted(list(child_ids), reverse=True) - if not child_ids: - continue - - # make groups of unread messages - id_min, id_max, nb = max(child_ids), 0, 0 - for child_id in child_ids: - if not child_id in message_ids: - nb += 1 - if id_min > child_id: - id_min = child_id - if id_max < child_id: - id_max = child_id - elif nb > 0: - exp_domain = [('id', '>=', id_min), ('id', '<=', id_max), ('id', 'child_of', message_id)] - idx = [msg.get('id') for msg in messages].index(child_id) + 1 - # messages.append(_get_expandable(exp_domain, nb, message_id, False)) - messages.insert(idx, _get_expandable(exp_domain, nb, message_id, False)) - id_min, id_max, nb = max(child_ids), 0, 0 - else: - id_min, id_max, nb = max(child_ids), 0, 0 - if nb > 0: - exp_domain = [('id', '>=', id_min), ('id', '<=', id_max), ('id', 'child_of', message_id)] - idx = [msg.get('id') for msg in messages].index(message_id) + 1 - # messages.append(_get_expandable(exp_domain, nb, message_id, id_min)) - messages.insert(idx, _get_expandable(exp_domain, nb, message_id, False)) - - return True - - @api.cr_uid_context - def message_read_wrapper(self, cr, uid, ids=None, domain=None, message_unload_ids=None, - thread_level=0, context=None, parent_id=False, limit=None): - return self.message_read(cr, uid, ids, domain=domain, message_unload_ids=message_unload_ids, thread_level=thread_level, parent_id=parent_id, limit=limit) - - @api.multi - def message_read(self, domain=None, message_unload_ids=None, thread_level=0, parent_id=False, limit=None): - """ Read messages from mail.message, and get back a list of structured - messages to be displayed as discussion threads. If IDs is set, - fetch these records. Otherwise use the domain to fetch messages. - After having fetch messages, their ancestors will be added to obtain - well formed threads, if uid has access to them. - - After reading the messages, expandable messages are added in the - message list (see ``_message_read_add_expandables``). It consists - in messages holding the 'read more' data: number of messages to - read, domain to apply. - - :param list ids: optional IDs to fetch - :param list domain: optional domain for searching ids if ids not set - :param list message_unload_ids: optional ids we do not want to fetch, - because i.e. they are already displayed somewhere - :param int parent_id: context of parent_id - - if parent_id reached when adding ancestors, stop going further - in the ancestor search - - if set in flat mode, ancestor_id is set to parent_id - :param int limit: number of messages to fetch, before adding the - ancestors and expandables - :return list: list of message structure for the Chatter widget - """ - assert thread_level in [0, 1], 'message_read() thread_level should be 0 (flat) or 1 (1 level of thread); given %s.' % thread_level - domain = domain if domain is not None else [] - message_unload_ids = message_unload_ids if message_unload_ids is not None else [] - if message_unload_ids: - domain += [('id', 'not in', message_unload_ids)] - limit = limit or self._message_read_limit - message_tree = {} - message_list = [] - parent_tree = {} - - # no specific IDS given: fetch messages according to the domain, add their parents if uid has access to - if not self.ids and domain: - self = self.search(domain, limit=limit) - - # fetch parent if threaded, sort messages - for message in self: - message_id = message.id - if message_id in message_tree: - continue - message_tree[message_id] = message - - # find parent_id - if thread_level == 0: - tree_parent_id = parent_id - else: - tree_parent_id = message_id - parent = message - while parent.parent_id and parent.parent_id.id != parent_id: - parent = parent.parent_id - tree_parent_id = parent.id - if parent.id not in message_tree: - message_tree[parent.id] = parent - # newest messages first - parent_tree.setdefault(tree_parent_id, []) - if tree_parent_id != message_id: - parent_tree[tree_parent_id].append(message_tree[message_id]._message_read_dict(parent_id=tree_parent_id)) - - if thread_level: - for key, message_id_list in parent_tree.iteritems(): - message_id_list.sort(key=lambda item: item['id']) - message_id_list.insert(0, message_tree[key]._message_read_dict()) - - # create final ordered message_list based on parent_tree - parent_list = parent_tree.items() - parent_list = sorted(parent_list, key=lambda item: max([msg.get('id') for msg in item[1]]) if item[1] else item[0], reverse=True) - message_list = [message for (key, msg_list) in parent_list for message in msg_list] - - # get the child expandable messages for the tree - self._message_read_dict_postprocess(message_list, message_tree) - self._message_read_add_expandables(message_list, message_tree, parent_tree, - thread_level=thread_level, message_unload_ids=message_unload_ids, domain=domain, parent_id=parent_id) - return message_list - - @api.multi - def get_like_names(self, limit=10): - """ Return the people list who liked this message. """ - self.ensure_one() - voter_names = [voter.name for voter in self.vote_user_ids[:limit]] - if len(self.vote_user_ids) > limit: - voter_names.append(_("and %s others like this") % (len(self.vote_user_ids) - limit)) - return voter_names - # compat - get_likers_list = get_like_names - - #------------------------------------------------------ - # mail_message internals - #------------------------------------------------------ - - def init(self, cr): - cr.execute("""SELECT indexname FROM pg_indexes WHERE indexname = 'mail_message_model_res_id_idx'""") - if not cr.fetchone(): - cr.execute("""CREATE INDEX mail_message_model_res_id_idx ON mail_message (model, res_id)""") - - @api.model - def _find_allowed_model_wise(self, doc_model, doc_dict): - doc_ids = doc_dict.keys() - allowed_doc_ids = self.env[doc_model].with_context(active_test=False).search([('id', 'in', doc_ids)]).ids - return set([message_id for allowed_doc_id in allowed_doc_ids for message_id in doc_dict[allowed_doc_id]]) - - @api.model - def _find_allowed_doc_ids(self, model_ids): - IrModelAccess = self.env['ir.model.access'] - allowed_ids = set() - for doc_model, doc_dict in model_ids.iteritems(): - if not IrModelAccess.check(doc_model, 'read', False): - continue - allowed_ids |= self._find_allowed_model_wise(doc_model, doc_dict) - return allowed_ids - - @api.model - def _search(self, args, offset=0, limit=None, order=None, count=False, access_rights_uid=None): - """ Override that adds specific access rights of mail.message, to remove ids uid could not see according to our custom rules. Please refer to check_access_rule for more details about those rules. - - Non employees users see only message with subtype (aka do not see - internal logs). - - After having received ids of a classic search, keep only: - - if author_id == pid, uid is the author, OR - - a notification (id, pid) exists, uid has been notified, OR - - uid have read access to the related document is model, res_id - - otherwise: remove the id - """ - # Rules do not apply to administrator - if self._uid == SUPERUSER_ID: - return super(Message, self)._search( - args, offset=offset, limit=limit, order=order, - count=count, access_rights_uid=access_rights_uid) - # Non-employee see only messages with a subtype (aka, no internal logs) - if not self.env['res.users'].has_group('base.group_user'): - args = ['&', ('subtype_id', '!=', False)] + list(args) - # Perform a super with count as False, to have the ids, not a counter - ids = super(Message, self)._search( - args, offset=offset, limit=limit, order=order, - count=False, access_rights_uid=access_rights_uid) - if not ids and count: - return 0 - elif not ids: - return ids - - pid = self.env.user.partner_id.id - author_ids, partner_ids, allowed_ids = set([]), set([]), set([]) - model_ids = {} - - # check read access rights before checking the actual rules on the given ids - 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, n.partner_id - FROM "%s" m LEFT JOIN "mail_notification" n - ON n.message_id=m.id AND n.partner_id = (%%s) - WHERE m.id = ANY (%%s)""" % self._table, (pid, ids,)) - for id, rmod, rid, author_id, partner_id in self._cr.fetchall(): - if author_id == pid: - author_ids.add(id) - elif partner_id == pid: - partner_ids.add(id) - elif rmod and rid: - model_ids.setdefault(rmod, {}).setdefault(rid, set()).add(id) - - allowed_ids = self._find_allowed_doc_ids(model_ids) - final_ids = author_ids | partner_ids | allowed_ids - - if count: - return len(final_ids) - else: - # re-construct a list based on ids, because set did not keep the original order - id_list = [id for id in ids if id in final_ids] - return id_list - - @api.multi - def check_access_rule(self, operation): - """ Access rules of mail.message: - - read: if - - author_id == pid, uid is the author, OR - - mail_notification (id, pid) exists, uid has been notified, OR - - uid have read access to the related document if model, res_id - - otherwise: raise - - create: if - - no model, no res_id, I create a private message OR - - pid in message_follower_ids if model, res_id OR - - mail_notification (parent_id.id, pid) exists, uid has been notified of the parent, OR - - uid have write or create access on the related document if model, res_id, OR - - otherwise: raise - - write: if - - author_id == pid, uid is the author, OR - - uid has write or create access on the related document if model, res_id - - otherwise: raise - - unlink: if - - uid has write or create access on the related document if model, res_id - - otherwise: raise - - Specific case: non employee users see only messages with subtype (aka do - not see internal logs). - """ - def _generate_model_record_ids(msg_val, msg_ids): - """ :param model_record_ids: {'model': {'res_id': (msg_id, msg_id)}, ... } - :param message_values: {'msg_id': {'model': .., 'res_id': .., 'author_id': ..}} - """ - model_record_ids = {} - for id in msg_ids: - vals = msg_val.get(id, {}) - if vals.get('model') and vals.get('res_id'): - model_record_ids.setdefault(vals['model'], set()).add(vals['res_id']) - return model_record_ids - - if self._uid == SUPERUSER_ID: - return - # Non employees see only messages with a subtype (aka, not internal logs) - if not self.env['res.users'].has_group('base.group_user'): - self._cr.execute('SELECT DISTINCT id FROM "%s" WHERE message_type = %%s AND subtype_id IS NULL AND id = ANY (%%s)' % (self._table), ('comment', self.ids,)) - if self._cr.fetchall(): - raise AccessError( - _('The requested operation cannot be completed due to security restrictions. Please contact your system administrator.\n\n(Document type: %s, Operation: %s)') % - (self._description, operation)) - - Notification = self.env['mail.notification'] - Followers = self.env['mail.followers'] - partner_id = self.env.user.partner_id.id - - # Read mail_message.ids to have their values - message_values = dict((res_id, {}) for res_id in self.ids) - self._cr.execute('SELECT DISTINCT id, model, res_id, author_id, parent_id FROM "%s" WHERE id = ANY (%%s)' % self._table, (self.ids,)) - for mid, rmod, rid, author_id, parent_id in self._cr.fetchall(): - message_values[mid] = {'model': rmod, 'res_id': rid, 'author_id': author_id, 'parent_id': parent_id} - - # Author condition (READ, WRITE, CREATE (private)) -> could become an ir.rule ? - author_ids = [] - if operation == 'read' or operation == 'write': - author_ids = [mid for mid, message in message_values.iteritems() - if message.get('author_id') and message.get('author_id') == partner_id] - elif operation == 'create': - author_ids = [mid for mid, message in message_values.iteritems() - if not message.get('model') and not message.get('res_id')] - - # Parent condition, for create (check for received notifications for the created message parent) - notified_ids = [] - if operation == 'create': - parent_ids = [message.get('parent_id') for mid, message in message_values.iteritems() - if message.get('parent_id')] - notifications = Notification.sudo().search([('message_id.id', 'in', parent_ids), ('partner_id', '=', partner_id)]) - not_parent_ids = [notif.message_id.id for notif in notifications] - notified_ids += [mid for mid, message in message_values.iteritems() - if message.get('parent_id') in not_parent_ids] - - # Notification condition, for read (check for received notifications and create (in message_follower_ids)) -> could become an ir.rule, but not till we do not have a many2one variable field - other_ids = set(self.ids).difference(set(author_ids), set(notified_ids)) - model_record_ids = _generate_model_record_ids(message_values, other_ids) - if operation == 'read': - notifications = Notification.sudo().search([ - ('partner_id', '=', partner_id), - ('message_id', 'in', self.ids)]) - notified_ids = [notification.message_id.id for notification in notifications] - elif operation == 'create': - for doc_model, doc_ids in model_record_ids.items(): - followers = Followers.sudo().search([ - ('res_model', '=', doc_model), - ('res_id', 'in', list(doc_ids)), - ('partner_id', '=', partner_id)]) - fol_mids = [follower.res_id for follower in followers] - notified_ids += [mid for mid, message in message_values.iteritems() - if message.get('model') == doc_model and message.get('res_id') in fol_mids] - - # 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) - document_related_ids += [mid for mid, message in message_values.iteritems() - if message.get('model') == model and message.get('res_id') in mids.ids] - - # Calculate remaining ids: if not void, raise an error - other_ids = other_ids.difference(set(document_related_ids)) - if not other_ids: - 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)') % - (self._description, operation)) - - @api.model - def _get_record_name(self, values): - """ Return the related document name, using name_get. It is done using - SUPERUSER_ID, to be sure to have the record name correctly stored. """ - model = values.get('model', self.env.context.get('default_model')) - res_id = values.get('res_id', self.env.context.get('default_res_id')) - if not model or not res_id or model not in self.pool: - return False - return self.env[model].sudo().browse(res_id).name_get()[0][1] - - @api.model - def _get_reply_to(self, values): - """ Return a specific reply_to: alias of the document through - message_get_reply_to or take the email_from """ - 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 ? - if model: - # return self.env[model].browse(res_id).message_get_reply_to([res_id], default=email_from)[res_id] - return self.env[model].message_get_reply_to([res_id], default=email_from)[res_id] - else: - # return self.env['mail.thread'].message_get_reply_to(default=email_from)[None] - return self.env['mail.thread'].message_get_reply_to([None], default=email_from)[None] - - @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'): - 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.model - def create(self, values): - default_starred = self.env.context.get('default_starred') - - if 'email_from' not in values: # needed to compute reply_to - values['email_from'] = self._get_default_from() - if not values.get('message_id'): - values['message_id'] = self._get_message_id(values) - if 'reply_to' not in values: - values['reply_to'] = self._get_reply_to(values) - if 'record_name' not in values and 'default_record_name' not in self.env.context: - values['record_name'] = self._get_record_name(values) - - message = super(Message, self).create(values) - - message._notify(force_send=self.env.context.get('mail_notify_force_send', True), - user_signature=self.env.context.get('mail_notify_user_signature', True)) - # TDE FIXME: handle default_starred. Why not setting an inv on starred ? - # Because starred will call set_message_starred, that looks for notifications. - # When creating a new mail_message, it will create a notification to a message - # that does not exist, leading to an error (key not existing). Also this - # this means unread notifications will be created, yet we can not assure - # this is what we want. - if default_starred: - message.set_message_starred(True) - return message - - @api.multi - def read(self, fields=None, load='_classic_read'): - """ Override to explicitely call check_access_rule, that is not called - by the ORM. It instead directly fetches ir.rules and apply them. """ - self.check_access_rule('read') - return super(Message, self).read(fields=fields, load=load) - - @api.multi - def unlink(self): - # cascade-delete attachments that are directly attached to the message (should only happen - # for mail.messages that act as parent for a standalone mail.mail record). - self.check_access_rule('unlink') - 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() - return super(Message, self).unlink() - - #------------------------------------------------------ - # Messaging API - #------------------------------------------------------ - - @api.multi - def _notify(self, force_send=False, user_signature=True): - """ Add the related record followers to the destination partner_ids if is not a private message. - Call mail_notification.notify to manage the email sending - """ - self.ensure_one() # tde: not sure, just for testinh, will see - partners_to_notify = self.env['res.partner'] - - # all followers of the mail.message document have to be added as partners and notified if a subtype is defined (otherwise: log message) - if self.subtype_id and self.model and self.res_id: - followers = self.env['mail.followers'].sudo().search([('res_model', '=', self.model), ('res_id', '=', self.res_id)]) - partners_to_notify |= followers.filtered(lambda fol: self.subtype_id in fol.subtype_ids).mapped('partner_id') - - # remove me from notified partners, unless the message is written on my own wall - if self.subtype_id and self.author_id and self.model == "res.partner" and self.res_id == self.author_id.id: - partners_to_notify |= self.author_id - elif self.author_id: - partners_to_notify -= self.author_id - - # all partner_ids of the mail.message have to be notified regardless of the above (even the author if explicitly added!) - partners_to_notify |= self.partner_ids - - # notify - self.env['mail.notification']._notify(self, recipients=partners_to_notify, force_send=force_send, user_signature=user_signature) - - # An error appear when a user receive a notification without notifying - # the parent message -> add a read notification for the parent - if self.parent_id: - # all notified_partner_ids of the mail.message have to be notified for the parented messages - partners_to_parent_notify = self.notified_partner_ids - self.parent_id.notified_partner_ids - self.parent_id.invalidate_cache() # avoid access rights issues, as notifications are used for access - Notification = self.env['mail.notification'].sudo() - for partner in partners_to_parent_notify: - Notification.create({ - 'message_id': self.parent_id.id, - 'partner_id': partner.id, - 'is_read': True}) |
