[REF] mail: refactored tracked feature. It is now based on a dict defined at model level, giving which subtypes are triggere when modifying some given fields. Cleaned code.
bzr revid: tde@openerp.com-20121218122558-mnrtlwouwai65gq6
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -25,6 +25,7 @@
|
||||
<field name="name"/>
|
||||
<field name="res_model"/>
|
||||
<field name="default"/>
|
||||
<field name="description"/>
|
||||
</group>
|
||||
</sheet>
|
||||
</form>
|
||||
|
||||
+112
-74
@@ -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 = """
|
||||
<span>${updated_fields}</span>
|
||||
%if message_description:
|
||||
<span>${message_description}</span>
|
||||
%endif
|
||||
<ul>
|
||||
%for chg in changes:
|
||||
<li><span>${chg[0]}</span>: ${chg[1]} -> ${chg[2]}</li>
|
||||
%for name, change in tracked_values.items():
|
||||
<li><span>${name}</span>:
|
||||
%if change.get('old_value'):
|
||||
${change.get('old_value')} ->
|
||||
%endif
|
||||
${change.get('new_value')}</li>
|
||||
%endfor
|
||||
</ul>
|
||||
"""
|
||||
@@ -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 <b>created</b>.', 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
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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 = {}
|
||||
|
||||
Reference in New Issue
Block a user