Skip to content
Permalink
Browse files

[IMP] mail: add tracking on computed stored fields

Also delay the tracking until the call to flush(), so that several
updates on a record generate a single tracking message.
  • Loading branch information
rco-odoo committed Nov 26, 2019
1 parent 09da30a commit 7886f5112015d46bb9193e921de7bfa95bee5ed5
@@ -27,6 +27,7 @@
from werkzeug import urls

from odoo import _, api, exceptions, fields, models, tools, registry, SUPERUSER_ID
from odoo.exceptions import MissingError
from odoo.osv import expression

from odoo.tools import pycompat, ustr
@@ -256,7 +257,9 @@ def create(self, vals_list):
- log a creation message
"""
if self._context.get('tracking_disable'):
return super(MailThread, self).create(vals_list)
threads = super(MailThread, self).create(vals_list)
threads._discard_tracking()
return threads

# subscribe uid unless asked not to
if not self._context.get('mail_create_nosubscribe'):
@@ -292,6 +295,7 @@ def create(self, vals_list):
threads_no_subtype._message_log_batch(bodies={t.id: body for t in threads_no_subtype})

# post track template if a tracked field changed
threads._discard_tracking()
if not self._context.get('mail_notrack'):
fields = self._get_tracked_fields()
for thread in threads:
@@ -308,35 +312,30 @@ def write(self, values):
if self._context.get('tracking_disable'):
return super(MailThread, self).write(values)

tracked_fields = None
if not self._context.get('mail_notrack'):
tracked_fields = self._get_tracked_fields()
if tracked_fields:
initial_values = {
record.id: {name: record[name] for name in tracked_fields}
for record in self
}
self._prepare_tracking(self._fields)

# Perform write
result = super(MailThread, self).write(values)

# update followers
self._message_auto_subscribe(values)

# Perform the tracking
if tracked_fields:
context = clean_context(self._context)
tracking = self.with_context(context).message_track(tracked_fields, initial_values)
if any(change for rec_id, (change, tracking_value_ids) in tracking.items()):
(changes, tracking_value_ids) = tracking[self[0].id]
self._message_track_post_template(changes)
return result

def _compute_field_value(self, field):
if not self._context.get('tracking_disable') and not self._context.get('mail_notrack'):
self._prepare_tracking(f.name for f in self._field_computed[field] if f.store)

return super()._compute_field_value(field)

def unlink(self):
""" Override unlink to delete messages and followers. This cannot be
cascaded, because link is done through (res_model, res_id). """
if not self:
return True
# discard pending tracking
self._discard_tracking()
self.env['mail.message'].search([('model', '=', self._name), ('res_id', 'in', self.ids), ('message_type', '!=', 'user_notification')]).unlink()
res = super(MailThread, self).unlink()
self.env['mail.followers'].sudo().search(
@@ -424,6 +423,46 @@ def fields_view_get(self, view_id=None, view_type='form', toolbar=False, submenu
# Technical methods / wrappers / tools
# ------------------------------------------------------

def _prepare_tracking(self, fields):
""" Prepare the tracking of ``fields`` for ``self``. """
fields = self._get_tracked_fields().intersection(fields)
if not fields:
return
func = self.browse()._finalize_tracking
[initial_values] = self.env.toflush(func, dict)
for record in self:
if not record.id:
continue
values = initial_values.setdefault(record.id, {})
if values is not None:
for name in fields:
values.setdefault(name, record[name])

def _discard_tracking(self):
""" Prevent any tracking of fields on ``self``. """
if not self._get_tracked_fields():
return
func = self.browse()._finalize_tracking
[initial_values] = self.env.toflush(func, dict)
# disable tracking by setting initial values to None
for id_ in self.ids:
initial_values[id_] = None

def _finalize_tracking(self, initial_values):
""" Generate the tracking messages for the records that have been
prepared with ``_prepare_tracking``.
"""
ids = [id_ for id_, vals in initial_values.items() if vals]
if not ids:
return
records = self.browse(ids).sudo()
fields = self._get_tracked_fields()
context = clean_context(self._context)
tracking = records.with_context(context).message_track(fields, initial_values)
for record in records:
changes, tracking_value_ids = tracking.get(record.id, (None, None))
record._message_track_post_template(changes)

def with_lang(self):
if not self._context.get("lang"):
return self.with_context(lang=self.env.user.lang)
@@ -540,12 +579,12 @@ def message_change_thread(self, new_thread):

@tools.ormcache()
def _get_tracked_fields(self):
""" Return the name of the tracked fields for the current model. """
return [
""" Return the set of tracked fields names for the current model. """
return {
name
for name, field in self._fields.items()
if getattr(field, 'tracking', None) or getattr(field, 'track_visibility', None)
]
}

def _creation_subtype(self):
""" Give the subtypes triggered by the creation of a record
@@ -606,6 +645,8 @@ def static_message_track(self, record, tracked_fields, initial):

# generate tracked_values data structure: {'col_name': {col_info, new_value, old_value}}
for col_name, col_info in tracked_fields.items():
if col_name not in initial:
continue
initial_value = initial[col_name]
new_value = record[col_name]

@@ -638,17 +679,21 @@ def message_track(self, tracked_fields, initial_values):
:param tracked_fields: iterable of field names to track
:param initial_values: mapping {record_id: {field_name: value}}
:return: mapping {record_id: (changed_field_names, tracking_value_ids)}
containing existing records only
"""
if not tracked_fields:
return True

tracked_fields = self.fields_get(tracked_fields)
tracking = dict()
for record in self:
tracking[record.id] = record._message_track(tracked_fields, initial_values[record.id])
try:
tracking[record.id] = record._message_track(tracked_fields, initial_values[record.id])
except MissingError:
continue

for record in self:
changes, tracking_value_ids = tracking[record.id]
changes, tracking_value_ids = tracking.get(record.id, (None, None))
if not changes:
continue

@@ -170,3 +170,14 @@ class MailTrackingModel(models.Model):
field_0 = fields.Char(tracking=True)
field_1 = fields.Char(tracking=True)
field_2 = fields.Char(tracking=True)


class MailCompute(models.Model):
_name = 'mail.test.compute'
_description = "Test model with several tracked computed fields"
_inherit = ['mail.thread']

partner_id = fields.Many2one('res.partner', tracking=True)
partner_name = fields.Char(related='partner_id.name', store=True, tracking=True)
partner_email = fields.Char(related='partner_id.email', store=True, tracking=True)
partner_phone = fields.Char(related='partner_id.phone', tracking=True)
@@ -17,3 +17,4 @@ access_mail_test_cc_user,mail.test.cc.user,model_mail_test_cc,base.group_user,1,
access_mail_test_multi_company_user,mail.test.multi.company.user,model_mail_test_multi_company,base.group_user,1,1,1,1
access_mail_test_multi_company_portal,mail.test.multi.company.portal,model_mail_test_multi_company,base.group_portal,1,0,0,0
access_mail_test_tracking_user,mail.test.tracking,model_mail_test_tracking,base.group_user,1,1,1,1
access_mail_test_compute,mail.test.compute,model_mail_test_compute,base.group_user,1,1,1,1
@@ -38,24 +38,29 @@ def test_post_subscribe_recipients(self):
def test_chatter_mail_create_nolog(self):
""" Test disable of automatic chatter message at create """
rec = self.env['mail.test.simple'].with_user(self.user_employee).with_context({'mail_create_nolog': True}).create({'name': 'Test'})
rec.flush()
self.assertEqual(rec.message_ids, self.env['mail.message'])

rec = self.env['mail.test.simple'].with_user(self.user_employee).with_context({'mail_create_nolog': False}).create({'name': 'Test'})
rec.flush()
self.assertEqual(len(rec.message_ids), 1)

def test_chatter_mail_notrack(self):
""" Test disable of automatic value tracking at create and write """
rec = self.env['mail.test.track'].with_user(self.user_employee).create({'name': 'Test', 'user_id': self.user_employee.id})
rec.flush()
self.assertEqual(len(rec.message_ids), 1,
"A creation message without tracking values should have been posted")
self.assertEqual(len(rec.message_ids.sudo().tracking_value_ids), 0,
"A creation message without tracking values should have been posted")

rec.with_context({'mail_notrack': True}).write({'user_id': self.user_admin.id})
rec.flush()
self.assertEqual(len(rec.message_ids), 1,
"No new message should have been posted with mail_notrack key")

rec.with_context({'mail_notrack': False}).write({'user_id': self.user_employee.id})
rec.flush()
self.assertEqual(len(rec.message_ids), 2,
"A tracking message should have been posted")
self.assertEqual(len(rec.message_ids.sudo().mapped('tracking_value_ids')), 1,
@@ -64,16 +69,20 @@ def test_chatter_mail_notrack(self):
def test_chatter_tracking_disable(self):
""" Test disable of all chatter features at create and write """
rec = self.env['mail.test.track'].with_user(self.user_employee).with_context({'tracking_disable': True}).create({'name': 'Test', 'user_id': self.user_employee.id})
rec.flush()
self.assertEqual(rec.sudo().message_ids, self.env['mail.message'])
self.assertEqual(rec.sudo().mapped('message_ids.tracking_value_ids'), self.env['mail.tracking.value'])

rec.write({'user_id': self.user_admin.id})
rec.flush()
self.assertEqual(rec.sudo().mapped('message_ids.tracking_value_ids'), self.env['mail.tracking.value'])

rec.with_context({'tracking_disable': False}).write({'user_id': self.user_employee.id})
rec.flush()
self.assertEqual(len(rec.sudo().mapped('message_ids.tracking_value_ids')), 1)

rec = self.env['mail.test.track'].with_user(self.user_employee).with_context({'tracking_disable': False}).create({'name': 'Test', 'user_id': self.user_employee.id})
rec.flush()
self.assertEqual(len(rec.sudo().message_ids), 1,
"Creation message without tracking values should have been posted")
self.assertEqual(len(rec.sudo().mapped('message_ids.tracking_value_ids')), 0,
@@ -14,6 +14,7 @@ def setUp(self):
record = self.env['mail.test.full'].with_user(self.user_employee).with_context(self._test_context).create({
'name': 'Test',
})
record.flush()
self.record = record.with_context(mail_notrack=False)

def test_message_track_no_tracking(self):
@@ -22,6 +23,7 @@ def test_message_track_no_tracking(self):
'name': 'Tracking or not',
'count': 32,
})
self.record.flush()
self.assertEqual(self.record.message_ids, self.env['mail.message'])

def test_message_track_no_subtype(self):
@@ -32,6 +34,7 @@ def test_message_track_no_subtype(self):
'name': 'Test2',
'customer_id': customer.id,
})
self.record.flush()

# one new message containing tracking; without subtype linked to tracking, a note is generated
self.assertEqual(len(self.record.message_ids), 1)
@@ -61,6 +64,7 @@ def test_message_track_subtype(self):
'email_from': 'noone@example.com',
'umbrella_id': umbrella.id,
})
self.record.flush()
# one new message containing tracking; subtype linked to tracking
self.assertEqual(len(self.record.message_ids), 1)
self.assertEqual(self.record.message_ids.subtype_id, self.env.ref('test_mail.st_mail_test_full_umbrella_upd'))
@@ -85,6 +89,7 @@ def test_message_track_template(self):
'name': 'Test2',
'customer_id': self.user_admin.partner_id.id,
})
self.record.flush()

self.assertEqual(len(self.record.message_ids), 2, 'should have 2 new messages: one for tracking, one for template')

@@ -113,6 +118,7 @@ def test_message_track_template_at_create(self):
'customer_id': self.user_admin.partner_id.id,
'mail_template': self.env.ref('test_mail.mail_test_full_tracking_tpl').id,
})
record.flush()

self.assertEqual(len(record.message_ids), 1, 'should have 1 new messages for template')
# one new message containing the template linked to tracking
@@ -129,6 +135,7 @@ def test_message_tracking_sequence(self):
'user_id': self.user_admin.id,
'umbrella_id': self.env['mail.test'].with_context(mail_create_nosubscribe=True).create({'name': 'Umbrella'}).id
})
self.record.flush()
self.assertEqual(len(self.record.message_ids), 1, 'should have 1 tracking message')

tracking_values = self.env['mail.tracking.value'].search([('mail_message_id', '=', self.record.message_ids.id)])
@@ -143,6 +150,7 @@ def test_track_groups(self):
field.groups = 'base.group_erp_manager'

self.record.sudo().write({'email_from': 'X'})
self.record.flush()

msg_emp = self.record.message_ids.message_format()
msg_sudo = self.record.sudo().message_ids.message_format()
@@ -156,6 +164,7 @@ def test_notify_track_groups(self):
field.groups = 'base.group_erp_manager'

self.record.sudo().write({'email_from': 'X'})
self.record.flush()

msg_emp = self.record._notify_prepare_template_context(self.record.message_ids, {})
msg_sudo = self.record.sudo()._notify_prepare_template_context(self.record.message_ids, {})
@@ -206,3 +215,72 @@ def patched_message_track_post_template(*args, **kwargs):
new_partner = Partner.search([('email', '=', email_new_partner)])
self.assertTrue(new_partner)
self.assertEqual(new_partner.company_id, company1)

def test_message_track_multiple(self):
""" check that multiple updates generate a single tracking message """
umbrella = self.env['mail.test'].with_context(mail_create_nosubscribe=True).create({'name': 'Umbrella'})
self.record.name = 'Zboub'
self.record.customer_id = self.user_admin.partner_id
self.record.user_id = self.user_admin
self.record.umbrella_id = umbrella
self.record.flush()

# should have a single message with all tracked fields
self.assertEqual(len(self.record.message_ids), 1, 'should have 1 tracking message')
self.assertTracking(self.record.message_ids[0], [
('customer_id', 'many2one', False, self.user_admin.partner_id),
('user_id', 'many2one', False, self.user_admin),
('umbrella_id', 'many2one', False, umbrella),
])

def test_tracked_compute(self):
# no tracking at creation
record = self.env['mail.test.compute'].create({})
record.flush()
self.assertEqual(len(record.message_ids), 1)
self.assertEqual(len(record.message_ids[0].tracking_value_ids), 0)

# assign partner_id: one tracking message for the modified field and all
# the stored and non-stored computed fields on the record
partner = self.env['res.partner'].create({
'name': 'Foo',
'email': 'foo@example.com',
'phone': '1234567890',
})
record.partner_id = partner
record.flush()
self.assertEqual(len(record.message_ids), 2)
self.assertEqual(len(record.message_ids[0].tracking_value_ids), 4)
self.assertTracking(record.message_ids[0], [
('partner_id', 'many2one', False, partner),
('partner_name', 'char', False, 'Foo'),
('partner_email', 'char', False, 'foo@example.com'),
('partner_phone', 'char', False, '1234567890'),
])

# modify partner: one tracking message for the only recomputed field
partner.write({'name': 'Fool'})
partner.flush()
self.assertEqual(len(record.message_ids), 3)
self.assertEqual(len(record.message_ids[0].tracking_value_ids), 1)
self.assertTracking(record.message_ids[0], [
('partner_name', 'char', 'Foo', 'Fool'),
])

# modify partner: one tracking message for both stored computed fields;
# the non-stored computed fields have no tracking
partner.write({
'name': 'Bar',
'email': 'bar@example.com',
'phone': '0987654321',
})
# force recomputation of 'partner_phone' to make sure it does not
# generate tracking values
self.assertEqual(record.partner_phone, '0987654321')
partner.flush()
self.assertEqual(len(record.message_ids), 4)
self.assertEqual(len(record.message_ids[0].tracking_value_ids), 2)
self.assertTracking(record.message_ids[0], [
('partner_name', 'char', 'Fool', 'Bar'),
('partner_email', 'char', 'foo@example.com', 'bar@example.com'),
])

0 comments on commit 7886f51

Please sign in to comment.
You can’t perform that action at this time.