[MERGE] Merged OpenChatter part 5.2: Mailbox Gangnam Style, Pooh Strikes Back

Improvements
- added Favorites, by adding the 'starred' mechanism. Starred messages are messages the user wants to keep in a specific mailbox, independantly of notifications and follow mechanism
- refactored the mailboxes, now having Inbox; To:me; Archives; Sent; Favorites
- refactored expandables
- overall refactoring of mail widget
- fixed message_read method
Next steps :
- CSS/design, client-side code
- more tests, maybe some doc, could be interesting ...

bzr revid: tde@openerp.com-20121019125158-k1rlrkhxw9q51t53
This commit is contained in:
Thibault Delavallée 2012-10-19 14:51:58 +02:00
commit 583f26d691
18 changed files with 873 additions and 541 deletions

View File

@ -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:

View File

@ -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',

View File

@ -0,0 +1,39 @@
# -*- 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, 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:

View File

@ -0,0 +1,44 @@
<?xml version="1.0"?>
<openerp>
<data>
<!-- FOLLOWERS !-->
<record model="ir.ui.view" id="view_mail_favorite_tree">
<field name="name">mail.favorite.tree</field>
<field name="model">mail.favorite</field>
<field name="arch" type="xml">
<tree string="Favorites">
<field name="user_id"/>
<field name="message_id"/>
</tree>
</field>
</record>
<record model="ir.ui.view" id="view_mail_favorite_form">
<field name="name">mail.favorite.form</field>
<field name="model">mail.favorite</field>
<field name="arch" type="xml">
<form string="Favorite Form" version="7.0">
<sheet>
<group>
<field name="user_id"/>
<field name="message_id"/>
</group>
</sheet>
</form>
</field>
</record>
<record id="action_view_favorites" model="ir.actions.act_window">
<field name="name">Favorites</field>
<field name="res_model">mail.favorite</field>
<field name="view_type">form</field>
<field name="view_mode">tree,form</field>
</record>
<!-- Add favorites related menu entries in Settings/Email -->
<menuitem name="Favorites" id="menu_email_favorites" parent="base.menu_email"
action="action_view_favorites" sequence="40" groups="base.group_no_one"/>
</data>
</openerp>

View File

@ -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

View File

@ -56,8 +56,9 @@
<field name="subject" string="Content" filter_domain="['|', ('subject', 'ilike', self), ('body', 'ilike', self)]" />
<field name="type"/>
<field name="author_id"/>
<field name="partner_ids"/>
<filter string="Unread"
name="messages_unread" help="Show messages to read"
name="message_unread" help="Show messages to read"
domain="[('to_read', '=', True)]"/>
<filter string="Comments"
name="comments" help="Comments"
@ -84,25 +85,5 @@
<!-- Add menu entry in Settings/Email -->
<menuitem name="Messages" id="menu_mail_message" parent="base.menu_email" action="action_view_mail_message"/>
<record id="action_mail_inbox_feeds" model="ir.actions.client">
<field name="name">Inbox</field>
<field name="tag">mail.wall</field>
<field name="params" eval="&quot;{'domain': [('notification_ids.partner_id.user_ids', 'in', [uid]), ('to_read', '=', True)],
'context': {'default_model': 'res.users', 'default_res_id': uid} }&quot;"/>
</record>
<record id="action_mail_archives_feeds" model="ir.actions.client">
<field name="name">Archives</field>
<field name="tag">mail.wall</field>
<field name="params" eval="&quot;{'domain': [('notification_ids.partner_id.user_ids', 'in', [uid]), ('to_read', '=', False)],
'context': {'default_model': 'res.users', 'default_res_id': uid} }&quot;"/>
</record>
<record id="action_mail_sent_feeds" model="ir.actions.client">
<field name="name">Sent</field>
<field name="tag">mail.wall</field>
<field name="params" eval="&quot;{'domain': [('author_id.user_ids', 'in', [uid])],
'context': {'default_model': 'res.users', 'default_res_id': uid} }&quot;"/>
</record>
</data>
</openerp>
</openerp>

View File

@ -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):

View File

@ -1,35 +1,82 @@
<?xml version="1.0"?>
<openerp>
<data>
<record id="action_mail_inbox_feeds" model="ir.actions.client">
<field name="name">Inbox</field>
<field name="tag">mail.wall</field>
<field name="params" eval="&quot;{'domain': [('notification_ids.partner_id.user_ids', 'in', [uid]), ('to_read', '=', True)],
'context': {'default_model': 'res.users', 'default_res_id': uid, 'typeof_thread': 'inbox'} }&quot;"/>
</record>
<record id="action_mail_to_me_feeds" model="ir.actions.client">
<field name="name">To: me</field>
<field name="tag">mail.wall</field>
<field name="params" eval="&quot;{'domain': [('partner_ids.user_ids', 'in', [uid]), ('to_read', '=', True), ('author_id.user_ids', 'in', [uid])],
'context': {'default_model': 'res.users', 'default_res_id': uid, 'typeof_thread': 'inbox'} }&quot;"/>
</record>
<record id="action_mail_star_feeds" model="ir.actions.client">
<field name="name">Favorites</field>
<field name="tag">mail.wall</field>
<field name="params" eval="&quot;{'domain': [('favorite_user_ids.user_ids', 'in', [uid])],
'context': {'default_model': 'res.users', 'default_res_id': uid, 'typeof_thread': 'stared'} }&quot;"/>
</record>
<record id="action_mail_archives_feeds" model="ir.actions.client">
<field name="name">Archives</field>
<field name="tag">mail.wall</field>
<field name="params" eval="&quot;{'domain': [('notification_ids.partner_id.user_ids', 'in', [uid]), ('to_read', '=', False)],
'context': {'default_model': 'res.users', 'default_res_id': uid, 'typeof_thread': 'archives'} }&quot;"/>
</record>
<record id="action_mail_sent_feeds" model="ir.actions.client">
<field name="name">Sent</field>
<field name="tag">mail.wall</field>
<field name="params" eval="&quot;{'domain': [('author_id.user_ids', 'in', [uid])],
'context': {'default_model': 'res.users', 'default_res_id': uid, 'typeof_thread': 'send'} }&quot;"/>
</record>
<!-- MENU -->
<!-- Top menu item -->
<menuitem name="Home"
id="mail_feeds_main"
<menuitem name="Mails"
id="mail.mail_feeds_main"
groups="base.group_user"
sequence="10"/>
<!-- Left-side menu: Feeds -->
<menuitem id="mail_feeds" name="Feeds" parent="mail.mail_feeds_main" groups="base.group_user" sequence="10"/>
<menuitem id="mail.mail_feeds" name="Feeds &amp; Mailbox" parent="mail.mail_feeds_main" groups="base.group_user" sequence="10"/>
<menuitem id="mail_my_stuff" name="Organizer" parent="mail.mail_feeds_main"/>
<record id="mail_inboxfeeds" model="ir.ui.menu">
<field name="name">Inbox</field>
<field name="sequence" eval="10"/>
<field name="action" ref="action_mail_inbox_feeds"/>
<field name="parent_id" ref="mail_feeds"/>
<field name="parent_id" ref="mail.mail_feeds"/>
</record>
<record id="mail_tomefeeds" model="ir.ui.menu">
<field name="name">To: me</field>
<field name="sequence" eval="11"/>
<field name="action" ref="action_mail_to_me_feeds"/>
<field name="parent_id" ref="mail.mail_feeds"/>
</record>
<record id="mail_starfeeds" model="ir.ui.menu">
<field name="name">Favorites</field>
<field name="sequence" eval="14"/>
<field name="action" ref="action_mail_star_feeds"/>
<field name="parent_id" ref="mail.mail_feeds"/>
</record>
<record id="mail_archivesfeeds" model="ir.ui.menu">
<field name="name">Archives</field>
<field name="sequence" eval="12"/>
<field name="sequence" eval="16"/>
<field name="action" ref="action_mail_archives_feeds"/>
<field name="parent_id" ref="mail_feeds"/>
<field name="parent_id" ref="mail.mail_feeds"/>
</record>
<record id="mail_sentfeeds" model="ir.ui.menu">
<field name="name">Sent</field>
<field name="sequence" eval="13"/>
<field name="sequence" eval="18"/>
<field name="action" ref="action_mail_sent_feeds"/>
<field name="parent_id" ref="mail_feeds"/>
<field name="parent_id" ref="mail.mail_feeds"/>
</record>
</data>
</openerp>
</openerp>

View File

@ -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,

View File

@ -11,7 +11,7 @@
</record>
<record id="mail_followers_read_own" model="ir.rule">
<field name="name">mail.followers: read its own entries</field>
<field name="name">mail.followers: read and write its own entries</field>
<field name="model_id" ref="model_mail_followers"/>
<field name="domain_force">[('partner_id', '=', user.partner_id.id)]</field>
<field name="perm_create" eval="False"/>

View File

@ -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;
}

View File

@ -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;
}

File diff suppressed because it is too large Load Diff

View File

@ -39,6 +39,7 @@ openerp_mail_followers = function(session, mail) {
this._check_visibility();
this.reinit();
this.bind_events();
this._super();
},
_check_visibility: function() {
@ -72,7 +73,7 @@ openerp_mail_followers = function(session, mail) {
target: 'new',
context: {
'default_res_model': self.view.dataset.model,
'default_res_id': self.view.datarecord.id
'default_res_id': self.view.datarecord.id,
},
}
self.do_action(action, {
@ -90,12 +91,6 @@ openerp_mail_followers = function(session, mail) {
});
},
set_value: function (value_) {
this._super(value_);
// TDE FIXME: render_value is never called... ask to niv
this.render_value();
},
render_value: function () {
this.reinit();
return this.fetch_followers(this.get("value"));
@ -187,7 +182,7 @@ openerp_mail_followers = function(session, mail) {
display_subtypes:function (data) {
var self = this;
var subtype_list_ul = this.$('.oe_subtypes');
var records = (data[this.view.datarecord.id] || data[null]).message_subtype_data;
var records = data[this.view.datarecord.id].message_subtype_data;
_(records).each(function (record, record_name) {
record.name = record_name;

View File

@ -11,7 +11,7 @@
<t t-name="mail.compose_message">
<div class="oe_mail_compose_textarea">
<img class="oe_mail_icon oe_mail_frame oe_left" alt="User img"/>
<div class="oe_mail_msg_content">
<div class="oe_msg_content">
<!-- contains the composition form -->
<!-- default content: old basic textarea -->
<div class="oe_mail_post_header">
@ -55,17 +55,12 @@
Template used to display attachments in a mail.message
-->
<t t-name="mail.thread.message.attachments">
<ul t-attf-class="oe_mail_msg_attachments #{widget.attachment_ids[0] and widget.options.thread.show_attachment_link?'':'oe_hidden'}">
<t t-foreach="widget.attachment_ids" t-as="attachment">
<ul t-attf-class="oe_msg_attachments #{widget.datasets.attachment_ids[0] and widget.options.thread.show_attachment_link?'':'oe_hidden'}">
<t t-foreach="widget.datasets.attachment_ids" t-as="attachment">
<li>
<span t-if="(attachment.upload or attachment.percent_loaded&lt;100)" t-attf-title="{(attachment.name || attachment.filename) + (attachment.date?' \n('+attachment.date+')':'' )}" t-attf-name="{attachment.name || attachment.filename}">
<div class="oe_upload_in_process">
<span>Upload in progress...</span>
<div t-attf-style="{attachment.percent_loaded&gt;0?'':'display:none;'}"/>
<div t-attf-style="{attachment.percent_loaded&gt;20?'':'display:none;'}"/>
<div t-attf-style="{attachment.percent_loaded&gt;40?'':'display:none;'}"/>
<div t-attf-style="{attachment.percent_loaded&gt;60?'':'display:none;'}"/>
<div t-attf-style="{attachment.percent_loaded&gt;80?'':'display:none;'}"/>
<span>...Upload in progress...</span>
</div>
<t t-raw="attachment.name || attachment.filename"/>
</span>
@ -94,14 +89,14 @@
-->
<t t-name="mail.thread.list_recipients">
<div class="oe_mail_list_recipients">
Post to:
<span t-if="widget.context.default_res_id and widget.context.default_res_id" class="oe_all_follower">All Followers</span>
<t t-if="!widget.context.default_res_id and widget.context.default_res_id and widget.partner_ids.length"> and </t>
Post to:
<span t-if="!widget.datasets.is_private" class="oe_all_follower">All Followers</span>
<t t-if="!widget.datasets.is_private and widget.datasets.partner_ids.length"> and </t>
<t t-set="inc" t-value="0"/>
<t t-if="widget.partner_ids.length" t-foreach="widget.partner_ids" t-as="partner"><span t-attf-class="oe_partner_follower #{inc>=3?'oe_hidden':''}"><t t-if="inc" t-raw="', '"/><a t-attf-href="#model=res.partner&amp;id=#{partner[0]}"><t t-raw="partner[1]"/></a></span><t t-set="inc" t-value="inc+1"/>
<t t-if="widget.datasets.partner_ids.length" t-foreach="widget.datasets.partner_ids" t-as="partner"><span t-attf-class="oe_partner_follower #{inc>=3?'oe_hidden':''}"><t t-if="inc" t-raw="', '"/><a t-attf-href="#model=res.partner&amp;id=#{partner[0]}"><t t-raw="partner[1]"/></a></span><t t-set="inc" t-value="inc+1"/>
</t>
<t t-if="widget.partner_ids.length>=3">
<span class="oe_more">, <a><t t-raw="widget.partner_ids.length-3"/> others...</a></span>
<t t-if="widget.datasets.partner_ids.length>=3">
<span class="oe_more">, <a><t t-raw="widget.datasets.partner_ids.length-3"/> others...</a></span>
<a class="oe_more_hidden">&lt;&lt;&lt;</a>
</t>
</div>
@ -111,8 +106,7 @@
wall main template
Template used to display the communication history in the wall.
-->
<div t-name="mail.wall" class="oe_view_manager oe_mail_wall oe_view_manag
er_current">
<div t-name="mail.wall" class="oe_view_manager oe_mail_wall oe_view_manager_current">
<table class="oe_view_manager_header">
<colgroup>
<col width="33%"/>
@ -186,59 +180,56 @@
</div>
<!-- default layout -->
<li t-name="mail.thread.message" t-attf-class="oe_mail oe_mail_thread_msg #{widget.to_read ?'oe_mail_unread':'oe_mail_read'}">
<div t-attf-class="oe_mail_msg_#{widget.type} oe_semantic_html_override">
<li t-name="mail.thread.message" t-attf-class="oe_mail oe_mail_thread_msg #{widget.datasets.to_read ?'oe_mail_unread':'oe_mail_read'}">
<div t-attf-class="oe_msg_#{widget.datasets.type} oe_semantic_html_override">
<!-- message actions (read/unread, reply, delete...) -->
<ul class="oe_header">
<li class="placeholder-mail-vote"><t t-call="mail.thread.message.vote"/></li>
<li t-if="!widget.options.thread.display_on_flat" title="Read" class="oe_read"><a class="oe_read oe_e">W</a></li>
<li t-if="!widget.options.thread.display_on_flat" title="Set back to unread" class="oe_unread"><a class="oe_unread oe_e">h</a></li>
<li title="Quick reply"><a class="oe_reply oe_e">)</a></li>
<t t-if="(widget.is_author and widget.options.message.show_dd_delete) or widget.type == 'email'">
<li class="placeholder-mail-star"><t t-call="mail.thread.message.star"/></li>
<li t-if="widget.datasets.show_read_unread" title="Read" class="oe_read"><a class="oe_read oe_e">W</a></li>
<li t-if="widget.datasets.show_read_unread" title="Set back to unread" class="oe_unread"><a class="oe_unread oe_e">h</a></li>
<li title="Quick reply" t-if="widget.datasets.show_reply"><a class="oe_reply oe_e">)</a></li>
<t t-if="(widget.datasets.is_author and widget.options.message.show_dd_delete) or widget.datasets.type == 'email'">
<li>
<span class="oe_dropdown_toggle">
<a class="oe_e" title="More options">í</a>
<ul class="oe_dropdown_menu">
<li t-if="widget.is_author and widget.options.message.show_dd_delete"><a class="oe_mail_msg_delete">Delete</a></li>
<!-- Uncomment when adding subtype hiding
<li t-if="display['show_hide']">
<a href="#" class="oe_mail_msg_hide_type" t-attf-data-subtype='{widget.subtype}'>Hide '<t t-esc="widget.subtype"/>' for this document</a>
</li> -->
<li t-if="widget.type == 'email'"><a class="oe_mail_msg_details" t-attf-href="#model=mail.message&amp;id=#{widget.id}" >Details</a></li>
<li t-if="widget.datasets.is_author and widget.options.message.show_dd_delete"><a class="oe_msg_delete">Delete</a></li>
<li t-if="widget.datasets.type == 'email'"><a class="oe_msg_details" t-attf-href="#model=mail.message&amp;id=#{widget.datasets.id}" >Details</a></li>
</ul>
</span>
</li>
</t>
</ul>
<a t-attf-href="#model=res.partner&amp;id=#{widget.author_id[0]}" t-att-title="widget.author_id[1]">
<img class="oe_mail_icon oe_mail_frame oe_left" t-att-src="widget.avatar"/>
<a t-attf-href="#model=res.partner&amp;id=#{widget.datasets.author_id[0]}" t-att-title="widget.datasets.author_id[1]">
<img class="oe_mail_icon oe_mail_frame oe_left" t-att-src="widget.datasets.avatar"/>
</a>
<div class="oe_mail_msg_content">
<div class="oe_msg_content">
<!-- message itself -->
<div class="oe_mail_msg">
<h1 t-if="widget.subject" class="oe_mail_msg_title">
<t t-raw="widget.subject"/>
<h1 t-if="widget.datasets.subject" class="oe_msg_title">
<t t-raw="widget.datasets.subject"/>
</h1>
<ul class="oe_mail_msg_footer">
<li t-if="widget.author_id"><a t-attf-href="#model=res.partner&amp;id=#{widget.author_id[0]}"><t t-raw="widget.author_id[1]"/></a></li>
<li><span t-att-title="widget.date"><t t-raw="widget.timerelative"/></span></li>
<li t-if="widget.attachment_ids.length > 0">
<a class="oe_mail_msg_view_attachments">
<t t-if="widget.attachment_ids.length == 1">1 Attachment</t>
<t t-if="widget.attachment_ids.length > 1"><t t-raw="widget.attachment_ids.length"/> Attachments</t>
<ul class="oe_msg_footer">
<li t-if="widget.datasets.author_id"><a t-attf-href="#model=res.partner&amp;id=#{widget.datasets.author_id[0]}"><t t-raw="widget.datasets.author_id[1]"/></a></li>
<li><span t-att-title="widget.datasets.date"><t t-raw="widget.datasets.timerelative"/></span></li>
<li t-if="widget.datasets.attachment_ids.length > 0">
<a class="oe_msg_view_attachments">
<t t-if="widget.datasets.attachment_ids.length == 1">1 Attachment</t>
<t t-if="widget.datasets.attachment_ids.length > 1"><t t-raw="widget.datasets.attachment_ids.length"/> Attachments</t>
</a>
</li>
</ul>
<div class="oe_clear"/>
<div class="oe_mail_msg_body">
<t t-if="widget.options.message.show_record_name and widget.record_name and (!widget.subject) and !widget.options.thread.thread_level and !widget.options.thread.display_on_flat and widget.model!='res.partner'">
<a class="oe_mail_action_model" t-attf-href="#model=#{widget.model}&amp;id=#{widget.res_id}"><t t-raw="widget.record_name"/></a>
<div class="oe_msg_body">
<t t-if="widget.options.message.show_record_name and widget.datasets.record_name and (!widget.datasets.subject) and !widget.options.thread.thread_level and !widget.options.thread.display_on_thread[0] and widget.datasets.model!='res.partner'">
<a class="oe_mail_action_model" t-attf-href="#model=#{widget.datasets.model}&amp;id=#{widget.res_id}"><t t-raw="widget.datasets.record_name"/></a>
</t>
<t t-raw="widget.body"/>
<t t-raw="widget.datasets.body"/>
</div>
<t t-if="widget.attachment_ids.length > 0">
<t t-if="widget.datasets.attachment_ids.length > 0">
<div class="oe_clear"></div>
<t t-call="mail.thread.message.attachments"/>
</t>
@ -250,30 +241,40 @@
<!-- expandable message layout -->
<li t-name="mail.thread.expandable" class="oe_mail oe_mail_thread_msg oe_mail_unread">
<div t-attf-class="oe_mail_msg_#{widget.type} oe_semantic_html_override">
<div class="oe_mail_msg_content oe_mail_msg_more_message">
<a class="oe_mail_fetch_more">Load more messages <span t-if="widget.nb_messages>0">(<t t-raw="widget.nb_messages"/> messages not display)</span>...</a>
<div t-attf-class="oe_msg_#{widget.datasets.type} oe_semantic_html_override">
<div class="oe_msg_content oe_msg_more_message">
<a class="oe_mail_fetch_more">Load more messages <span t-if="widget.datasets.nb_messages>0">(<t t-raw="widget.datasets.nb_messages"/> messages not display)</span>...</a>
</div>
</div>
</li>
<!--
mail.compose_message.button_top_bar
render of the button on the user bar for open wizard compose message
-->
<t t-name="mail.compose_message.button_top_bar">
<div class="oe_topbar_compose_full_email">
<button class="oe_button oe_highlight">Write an email</button>
</div>
</t>
<!-- mail.thread.message.vote
Template used to display Like/Unlike in a mail.message
-->
<span t-name="mail.thread.message.vote">
<span class="oe_left oe_mail_vote_count">
<t t-if='widget.has_voted'>
<t t-if='widget.datasets.has_voted'>
You
</t>
<t t-if='(widget.vote_user_ids.length-(widget.has_voted?1:0)) > 0'>
<t t-if='widget.has_voted'> and </t>
<t t-esc="widget.vote_user_ids.length"/> people
<t t-if='(widget.datasets.vote_user_ids.length-(widget.datasets.has_voted?1:0)) > 0'>
<t t-if='widget.datasets.has_voted'> and </t>
<t t-esc="widget.datasets.vote_user_ids.length"/> people
</t>
<t t-if='widget.vote_user_ids.length > 0'>
<t t-if='widget.datasets.vote_user_ids.length > 0'>
agree
</t>
</span>
<button t-attf-class="oe_mail_msg_vote oe_tag">
<button t-attf-class="oe_msg_vote oe_tag">
<span>
<t t-if="!widget.has_voted">Agree</t>
<t t-if="widget.has_voted">Undo</t>
@ -281,4 +282,13 @@
</button>
</span>
<!-- mail.thread.message.star
Template used to display stared/unstared message in a mail.message
-->
<span t-name="mail.thread.message.star">
<span class="oe_left">
<button t-attf-class="oe_mail_starbox oe_tag #{widget.datasets.is_favorite?'oe_stared':''}">*</button>
</span>
</span>
</template>

View File

@ -520,6 +520,10 @@ class test_mail(TestMailMockups):
self.assertEqual(message2.subject, _subject, 'mail.message subject incorrect')
self.assertEqual(message2.body, group_bird.description, 'mail.message body incorrect')
def test_30_message_read(self):
""" Tests for message_read and expandables. """
self.assertTrue(1 == 1, 'Test not implemented, do not replace by return True')
def test_40_needaction(self):
""" Tests for mail.message needaction. """
cr, uid, user_admin, group_pigs = self.cr, self.uid, self.user_admin, self.group_pigs
@ -577,11 +581,19 @@ class test_mail(TestMailMockups):
reply_msg = MAIL_TEMPLATE.format(to='Pretty Pigs <group+pigs@example.com>, other@gmail.com', subject='Re: 1',
extra='In-Reply-To: %s' % msg1.message_id)
self.mail_group.message_process(cr, uid, None, reply_msg)
# TDE note: temp various asserts because of the random bug about msg1.child_ids
msg_ids = self.mail_message.search(cr, uid, [('model', '=', 'mail.group'), ('res_id', '=', self.group_pigs_id)], limit=1)
new_msg = self.mail_message.browse(cr, uid, msg_ids[0])
self.assertEqual(new_msg.parent_id, msg1, 'Newly processed mail_message (%d) should have msg1 as parent' % (new_msg.id))
# 2. References header
reply_msg2 = MAIL_TEMPLATE.format(to='Pretty Pigs <group+pigs@example.com>, other@gmail.com', subject='Re: Re: 1',
extra='References: <2233@a.com>\r\n\t<3edss_dsa@b.com> %s' % msg1.message_id)
self.mail_group.message_process(cr, uid, None, reply_msg2)
# TDE note: temp various asserts because of the random bug about msg1.child_ids
msg_ids = self.mail_message.search(cr, uid, [('model', '=', 'mail.group'), ('res_id', '=', self.group_pigs_id)], limit=1)
new_msg = self.mail_message.browse(cr, uid, msg_ids[0])
self.assertEqual(new_msg.parent_id, msg1, 'Newly processed mail_message should have msg1 as parent')
# 3. Subject contains [<ID>] + model passed to message+process -> only attached to group, not to mail
reply_msg3 = MAIL_TEMPLATE.format(to='Pretty Pigs <group+pigs@example.com>, other@gmail.com',
@ -591,9 +603,15 @@ class test_mail(TestMailMockups):
group_pigs.refresh()
msg1.refresh()
self.assertEqual(5, len(group_pigs.message_ids), 'group should contain 5 messages')
# TDE note: python test + debug because of the random error we see with the next assert
if len(msg1.child_ids) != 2:
msg_ids = self.mail_message.search(cr, uid, [('model', '=', 'mail.group'), ('res_id', '=', self.group_pigs_id)], limit=10)
for new_msg in self.mail_message.browse(cr, uid, msg_ids):
print new_msg.subject, '(id', new_msg.id, ')', 'parent_id:', new_msg.parent_id
print '\tchild_ids', [child.id for child in new_msg.child_ids]
self.assertEqual(2, len(msg1.child_ids), 'msg1 should have 2 children now')
def test_60_vote(self):
def test_60_message_vote(self):
""" Test designed for the vote/unvote feature. """
cr, uid = self.cr, self.uid
user_admin = self.res_users.browse(cr, uid, uid)
@ -613,7 +631,7 @@ class test_mail(TestMailMockups):
# Test: msg1 has Admin as voter
self.assertEqual(set(msg1.vote_user_ids), set([user_admin]), 'after voting, Admin is not the voter')
# Do: Bert vote for msg1
self.mail_message.vote_toggle(cr, uid, [msg1.id], [user_bert_id])
self.mail_message.vote_toggle(cr, user_bert_id, [msg1.id])
msg1.refresh()
# Test: msg1 has Admin and Bert as voters
self.assertEqual(set(msg1.vote_user_ids), set([user_admin, user_bert]), 'after voting, Admin and Bert are not the voters')
@ -622,3 +640,7 @@ class test_mail(TestMailMockups):
msg1.refresh()
# Test: msg1 has Bert as voter
self.assertEqual(set(msg1.vote_user_ids), set([user_bert]), 'after unvoting for Admin, Bert is not the voter')
def test_70_message_favorite(self):
""" Tests for favorites. """
self.assertTrue(1 == 1, 'Test not implemented, do not replace by return True')

View File

@ -6,14 +6,14 @@
<field name="model">mail.compose.message</field>
<field name="arch" type="xml">
<form string="Compose Email" version="7.0">
<!-- truly invisible fields for control and options -->
<field name="composition_mode" nolabel="1" invisible="0"/>
<field name="model" nolabel="1" invisible="0"/>
<field name="res_id" nolabel="1" invisible="0"/>
<field name="parent_id" nolabel="1" invisible="0"/>
<field name="content_subtype" nolabel="1" invisible="0"/>
<!-- visible wizard -->
<group>
<!-- truly invisible fields for control and options -->
<field name="composition_mode" invisible="1"/>
<field name="model" invisible="1"/>
<field name="res_id" invisible="1"/>
<field name="parent_id" invisible="1"/>
<field name="content_subtype" invisible="1"/>
<!-- visible wizard -->
<field name="subject" placeholder="Subject..."
attrs="{'invisible':[('content_subtype', '=', 'plain')]}"/>/>
<field name="partner_ids" widget="many2many_tags" placeholder="Add contacts to notify..."

View File

@ -110,7 +110,7 @@ class procurement_order(osv.osv):
def production_order_create_note(self, cr, uid, ids, context=None):
for procurement in self.browse(cr, uid, ids, context=context):
body = _("Manufacturing Order created.")
body = _("Manufacturing Order <em>%s</em> created.") % ( procurement.production_id.name,)
self.message_post(cr, uid, [procurement.id], body=body, context=context)
procurement_order()