[REF] mail: use tracking models rather than fields

Introducing tracking_value and tracking_value_item models for tracking value.

task-2817111

closes odoo/odoo#88547

Signed-off-by: Sébastien Theys (seb) <seb@odoo.com>
This commit is contained in:
Zelong Lin
2022-05-23 17:31:42 +02:00
parent e423618dca
commit 659c61219f
12 changed files with 264 additions and 169 deletions
+14 -19
View File
@@ -844,20 +844,6 @@ class Message(models.Model):
else:
author = (0, message_sudo.email_from)
# Tracking values
tracking_value_ids = []
for tracking in message_sudo.tracking_value_ids:
groups = tracking.field_groups
if not groups or self.env.is_superuser() or self.user_has_groups(groups):
tracking_value_ids.append({
'id': tracking.id,
'changed_field': tracking.field_desc,
'old_value': tracking.get_old_display_value()[0],
'new_value': tracking.get_new_display_value()[0],
'field_type': tracking.field_type,
'currency_id': tracking.currency_id.id,
})
if message_sudo.model and message_sudo.res_id:
record_name = self.env[message_sudo.model] \
.browse(message_sudo.res_id) \
@@ -886,10 +872,11 @@ class Message(models.Model):
} for content, reactions in reactions_per_content.items()])]
if format_reply and message_sudo.model == 'mail.channel' and message_sudo.parent_id:
vals['parentMessage'] = message_sudo.parent_id.message_format(format_reply=False)[0]
allowed_tracking_ids = message_sudo.tracking_value_ids.filtered(lambda tracking: not tracking.field_groups or self.env.is_superuser() or self.user_has_groups(tracking.field_groups))
vals.update({
'notifications': message_sudo.notification_ids._filtered_for_web_client()._notification_format(),
'attachment_ids': message_sudo.attachment_ids._attachment_format(),
'tracking_value_ids': tracking_value_ids,
'trackingValues': [('insert-and-replace', allowed_tracking_ids._tracking_value_format())],
'messageReactionGroups': reaction_groups,
'record_name': record_name,
})
@@ -930,12 +917,20 @@ class Message(models.Model):
],
'needaction_partner_ids': [], # list of partner ids
'res_id': 7,
'tracking_value_ids': [
'trackingValues': [('insert-and-replace',
{
'old_value': "",
'changed_field': "Customer",
'changedField': "Customer",
'id': 2965,
'new_value': "Axelor"
'newValue': [('insert-and-replace', {
'currencyId': "",
'fieldType': 'char',
'value': "Axelor",
})],
'oldValue': [('insert-and-replace', {
'currencyId': "",
'fieldType': 'char',
'value': "",
})],
}
],
'author_id': (3, u'Administrator'),
+2 -2
View File
@@ -2580,8 +2580,8 @@ class MailThread(models.AbstractModel):
groups = tracking_value.field_groups
if not groups or self.env.is_superuser() or self.user_has_groups(groups):
tracking.append((tracking_value.field_desc,
tracking_value.get_old_display_value()[0],
tracking_value.get_new_display_value()[0]))
tracking_value._get_old_display_value()[0],
tracking_value._get_new_display_value()[0]))
subtype_id = msg_vals.get('subtype_id') if msg_vals and 'subtype_id' in msg_vals else message.subtype_id.id
is_discussion = subtype_id == self.env['ir.model.data']._xmlid_to_res_id('mail.mt_comment')
+31 -16
View File
@@ -88,34 +88,49 @@ class MailTracking(models.Model):
return values
return {}
def get_display_value(self, type):
assert type in ('new', 'old')
def _tracking_value_format(self):
tracking_values = [{
'changedField': tracking.field_desc,
'id': tracking.id,
'newValue': [('insert-and-replace', {
'currencyId': tracking.currency_id.id,
'fieldType': tracking.field_type,
'value': tracking._get_new_display_value()[0]})],
'oldValue': [('insert-and-replace', {
'currencyId': tracking.currency_id.id,
'fieldType': tracking.field_type,
'value': tracking._get_old_display_value()[0]})],
} for tracking in self]
return tracking_values
def _get_display_value(self, prefix):
assert prefix in ('new', 'old')
result = []
for record in self:
if record.field_type in ['integer', 'float', 'char', 'text', 'monetary']:
result.append(getattr(record, '%s_value_%s' % (type, record.field_type)))
result.append(record[f'{prefix}_value_{record.field_type}'])
elif record.field_type == 'datetime':
if record['%s_value_datetime' % type]:
new_datetime = getattr(record, '%s_value_datetime' % type)
result.append('%sZ' % new_datetime)
if record[f'{prefix}_value_datetime']:
new_datetime = record[f'{prefix}_value_datetime']
result.append(f'{new_datetime}Z')
else:
result.append(record['%s_value_datetime' % type])
result.append(record[f'{prefix}_value_datetime'])
elif record.field_type == 'date':
if record['%s_value_datetime' % type]:
new_date = record['%s_value_datetime' % type]
if record[f'{prefix}_value_datetime']:
new_date = record[f'{prefix}_value_datetime']
result.append(fields.Date.to_string(new_date))
else:
result.append(record['%s_value_datetime' % type])
result.append(record[f'{prefix}_value_datetime'])
elif record.field_type == 'boolean':
result.append(bool(record['%s_value_integer' % type]))
result.append(bool(record[f'{prefix}_value_integer']))
else:
result.append(record['%s_value_char' % type])
result.append(record[f'{prefix}_value_char'])
return result
def get_old_display_value(self):
def _get_old_display_value(self):
# grep : # old_value_integer | old_value_datetime | old_value_char
return self.get_display_value('old')
return self._get_display_value('old')
def get_new_display_value(self):
def _get_new_display_value(self):
# grep : # new_value_integer | new_value_datetime | new_value_char
return self.get_display_value('new')
return self._get_display_value('new')
@@ -147,8 +147,10 @@
</t>
<t t-if="messageView.message.trackingValues.length > 0">
<ul class="o_Message_trackingValues mt-2">
<t t-foreach="messageView.message.trackingValues" t-as="value" t-key="value.id">
<TrackingValue value="value"/>
<t t-foreach="messageView.message.trackingValues" t-as="trackingValue" t-key="trackingValue.id">
<li>
<TrackingValue value="trackingValue"/>
</li>
</t>
</ul>
</t>
@@ -4,7 +4,16 @@ import { registerMessagingComponent } from '@mail/utils/messaging_component';
const { Component } = owl;
export class TrackingValue extends Component {}
export class TrackingValue extends Component {
/**
* @returns {TrackingValue}
*/
get trackingValue() {
return this.props.value;
}
}
Object.assign(TrackingValue, {
props: { value: Object },
@@ -1,6 +0,0 @@
.o_TrackingValue_item {
// Could be replaced by "me-1" after the migration to BS5
// for the backend AND the frontend
margin-inline-end: map-get($spacers, 1);
}
@@ -1,21 +1,11 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates xml:space="preserve">
<?xml version="1.0" encoding="utf-8"?>
<templates>
<t t-name="mail.TrackingValue" owl="1">
<li class="o_TrackingValue" t-attf-class="{{ className }}" t-ref="root">
<div class="o_TrackingValue_container d-flex align-items-center flex-wrap">
<div class="o_TrackingValue_fieldName o_TrackingValue_item" t-esc="props.value.changed_field"/>
<t t-if="props.value.old_value">
<div class="o_TrackingValue_oldValue o_TrackingValue_item" t-esc="props.value.old_value"/>
</t>
<i t-else="" class="mr-2">None</i>
<div class="o_TrackingValue_separator o_TrackingValue_item fa fa-long-arrow-right" title="Changed" role="img"/>
<t t-if="props.value.new_value">
<div class="o_TrackingValue_newValue o_TrackingValue_item" t-esc="props.value.new_value"/>
</t>
<i t-else="">None</i>
</div>
</li>
<div class="o_TrackingValue d-inline-block d-flex align-items-center flex-wrap" t-attf-class="{{ className }}" t-ref="root">
<span class="o_TrackingValue_fieldName mr-1" t-esc="trackingValue.formattedChangedField"/>
<span class="o_TrackingValue_oldValue mr-1" t-esc="trackingValue.oldValue.formattedValueOrNone" t-att-class="{ 'font-italic': !trackingValue.oldValue.formattedValue }"/>
<i class="o_TrackingValue_separator fa fa-long-arrow-right mx-1" title="Changed" role="img"/>
<span class="o_TrackingValue_newValue mr-1" t-esc="trackingValue.newValue.formattedValueOrNone" t-att-class="{ 'font-italic': !trackingValue.newValue.formattedValue }"/>
</div>
</t>
</templates>
</templates>
+7 -93
View File
@@ -7,9 +7,7 @@ import emojis from '@mail/js/emojis';
import { addLink, htmlToTextContentInline, parseAndTransform } from '@mail/js/utils';
import { session } from '@web/session';
import { sprintf } from '@web/core/utils/strings';
import { format } from 'web.field_utils';
import { getLangDatetimeFormat, str_to_datetime } from 'web.time';
const { markup } = owl;
@@ -125,8 +123,8 @@ registerModel({
if ('subtype_id' in data) {
data2.subtype_id = data.subtype_id;
}
if ('tracking_value_ids' in data) {
data2.tracking_value_ids = data.tracking_value_ids;
if ('trackingValues' in data) {
data2.trackingValues = data.trackingValues;
}
return data2;
@@ -333,7 +331,7 @@ registerModel({
if (!this.originThread) {
return false;
}
if (this.tracking_value_ids.length > 0) {
if (this.trackingValues.length > 0) {
return false;
}
if (this.message_type !== 'comment') {
@@ -479,7 +477,7 @@ registerModel({
return (
this.isBodyEmpty &&
!this.hasAttachments &&
this.tracking_value_ids.length === 0 &&
this.trackingValues.length === 0 &&
!this.subtype_description
);
},
@@ -599,88 +597,6 @@ registerModel({
}
return replace(threads);
},
/**
* @private
* @returns {Object[]}
*/
_computeTrackingValues() {
return this.tracking_value_ids.map(trackingValue => {
const value = Object.assign({}, trackingValue);
value.changed_field = sprintf(this.env._t("%s:"), value.changed_field);
/**
* Maps tracked field type to a JS formatter. Tracking values are
* not always stored in the same field type as their origin type.
* Field types that are not listed here are not supported by
* tracking in Python. Also see `create_tracking_values` in Python.
*/
switch (value.field_type) {
case 'boolean':
value.old_value = value.old_value ? this.env._t('Yes') : this.env._t('No');
value.new_value = value.new_value ? this.env._t('Yes') : this.env._t('No');
break;
/**
* many2one formatter exists but is expecting id/name_get or data
* object but only the target record name is known in this context.
*
* Selection formatter exists but requires knowing all
* possibilities and they are not given in this context.
*/
case 'char':
case 'many2one':
case 'selection':
value.old_value = format.char(value.old_value);
value.new_value = format.char(value.new_value);
break;
case 'date':
if (value.old_value) {
value.old_value = moment.utc(value.old_value);
}
if (value.new_value) {
value.new_value = moment.utc(value.new_value);
}
value.old_value = format.date(value.old_value);
value.new_value = format.date(value.new_value);
break;
case 'datetime':
if (value.old_value) {
value.old_value = moment.utc(value.old_value);
}
if (value.new_value) {
value.new_value = moment.utc(value.new_value);
}
value.old_value = format.datetime(value.old_value);
value.new_value = format.datetime(value.new_value);
break;
case 'float':
value.old_value = format.float(value.old_value);
value.new_value = format.float(value.new_value);
break;
case 'integer':
value.old_value = format.integer(value.old_value);
value.new_value = format.integer(value.new_value);
break;
case 'monetary':
value.old_value = format.monetary(value.old_value, undefined, {
currency: value.currency_id
? this.env.session.currencies[value.currency_id]
: undefined,
forceString: true,
});
value.new_value = format.monetary(value.new_value, undefined, {
currency: value.currency_id
? this.env.session.currencies[value.currency_id]
: undefined,
forceString: true,
});
break;
case 'text':
value.old_value = format.text(value.old_value);
value.new_value = format.text(value.new_value);
break;
}
return value;
});
},
},
fields: {
authorName: attr({
@@ -922,11 +838,9 @@ registerModel({
compute: '_computeThreads',
inverse: 'messages',
}),
trackingValues: attr({
compute: '_computeTrackingValues',
}),
tracking_value_ids: attr({
default: [],
trackingValues: many('TrackingValue', {
inverse: 'messageOwner',
isCausal: true,
}),
},
});
@@ -0,0 +1,49 @@
/** @odoo-module **/
import { registerModel } from '@mail/model/model_core';
import { attr, one } from '@mail/model/model_field';
import { sprintf } from '@web/core/utils/strings';
registerModel({
name: 'TrackingValue',
identifyingFields: ['id'],
recordMethods: {
/**
* @private
* @returns {string}
*/
_computeFormattedChangedField() {
return sprintf(this.env._t("%s:"), this.changedField);
},
},
fields: {
/**
* States the original field of changed tracking value, such as "Status", "Date".
*/
changedField: attr({
required: true,
}),
/**
* The translated `changedFiled` according to the language setting.
*/
formattedChangedField: attr({
compute: '_computeFormattedChangedField',
}),
id: attr({
readonly: true,
required: true,
}),
messageOwner: one('Message', {
inverse: 'trackingValues',
readonly: true,
required: true,
}),
newValue: one('TrackingValueItem', {
inverse: 'trackingValueAsNewValue',
isCausal: true,
}),
oldValue: one('TrackingValueItem', {
inverse: 'trackingValueAsOldValue',
isCausal: true,
}),
}});
@@ -0,0 +1,100 @@
/** @odoo-module **/
import { registerModel } from '@mail/model/model_core';
import { attr, one } from '@mail/model/model_field';
import { format } from 'web.field_utils';
registerModel({
name: 'TrackingValueItem',
identifyingFields: [['trackingValueAsNewValue','trackingValueAsOldValue']],
recordMethods: {
/**
* @private
* @returns {string}
*/
_computeFormattedValue() {
/**
* Maps tracked field type to a JS formatter. Tracking values are
* not always stored in the same field type as their origin type.
* Field types that are not listed here are not supported by
* tracking in Python. Also see `create_tracking_values` in Python.
*/
switch (this.fieldType) {
case 'boolean':
return this.value ? this.env._t("Yes") : this.env._t("No");
/**
* many2one formatter exists but is expecting id/name_get or data
* object but only the target record name is known in this context.
*
* Selection formatter exists but requires knowing all
* possibilities and they are not given in this context.
*/
case 'char':
case 'many2one':
case 'selection':
return format.char(this.value);
case 'date':
if (this.value) {
return format.date(moment.utc(this.value));
}
return format.date(this.value);
case 'datetime':
if (this.value) {
return format.datetime(moment.utc(this.value));
}
return format.datetime(this.value);
case 'float':
return format.float(this.value);
case 'integer':
return format.integer(this.value);
case 'text':
return format.text(this.value);
case 'monetary':
return format.monetary(this.value, undefined, {
currency: this.currencyId
? this.env.session.currencies[this.currencyId]
: undefined,
forceString: true,
});
default :
return this.value;
}
},
/**
* @private
* @returns {string}
*/
_computeFormattedValueOrNone() {
return this.formattedValue || this.env._t("None");
},
},
fields: {
/**
* Used when the currency changes as the tracking value. This only makes sense for field of type monetary.
*/
currencyId: attr(),
/**
* Indicates the type of the tracking value item.
* The supported types are: boolean, char, many2one, selection, date, datetime, float, integer, text, monetary.
*/
fieldType: attr(),
formattedValue: attr({
compute: '_computeFormattedValue',
}),
formattedValueOrNone: attr({
compute: '_computeFormattedValueOrNone',
}),
trackingValueAsNewValue: one('TrackingValue', {
inverse: 'newValue',
readonly: true,
}),
trackingValueAsOldValue: one('TrackingValue', {
inverse: 'oldValue',
readonly: true,
}),
/**
* The original value of the tracking value item.
*/
value: attr(),
}});
@@ -1515,13 +1515,7 @@ MockServer.include({
const trackingValueIds = this.getRecords('mail.tracking.value', [
['id', 'in', message.tracking_value_ids],
]);
const formattedTrackingValues = trackingValueIds.map(tracking => ({
changed_field: tracking.field_desc,
field_type: tracking.field_type,
id: tracking.id,
new_value: this._mockMailTrackingValue_GetDisplayValue(tracking, 'new'),
old_value: this._mockMailTrackingValue_GetDisplayValue(tracking, 'old'),
}));
const formattedTrackingValues = [['insert-and-replace', this._mockMailTrackingValue_TrackingValueFormat(trackingValueIds)]];
const partners = this.getRecords(
'res.partner',
[['id', 'in', message.partner_ids]],
@@ -1535,7 +1529,7 @@ MockServer.include({
parentMessage: message.parent_id ? this._mockMailMessageMessageFormat([message.parent_id])[0] : false,
recipients: partners.map(p => ({ id: p.id, name: p.name })),
record_name: thread && (thread.name !== undefined ? thread.name : thread.display_name),
tracking_value_ids: formattedTrackingValues,
trackingValues: formattedTrackingValues,
});
if (message.subtype_id) {
const subtype = this.getRecords('mail.message.subtype', [
@@ -2087,7 +2081,25 @@ MockServer.include({
return false;
},
/**
* Simulates `get_display_value` on `mail.tracking.value`
* Simulates `_tracking_value_format` on `mail.tracking.value`
*/
_mockMailTrackingValue_TrackingValueFormat(tracking_value_ids) {
const trackingValues = tracking_value_ids.map(tracking => ({
changedField: tracking.field_desc,
id: tracking.id,
newValue: [['insert-and-replace', {
fieldType: tracking.field_type,
value: this._mockMailTrackingValue_GetDisplayValue(tracking, 'new')
}]],
oldValue: [['insert-and-replace', {
fieldType: tracking.field_type,
value: this._mockMailTrackingValue_GetDisplayValue(tracking, 'old')
}]],
}));
return trackingValues;
},
/**
* Simulates `_get_display_value` on `mail.tracking.value`
*/
_mockMailTrackingValue_GetDisplayValue(record, type) {
switch (record.field_type) {
+17 -2
View File
@@ -346,8 +346,23 @@ class TestTrackingInternals(TestMailCommon):
msg_emp = self.record.message_ids.message_format()
msg_sudo = self.record.sudo().message_ids.message_format()
self.assertFalse(msg_emp[0].get('tracking_value_ids'), "should not have protected tracking values")
self.assertTrue(msg_sudo[0].get('tracking_value_ids'), "should have protected tracking values")
tracking_values = self.env['mail.tracking.value'].search([('mail_message_id', '=', self.record.message_ids.id)])
formattedTrackingValues = [('insert-and-replace', [{
'changedField': 'Email From',
'id': tracking_values[0]['id'],
'newValue': [('insert-and-replace', {
'currencyId': False,
'fieldType': 'char',
'value': 'X',
})],
'oldValue': [('insert-and-replace', {
'currencyId': False,
'fieldType': 'char',
'value': False,
})],
}])]
self.assertEqual(msg_emp[0].get('trackingValues'), [('insert-and-replace', [])], "should not have protected tracking values")
self.assertEqual(msg_sudo[0].get('trackingValues'), formattedTrackingValues, "should have protected tracking values")
msg_emp = self.record._notify_by_email_prepare_rendering_context(self.record.message_ids, {})
msg_sudo = self.record.sudo()._notify_by_email_prepare_rendering_context(self.record.message_ids, {})