[REF] portal: do not depend on mail message format

RATIONALE

Purpose of this change to rewrite the formatting done on messages displayed
on simple frontend i.e. portal chatter widget and project chatter.

We stop calling 'message_format' which computes a lot of unnecessary data
and sends too much information to frontend. We choose to instead manually
handcraft the returned data, already tailored for frontend widget.

SPECIFICATIONS

Remove call to '_message_format' in 'portal_message_format'. Instead have
a list of properties (fields or computation based on fields e.g. rating publisher
information) that can be overridden in sub-addons. Use those to generate
the data used by frontend chatter widget.

Remove extra formatting or data computation done in JS files. Do it directly
in 'portal_message_format' in order to have clean information sent.

This change targets mainly portal and portal_rating. Making those modules
Independent from backend message formatting allows to save queries and
also to avoid sending useless information to the frontend.

SIDE SPECS

Improve 'message_format' tests, and make new specific test for its portal
counterpart.

Provide some fixes for bugfixes that occured during the testing of this
task. Those are not crucial for stable, so currently kept in this master PR.

Task-3322905

closes odoo/odoo#121104

Related: odoo/enterprise#41056
Signed-off-by: Thibault Delavallee (tde) <tde@openerp.com>
This commit is contained in:
Odoo's Mergebot
2023-05-16 15:55:19 +02:00
committed by GitHub
19 changed files with 689 additions and 283 deletions
+2 -2
View File
@@ -7,7 +7,7 @@ from odoo import models
class MailMessage(models.Model):
_inherit = 'mail.message'
def _message_format(self, fnames, format_reply=True, legacy=False):
def _message_format(self, fnames, format_reply=True):
"""Override to remove email_from and to return the livechat username if applicable.
A third param is added to the author_id tuple in this case to be able to differentiate it
from the normal name in client code.
@@ -17,7 +17,7 @@ class MailMessage(models.Model):
This allows the frontend display to include the additional features
(e.g: Show additional buttons with the available answers for this step). """
vals_list = super()._message_format(fnames=fnames, format_reply=format_reply, legacy=legacy)
vals_list = super()._message_format(fnames=fnames, format_reply=format_reply)
for vals in vals_list:
message_sudo = self.browse(vals['id']).sudo().with_prefetch(self.ids)
discuss_channel = self.env['discuss.channel'].browse(message_sudo.res_id) if message_sudo.model == 'discuss.channel' else self.env['discuss.channel']
+8 -15
View File
@@ -50,29 +50,22 @@ class IrAttachment(models.Model):
self.ensure_one()
return self.env.user.partner_id
def _attachment_format(self, legacy=False):
def _attachment_format(self):
safari = request and request.httprequest.user_agent and request.httprequest.user_agent.browser == 'safari'
res_list = []
for attachment in self:
res = {
return [
{
'checksum': attachment.checksum,
'id': attachment.id,
'filename': attachment.name,
'name': attachment.name,
'mimetype': 'application/octet-stream' if safari and attachment.mimetype and 'video' in attachment.mimetype else attachment.mimetype,
}
if not legacy:
res['originThread'] = [('insert', {
'originThread': [('insert', {
'id': attachment.res_id,
'model': attachment.res_model,
})]
else:
res.update({
'res_id': attachment.res_id,
'res_model': attachment.res_model,
})
res_list.append(res)
return res_list
})],
}
for attachment in self
]
@api.model
def _get_upload_env(self, request, thread_model, thread_id):
+2 -2
View File
@@ -866,7 +866,7 @@ class Message(models.Model):
# MESSAGE READ / FETCH / FAILURE API
# ------------------------------------------------------
def _message_format(self, fnames, format_reply=True, legacy=False):
def _message_format(self, fnames, format_reply=True):
"""Reads values from messages and formats them for the web client."""
vals_list = self._read_format(fnames)
thread_ids_by_model_name = defaultdict(set)
@@ -908,7 +908,7 @@ class Message(models.Model):
'default_subject': default_subject,
'guestAuthor': guestAuthor,
'notifications': message_sudo.notification_ids._filtered_for_web_client()._notification_format(),
'attachment_ids': message_sudo.attachment_ids._attachment_format() if not legacy else message_sudo.attachment_ids._attachment_format(legacy=True),
'attachment_ids': message_sudo.attachment_ids._attachment_format(),
'trackingValues': allowed_tracking_ids._tracking_value_format(),
'linkPreviews': message_sudo.link_preview_ids._link_preview_format(),
'messageReactionGroups': reaction_groups,
+1 -1
View File
@@ -213,7 +213,7 @@ class PortalChatter(http.Controller):
domain = expression.AND([Message._get_search_domain_share(), domain])
Message = request.env['mail.message'].sudo()
return {
'messages': Message.search(domain, limit=limit, offset=offset).portal_message_format(),
'messages': Message.search(domain, limit=limit, offset=offset).portal_message_format(options=kw),
'message_count': Message.search_count(domain)
}
+107 -16
View File
@@ -2,26 +2,117 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import models
from odoo.http import request
from odoo.tools import format_datetime
class MailMessage(models.Model):
_inherit = 'mail.message'
def portal_message_format(self):
self.check_access_rule('read')
return self._portal_message_format([
'id', 'body', 'date', 'author_id', 'email_from', # base message fields
'message_type', 'subtype_id', 'is_internal', 'subject', # message specific
'model', 'res_id', 'record_name', # document related
])
def portal_message_format(self, options=None):
""" Simpler and portal-oriented version of 'message_format'. Purpose
is to prepare, organize and format values required by frontend widget
(frontend Chatter).
def _portal_message_format(self, fields_list):
vals_list = self._message_format(fields_list, legacy=True)
message_subtype_note_id = self.env['ir.model.data']._xmlid_to_res_id('mail.mt_note')
IrAttachmentSudo = self.env['ir.attachment'].sudo()
for vals in vals_list:
vals['is_message_subtype_note'] = message_subtype_note_id and (vals.get('subtype_id') or [False])[0] == message_subtype_note_id
for attachment in vals.get('attachment_ids', []):
if not attachment.get('access_token'):
attachment['access_token'] = IrAttachmentSudo.browse(attachment['id']).generate_access_token()[0]
This public API asks for read access on messages before doing the
actual computation in the private implementation.
:param dict options: options, used notably for inheritance and adding
specific fields or properties to compute;
:return list: list of dict, one per message in self. Each dict contains
values for either fields, either properties derived from fields.
"""
self.check_access_rule('read')
return self._portal_message_format(
self._portal_get_default_format_properties_names(options=options)
)
def _portal_get_default_format_properties_names(self, options=None):
""" Fields and values to compute for portal format.
:param dict options: options, used notably for inheritance and adding
specific fields or properties to compute;
:return set: fields or properties derived from fields
"""
return {
'attachment_ids',
'author_avatar_url',
'author_id',
'body',
'date',
'id',
'is_internal',
'is_message_subtype_note',
'published_date_str',
'subtype_id',
}
def _portal_message_format(self, properties_names):
""" Format messages for portal frontend. This private implementation
does not check for access that should be checked beforehand.
Notes:
* when asking for attachments: ensure an access token is present then
access them (using sudo);
:param set properties_names: fields or properties derived from fields
for which we are going to compute values;
:return list: list of dict, one per message in self. Each dict contains
values for either fields, either properties derived from fields.
"""
message_to_attachments = {}
if 'attachment_ids' in properties_names:
properties_names.remove('attachment_ids')
attachments_sudo = self.sudo().attachment_ids
attachments_sudo.generate_access_token()
related_attachments = {
att_read_values['id']: att_read_values
for att_read_values in attachments_sudo.read(
["access_token", "checksum", "id", "mimetype", "name", "res_id", "res_model"]
)
}
message_to_attachments = {
message.id: [
self._portal_message_format_attachments(related_attachments[att_id])
for att_id in message.attachment_ids.ids
]
for message in self.sudo()
}
fnames = {
property_name for property_name in properties_names
if property_name in self._fields
}
vals_list = self._read_format(fnames)
note_id = self.env['ir.model.data']._xmlid_to_res_id('mail.mt_note')
for message, values in zip(self, vals_list):
if message_to_attachments:
values['attachment_ids'] = message_to_attachments.get(message.id, {})
if 'author_avatar_url' in properties_names:
values['author_avatar_url'] = f'/web/image/mail.message/{message.id}/author_avatar/50x50'
if 'is_message_subtype_note' in properties_names:
values['is_message_subtype_note'] = (values.get('subtype_id') or [False, ''])[0] == note_id
if 'published_date_str' in properties_names:
values['published_date_str'] = format_datetime(self.env, values['date']) if values.get('date') else ''
return vals_list
def _portal_message_format_attachments(self, attachment_values):
""" From 'attachment_values' get an updated version formatted for
frontend display.
:param dict attachment_values: values coming from reading attachments
in database;
:return dict: updated attachment_values
"""
safari = request and request.httprequest.user_agent and request.httprequest.user_agent.browser == 'safari'
attachment_values['filename'] = attachment_values['name']
attachment_values['mimetype'] = (
'application/octet-stream' if safari and
'video' in (attachment_values["mimetype"] or "")
else attachment_values["mimetype"])
return attachment_values
@@ -3,14 +3,11 @@
import core from "web.core";
import dom from "web.dom";
import publicWidget from "web.public.widget";
import time from "web.time";
import portalComposer from "portal.composer";
import {Markup} from "web.utils";
import { range } from "@web/core/utils/numbers";
import { sprintf } from "@web/core/utils/strings";
var qweb = core.qweb;
var _t = core._t;
/**
* Widget PortalChatter
@@ -103,8 +100,6 @@ var PortalChatter = publicWidget.Widget.extend({
*/
preprocessMessages(messages) {
messages.forEach((m) => {
m['author_avatar_url'] = sprintf('/web/image/%s/%s/author_avatar/50x50', 'mail.message', m.id);
m['published_date_str'] = sprintf(_t('Published on %s'), moment(time.str_to_datetime(m.date)).format('MMMM Do YYYY, h:mm:ss a'));
m['body'] = Markup(m.body);
});
return messages;
@@ -86,7 +86,7 @@
<div class="o_portal_chatter_message_title">
<h5 class='mb-1'><t t-esc="message.author_id[1]"/></h5>
<p class="o_portal_chatter_puslished_date"><t t-esc="message.published_date_str"/></p>
<p class="o_portal_chatter_puslished_date">Published on <t t-esc="message.published_date_str"/></p>
</div>
<t t-out="message.body"/>
@@ -14,31 +14,8 @@ class PortalChatter(mail.PortalChatter):
fields += ['rating_value', 'rating_feedback']
return fields
def _portal_rating_stats(self, res_model, res_id, **kwargs):
# get the rating statistics for the record
if kwargs.get('rating_include'):
record = request.env[res_model].browse(res_id)
if hasattr(record, 'rating_get_stats'):
return {'rating_stats': record.sudo().rating_get_stats()}
return {}
@http.route()
def portal_chatter_post(self, res_model, res_id, message, attachment_ids='', attachment_tokens='', **kwargs):
if kwargs.get('rating_value'):
kwargs['rating_feedback'] = kwargs.pop('rating_feedback', message)
return super(PortalChatter, self).portal_chatter_post(res_model, res_id, message, attachment_ids=attachment_ids, attachment_tokens=attachment_tokens, **kwargs)
@http.route()
def portal_chatter_init(self, res_model, res_id, domain=False, limit=False, **kwargs):
result = super(PortalChatter, self).portal_chatter_init(res_model, res_id, domain=domain, limit=limit, **kwargs)
result.update(self._portal_rating_stats(res_model, res_id, **kwargs))
return result
@http.route()
def portal_message_fetch(self, res_model, res_id, domain=False, limit=False, offset=False, **kw):
# add 'rating_include' in context, to fetch them in portal_message_format
if kw.get('rating_include'):
request.update_context(rating_include=True)
result = super(PortalChatter, self).portal_message_fetch(res_model, res_id, domain=domain, limit=limit, offset=offset, **kw)
result.update(self._portal_rating_stats(res_model, res_id, **kw))
return result
@@ -17,4 +17,6 @@ class PortalRating(http.Controller):
return {'error': _('Invalid rating')}
rating.write({'publisher_comment': publisher_comment})
# return to the front-end the created/updated publisher comment
return rating.read(['publisher_comment', 'publisher_id', 'publisher_datetime'])[0]
return request.env['mail.message']._portal_message_format_rating(
rating.read(['publisher_comment', 'publisher_id', 'publisher_datetime'])[0]
)
+54 -15
View File
@@ -2,27 +2,66 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import models
from odoo.tools import format_datetime
class MailMessage(models.Model):
_inherit = 'mail.message'
def _portal_message_format(self, field_list):
# inlude rating value in data if requested
if self._context.get('rating_include'):
field_list += ['rating_value']
return super(MailMessage, self)._portal_message_format(field_list)
def _portal_get_default_format_properties_names(self, options=None):
""" Add request for rating information
def _message_format(self, fnames, format_reply=True, legacy=False):
""" Override the method to add information about a publisher comment
on each rating messages if requested, and compute a plaintext value of it.
:param dict options: supports 'rating_include' option allowing to
conditionally include rating information;
"""
vals_list = super(MailMessage, self)._message_format(fnames, format_reply=format_reply, legacy=legacy)
properties_names = super()._portal_get_default_format_properties_names()
if options and options.get('rating_include'):
properties_names |= {
'rating',
'rating_value',
}
return properties_names
def _portal_message_format(self, properties_names):
""" If requested, add rating information to returned formatted values.
Note: rating information combine both statistics (see 'rating_get_stats'
if available on model) and rating / publication information. """
vals_list = super()._portal_message_format(properties_names)
if not 'rating' in properties_names:
return vals_list
related_rating = self.env['rating.rating'].sudo().search_read(
[('message_id', 'in', self.ids)],
["id", "publisher_comment", "publisher_id", "publisher_datetime", "message_id"]
)
message_to_rating = {
rating['message_id'][0]: self._portal_message_format_rating(rating)
for rating in related_rating
}
for message, values in zip(self, vals_list):
values["rating"] = message_to_rating.get(message.id, {})
record = self.env[message.model].browse(message.res_id)
if hasattr(record, 'rating_get_stats'):
values['rating_stats'] = record.sudo().rating_get_stats()
if self._context.get('rating_include'):
infos = ["id", "publisher_comment", "publisher_id", "publisher_datetime", "message_id"]
related_rating = self.env['rating.rating'].sudo().search([('message_id', 'in', self.ids)]).read(infos)
mid_rating_tree = dict((rating['message_id'][0], rating) for rating in related_rating)
for vals in vals_list:
vals["rating"] = mid_rating_tree.get(vals['id'], {})
return vals_list
def _portal_message_format_rating(self, rating_values):
""" From 'rating_values' get an updated version formatted for frontend
display.
:param dict rating_values: values coming from reading ratings
in database;
:return dict: updated rating_values
"""
publisher_id, publisher_name = rating_values['publisher_id'] or [False, '']
rating_values['publisher_avatar'] = f'/web/image/res.partner/{publisher_id}/avatar_128/50x50' if publisher_id else ''
rating_values['publisher_comment'] = rating_values['publisher_comment'] or ''
rating_values['publisher_datetime'] = format_datetime(self.env, rating_values['publisher_datetime'])
rating_values['publisher_id'] = publisher_id
rating_values['publisher_name'] = publisher_name
return rating_values
+37 -4
View File
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
from odoo import fields, models, exceptions, _
from odoo import api, fields, models, exceptions, _
class Rating(models.Model):
@@ -12,12 +12,45 @@ class Rating(models.Model):
ondelete='set null', readonly=True)
publisher_datetime = fields.Datetime("Commented on", readonly=True)
@api.model_create_multi
def create(self, values_list):
for values in values_list:
self._synchronize_publisher_values(values)
ratings = super().create(values_list)
if any(rating.publisher_comment for rating in ratings):
ratings._check_synchronize_publisher_values()
return ratings
def write(self, values):
self._synchronize_publisher_values(values)
return super().write(values)
def _check_synchronize_publisher_values(self):
""" Either current user is a member of website restricted editor group
(done here by fetching the group record then using has_group, as it may
not be defined and we do not want to make a complete bridge module just
for that). Either write access on document is granted. """
editor_group = self.env['ir.model.data']._xmlid_to_res_id('website.group_website_restricted_editor')
if editor_group and self.env.user.has_group('website.group_website_restricted_editor'):
return
for model, model_data in self._classify_by_model().items():
records = self.env[model].browse(model_data['record_ids'])
try:
records.check_access_rights('write')
records.check_access_rule('write')
except exceptions.AccessError as e:
raise exceptions.AccessError(
_("Updating rating comment require write access on related record")
) from e
def _synchronize_publisher_values(self, values):
""" Force publisher partner and date if not given in order to have
coherent values. Those fields are readonly as they are not meant
to be modified manually, behaving like a tracking. """
if values.get('publisher_comment'):
if not self.env.user.has_group("website.group_website_restricted_editor"):
raise exceptions.AccessError(_("Only the publisher of the website can change the rating comment"))
self._check_synchronize_publisher_values()
if not values.get('publisher_datetime'):
values['publisher_datetime'] = fields.Datetime.now()
if not values.get('publisher_id'):
values['publisher_id'] = self.env.user.partner_id.id
return super(Rating, self).write(values)
return values
@@ -3,7 +3,6 @@
import core from "web.core";
import portalChatter from "portal.chatter";
import { roundPrecision } from "@web/core/utils/numbers";
import time from "web.time";
var _t = core._t;
var PortalChatter = portalChatter.PortalChatter;
@@ -192,17 +191,13 @@ PortalChatter.include({
var ratingData = {
id: rawRating.id,
mes_index: messageIndex,
publisher_datetime: rawRating.publisher_datetime ? moment(time.str_to_datetime(rawRating.publisher_datetime)).format('MMMM Do YYYY, h:mm:ss a') : "",
publisher_comment: rawRating.publisher_comment ? rawRating.publisher_comment : '',
publisher_avatar: rawRating.publisher_avatar,
publisher_comment: rawRating.publisher_comment,
publisher_datetime: rawRating.publisher_datetime,
publisher_id: rawRating.publisher_id,
publisher_name: rawRating.publisher_name,
};
// split array (id, display_name) of publisher_id into publisher_id and publisher_name
if (rawRating.publisher_id && rawRating.publisher_id.length >= 2) {
ratingData.publisher_id = rawRating.publisher_id[0];
ratingData.publisher_name = rawRating.publisher_id[1];
ratingData.publisher_avatar = `/web/image/res.partner/${ratingData.publisher_id}/avatar_128/50x50`;
}
var commentData = Object.assign(this._newPublisherCommentData(messageIndex), ratingData);
var commentData = {...this._newPublisherCommentData(messageIndex), ...ratingData};
return commentData;
},
@@ -1,8 +1,6 @@
/** @odoo-module */
import { formatDateTime, parseDateTime } from "@web/core/l10n/dates";
import { useService } from "@web/core/utils/hooks";
import { sprintf } from '@web/core/utils/strings';
import { ChatterComposer } from "./chatter_composer";
import { ChatterMessageCounter } from "./chatter_message_counter";
import { ChatterMessages } from "./chatter_messages";
@@ -124,16 +122,6 @@ export class ChatterContainer extends Component {
preprocessMessages(messages) {
return messages.map(m => ({
...m,
author_avatar_url: sprintf('/web/image/mail.message/%s/author_avatar/50x50', m.id),
published_date_str: sprintf(
this.env._t('Published on %s'),
formatDateTime(
parseDateTime(
m.date,
{ format: 'MM-dd-yyy HH:mm:ss' },
),
)
),
body: markup(m.body),
}));
}
@@ -28,7 +28,7 @@
</t>
<div class="o_portal_chatter_message_title">
<h5 class='mb-1'><t t-out="message.author_id[1]"/></h5>
<p class="o_portal_chatter_puslished_date"><t t-out="message.published_date_str"/></p>
<p class="o_portal_chatter_puslished_date">Published on <t t-out="message.published_date_str"/></p>
</div>
<t t-out="message.body"/>
<div class="o_portal_chatter_attachments">
+34
View File
@@ -108,6 +108,10 @@ class Rating(models.Model):
for rating in self:
rating.rating_text = rating_data._rating_to_text(rating.rating)
# ------------------------------------------------------------
# CRUD
# ------------------------------------------------------------
@api.model_create_multi
def create(self, vals_list):
for values in vals_list:
@@ -141,6 +145,10 @@ class Rating(models.Model):
data['parent_res_id'] = parent_res_model.id
return data
# ------------------------------------------------------------
# ACTIONS
# ------------------------------------------------------------
def reset(self):
for record in self:
record.write({
@@ -158,3 +166,29 @@ class Rating(models.Model):
'res_id': self.res_id,
'views': [[False, 'form']]
}
# ------------------------------------------------------------
# TOOLS
# ------------------------------------------------------------
def _classify_by_model(self):
""" To ease batch computation of various ratings related methods they
are classified by model. Ratings not linked to a valid record through
res_model / res_id are ignored.
:return dict: for each model having at least one rating in self, have
a sub-dict containing
* ratings: ratings related to that model;
* record IDs: records linked to the ratings of that model, in same
order;
"""
data_by_model = {}
for rating in self.filtered(lambda act: act.res_model and act.res_id):
if rating.res_model not in data_by_model:
data_by_model[rating.res_model] = {
'ratings': self.env['rating.rating'],
'record_ids': [],
}
data_by_model[rating.res_model]['ratings'] += rating
data_by_model[rating.res_model]['record_ids'].append(rating.res_id)
return data_by_model
+186 -148
View File
@@ -35,12 +35,20 @@ class BaseMailPerformance(MailCommon, TransactionCaseWithUserDemo):
'groups_id': [(6, 0, [cls.env.ref('base.group_user').id])],
})
cls.customer = cls.env['res.partner'].with_context(cls._test_context).create({
'country_id': cls.env.ref('base.be').id,
'email': 'customer.test@example.com',
'name': 'Test Customer',
'mobile': '0456123456',
})
cls.customers = cls.env['res.partner'].with_context(cls._test_context).create([
{
'country_id': cls.env.ref('base.be').id,
'email': 'customer.test@example.com',
'name': 'Test Customer',
'mobile': '0456123456',
}, {
'country_id': cls.env.ref('base.be').id,
'email': 'customer.test.2@example.com',
'name': 'Test Customer 2',
'mobile': '0456654321',
}
])
cls.customer = cls.customers[0]
cls.test_attachments_vals = cls._generate_attachments_data(3, 'mail.compose.message', 0)
@@ -348,7 +356,7 @@ class TestMailAPIPerformance(BaseMailPerformance):
'partner_ids': [(4, customer_id)],
})
with self.assertQueryCount(admin=36, employee=36):
with self.assertQueryCount(admin=35, employee=35):
composer._action_send_mail()
@users('admin', 'employee')
@@ -369,7 +377,7 @@ class TestMailAPIPerformance(BaseMailPerformance):
'partner_ids': [(4, customer.id)],
})
with self.assertQueryCount(admin=37, employee=37):
with self.assertQueryCount(admin=35, employee=35):
composer._action_send_mail()
@users('admin', 'employee')
@@ -379,7 +387,7 @@ class TestMailAPIPerformance(BaseMailPerformance):
test_record, _test_template = self._create_test_records()
customer = self.env['res.partner'].browse(self.customer.ids)
attachments = self.env['ir.attachment'].with_user(self.env.user).create(self.test_attachments_vals)
with self.assertQueryCount(admin=10, employee=10):
with self.assertQueryCount(admin=9, employee=9):
composer_form = Form(
self.env['mail.compose.message'].with_context({
'default_composition_mode': 'comment',
@@ -393,7 +401,7 @@ class TestMailAPIPerformance(BaseMailPerformance):
composer_form.attachment_ids.add(attachment)
composer = composer_form.save()
with self.assertQueryCount(admin=49, employee=49): # tm+com 48/48
with self.assertQueryCount(admin=47, employee=47): # tm+com 46/46
composer._action_send_mail()
# notifications
@@ -415,7 +423,7 @@ class TestMailAPIPerformance(BaseMailPerformance):
'default_template_id': test_template.id,
}).create({})
with self.assertQueryCount(admin=119, employee=122), self.mock_mail_gateway():
with self.assertQueryCount(admin=108, employee=111), self.mock_mail_gateway():
composer._action_send_mail()
self.assertEqual(len(self._new_mails), 10)
@@ -437,7 +445,7 @@ class TestMailAPIPerformance(BaseMailPerformance):
'partner_ids': [(4, customer_id)],
})
with self.assertQueryCount(admin=36, employee=36):
with self.assertQueryCount(admin=35, employee=35):
composer._action_send_mail()
@users('admin', 'employee')
@@ -447,7 +455,7 @@ class TestMailAPIPerformance(BaseMailPerformance):
test_record, test_template = self._create_test_records()
test_template.write({'attachment_ids': [(5, 0)]})
with self.assertQueryCount(admin=25, employee=25): # tm 15/15 / com 23/23
with self.assertQueryCount(admin=25, employee=25): # tm 15/15 / com 24/24
composer = self.env['mail.compose.message'].with_context({
'default_composition_mode': 'comment',
'default_model': test_record._name,
@@ -455,7 +463,7 @@ class TestMailAPIPerformance(BaseMailPerformance):
'default_template_id': test_template.id,
}).create({})
with self.assertQueryCount(admin=36, employee=36):
with self.assertQueryCount(admin=35, employee=35):
composer._action_send_mail()
# notifications
@@ -471,7 +479,7 @@ class TestMailAPIPerformance(BaseMailPerformance):
def test_mail_composer_w_template_attachments(self):
test_record, test_template = self._create_test_records()
with self.assertQueryCount(admin=25, employee=25): # tm 15/15 / com 23/23
with self.assertQueryCount(admin=25, employee=25): # tm 15/15 / com 24/24
composer = self.env['mail.compose.message'].with_context({
'default_composition_mode': 'comment',
'default_model': test_record._name,
@@ -479,7 +487,7 @@ class TestMailAPIPerformance(BaseMailPerformance):
'default_template_id': test_template.id,
}).create({})
with self.assertQueryCount(admin=44, employee=44):
with self.assertQueryCount(admin=42, employee=42):
composer._action_send_mail()
# notifications
@@ -500,7 +508,7 @@ class TestMailAPIPerformance(BaseMailPerformance):
test_template.write({'attachment_ids': [(5, 0)]})
customer = self.env['res.partner'].browse(self.customer.ids)
with self.assertQueryCount(admin=38, employee=38): # tm 28/28 / com 36/36
with self.assertQueryCount(admin=38, employee=38): # tm 28/28 / com 37/37
composer_form = Form(
self.env['mail.compose.message'].with_context({
'default_composition_mode': 'comment',
@@ -511,7 +519,7 @@ class TestMailAPIPerformance(BaseMailPerformance):
)
composer = composer_form.save()
with self.assertQueryCount(admin=46, employee=46):
with self.assertQueryCount(admin=44, employee=44):
composer._action_send_mail()
# notifications
@@ -530,7 +538,7 @@ class TestMailAPIPerformance(BaseMailPerformance):
test_record, test_template = self._create_test_records()
customer = self.env['res.partner'].browse(self.customer.ids)
with self.assertQueryCount(admin=37, employee=37): # tm 27/27 / com 35/35
with self.assertQueryCount(admin=37, employee=37): # tm 27/27 / com 36/36
composer_form = Form(
self.env['mail.compose.message'].with_context({
'default_composition_mode': 'comment',
@@ -541,7 +549,7 @@ class TestMailAPIPerformance(BaseMailPerformance):
)
composer = composer_form.save()
with self.assertQueryCount(admin=64, employee=64):
with self.assertQueryCount(admin=61, employee=61):
composer._action_send_mail()
# notifications
@@ -568,7 +576,7 @@ class TestMailAPIPerformance(BaseMailPerformance):
# use another user already pre-defined with the email notification type,
# so the ormcache is preserved.
record = self.env['mail.test.track'].create({'name': 'Test'})
with self.assertQueryCount(admin=39, employee=39):
with self.assertQueryCount(admin=38, employee=38):
record.write({
'user_id': self.user_test_email.id,
})
@@ -651,7 +659,7 @@ class TestMailAPIPerformance(BaseMailPerformance):
def test_message_post_one_email_notification(self):
record = self.env['mail.test.simple'].create({'name': 'Test'})
with self.assertQueryCount(admin=31, employee=31):
with self.assertQueryCount(admin=30, employee=30):
record.message_post(
body=Markup('<p>Test Post Performances with an email ping</p>'),
partner_ids=self.customer.ids,
@@ -741,33 +749,136 @@ class TestMailAPIPerformance(BaseMailPerformance):
@tagged('mail_performance', 'post_install', '-at_install')
class TestMailComplexPerformance(BaseMailPerformance):
def setUp(self):
super(TestMailComplexPerformance, self).setUp()
self.user_portal = self.env['res.users'].with_context(self._test_context).create({
@classmethod
def setUpClass(cls):
super().setUpClass()
cls.user_portal = cls.env['res.users'].with_context(cls._test_context).create({
'name': 'Olivia Portal',
'login': 'port',
'email': 'p.p@example.com',
'signature': '--\nOlivia',
'notification_type': 'email',
'groups_id': [(6, 0, [self.env.ref('base.group_portal').id])],
'groups_id': [(6, 0, [cls.env.ref('base.group_portal').id])],
})
self.container = self.env['mail.test.container'].with_context(mail_create_nosubscribe=True).create({
'name': 'Test Container',
'customer_id': self.customer.id,
'alias_name': 'test-alias',
})
Partners = self.env['res.partner'].with_context(self._test_context)
self.partners = self.env['res.partner']
for x in range(0, 10):
self.partners |= Partners.create({'name': 'Test %s' % x, 'email': 'test%s@example.com' % x})
self.container.message_subscribe(self.partners.ids, subtype_ids=[
self.env.ref('mail.mt_comment').id,
self.env.ref('test_mail.st_mail_test_container_child_full').id
cls.containers = cls.env['mail.test.container'].with_context(mail_create_nosubscribe=True).create([
{
'alias_name': 'test-alias',
'customer_id': cls.customers[0].id,
'name': 'Test Container',
}, {
'alias_name': 'test-alias-2',
'customer_id': cls.customers[1].id,
'name': 'Test Container 2',
},
])
Partners = cls.env['res.partner'].with_context(cls._test_context)
cls.partners = Partners.create([
{
'name': f'Test {idx}',
'email': f'test{idx}@example.com',
}
for idx in range(10)
])
cls.containers.message_subscribe(cls.partners.ids, subtype_ids=[
cls.env.ref('mail.mt_comment').id,
cls.env.ref('test_mail.st_mail_test_container_child_full').id
])
cls.container = cls.containers[0]
name_field = cls.env['ir.model.fields']._get(cls.container._name, 'name')
customer_id_field = cls.env['ir.model.fields']._get(cls.container._name, 'customer_id')
comment_subtype_id = cls.env['ir.model.data']._xmlid_to_res_id('mail.mt_comment')
cls.messages_all = cls.env['mail.message'].sudo().create([
{
'attachment_ids': [
(0, 0, {
'datas': 'data',
'name': f'Test file {att_idx}',
'res_id': record.id,
'res_model': record._name,
})
for att_idx in range(2)
],
'author_id': cls.partners[msg_idx].id,
'body': f'<p>Test {msg_idx}</p>',
'email_from': cls.partners[msg_idx].email_formatted,
'link_preview_ids': [
(0, 0, {
'source_url': 'https://www.odoo.com',
}), (0, 0, {
'source_url': 'https://www.example.com',
}),
],
'message_type': 'comment',
'model': record._name,
'notification_ids': [
(0, 0, {
'is_read': False,
'notification_type': 'inbox',
'res_partner_id': cls.partners[(record_idx * 5) + (msg_idx * 2)].id,
}),
(0, 0, {
'is_read': True,
'notification_type': 'email',
'notification_status': 'sent',
'res_partner_id': cls.partners[(record_idx * 5) + (msg_idx * 2) + 1].id,
}),
(0, 0, {
'is_read': True,
'notification_type': 'email',
'notification_status': 'exception',
'res_partner_id': cls.partners[(record_idx * 5) + (msg_idx * 2) + 2].id,
}),
],
'partner_ids': [
(4, cls.partners[(record_idx * 5) + msg_idx].id),
(4, cls.partners[(record_idx * 5) + msg_idx + 1].id),
],
'reaction_ids': [
(0, 0, {
'content': '\U0001F4E7',
'partner_id': cls.partners[(record_idx * 5)].id
}), (0, 0, {
'content': '\U0001F4E8',
'partner_id': cls.partners[(record_idx * 5) + 1].id
}),
],
'res_id': record.id,
'starred_partner_ids': [
(4, cls.partners[(record_idx * 5) + msg_idx].id),
(4, cls.partners[(record_idx * 5) + (msg_idx * 2) + 1].id),
],
'subject': f'Test Container {msg_idx}',
'subtype_id': comment_subtype_id,
'tracking_value_ids': [
(0, 0, {
'field': name_field.id,
'field_desc': 'Name',
'new_value_char': 'new 0',
'old_value_char': 'old 0',
}),
(0, 0, {
'field': customer_id_field.id,
'field_desc': 'Customer',
'new_value_char': 'new 1',
'new_value_integer': cls.partners[(record_idx * 5)].id,
'old_value_char': 'old 1',
'old_value_integer': cls.partners[(record_idx * 5) + 1].id,
}),
]
}
for msg_idx in range(2)
for record_idx, record in enumerate(cls.containers)
])
# `test_complex_mail_mail_send`
self.env.flush_all()
cls.env.flush_all()
def test_assert_initial_values(self):
self.assertEqual(len(self.messages_all), 2*2)
@mute_logger('odoo.tests', 'odoo.addons.mail.models.mail_mail', 'odoo.models.unlink')
@users('admin', 'employee')
@@ -793,7 +904,7 @@ class TestMailComplexPerformance(BaseMailPerformance):
'mail_message_id': message.id,
'recipient_ids': [(4, pid) for pid in self.partners.ids],
})
with self.assertQueryCount(admin=9, employee=9):
with self.assertQueryCount(admin=7, employee=7):
self.env['mail.mail'].sudo().browse(mail.ids).send()
@mute_logger('odoo.tests', 'odoo.addons.mail.models.mail_mail', 'odoo.models.unlink')
@@ -832,7 +943,7 @@ class TestMailComplexPerformance(BaseMailPerformance):
unlinked_mails |= set(records.ids)
unlinked_mails = set()
with self.assertQueryCount(admin=32, employee=32), \
with self.assertQueryCount(admin=30, employee=30), \
patch.object(type(self.env['mail.mail']), 'unlink', _patched_unlink):
self.env['mail.mail'].sudo().browse(mails.ids).send()
@@ -852,7 +963,7 @@ class TestMailComplexPerformance(BaseMailPerformance):
record = self.container.with_user(self.env.user)
# about 20 (19?) queries per additional customer group
with self.assertQueryCount(admin=56, employee=55):
with self.assertQueryCount(admin=55, employee=54):
record.message_post(
body=Markup('<p>Test Post Performances</p>'),
message_type='comment',
@@ -870,7 +981,7 @@ class TestMailComplexPerformance(BaseMailPerformance):
template = self.env.ref('test_mail.mail_test_container_tpl')
# about 20 (19 ?) queries per additional customer group
with self.assertQueryCount(admin=63, employee=62):
with self.assertQueryCount(admin=62, employee=61):
record.message_post_with_source(
template,
message_type='comment',
@@ -960,7 +1071,7 @@ class TestMailComplexPerformance(BaseMailPerformance):
})
rec1 = rec.with_context(active_test=False) # to see inactive records
self.assertEqual(rec1.message_partner_ids, self.partners | self.env.user.partner_id)
with self.assertQueryCount(admin=39, employee=39):
with self.assertQueryCount(admin=38, employee=38):
rec.write({'user_id': self.user_portal.id})
self.assertEqual(rec1.message_partner_ids, self.partners | self.env.user.partner_id | self.user_portal.partner_id)
# write tracking message
@@ -980,7 +1091,7 @@ class TestMailComplexPerformance(BaseMailPerformance):
customer_id = self.customer.id
user_id = self.user_portal.id
with self.assertQueryCount(admin=94, employee=94):
with self.assertQueryCount(admin=92, employee=92):
rec = self.env['mail.test.ticket'].create({
'name': 'Test',
'container_id': container_id,
@@ -1009,7 +1120,7 @@ class TestMailComplexPerformance(BaseMailPerformance):
rec1 = rec.with_context(active_test=False) # to see inactive records
self.assertEqual(rec1.message_partner_ids, self.user_portal.partner_id | self.env.user.partner_id)
self.assertEqual(len(rec1.message_ids), 1)
with self.assertQueryCount(admin=59, employee=59):
with self.assertQueryCount(admin=58, employee=58):
rec.write({
'name': 'Test2',
'container_id': self.container.id,
@@ -1046,7 +1157,7 @@ class TestMailComplexPerformance(BaseMailPerformance):
rec1 = rec.with_context(active_test=False) # to see inactive records
self.assertEqual(rec1.message_partner_ids, self.user_portal.partner_id | self.env.user.partner_id)
with self.assertQueryCount(admin=65, employee=65):
with self.assertQueryCount(admin=64, employee=64):
rec.write({
'name': 'Test2',
'container_id': container_id,
@@ -1079,7 +1190,7 @@ class TestMailComplexPerformance(BaseMailPerformance):
rec1 = rec.with_context(active_test=False) # to see inactive records
self.assertEqual(rec1.message_partner_ids, self.partners | self.env.user.partner_id | self.user_portal.partner_id)
with self.assertQueryCount(admin=33, employee=33):
with self.assertQueryCount(admin=32, employee=32):
rec.write({
'name': 'Test2',
'customer_id': customer_id,
@@ -1100,115 +1211,42 @@ class TestMailComplexPerformance(BaseMailPerformance):
@mute_logger('odoo.tests', 'odoo.addons.mail.models.mail_mail', 'odoo.models.unlink')
@users('employee')
@warmup
def test_message_format(self):
def test_message_format_multi(self):
"""Test performance of `_message_format` and of `message_format` with
multiple messages with multiple attachments, different authors, various
notifications, and different tracking values.
Those messages might not make sense functionally but they are crafted to
cover as much of the code as possible in regard to number of queries.
Setup :
* 2 records (self.containers -> 2 mail.test.container record, with
a different customer_id each)
* 2 messages / record
* 2 attachments / message
* 3 notifications / message
* 2 tracking values / message
"""
name_field = self.env['ir.model.fields']._get(self.container._name, 'name')
customer_id_field = self.env['ir.model.fields']._get(self.container._name, 'customer_id')
messages_all = self.messages_all.with_env(self.env)
messages = self.env['mail.message'].sudo().create([{
'subject': 'Test 0',
'body': '<p>Test 0</p>',
'author_id': self.partners[0].id,
'email_from': self.partners[0].email,
'model': 'mail.test.container',
'res_id': self.container.id,
'subtype_id': self.env['ir.model.data']._xmlid_to_res_id('mail.mt_comment'),
'attachment_ids': [
(0, 0, {
'name': 'test file 0 - %d' % j,
'datas': 'data',
}) for j in range(2)
],
'notification_ids': [
(0, 0, {
'res_partner_id': self.partners[3].id,
'notification_type': 'inbox',
}),
(0, 0, {
'res_partner_id': self.partners[4].id,
'notification_type': 'email',
'notification_status': 'exception',
}),
(0, 0, {
'res_partner_id': self.partners[6].id,
'notification_type': 'email',
'notification_status': 'exception',
}),
],
'tracking_value_ids': [
(0, 0, {
'field': name_field.id,
'field_desc': 'Name',
'old_value_char': 'old 0',
'new_value_char': 'new 0',
}),
(0, 0, {
'field': customer_id_field.id,
'field_desc': 'Customer',
'old_value_integer': self.partners[7].id,
'new_value_integer': self.partners[8].id,
}),
]
}, {
'subject': 'Test 1',
'body': '<p>Test 1</p>',
'author_id': self.partners[1].id,
'email_from': self.partners[1].email,
'model': 'mail.test.container',
'res_id': self.container.id,
'subtype_id': self.env['ir.model.data']._xmlid_to_res_id('mail.mt_note'),
'attachment_ids': [
(0, 0, {
'name': 'test file 1 - %d' % j,
'datas': 'data',
}) for j in range(2)
],
'notification_ids': [
(0, 0, {
'res_partner_id': self.partners[5].id,
'notification_type': 'inbox',
}),
(0, 0, {
'res_partner_id': self.partners[6].id,
'notification_type': 'email',
'notification_status': 'exception',
}),
],
'tracking_value_ids': [
(0, 0, {
'field': name_field.id,
'field_desc': 'Name',
'old_value_char': 'old 1',
'new_value_char': 'new 1',
}),
(0, 0, {
'field': customer_id_field.id,
'field_desc': 'Customer',
'old_value_integer': self.partners[7].id,
'new_value_integer': self.partners[8].id,
}),
]
}])
with self.assertQueryCount(employee=24): # test_mail: 24
res = messages_all.message_format()
with self.assertQueryCount(employee=7):
res = messages.message_format()
self.assertEqual(len(res), 2)
for message in res:
self.assertEqual(len(message['attachment_ids']), 2)
self.assertEqual(len(res), 2*2)
for message in res:
self.assertEqual(len(message['attachment_ids']), 2)
self.env.flush_all()
self.env.invalidate_all()
@mute_logger('odoo.tests', 'odoo.addons.mail.models.mail_mail', 'odoo.models.unlink')
@users('employee')
@warmup
def test_message_format_single(self):
message = self.messages_all[0].with_env(self.env)
with self.assertQueryCount(employee=18):
res = messages.message_format()
self.assertEqual(len(res), 2)
for message in res:
self.assertEqual(len(message['attachment_ids']), 2)
with self.assertQueryCount(employee=21): # test_mail: 21
res = message.message_format()
self.assertEqual(len(res), 1)
self.assertEqual(len(res[0]['attachment_ids']), 2)
@mute_logger('odoo.tests', 'odoo.addons.mail.models.mail_mail', 'odoo.models.unlink')
@users('employee')
@@ -1309,7 +1347,7 @@ class TestMailHeavyPerformancePost(BaseMailPerformance):
attachments = self.env['ir.attachment'].with_user(self.env.user).create(self.test_attachments_vals)
# enable_logging = self.cr._enable_logging() if self.warm else nullcontext()
# with self.assertQueryCount(employee=63), enable_logging:
with self.assertQueryCount(employee=62):
with self.assertQueryCount(employee=60):
record_container.with_context({}).message_post(
body=Markup('<p>Test body <img src="cid:cid1"> <img src="cid:cid2"></p>'),
subject='Test Subject',
@@ -104,7 +104,6 @@ class MailTestRatingThread(models.Model):
- partner_id: value returned by the base _rating_get_partner method
- user_id: value returned by the base _rating_get_operator method
"""
_description = 'Model for testing rating without the rating mixin'
_name = 'mail.test.rating.thread'
_inherit = 'mail.thread'
@@ -1,7 +1,9 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from datetime import datetime, timedelta
from markupsafe import Markup
from odoo.addons.mail.tests.common import mail_new_test_user
from odoo.addons.test_mail.tests.test_performance import BaseMailPerformance
from odoo.tests.common import users, warmup
@@ -10,11 +12,11 @@ from odoo.tools import mute_logger
@tagged('mail_performance', 'post_install', '-at_install')
class TestMailPerformance(BaseMailPerformance):
class FullBaseMailPerformance(BaseMailPerformance):
@classmethod
def setUpClass(cls):
super(TestMailPerformance, cls).setUpClass()
super().setUpClass()
# users / followers
cls.user_emp_email = mail_new_test_user(
@@ -38,36 +40,43 @@ class TestMailPerformance(BaseMailPerformance):
name='Paul Portal',
)
cls.customers = cls.env['res.partner'].create([
{'country_id': cls.env.ref('base.be').id,
'email': 'customer.full.test.1@example.com',
'name': 'Test Full Customer 1',
'mobile': '0456112233',
'phone': '0456112233',
{
'country_id': cls.env.ref('base.be').id,
'email': f'customer.full.test.{idx}@example.com',
'name': f'Test Full Customer {idx}',
'mobile': f'045600000{idx}',
'phone': f'045611111{idx}',
} for idx in range(5)
])
cls.test_users = cls.user_employee + cls.user_test + cls.user_test_email + cls.user_emp_email + cls.user_portal
# records
cls.record_containers = cls.env['mail.test.container.mc'].create([
{
'alias_name': 'test-alias-0',
'customer_id': cls.customers[0].id,
'name': 'Test Container 1',
},
{'country_id': cls.env.ref('base.be').id,
'email': 'customer.full.test.2@example.com',
'name': 'Test Full Customer 2',
'mobile': '0456223344',
'phone': '0456112233',
{
'alias_name': 'test-alias-1',
'customer_id': cls.customers[1].id,
'name': 'Test Container 2',
},
])
# record
cls.record_container = cls.env['mail.test.container.mc'].create({
'alias_name': 'test-alias',
'customer_id': cls.customer.id,
'name': 'Test Container',
})
cls.record_ticket = cls.env['mail.test.ticket.mc'].create({
'email_from': 'email.from@test.example.com',
'container_id': cls.record_container.id,
'container_id': cls.record_containers[0].id,
'customer_id': False,
'name': 'Test Ticket',
'user_id': cls.user_emp_email.id,
})
cls.record_ticket.message_subscribe(cls.customers.ids + cls.user_admin.partner_id.ids + cls.user_portal.partner_id.ids)
def test_initial_values(self):
@tagged('mail_performance', 'post_install', '-at_install')
class TestMailPerformance(FullBaseMailPerformance):
def test_assert_initial_values(self):
""" Simply ensure some values through all tests """
record_ticket = self.env['mail.test.ticket.mc'].browse(self.record_ticket.ids)
self.assertEqual(record_ticket.message_partner_ids,
@@ -82,7 +91,7 @@ class TestMailPerformance(BaseMailPerformance):
record_ticket = self.env['mail.test.ticket.mc'].browse(self.record_ticket.ids)
attachments = self.env['ir.attachment'].create(self.test_attachments_vals)
with self.assertQueryCount(employee=91): # tmf: 90
with self.assertQueryCount(employee=89): # test_mail_full: 88
new_message = record_ticket.message_post(
attachment_ids=attachments.ids,
body=Markup('<p>Test Content</p>'),
@@ -95,3 +104,216 @@ class TestMailPerformance(BaseMailPerformance):
new_message.notified_partner_ids,
self.user_emp_email.partner_id + self.user_admin.partner_id + self.customers + self.user_portal.partner_id
)
@tagged('mail_performance', 'post_install', '-at_install')
class TestPortalFormatPerformance(FullBaseMailPerformance):
"""Test performance of `portal_message_format` with multiple messages
with multiple attachments, with ratings.
Those messages might not make sense functionally but they are crafted to
cover as much of the code as possible in regard to number of queries.
Setup :
* 5 records (self.containers -> 5 mail.test.rating records, with
a different customer_id each)
* 2 messages / record
* 2 attachments / message
"""
@classmethod
def setUpClass(cls):
super().setUpClass()
# rating-enabled test records
cls.record_ratings = cls.env['mail.test.rating'].create([
{
'customer_id': cls.customers[idx].id,
'name': f'TestRating_{idx}',
'user_id': cls.test_users[idx].id,
}
for idx in range(5)
])
# messages and ratings
user_id_field = cls.env['ir.model.fields']._get(cls.record_ratings._name, 'user_id')
comment_subtype_id = cls.env['ir.model.data']._xmlid_to_res_id('mail.mt_comment')
cls.messages_all = cls.env['mail.message'].sudo().create([
{
'attachment_ids': [
(0, 0, {
'datas': 'data',
'name': f'Test file {att_idx}',
'res_id': record.id,
'res_model': record._name,
})
for att_idx in range(2)
],
'author_id': record.customer_id.id,
'body': f'<p>Test {msg_idx}</p>',
'date': datetime(2023, 5, 15, 10, 30, 5),
'email_from': record.customer_id.email_formatted,
'link_preview_ids': [
(0, 0, {
'source_url': 'https://www.odoo.com',
}), (0, 0, {
'source_url': 'https://www.example.com',
}),
],
'notification_ids': [
(0, 0, {
'is_read': False,
'notification_type': 'inbox',
'res_partner_id': cls.customers[(msg_idx * 2)].id,
}),
(0, 0, {
'is_read': True,
'notification_type': 'email',
'notification_status': 'sent',
'res_partner_id': cls.customers[(msg_idx * 2) + 1].id,
}),
],
'message_type': 'comment',
'model': record._name,
'partner_ids': [
(4, cls.customers[(msg_idx * 2)].id),
(4, cls.customers[record_idx].id),
],
'reaction_ids': [
(0, 0, {
'content': 'https://www.odoo.com',
'partner_id': cls.customers[(msg_idx * 2) + 1].id
}), (0, 0, {
'content': 'https://www.example.com',
'partner_id': cls.customers[record_idx].id
}),
],
'res_id': record.id,
'subject': f'Test Rating {msg_idx}',
'subtype_id': comment_subtype_id,
'starred_partner_ids': [
(4, cls.customers[(msg_idx * 2)].id),
(4, cls.customers[(msg_idx * 2) + 1].id),
],
'tracking_value_ids': [
(0, 0, {
'field': user_id_field.id,
'field_desc': 'Responsible',
'new_value_char': 'new 1',
'new_value_integer': record.user_id.id,
'old_value_char': 'old 1',
'old_value_integer': cls.user_admin.id,
}),
]
}
for msg_idx in range(2)
for record_idx, record in enumerate(cls.record_ratings)
])
cls.messages_records = [cls.env[message.model].browse(message.res_id) for message in cls.messages_all]
# ratings values related to rating-enabled records
cls.ratings_all = cls.env['rating.rating'].sudo().create([
{
'consumed': True,
'message_id': message.id,
'partner_id': record.customer_id.id,
'publisher_comment': 'Comment',
'publisher_id': cls.user_admin.partner_id.id,
'publisher_datetime': datetime(2023, 5, 15, 10, 30, 5) - timedelta(days=2),
'rated_partner_id': record.user_id.partner_id.id,
'rating': 4,
'res_id': message.res_id,
'res_model_id': cls.env['ir.model']._get_id(message.model),
}
for rating_idx in range(2)
for message, record in zip(cls.messages_all, cls.messages_records)
])
def test_assert_initial_values(self):
self.assertEqual(len(self.messages_all), 5 * 2)
self.assertEqual(len(self.ratings_all), len(self.messages_all) * 2)
@mute_logger('odoo.tests', 'odoo.addons.mail.models.mail_mail', 'odoo.models.unlink')
@users('employee')
@warmup
def test_portal_message_format_norating(self):
messages_all = self.messages_all.with_user(self.env.user)
with self.assertQueryCount(employee=31):
# res = messages_all.portal_message_format(options=None)
res = messages_all.portal_message_format(options={'rating_include': False})
comment_subtype = self.env.ref('mail.mt_comment')
self.assertEqual(len(res), len(messages_all))
for format_res, message, record in zip(res, messages_all, self.messages_records):
self.assertEqual(len(format_res['attachment_ids']), 2)
self.assertEqual(
format_res['attachment_ids'],
[
{
'access_token': message.attachment_ids[0].access_token,
'checksum': message.attachment_ids[0].checksum,
'filename': 'Test file 1',
'id': message.attachment_ids[0].id,
'mimetype': 'application/octet-stream',
'name': 'Test file 1',
'res_id': record.id,
'res_model': record._name,
}, {
'access_token': message.attachment_ids[1].access_token,
'checksum': message.attachment_ids[1].checksum,
'filename': 'Test file 0',
'id': message.attachment_ids[1].id,
'mimetype': 'application/octet-stream',
'name': 'Test file 0',
'res_id': record.id,
'res_model': record._name,
}
]
)
self.assertEqual(format_res['author_id'], (record.customer_id.id, record.customer_id.display_name))
self.assertEqual(format_res['author_avatar_url'], f'/web/image/mail.message/{message.id}/author_avatar/50x50')
self.assertEqual(format_res['date'], datetime(2023, 5, 15, 10, 30, 5))
self.assertEqual(format_res['published_date_str'], 'May 15, 2023, 10:30:05 AM')
self.assertEqual(format_res['id'], message.id)
self.assertFalse(format_res['is_internal'])
self.assertFalse(format_res['is_message_subtype_note'])
self.assertEqual(format_res['subtype_id'], (comment_subtype.id, comment_subtype.name))
# should not be in, not asked
self.assertNotIn('rating', format_res)
self.assertNotIn('rating_stats', format_res)
self.assertNotIn('rating_value', format_res)
@mute_logger('odoo.tests', 'odoo.addons.mail.models.mail_mail', 'odoo.models.unlink')
@users('employee')
@warmup
def test_portal_message_format_rating(self):
messages_all = self.messages_all.with_user(self.env.user)
with self.assertQueryCount(employee=45):
res = messages_all.portal_message_format(options={'rating_include': True})
self.assertEqual(len(res), len(messages_all))
for format_res, _message, _record in zip(res, messages_all, self.messages_records):
self.assertEqual(format_res['rating']['publisher_avatar'], f'/web/image/res.partner/{self.partner_admin.id}/avatar_128/50x50')
self.assertEqual(format_res['rating']['publisher_comment'], 'Comment')
self.assertEqual(format_res['rating']['publisher_id'], self.partner_admin.id)
self.assertEqual(format_res['rating']['publisher_datetime'], 'May 13, 2023, 10:30:05 AM')
self.assertEqual(format_res['rating']['publisher_name'], self.partner_admin.display_name)
self.assertDictEqual(
format_res['rating_stats'],
{'avg': 4.0, 'total': 4, 'percent': {1: 0.0, 2: 0.0, 3: 0.0, 4: 100.0, 5: 0.0}}
)
self.assertEqual(format_res['rating_value'], 4)
@mute_logger('odoo.tests', 'odoo.addons.mail.models.mail_mail', 'odoo.models.unlink')
@users('employee')
@warmup
def test_portal_message_format_monorecord(self):
message = self.messages_all[0].with_user(self.env.user)
with self.assertQueryCount(employee=18):
res = message.portal_message_format(options={'rating_include': True})
self.assertEqual(len(res), 1)
@@ -35,7 +35,7 @@ class TestSMSPerformance(BaseMailPerformance, sms_common.SMSCase):
def test_message_sms_record_1_partner(self):
record = self.test_record.with_user(self.env.user)
pids = self.customer.ids
with self.mockSMSGateway(sms_allow_unlink=True), self.assertQueryCount(employee=26):
with self.mockSMSGateway(sms_allow_unlink=True), self.assertQueryCount(employee=25):
messages = record._message_sms(
body='Performance Test',
partner_ids=pids,
@@ -50,7 +50,7 @@ class TestSMSPerformance(BaseMailPerformance, sms_common.SMSCase):
def test_message_sms_record_10_partners(self):
record = self.test_record.with_user(self.env.user)
pids = self.partners.ids
with self.mockSMSGateway(sms_allow_unlink=True), self.assertQueryCount(employee=26):
with self.mockSMSGateway(sms_allow_unlink=True), self.assertQueryCount(employee=25):
messages = record._message_sms(
body='Performance Test',
partner_ids=pids,
@@ -64,7 +64,7 @@ class TestSMSPerformance(BaseMailPerformance, sms_common.SMSCase):
@warmup
def test_message_sms_record_default(self):
record = self.test_record.with_user(self.env.user)
with self.mockSMSGateway(sms_allow_unlink=True), self.assertQueryCount(employee=28):
with self.mockSMSGateway(sms_allow_unlink=True), self.assertQueryCount(employee=27):
messages = record._message_sms(
body='Performance Test',
)