diff --git a/addons/mail/__init__.py b/addons/mail/__init__.py index abf714ad534..aea7ee62cbc 100644 --- a/addons/mail/__init__.py +++ b/addons/mail/__init__.py @@ -27,6 +27,7 @@ import mail_mail import mail_thread import mail_group import mail_vote +import mail_favorite import res_partner import res_users import report @@ -36,4 +37,3 @@ import mail_group_menu import update # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: - diff --git a/addons/mail/__openerp__.py b/addons/mail/__openerp__.py index 39a3f1bee13..27565bc4102 100644 --- a/addons/mail/__openerp__.py +++ b/addons/mail/__openerp__.py @@ -28,7 +28,7 @@ 'description': """ Business oriented Social Networking =================================== -The Social Networking module provides a unified social network abstraction layer allowing applications to display a complete +The Social Networking module provides a unified social network abstraction layer allowing applications to display a complete communication history on documents with a fully-integrated email and message management system. It enables the users to read and send messages as well as emails. It also provides a feeds page combined to a subscription mechanism that allows to follow documents and to be constantly updated about recent news. @@ -54,6 +54,7 @@ Main Features 'mail_message_view.xml', 'mail_mail_view.xml', 'mail_followers_view.xml', + 'mail_favorite_view.xml', 'mail_thread_view.xml', 'mail_group_view.xml', 'res_partner_view.xml', diff --git a/addons/mail/mail_favorite.py b/addons/mail/mail_favorite.py new file mode 100644 index 00000000000..4fe5fbdb557 --- /dev/null +++ b/addons/mail/mail_favorite.py @@ -0,0 +1,39 @@ +# -*- coding: utf-8 -*- +############################################################################## +# +# OpenERP, Open Source Management Solution +# Copyright (C) 2012-Today OpenERP SA (). +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see +# +############################################################################## + +from osv import osv, fields + + +class mail_favorite(osv.Model): + ''' Favorite model: relationship table between messages and users. A favorite + message is a message the user wants to see in a specific 'Favorite' + mailbox, like a starred mechanism. ''' + + _name = 'mail.favorite' + _description = 'Favorite messages' + _columns = { + 'message_id': fields.many2one('mail.message', 'Message', select=1, + ondelete='cascade', required=True), + 'user_id': fields.many2one('res.users', 'User', select=1, + ondelete='cascade', required=True), + } + +# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: diff --git a/addons/mail/mail_favorite_view.xml b/addons/mail/mail_favorite_view.xml new file mode 100644 index 00000000000..3aa73d5463e --- /dev/null +++ b/addons/mail/mail_favorite_view.xml @@ -0,0 +1,44 @@ + + + + + + + mail.favorite.tree + mail.favorite + + + + + + + + + + mail.favorite.form + mail.favorite + +
+ + + + + + +
+
+
+ + + Favorites + mail.favorite + form + tree,form + + + + + +
+
diff --git a/addons/mail/mail_message.py b/addons/mail/mail_message.py index b8c1edc8032..8719d59bf04 100644 --- a/addons/mail/mail_message.py +++ b/addons/mail/mail_message.py @@ -20,12 +20,10 @@ ############################################################################## import logging -import openerp import tools from email.header import decode_header from openerp import SUPERUSER_ID -from operator import itemgetter from osv import osv, orm, fields from tools.translate import _ @@ -48,7 +46,10 @@ class mail_message(osv.Model): _order = 'id desc' _message_read_limit = 10 + _message_read_fields = ['id', 'parent_id', 'model', 'res_id', 'body', 'subject', 'date', 'to_read', + 'type', 'vote_user_ids', 'attachment_ids', 'author_id', 'partner_ids', 'record_name', 'favorite_user_ids'] _message_record_name_length = 18 + _message_read_more_limit = 1024 def _shorten_name(self, name): if len(name) <= (self._message_record_name_length + 3): @@ -56,7 +57,9 @@ class mail_message(osv.Model): return name[:self._message_record_name_length] + '...' def _get_record_name(self, cr, uid, ids, name, arg, context=None): - """ Return the related document name, using get_name. """ + """ Return the related document name, using name_get. It is included in + a try/except statement, because if uid cannot read the related + document, he should see a void string instead of crashing. """ result = dict.fromkeys(ids, False) for message in self.read(cr, uid, ids, ['model', 'res_id'], context=context): if not message['model'] or not message['res_id']: @@ -69,7 +72,7 @@ class mail_message(osv.Model): def _get_to_read(self, cr, uid, ids, name, arg, context=None): """ Compute if the message is unread by the current user. """ - res = dict((id, {'to_read': False}) for id in ids) + res = dict((id, False) for id in ids) partner_id = self.pool.get('res.users').read(cr, uid, uid, ['partner_id'], context=context)['partner_id'][0] notif_obj = self.pool.get('mail.notification') notif_ids = notif_obj.search(cr, uid, [ @@ -78,16 +81,16 @@ class mail_message(osv.Model): ('read', '=', False) ], context=context) for notif in notif_obj.browse(cr, uid, notif_ids, context=context): - res[notif.message_id.id]['to_read'] = True + res[notif.message_id.id] = not notif.read return res def _search_to_read(self, cr, uid, obj, name, domain, context=None): """ Search for messages to read by the current user. Condition is inversed because we search unread message on a read column. """ if domain[0][2]: - read_cond = '(read = false or read is null)' + read_cond = "(read = False OR read IS NULL)" else: - read_cond = 'read = true' + read_cond = "read = True" partner_id = self.pool.get('res.users').read(cr, uid, uid, ['partner_id'], context=context)['partner_id'][0] cr.execute("SELECT message_id FROM mail_notification "\ "WHERE partner_id = %%s AND %s" % read_cond, @@ -132,8 +135,12 @@ class mail_message(osv.Model): type='boolean', string='To read', help='Functional field to search for messages the current user has to read'), 'subtype_id': fields.many2one('mail.message.subtype', 'Subtype'), - 'vote_user_ids': fields.many2many('res.users', 'mail_vote', 'message_id', 'user_id', string='Votes', + 'vote_user_ids': fields.many2many('res.users', 'mail_vote', + 'message_id', 'user_id', string='Votes', help='Users that voted for this message'), + 'favorite_user_ids': fields.many2many('res.users', 'mail_favorite', + 'message_id', 'user_id', string='Favorite', + help='Users that set this message in their favorites'), } def _needaction_domain_get(self, cr, uid, context=None): @@ -142,8 +149,7 @@ class mail_message(osv.Model): return [] def _get_default_author(self, cr, uid, context=None): - # remove context to avoid possible hack in browse with superadmin using context keys that could trigger a specific behavior - return self.pool.get('res.users').browse(cr, SUPERUSER_ID, uid, context=None).partner_id.id + return self.pool.get('res.users').read(cr, uid, uid, ['partner_id'], context=context)['partner_id'][0] _defaults = { 'type': 'email', @@ -156,33 +162,49 @@ class mail_message(osv.Model): # Vote/Like #------------------------------------------------------ - def vote_toggle(self, cr, uid, ids, user_ids=None, context=None): - ''' Toggles voting. Done as SUPERUSER_ID because of write access on - mail.message not always granted. ''' - if not user_ids: - user_ids = [uid] + def vote_toggle(self, cr, uid, ids, context=None): + ''' Toggles vote. Performed using read to avoid access rights issues. + Done as SUPERUSER_ID because uid may vote for a message he cannot modify. ''' for message in self.read(cr, uid, ids, ['vote_user_ids'], context=context): - for user_id in user_ids: - has_voted = user_id in message.get('vote_user_ids') - if not has_voted: - self.write(cr, SUPERUSER_ID, message.get('id'), {'vote_user_ids': [(4, user_id)]}, context=context) - else: - self.write(cr, SUPERUSER_ID, message.get('id'), {'vote_user_ids': [(3, user_id)]}, context=context) - return not(has_voted) or False + new_has_voted = not (uid in message.get('vote_user_ids')) + if new_has_voted: + self.write(cr, SUPERUSER_ID, message.get('id'), {'vote_user_ids': [(4, uid)]}, context=context) + else: + self.write(cr, SUPERUSER_ID, message.get('id'), {'vote_user_ids': [(3, uid)]}, context=context) + return new_has_voted or False + + #------------------------------------------------------ + # Favorite + #------------------------------------------------------ + + def favorite_toggle(self, cr, uid, ids, context=None): + ''' Toggles favorite. Performed using read to avoid access rights issues. + Done as SUPERUSER_ID because uid may star a message he cannot modify. ''' + for message in self.read(cr, uid, ids, ['favorite_user_ids'], context=context): + new_is_favorite = not (uid in message.get('favorite_user_ids')) + if new_is_favorite: + self.write(cr, SUPERUSER_ID, message.get('id'), {'favorite_user_ids': [(4, uid)]}, context=context) + else: + self.write(cr, SUPERUSER_ID, message.get('id'), {'favorite_user_ids': [(3, uid)]}, context=context) + return new_is_favorite or False #------------------------------------------------------ # Message loading for web interface #------------------------------------------------------ def _message_get_dict(self, cr, uid, message, context=None): - """ Return a dict representation of the message. + """ Return a dict representation of the message. This representation is + used in the JS client code, to display the messages. :param dict message: read result of a mail.message """ + has_voted = False if uid in message['vote_user_ids']: has_voted = True - else: - has_voted = False + + is_favorite = False + if uid in message['favorite_user_ids']: + is_favorite = True try: attachment_ids = [{'id': attach[0], 'name': attach[1]} for attach in self.pool.get('ir.attachment').name_get(cr, uid, message['attachment_ids'], context=context)] @@ -206,35 +228,48 @@ class mail_message(osv.Model): 'date': message['date'], 'author_id': message['author_id'], 'is_author': message['author_id'] and message['author_id'][0] == uid, + # TDE note: is this useful ? to check 'partner_ids': partner_ids, 'parent_id': message['parent_id'] and message['parent_id'][0] or False, # TDE note: see with CHM about votes, how they are displayed (only number, or name_get ?) - # 'vote_user_ids': vote_ids, + # vote: should only use number of votes + 'vote_nb': len(message['vote_user_ids']), 'has_voted': has_voted, + 'is_private': message['model'] and message['res_id'], + 'is_favorite': is_favorite, 'to_read': message['to_read'], } - def _message_read_expandable(self, cr, uid, tree, result, message_loaded, domain, context, parent_id, limit): + def _message_read_expandable(self, cr, uid, message_list, read_messages, + message_loaded_ids=[], domain=[], context=None, parent_id=False, limit=None): """ Create the expandable message for all parent message read this function is used by message_read - TDE note: add default values for args, add some comments - :param dict tree: tree of message ids + :param list message_list: list of messages given by message_read to + which we have to add expandables + :param dict read_messages: dict [id]: read result of the messages to + easily have access to their values, given their ID """ + # sort for group items / TDE: move to message_read + # result = sorted(result, key=lambda k: k['id']) tree_not = [] # expandable for not show message - for msg_id in tree: - # get all childs - not_loaded_ids = self.search(cr, SUPERUSER_ID, [ - ('parent_id', '=', msg_id), - ('id', 'not in', message_loaded) - ], context=context, limit=1000) + id_list = sorted(read_messages.keys()) + for message_id in id_list: + message = read_messages[message_id] + + # TDE note: check search is correctly implemented in mail.message + not_loaded_ids = self.search(cr, uid, [ + ('parent_id', '=', message['id']), + ('id', 'not in', message_loaded_ids), + ], context=context, limit=self._message_read_more_limit) # group childs not read id_min = None id_max = None nb = 0 + for not_loaded_id in not_loaded_ids: - if not_loaded_id not in tree: + if not read_messages.get(not_loaded_id): nb += 1 if id_min == None or id_min > not_loaded_id: id_min = not_loaded_id @@ -243,42 +278,43 @@ class mail_message(osv.Model): tree_not.append(not_loaded_id) else: if nb > 0: - result.append({ - 'domain': [('id', '>=', id_min), ('id', '<=', id_max), ('parent_id', '=', msg_id)], + message_list.append({ + 'domain': [('id', '>=', id_min), ('id', '<=', id_max), ('parent_id', '=', message_id)], 'nb_messages': nb, 'type': 'expandable', - 'parent_id': msg_id, + 'parent_id': message_id, 'id': id_min, + 'model': message['model'] }) id_min = None id_max = None nb = 0 if nb > 0: - result.append({ - 'domain': [('id', '>=', id_min), ('id', '<=', id_max), ('parent_id', '=', msg_id)], + message_list.append({ + 'domain': [('id', '>=', id_min), ('id', '<=', id_max), ('parent_id', '=', message_id)], 'nb_messages': nb, 'type': 'expandable', - 'parent_id': msg_id, - 'id': id_min + 'parent_id': message_id, + 'id': id_min, + 'model': message['model'], }) + for msg_id in read_messages.keys() + tree_not: + message_loaded_ids.append(msg_id) + # expandable for limit max - ids = self.search(cr, SUPERUSER_ID, domain + [('id', 'not in', message_loaded + tree + tree_not)], context=context, limit=1) + ids = self.search(cr, uid, domain + [('id', 'not in', message_loaded_ids)], context=context, limit=1) if len(ids) > 0: - result.append({ + message_list.append({ 'domain': domain, 'nb_messages': 0, 'type': 'expandable', 'parent_id': parent_id, - 'id': -1 + 'id': -1, + 'max_limit': True, }) - result = sorted(result, key=lambda k: k['id']) - - return result - - _message_read_fields = ['id', 'parent_id', 'model', 'res_id', 'body', 'subject', 'date', 'to_read', - 'type', 'vote_user_ids', 'attachment_ids', 'author_id', 'partner_ids', 'record_name'] + return message_list def _get_parent(self, cr, uid, message, context=None): """ Tools method that tries to get the parent of a mail.message. If @@ -295,11 +331,11 @@ class mail_message(osv.Model): except (orm.except_orm, osv.except_osv): return False - def message_read(self, cr, uid, ids=False, domain=[], context=None, parent_id=False, limit=None): + def message_read(self, cr, uid, ids=False, domain=[], message_loaded_ids=[], context=None, parent_id=False, limit=None): """ Read messages from mail.message, and get back a structured tree of 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 parents will be added to obtain + After having fetch messages, their parents & child will be added to obtain well formed threads. TDE note: update this comment after final method implementation @@ -310,60 +346,51 @@ class mail_message(osv.Model): further parents :return list: list of trees of messages """ - # don't read the message display by .js, in context message_loaded list - # TDE note: use an argument, do not use context - if context is None: - context = {} - if context.get('message_loaded'): - domain += [('id', 'not in', context.get('message_loaded'))] + if message_loaded_ids: + domain += [('id', 'not in', message_loaded_ids)] limit = limit or self._message_read_limit - id_tree = [] + read_messages = {} message_list = [] - record = None - # select ids - # TDE note: should not receive [None] -> investigate - if ids and ids != [None]: + # specific IDs given: fetch those ids and return directly the message list + if ids: for message in self.read(cr, uid, ids, self._message_read_fields, context=context): message_list.append(self._message_get_dict(cr, uid, message, context=context)) + message_list = sorted(message_list, key=lambda k: k['id']) return message_list - # key: ID, value: record - ids = self.search(cr, SUPERUSER_ID, domain, context=context, limit=limit) - + # TDE FIXME: check access rights on search are implemented for mail.message + # fetch messages according to the domain, add their parents if uid has access to + ids = self.search(cr, uid, domain, context=context, limit=limit) for message in self.read(cr, uid, ids, self._message_read_fields, context=context): - # if not in record and not in message_loded list - if message['id'] not in id_tree and message['id'] not in context.get('message_loaded', []): - record = self._message_get_dict(cr, uid, message, context=context) - id_tree.append(message['id']) - message_list.append(record) + # if not in tree and not in message_loded list + if not read_messages.get(message.get('id')) and message.get('id') not in message_loaded_ids: + read_messages[message.get('id')] = message + message_list.append(self._message_get_dict(cr, uid, message, context=context)) - parent = self._get_parent(cr, uid, message, context=context) - while parent and parent['id'] != parent_id: - if parent['id'] not in id_tree: - message = parent - id_tree.append(message['id']) - # if not in record and not in message_loded list - if message['id'] not in context.get('message_loaded', []): - record = self._message_get_dict(cr, uid, message, context=context) - message_list.append(record) - parent = self._get_parent(cr, uid, parent, context=context) + # get all parented message if the user have the access + parent = self._get_parent(cr, uid, message, context=context) + while parent and parent.get('id') != parent_id: + if not read_messages.get(parent.get('id')) and parent.get('id') not in message_loaded_ids: + read_messages[parent.get('id')] = parent + message_list.append(self._message_get_dict(cr, uid, parent, context=context)) + parent = self._get_parent(cr, uid, parent, context=context) + # get the child expandable messages for the tree message_list = sorted(message_list, key=lambda k: k['id']) + message_list = self._message_read_expandable(cr, uid, message_list, read_messages, + message_loaded_ids=message_loaded_ids, domain=domain, context=context, parent_id=parent_id, limit=limit) - message_list = self._message_read_expandable(cr, uid, id_tree, message_list, context.get('message_loaded', []), domain, context, parent_id, limit) - + # message_list = sorted(message_list, key=lambda k: k['id']) return message_list # TDE Note: do we need this ? # def user_free_attachment(self, cr, uid, context=None): - # attachment_list = [] - # attachment = self.pool.get('ir.attachment') - # attachment_ids = attachment.search(cr, uid, [('res_model','=',''),('create_uid','=',uid)]) + # attachment_list = [] + # attachment_ids = attachment.search(cr, uid, [('res_model', '=', 'mail.message'), ('create_uid', '=', uid)]) # if len(attachment_ids): # attachment_list = [{'id': attach.id, 'name': attach.name, 'date': attach.create_date} for attach in attachment.browse(cr, uid, attachment_ids, context=context)] - # return attachment_list #------------------------------------------------------ @@ -384,7 +411,8 @@ class mail_message(osv.Model): - Otherwise: raise - create: if - I am in the document message_follower_ids OR - - I can write on the related document if res_model, res_id + - I can write on the related document if res_model, res_id OR + - I create a private message (no model, no res_id) - Otherwise: raise - write: if - I can write on the related document if res_model, res_id @@ -422,6 +450,10 @@ class mail_message(osv.Model): if operation == 'read': author_ids = [mid for mid, message in message_values.iteritems() if message.get('author_id') and message.get('author_id') == partner_id] + # Create: Check messages you create that are private messages -> ir.rule ? + elif operation == 'create': + author_ids = [mid for mid, message in message_values.iteritems() + if not message.get('model') and not message.get('res_id')] else: author_ids = [] @@ -480,13 +512,14 @@ class mail_message(osv.Model): def read(self, cr, uid, ids, fields=None, context=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. """ - res = super(mail_message, self).read(cr, uid, ids, fields=fields, context=context, load=load) self.check_access_rule(cr, uid, ids, 'read', context=context) + res = super(mail_message, self).read(cr, uid, ids, fields=fields, context=context, load=load) return res def unlink(self, cr, uid, ids, context=None): # 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(cr, uid, ids, 'unlink', context=context) attachments_to_delete = [] for message in self.browse(cr, uid, ids, context=context): for attach in message.attachment_ids: @@ -514,7 +547,6 @@ class mail_message(osv.Model): fol_objs = fol_obj.browse(cr, uid, fol_ids, context=context) extra_notified = set(fol.partner_id.id for fol in fol_objs) missing_notified = extra_notified - partners_to_notify - missing_notified = missing_notified if missing_notified: self.write(cr, SUPERUSER_ID, [newid], {'partner_ids': [(4, p_id) for p_id in missing_notified]}, context=context) @@ -523,7 +555,7 @@ class mail_message(osv.Model): Call mail_notification.notify to manage the email sending """ message = self.browse(cr, uid, newid, context=context) - if message and message.model and message.res_id: + if message.model and message.res_id: self._notify_followers(cr, uid, newid, message, context=context) # add myself if I wrote on my wall, otherwise remove myself author diff --git a/addons/mail/mail_message_view.xml b/addons/mail/mail_message_view.xml index 59ca3333755..4677beee511 100644 --- a/addons/mail/mail_message_view.xml +++ b/addons/mail/mail_message_view.xml @@ -56,8 +56,9 @@ + - - Inbox - mail.wall - - - - - Archives - mail.wall - - - - - Sent - mail.wall - - - + \ No newline at end of file diff --git a/addons/mail/mail_thread.py b/addons/mail/mail_thread.py index 7c92e9158e3..04da686b49d 100644 --- a/addons/mail/mail_thread.py +++ b/addons/mail/mail_thread.py @@ -703,12 +703,12 @@ class mail_thread(osv.AbstractModel): if attachments: ir_attachment = self.pool.get('ir.attachment') mail_message = self.pool.get('mail.message') - attachment_ids = ir_attachment.search(cr, SUPERUSER_ID, [('res_model', '=', False), ('res_id', '=', False), ('user_id', '=', uid), ('id', 'in', attachments)], context=context) + attachment_ids = ir_attachment.search(cr, SUPERUSER_ID, [('res_model', '=', 'mail.message'), ('res_id', '=', 0), ('create_uid', '=', uid), ('id', 'in', attachments)], context=context) if attachment_ids: ir_attachment.write(cr, SUPERUSER_ID, attachment_ids, {'res_model': self._name, '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) - new_message = self.pool.get('mail.message').message_read(cr, uid, [new_message_id], context=context) + new_message = self.pool.get('mail.message').message_read(cr, uid, [new_message_id], context=context) return new_message #------------------------------------------------------ @@ -716,7 +716,7 @@ class mail_thread(osv.AbstractModel): #------------------------------------------------------ def message_get_subscription_data(self, cr, uid, ids, context=None): - """ Wrapper to get subtypes. """ + """ Wrapper to get subtypes data. """ return self._get_subscription_data(cr, uid, ids, None, None, context=context) def message_subscribe_users(self, cr, uid, ids, user_ids=None, subtype_ids=None, context=None): diff --git a/addons/mail/mail_thread_view.xml b/addons/mail/mail_thread_view.xml index 696edde0e15..5ef5e14b846 100644 --- a/addons/mail/mail_thread_view.xml +++ b/addons/mail/mail_thread_view.xml @@ -1,35 +1,82 @@ + + Inbox + mail.wall + + + + + To: me + mail.wall + + + + + Favorites + mail.wall + + + + + Archives + mail.wall + + + + + Sent + mail.wall + + + + - - + Inbox - + + + + To: me + + + + + + Favorites + + + Archives - + - + Sent - + - + - - + \ No newline at end of file diff --git a/addons/mail/res_users.py b/addons/mail/res_users.py index c318538541f..54b8a76703f 100644 --- a/addons/mail/res_users.py +++ b/addons/mail/res_users.py @@ -126,6 +126,19 @@ class res_users(osv.Model): return self.pool.get('res.partner').message_post_api(cr, uid, partner_id, body=body, subject=subject, type=type, subtype=subtype, parent_id=parent_id, attachments=attachments, context=context, **kwargs) + def message_post(self, cr, uid, thread_id, context=None, **kwargs): + """ Redirect the posting of message on res.users to the related partner. + This is done because when giving the context of Chatter on the + various mailboxes, we do not have access to the current partner_id. + We therefore post on the user and redirect on its partner. """ + assert thread_id, "res.users does not support posting global messages" + if context and 'thread_model' in context: + context['thread_model'] = 'res.partner' + if isinstance(thread_id, (list, tuple)): + thread_id = thread_id[0] + partner_id = self.pool.get('res.users').read(cr, uid, thread_id, ['partner_id'], context=context)['partner_id'][0] + return self.pool.get('res.partner').message_post(cr, uid, partner_id, context=context, **kwargs) + def message_update(self, cr, uid, ids, msg_dict, update_vals=None, context=None): partner_id = self.pool.get('res.users').browse(cr, uid, ids)[0].partner_id.id return self.pool.get('res.partner').message_update(cr, uid, [partner_id], msg_dict, diff --git a/addons/mail/security/mail_security.xml b/addons/mail/security/mail_security.xml index 53083a51d5a..3f2b7bf3e9a 100644 --- a/addons/mail/security/mail_security.xml +++ b/addons/mail/security/mail_security.xml @@ -11,7 +11,7 @@ - mail.followers: read its own entries + mail.followers: read and write its own entries [('partner_id', '=', user.partner_id.id)] diff --git a/addons/mail/static/src/css/mail.css b/addons/mail/static/src/css/mail.css index 0f1f90d04fb..50ce0e000a9 100644 --- a/addons/mail/static/src/css/mail.css +++ b/addons/mail/static/src/css/mail.css @@ -54,13 +54,13 @@ /* Specific display of threads in the wall */ /* ------------------------------------------------------------ */ -.openerp ul.oe_mail_wall_threads .oe_mail_msg_content textarea.oe_mail_compose_textarea { +.openerp ul.oe_mail_wall_threads .oe_msg_content textarea.oe_mail_compose_textarea { width: 434px; height: 30px; padding: 4px; } -.openerp li.oe_mail_wall_thread:first .oe_mail_msg_notification { +.openerp li.oe_mail_wall_thread:first .oe_msg_notification { border-top: 0; } @@ -69,7 +69,7 @@ height: 28px; } -.openerp div.oe_thread_placeholder div.oe_mail_msg_content { +.openerp div.oe_thread_placeholder div.oe_msg_content { width: 440px; } @@ -181,8 +181,8 @@ } /* default textarea (oe_mail_compose_textarea), and body textarea for compose form view */ -.openerp .oe_mail_msg_content textarea.oe_mail_compose_textarea:focus, -.openerp .oe_mail_msg_content div.oe_mail_compose_message_body textarea:focus { +.openerp .oe_msg_content textarea.oe_mail_compose_textarea:focus, +.openerp .oe_msg_content div.oe_mail_compose_message_body textarea:focus { outline: 0; border-color: rgba(82, 168, 236, 0.8); -moz-box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.1), 0 0 8px rgba(82, 168, 236, 0.6); @@ -191,10 +191,17 @@ } .openerp .oe_mail_vote_count, -.openerp .oe_mail_msg_vote{ +.openerp .oe_msg_vote{ vertical-align: bottom; } +.openerp button.oe_mail_starbox{ + background: #ff0000; +} +.openerp button.oe_mail_starbox.oe_stared{ + background: #00FF00; +} + .openerp div.oe_mail_thread_display { white-space: normal; } @@ -247,35 +254,35 @@ margin: 0 0 4px 0; } -.openerp .oe_mail_msg_notification, -.openerp .oe_mail_msg_expandable, -.openerp .oe_mail_msg_comment, -.openerp .oe_mail_msg_email { +.openerp .oe_msg_notification, +.openerp .oe_msg_expandable, +.openerp .oe_msg_comment, +.openerp .oe_msg_email { padding: 8px; background: white; position: relative; } -.openerp .oe_mail_msg_notification:after, -.openerp .oe_mail_msg_comment:after, -.openerp .oe_mail_msg_email:after { +.openerp .oe_msg_notification:after, +.openerp .oe_msg_comment:after, +.openerp .oe_msg_email:after { content: ""; display: block; clear: both; } -.openerp div.oe_mail_msg_content { +.openerp div.oe_msg_content { float: left; position: relative; width: 486px; } -.openerp div.oe_mail_msg_content > li { +.openerp div.oe_msg_content > li { float: left; margin-right: 3px; } -.openerp .oe_mail_msg_content:after { +.openerp .oe_msg_content:after { content: ""; display: block; clear: both; @@ -319,23 +326,23 @@ /* Messages layout /* ------------------------------------------------------------ */ -.openerp .oe_mail_msg .oe_mail_msg_title { +.openerp .oe_mail_msg .oe_msg_title { margin: 0; font-size: 1.3em; font-weight: bold; } -.openerp .oe_mail_msg .oe_mail_msg_title a:link, -.openerp .oe_mail_msg .oe_mail_msg_title a:visited { +.openerp .oe_mail_msg .oe_msg_title a:link, +.openerp .oe_mail_msg .oe_msg_title a:visited { color: #4C4C4C; text-decoration: none; } -.openerp .oe_mail_msg .oe_mail_msg_body { +.openerp .oe_mail_msg .oe_msg_body { margin-bottom: .5em; text-align: justify; } -.openerp .oe_mail_msg .oe_mail_msg_body pre { +.openerp .oe_mail_msg .oe_msg_body pre { font-family: "Lucida Grande", Helvetica, Verdana, Arial, sans-serif; margin: 0px; white-space: pre-wrap; @@ -373,31 +380,31 @@ } /* Message footer */ -.openerp .oe_mail_msg .oe_mail_msg_footer { +.openerp .oe_mail_msg .oe_msg_footer { color: #888; } -.openerp .oe_mail_msg .oe_mail_msg_footer li { +.openerp .oe_mail_msg .oe_msg_footer li { float: left; margin-right: 3px; } -.openerp .oe_mail_msg .oe_mail_msg_footer li:after { +.openerp .oe_mail_msg .oe_msg_footer li:after { content: " · "; } -.openerp .oe_mail_msg .oe_mail_msg_footer li:last-child:after { +.openerp .oe_mail_msg .oe_msg_footer li:last-child:after { content: ""; } /* Attachments list */ -.openerp .oe_mail_msg_content ul.oe_mail_msg_attachments { +.openerp .oe_msg_content ul.oe_msg_attachments { width: 100%; margin: .5em 0 0 0; padding: .5em 0; list-style-position: inside; } -.openerp .oe_mail_msg_content ul.oe_mail_msg_attachments.oe_hidden { +.openerp .oe_msg_content ul.oe_msg_attachments.oe_hidden { display: none; } -.openerp .oe_mail_msg_content ul.oe_mail_msg_attachments li { +.openerp .oe_msg_content ul.oe_msg_attachments li { float: none; height: 20px; line-height: 20px; @@ -405,20 +412,28 @@ padding: 0; list-style-type: square; } -.openerp .oe_mail_msg_content ul.oe_mail_msg_attachments .oe_upload_in_process { +.openerp .oe_msg_content ul.oe_msg_attachments .oe_upload_in_process { float: right; width: 200px; height: 16px; } -.openerp .oe_mail_msg_content ul.oe_mail_msg_attachments .oe_upload_in_process div { +.openerp .oe_msg_content ul.oe_msg_attachments .oe_upload_in_process div { float: left; width: 38px; height: 16px; margin-right: 2px; background: #66FF66; } -.openerp .oe_mail_msg_content ul.oe_mail_msg_attachments .oe_upload_in_process span { +.openerp .oe_msg_content ul.oe_msg_attachments .oe_upload_in_process span { color: #aaaaaa; position: absolute; } +/* ------------------------------------------------------------ */ +/* Topbar button +/* ------------------------------------------------------------ */ + +.openerp .oe_topbar .oe_topbar_compose_full_email { + float: right; + margin: 3px 25px 0 0; +} \ No newline at end of file diff --git a/addons/mail/static/src/css/mail_compose_message.css b/addons/mail/static/src/css/mail_compose_message.css index e2f136eb755..c706c55bcde 100644 --- a/addons/mail/static/src/css/mail_compose_message.css +++ b/addons/mail/static/src/css/mail_compose_message.css @@ -2,11 +2,11 @@ /* Compose Message */ /* ------------------------------ */ -.openerp .oe_mail_msg_content .oe_mail_compose_message_footer { +.openerp .oe_msg_content .oe_mail_compose_message_footer { height: 24px; } -.openerp .oe_mail_msg_content .oe_mail_compose_message_footer button.oe_mail_compose_message_button_send { +.openerp .oe_msg_content .oe_mail_compose_message_footer button.oe_mail_compose_message_button_send { float: left; } @@ -98,7 +98,7 @@ font-size: 30px; } -.openerp .oe_mail .oe_mail_msg_attachments input { +.openerp .oe_mail .oe_msg_attachments input { visibility: hidden; } @@ -128,39 +128,39 @@ } /* form_view: delete white background */ -.openerp .oe_mail_msg_content div.oe_formview { +.openerp .oe_msg_content div.oe_formview { background-color: transparent; } -.openerp .oe_mail_msg_content div.oe_form_nosheet { +.openerp .oe_msg_content div.oe_form_nosheet { margin: 0px; } -.openerp .oe_mail_msg_content table.oe_form_group { +.openerp .oe_msg_content table.oe_form_group { margin: 0px; } -.openerp .oe_mail_msg_content table.oe_form_field, -.openerp .oe_mail_msg_content div.oe_form_field { +.openerp .oe_msg_content table.oe_form_field, +.openerp .oe_msg_content div.oe_form_field { padding: 0px; } -.openerp .oe_mail_msg_content td.oe_form_group_cell { +.openerp .oe_msg_content td.oe_form_group_cell { vertical-align: bottom; } /* subject: change width */ -.openerp .oe_mail_msg_content .oe_form .oe_form_field input[type='text'] { +.openerp .oe_msg_content .oe_form .oe_form_field input[type='text'] { width: 472px; } /* body_html: cleditor */ -.openerp .oe_mail_msg_content div.cleditorMain { +.openerp .oe_msg_content div.cleditorMain { border: 1px solid #cccccc; } /* destination_partner_ids */ -.openerp .oe_mail_msg_content div.text-core { +.openerp .oe_msg_content div.text-core { height: 22px !important; width: 472px; } \ No newline at end of file diff --git a/addons/mail/static/src/js/mail.js b/addons/mail/static/src/js/mail.js index b70e64ddfce..7501eabe104 100644 --- a/addons/mail/static/src/js/mail.js +++ b/addons/mail/static/src/js/mail.js @@ -12,37 +12,25 @@ openerp.mail = function(session) { * ------------------------------------------------------------ * * Override of formview do_action method, to catch all return action about - * mail.compose.message. The purpose is to bind 'Send by e-mail' buttons - * and redirect them to the Chatter. + * mail.compose.message. The purpose is to bind 'Send by e-mail' buttons. */ session.web.FormView = session.web.FormView.extend({ do_action: function(action) { if (action.res_model == 'mail.compose.message') { - /* hack for stop context propagation of wrong value * delete this hack when a global method to clean context is create - */ - for(var key in action.context){ - if( key!='default_template_id' && - key!='default_composition_mode' && - key!='default_use_template' && - key!='default_model' && - key!='default_res_id' && - key!='default_subtype' && - key!='active_id' && - key!='lang' && - key!='bin_raw' && - key!='tz' && - key!='active_model' && - key!='edi_web_url_view' && - key!='active_ids') - action.context[key]=null; - }; + */ + var context_keys = ['default_template_id', 'default_composition_mode', + 'default_use_template', 'default_partner_ids', 'default_model', + 'default_res_id', 'default_content_subtype', 'active_id', 'lang', + 'bin_raw', 'tz', 'active_model', 'edi_web_url_view', 'active_ids'] + for (var key in action.context) { + if (_.indexOf(context_keys, key) == -1) { + action.context[key] = null; + } + } /* end hack */ - - $('.openerp .oe_mail_wall_threads .oe_mail_thread button.oe_mail_wall_button_fetch').click(); - } return this._super.apply(this, arguments); }, @@ -92,6 +80,7 @@ openerp.mail = function(session) { /* replace textarea text into html text * (add

, ) + * TDE note : should not be here, but server-side I think ... */ get_text2html: function(text){ return text @@ -124,26 +113,27 @@ openerp.mail = function(session) { init: function (parent, options) { var self = this; this._super(parent); - this.attachment_ids = []; - this.context = options.context || {}; - this.id = options.parameters.id; - this.model = options.parameters.model; - this.res_id = options.parameters.res_id; - this.partner_ids = options.parameters.partner_ids; - this.options={thread:{}}; - this.options.thread.show_header_compose = options.parameters.options.thread.show_header_compose; - this.options.thread.display_on_flat = options.parameters.options.thread.display_on_flat; - - this.attachment_ids = []; + this.datasets = { + 'attachment_ids' : [], + 'id': options.datasets.id, + 'model': options.datasets.model, + 'res_model': options.datasets.res_model, + 'is_private': options.datasets.is_private || false, + 'partner_ids': options.datasets.partner_ids || [] + }; + this.options={}; + this.options.thread={}; + this.options.thread.show_header_compose = options.options.thread.show_header_compose; + this.options.thread.display_on_thread = options.options.thread.display_on_thread; this.options.thread.show_attachment_delete = true; this.options.thread.show_attachment_link = true; this.parent_thread= parent.messages!= undefined ? parent : false; - this.ds_attachment = new session.web.DataSetSearch(this, 'ir.attachment'); + this.fileupload_id = _.uniqueId('oe_fileupload_temp'); $(window).on(self.fileupload_id, self.on_attachment_loaded); }, @@ -167,7 +157,7 @@ openerp.mail = function(session) { } else { this.list_attachment.replaceWith( render ); } - this.list_attachment = this.$("ul.oe_mail_msg_attachments"); + this.list_attachment = this.$("ul.oe_msg_attachments"); // event: delete an attachment this.$el.on('click', '.oe_mail_attachment_delete', self.on_attachment_delete); @@ -182,26 +172,25 @@ openerp.mail = function(session) { // if the files exits for this answer, delete the file before upload var attachments=[]; - for(var i in this.attachment_ids){ - if((this.attachment_ids[i].filename || this.attachment_ids[i].name) == filename){ - if(this.attachment_ids[i].upload){ + for(var i in this.datasets.attachment_ids){ + if((this.datasets.attachment_ids[i].filename || this.datasets.attachment_ids[i].name) == filename){ + if(this.datasets.attachment_ids[i].upload){ return false; } - this.ds_attachment.unlink([this.attachment_ids[i].id]); + this.ds_attachment.unlink([this.datasets.attachment_ids[i].id]); } else { - attachments.push(this.attachment_ids[i]); + attachments.push(this.datasets.attachment_ids[i]); } } - this.attachment_ids = attachments; + this.datasets.attachment_ids = attachments; // submit file //session.web.blockUI(); self.$('form.oe_form_binary_form').submit(); - //self.submit_ajax_attachment(); this.$(".oe_attachment_file").hide(); - this.attachment_ids.push({ + this.datasets.attachment_ids.push({ 'id': 0, 'name': filename, 'filename': filename, @@ -212,57 +201,11 @@ openerp.mail = function(session) { } }, - submit_ajax_attachment: function(){ - var self=this; - var $form = self.$('form.oe_form_binary_form'); - var filename = this.$('input.oe_form_binary_file').val().replace(/.*[\\\/]/,''); - - // create form data - var fomdata = new FormData(); - $.each($form.find('input'), function(i, field) { - var $field=$(field); - if($field.attr('type')!='file'){ - fomdata.append($field.attr('name'), $field.val()); - } else { - fomdata.append($field.attr('name'), field.files[0]); - } - }); - - var progress=function(event) { - self.$("span[name='"+filename+"'] div:lt("+Math.floor(event.loaded / event.total*5)+")").show(); - }; - - $.ajax({ - url: $form.attr("action"), - data: fomdata, - cache: false, - contentType: false, - processData: false, - type: 'POST', - enctype: 'multipart/form-data', - xhr: function() { - // custom xhr - myXhr = $.ajaxSettings.xhr(); - if(myXhr.upload){ - // for handling the progress of the upload - myXhr.upload.addEventListener('progress', progress, false); - } - myXhr.addEventListener('progress', progress, false); - return myXhr; - }, - success: function(data){ - $iframe=$('