diff --git a/addons/mail/mail_message_subtype.py b/addons/mail/mail_message_subtype.py index 8a9bac86f0c..12da272fa4b 100644 --- a/addons/mail/mail_message_subtype.py +++ b/addons/mail/mail_message_subtype.py @@ -28,7 +28,7 @@ class mail_message_subtype(osv.osv): the follower subscription, allowing only some subtypes to be pushed on the Wall. """ _name = 'mail.message.subtype' - _description = 'mail_message_subtype' + _description = 'Message subtypes' _columns = { 'name': fields.char('Message Type', required=True, translate=True, help='Message subtype gives a more precise type on the message, '\ @@ -36,10 +36,13 @@ class mail_message_subtype(osv.osv): 'a notification related to a new record (New), or to a stage '\ 'change in a process (Stage change). Message subtypes allow to '\ 'precisely tune the notifications the user want to receive on its wall.'), + 'description': fields.text('Description', translate=True, + help='Description that will be added in the message posted for this '\ + 'subtype. If void, no message will be added.'), 'res_model': fields.char('Model', - help="Related model of the subtype. If False, this subtype exists for all models."), + help="Model the subtype applies to. If False, this subtype exists for all models."), 'default': fields.boolean('Default', - help="Checked by default when subscribing."), + help="Activated by default when subscribing."), } _defaults = { 'default': True, diff --git a/addons/mail/mail_message_subtype.xml b/addons/mail/mail_message_subtype.xml index c1cd510642d..3084a98d481 100644 --- a/addons/mail/mail_message_subtype.xml +++ b/addons/mail/mail_message_subtype.xml @@ -25,6 +25,7 @@ + diff --git a/addons/mail/mail_thread.py b/addons/mail/mail_thread.py index 47d807478d4..cdd22d14134 100644 --- a/addons/mail/mail_thread.py +++ b/addons/mail/mail_thread.py @@ -20,10 +20,8 @@ ############################################################################## import base64 -from collections import defaultdict import dateutil import email -from functools import partial import logging import pytz import time @@ -74,11 +72,18 @@ class mail_thread(osv.AbstractModel): _name = 'mail.thread' _description = 'Email Thread' _mail_flat_thread = True + _TRACK_TEMPLATE = """ - ${updated_fields} + %if message_description: + ${message_description} + %endif
    - %for chg in changes: -
  • ${chg[0]}: ${chg[1]} -> ${chg[2]}
  • + %for name, change in tracked_values.items(): +
  • ${name}: + %if change.get('old_value'): + ${change.get('old_value')} -> + %endif + ${change.get('new_value')}
  • %endfor
""" @@ -222,18 +227,37 @@ class mail_thread(osv.AbstractModel): } #------------------------------------------------------ - # Automatic subscription when creating + # CRUD overrides for automatic subscription and logging #------------------------------------------------------ - def create(self, cr, uid, vals, context=None): + def create(self, cr, uid, values, context=None): """ Override to subscribe the current user. """ if context is None: context = {} - thread_id = super(mail_thread, self).create(cr, uid, vals, context=context) + thread_id = super(mail_thread, self).create(cr, uid, values, context=context) + + # subscribe uid unless asked not to if not context.get('mail_nosubscribe'): self.message_subscribe_users(cr, uid, [thread_id], [uid], context=context) + + # automatic logging + # self.message_post(cr, uid, thread_id, body='Document created.', context=context) + return thread_id + def write(self, cr, uid, ids, values, context=None): + tracked_fields = self._get_tracked_fields(cr, uid, values.keys(), context=context) + to_log = [name for name in values.keys() if name in tracked_fields] + if to_log: + initial = self.read(cr, uid, ids, [name for name, info in tracked_fields.items()], context=context) + initial_values = dict((item['id'], item) for item in initial) + + result = super(mail_thread, self).write(cr, uid, ids, values, context=context) + + if to_log: + self.message_track(cr, uid, ids, values.keys(), initial_values, context=context) + return result + def unlink(self, cr, uid, ids, context=None): """ Override unlink to delete messages and followers. This cannot be cascaded, because link is done through (res_model, res_id). """ @@ -259,82 +283,96 @@ class mail_thread(osv.AbstractModel): # Automatically log tracked fields #------------------------------------------------------ - def write(self, cr, uid, ids, values, context=None): - - if context is None: - context = {} - #import pudb;pudb.set_trace() + def _get_tracked_fields(self, cr, uid, updated_fields, context=None): + """ Return a structure of tracked fields for the current model. + :param list updated_fields: modified field names + :return list: a list of (field_name, column_info obj), containing + always tracked fields and modified on_change fields + """ + return dict((name, column_info) + for name, column_info in self._all_columns.items() + if getattr(column_info.column, '_track_visibility', False) == 2 + or (getattr(column_info.column, '_track_visibility', False) == 1 and name in updated_fields)) - def false_value(f): - if f._type == 'boolean': + def message_track(self, cr, uid, ids, updated_fields, initial_values, log_message='', context=None): + """ + :param list updated_fields: modified field names + """ + translation_obj = self.pool.get('ir.translation') + + def format_false_value(field_obj): + if field_obj._type == 'boolean': return False - return f._symbol_set[1](False) + return field_obj._symbol_set[1](False) - def convert_for_comparison(v, f): - # It will convert value for comparison between current and new. - if not v: - return false_value(f) - if isinstance(v, browse_record): - return v.id - return v + def convert_for_comparison(value, field_obj): + if not value: + return format_false_value(field_obj) + if isinstance(value, browse_record): # compare browse record on id only + return value.id + if isinstance(value, tuple) and len(value) == 2: # name_get result + return value[0] + return value - tracked = dict((n, f) for n, f in self._all_columns.items() if getattr(f.column, 'tracked', False)) - to_log = [k for k in values if k in tracked] + def convert_for_display(value, field_obj): + if not value: + return format_false_value(field_obj) + if field_obj._type == 'many2one': + if isinstance(value, tuple) and len(value) == 2: # already name_get result + return value[1] + if not isinstance(value, browse_record): # value should be an ID + value = self.pool.get(field_obj._obj).browse(cr, SUPERUSER_ID, value, context=None) + return value.name_get()[0][1] + if field_obj._type == 'selection': # CHS/TDE TODO: translated value ? + select_value = filter(lambda item: item[0] == value, field_obj.selection) + return select_value[0][1] + return value - from_ = None - changes = defaultdict(list) - if to_log: - for record in self.browse(cr, uid, ids, context): - for tl in to_log: - column = tracked[tl].column - current = convert_for_comparison(record[tl], column) - new = convert_for_comparison(values[tl], column) - if new != current: - changes[record].append(tl) - from_ = record[tl] + def translate_field(column_info): + model = column_info.parent_model or self._name + return translation_obj._get_source(cr, uid, '{0},{1}'.format(model, column_info.name), 'field', context.get('lang'), column_info.column.string) - result = super(mail_thread, self).write(cr, uid, ids, values, context=context) + tracked_fields = self._get_tracked_fields(cr, uid, updated_fields, context=context) + to_log = [name for name in updated_fields if name in tracked_fields] + if not to_log: + return True - updated_fields = _('Updated Fields:') + # browse with SUPERUSER_ID to avoid rights issues (i.e. tracking res.partner relational field -> name_get result should always be visible) + for record in self.browse(cr, SUPERUSER_ID, ids, context=context): + tracked_values = {} + default_log = True + changes_found = False + initial = initial_values[record.id] - Trans = self.pool['ir.translation'] - def _t(c): - # translate field - model = c.parent_model or self._name - lang = context.get('lang') - return Trans._get_source(cr, uid, '{0},{1}'.format(model, c.name), 'field', lang, ci.column.string) - - def get_subtype(model, record): - # it will return subtype name(xml_id) for stage. - record_model = self.pool[model].browse(cr, SUPERUSER_ID, record) - if record_model.__hasattr__('subtype'): - return record_model.subtype - return False + # generate tracked_values data structure: {'col_name': {col_info, new_value, old_value}} + for col_name, col_info in tracked_fields.items(): + old_value = convert_for_comparison(initial[col_name], col_info.column) + new_value = convert_for_comparison(record[col_name], col_info.column) + if old_value == new_value and col_info.column._track_visibility == 2: + tracked_values[col_name] = dict(col_info=col_info, new_value=convert_for_display(record[col_name], col_info.column)) + elif old_value != new_value: + tracked_values[col_name] = dict(col_info=col_info, old_value=convert_for_display(initial[col_name], col_info.column), new_value=convert_for_display(record[col_name], col_info.column)) + changes_found = True + if not changes_found: + continue - for record, changed_fields in changes.items(): - # TODO tpl changed_fields - chg = [] - subtype = False - for f in changed_fields: - to = self.browse(cr, uid, ids[0], context)[f] - ci = tracked[f] - if ci.column._type == "many2one": - if to: - to = to.name_get()[0][1] - else: - to = "Removed" - if isinstance(from_, browse_record): - from_ = from_.name_get()[0][1] - - subtype = get_subtype(ci.column._obj,values[f]) - chg.append((_t(ci), from_, to)) + # find subtypes and post messages or log if no subtype found + subtypes = set([subtype for field, track_info in self._track.items() if field in to_log + for subtype, method in track_info.items() if method(self, cr, uid, record, context)]) + for subtype in subtypes: + subtype_data = subtype.split('.') + subtype_ref = self.pool.get('ir.model.data').get_object_reference(cr, uid, subtype_data[0], subtype_data[1]) + if not subtype_ref: + continue + subtype_rec = self.pool.get('mail.message.subtype').browse(cr, uid, subtype_ref[1], context=context) + message = MakoTemplate(self._TRACK_TEMPLATE).render_unicode(message_description=subtype_rec.description, tracked_values=tracked_values) + self.message_post(cr, uid, record.id, body=message, subtype=subtype, context=context) + default_log = False + if default_log: + message = MakoTemplate(self._TRACK_TEMPLATE).render_unicode(message_description=log_message, tracked_values=tracked_values) + self.message_post(cr, uid, record.id, body=message, context=context) - message = MakoTemplate(self._TRACK_TEMPLATE).render_unicode(updated_fields=updated_fields, - changes=chg) - - record.message_post(message,subtype=subtype) - - return result + return True #------------------------------------------------------ # mail.message wrappers and tools diff --git a/addons/mail/tests/test_mail_base.py b/addons/mail/tests/test_mail_base.py index b5ce495e713..b71e31ed872 100644 --- a/addons/mail/tests/test_mail_base.py +++ b/addons/mail/tests/test_mail_base.py @@ -51,6 +51,7 @@ class TestMailBase(common.TransactionCase): # Usefull models self.ir_model = self.registry('ir.model') + self.ir_model_data = self.registry('ir.model.data') self.ir_attachment = self.registry('ir.attachment') self.mail_alias = self.registry('mail.alias') self.mail_thread = self.registry('mail.thread') diff --git a/addons/mail/tests/test_mail_features.py b/addons/mail/tests/test_mail_features.py index 1b5dfab68ad..3666c139279 100644 --- a/addons/mail/tests/test_mail_features.py +++ b/addons/mail/tests/test_mail_features.py @@ -588,31 +588,92 @@ class test_mail(TestMailBase): na_demo_group = self.mail_message._needaction_count(cr, user_raoul.id, domain=[('model', '=', 'mail.group'), ('res_id', '=', self.group_pigs_id)]) self.assertEqual(na_demo, na_demo_base + 0, 'Demo should have 0 new needaction') self.assertEqual(na_demo_group, 0, 'Demo should have 0 needaction related to Pigs') - + def test_40_track_field(self): - """ Test designed for tracking field. """ - - cr, uid, user_admin, user_raoul, group_pigs = self.cr, self.uid, self.user_admin, self.user_raoul, self.group_pigs - partner_user_id = self.res_partner._columns.get('user_id') + """ Testing auto tracking of fields. """ + def _strip_string_spaces(body): + return body.replace(' ', '').replace('\n', '') - # Set tracked attribute as True for tracked field. - partner_user_id.tracked = True - p_d_id = self.res_partner.create(cr, uid, {'name': 'Osbert Armour', 'notification_email_send': 'all'}) + cr, uid = self.cr, self.uid - #Set Raoul as sales person. - self.res_partner.write(cr, uid, [p_d_id], {'user_id' : user_raoul.id}) - mail_ids = self.mail_message.search(cr, uid, [('model', '=', 'res.partner'),('res_id','=',p_d_id)]) + # Data: subscribe Raoul to Pigs, because he will change the public attribute and may loose access to the record + self.mail_group.message_subscribe_users(cr, uid, [self.group_pigs_id], [self.user_raoul_id]) - #Test: tracked record logged in openchatter - self.assertEqual(1,len(mail_ids), ' After change in field logged in openchatter.') + # Data: res.users.group, to test group_public_id automatic logging + group_system_ref = self.registry('ir.model.data').get_object_reference(cr, uid, 'base', 'group_system') + group_system_id = group_system_ref and group_system_ref[1] or False - # Set tracked attribute as False for tracked field. - partner_user_id.tracked = False - p_a_id = self.res_partner.create(cr, uid, {'name': 'Timmy Simons', 'notification_email_send': 'all'}) + # Data: custom subtypes + mt_private_id = self.mail_message_subtype.create(cr, uid, {'name': 'private', 'description': 'Private public'}) + self.ir_model_data.create(cr, uid, {'name': 'mt_private', 'model': 'mail.group', 'module': 'mail', 'res_id': mt_private_id}) + mt_name_supername_id = self.mail_message_subtype.create(cr, uid, {'name': 'name_supername', 'description': 'Supername name'}) + self.ir_model_data.create(cr, uid, {'name': 'mt_name_supername', 'model': 'mail.group', 'module': 'mail', 'res_id': mt_name_supername_id}) + mt_group_public_id = self.mail_message_subtype.create(cr, uid, {'name': 'group_public', 'description': 'Group changed'}) + self.ir_model_data.create(cr, uid, {'name': 'mt_group_public', 'model': 'mail.group', 'module': 'mail', 'res_id': mt_group_public_id}) - #Set Admin as sales person. - self.res_partner.write(cr, uid, [p_a_id], {'user_id' : user_admin.id}) - mail_a_ids = self.mail_message.search(cr, uid, [('model', '=', 'res.partner'),('res_id','=',p_a_id)]) + # Data: alter mail_group model for testing purposes (test on classic, selection and many2one fields) + self.mail_group._track = { + 'public': { + 'mail.mt_private': lambda self, cr, uid, obj, ctx=None: obj.public == 'private', + }, + 'name': { + 'mail.mt_name_supername': lambda self, cr, uid, obj, ctx=None: obj.name == 'supername', + }, + 'group_public_id': { + 'mail.mt_group_public': lambda self, cr, uid, obj, ctx=None: True, + }, + } + public_col = self.mail_group._columns.get('public') + name_col = self.mail_group._columns.get('name') + group_public_col = self.mail_group._columns.get('group_public_id') + public_col._track_visibility = 1 + name_col._track_visibility = 2 + group_public_col._track_visibility = 1 - #Test: tracked record logged in openchatter. - self.assertEqual(len(mail_a_ids),0,' No more logged openchatter.') + # Test: change name -> always tracked, not related to a subtype + self.mail_group.write(cr, self.user_raoul_id, [self.group_pigs_id], {'public': 'public'}) + self.group_pigs.refresh() + self.assertEqual(len(self.group_pigs.message_ids), 1, 'tracked: a message should have been produced') + # Test: first produced message: no subtype, name change tracked + last_msg = self.group_pigs.message_ids[-1] + self.assertFalse(last_msg.subtype_id, 'tracked: message should not have been linked to a subtype') + self.assertIn('SelectedGroupOnly->Public', _strip_string_spaces(last_msg.body), 'tracked: message body incorrect') + self.assertIn('Pigs', _strip_string_spaces(last_msg.body), 'tracked: message body does not hold always tracked field') + + # Test: change name as supername, public as private -> 2 subtypes + self.mail_group.write(cr, self.user_raoul_id, [self.group_pigs_id], {'name': 'supername', 'public': 'private'}) + self.group_pigs.refresh() + self.assertEqual(len(self.group_pigs.message_ids), 3, 'tracked: two messages should have been produced') + # Test: first produced message: mt_name_supername + last_msg = self.group_pigs.message_ids[-2] + self.assertEqual(last_msg.subtype_id.id, mt_private_id, 'tracked: message should be linked to mt_private subtype') + self.assertIn('Private public', last_msg.body, 'tracked: message body does not hold the subtype description') + self.assertIn('Pigs->supername', _strip_string_spaces(last_msg.body), 'tracked: message body incorrect') + # Test: second produced message: mt_name_supername + last_msg = self.group_pigs.message_ids[-3] + self.assertEqual(last_msg.subtype_id.id, mt_name_supername_id, 'tracked: message should be linked to mt_name_supername subtype') + self.assertIn('Supername name', last_msg.body, 'tracked: message body does not hold the subtype description') + self.assertIn('Public->Private', _strip_string_spaces(last_msg.body), 'tracked: message body incorrect') + self.assertIn('Pigs->supername', _strip_string_spaces(last_msg.body), 'tracked feature: message body does not hold always tracked field') + + # Test: change public as public, group_public_id -> 1 subtype, name always tracked + self.mail_group.write(cr, self.user_raoul_id, [self.group_pigs_id], {'public': 'public', 'group_public_id': group_system_id}) + self.group_pigs.refresh() + self.assertEqual(len(self.group_pigs.message_ids), 4, 'tracked: one message should have been produced') + # Test: first produced message: mt_group_public_id, with name always tracked, public tracked on change + last_msg = self.group_pigs.message_ids[-4] + self.assertEqual(last_msg.subtype_id.id, mt_group_public_id, 'tracked: message should not be linked to any subtype') + self.assertIn('Group changed', last_msg.body, 'tracked: message body does not hold the subtype description') + self.assertIn('Private->Public', _strip_string_spaces(last_msg.body), 'tracked: message body does not hold changed tracked field') + self.assertIn('HumanResources/Employee->Administration/Settings', _strip_string_spaces(last_msg.body), 'tracked: message body does not hold always tracked field') + + # Test: change not tracked field, no tracking message + self.mail_group.write(cr, self.user_raoul_id, [self.group_pigs_id], {'description': 'Dummy'}) + self.group_pigs.refresh() + self.assertEqual(len(self.group_pigs.message_ids), 4, 'tracked: No message should have been produced') + + # Data: removed changes + public_col._track_visibility = False + name_col._track_visibility = False + group_public_col._track_visibility = False + self.mail_group._track = {}