[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:
Thibault Delavallée
2012-12-18 13:25:58 +01:00
parent f9cc1d4513
commit bfea9dad64
5 changed files with 202 additions and 98 deletions
+6 -3
View File
@@ -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,
+1
View File
@@ -25,6 +25,7 @@
<field name="name"/>
<field name="res_model"/>
<field name="default"/>
<field name="description"/>
</group>
</sheet>
</form>
+112 -74
View File
@@ -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
+1
View File
@@ -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')
+82 -21
View File
@@ -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-&gt;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-&gt;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-&gt;Private', _strip_string_spaces(last_msg.body), 'tracked: message body incorrect')
self.assertIn('Pigs-&gt;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-&gt;Public', _strip_string_spaces(last_msg.body), 'tracked: message body does not hold changed tracked field')
self.assertIn('HumanResources/Employee-&gt;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 = {}