diff options
| author | Atul Patel (OpenERP) <atp@tinyerp.com> | 2012-08-16 10:50:21 +0530 |
|---|---|---|
| committer | Atul Patel (OpenERP) <atp@tinyerp.com> | 2012-08-16 10:50:21 +0530 |
| commit | 22a810507cc6b4e48a067b6baaf9869d72e5a38e (patch) | |
| tree | 1808e0671b83dec971ae431b6ab1f09cfbfb44e8 /addons/mail | |
| parent | 1d2168a02ebeeb7441c9171bd9dcda8226d29cdc (diff) | |
| parent | f4f7de136921528f4b465629ff756b367ee0bba7 (diff) | |
[MERGE]: Merged with lp:openobject-addons
bzr revid: atp@tinyerp.com-20120816052021-igdj7knaimwg9x7z
Diffstat (limited to 'addons/mail')
32 files changed, 1007 insertions, 654 deletions
diff --git a/addons/mail/__init__.py b/addons/mail/__init__.py index 4cf2caf8804..2669eb9f269 100644 --- a/addons/mail/__init__.py +++ b/addons/mail/__init__.py @@ -21,16 +21,18 @@ import mail_alias import mail_message +import mail_followers import mail_thread import mail_group import mail_subscription import mail_vote import ir_needaction -import res_users import res_partner +import res_users import report import wizard import res_config +import mail_group_menu # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: diff --git a/addons/mail/__openerp__.py b/addons/mail/__openerp__.py index fea3d603f6a..0e1caabcf65 100644 --- a/addons/mail/__openerp__.py +++ b/addons/mail/__openerp__.py @@ -64,7 +64,7 @@ The main features of the module are: 'wizard/mail_compose_message_view.xml', 'res_config_view.xml', 'mail_message_view.xml', - 'mail_subscription_view.xml', + 'mail_followers_view.xml', 'mail_thread_view.xml', 'mail_group_view.xml', 'res_partner_view.xml', @@ -102,9 +102,11 @@ The main features of the module are: 'js': [ 'static/lib/jquery.expander/jquery.expander.js', 'static/src/js/mail.js', + 'static/src/js/mail_followers.js', ], 'qweb': [ 'static/src/xml/mail.xml', + 'static/src/xml/mail_followers.xml', ], } # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: diff --git a/addons/mail/data/mail_demo.xml b/addons/mail/data/mail_demo.xml index 8be63f22804..e3d7c2ab1e1 100644 --- a/addons/mail/data/mail_demo.xml +++ b/addons/mail/data/mail_demo.xml @@ -2,49 +2,22 @@ <openerp> <data noupdate="1"> - <record id="message_blogpost0_attachment0" model="ir.attachment"> - <field name="name">A cool attachment</field> - <field name="datas">PT1Tb21lIGNvb2wgYXR0YWNobWVudD09ClByYWVzZW50IHZlbCBtYXNzYSBzZWQgbWFzc2EgY29uc2VxdWF0IGVnZXN0YXMgaW4gdHJpc3RpcXVlIG9yY2ku</field> - <field name="datas_fname">a_cool_attachment</field> - <field name="description"></field> - <field name="res_name">Mail group</field> - <field name="res_model">mail.group</field> - <field name="res_id" ref="group_all_company"/> - <field name="type">binary</field> - </record> - - <record id="message_blogpost0_attachment1" model="ir.attachment"> - <field name="name">Another clean attachment</field> - <field name="datas">PT1Tb21lIGNsZWFuIGF0dGFjaG1lbnQ9PQpOdWxsYSB0dXJwaXMgbGVvLCByaG9uY3VzIHV0IGVnZXN0YXMgc2l0IGFtZXQsIGNvbnNlY3RldHVyIHZpdGFlIHVybmEu</field> - <field name="datas_fname">a_clean_attachment</field> - <field name="description"></field> - <field name="res_name">Mail group</field> - <field name="res_model">mail.group</field> - <field name="res_id" ref="group_all_company"/> - <field name="type">binary</field> - </record> - <record id="message_blogpost0" model="mail.message"> <field name="subject">Internal company announce</field> <field name="model">mail.group</field> - <field name="res_id" ref="group_all_company"/> + <field name="res_id" ref="group_all_employees"/> <field name="content_subtype">html</field> - <field name="body_html"><![CDATA[Lorem ipsum dolor <b>sit amet</b>, consectetur <em>adipiscing elit</em>. Pellentesque et quam sapien, in sagittis tellus. -Praesent vel massa sed massa consequat egestas in tristique orci. Praesent iaculis libero et neque vehicula iaculis. Vivamus placerat tincidunt orci ac ornare. Proin ut dolor fringilla velit ultricies consequat. Maecenas sit amet ipsum non leo interdum imperdiet. Donec sapien mi, varius a consequat id, consectetur sit amet nulla. - -Fusce tempus elit volutpat mi auctor adipiscing. Nam congue luctus suscipit. Duis vestibulum dapibus enim vitae dignissim. Sed tellus libero, venenatis ut mollis ut, luctus quis dui. Sed rhoncus pulvinar orci in consectetur. - -Nulla turpis leo, rhoncus ut egestas sit amet, consectetur vitae urna. Mauris in dolor in sapien tempus vehicula.]]></field> + <field name="body_html"><![CDATA[Your monthly meal vouchers arrived. You can get them at Christine's office. +This month you also have 250 EUR of eco-checks for all employees that worked with us since 1 year minimum.]]></field> <field name="type">comment</field> <field name="user_id" ref="base.user_root"/> - <field name="attachment_ids" eval="[(6,0,[ref('message_blogpost0_attachment0'),ref('message_blogpost0_attachment1')])]"/> </record> <record id="message_blogpost0_comment0" model="mail.message"> <field name="model">mail.group</field> - <field name="res_id" ref="group_all_company"/> + <field name="res_id" ref="group_all_employees"/> <field name="content_subtype">html</field> - <field name="body_html"><![CDATA[That was such a <b>tremendous</b> blogpost ! (first comment)]]></field> + <field name="body_html"><![CDATA[Great.]]></field> <field name="parent_id" ref="message_blogpost0"/> <field name="type">comment</field> <field name="user_id" ref="base.user_root"/> @@ -52,29 +25,17 @@ Nulla turpis leo, rhoncus ut egestas sit amet, consectetur vitae urna. Mauris in <record id="message_blogpost0_comment1" model="mail.message"> <field name="model">mail.group</field> - <field name="res_id" ref="group_all_company"/> + <field name="res_id" ref="group_all_employees"/> <field name="content_subtype">html</field> - <field name="body_html"><![CDATA[Agreed ! -Would it be possible to learn more about the author ? (second comment)]]></field> + <field name="body_html"><![CDATA[Yes, that's a good news.]]></field> <field name="parent_id" ref="message_blogpost0"/> <field name="type">comment</field> <field name="user_id" ref="base.user_demo"/> </record> - <record id="message_blogpost0_comment2_attachment0" model="ir.attachment"> - <field name="name">Author's full biography</field> - <field name="datas">PT1BYm91dCB0aGUgYXV0aG9yPT0KSW50ZWdlciBzb2RhbGVzIGRvbG9yIGV1IGVsaXQgZnJpbmdpbGxhIGJsYW5kaXQu</field> - <field name="datas_fname">blogpost_author_biography</field> - <field name="description"></field> - <field name="res_name">Mail group</field> - <field name="res_model">mail.group</field> - <field name="res_id" ref="group_all_company"/> - <field name="type">binary</field> - </record> - <record id="message_blogpost0_comment2" model="mail.message"> <field name="model">mail.group</field> - <field name="res_id" ref="group_all_company"/> + <field name="res_id" ref="group_all_employees"/> <field name="content_subtype">html</field> <field name="body_html"><![CDATA[Sure: Curabitur tempor bibendum diam, et euismod ante rutrum vel. @@ -85,7 +46,6 @@ Check the file in attachment for more information ! (third comment)]]></field> <field name="parent_id" ref="message_blogpost0"/> <field name="type">comment</field> <field name="user_id" ref="base.user_root"/> - <field name="attachment_ids" eval="[(4,ref('message_blogpost0_comment2_attachment0'))]"/> </record> <record model="ir.config_parameter" id="user_mail_alias"> diff --git a/addons/mail/data/mail_group_data.xml b/addons/mail/data/mail_group_data.xml index 09a6ca29a2d..2152706d056 100644 --- a/addons/mail/data/mail_group_data.xml +++ b/addons/mail/data/mail_group_data.xml @@ -2,16 +2,14 @@ <openerp> <data noupdate="1"> - <record model="mail.group" id="group_all_company"> - <field name="name">All Company</field> - <field name="description">All company users can come here and discuss.</field> - </record> <record model="mail.group" id="group_sales"> <field name="name">Sales</field> + <field name="description">Discussion about best sales practices and deals.</field> </record> <record model="mail.group" id="group_all_employees"> <field name="name">All Employees</field> <field name="group_ids" eval="[(4, ref('base.group_user'))]"/> + <field name="description">Discussion about best sales practices and deals.</field> </record> <!-- notify all employees of module installation --> @@ -21,7 +19,7 @@ <value>Welcome to OpenERP!</value> <value>Your homepage is a summary of messages you received and key information about documents you follow. -The top menu bar contains all applications you installed. You can use this <i>Settings</i> menu to intall more applications, activate others features or give access to new users. +The top menu bar contains all applications you installed. You can use this <i>Settings</i> menu to install more applications, activate others features or give access to new users. To setup your preferences (name, email signature, avatar), click on the top right corner.</value> </function> diff --git a/addons/mail/doc/mail_openchatter_howto.rst b/addons/mail/doc/mail_openchatter_howto.rst index 4c8797e7f8b..789acdd87eb 100644 --- a/addons/mail/doc/mail_openchatter_howto.rst +++ b/addons/mail/doc/mail_openchatter_howto.rst @@ -45,7 +45,6 @@ Use the thread viewer widget inside your form view by using the mail_thread widg <record model="ir.ui.view" id="my_task_form_view"> <field name="name">My Task</field> <field name="model">my.task</field> - <field name="type">form</field> <field name="priority">1</field> <field name="arch" type="xml"> <form> diff --git a/addons/mail/mail_alias.py b/addons/mail/mail_alias.py index 807ed5dbe61..82b20f39480 100644 --- a/addons/mail/mail_alias.py +++ b/addons/mail/mail_alias.py @@ -20,9 +20,19 @@ ############################################################################## import re +import unicodedata from openerp.osv import fields, osv +from openerp.tools import ustr +# Inspired by http://stackoverflow.com/questions/517923 +def remove_accents(input_str): + """Suboptimal-but-better-than-nothing way to replace accented + latin letters by an ASCII equivalent. Will obviously change the + meaning of input_str and work only for some cases""" + input_str = ustr(input_str) + nkfd_form = unicodedata.normalize('NFKD', input_str) + return u''.join([c for c in nkfd_form if not unicodedata.combining(c)]) class mail_alias(osv.Model): """A Mail Alias is a mapping of an email address with a given OpenERP Document @@ -40,6 +50,7 @@ class mail_alias(osv.Model): _name = 'mail.alias' _description = "Email Aliases" _rec_name = 'alias_name' + _order = 'alias_model_id, alias_name' def _get_alias_domain(self, cr, uid, ids, name, args, context=None): ir_config_parameter = self.pool.get("ir.config_parameter") @@ -55,10 +66,9 @@ class mail_alias(osv.Model): "corresponds. Any incoming email that does not reply to an " "existing record will cause the creation of a new record " "of this model (e.g. a Project Task)", - # only allow selecting mail_thread models! - #TODO kept doamin temporarily in comment, need to redefine domain - #domain="[('field_id', 'in', 'message_ids')]" - ), + # hack to only allow selecting mail_thread models (we might + # (have a few false positives, though) + domain="[('field_id.name', '=', 'message_ids')]"), 'alias_user_id': fields.many2one('res.users', 'Owner', help="The owner of records created upon receiving emails on this alias. " "If this field is not set the system will attempt to find the right owner " @@ -76,7 +86,10 @@ class mail_alias(osv.Model): _defaults = { 'alias_defaults': '{}', - 'alias_user_id': lambda self,cr,uid, context: uid + 'alias_user_id': lambda self,cr,uid,context: uid, + + # looks better when creating new aliases - even if the field is informative only + 'alias_domain': lambda self,cr,uid,context: self._get_alias_domain(cr,1,[1],None,None)[1] } _sql_constraints = [ @@ -122,12 +135,10 @@ class mail_alias(osv.Model): make it unique, and the ``alias_model_id`` value will set to the model ID of the ``model_name`` value, if provided, """ - alias_name = re.sub(r'\W+', '_', vals['alias_name']).lower() + alias_name = re.sub(r'[^\w+]', '-', remove_accents(vals['alias_name'])).lower() alias_name = self._find_unique(cr, uid, alias_name, context=context) vals['alias_name'] = alias_name if model_name: model_id = self.pool.get('ir.model').search(cr, uid, [('model', '=', model_name)], context=context)[0] vals['alias_model_id'] = model_id return self.create(cr, uid, vals, context=context) - - diff --git a/addons/mail/mail_alias_view.xml b/addons/mail/mail_alias_view.xml index 61a89b774ca..e0d8e173ebc 100644 --- a/addons/mail/mail_alias_view.xml +++ b/addons/mail/mail_alias_view.xml @@ -6,19 +6,16 @@ <record model="ir.ui.view" id="view_mail_alias_form"> <field name="name">mail.alias.form</field> <field name="model">mail.alias</field> - <field name="type">form</field> <field name="arch" type="xml"> <form string="Alias" version="7.0"> <sheet> <label for="alias_name" class="oe_edit_only"/> - <h2><field name="alias_name" class="oe_inline"/>@<field name="alias_domain" class="oe_inline"/></h2> - <group colspan="4" col="4"> - <field name="alias_model_id" readonly="1"/> - <field name="alias_user_id" readonly="1"/> - <field name="alias_force_thread_id" readonly="1"/> - <newline/> - <separator string="Default Values" colspan="4"/> - <field name="alias_defaults" colspan="4" nolabel="1" readonly="1"/> + <h2><field name="alias_name" class="oe_inline"/>@<field name="alias_domain" class="oe_inline"/></h2> + <group> + <field name="alias_model_id"/> + <field name="alias_user_id"/> + <field name="alias_force_thread_id"/> + <field name="alias_defaults"/> </group> </sheet> </form> @@ -29,7 +26,6 @@ <record model="ir.ui.view" id="view_mail_alias_tree"> <field name="name">mail.alias.tree</field> <field name="model">mail.alias</field> - <field name="type">tree</field> <field name="arch" type="xml"> <tree string="Alias"> <field name="alias_name"/> @@ -44,7 +40,6 @@ <record model="ir.ui.view" id="view_mail_alias_search"> <field name="name">mail.alias.search</field> <field name="model">mail.alias</field> - <field name="type">search</field> <field name="arch" type="xml"> <search string="Search Alias"> <field name="alias_name"/> diff --git a/addons/mail/mail_subscription.py b/addons/mail/mail_followers.py index fc82af879a9..fcdc78d5f0a 100644 --- a/addons/mail/mail_subscription.py +++ b/addons/mail/mail_followers.py @@ -22,18 +22,21 @@ from osv import osv from osv import fields -class mail_subscription(osv.osv): - """ - mail_subscription holds the data related to the follow mechanism inside OpenERP. +class mail_followers(osv.Model): + """ mail_followers holds the data related to the follow mechanism inside + OpenERP. Users can choose to follow documents (records) of any kind that + inherits from mail.thread. Following documents allow to receive + notifications for new messages. A subscription is characterized by: :param: res_model: model of the followed objects :param: res_id: ID of resource (may be 0 for every objects) :param: user_id: user_id of the follower """ - _name = 'mail.subscription' + _name = 'mail.followers' _rec_name = 'id' + _log_access = False _order = 'res_model asc' - _description = 'Mail subscription' + _description = 'Mail Document Followers' _columns = { 'res_model': fields.char('Related Document Model', size=128, required=True, select=1, @@ -44,10 +47,8 @@ class mail_subscription(osv.osv): ondelete='cascade', required=True, select=1), } -class mail_notification(osv.osv): - """ - mail_notification is a relational table modeling messages pushed to users. - :param: read: not used currently +class mail_notification(osv.Model): + """ mail_notification is a relational table modeling messages pushed to users. """ _name = 'mail.notification' _rec_name = 'id' @@ -59,9 +60,4 @@ class mail_notification(osv.osv): ondelete='cascade', required=True, select=1), 'message_id': fields.many2one('mail.message', string='Message', ondelete='cascade', required=True, select=1), - 'read': fields.boolean('Read', help="Not used currently",), - # TODO: add a timestamp ? or use message date ? - } - _defaults = { - 'read': False, } diff --git a/addons/mail/mail_subscription_view.xml b/addons/mail/mail_followers_view.xml index c3d79d48308..62e93430675 100644 --- a/addons/mail/mail_subscription_view.xml +++ b/addons/mail/mail_followers_view.xml @@ -2,17 +2,14 @@ <openerp> <data> - <!-- - SUBSCRIPTION - !--> - - <record model="ir.ui.view" id="view_subscription_tree"> - <field name="name">mail.subscription.tree</field> - <field name="model">mail.subscription</field> + <!-- FOLLOWERS !--> + <record model="ir.ui.view" id="view_followers_tree"> + <field name="name">mail.followers.tree</field> + <field name="model">mail.followers</field> <field name="type">tree</field> <field name="priority">10</field> <field name="arch" type="xml"> - <tree string="Subscription"> + <tree string="Followers"> <field name="res_model"/> <field name="res_id"/> <field name="user_id"/> @@ -20,27 +17,22 @@ </field> </record> - <!-- - NOTIFICATION - !--> - + <!-- NOTIFICATION !--> <record model="ir.ui.view" id="view_notification_tree"> <field name="name">mail.notification.tree</field> <field name="model">mail.notification</field> - <field name="type">tree</field> <field name="priority">10</field> <field name="arch" type="xml"> - <tree string="Subscription"> + <tree string="Notifications"> <field name="user_id"/> <field name="message_id"/> - <field name="read"/> </tree> </field> </record> - <record id="action_view_subscriptions" model="ir.actions.act_window"> - <field name="name">Subscriptions</field> - <field name="res_model">mail.subscription</field> + <record id="action_view_followers" model="ir.actions.act_window"> + <field name="name">Followers</field> + <field name="res_model">mail.followers</field> <field name="view_type">form</field> <field name="view_mode">tree,form</field> </record> @@ -52,9 +44,9 @@ <field name="view_mode">tree,form</field> </record> - <!-- Add subscriptions related menu entries in Settings/Email --> - <menuitem name="Subscriptions" id="menu_email_subscriptions" parent="base.menu_email" - action="action_view_subscriptions" sequence="30" groups="base.group_no_one"/> --> + <!-- Add followers related menu entries in Settings/Email --> + <menuitem name="Followers" id="menu_email_followers" parent="base.menu_email" + action="action_view_followers" sequence="30" groups="base.group_no_one"/> --> <!-- Add notifications related menu entry in Settings/Email --> <menuitem name="Notifications" id="menu_email_notifications" parent="base.menu_email" diff --git a/addons/mail/mail_group.py b/addons/mail/mail_group.py index b850f1c29f7..6a14fef83aa 100644 --- a/addons/mail/mail_group.py +++ b/addons/mail/mail_group.py @@ -25,28 +25,20 @@ import openerp.tools as tools from operator import itemgetter from osv import osv from osv import fields -import tools from tools.translate import _ -from lxml import etree -class mail_group(osv.osv): +class mail_group(osv.Model): """ A mail_group is a collection of users sharing messages in a discussion group. Group users are users that follow the mail group, using the subscription/follow mechanism of OpenSocial. A mail group has nothing in common with res.users.group. - Additional information on fields: - - ``member_ids``: user member of the groups are calculated with - ``message_get_subscribers`` method from mail.thread - - ``member_count``: calculated with member_ids - - ``is_subscriber``: calculated with member_ids - """ _description = 'Discussion group' _name = 'mail.group' _inherit = ['mail.thread'] - _inherits = {'mail.alias': 'alias_id'} + _inherits = {'mail.alias': 'alias_id', 'ir.ui.menu': 'menu_id'} def _get_image(self, cr, uid, ids, name, args, context=None): result = dict.fromkeys(ids, False) @@ -57,28 +49,8 @@ class mail_group(osv.osv): def _set_image(self, cr, uid, id, name, value, args, context=None): return self.write(cr, uid, [id], {'image': tools.image_resize_image_big(value)}, context=context) - def get_member_ids(self, cr, uid, ids, field_names, args, context=None): - if context is None: - context = {} - result = dict.fromkeys(ids) - for id in ids: - result[id] = {} - 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 - - def search_member_ids(self, cr, uid, obj, name, args, context=None): - if context is None: - context = {} - sub_obj = self.pool.get('mail.subscription') - sub_ids = sub_obj.search(cr, uid, ['&', ('res_model', '=', obj._name), ('user_id', '=', args[0][2])], context=context) - subs = sub_obj.read(cr, uid, sub_ids, context=context) - return [('id', 'in', map(itemgetter('res_id'), subs))] - - def get_last_month_msg_nbr(self, cr, uid, ids, name, args, context=None): + def _get_last_month_msg_nbr(self, cr, uid, ids, name, args, context=None): result = {} - message_obj = self.pool.get('mail.message') for id in ids: lower_date = (DT.datetime.now() - DT.timedelta(days=30)).strftime(tools.DEFAULT_SERVER_DATE_FORMAT) result[id] = self.message_search(cr, uid, [id], limit=None, domain=[('date', '>=', lower_date)], count=True, context=context) @@ -89,18 +61,21 @@ class mail_group(osv.osv): return tools.image_resize_image_big(open(image_path, 'rb').read().encode('base64')) _columns = { - 'name': fields.char('Name', size=64, required=True), 'description': fields.text('Description'), + 'menu_id': fields.many2one('ir.ui.menu', string='Related Menu', required=True, ondelete="cascade"), 'responsible_id': fields.many2one('res.users', string='Responsible', ondelete='set null', required=True, select=1, help="Responsible of the group that has all rights on the record."), - 'public': fields.boolean('Visible by non members', help='This group is visible by non members. \ - Invisible groups can add members through the invite button.'), + 'public': fields.selection([('public', 'Public'), ('private', 'Private'), ('groups', 'Selected Group Only')], + string='Privacy', required=True, + help='This group is visible by non members. '\ + 'Invisible groups can add members through the invite button.'), + 'group_public_id': fields.many2one('res.groups', string='Authorized Group'), 'group_ids': fields.many2many('res.groups', rel='mail_group_res_group_rel', - id1='mail_group_id', id2='groups_id', string='Linked groups', + id1='mail_group_id', id2='groups_id', string='Auto Subscription', help="Members of those groups will automatically added as followers. "\ - "Note that they will be able to manage their subscription manually "\ - "if necessary."), + "Note that they will be able to manage their subscription manually "\ + "if necessary."), 'image': fields.binary("Photo", help="This field holds the image used as photo for the "\ "user. The image is base64 encoded, and PIL-supported. "\ @@ -121,23 +96,28 @@ class mail_group(osv.osv): help="Small-sized photo of the group. It is automatically "\ "resized as a 50x50px image, with aspect ratio preserved. "\ "Use this field anywhere a small image is required."), - 'member_ids': fields.function(get_member_ids, fnct_search=search_member_ids, - type='many2many', relation='res.users', string='Group members', multi='get_member_ids'), - 'member_count': fields.function(get_member_ids, type='integer', - string='Member count', multi='get_member_ids'), - 'is_subscriber': fields.function(get_member_ids, type='boolean', - string='Joined', multi='get_member_ids'), - 'last_month_msg_nbr': fields.function(get_last_month_msg_nbr, type='integer', + 'last_month_msg_nbr': fields.function(_get_last_month_msg_nbr, type='integer', string='Messages count for last month'), - 'alias_id': fields.many2one('mail.alias', 'Alias', ondelete="cascade", required=True, + 'alias_id': fields.many2one('mail.alias', 'Alias', ondelete="cascade", help="The email address associated with this group. New emails received will automatically " "create new topics."), } + def _get_default_employee_group(self, cr, uid, context=None): + ref = self.pool.get('ir.model.data').get_object_reference(cr, uid, 'base', 'group_user') + return ref and ref[1] or False + + def _get_menu_parent(self, cr, uid, context=None): + ref = self.pool.get('ir.model.data').get_object_reference(cr, uid, 'mail', 'mail_group_root') + return ref and ref[1] or False + _defaults = { - 'public': True, + 'public': 'groups', + 'group_public_id': _get_default_employee_group, 'responsible_id': (lambda s, cr, uid, ctx: uid), 'image': _get_default_image, + 'parent_id': _get_menu_parent, + 'alias_domain': False, # always hide alias during creation } def _subscribe_user_with_group_m2m_command(self, cr, uid, ids, group_ids_command, context=None): @@ -153,15 +133,34 @@ class mail_group(osv.osv): return self.message_subscribe(cr, uid, ids, user_ids, context=context) def create(self, cr, uid, vals, context=None): - alias_pool = self.pool.get('mail.alias') + mail_alias = self.pool.get('mail.alias') if not vals.get('alias_id'): - name = vals.get('alias_name') or vals['name'] - alias_id = alias_pool.create_unique_alias(cr, uid, - {'alias_name': "group_"+name}, - model_name=self._name, context=context) + vals.pop('alias_name', None) # prevent errors during copy() + alias_id = mail_alias.create_unique_alias(cr, uid, + # Using '+' allows using subaddressing for those who don't + # have a catchall domain setup. + {'alias_name': "group+"+vals['name']}, + model_name=self._name, context=context) vals['alias_id'] = alias_id + mail_group_id = super(mail_group, self).create(cr, uid, vals, context) - alias_pool.write(cr, uid, [vals['alias_id']], {"alias_force_thread_id": mail_group_id}, context) + + # Create client action for this group and link the menu to it + ref = self.pool.get('ir.model.data').get_object_reference(cr, uid, 'mail', 'action_mail_group_feeds') + if ref: + search_ref = self.pool.get('ir.model.data').get_object_reference(cr, uid, 'mail', 'view_message_search_wall') + params = { + 'search_view_id': search_ref and search_ref[1] or False, + 'domain': [('model','=','mail.group'),('res_id','=',mail_group_id)], + 'res_model': 'mail.group', + 'res_id': mail_group_id, + 'thread_level': 2 + } + cobj = self.pool.get('ir.actions.client') + newref = cobj.copy(cr, uid, ref[1], default={'params': str(params), 'name': vals['name']}, context=context) + self.write(cr, uid, [mail_group_id], {'action': 'ir.actions.client,'+str(newref), 'mail_group_id': mail_group_id}, context=context) + + mail_alias.write(cr, uid, [vals['alias_id']], {"alias_force_thread_id": mail_group_id}, context) if vals.get('group_ids'): self._subscribe_user_with_group_m2m_command(cr, uid, [mail_group_id], vals.get('group_ids'), context=context) @@ -186,3 +185,12 @@ class mail_group(osv.osv): def action_group_leave(self, cr, uid, ids, context=None): return self.message_unsubscribe(cr, uid, ids, context=context) + + # ---------------------------------------- + # OpenChatter methods and notifications + # ---------------------------------------- + + def message_get_monitored_follower_fields(self, cr, uid, ids, context=None): + """ Add 'responsible_id' to the monitored fields """ + res = super(mail_group, self).message_get_monitored_follower_fields(cr, uid, ids, context=context) + return res + ['responsible_id'] diff --git a/addons/mail/mail_group_menu.py b/addons/mail/mail_group_menu.py new file mode 100644 index 00000000000..8625285d640 --- /dev/null +++ b/addons/mail/mail_group_menu.py @@ -0,0 +1,52 @@ +# -*- 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 +from osv import fields +from tools.translate import _ + +class ir_ui_menu(osv.osv): + """ Override of ir.ui.menu class. When adding mail_thread module, each + new mail.group will create a menu entry. This overrides checks that + the current user is in the mail.group followers. If not, the menu + entry is taken off the list of menu ids. This way the user will see + menu entries for the mail.group he is following. + """ + _inherit = 'ir.ui.menu' + + _columns = { + 'mail_group_id': fields.many2one('mail.group', 'Mail Group') + } + + def search(self, cr, uid, args, offset=0, limit=None, order=None, context=None, count=False): + """ Override to take off menu entries (mail.group) the user is not + following. """ + ids = super(ir_ui_menu, self).search(cr, uid, args, offset=0, limit=None, order=order, context=context, count=False) + follower_obj = self.pool.get('mail.followers') + for menu in self.browse(cr, uid, ids, context=context): + if menu.mail_group_id: + sub_ids = follower_obj.search(cr, uid, [ + ('user_id', '=', uid), ('res_model', '=', 'mail.group'), + ('res_id', '=', menu.mail_group_id.id) + ], context=context) + if not sub_ids: + ids.remove(menu.id) + return ids diff --git a/addons/mail/mail_group_view.xml b/addons/mail/mail_group_view.xml index 5c81d426d6a..a71ec972326 100644 --- a/addons/mail/mail_group_view.xml +++ b/addons/mail/mail_group_view.xml @@ -1,15 +1,21 @@ <?xml version="1.0"?> <openerp> <data> + <record id="action_mail_group_feeds" model="ir.actions.client"> + <field name="name">Discussion Group</field> + <field name="tag">mail.wall</field> + <field name="params" eval="{'search_view_id': ref('view_message_search_wall')}"/> + </record> <!-- Group Kanban View !--> <record model="ir.ui.view" id="view_group_kanban"> <field name="name">mail.group.kanban</field> <field name="model">mail.group</field> - <field name="type">kanban</field> <field name="priority" eval="10"/> <field name="arch" type="xml"> <kanban> + <field name="message_follower_ids"/> + <field name="message_is_follower"/> <templates> <t t-name="kanban-description"> <div class="oe_group_description" t-if="record.description.raw_value"> @@ -17,17 +23,19 @@ </div> </t> <t t-name="kanban-box"> - <div t-attf-class="{record.is_subscriber.raw_value} oe_group_vignette"> + <div t-attf-class="{record.message_is_follower.raw_value} oe_group_vignette"> <div class="oe_group_image"> <a type="edit"><img t-att-src="kanban_image('mail.group', 'image_medium', record.id.value)" class="oe_group_photo" tooltip="kanban-description"/></a> </div> <div class="oe_group_details"> <h4><a type="edit"><field name="name"/></a></h4> - <span style="display: none;"><field name="is_subscriber"/></span> <ul> - <li><field name="member_count"/> members</li> - <li t-if="! record.is_subscriber.raw_value"><a name="action_group_join" string="Join" type="object" class="oe_group_join">Not following</a></li> - <li t-if="record.is_subscriber.raw_value"><a name="action_group_leave" string="Join" type="object" class="oe_group_leave">Following</a></li> + <!-- <li><field name="message_follower_count"/> member(s)</li> --> + <li> + <t t-raw="record.message_follower_ids.raw_value.length"/> member(s) + </li> + <li t-if="! record.message_is_follower.raw_value"><a name="action_group_join" string="Join" type="object" class="oe_group_join">Not following</a></li> + <li t-if="record.message_is_follower.raw_value"><a name="action_group_leave" string="Join" type="object" class="oe_group_leave">Following</a></li> <li><field name="last_month_msg_nbr"/> messages last month</li> </ul> </div> @@ -46,42 +54,42 @@ <record model="ir.ui.view" id="view_group_form"> <field name="name">mail.group.form</field> <field name="model">mail.group</field> - <field name="type">form</field> <field name="priority" eval="10"/> <field name="arch" type="xml"> <form string="Group Form" version="7.0"> <sheet class="openerp_mail_group_sheet"> - <group colspan="4" col="8"> - <group colspan="1" col="2"> - <field name="image_small" widget='image' class="oe_image_small" nolabel="1"/> + <field name="image_small" widget='image' class="oe_avatar oe_left"/> + <div class="oe_title"> + <div class="oe_edit_only"> + <label for="name" string="Group Name"/> + </div> + <h1><field name="name"/></h1> + <div name="alias_box" colspan="4" attrs="{'invisible': [('alias_domain', '=', False)]}"> + <field name="alias_id" invisible="1" required="0"/> + <label for="alias_name" class="oe_edit_only"/> + <field name="alias_name" nolabel="1" class="oe_inline" attrs="{'required': [('alias_id', '!=', False)]}"/>@<field name="alias_domain" nolabel="1" class="oe_inline"/> + </div> + <field name="description" placeholder="Topics discussed in this group..."/> + </div> + <div class="oe_clear"/> + <group col="1"> + <group class="oe_edit_only"> + <field name="public" class="oe_inline"/> + <field name="group_public_id" class="oe_inline" + attrs="{'invisible': [('public','<>','groups')], 'required': [('public','=','groups')]}" + /> + <field name="group_ids" widget="many2many_tags" class="oe_inline"/> </group> - <group colspan="7" col="6"> - <div class="oe_edit_only"> - <label for="name"/> - </div> - <div> - <h2><field name="name" colspan="4"/></h2> - <div name="alias_box" colspan="4" attrs="{'invisible': [('alias_domain', '=', False)]}" class="oe_edit_only"> - <field name="alias_id" invisible="1"/> - <label for="alias_name" class="oe_edit_only"/> - <field name="alias_name" nolabel="1" class="oe_inline" attrs="{'required': [('alias_id', '!=', False)]}"/>@<field name="alias_domain" nolabel="1" class="oe_inline"/> - </div> - </div> - <newline/> - <group colspan="4" col="4"> - <field name="description" colspan="4" nolabel="1"/> - <field name="group_ids" colspan="4" widget="many2many_tags" class="oe_edit_only"/> - </group> - <group colspan="2" col="2" class="oe_edit_only"> - <field name="responsible_id" colspan="2"/> - <field name="public" colspan="2"/> - </group> + <group class="oe_edit_only"> + <field name="responsible_id" class="oe_inline"/> </group> </group> </sheet> - <div class="oe_chatter oe_mail_group_footer"> + <div class="oe_chatter"> <field name="message_ids" widget="mail_thread" options='{"thread_level": 1}'/> + <field name="message_follower_ids" widget="mail_followers" + context="{'lapin': 'nouille'}" image="image_small"/> </div> </form> </field> @@ -91,11 +99,10 @@ <record model="ir.ui.view" id="view_group_tree"> <field name="name">mail.group.tree</field> <field name="model">mail.group</field> - <field name="type">tree</field> <field name="priority" eval="10"/> <field name="arch" type="xml"> <tree string="Groups"> - <field name="name"/> + <field name="name" string="Group Name"/> <field name="responsible_id"/> </tree> </field> @@ -105,7 +112,6 @@ <record model="ir.ui.view" id="view_group_search"> <field name="name">mail.group.search</field> <field name="model">mail.group</field> - <field name="type">search</field> <field name="priority" eval="10"/> <field name="arch" type="xml"> <search string="Search Groups"> @@ -125,6 +131,7 @@ </record> <!-- left-side menu: Groups !--> - <menuitem id="mail_allgroups" name="All Groups" parent="mail_feeds" sequence="12" action="action_view_groups"/> + <menuitem id="mail_group_root" parent="mail_feeds_main" sequence="12" name="My Groups"/> + <menuitem id="mail_allgroups" parent="mail_feeds" sequence="12" action="action_view_groups"/> </data> </openerp> diff --git a/addons/mail/mail_message.py b/addons/mail/mail_message.py index 587c7485d19..9fab2e180bb 100644 --- a/addons/mail/mail_message.py +++ b/addons/mail/mail_message.py @@ -93,6 +93,9 @@ class mail_message_common(osv.TransientModel): return result def name_get(self, cr, uid, ids, context=None): + # name_get may receive int id instead of an id list + if isinstance(ids, (int, long)): + ids = [ids] res = [] for message in self.browse(cr, uid, ids, context=context): name = '' @@ -129,7 +132,7 @@ class mail_message_common(osv.TransientModel): help="Type of message, usually 'html' or 'plain', used to select "\ "plain-text or rich-text contents accordingly"), 'body_text': fields.text('Text Contents', help="Plain-text version of the message"), - 'body_html': fields.text('Rich-text Contents', help="Rich-text/HTML version of the message"), + 'body_html': fields.html('Rich-text Contents', help="Rich-text/HTML version of the message"), 'body': fields.function(get_body, fnct_search = search_body, type='text', string='Message Content', store=True, help="Content of the message. This content equals the body_text field "\ @@ -208,12 +211,13 @@ class mail_message(osv.Model): 'partner_id': fields.many2one('res.partner', 'Related partner', help="Deprecated field. Use partner_ids instead."), 'partner_ids': fields.many2many('res.partner', - 'mail_message_destination_partner_rel', + 'mail_message_res_partner_rel', 'message_id', 'partner_id', 'Destination partners', help="When sending emails through the social network composition wizard"\ "you may choose to send a copy of the mail to partners."), 'user_id': fields.many2one('res.users', 'Related User', readonly=1), - 'attachment_ids': fields.many2many('ir.attachment', 'message_attachment_rel', 'message_id', 'attachment_id', 'Attachments'), + 'attachment_ids': fields.many2many('ir.attachment', 'message_attachment_rel', + 'message_id', 'attachment_id', 'Attachments'), 'mail_server_id': fields.many2one('ir.mail_server', 'Outgoing mail server', readonly=1), 'state': fields.selection([ ('outgoing', 'Outgoing'), @@ -391,7 +395,8 @@ class mail_message(osv.Model): } email_msg_id = self.create(cr, uid, msg_vals, context) attachment_ids = [] - for fname, fcontent in attachments.iteritems(): + for attachment in attachments: + fname, fcontent = attachment attachment_data = { 'name': fname, 'datas_fname': fname, diff --git a/addons/mail/mail_message_view.xml b/addons/mail/mail_message_view.xml index b967035bec3..0aa5261d7e7 100644 --- a/addons/mail/mail_message_view.xml +++ b/addons/mail/mail_message_view.xml @@ -5,7 +5,6 @@ <record model="ir.ui.view" id="view_message_tree_short"> <field name="name">mail.message.tree.short</field> <field name="model">mail.message</field> - <field name="type">tree</field> <field name="priority">20</field> <field name="arch" type="xml"> <tree string="Messages"> @@ -22,7 +21,6 @@ <record model="ir.ui.view" id="view_message_form_short"> <field name="name">mail.message.form.short</field> <field name="model">mail.message</field> - <field name="type">form</field> <field name="priority">20</field> <field name="arch" type="xml"> <form string="Message" version="7.0"> @@ -44,7 +42,7 @@ </group> <notebook> <page string="Body (Rich)"> - <field name="body_html" widget="text_html"/> + <field name="body_html"/> </page> <page string="Body (Plain)"> <field name="body_text" widget="text"/> @@ -59,7 +57,6 @@ <record model="ir.ui.view" id="view_message_search_short"> <field name="name">mail.message.search.short</field> <field name="model">mail.message</field> - <field name="type">search</field> <field name="priority">20</field> <field name="arch" type="xml"> <search string="Messages Search"> @@ -74,7 +71,6 @@ <record model="ir.ui.view" id="view_message_search_wall"> <field name="name">mail.message.search.wall</field> <field name="model">mail.message</field> - <field name="type">search</field> <field name="priority">25</field> <field name="arch" type="xml"> <search string="Messages Search"> @@ -110,7 +106,6 @@ <record model="ir.ui.view" id="view_email_message_form"> <field name="name">mail.message.form</field> <field name="model">mail.message</field> - <field name="type">form</field> <field name="arch" type="xml"> <form string="Email message" version="7.0"> <sheet> @@ -138,7 +133,7 @@ </group> <notebook> <page string="Body (Rich)"> - <field name="body_html" widget="text_html"/> + <field name="body_html"/> </page> <page string="Body (Plain)"> <field name="body_text" widget="text"/> @@ -178,7 +173,6 @@ <record model="ir.ui.view" id="view_email_message_tree"> <field name="name">mail.message.tree</field> <field name="model">mail.message</field> - <field name="type">tree</field> <field name="arch" type="xml"> <tree string="Emails" colors="grey:state in ('sent', 'cancel');blue:state=='outgoing';red:state=='exception';black:state=='received'"> <field name="date"/> @@ -202,7 +196,6 @@ <record model="ir.ui.view" id="view_email_message_search"> <field name="name">mail.message.search</field> <field name="model">mail.message</field> - <field name="type">search</field> <field name="arch" type="xml"> <search string="Email Search"> <field name="email_from" filter_domain="['|' '|',('email_from','ilike',self), ('email_to','ilike',self), ('subject','ilike',self)]" string="Email"/> diff --git a/addons/mail/mail_thread.py b/addons/mail/mail_thread.py index 9cbf193a1c7..1bbd5f8a423 100644 --- a/addons/mail/mail_thread.py +++ b/addons/mail/mail_thread.py @@ -21,19 +21,77 @@ import base64 import email -from email.utils import parsedate import logging -from mail_message import decode, to_email -from operator import itemgetter -from osv import osv, fields import re import time +import xmlrpclib +from email.utils import parsedate +from email.message import Message + +from osv import osv, fields +from mail_message import decode, to_email import tools from tools.translate import _ -import xmlrpclib +from tools.safe_eval import safe_eval as eval _logger = logging.getLogger(__name__) +def decode_header(message, header, separator=' '): + return separator.join(map(decode,message.get_all(header, []))) + +class many2many_reference(fields.many2many): + """ many2many_reference is an override of fields.many2many. It manages + many2many-like table where one id is given by two fields, res_model + and res_id. + """ + + def _get_query_and_where_params(self, cr, model, ids, values, where_params): + """ Add in where: + - mail_followers.res_model = 'crm.lead' + """ + query = 'SELECT %(rel)s.%(id2)s, %(rel)s.%(id1)s \ + FROM %(rel)s, %(from_c)s \ + WHERE %(rel)s.%(id1)s IN %%s \ + AND %(rel)s.%(id2)s = %(tbl)s.id \ + AND %(rel)s.res_model = %%s \ + %(where_c)s \ + %(order_by)s \ + %(limit)s \ + OFFSET %(offset)d' \ + % values + where_params = [model._name] + where_params + return query, where_params + + def set(self, cr, model, id, name, values, user=None, context=None): + """ Override to add the res_model field in queries. """ + if not values: return + rel, id1, id2 = self._sql_names(model) + obj = model.pool.get(self._obj) + for act in values: + if not (isinstance(act, list) or isinstance(act, tuple)) or not act: + continue + if act[0] == 0: + idnew = obj.create(cr, user, act[2], context=context) + cr.execute('INSERT INTO '+rel+' ('+id1+','+id2+',res_model) VALUES (%s,%s,%s)', (id, idnew, model._name)) + elif act[0] == 3: + cr.execute('DELETE FROM '+rel+' WHERE '+id1+'=%s AND '+id2+'=%s AND res_model=%s', (id, act[1], model._name)) + elif act[0] == 4: + # following queries are in the same transaction - so should be relatively safe + cr.execute('SELECT 1 FROM '+rel+' WHERE '+id1+'=%s AND '+id2+'=%s AND res_model=%s', (id, act[1], model._name)) + if not cr.fetchone(): + cr.execute('INSERT INTO '+rel+' ('+id1+','+id2+',res_model) VALUES (%s,%s,%s)', (id, act[1], model._name)) + elif act[0] == 6: + d1, d2,tables = obj.pool.get('ir.rule').domain_get(cr, user, obj._name, context=context) + if d1: + d1 = ' and ' + ' and '.join(d1) + else: + d1 = '' + cr.execute('DELETE FROM '+rel+' WHERE '+id1+'=%s AND res_model=%s AND '+id2+' IN (SELECT '+rel+'.'+id2+' FROM '+rel+', '+','.join(tables)+' WHERE '+rel+'.'+id1+'=%s AND '+rel+'.'+id2+' = '+obj._table+'.id '+ d1 +')', [id, model._name, id]+d2) + for act_nbr in act[2]: + cr.execute('INSERT INTO '+rel+' ('+id1+','+id2+',res_model) VALUES (%s,%s,%s)', (id, act_nbr, model._name)) + else: + return super(many2many_reference, self).set(cr, model, id, name, values, user, context) + 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. @@ -55,22 +113,24 @@ class mail_thread(osv.Model): default implementation will work for any model. However it is common to override at least the ``message_new`` and ``message_update`` methods (calling ``super``) to add model-specific behavior at - creation and update of a thread; and ``message_get_subscribers`` - to manage more precisely the social aspect of the thread through - the followers. + creation and update of a thread. + + #TODO: UPDATE WITH SUBTYPE / NEW FOLLOW MECHANISM ''' _name = 'mail.thread' _description = 'Email Thread' - def _get_message_ids(self, cr, uid, ids, name, args, context=None): - res = {} + def _get_message_data(self, cr, uid, ids, field_names, args, context=None): + res = dict.fromkeys(ids) for id in ids: - message_ids = self.message_search(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><span class='oe_e'>9</span> %d</span> <span><span class='oe_e'>+</span> %d</span>" % (len(message_ids), len(subscriber_ids)), - } + res[id] = {'message_ids': self.message_search(cr, uid, [id], context=context)} + for thread in self.browse(cr, uid, ids, context=context): + message_follower_ids = [follower.id for follower in thread.message_follower_ids] + res[thread.id].update({ + 'message_is_follower': uid in message_follower_ids, + 'message_summary': "<span><span class='oe_e'>9</span> %d</span> <span><span class='oe_e'>+</span> %d</span>" % + (len(res[thread.id]['message_ids']), len(thread.message_follower_ids)) + }) return res def _search_message_ids(self, cr, uid, obj, name, args, context=None): @@ -79,20 +139,27 @@ class mail_thread(osv.Model): return [('id', 'in', msg_ids)] _columns = { - 'message_ids': fields.function(_get_message_ids, + 'message_ids': fields.function(_get_message_data, 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."), + string='Messages', multi="_get_message_data", + help="Field holding discussion about the current document."), + 'message_follower_ids': many2many_reference('res.users', + rel='mail_followers', id1='res_id', id2='user_id', string="Followers", + help="Followers of the document. The followers have full access to " \ + "the document details, as well as the conversation."), + 'message_is_follower': fields.function(_get_message_data, method=True, + type='boolean', string='I am Follower', multi='_get_message_data', + help='True if the current user is following 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", + 'message_summary': fields.function(_get_message_data, method=True, + type='text', string='Summary', multi='_get_message_data', 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, } @@ -102,40 +169,93 @@ class mail_thread(osv.Model): #------------------------------------------------------ def create(self, cr, uid, vals, context=None): - """Automatically subscribe the creator """ + """ Override of create to subscribe : + - the writer + - followers given by the monitored fields + """ 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) + followers_command = self.message_get_automatic_followers(cr, uid, thread_id, vals, fetch_missing=False, context=context) + if followers_command: + self.write(cr, uid, [thread_id], {'message_follower_ids': followers_command}, context=context) return thread_id def write(self, cr, uid, ids, vals, context=None): - """Automatically subscribe the writer""" + """ Override of write to subscribe : + - the writer + - followers given by the monitored fields + """ if isinstance(ids, (int, long)): ids = [ids] - write_res = super(mail_thread, self).write(cr, uid, ids, vals, context=context); - if write_res: - self.message_subscribe(cr, uid, ids, [uid], context=context) - return write_res; + for id in ids: + # copy original vals because we are going to modify it + specific_vals = dict(vals) + # we modify followers: do not subscribe the uid + if specific_vals.get('message_follower_ids'): + followers_command = self.message_get_automatic_followers(cr, uid, id, specific_vals, add_uid=False, context=context) + specific_vals['message_follower_ids'] += followers_command + else: + followers_command = self.message_get_automatic_followers(cr, uid, id, specific_vals, context=context) + specific_vals['message_follower_ids'] = followers_command + write_res = super(mail_thread, self).write(cr, uid, ids, specific_vals, context=context) + return True def unlink(self, cr, uid, ids, context=None): - """Override unlink, to automatically delete - - subscriptions - - messages + """Override unlink, to automatically delete messages that are linked with res_model and res_id, not through a foreign key with a 'cascade' ondelete attribute. Notifications will be deleted with messages """ - subscr_obj = self.pool.get('mail.subscription') msg_obj = self.pool.get('mail.message') - # delete subscriptions - subscr_to_del_ids = subscr_obj.search(cr, uid, [('res_model', '=', self._name), ('res_id', 'in', ids)], context=context) - subscr_obj.unlink(cr, uid, subscr_to_del_ids, context=context) # delete messages and notifications msg_to_del_ids = msg_obj.search(cr, uid, [('model', '=', self._name), ('res_id', 'in', ids)], context=context) msg_obj.unlink(cr, uid, msg_to_del_ids, context=context) - return super(mail_thread, self).unlink(cr, uid, ids, context=context) + def message_get_automatic_followers(self, cr, uid, id, record_vals, add_uid=True, fetch_missing=False, context=None): + """ Return the command for the many2many follower_ids field to manage + subscribers. Behavior : + - get the monitored fields (ex: ['user_id', 'responsible_id']); those + fields should be relationships to res.users (#TODO: res.partner) + - if this field is in the record_vals: it means it has been modified + thus add its value to the followers + - if this fields is not in record_vals, but fetch_missing paramter + is set to True: fetch the value in the record (use: at creation + for default values, not present in record_vals) + - if add_uid: add the current user (for example: writer is subscriber) + - generate the command and return it + This method has to be used on 1 id, because otherwise it would imply + to track which user.id is used for which record.id. + + :param record_vals: values given to the create method of the new + record, or values updated in a write. + :param monitored_fields: a list of fields that are monitored. Those + fields must be many2one fields to the res.users model. + :param fetch_missing: is set to True, the method will read the + record to find values that are not present in record_vals. + + #TODO : UPDATE WHEN MERGING TO PARTNERS + """ + # get monitored fields + monitored_fields = self.message_get_monitored_follower_fields(cr, uid, [id], context=context) + modified_fields = [field for field in monitored_fields if field in record_vals.iterkeys()] + other_fields = [field for field in monitored_fields if field not in record_vals.iterkeys()] if fetch_missing else [] + # for each monitored field: if in record_vals, it has been modified/added + follower_ids = [] + for field in modified_fields: + # do not add 'False' + if record_vals.get(fields): + follower_ids.append(record_vals.get(field)) + # for other fields: read in record if fetch_missing (otherwise list is void) + for field in other_fields: + record = self.browse(cr, uid, id, context=context) + value = getattr(record, field) + if value: + follower_ids.append(value) + # add uid if asked and not already present + if add_uid and uid not in follower_ids: + follower_ids.append(uid) + return self.message_subscribe_get_command(cr, uid, follower_ids, context=context) + #------------------------------------------------------ # mail.message wrappers and tools #------------------------------------------------------ @@ -144,22 +264,21 @@ class mail_thread(osv.Model): """ OpenChatter: wrapper of mail.message create method - creates the mail.message - automatically subscribe the message writer - - push the message to subscribed users + - push the message to followers """ if context is None: context = {} - - message_obj = self.pool.get('mail.message') - notification_obj = self.pool.get('mail.notification') - body = vals.get('body_html', '') if vals.get('content_subtype') == 'html' else vals.get('body_text', '') - - # automatically subscribe the writer of the message - if vals['user_id']: - self.message_subscribe(cr, uid, [thread_id], [vals['user_id']], context=context) - + # create message - msg_id = message_obj.create(cr, uid, vals, context=context) - + msg_id = self.pool.get('mail.message').create(cr, uid, vals, context=context) + + # automatically subscribe the writer of the message + if vals.get('user_id'): + record = self.browse(cr, uid, thread_id, context=context) + follower_ids = [follower.id for follower in record.message_follower_ids] + if vals.get('user_id') not in follower_ids: + self.message_subscribe(cr, uid, [thread_id], [vals.get('user_id')], context=context) + # Set as unread if writer is not the document responsible self.message_create_set_unread(cr, uid, [thread_id], context=context) @@ -168,28 +287,27 @@ class mail_thread(osv.Model): return msg_id # get users that will get a notification pushed + notification_obj = self.pool.get('mail.notification') user_to_push_ids = self.message_get_user_ids_to_notify(cr, uid, [thread_id], vals, context=context) for id in user_to_push_ids: notification_obj.create(cr, uid, {'user_id': id, 'message_id': msg_id}, context=context) # create the email to send - email_id = self.message_create_notify_by_email(cr, uid, vals, user_to_push_ids, context=context) + self.message_create_notify_by_email(cr, uid, vals, user_to_push_ids, context=context) return msg_id def message_get_user_ids_to_notify(self, cr, uid, thread_ids, new_msg_vals, context=None): - subscription_obj = self.pool.get('mail.subscription') # get body body = new_msg_vals.get('body_html', '') if new_msg_vals.get('content_subtype') == 'html' else new_msg_vals.get('body_text', '') # get subscribers - notif_user_ids = self.message_get_subscribers(cr, uid, thread_ids, context=context) - - # add users requested via parsing message (@login) - notif_user_ids += self.message_parse_users(cr, uid, body, context=context) - + subscr_obj = self.pool.get('mail.followers') + subscr_ids = subscr_obj.search(cr, uid, ['&', ('res_model', '=', self._name), ('res_id', 'in', thread_ids)], context=context) + notif_user_ids = [sub['user_id'][0] for sub in subscr_obj.read(cr, uid, subscr_ids, ['user_id'], context=context)] + # add users requested to perform an action (need_action mechanism) - if hasattr(self, 'get_needaction_user_ids'): + if hasattr(self, 'get_needaction_user_ids') and self._columns.get('user_id'): user_ids_dict = self.get_needaction_user_ids(cr, uid, thread_ids, context=context) for id, user_ids in user_ids_dict.iteritems(): notif_user_ids += user_ids @@ -205,17 +323,6 @@ class mail_thread(osv.Model): notif_user_ids = list(set(notif_user_ids)) return notif_user_ids - def message_parse_users(self, cr, uid, string, context=None): - """Parse message content - - if find @login -(^|\s)@((\w|@|\.)*)-: returns the related ids - this supports login that are emails (such as @raoul@grobedon.net) - """ - regex = re.compile('(^|\s)@((\w|@|\.)*)') - login_lst = [item[1] for item in regex.findall(string)] - if not login_lst: return [] - user_ids = self.pool.get('res.users').search(cr, uid, [('login', 'in', login_lst)], context=context) - return user_ids - #------------------------------------------------------ # Generic message api #------------------------------------------------------ @@ -301,7 +408,6 @@ class mail_thread(osv.Model): threads = model_pool.browse(cr, uid, threads, context=context) ir_attachment = self.pool.get('ir.attachment') - mail_message = self.pool.get('mail.message') new_msg_ids = [] for thread in threads: @@ -330,7 +436,7 @@ class mail_thread(osv.Model): data = { 'subject': subject, - 'body_text': body_text or (hasattr(thread, 'description') and thread.description or ''), + 'body_text': body_text or thread._model._columns.get('description') and thread.description or '', 'body_html': body_html or '', 'parent_id': parent_id, 'date': email_date or fields.datetime.now(), @@ -353,7 +459,7 @@ class mail_thread(osv.Model): data.update({ 'email_to': email_to, 'email_from': email_from or \ - (hasattr(thread, 'user_id') and thread.user_id and thread.user_id.user_email), + thread._model._columns.get('user_id') and thread.user_id and thread.user_id.user_email, 'email_cc': email_cc, 'email_bcc': email_bcc, 'references': references, @@ -501,10 +607,6 @@ class mail_thread(osv.Model): """ Retrieve all attachments names """ map_id_to_name = dict((attachment_id, '') for message in messages for attachment_id in message['attachment_ids']) - map_id_to_name = {} - for msg in messages: - for attach_id in msg["attachment_ids"]: - map_id_to_name[attach_id] = '' # use empty string as a placeholder ids = map_id_to_name.keys() names = self.pool.get('ir.attachment').name_get(cr, uid, ids, context=context) @@ -560,79 +662,127 @@ class mail_thread(osv.Model): msgs = msg_obj.read(cr, uid, msg_ids, context=context) return msgs - - def _get_user(self, cr, uid, alias, context): - """ - param alias: browse record of alias. - return: int user_id. - """ - - user_obj = self.pool.get('res.user') - user_id = 1 - if alias.alias_user_id: - user_id = alias.alias_user_id.id - #if user_id not defined in the alias then search related user using name of Email sender - else: - from_email = msg.get('from') - user_ids = user_obj.search(cr, uid, [('name','=',from_email)], context) - if user_ids: - user_id = user_obj.browse(cr, uid, user_ids[0], context).id - return user_id - - def message_catchall(self, cr, uid, message, context=None): - """ - Process incoming mail and call messsage_process using details of the mail.alias model - else raise Exception so that mailgate script will reject the mail and - send notification mail sender that this mailbox does not exist so your mail have been rejected. - """ - mail_alias = self.pool.get('mail.alias') - mail_message = self.pool.get('mail.message') - if isinstance(message, xmlrpclib.Binary): - message = str(message.data) - if isinstance(message, unicode): - message = message.encode('utf-8') - msg_txt = email.message_from_string(message) - msg = mail_message.parse_message(msg_txt) - alias_name = msg.get('to').split("@")[0] # @@@@ - alias_ids = mail_alias.search(cr, uid, [('alias_name','=',alias_name)]) - #if alias found then call message_process method. # @@@@ - if alias_ids: - alias_id = mail_alias.browse(cr, uid, alias_ids[0], context) - user_id = self._get_user( cr, uid, alias_id, context) - alias_defaults = dict(eval(alias_id.alias_defaults or {})) - self.message_process(cr, user_id, alias_id.alias_model_id.model, message, - custom_values=alias_defaults, - thread_id=alias_id.alias_force_thread_id or False, - context=context) - else: - #if Mail box for the intended Mail Alias then give logger warning - _logger.warning("No Mail Alias Found for the name '%s'."%(alias_name)) - raise # @@@@ - return True + def _message_find_user_id(self, cr, uid, message, context=None): + from_local_part = to_email(decode(message.get('From')))[0] + user_ids = self.pool.get('res.users').search(cr, uid, [('login', '=', from_local_part)], context=context) + return user_ids[0] if user_ids else uid #------------------------------------------------------ # Mail gateway #------------------------------------------------------ # message_process will call either message_new or message_update. + def message_route(self, cr, uid, message, model=None, thread_id=None, + custom_values=None, context=None): + """Attempt to figure out the correct target model, thread_id, + custom_values and user_id to use for an incoming message. + Multiple values may be returned, if a message had multiple + recipients matching existing mail.aliases, for example. + + The following heuristics are used, in this order: + 1. If the message replies to an existing thread_id, and + properly contains the thread model in the 'In-Reply-To' + header, use this model/thread_id pair, and ignore + custom_value (not needed as no creation will take place) + 2. Look for a mail.alias entry matching the message + recipient, and use the corresponding model, thread_id, + custom_values and user_id. + 3. Fallback to the ``model``, ``thread_id`` and ``custom_values`` + provided. + 4. If all the above fails, raise an exception. + + :param string message: an email.message instance + :param string model: the fallback model to use if the message + does not match any of the currently configured mail aliases + (may be None if a matching alias is supposed to be present) + :type dict custom_values: optional dictionary of default field values + to pass to ``message_new`` if a new record needs to be created. + Ignored if the thread record already exists, and also if a + matching mail.alias was found (aliases define their own defaults) + :param int thread_id: optional ID of the record/thread from ``model`` + to which this mail should be attached. Only used if the message + does not reply to an existing thread and does not match any mail alias. + :return: list of [model, thread_id, custom_values, user_id] + """ + assert isinstance(message, Message), 'message must be an email.message.Message at this point' + message_id = message.get('Message-Id') + + # 1. Verify if this is a reply to an existing thread + references = decode_header(message, 'References') or decode_header(message, 'In-Reply-To') + ref_match = references and tools.reference_re.search(references) + if ref_match: + thread_id = int(ref_match.group(1)) + model = ref_match.group(2) or model + model_pool = self.pool.get(model) + if thread_id and model and model_pool and model_pool.exists(cr, uid, thread_id) \ + and hasattr(model_pool, 'message_update'): + _logger.debug('Routing mail with Message-Id %s: direct reply to model: %s, thread_id: %s, custom_values: %s, uid: %s', + message_id, model, thread_id, custom_values, uid) + return [(model, thread_id, custom_values, uid)] + + # 2. Look for a matching mail.alias entry + # Delivered-To is a safe bet in most modern MTAs, but we have to fallback on To + Cc values + # for all the odd MTAs out there, as there is no standard header for the envelope's `rcpt_to` value. + rcpt_tos = decode_header(message, 'Delivered-To') or \ + ','.join([decode_header(message, 'To'), + decode_header(message, 'Cc'), + decode_header(message, 'Resent-To'), + decode_header(message, 'Resent-Cc')]) + local_parts = [e.split('@')[0] for e in to_email(rcpt_tos)] + if local_parts: + mail_alias = self.pool.get('mail.alias') + alias_ids = mail_alias.search(cr, uid, [('alias_name', 'in', local_parts)]) + if alias_ids: + routes = [] + for alias in mail_alias.browse(cr, uid, alias_ids, context=context): + user_id = alias.alias_user_id.id + if not user_id: + user_id = self._message_find_user_id(cr, uid, message, context=context) + routes.append((alias.alias_model_id.model, alias.alias_force_thread_id, \ + eval(alias.alias_defaults), user_id)) + _logger.debug('Routing mail with Message-Id %s: direct alias match: %r', message_id, routes) + return routes + + # 3. Fallback to the provided parameters, if they work + model_pool = self.pool.get(model) + if not thread_id: + # Legacy: fallback to matching [ID] in the Subject + match = tools.res_re.search(decode_header(message, 'Subject')) + thread_id = match and match.group(1) + assert thread_id and hasattr(model_pool, 'message_update') or hasattr(model_pool, 'message_new'), \ + "No possible route found for incoming message with Message-Id %s. " \ + "Create an appropriate mail.alias or force the destination model." % message_id + if thread_id and not model_pool.exists(cr, uid, thread_id): + _logger.warning('Received mail reply to missing document %s! Ignoring and creating new document instead for Message-Id %s', + thread_id, message_id) + thread_id = None + _logger.debug('Routing mail with Message-Id %s: fallback to model:%s, thread_id:%s, custom_values:%s, uid:%s', + message_id, model, thread_id, custom_values, uid) + return [(model, thread_id, custom_values, uid)] + + def message_process(self, cr, uid, model, message, custom_values=None, save_original=False, strip_attachments=False, thread_id=None, context=None): - """Process an incoming RFC2822 email message related to the - given thread model, relying on ``mail.message.parse()`` - for the parsing operation, and then calling ``message_new`` - (if the thread record did not exist) or ``message_update`` - (if it did), then calling ``message_forward`` to automatically - notify other people that should receive this message. - - :param string model: the thread model for which a new message - must be processed - :param message: source of the RFC2822 mail + """Process an incoming RFC2822 email message, relying on + ``mail.message.parse()`` for the parsing operation, + and ``message_route()`` to figure out the target model. + + Once the target model is known, its ``message_new`` method + is called with the new message (if the thread record did not exist) + or its ``message_update`` method (if it did). Finally, + ``message_forward`` is called to automatically notify other + people that should receive this message. + + :param string model: the fallback model to use if the message + does not match any of the currently configured mail aliases + (may be None if a matching alias is supposed to be present) + :param message: source of the RFC2822 message :type message: string or xmlrpclib.Binary :type dict custom_values: optional dictionary of field values - to pass to ``message_new`` if a new - record needs to be created. Ignored - if the thread record already exists. + to pass to ``message_new`` if a new record needs to be created. + Ignored if the thread record already exists, and also if a + matching mail.alias was found (aliases define their own defaults) :param bool save_original: whether to keep a copy of the original email source attached to the message after it is imported. :param bool strip_attachments: whether to strip all attachments @@ -642,66 +792,41 @@ class mail_thread(osv.Model): overrides the automatic detection based on the message headers. """ + if context is None: context = {} + # extract message bytes - we are forced to pass the message as binary because # we don't know its encoding until we parse its headers and hence can't # convert it to utf-8 for transport between the mailgate script and here. if isinstance(message, xmlrpclib.Binary): message = str(message.data) - - if context is None: context = {} - - mail_message = self.pool.get('mail.message') - model_pool = self.pool.get(model) - if self._name != model: - context.update({'thread_model': model}) - - # Parse Message # Warning: message_from_string doesn't always work correctly on unicode, # we must use utf-8 strings here :-( if isinstance(message, unicode): message = message.encode('utf-8') msg_txt = email.message_from_string(message) - msg = mail_message.parse_message(msg_txt, save_original=save_original, context=context) - - # update state - msg['state'] = 'received' - + routes = self.message_route(cr, uid, msg_txt, model, + thread_id, custom_values, + context=context) + msg = self.pool.get('mail.message').parse_message(msg_txt, save_original=save_original, context=context) + msg['state'] = 'received' if strip_attachments and 'attachments' in msg: del msg['attachments'] - - # Create New Record into particular model - def create_record(msg): - if hasattr(model_pool, 'message_new'): - return model_pool.message_new(cr, uid, msg, - custom_values, - context=context) - if not thread_id and (msg.get('references') or msg.get('in-reply-to')): - references = msg.get('references') or msg.get('in-reply-to') - if '\r\n' in references: - references = references.split('\r\n') + for model, thread_id, custom_values, user_id in routes: + if self._name != model: + context.update({'thread_model': model}) + model_pool = self.pool.get(model) + assert thread_id and hasattr(model_pool, 'message_update') or hasattr(model_pool, 'message_new'), \ + "Undeliverable mail with Message-Id %s, model %s does not accept incoming emails" % \ + (msg['message-id'], model) + if thread_id and hasattr(model_pool, 'message_update'): + model_pool.message_update(cr, user_id, [thread_id], msg, context=context) else: - references = references.split(' ') - for ref in references: - ref = ref.strip() - thread_id = tools.reference_re.search(ref) - if not thread_id: - thread_id = tools.res_re.search(msg['subject']) - if thread_id: - thread_id = int(thread_id.group(1)) - if not model_pool.exists(cr, uid, thread_id) or \ - not hasattr(model_pool, 'message_update'): - # referenced thread not found or not updatable, - # -> create a new one - thread_id = False - if not thread_id: - thread_id = create_record(msg) - else: - model_pool.message_update(cr, uid, [thread_id], msg, {}, context=context) - # To forward the email to other followers - self.message_forward(cr, uid, model, [thread_id], msg_txt, context=context) - # Set as Unread - model_pool.message_mark_as_unread(cr, uid, [thread_id], context=context) - return thread_id + thread_id = model_pool.message_new(cr, user_id, msg, custom_values, context=context) + + # Forward the email to other followers + self.message_forward(cr, uid, model, [thread_id], msg_txt, context=context) + model_pool.message_mark_as_unread(cr, uid, [thread_id], context=context) + return True def message_new(self, cr, uid, msg_dict, custom_values=None, context=None): """Called by ``message_process`` when a new message is received @@ -735,7 +860,7 @@ class mail_thread(osv.Model): fields = model_pool.fields_get(cr, uid, context=context) data = model_pool.default_get(cr, uid, fields, context=context) if 'name' in fields and not data.get('name'): - data['name'] = msg_dict.get('from', '') + data['name'] = msg_dict.get('subject', '') if custom_values and isinstance(custom_values, dict): data.update(custom_values) res_id = model_pool.create(cr, uid, data, context=context) @@ -771,7 +896,7 @@ class mail_thread(osv.Model): for thread in self.browse(cr, uid, ids, context=context): l = set() for message in thread.message_ids: - l.add((message.user_id and message.user_id.user_email) or '') + l.add((message.user_id and message.user_id.email) or '') l.add(message.email_from or '') l.add(message.email_cc or '') res[thread.id] = filter(None, l) @@ -791,7 +916,6 @@ class mail_thread(osv.Model): """ model_pool = self.pool.get(model) smtp_server_obj = self.pool.get('ir.mail_server') - mail_message = self.pool.get('mail.message') for res in model_pool.browse(cr, uid, thread_ids, context=context): if hasattr(model_pool, 'message_thread_followers'): followers = model_pool.message_thread_followers(cr, uid, [res.id])[res.id] @@ -805,7 +929,7 @@ class mail_thread(osv.Model): forward_to = [i for i in message_followers_emails if (i and (i not in message_recipients))] if forward_to: # TODO: we need an interface for this for all types of objects, not just leads - if hasattr(res, 'section_id'): + if model_pool._columns.get('section_id'): del msg['reply-to'] msg['reply-to'] = res.section_id.reply_to @@ -881,70 +1005,41 @@ class mail_thread(osv.Model): # Subscription mechanism #------------------------------------------------------ - 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. - This method can be overriden to add implicit subscribers, such - as project managers, by adding their user_id to the list of - ids returned by this method. - """ - subscr_obj = self.pool.get('mail.subscription') - subscr_ids = subscr_obj.search(cr, uid, ['&', ('res_model', '=', self._name), ('res_id', 'in', ids)], context=context) - return [sub['user_id'][0] for sub in subscr_obj.read(cr, uid, subscr_ids, ['user_id'], context=context)] - - def message_read_subscribers(self, cr, uid, ids, fields=['id', 'name', 'image_small'], 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): - """ 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 + def message_get_monitored_follower_fields(self, cr, uid, ids, context=None): + """ Returns a list of fields containing a res.user.id. Those fields + will be checked to automatically subscribe those users. """ - sub_user_id = uid if user_id is None else user_id - if sub_user_id in self.message_get_subscribers(cr, uid, ids, context=context): - return True - return False + return [] 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 + :param return: new value of followers, for Chatter """ - 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 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 + write_res = self.write(cr, uid, ids, {'message_follower_ids': self.message_subscribe_get_command(cr, uid, to_subscribe_uids, context)}, context=context) + return [follower.id for thread in self.browse(cr, uid, ids, context=context) for follower in thread.message_follower_ids] + + def message_subscribe_get_command(self, cr, uid, follower_ids, context=None): + """ Generate the many2many command to add followers. """ + return [(4, id) for id in follower_ids] def message_unsubscribe(self, cr, uid, ids, user_ids = None, context=None): """ Unsubscribe the user (or user_ids) from the current document. :param user_ids: a list of user_ids; if not set, subscribe uid instead + :param return: new value of followers, for Chatter """ - # 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 to_unsubscribe_uids = [uid] if user_ids is None else user_ids - subscription_obj = self.pool.get('mail.subscription') - 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) - if not to_delete_sub_ids: - return False - return subscription_obj.unlink(cr, uid, to_delete_sub_ids, context=context) + write_res = self.write(cr, uid, ids, {'message_follower_ids': self.message_unsubscribe_get_command(cr, uid, to_unsubscribe_uids, context)}, context=context) + return [follower.id for thread in self.browse(cr, uid, ids, context=context) for follower in thread.message_follower_ids] + + def message_unsubscribe_get_command(self, cr, uid, follower_ids, context=None): + """ Generate the many2many command to remove followers. """ + return [(3, id) for id in follower_ids] #------------------------------------------------------ # Notification API @@ -975,19 +1070,16 @@ class mail_thread(osv.Model): # remove message writer if user_to_notify_ids.count(new_msg_values.get('user_id')) > 0: user_to_notify_ids.remove(new_msg_values.get('user_id')) - - # get user_ids directly asked - user_to_push_from_parse_ids = self.message_parse_users(cr, uid, body, context=context) - + # try to find an email_to email_to = '' for user in res_users_obj.browse(cr, uid, user_to_notify_ids, context=context): - if not user.notification_email_pref == 'all' and \ - not (user.notification_email_pref == 'to_me' and user.id in user_to_push_from_parse_ids): + # TO BE REFACTORED BY FP, JUSTE REMOVED TO_ME, NOT SURE WHAT S NEW BEHAVIOR + if not user.notification_email_pref == 'all': continue - if not user.user_email: + if not user.email: continue - email_to = '%s, %s' % (email_to, user.user_email) + email_to = '%s, %s' % (email_to, user.email) email_to = email_to.lstrip(', ') # did not find any email address: not necessary to create an email @@ -998,7 +1090,7 @@ class mail_thread(osv.Model): current_user = res_users_obj.browse(cr, uid, [uid], context=context)[0] email_from = new_msg_values.get('email_from') if not email_from: - email_from = current_user.user_email + email_from = current_user.email # get email content, create it (with mail_message.create) email_values = self.message_create_notify_get_email_dict(cr, uid, new_msg_values, email_from, email_to, context) @@ -1047,14 +1139,14 @@ class mail_thread(osv.Model): """ 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): + if obj.message_state and self._columns.get('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): + if obj.message_state and self._columns.get('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): @@ -1064,7 +1156,7 @@ class mail_thread(osv.Model): 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: + if not obj.message_state and self._columns.get('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): diff --git a/addons/mail/res_config_view.xml b/addons/mail/res_config_view.xml index a17a1f22a80..c4bc23c9357 100644 --- a/addons/mail/res_config_view.xml +++ b/addons/mail/res_config_view.xml @@ -5,7 +5,6 @@ <field name="name">base.config.settings.mail.alias</field> <field name="model">base.config.settings</field> <field name="inherit_id" ref="base_setup.view_general_configuration"/> - <field name="type">form</field> <field name="arch" type="xml"> <xpath expr="/form/group[last()]/div[last()]/div[last()]" position='after' version="7.0"> <div> diff --git a/addons/mail/res_partner.py b/addons/mail/res_partner.py index 3dc1f91d857..d3a61b31a8a 100644 --- a/addons/mail/res_partner.py +++ b/addons/mail/res_partner.py @@ -20,20 +20,28 @@ ############################################################################## from osv import osv -from osv import fields -class res_partner_mail(osv.osv): +class res_partner_mail(osv.Model): """ Inherits partner and adds CRM information in the partner form """ _name = "res.partner" _inherit = ['res.partner', 'mail.thread'] def message_search_get_domain(self, cr, uid, ids, context=None): """ Override of message_search_get_domain for partner discussion page. - The purpose is to add messages directly sent to the partner. + The purpose is to add messages directly sent to the partner. It also + adds messages pushed to the related user, if any, using @login. """ initial_domain = super(res_partner_mail, self).message_search_get_domain(cr, uid, ids, context=context) - if self._name == 'res.partner': # to avoid models inheriting from res.partner - search_domain = ['|'] + initial_domain + ['|', ('partner_id', 'in', ids), ('partner_ids', 'in', ids)] + # to avoid models inheriting from res.partner + if self._name != 'res.partner': + return initial_domain + # add message linked to the partner + search_domain = ['|'] + initial_domain + ['|', ('partner_id', 'in', ids), ('partner_ids', 'in', ids)] + # if partner is linked to a user: find @login + res_users_obj = self.pool.get('res.users') + user_ids = res_users_obj.search(cr, uid, [('partner_id', 'in', ids)], context=context) + for user in res_users_obj.browse(cr, uid, user_ids, context=context): + search_domain = ['|'] + search_domain + ['|', ('body_text', 'like', '@%s' % (user.login)), ('body_html', 'like', '@%s' % (user.login))] return search_domain # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: diff --git a/addons/mail/res_partner_view.xml b/addons/mail/res_partner_view.xml index 8b4d89f89f4..2a8430926d5 100644 --- a/addons/mail/res_partner_view.xml +++ b/addons/mail/res_partner_view.xml @@ -5,13 +5,13 @@ <record id="view_emails_partner_info_form" model="ir.ui.view"> <field name="name">res.partner.emails.info.inherit</field> <field name="model">res.partner</field> - <field name="type">form</field> <field name="inherit_id" ref="base.view_partner_form"/> <field name="arch" type="xml"> <xpath expr="//sheet" position="after"> <div class="oe_chatter"> <field name="message_ids" widget="mail_thread" options='{"thread_level": 1}'/> + <field name="message_follower_ids" widget="mail_followers"/> </div> </xpath> </field> diff --git a/addons/mail/res_users.py b/addons/mail/res_users.py index 8a590fc318f..f985ac4f88d 100644 --- a/addons/mail/res_users.py +++ b/addons/mail/res_users.py @@ -28,32 +28,32 @@ from tools.translate import _ _logger = logging.getLogger(__name__) -class res_users(osv.osv): +class res_users(osv.Model): """ Update of res.users class - add a preference about sending emails about notifications - make a new user follow itself - add a welcome message """ _name = 'res.users' - _inherit = ['res.users', 'mail.thread'] + _inherit = ['res.users'] _inherits = {'mail.alias': 'alias_id'} - + _columns = { 'notification_email_pref': fields.selection([ - ('all', 'All feeds'), - ('comments', 'Only comments'), - ('to_me', 'Only when sent directly to me'), + ('all', 'All Feeds'), + ('to_me', 'Only send directly to me'), ('none', 'Never') ], 'Receive Feeds by Email', required=True, help="Choose in which case you want to receive an email when you "\ - "receive new feeds."), + "receive new feeds."), 'alias_id': fields.many2one('mail.alias', 'Alias', ondelete="cascade", required=True, - help="Email address internally associated with this user. Incoming emails will appear " - "in the user's notifications."), + help="Email address internally associated with this user. Incoming "\ + "emails will appear in the user's notifications."), } _defaults = { 'notification_email_pref': 'to_me', + 'alias_domain': False, # always hide alias during creation } def __init__(self, pool, cr): @@ -67,44 +67,62 @@ class res_users(osv.osv): self.SELF_WRITEABLE_FIELDS.append('notification_email_pref') return init_res - def init(self, cr): - # Installation hook to create aliases for all users, right after _auto_init + def _auto_init(self, cr, context=None): + """Installation hook to create aliases for all users and avoid constraint errors.""" + + # disable the unique alias_id not null constraint, to avoid spurious warning during + # super.auto_init. We'll reinstall it afterwards. + self._columns['alias_id'].required = False + + super(res_users,self)._auto_init(cr, context=context) + registry = RegistryManager.get(cr.dbname) mail_alias = registry.get('mail.alias') - res_users = registry.get('res.users') - users_no_alias = res_users.search(cr, SUPERUSER_ID, [('alias_id', '=', False)]) + res_users_model = registry.get('res.users') + users_no_alias = res_users_model.search(cr, SUPERUSER_ID, [('alias_id', '=', False)]) # Use read() not browse(), to avoid prefetching uninitialized inherited fields - for user_data in res_users.read(cr, SUPERUSER_ID, users_no_alias, ['login']): + for user_data in res_users_model.read(cr, SUPERUSER_ID, users_no_alias, ['login']): alias_id = mail_alias.create_unique_alias(cr, SUPERUSER_ID, {'alias_name': user_data['login'], 'alias_force_id': user_data['id']}, model_name=self._name) - res_users.write(cr, SUPERUSER_ID, user_data['id'], {'alias_id': alias_id}) + res_users_model.write(cr, SUPERUSER_ID, user_data['id'], {'alias_id': alias_id}) _logger.info('Mail alias created for user %s (uid %s)', user_data['login'], user_data['id']) # Finally attempt to reinstate the missing constraint try: cr.execute('ALTER TABLE res_users ALTER COLUMN alias_id SET NOT NULL') except Exception: - pass - - + _logger.warning("Table '%s': unable to set a NOT NULL constraint on column '%s' !\n"\ + "If you want to have it, you should update the records and execute manually:\n"\ + "ALTER TABLE %s ALTER COLUMN %s SET NOT NULL", + self._table, 'alias_id', self._table, 'alias_id') + + self._columns['alias_id'].required = True + def create(self, cr, uid, data, context=None): # create default alias same as the login mail_alias = self.pool.get('mail.alias') alias_id = mail_alias.create_unique_alias(cr, uid, {'alias_name': data['login']}, model_name=self._name, context=context) data['alias_id'] = alias_id + data.pop('alias_name', None) # prevent errors during copy() + # create user that follows its related partner user_id = super(res_users, self).create(cr, uid, data, context=context) - mail_alias.write(cr, SUPERUSER_ID, [alias_id], {"alias_force_thread_id": user_id}, context) - user = self.browse(cr, uid, user_id, context=context) - # make user follow itself - self.message_subscribe(cr, uid, [user_id], [user_id], context=context) + self.pool.get('res.partner').message_subscribe(cr, uid, [user.partner_id.id], [user_id], context=context) + # alias + mail_alias.write(cr, SUPERUSER_ID, [alias_id], {"alias_force_thread_id": user_id}, context) # create a welcome message - company_name = user.company_id.name if user.company_id else _('the company') - message = _('%s joined the %s network! Take a moment to welcome %s.') % (user.name, company_name, user.name) - self.message_append_note(cr, uid, [user_id], body=message, type='comment', context=context) + self.create_welcome_message(cr, uid, user, context=context) return user_id - + + def create_welcome_message(self, cr, uid, user, context=None): + company_name = user.company_id.name if user.company_id else _('the company') + subject = '''%s has joined %s.''' % (user.name, company_name) + body = '''Welcome to OpenERP !''' + # TODO change 1 into user.id but catch errors + return self.pool.get('res.partner').message_append_note(cr, 1, [user.partner_id.id], + subject=subject, body=body, type='comment', content_subtype='html', context=context) + def write(self, cr, uid, ids, vals, context=None): # User alias is sync'ed with login if vals.get('login'): vals['alias_name'] = vals['login'] @@ -117,28 +135,52 @@ class res_users(osv.osv): res = super(res_users, self).unlink(cr, uid, ids, context=context) alias_pool.unlink(cr, uid, alias_ids, context=context) return res - - def message_search_get_domain(self, cr, uid, ids, context=None): - """ Override of message_search_get_domain for partner discussion page. - The purpose is to add messages directly sent to user using - @user_login. - """ - initial_domain = super(res_users, self).message_search_get_domain(cr, uid, ids, context=context) - custom_domain = [] + + # -------------------------------------------------- + # Wrappers on partner methods for Chatter + # #FIXME: another branch holds a refactoring of mail.thread + # that should help cleaning those wrappers + # -------------------------------------------------- + + def message_append(self, cr, uid, threads, subject, body_text=None, body_html=None, + type='email', email_date=None, parent_id=False, + content_subtype='plain', state=None, + partner_ids=None, email_from=False, email_to=False, + email_cc=None, email_bcc=None, reply_to=None, + headers=None, message_id=False, references=None, + attachments=None, original=None, context=None): + for user in self.browse(cr, uid, threads, context=context): + user.partner_id.message_append(subject, body_text, body_html, type, email_date, parent_id, + content_subtype, state, partner_ids, email_from, email_to, email_cc, email_bcc, reply_to, + headers, message_id, references, attachments, original) + + def message_read(self, cr, uid, ids, fetch_ancestors=False, ancestor_ids=None, + limit=100, offset=0, domain=None, context=None): + for user in self.browse(cr, uid, ids, context=context): + return user.partner_id.message_read(fetch_ancestors, ancestor_ids, limit, offset, domain) + + def message_search(self, cr, uid, ids, fetch_ancestors=False, ancestor_ids=None, + limit=100, offset=0, domain=None, count=False, context=None): + for user in self.browse(cr, uid, ids, context=context): + return user.partner_id.message_search(fetch_ancestors, ancestor_ids, limit, offset, domain, count) + + def message_subscribe(self, cr, uid, ids, user_ids = None, context=None): for user in self.browse(cr, uid, ids, context=context): - if custom_domain: - custom_domain += ['|'] - custom_domain += ['|', ('body_text', 'like', '@%s' % (user.login)), ('body_html', 'like', '@%s' % (user.login))] - return ['|'] + initial_domain + custom_domain + return user.partner_id.message_subscribe(user_ids) -class res_users_mail_group(osv.osv): + def message_unsubscribe(self, cr, uid, ids, user_ids = None, context=None): + for user in self.browse(cr, uid, ids, context=context): + return user.partner_id.message_unsubscribe(user_ids) + + +class res_users_mail_group(osv.Model): """ Update of res.groups class - if adding/removing users from a group, check mail.groups linked to this user group, and subscribe / unsubscribe them from the discussion group. This is done by overriding the write method. """ _name = 'res.users' - _inherit = ['res.users', 'mail.thread'] + _inherit = ['res.users'] def write(self, cr, uid, ids, vals, context=None): write_res = super(res_users_mail_group, self).write(cr, uid, ids, vals, context=context) @@ -150,9 +192,8 @@ class res_users_mail_group(osv.osv): mail_group_ids = mail_group_obj.search(cr, uid, [('group_ids', 'in', user_group_ids)], context=context) mail_group_obj.message_subscribe(cr, uid, mail_group_ids, ids, context=context) return write_res - -class res_groups_mail_group(osv.osv): +class res_groups_mail_group(osv.Model): """ Update of res.groups class - if adding/removing users from a group, check mail.groups linked to this user group, and subscribe / unsubscribe them from the discussion @@ -170,3 +211,5 @@ class res_groups_mail_group(osv.osv): mail_group_ids = mail_group_obj.search(cr, uid, [('group_ids', 'in', ids)], context=context) mail_group_obj.message_subscribe(cr, uid, mail_group_ids, user_ids, context=context) return super(res_groups_mail_group, self).write(cr, uid, ids, vals, context=context) + +# vim:et: diff --git a/addons/mail/res_users_view.xml b/addons/mail/res_users_view.xml index d1429f0afbc..005d71cb09f 100644 --- a/addons/mail/res_users_view.xml +++ b/addons/mail/res_users_view.xml @@ -9,7 +9,7 @@ <field name="inherit_id" ref="base.view_users_form_simple_modif"/> <field name="arch" type="xml"> <data> - <field name="user_email" position="before"> + <field name="email" position="before"> <field name="notification_email_pref" readonly="0"/> </field> </data> @@ -23,17 +23,12 @@ <field name="inherit_id" ref="base.view_users_form"/> <field name="arch" type="xml"> <data> - <field name="user_email" position="before"> + <field name="email" position="before"> <field name="notification_email_pref"/> </field> - <xpath expr="/form/sheet" position="after"> - <div class="oe_chatter"> - <field name="message_ids" colspan="4" widget="mail_thread" nolabel="1"/> - </div> - </xpath> - <field name="user_email" position="after"> + <field name="email" position="after"> <field name="alias_domain" invisible="1"/> - <field name="alias_id" readonly="1" attrs="{'invisible': [('alias_domain', '=', False)]}"/> + <field name="alias_id" readonly="1" required="0" attrs="{'invisible': [('alias_domain', '=', False)]}"/> </field> </data> </field> diff --git a/addons/mail/security/ir.model.access.csv b/addons/mail/security/ir.model.access.csv index 1941a41a259..926761182ce 100644 --- a/addons/mail/security/ir.model.access.csv +++ b/addons/mail/security/ir.model.access.csv @@ -2,7 +2,7 @@ id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink access_mail_message_all,mail.message.all,model_mail_message,,1,0,0,0
access_mail_message_group_user,mail.message.group.user,model_mail_message,base.group_user,1,1,1,1
access_mail_thread,mail.thread,model_mail_thread,base.group_user,1,1,1,0
-access_mail_subscription_all,mail.subscription.all,model_mail_subscription,,1,1,1,1
+access_mail_followers_all,mail.followers.all,model_mail_followers,,1,1,1,1
access_mail_notification_all,mail.notification.all,model_mail_notification,,1,1,1,1
access_mail_group,mail.group,model_mail_group,base.group_user,1,1,1,1
access_mail_alias_user,mail.alias,model_mail_alias,base.group_user,1,1,1,0
diff --git a/addons/mail/security/mail_security.xml b/addons/mail/security/mail_security.xml index 34fcbac3088..a12314e6f25 100644 --- a/addons/mail/security/mail_security.xml +++ b/addons/mail/security/mail_security.xml @@ -1,6 +1,6 @@ <?xml version="1.0" encoding="utf-8"?> <openerp> - <data noupdate="1"> + <data> <!-- CATEGORY --> <record model="ir.module.category" id="module_category_social"> @@ -20,23 +20,8 @@ <record id="group_rule_public_and_joined" model="ir.rule"> <field name="name">Mail.group: access only public and joined groups</field> <field name="model_id" ref="model_mail_group"/> - <field name="domain_force">['|', ('public', '=', True), ('member_ids', 'in', user.id)]</field> - <field name="perm_create" eval="False"/> - <field name="perm_unlink" eval="False"/> - </record> - <record id="group_rule_delete_mygroup" model="ir.rule"> - <field name="name">Mail.group: delete my groups only</field> - <field name="model_id" ref="model_mail_group"/> - <field name="domain_force">[('responsible_id', '=', user.id)]</field> - <field name="perm_read" eval="False"/> - <field name="perm_write" eval="False"/> - <field name="perm_create" eval="False"/> - </record> - <record id="group_rule_all" model="ir.rule"> - <field name="name">Mail.group: all groups</field> - <field name="model_id" ref="model_mail_group"/> - <field name="domain_force">[(1,'=',1)]</field> - <field name="groups" eval="[(4, ref('group_mail_manager'))]"/> + <!-- This rule has to be improved for employee only groups --> + <field name="domain_force">['|', '|', ('public', '=', 'public'), ('message_follower_ids', 'in', [user.id]), '&', ('public','=','groups'), ('group_public_id','in', [x.id for x in user.groups_id])]</field> </record> </data> diff --git a/addons/mail/static/scripts/openerp_mailgate.py b/addons/mail/static/scripts/openerp_mailgate.py index 64dafc56785..99ebc8f770b 100755 --- a/addons/mail/static/scripts/openerp_mailgate.py +++ b/addons/mail/static/scripts/openerp_mailgate.py @@ -106,15 +106,15 @@ class EmailParser(object): self.email_default = email_default - def parse(self, method, message, custom_values=None, save_original=None): + def parse(self, message, custom_values=None, save_original=None): # pass message as bytes because we don't know its encoding until we parse its headers # and hence can't convert it to utf-8 for transport - res_id = self.rpc('mail.thread', - method, - self.model, - xmlrpclib.Binary(message), - custom_values or {}, - save_original or False) + return self.rpc('mail.thread', + 'message_process', + self.model, + xmlrpclib.Binary(message), + custom_values or {}, + save_original or False) def configure_parser(): parser = optparse.OptionParser(usage='usage: %prog [options]', version='%prog v1.1') @@ -123,32 +123,32 @@ def configure_parser(): "with the OpenERP server for case management in the CRM module.") parser.add_option_group(group) parser.add_option("-u", "--user", dest="userid", - help="ID of the user in OpenERP", + help="OpenERP user id to connect with", default=config.OPENERP_DEFAULT_USER_ID, type='int') parser.add_option("-p", "--password", dest="password", - help="Password of the user in OpenERP", + help="OpenERP user password", default=config.OPENERP_DEFAULT_PASSWORD) parser.add_option("-o", "--model", dest="model", - help="Name or ID of crm model", + help="Name or ID of destination model", default="crm.lead") parser.add_option("-m", "--default", dest="default", - help="Default eMail in case of any trouble.", + help="Admin email for error notifications.", default=None) parser.add_option("-d", "--dbname", dest="dbname", - help="Database name (default: %default)", + help="OpenERP database name (default: %default)", default=config.OPENERP_DEFAULT_DATABASE) parser.add_option("--host", dest="host", - help="Hostname of the OpenERP Server", + help="OpenERP Server hostname", default=config.OPENERP_HOSTNAME) parser.add_option("--port", dest="port", - help="Port of the OpenERP Server", + help="OpenERP Server XML-RPC port number", default=config.OPENERP_PORT) parser.add_option("--custom-values", dest="custom_values", - help="Add Custom Values to the object", + help="Dictionary of extra values to pass when creating records", default=None) parser.add_option("-s", dest="save_original", action="store_true", - help="Attach a copy of original email to the message entry", + help="Keep a full copy of the email source attached to each message", default=False) return parser @@ -160,7 +160,6 @@ def main(): parser = configure_parser() (options, args) = parser.parse_args() - method = "message_process" email_parser = EmailParser(options.userid, options.password, options.dbname, @@ -170,8 +169,6 @@ def main(): email_default= options.default) msg_txt = sys.stdin.read() custom_values = {} - if not options.model: - method = "message_catchall" try: custom_values = dict(eval(options.custom_values or "{}" )) except: @@ -179,7 +176,7 @@ def main(): traceback.print_exc() try: - email_parser.parse(method, msg_txt, custom_values, options.save_original or False) + email_parser.parse(msg_txt, custom_values, options.save_original or False) except Exception: msg = '\n'.join([ 'parameters', diff --git a/addons/mail/static/src/css/mail.css b/addons/mail/static/src/css/mail.css index ae934eeb4ff..baab34bca82 100644 --- a/addons/mail/static/src/css/mail.css +++ b/addons/mail/static/src/css/mail.css @@ -89,10 +89,14 @@ /* RecordThread /* ------------------------------------------------------------ */ -.openerp div.oe_mail_recthread { +.openerp .oe_form div.oe_chatter { overflow: auto; } +.openerp div.oe_mail_recthread { + /*overflow: auto;*/ +} + .openerp div.oe_mail_recthread_main { float: left; width: 560px; diff --git a/addons/mail/static/src/js/mail.js b/addons/mail/static/src/js/mail.js index 2923f723952..d9ebf138ff2 100644 --- a/addons/mail/static/src/js/mail.js +++ b/addons/mail/static/src/js/mail.js @@ -4,6 +4,8 @@ openerp.mail = function(session) { var mail = session.mail = {}; + openerp_mail_followers(session, mail); // import mail_followers.js + /** * ------------------------------------------------------------ * FormView @@ -887,32 +889,17 @@ openerp.mail = function(session) { this._super.apply(this, arguments); this.params = this.get_definition_options(); this.params.thread_level = this.params.thread_level || 0; - this.params.see_subscribers = true; - this.params.see_subscribers_options = this.params.see_subscribers_options || false; this.thread = null; this.ds = new session.web.DataSet(this, this.view.model); this.ds_users = new session.web.DataSet(this, 'res.users'); }, start: function() { - var self = this; - // NB: all the widget should be modified to check the actual_mode property on view, not use // any other method to know if the view is in create mode anymore this.view.on("change:actual_mode", this, this._check_visibility); this._check_visibility(); - mail.ChatterUtils.bind_events(this); - this.$element.find('button.oe_mail_button_followers').click(function () { self.do_toggle_followers(); }); - if (! this.params.see_subscribers_options) { - this.$element.find('button.oe_mail_button_followers').hide(); } - this.$element.find('button.oe_mail_button_follow').click(function () { self.do_follow(); }) - .mouseover(function () { $(this).html('Follow').removeClass('oe_mail_button_mouseout').addClass('oe_mail_button_mouseover'); }) - .mouseleave(function () { $(this).html('Not following').removeClass('oe_mail_button_mouseover').addClass('oe_mail_button_mouseout'); }); - this.$element.find('button.oe_mail_button_unfollow').click(function () { self.do_unfollow(); }) - .mouseover(function () { $(this).html('Unfollow').removeClass('oe_mail_button_mouseout').addClass('oe_mail_button_mouseover'); }) - .mouseleave(function () { $(this).html('Following').removeClass('oe_mail_button_mouseover').addClass('oe_mail_button_mouseout'); }); - this.reinit(); }, _check_visibility: function() { @@ -923,72 +910,20 @@ openerp.mail = function(session) { this._super.apply(this, arguments); }, - reinit: function() { - this.params.see_subscribers = true; - this.params.see_subscribers_options = this.params.see_subscribers_options || false; - this.$element.find('button.oe_mail_button_followers').html('Hide followers') - this.$element.find('button.oe_mail_button_follow').hide(); - this.$element.find('button.oe_mail_button_unfollow').hide(); - }, - set_value: function() { this._super.apply(this, arguments); - var self = this; - this.reinit(); if (! this.view.datarecord.id || session.web.BufferedDataSet.virtual_id_regex.test(this.view.datarecord.id)) { this.$element.find('.oe_mail_thread').hide(); return; } - // fetch followers - var fetch_sub_done = this.fetch_subscribers(); // create and render Thread widget this.$element.find('div.oe_mail_recthread_main').empty(); if (this.thread) this.thread.destroy(); this.thread = new mail.Thread(this, {'res_model': this.view.model, 'res_id': this.view.datarecord.id, 'uid': this.session.uid, 'thread_level': this.params.thread_level, 'show_post_comment': true, 'limit': 15}); var thread_done = this.thread.appendTo(this.$element.find('div.oe_mail_recthread_main')); - return fetch_sub_done && thread_done; - }, - - fetch_subscribers: function () { - return this.ds.call('message_read_subscribers', [[this.view.datarecord.id]]).then(this.proxy('display_subscribers')); - }, - - display_subscribers: function (records) { - var self = this; - this.is_subscriber = false; - var user_list = this.$element.find('ul.oe_mail_followers_display').empty(); - this.$element.find('div.oe_mail_recthread_followers h4').html('Followers (' + records.length + ')'); - _(records).each(function (record) { - if (record.id == self.session.uid) { self.is_subscriber = true; } - record.avatar_url = mail.ChatterUtils.get_image(self.session.prefix, self.session.session_id, 'res.users', 'image_small', record.id); - $(session.web.qweb.render('mail.record_thread.subscriber', {'record': record})).appendTo(user_list); - }); - if (self.is_subscriber) { - self.$element.find('button.oe_mail_button_follow').hide(); - self.$element.find('button.oe_mail_button_unfollow').show(); } - else { - self.$element.find('button.oe_mail_button_follow').show(); - self.$element.find('button.oe_mail_button_unfollow').hide(); } - }, - - do_follow: function () { - return this.ds.call('message_subscribe', [[this.view.datarecord.id]]).pipe(this.proxy('fetch_subscribers')); - }, - - do_unfollow: function () { - var self = this; - return this.ds.call('message_unsubscribe', [[this.view.datarecord.id]]).then(function (record) { - if (record == false) self.do_notify("Impossible to unsubscribe", "You are automatically subscribed to this record. You cannot unsubscribe."); - }).pipe(this.proxy('fetch_subscribers')); - }, - - do_toggle_followers: function () { - this.params.see_subscribers = ! this.params.see_subscribers; - if (this.params.see_subscribers) { this.$element.find('button.oe_mail_button_followers').html('Hide followers'); } - else { this.$element.find('button.oe_mail_button_followers').html('Show followers'); } - this.$element.find('div.oe_mail_recthread_followers').toggle(); + return thread_done; }, }); @@ -1021,6 +956,8 @@ openerp.mail = function(session) { this.params.limit = params.limit || 25; this.params.domain = params.domain || []; this.params.context = params.context || {}; + this.params.res_model = params.res_model || false; + this.params.res_id = params.res_id || false; this.params.search_view_id = params.search_view_id || false; this.params.thread_level = params.thread_level || 1; this.comments_structure = {'root_ids': [], 'new_root_ids': [], 'msgs': {}, 'tree_struct': {}, 'model_to_root_ids': {}}; @@ -1068,8 +1005,8 @@ openerp.mail = function(session) { this.compose_message_widget.destroy(); } this.compose_message_widget = new mail.ComposeMessage(this, { - 'extended_mode': false, 'uid': this.session.uid, 'res_model': 'res.users', - 'res_id': this.session.uid, 'mode': mode || 'comment', 'msg_id': msg_id }); + 'extended_mode': false, 'uid': this.session.uid, 'res_model': this.params.res_model, + 'res_id': this.params.res_id, 'mode': mode || 'comment', 'msg_id': msg_id }); var composition_node = this.$element.find('div.oe_mail_wall_action'); composition_node.empty(); var compose_done = this.compose_message_widget.appendTo(composition_node); diff --git a/addons/mail/static/src/js/mail_followers.js b/addons/mail/static/src/js/mail_followers.js new file mode 100644 index 00000000000..0e920f73041 --- /dev/null +++ b/addons/mail/static/src/js/mail_followers.js @@ -0,0 +1,122 @@ +openerp_mail_followers = function(session, mail) { + var _t = session.web._t, + _lt = session.web._lt; + + var mail_followers = session.mail_followers = {}; + + /** + * ------------------------------------------------------------ + * mail_followers Widget + * ------------------------------------------------------------ + * + * This widget handles the display of a list of records as a vetical + * list, with an image on the left. The widget itself is a floatting + * right-sided box. + * This widget is mainly used to display the followers of records + * in OpenChatter. + */ + + /* Add the widget to registry */ + session.web.form.widgets.add('mail_followers', 'openerp.mail_followers.Followers'); + + mail_followers.Followers = session.web.form.AbstractField.extend({ + template: 'mail.followers', + + init: function() { + this._super.apply(this, arguments); + this.params = {}; + this.params.image = this.node.attrs.image || 'image_small'; + this.params.title = this.node.attrs.title || 'Followers'; + this.params.display_followers = true; + this.params.display_control = this.node.attrs.display_control || false; + this.params.display_actions = this.node.attrs.display_actions || false; + this.ds_model = new session.web.DataSetSearch(this, this.view.model); + this.ds_follow = new session.web.DataSetSearch(this, this.field.relation); + }, + + start: function() { + var self = this; + // NB: all the widget should be modified to check the actual_mode property on view, not use + // any other method to know if the view is in create mode anymore + this.view.on("change:actual_mode", this, this._check_visibility); + this._check_visibility(); + this.$element.find('button.oe_mail_button_followers').click(function () { self.do_toggle_followers(); }); + if (! this.params.display_control) { + this.$element.find('button.oe_mail_button_followers').hide(); } + this.$element.find('button.oe_mail_button_follow').click(function () { self.do_follow(); }) + .mouseover(function () { $(this).html('Follow').removeClass('oe_mail_button_mouseout').addClass('oe_mail_button_mouseover'); }) + .mouseleave(function () { $(this).html('Not following').removeClass('oe_mail_button_mouseover').addClass('oe_mail_button_mouseout'); }); + this.$element.find('button.oe_mail_button_unfollow').click(function () { self.do_unfollow(); }) + .mouseover(function () { $(this).html('Unfollow').removeClass('oe_mail_button_mouseout').addClass('oe_mail_button_mouseover'); }) + .mouseleave(function () { $(this).html('Following').removeClass('oe_mail_button_mouseover').addClass('oe_mail_button_mouseout'); }); + this.reinit(); + }, + + _check_visibility: function() { + this.$element.toggle(this.view.get("actual_mode") !== "create"); + }, + + destroy: function () { + this._super.apply(this, arguments); + }, + + reinit: function() { + this.params.display_followers = true; + this.params.display_control = this.node.attrs.display_control || false; + this.params.display_actions = this.node.attrs.display_actions || false; + this.$element.find('button.oe_mail_button_followers').html('Hide followers') + this.$element.find('button.oe_mail_button_follow').hide(); + this.$element.find('button.oe_mail_button_unfollow').hide(); + }, + + set_value: function(value_) { + this.reinit(); + if (! this.view.datarecord.id || + session.web.BufferedDataSet.virtual_id_regex.test(this.view.datarecord.id)) { + this.$element.find('div.oe_mail_recthread_aside').hide(); + return; + } + return this.fetch_subscribers(value_); + }, + + fetch_subscribers: function (value_) { + return this.ds_follow.call('read', [value_ || this.get_value(), ['name', this.params.image]]).then(this.proxy('display_subscribers')); + }, + + /** + * Display the followers. + * TODO: replace the is_subscriber check by fields read */ + display_subscribers: function (records) { + var self = this; + this.is_subscriber = false; + var user_list = this.$element.find('ul.oe_mail_followers_display').empty(); + this.$element.find('div.oe_mail_recthread_followers h4').html(this.params.title + ' (' + records.length + ')'); + _(records).each(function (record) { + if (record.id == self.session.uid) { self.is_subscriber = true; } + record.avatar_url = mail.ChatterUtils.get_image(self.session.prefix, self.session.session_id, 'res.users', 'image_small', record.id); + $(session.web.qweb.render('mail.followers.partner', {'record': record})).appendTo(user_list); + }); + if (this.is_subscriber) { + this.$element.find('button.oe_mail_button_follow').hide(); + this.$element.find('button.oe_mail_button_unfollow').show(); } + else { + this.$element.find('button.oe_mail_button_follow').show(); + this.$element.find('button.oe_mail_button_unfollow').hide(); } + }, + + do_follow: function () { + return this.ds_model.call('message_subscribe', [[this.view.datarecord.id]]).pipe(this.proxy('set_value')); + }, + + do_unfollow: function () { + return this.ds_model.call('message_unsubscribe', [[this.view.datarecord.id]]).pipe(this.proxy('set_value')); + }, + + do_toggle_followers: function () { + this.params.see_subscribers = ! this.params.see_subscribers; + if (this.params.see_subscribers) { this.$element.find('button.oe_mail_button_followers').html('Hide followers'); } + else { this.$element.find('button.oe_mail_button_followers').html('Show followers'); } + this.$element.find('div.oe_mail_recthread_followers').toggle(); + }, + }); +}; diff --git a/addons/mail/static/src/xml/mail.xml b/addons/mail/static/src/xml/mail.xml index bf29ada9f63..00c7cd1d3c6 100644 --- a/addons/mail/static/src/xml/mail.xml +++ b/addons/mail/static/src/xml/mail.xml @@ -16,7 +16,10 @@ <tr class="oe_header_row oe_header_row_top"> <td colspan="2"> <h2 class="oe_view_title"> - <span class="oe_view_title_text">All Feeds</span> + <span class="oe_view_title_text"> + <t t-if="! widget.params.res_model">News Feeds</t> + <t t-if="widget.params.res_model" t-esc="widget.params.res_model"/> + </span> </h2> </td> <td><div class="oe_view_manager_view_search" t-opentag="true"/></td> @@ -58,29 +61,9 @@ <div class="oe_mail_recthread_main"> <!-- contains the document thread --> </div> - <div class="oe_mail_recthread_aside"> - <div class="oe_mail_recthread_actions"> - <button type="button" class="oe_mail_button_follow oe_mail_button_mouseout">Not following</button> - <button type="button" class="oe_mail_button_unfollow oe_mail_button_mouseout">Following</button> - <button type="button" class="oe_mail_button_followers">Show followers</button> - </div> - <div class="oe_mail_recthread_followers"> - <h4>Followers</h4> - <ul class="oe_mail_followers_display"></ul> - </div> - </div> </div> <!-- - record_thread.subscriber template - Template used to display a subscriber. - --> - <li t-name="mail.record_thread.subscriber"> - <img class="oe_mail_thumbnail oe_mail_frame" t-attf-src="{record.avatar_url}"/> - <a t-attf-href="#model=res.users&id=#{record.id}"><t t-raw="record.name"/></a> - </li> - - <!-- mail.compose_message template This template holds the composition form to write a note or send an e-mail. It contains by default a textarea, that will be replaced diff --git a/addons/mail/static/src/xml/mail_followers.xml b/addons/mail/static/src/xml/mail_followers.xml new file mode 100644 index 00000000000..53c899b9569 --- /dev/null +++ b/addons/mail/static/src/xml/mail_followers.xml @@ -0,0 +1,31 @@ +<?xml version="1.0" encoding="UTF-8"?> +<template> + + <!-- + followers main template + Template used to display the followers and the actions in a record. + --> + <div t-name="mail.followers" class="oe_mail_recthread_aside"> + <div class="oe_mail_recthread_actions"> + <button type="button" class="oe_mail_button_follow oe_mail_button_mouseout">Not following</button> + <button type="button" class="oe_mail_button_unfollow oe_mail_button_mouseout">Following</button> + <button type="button" class="oe_mail_button_followers">Show followers</button> + </div> + <div class="oe_mail_recthread_followers"> + <t t-if="widget.params.title"> + <h4><t t-raw="widget.params.title"/></h4> + </t> + <ul class="oe_mail_followers_display"></ul> + </div> + </div> + + <!-- + followers.partner template + Template used to display a partner following the record + --> + <li t-name="mail.followers.partner"> + <img class="oe_mail_thumbnail oe_mail_frame" t-attf-src="{record.avatar_url}"/> + <a t-attf-href="#model=res.users&id=#{record.id}"><t t-raw="record.name"/></a> + </li> + +</template> diff --git a/addons/mail/tests/__init__.py b/addons/mail/tests/__init__.py new file mode 100644 index 00000000000..d63c5634cc3 --- /dev/null +++ b/addons/mail/tests/__init__.py @@ -0,0 +1,27 @@ +# -*- coding: utf-8 -*- +############################################################################## +# +# OpenERP, Open Source Business Applications +# Copyright (c) 2012-TODAY OpenERP S.A. <http://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 . import test_mail + +checks = [ + test_mail, +] + +# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
\ No newline at end of file diff --git a/addons/mail/tests/test_mail.py b/addons/mail/tests/test_mail.py new file mode 100644 index 00000000000..0940d72c490 --- /dev/null +++ b/addons/mail/tests/test_mail.py @@ -0,0 +1,112 @@ +# -*- coding: utf-8 -*- +############################################################################## +# +# OpenERP, Open Source Business Applications +# Copyright (c) 2012-TODAY OpenERP S.A. <http://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 openerp.tests import common + +MAIL_TEMPLATE = """Return-Path: <whatever-2a840@postmaster.twitter.com> +To: {to} +Received: by mail1.openerp.com (Postfix, from userid 10002) + id 5DF9ABFB2A; Fri, 10 Aug 2012 16:16:39 +0200 (CEST) +From: Sylvie Lelitre <sylvie.lelitre@agrolait.com> +Subject: {subject} +MIME-Version: 1.0 +Content-Type: multipart/alternative; + boundary="----=_Part_4200734_24778174.1344608186754" +Date: Fri, 10 Aug 2012 14:16:26 +0000 +Message-ID: <1198923581.41972151344608186760.JavaMail@agrolait.com> +{extra} +------=_Part_4200734_24778174.1344608186754 +Content-Type: text/plain; charset=utf-8 +Content-Transfer-Encoding: quoted-printable + +Please call me as soon as possible this afternoon! + +-- +Sylvie +------=_Part_4200734_24778174.1344608186754 +Content-Type: text/html; charset=utf-8 +Content-Transfer-Encoding: quoted-printable + +<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd"> +<html> + <head>=20 + <meta http-equiv=3D"Content-Type" content=3D"text/html; charset=3Dutf-8" /> + </head>=20 + <body style=3D"margin: 0; padding: 0; background: #ffffff;-webkit-text-size-adjust: 100%;">=20 + + <p>Please call me as soon as possible this afternoon!</p> + + <p>--<br/> + Sylvie + <p> + </body> +</html> +------=_Part_4200734_24778174.1344608186754-- +""" + + +class test_mail(common.TransactionCase): + + def setUp(self): + super(test_mail, self).setUp() + self.ir_model = self.registry('ir.model') + self.mail_alias = self.registry('mail.alias') + self.mail_thread = self.registry('mail.thread') + self.mail_group = self.registry('mail.group') + self.res_users = self.registry('res.users') + + # groups@.. will cause the creation of new mail groups + self.mail_group_model_id = self.ir_model.search(self.cr, self.uid, [('model','=', 'mail.group')])[0] + self.mail_alias.create(self.cr, self.uid, {'alias_name': 'groups', + 'alias_model_id': self.mail_group_model_id}) + + # tech@... will append new messages to the 'tech' group + self.group_tech_id = self.mail_group.create(self.cr, self.uid, {'name': 'tech'}) + + def test_message_process(self): + # Incoming mail creates a new mail_group "frogs" + self.assertEqual(self.mail_group.search(self.cr, self.uid, [('name','=','frogs')]), []) + mail_frogs = MAIL_TEMPLATE.format(to='groups@example.com, other@gmail.com', subject='frogs', extra='') + self.mail_thread.message_process(self.cr, self.uid, None, mail_frogs) + frog_groups = self.mail_group.search(self.cr, self.uid, [('name','=','frogs')]) + self.assertTrue(len(frog_groups) == 1) + + # Previously-created group can be emailed now - it should have an implicit alias group+frogs@... + frog_group = self.mail_group.browse(self.cr, self.uid, frog_groups[0]) + group_messages = frog_group.message_ids + self.assertTrue(len(group_messages) == 1, 'New group should only have the original message') + mail_frog_news = MAIL_TEMPLATE.format(to='Friendly Frogs <group+frogs@example.com>', subject='news', extra='') + self.mail_thread.message_process(self.cr, self.uid, None, mail_frog_news) + frog_group.refresh() + self.assertTrue(len(frog_group.message_ids) == 2, 'Group should contain 2 messages now') + + # Even with a wrong destination, a reply should end up in the correct thread + mail_reply = MAIL_TEMPLATE.format(to='erroneous@example.com>', subject='Re: news', + extra='In-Reply-To: <12321321-openerp-%d-mail.group@example.com>\n'%frog_group.id) + self.mail_thread.message_process(self.cr, self.uid, None, mail_reply) + frog_group.refresh() + self.assertTrue(len(frog_group.message_ids) == 3, 'Group should contain 3 messages now') + + # No model passed and no matching alias must raise + mail_spam = MAIL_TEMPLATE.format(to='noone@example.com', subject='spam', extra='') + self.assertRaises(Exception, + self.mail_thread.message_process, + self.cr, self.uid, None, mail_spam) diff --git a/addons/mail/wizard/mail_compose_message.py b/addons/mail/wizard/mail_compose_message.py index d26511464f8..fe87b7584e0 100644 --- a/addons/mail/wizard/mail_compose_message.py +++ b/addons/mail/wizard/mail_compose_message.py @@ -100,7 +100,7 @@ class mail_compose_message(osv.TransientModel): # Try to provide default email_from if not specified yet if not result.get('email_from'): current_user = self.pool.get('res.users').browse(cr, uid, uid, context=context) - result['email_from'] = current_user.user_email or False + result['email_from'] = current_user.email or False return result _columns = { @@ -133,7 +133,7 @@ class mail_compose_message(osv.TransientModel): result.update({ 'model': model, 'res_id': res_id, - 'email_from': user.user_email or tools.config.get('email_from', False), + 'email_from': user.email or tools.config.get('email_from', False), 'body_html': False, 'body_text': False, 'subject': False, @@ -223,7 +223,7 @@ class mail_compose_message(osv.TransientModel): 'dest_partner_ids': dest_partner_ids, 'model': message_data.model or False, 'res_id': message_data.res_id or False, - 'email_from': current_user.user_email or message_data.email_to or False, + 'email_from': current_user.email or message_data.email_to or False, 'email_to': message_data.reply_to or message_data.email_from or False, 'email_cc': message_data.email_cc or False, 'user_id': uid, diff --git a/addons/mail/wizard/mail_compose_message_view.xml b/addons/mail/wizard/mail_compose_message_view.xml index 8fa4eaa9c50..73fb03cdc28 100644 --- a/addons/mail/wizard/mail_compose_message_view.xml +++ b/addons/mail/wizard/mail_compose_message_view.xml @@ -4,7 +4,6 @@ <record model="ir.ui.view" id="email_compose_message_wizard_form"> <field name="name">mail.compose.message.form</field> <field name="model">mail.compose.message</field> - <field name="type">form</field> <field name="arch" type="xml"> <form string="Compose Email" version="7.0"> <group> @@ -41,7 +40,6 @@ <field name="name">mail.compose.message.form</field> <field name="model">mail.compose.message</field> <field name="priority">18</field> - <field name="type">form</field> <field name="arch" type="xml"> <form string="Compose Email" version="7.0" > <group> @@ -50,7 +48,7 @@ <field name="body_text" colspan="2" nolabel="1" placeholder="What are you working on ?" class="oe_mail_compose_message_body_text"/> <field name="body_html" colspan="2" nolabel="1" placeholder="What are you working on HTML ?" - class="oe_mail_compose_message_body_html oe_mail_compose_message_invisible" widget="text_html"/> + class="oe_mail_compose_message_body_html oe_mail_compose_message_invisible"/> <field name="dest_partner_ids" colspan="2" nolabel="1" widget="many2many_tags" placeholder="Add contacts to notify..." class="oe_mail_compose_message_partner_ids oe_mail_compose_message_invisible"/> |
