diff options
| author | Divyesh Makwana (Open ERP) <mdi@tinyerp.com> | 2012-07-03 14:28:16 +0530 |
|---|---|---|
| committer | Divyesh Makwana (Open ERP) <mdi@tinyerp.com> | 2012-07-03 14:28:16 +0530 |
| commit | f28cfcfe8a16e77acd7e390e25fff2793e4e6548 (patch) | |
| tree | fe3fad1ad3f95ff11a34638ea5c7f325cecfa25c /addons/mail | |
| parent | 9ce0d57fbe8357e00e72123e7e4c07326b26b261 (diff) | |
| parent | 1e123ce0c86060be9ad66fa8962912b14597c699 (diff) | |
[Merge] Merge with main addons.
bzr revid: mdi@tinyerp.com-20120703043709-c8o4k6hjfw5v8gn0
bzr revid: mdi@tinyerp.com-20120703085816-mc75d3ukecz03m79
Diffstat (limited to 'addons/mail')
| -rw-r--r-- | addons/mail/__init__.py | 1 | ||||
| -rw-r--r-- | addons/mail/__openerp__.py | 1 | ||||
| -rw-r--r-- | addons/mail/doc/index.rst.inc | 1 | ||||
| -rw-r--r-- | addons/mail/doc/mail_state.rst | 28 | ||||
| -rw-r--r-- | addons/mail/ir_needaction.py | 46 | ||||
| -rw-r--r-- | addons/mail/mail_group.py | 2 | ||||
| -rw-r--r-- | addons/mail/mail_group_view.xml | 46 | ||||
| -rw-r--r-- | addons/mail/mail_message.py | 17 | ||||
| -rw-r--r-- | addons/mail/mail_thread.py | 190 | ||||
| -rw-r--r-- | addons/mail/static/lib/jquery.expander/jquery.expander.js | 385 | ||||
| -rw-r--r-- | addons/mail/static/src/css/mail.css | 4 | ||||
| -rw-r--r-- | addons/mail/static/src/css/mail_group.css | 43 | ||||
| -rw-r--r-- | addons/mail/static/src/js/mail.js | 33 | ||||
| -rw-r--r-- | addons/mail/static/src/xml/mail.xml | 5 | ||||
| -rw-r--r-- | addons/mail/wizard/mail_compose_message_view.xml | 18 |
15 files changed, 700 insertions, 120 deletions
diff --git a/addons/mail/__init__.py b/addons/mail/__init__.py index d9835f86fc5..805c27386f3 100644 --- a/addons/mail/__init__.py +++ b/addons/mail/__init__.py @@ -23,6 +23,7 @@ import mail_message import mail_thread import mail_group import mail_subscription +import ir_needaction import res_users import res_partner import report diff --git a/addons/mail/__openerp__.py b/addons/mail/__openerp__.py index 95ce7d5619d..f501cc58d2c 100644 --- a/addons/mail/__openerp__.py +++ b/addons/mail/__openerp__.py @@ -86,6 +86,7 @@ The main features are: 'static/src/css/mail_group.css', ], 'js': [ + 'static/lib/jquery.expander/jquery.expander.js', 'static/src/js/mail.js', ], 'qweb': [ diff --git a/addons/mail/doc/index.rst.inc b/addons/mail/doc/index.rst.inc index 822d855d5b4..c4b87cc18f9 100644 --- a/addons/mail/doc/index.rst.inc +++ b/addons/mail/doc/index.rst.inc @@ -10,3 +10,4 @@ Mail Module documentation topics mail_openchatter_howto mail_needaction_howto mail_partner + mail_state diff --git a/addons/mail/doc/mail_state.rst b/addons/mail/doc/mail_state.rst new file mode 100644 index 00000000000..527a31ba835 --- /dev/null +++ b/addons/mail/doc/mail_state.rst @@ -0,0 +1,28 @@ +.. _mail_state: + +message_state +============= + +``message_state`` is a boolean field that states whether the document +has unread messages. In previous versions, some documents were going +back to ``pending`` state when receiving an email through the mail +gateway. Now the state related to messages differs from the state or +stage of the document itself. + +message_state and need action mechanism ++++++++++++++++++++++++++++++++++++++++ + +The ``mail`` module introduces a default behavior for the need_action +mechanism [REF]. + +:: + + def get_needaction_user_ids(self, cr, uid, ids, context=None): + """ Returns the user_ids that have to perform an action + :return: dict { record_id: [user_ids], } + """ + result = super(ir_needaction_mixin, self).get_needaction_user_ids(cr, uid, ids, context=context) + for obj in self.browse(cr, uid, ids, context=context): + if obj.message_state == False and obj.user_id: + result[obj.id].append(obj.user_id.id) + return result diff --git a/addons/mail/ir_needaction.py b/addons/mail/ir_needaction.py new file mode 100644 index 00000000000..8193ac4f385 --- /dev/null +++ b/addons/mail/ir_needaction.py @@ -0,0 +1,46 @@ +# -*- coding: utf-8 -*- +############################################################################## +# +# OpenERP, Open Source Management Solution +# Copyright (C) 2012-today OpenERP SA (<http://www.openerp.com>) +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/> +# +############################################################################## + +from osv import osv + +class ir_needaction_mixin(osv.Model): + """ Update of ir.needaction_mixin class + - override the get_needaction_user_ids method to define the default + mail gateway need_action: when the object is unread, the object + responsible has an action to perform. + """ + _name = 'ir.needaction_mixin' + _inherit = ['ir.needaction_mixin'] + + def get_needaction_user_ids(self, cr, uid, ids, context=None): + """ Returns the user_ids that have to perform an action. It the + document mail state is unread (False), return object.user_id.id + as need_action uid. + :return: dict { record_id: [user_ids], } + """ + result = super(ir_needaction_mixin, self).get_needaction_user_ids(cr, uid, ids, context=context) + for obj in self.browse(cr, uid, ids, context=context): + if obj.message_state == False and obj.user_id: + result[obj.id].append(obj.user_id.id) + return result + + +# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: diff --git a/addons/mail/mail_group.py b/addons/mail/mail_group.py index 0a8677d7862..f691d75e0bc 100644 --- a/addons/mail/mail_group.py +++ b/addons/mail/mail_group.py @@ -87,7 +87,7 @@ class mail_group(osv.osv): result = dict.fromkeys(ids) for id in ids: result[id] = {} - result[id]['member_ids'] = self.message_get_subscribers_ids(cr, uid, [id], context=context) + result[id]['member_ids'] = self.message_get_subscribers(cr, uid, [id], context=context) result[id]['member_count'] = len(result[id]['member_ids']) result[id]['is_subscriber'] = uid in result[id]['member_ids'] return result diff --git a/addons/mail/mail_group_view.xml b/addons/mail/mail_group_view.xml index 921c3b528c4..4b9cfd09c3d 100644 --- a/addons/mail/mail_group_view.xml +++ b/addons/mail/mail_group_view.xml @@ -51,28 +51,30 @@ <field name="priority" eval="10"/> <field name="arch" type="xml"> <form string="Group Form" version="7.0"> - <div style="padding: 8px"> - <div class="oe_right"> - <field name="photo" widget='image' on_change="onchange_photo(photo)"/> - </div> - <div class="oe_title"> - <div class="oe_edit_only"> - <label for="name"/> - </div> - <h1><field name="name"/></h1> - <div class="oe_edit_only"> - <label for="responsible_id"/> - </div> - <field name="responsible_id" class="oe_inline"/> - <div class="oe_edit_only"> - <label for="public"/> - <field name="public" class="oe_inline"/> - </div> - <field name="description" colspan="4" nolabel="1"/> - </div> - </div> - <footer> - <field name="message_ids" colspan="4" widget="ThreadView" options='{"thread_level": 1}'/> + <sheet class="openerp_mail_group_sheet"> + <group colspan="4" col="8"> + <group colspan="1" col="2"> + <field name="photo" widget='image' nolabel="1" on_change="onchange_photo(photo)"/> + </group> + <group colspan="7" col="6"> + <div class="oe_edit_only"> + <label for="name"/> + </div> + <h2><field name="name" colspan="4"/></h2> + <newline/> + <group colspan="4" col="4"> + <field name="description" colspan="4" nolabel="1"/> + </group> + <group colspan="2" col="2" class="oe_edit_only"> + <field name="responsible_id" colspan="2"/> + <field name="public" colspan="2"/> + </group> + </group> + </group> + </sheet> + <footer class="openerp_mail_group_footer"> + <field name="message_ids" widget="ThreadView" + options='{"thread_level": 1}'/> </footer> </form> </field> diff --git a/addons/mail/mail_message.py b/addons/mail/mail_message.py index 1c1e980e3ed..301657ba4c2 100644 --- a/addons/mail/mail_message.py +++ b/addons/mail/mail_message.py @@ -96,11 +96,24 @@ class mail_message_common(osv.osv_memory): continue result[message.id] = self.pool.get(message.model).name_get(cr, uid, [message.res_id], context=context)[0][1] return result - + + def name_get(self, cr, uid, ids, context=None): + res = [] + for message in self.browse(cr, uid, ids, context=context): + name = '' + if message.subject: + name = '%s: ' % (message.subject) + if message.body_text: + name = '%s%s ' % (name, message.body_text[0:20]) + if message.date: + name = '%s(%s)' % (name, message.date) + res.append((message.id, name)) + return res + _name = 'mail.message.common' _rec_name = 'subject' _columns = { - 'subject': fields.char('Subject', size=512, required=True), + 'subject': fields.char('Subject', size=512), 'model': fields.char('Related Document Model', size=128, select=1), 'res_id': fields.integer('Related Document ID', select=1), 'record_name': fields.function(get_record_name, type='string', string='Message Record Name', diff --git a/addons/mail/mail_thread.py b/addons/mail/mail_thread.py index e0f2bfaf455..5668709af17 100644 --- a/addons/mail/mail_thread.py +++ b/addons/mail/mail_thread.py @@ -34,7 +34,7 @@ import xmlrpclib _logger = logging.getLogger(__name__) -class mail_thread(osv.osv): +class mail_thread(osv.Model): '''Mixin model, meant to be inherited by any model that needs to act as a discussion topic on which messages can be attached. Public methods are prefixed with ``message_`` in order to avoid @@ -62,10 +62,15 @@ class mail_thread(osv.osv): _name = 'mail.thread' _description = 'Email Thread' - def _get_message_ids(self, cr, uid, ids, name, arg, context=None): + def _get_message_ids(self, cr, uid, ids, name, args, context=None): res = {} for id in ids: - res[id] = self.message_load_ids(cr, uid, [id], context=context) + message_ids = self.message_load_ids(cr, uid, [id], context=context) + subscriber_ids = self.message_get_subscribers(cr, uid, [id], context=context) + res[id] = { + 'message_ids': message_ids, + 'message_summary': "<span>Msg: %d</span> . <span>Fol: %d</span>" % (len(message_ids), len(subscriber_ids)), + } return res def _search_message_ids(self, cr, uid, obj, name, args, context=None): @@ -73,10 +78,23 @@ class mail_thread(osv.osv): msg_ids = msg_obj.search(cr, uid, ['&', ('res_id', 'in', args[0][2]), ('model', '=', self._name)], context=context) return [('id', 'in', msg_ids)] - # OpenChatter: message_ids is a dummy field that should not be used _columns = { - 'message_ids': fields.function(_get_message_ids, method=True, fnct_search=_search_message_ids, - type='one2many', obj='mail.message', string='Temp messages', _fields_id = 'res_id'), + 'message_ids': fields.function(_get_message_ids, method=True, + fnct_search=_search_message_ids, + type='one2many', obj='mail.message', _fields_id = 'res_id', + string='Temp messages', multi="_get_message_ids", + help="Functional field holding messages related to the current document."), + 'message_state': fields.boolean('Read', + help="When checked, new messages require your attention."), + 'message_summary': fields.function(_get_message_ids, method=True, + type='text', string='Summary', multi="_get_message_ids", + help="Holds the Chatter summary (number of messages, ...). "\ + "This summary is directly in html format in order to "\ + "be inserted in kanban views."), + } + + _defaults = { + 'message_state': True, } #------------------------------------------------------ @@ -84,10 +102,11 @@ class mail_thread(osv.osv): #------------------------------------------------------ def create(self, cr, uid, vals, context=None): - """Automatically subscribe the creator""" - thread_id = super(mail_thread, self).create(cr, uid, vals, context=context); - self.message_subscribe(cr, uid, [thread_id], [uid], context=context) - return thread_id; + """Automatically subscribe the creator """ + thread_id = super(mail_thread, self).create(cr, uid, vals, context=context) + if thread_id: + self.message_subscribe(cr, uid, [thread_id], [uid], context=context) + return thread_id def write(self, cr, uid, ids, vals, context=None): """Automatically subscribe the writer""" @@ -106,8 +125,6 @@ class mail_thread(osv.osv): a foreign key with a 'cascade' ondelete attribute. Notifications will be deleted with messages """ - if context is None: - context = {} subscr_obj = self.pool.get('mail.subscription') msg_obj = self.pool.get('mail.message') # delete subscriptions @@ -120,17 +137,18 @@ class mail_thread(osv.osv): return super(mail_thread, self).unlink(cr, uid, ids, context=context) #------------------------------------------------------ - # Generic message api + # mail.message wrappers and tools #------------------------------------------------------ def message_create(self, cr, uid, thread_id, vals, context=None): - """OpenSocial: wrapper of mail.message create method + """ OpenChatter: wrapper of mail.message create method - creates the mail.message - automatically subscribe the message writer - push the message to subscribed users """ if context is None: context = {} + message_obj = self.pool.get('mail.message') subscription_obj = self.pool.get('mail.subscription') notification_obj = self.pool.get('mail.notification') @@ -167,7 +185,10 @@ class mail_thread(osv.osv): # create message msg_id = message_obj.create(cr, uid, vals, context=context) - + + # Set as unread if writer is not the document responsible + self.message_create_set_unread(cr, uid, [thread_id], context=context) + # special: if install mode, do not push demo data if context.get('install_mode', False): return True @@ -186,7 +207,7 @@ class mail_thread(osv.osv): body = new_msg_vals.get('body_html', '') if new_msg_vals.get('subtype', 'plain') == 'html' else new_msg_vals.get('body_text', '') for thread_id in thread_ids: # add subscribers - notif_user_ids += [user['id'] for user in self.message_get_subscribers(cr, uid, [thread_id], context=context)] + notif_user_ids += self.message_get_subscribers(cr, uid, [thread_id], context=context) # add users requested via parsing message (@login) notif_user_ids += self.message_parse_users(cr, uid, [thread_id], body, context=context) # add users requested to perform an action (need_action mechanism) @@ -214,6 +235,10 @@ class mail_thread(osv.osv): user_ids = self.pool.get('res.users').search(cr, uid, [('login', 'in', login_lst)], context=context) return user_ids + #------------------------------------------------------ + # Generic message api + #------------------------------------------------------ + def message_capable_models(self, cr, uid, context=None): ret_dict = {} for model_name in self.pool.obj_list(): @@ -376,7 +401,6 @@ class mail_thread(osv.osv): original = msg_dict.get('original'), context = context) - # Message loading def _message_add_ancestor_ids(self, cr, uid, ids, child_ids, root_ids, context=None): """ Given message child_ids Find their ancestors until root ids""" @@ -420,6 +444,9 @@ class mail_thread(osv.osv): msg_ids = self.message_load_ids(cr, uid, ids, limit, offset, domain, ascent, root_ids, context=context) msgs = self.pool.get('mail.message').read(cr, uid, msg_ids, [], context=context) + # Set as read + self.message_check_and_set_read(cr, uid, ids, context=context) + """ Retrieve all attachments names """ map_id_to_name = {} @@ -474,7 +501,7 @@ class mail_thread(osv.osv): return msgs #------------------------------------------------------ - # Email specific + # Mail gateway #------------------------------------------------------ # message_process will call either message_new or message_update. @@ -549,7 +576,7 @@ class mail_thread(osv.osv): if res_id: res_id = res_id.group(1) if res_id: - res_id = int(res_id) + res_id = res_id if model_pool.exists(cr, uid, res_id): if hasattr(model_pool, 'message_update'): model_pool.message_update(cr, uid, [res_id], msg, {}, context=context) @@ -558,7 +585,7 @@ class mail_thread(osv.osv): res_id = False if not res_id: res_id = create_record(msg) - #To forward the email to other followers + # To forward the email to other followers self.message_forward(cr, uid, model, [res_id], msg_txt, context=context) return res_id @@ -594,29 +621,34 @@ class mail_thread(osv.osv): 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('from','') + data['name'] = msg_dict.get('from', '') if custom_values and isinstance(custom_values, dict): data.update(custom_values) res_id = model_pool.create(cr, uid, data, context=context) self.message_append_dict(cr, uid, [res_id], msg_dict, context=context) return res_id - def message_update(self, cr, uid, ids, msg_dict, vals={}, default_act=None, context=None): - """Called by ``message_process`` when a new message is received - for an existing thread. The default behavior is to create a - new mail.message in the given thread (by calling - ``message_append_dict``) - Additional behavior may be implemented by overriding this - method. + def message_update(self, cr, uid, ids, msg_dict, update_vals=None, context=None): + """ Called by ``message_process`` when a new message is received + for an existing thread. The default behavior is to create a + new mail.message in the given thread (by calling + ``message_append_dict``) + Additional behavior may be implemented by overriding this + method. - :param dict msg_dict: a map containing the email details and + :param dict msg_dict: a map containing the email details and attachments. See ``message_process`` and ``mail.message.parse()`` for details. - :param dict context: if a ``thread_model`` value is present + :param dict vals: a dict containing values to update records + given their ids; if the dict is None or is + void, no write operation is performed. + :param dict context: if a ``thread_model`` value is present in the context, its value will be used to determine the model of the thread to update (instead of the current model). """ + if update_vals: + self.write(cr, uid, ids, update_vals, context=context) return self.message_append_dict(cr, uid, ids, msg_dict, context=context) def message_thread_followers(self, cr, uid, ids, context=None): @@ -746,13 +778,8 @@ class mail_thread(osv.osv): self.message_append_note(cr, uid, [id], 'res.log', message, context=context) def message_append_note(self, cr, uid, ids, subject=None, body=None, parent_id=False, type='notification', subtype='html', context=None): - if subject is None: - if type == 'notification': - subject = _('System notification') - elif type == 'comment' and not parent_id: - subject = _('Comment') - elif type == 'comment' and parent_id: - subject = _('Reply') + if type in ['notification', 'reply']: + subject = None if subtype == 'html': body_html = body body_text = body @@ -765,51 +792,77 @@ class mail_thread(osv.osv): # Subscription mechanism #------------------------------------------------------ - def message_get_subscribers_ids(self, cr, uid, ids, context=None): + def message_get_subscribers(self, cr, uid, ids, context=None): + """ Returns the current document followers. Basically this method + checks in mail.subscription for entries with matching res_model, + res_id. + + :param get_ids: if set to True, return the ids of users; if set + to False, returns the result of a read in res.users + """ subscr_obj = self.pool.get('mail.subscription') subscr_ids = subscr_obj.search(cr, uid, ['&', ('res_model', '=', self._name), ('res_id', 'in', ids)], context=context) - subs = subscr_obj.read(cr, uid, subscr_ids, context=context) - return [sub['user_id'][0] for sub in subs] + return [sub['user_id'][0] for sub in subscr_obj.read(cr, uid, subscr_ids, ['user_id'], context=context)] - def message_get_subscribers(self, cr, uid, ids, context=None): - user_ids = self.message_get_subscribers_ids(cr, uid, ids, context=context) - users = self.pool.get('res.users').read(cr, uid, user_ids, fields=['id', 'name', 'avatar'], context=context) - return users + def message_read_subscribers(self, cr, uid, ids, fields=['id', 'name', 'avatar'], context=None): + """ Returns the current document followers as a read result. Used + mainly for Chatter having only one method to call to have + details about users. + """ + user_ids = self.message_get_subscribers(cr, uid, ids, context=context) + return self.pool.get('res.users').read(cr, uid, user_ids, fields=fields, context=context) def message_is_subscriber(self, cr, uid, ids, user_id = None, context=None): - users = self.message_get_subscribers(cr, uid, ids, context=context) + """ Check if uid or user_id (if set) is a subscriber to the current + document. + + :param user_id: if set, check is done on user_id; if not set + check is done on uid + """ sub_user_id = uid if user_id is None else user_id - if sub_user_id in [user['id'] for user in users]: + if sub_user_id in self.message_get_subscribers(cr, uid, ids, context=context): return True return False def message_subscribe(self, cr, uid, ids, user_ids = None, context=None): + """ Subscribe the user (or user_ids) to the current document. + + :param user_ids: a list of user_ids; if not set, subscribe + uid instead + """ subscription_obj = self.pool.get('mail.subscription') to_subscribe_uids = [uid] if user_ids is None else user_ids create_ids = [] for id in ids: + already_subscribed_user_ids = self.message_get_subscribers(cr, uid, [id], context=context) for user_id in to_subscribe_uids: - if self.message_is_subscriber(cr, uid, [id], user_id=user_id, context=context): continue + if user_id in already_subscribed_user_ids: continue create_ids.append(subscription_obj.create(cr, uid, {'res_model': self._name, 'res_id': id, 'user_id': user_id}, context=context)) return create_ids def message_unsubscribe(self, cr, uid, ids, user_ids = None, context=None): - if not user_ids and not uid in self.message_get_subscribers_ids(cr, uid, ids, context=context): + """ Unsubscribe the user (or user_ids) from the current document. + + :param user_ids: a list of user_ids; if not set, subscribe + uid instead + """ + # Trying to unsubscribe somebody not in subscribers: returns False + # if special management is needed; allows to know that an automatically + # subscribed user tries to unsubscribe and allows to warn him + mail_thread_model = self.pool.get('mail.thread') + if not user_ids and not uid in mail_thread_model.message_get_subscribers(cr, uid, ids, context=context): return False subscription_obj = self.pool.get('mail.subscription') to_unsubscribe_uids = [uid] if user_ids is None else user_ids to_delete_sub_ids = subscription_obj.search(cr, uid, ['&', '&', ('res_model', '=', self._name), ('res_id', 'in', ids), ('user_id', 'in', to_unsubscribe_uids)], context=context) - subscription_obj.unlink(cr, uid, to_delete_sub_ids, context=context) - return True + return subscription_obj.unlink(cr, uid, to_delete_sub_ids, context=context) #------------------------------------------------------ # Notification API #------------------------------------------------------ def message_remove_pushed_notifications(self, cr, uid, ids, msg_ids, remove_childs=True, context=None): - if context is None: - context = {} notif_obj = self.pool.get('mail.notification') msg_obj = self.pool.get('mail.message') if remove_childs: @@ -819,4 +872,37 @@ class mail_thread(osv.osv): to_del_notif_ids = notif_obj.search(cr, uid, ['&', ('user_id', '=', uid), ('message_id', 'in', notif_msg_ids)], context=context) return notif_obj.unlink(cr, uid, to_del_notif_ids, context=context) + #------------------------------------------------------ + # Thread_state + #------------------------------------------------------ + + def message_create_set_unread(self, cr, uid, ids, context=None): + """ When creating a new message, set as unread if uid is not the + object responsible. """ + for obj in self.browse(cr, uid, ids, context=context): + if obj.message_state and hasattr(obj, 'user_id') and (not obj.user_id or obj.user_id.id != uid): + self.message_mark_as_unread(cr, uid, [obj.id], context=context) + + def message_check_and_set_unread(self, cr, uid, ids, context=None): + """ Set unread if uid is the object responsible or if the object has + no responsible. """ + for obj in self.browse(cr, uid, ids, context=context): + if obj.message_state and hasattr(obj, 'user_id') and (not obj.user_id or obj.user_id.id == uid): + self.message_mark_as_unread(cr, uid, [obj.id], context=context) + + def message_mark_as_unread(self, cr, uid, ids, context=None): + """ Set as unread. """ + return self.write(cr, uid, ids, {'message_state': False}, context=context) + + def message_check_and_set_read(self, cr, uid, ids, context=None): + """ Set read if uid is the object responsible. """ + for obj in self.browse(cr, uid, ids, context=context): + if not obj.message_state and hasattr(obj, 'user_id') and obj.user_id and obj.user_id.id == uid: + self.message_mark_as_read(cr, uid, [obj.id], context=context) + + def message_mark_as_read(self, cr, uid, ids, context=None): + """ Set as read. """ + return self.write(cr, uid, ids, {'message_state': True}, context=context) + + # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: diff --git a/addons/mail/static/lib/jquery.expander/jquery.expander.js b/addons/mail/static/lib/jquery.expander/jquery.expander.js new file mode 100644 index 00000000000..91cc03db918 --- /dev/null +++ b/addons/mail/static/lib/jquery.expander/jquery.expander.js @@ -0,0 +1,385 @@ +/*! + * jQuery Expander Plugin v1.4.2 + * + * Date: Fri Mar 16 14:29:56 2012 EDT + * Requires: jQuery v1.3+ + * + * Copyright 2011, Karl Swedberg + * Dual licensed under the MIT and GPL licenses (just like jQuery): + * http://www.opensource.org/licenses/mit-license.php + * http://www.gnu.org/licenses/gpl.html + * + * + * + * +*/ + +(function($) { + $.expander = { + version: '1.4.2', + defaults: { + // the number of characters at which the contents will be sliced into two parts. + slicePoint: 100, + + // whether to keep the last word of the summary whole (true) or let it slice in the middle of a word (false) + preserveWords: true, + + // a threshold of sorts for whether to initially hide/collapse part of the element's contents. + // If after slicing the contents in two there are fewer words in the second part than + // the value set by widow, we won't bother hiding/collapsing anything. + widow: 4, + + // text displayed in a link instead of the hidden part of the element. + // clicking this will expand/show the hidden/collapsed text + expandText: 'read more', + expandPrefix: '… ', + + expandAfterSummary: false, + + // class names for summary element and detail element + summaryClass: 'summary', + detailClass: 'details', + + // class names for <span> around "read-more" link and "read-less" link + moreClass: 'read-more', + lessClass: 'read-less', + + // number of milliseconds after text has been expanded at which to collapse the text again. + // when 0, no auto-collapsing + collapseTimer: 0, + + // effects for expanding and collapsing + expandEffect: 'fadeIn', + expandSpeed: 250, + collapseEffect: 'fadeOut', + collapseSpeed: 200, + + // allow the user to re-collapse the expanded text. + userCollapse: true, + + // text to use for the link to re-collapse the text + userCollapseText: 'read less', + userCollapsePrefix: ' ', + + + // all callback functions have the this keyword mapped to the element in the jQuery set when .expander() is called + + onSlice: null, // function() {} + beforeExpand: null, // function() {}, + afterExpand: null, // function() {}, + onCollapse: null // function(byUser) {} + } + }; + + $.fn.expander = function(options) { + var meth = 'init'; + + if (typeof options == 'string') { + meth = options; + options = {}; + } + + var opts = $.extend({}, $.expander.defaults, options), + rSelfClose = /^<(?:area|br|col|embed|hr|img|input|link|meta|param).*>$/i, + rAmpWordEnd = opts.wordEnd || /(&(?:[^;]+;)?|[a-zA-Z\u00C0-\u0100]+)$/, + rOpenCloseTag = /<\/?(\w+)[^>]*>/g, + rOpenTag = /<(\w+)[^>]*>/g, + rCloseTag = /<\/(\w+)>/g, + rLastCloseTag = /(<\/[^>]+>)\s*$/, + rTagPlus = /^<[^>]+>.?/, + delayedCollapse; + + var methods = { + init: function() { + this.each(function() { + var i, l, tmp, newChar, summTagless, summOpens, summCloses, + lastCloseTag, detailText, detailTagless, + $thisDetails, $readMore, + openTagsForDetails = [], + closeTagsForsummaryText = [], + defined = {}, + thisEl = this, + $this = $(this), + $summEl = $([]), + o = $.meta ? $.extend({}, opts, $this.data()) : opts, + hasDetails = !!$this.find('.' + o.detailClass).length, + hasBlocks = !!$this.find('*').filter(function() { + var display = $(this).css('display'); + return (/^block|table|list/).test(display); + }).length, + el = hasBlocks ? 'div' : 'span', + detailSelector = el + '.' + o.detailClass, + moreSelector = 'span.' + o.moreClass, + expandSpeed = o.expandSpeed || 0, + allHtml = $.trim( $this.html() ), + allText = $.trim( $this.text() ), + summaryText = allHtml.slice(0, o.slicePoint); + + // bail out if we've already set up the expander on this element + if ( $.data(this, 'expander') ) { + return; + } + + $.data(this, 'expander', true); + + // determine which callback functions are defined + $.each(['onSlice','beforeExpand', 'afterExpand', 'onCollapse'], function(index, val) { + defined[val] = $.isFunction(o[val]); + }); + + // back up if we're in the middle of a tag or word + summaryText = backup(summaryText); + + // summary text sans tags length + summTagless = summaryText.replace(rOpenCloseTag, '').length; + + // add more characters to the summary, one for each character in the tags + while (summTagless < o.slicePoint) { + newChar = allHtml.charAt(summaryText.length); + if (newChar == '<') { + newChar = allHtml.slice(summaryText.length).match(rTagPlus)[0]; + } + summaryText += newChar; + summTagless++; + } + + summaryText = backup(summaryText, o.preserveWords); + + // separate open tags from close tags and clean up the lists + summOpens = summaryText.match(rOpenTag) || []; + summCloses = summaryText.match(rCloseTag) || []; + + // filter out self-closing tags + tmp = []; + $.each(summOpens, function(index, val) { + if ( !rSelfClose.test(val) ) { + tmp.push(val); + } + }); + summOpens = tmp; + + // strip close tags to just the tag name + l = summCloses.length; + for (i = 0; i < l; i++) { + summCloses[i] = summCloses[i].replace(rCloseTag, '$1'); + } + + // tags that start in summary and end in detail need: + // a). close tag at end of summary + // b). open tag at beginning of detail + $.each(summOpens, function(index, val) { + var thisTagName = val.replace(rOpenTag, '$1'); + var closePosition = $.inArray(thisTagName, summCloses); + if (closePosition === -1) { + openTagsForDetails.push(val); + closeTagsForsummaryText.push('</' + thisTagName + '>'); + + } else { + summCloses.splice(closePosition, 1); + } + }); + + // reverse the order of the close tags for the summary so they line up right + closeTagsForsummaryText.reverse(); + + // create necessary summary and detail elements if they don't already exist + if ( !hasDetails ) { + + // end script if there is no detail text or if detail has fewer words than widow option + detailText = allHtml.slice(summaryText.length); + detailTagless = $.trim( detailText.replace(rOpenCloseTag, '') ); + + if ( detailTagless === '' || detailTagless.split(/\s+/).length < o.widow ) { + return; + } + // otherwise, continue... + lastCloseTag = closeTagsForsummaryText.pop() || ''; + summaryText += closeTagsForsummaryText.join(''); + detailText = openTagsForDetails.join('') + detailText; + + } else { + // assume that even if there are details, we still need readMore/readLess/summary elements + // (we already bailed out earlier when readMore el was found) + // but we need to create els differently + + // remove the detail from the rest of the content + detailText = $this.find(detailSelector).remove().html(); + + // The summary is what's left + summaryText = $this.html(); + + // allHtml is the summary and detail combined (this is needed when content has block-level elements) + allHtml = summaryText + detailText; + + lastCloseTag = ''; + } + o.moreLabel = $this.find(moreSelector).length ? '' : buildMoreLabel(o); + + if (hasBlocks) { + detailText = allHtml; + } + summaryText += lastCloseTag; + + // onSlice callback + o.summary = summaryText; + o.details = detailText; + o.lastCloseTag = lastCloseTag; + + if (defined.onSlice) { + // user can choose to return a modified options object + // one last chance for user to change the options. sneaky, huh? + // but could be tricky so use at your own risk. + tmp = o.onSlice.call(thisEl, o); + + // so, if the returned value from the onSlice function is an object with a details property, we'll use that! + o = tmp && tmp.details ? tmp : o; + } + + // build the html with summary and detail and use it to replace old contents + var html = buildHTML(o, hasBlocks); + + $this.html( html ); + + // set up details and summary for expanding/collapsing + $thisDetails = $this.find(detailSelector); + $readMore = $this.find(moreSelector); + $thisDetails.hide(); + $readMore.find('a').unbind('click.expander').bind('click.expander', expand); + + $summEl = $this.find('div.' + o.summaryClass); + + if ( o.userCollapse && !$this.find('span.' + o.lessClass).length ) { + $this + .find(detailSelector) + .append('<span class="' + o.lessClass + '">' + o.userCollapsePrefix + '<a href="#">' + o.userCollapseText + '</a></span>'); + } + + $this + .find('span.' + o.lessClass + ' a') + .unbind('click.expander') + .bind('click.expander', function(event) { + event.preventDefault(); + clearTimeout(delayedCollapse); + var $detailsCollapsed = $(this).closest(detailSelector); + reCollapse(o, $detailsCollapsed); + if (defined.onCollapse) { + o.onCollapse.call(thisEl, true); + } + }); + + function expand(event) { + event.preventDefault(); + $readMore.hide(); + $summEl.hide(); + if (defined.beforeExpand) { + o.beforeExpand.call(thisEl); + } + + $thisDetails.stop(false, true)[o.expandEffect](expandSpeed, function() { + $thisDetails.css({zoom: ''}); + if (defined.afterExpand) {o.afterExpand.call(thisEl);} + delayCollapse(o, $thisDetails, thisEl); + }); + } + + }); // this.each + }, + destroy: function() { + if ( !this.data('expander') ) { + return; + } + this.removeData('expander'); + this.each(function() { + var $this = $(this), + o = $.meta ? $.extend({}, opts, $this.data()) : opts, + details = $this.find('.' + o.detailClass).contents(); + + $this.find('.' + o.moreClass).remove(); + $this.find('.' + o.summaryClass).remove(); + $this.find('.' + o.detailClass).after(details).remove(); + $this.find('.' + o.lessClass).remove(); + + }); + } + }; + + // run the methods (almost always "init") + if ( methods[meth] ) { + methods[ meth ].call(this); + } + + // utility functions + function buildHTML(o, blocks) { + var el = 'span', + summary = o.summary; + if ( blocks ) { + el = 'div'; + // if summary ends with a close tag, tuck the moreLabel inside it + if ( rLastCloseTag.test(summary) && !o.expandAfterSummary) { + summary = summary.replace(rLastCloseTag, o.moreLabel + '$1'); + } else { + // otherwise (e.g. if ends with self-closing tag) just add moreLabel after summary + // fixes #19 + summary += o.moreLabel; + } + + // and wrap it in a div + summary = '<div class="' + o.summaryClass + '">' + summary + '</div>'; + } else { + summary += o.moreLabel; + } + + return [ + summary, + '<', + el + ' class="' + o.detailClass + '"', + '>', + o.details, + '</' + el + '>' + ].join(''); + } + + function buildMoreLabel(o) { + var ret = '<span class="' + o.moreClass + '">' + o.expandPrefix; + ret += '<a href="#">' + o.expandText + '</a></span>'; + return ret; + } + + function backup(txt, preserveWords) { + if ( txt.lastIndexOf('<') > txt.lastIndexOf('>') ) { + txt = txt.slice( 0, txt.lastIndexOf('<') ); + } + if (preserveWords) { + txt = txt.replace(rAmpWordEnd,''); + } + + return $.trim(txt); + } + + function reCollapse(o, el) { + el.stop(true, true)[o.collapseEffect](o.collapseSpeed, function() { + var prevMore = el.prev('span.' + o.moreClass).show(); + if (!prevMore.length) { + el.parent().children('div.' + o.summaryClass).show() + .find('span.' + o.moreClass).show(); + } + }); + } + + function delayCollapse(option, $collapseEl, thisEl) { + if (option.collapseTimer) { + delayedCollapse = setTimeout(function() { + reCollapse(option, $collapseEl); + if ( $.isFunction(option.onCollapse) ) { + option.onCollapse.call(thisEl, false); + } + }, option.collapseTimer); + } + } + + return this; + }; + + // plugin defaults + $.fn.expander.defaults = $.expander.defaults; +})(jQuery); diff --git a/addons/mail/static/src/css/mail.css b/addons/mail/static/src/css/mail.css index 2421b28fbcb..0cf932f9a25 100644 --- a/addons/mail/static/src/css/mail.css +++ b/addons/mail/static/src/css/mail.css @@ -314,8 +314,8 @@ text-align: justify; } -.openerp .oe_mail_msg_tail { - display: none; +.openerp .oe_mail_msg_body span.oe_mail_msg_tail { + display: inline; } /* Read more/less link */ diff --git a/addons/mail/static/src/css/mail_group.css b/addons/mail/static/src/css/mail_group.css index 90261499753..048b7a06415 100644 --- a/addons/mail/static/src/css/mail_group.css +++ b/addons/mail/static/src/css/mail_group.css @@ -1,3 +1,40 @@ +/* ------------------------------ */ +/* Group Form */ +/* ------------------------------ */ + +/* Resize sheet width */ +.openerp .oe_form_sheetbg.openerp_mail_group_sheet { + min-height: 0px; + max-height: none; +} + +.openerp .oe_form_sheetbg.openerp_mail_group_sheet .oe_form_sheet { + min-height: 0px; + max-height: none; + padding: 0px 18px; + max-width: 80%; +} + +/* Resize footer width */ +.openerp .oe_form footer.openerp_mail_group_footer { + max-width: 80%; +} + +/* Resize group logo */ +.openerp .oe_form_sheetbg.openerp_mail_group_sheet .oe_form_field_image > img { + max-width: 100px; + max-height: 100px; +} + +/* Resize group description */ +.openerp .oe_form_sheetbg.openerp_mail_group_sheet .oe_form_field_text > textarea { + height: 40px; +} + +/* ------------------------------ */ +/* Group Kanban */ +/* ------------------------------ */ + .oe_group_vignette { padding: 8px 0; min-height: 100px; @@ -71,9 +108,3 @@ .oe_group_details li { margin: 2px 0; } - -.oe_group_description { -} - -.oe_kanban_record div.true { -} diff --git a/addons/mail/static/src/js/mail.js b/addons/mail/static/src/js/mail.js index 5633a4bdae0..da54130d093 100644 --- a/addons/mail/static/src/js/mail.js +++ b/addons/mail/static/src/js/mail.js @@ -302,17 +302,20 @@ openerp.mail = function(session) { record.body = this.do_clean_text(record.body); record.body = this.do_replace_internal_links(record.body); - // split for see more - var split = this.do_truncate_string(record.body, this.params.msg_more_limit); - record.body_head = split[0]; - record.body_tail = split[1]; - // format date according to the user timezone record.date = session.web.format_value(record.date, {type:"datetime"}); - var rendered = session.web.qweb.render('mail.Thread.message', {'record': record, 'thread': this, 'params': this.params, 'display': this.display}); - $( rendered).appendTo(this.$element.children('div.oe_mail_thread_display:first')); + $(rendered).appendTo(this.$element.children('div.oe_mail_thread_display:first')); + // expand feature + this.$element.find('div.oe_mail_msg_body:last').expander({ + slicePoint: this.params.msg_more_limit, + expandText: 'see more', + userCollapseText: 'see less', + detailClass: 'oe_mail_msg_tail', + moreClass: 'oe_mail_expand', + lesClass: 'oe_mail_reduce', + }); }, /** @@ -447,20 +450,6 @@ openerp.mail = function(session) { return this.session.prefix + '/web/binary/image?session_id=' + this.session.session_id + '&model=' + model + '&field=' + field + '&id=' + (id || ''); }, - /** - * @param {String} string to truncate - * @param {Number} max number of chars to display - * @returns {String} truncated string - */ - do_truncate_string: function(string, max_length) { - // multiply by 1.2: prevent truncating an just too little long string - if (string.length <= (max_length * 1.2)) { - return [string, ""]; - } else { - return [string.slice(0, max_length), string.slice(max_length)]; - } - }, - /** Removes html tags, except b, em, br */ do_clean_text: function (string) { var html = $('<div/>').text(string.replace(/\s+/g, ' ')).html().replace(new RegExp('<(/)?(b|em|br|br /)\\s*>', 'gi'), '<$1$2>'); @@ -565,7 +554,7 @@ openerp.mail = function(session) { }, fetch_subscribers: function () { - return this.ds.call('message_get_subscribers', [[this.view.datarecord.id]]).then(this.proxy('display_subscribers')); + return this.ds.call('message_read_subscribers', [[this.view.datarecord.id]]).then(this.proxy('display_subscribers')); }, display_subscribers: function (records) { diff --git a/addons/mail/static/src/xml/mail.xml b/addons/mail/static/src/xml/mail.xml index 34445c48f8d..2eb3a390933 100644 --- a/addons/mail/static/src/xml/mail.xml +++ b/addons/mail/static/src/xml/mail.xml @@ -91,10 +91,7 @@ </div> <div class="oe_mail_msg_body"> <a href="#" data-res-model='res.users' t-attf-data-res-id='{record.user_id[0]}' class="oe_mail_internal_link"><t t-raw="record.user_id[1]"/></a> - <t t-raw="record.body_head"/> - <t t-if="record.body_tail"><a href="#" class="oe_mail_msg_more">... See more</a> - <span class="oe_mail_msg_tail"><t t-raw="record.body_tail"/></span> - </t> + <t t-raw="record.body"/> </div> <ul class="oe_mail_msg_footer"> <li><span t-att-title="record.date"><t t-raw="record.timerelative"/></span></li> diff --git a/addons/mail/wizard/mail_compose_message_view.xml b/addons/mail/wizard/mail_compose_message_view.xml index 99eb7192738..3da63fb1adc 100644 --- a/addons/mail/wizard/mail_compose_message_view.xml +++ b/addons/mail/wizard/mail_compose_message_view.xml @@ -12,18 +12,18 @@ or <button string="Cancel" class="oe_link" special="cancel" /> </header> - <group col="6" colspan="4"> + <group> <field name="model" invisible="1"/> <field name="res_id" invisible="1"/> <field name='filter_id' invisible="context.get('active_model',False)"/> - <field name="email_from" colspan="4" required="1"/> - <field name="email_to" colspan="4" required="1"/> - <field name="email_cc" colspan="4"/> - <field name="email_bcc" colspan="4"/> - <field name="reply_to" colspan="4"/> - <field name="subject" colspan="4" widget="char" size="512"/> - <field name="references" invisible="1"/> - <field name="message_id" invisible="1"/> + <field name="email_from" required="1"/> + <field name="email_to" required="1"/> + <field name="email_cc"/> + <field name="email_bcc"/> + <field name="reply_to"/> + <field name="subject" widget="char" size="512"/> + <field name="references"/> + <field name="message_id"/> </group> <notebook colspan="4"> <page string="Body"> |
