diff --git a/addons/mail/models/mail_message.py b/addons/mail/models/mail_message.py index 21d92b5193e..cb5b71d10b8 100644 --- a/addons/mail/models/mail_message.py +++ b/addons/mail/models/mail_message.py @@ -941,35 +941,29 @@ class Message(models.Model): # MESSAGE READ / FETCH / FAILURE API # ------------------------------------------------------ - @api.model - def _message_read_dict_postprocess(self, messages, message_tree): - """ Post-processing on values given by message_read. This method will - handle partners in batch to avoid doing numerous queries. - - :param list messages: list of message, as get_dict result - :param dict message_tree: {[msg.id]: msg browse record as super user} - """ + def _message_format(self, fnames): + """Reads values from messages and formats them for the web client.""" + self.check_access_rule('read') + vals_list = self._read_format(fnames) safari = request and request.httprequest.user_agent.browser == 'safari' - - for message_dict in messages: - message_id = message_dict.get('id') - message = message_tree[message_id] + for vals in vals_list: + message_sudo = self.browse(vals['id']).sudo().with_prefetch(self.ids) # Author - if message.author_id: - author = (message.author_id.id, message.author_id.display_name) + if message_sudo.author_id: + author = (message_sudo.author_id.id, message_sudo.author_id.display_name) else: - author = (0, message.email_from) + author = (0, message_sudo.email_from) # Notifications customer_email_status = ( - (all(n.notification_status == 'sent' for n in message.notification_ids if n.notification_type == 'email') and 'sent') or - (any(n.notification_status == 'exception' for n in message.notification_ids if n.notification_type == 'email') and 'exception') or - (any(n.notification_status == 'bounce' for n in message.notification_ids if n.notification_type == 'email') and 'bounce') or + (all(n.notification_status == 'sent' for n in message_sudo.notification_ids if n.notification_type == 'email') and 'sent') or + (any(n.notification_status == 'exception' for n in message_sudo.notification_ids if n.notification_type == 'email') and 'exception') or + (any(n.notification_status == 'bounce' for n in message_sudo.notification_ids if n.notification_type == 'email') and 'bounce') or 'ready' ) customer_email_data = [] - filtered_notifications = message.notification_ids.filtered(lambda n: + filtered_notifications = message_sudo.notification_ids.filtered(lambda n: n.notification_type == 'email' and n.res_partner_id.active and (n.notification_status in ('bounce', 'exception', 'canceled') or n.res_partner_id.partner_share) ) @@ -978,10 +972,10 @@ class Message(models.Model): # Attachments main_attachment = self.env['ir.attachment'] - if message.attachment_ids and message.res_id and issubclass(self.pool[message.model], self.pool['mail.thread']): - main_attachment = self.env[message.model].sudo().browse(message.res_id).message_main_attachment_id + if message_sudo.attachment_ids and message_sudo.res_id and issubclass(self.pool[message_sudo.model], self.pool['mail.thread']): + main_attachment = self.env[message_sudo.model].sudo().browse(message_sudo.res_id).message_main_attachment_id attachment_ids = [] - for attachment in message.attachment_ids: + for attachment in message_sudo.attachment_ids: attachment_ids.append({ 'id': attachment.id, 'filename': attachment.name, @@ -992,7 +986,7 @@ class Message(models.Model): # Tracking values tracking_value_ids = [] - for tracking in message.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({ @@ -1003,7 +997,7 @@ class Message(models.Model): 'field_type': tracking.field_type, }) - message_dict.update({ + vals.update({ 'author_id': author, 'customer_email_status': customer_email_status, 'customer_email_data': customer_email_data, @@ -1011,7 +1005,7 @@ class Message(models.Model): 'tracking_value_ids': tracking_value_ids, }) - return True + return vals_list def message_fetch_failed(self): messages = self.search([ @@ -1091,28 +1085,25 @@ class Message(models.Model): 'moderation_status': 'pending_moderation' } """ - message_values = self.read(self._get_message_format_fields()) - message_tree = dict((m.id, m) for m in self.sudo()) - self._message_read_dict_postprocess(message_values, message_tree) + vals_list = self._message_format(self._get_message_format_fields()) com_id = self.env['ir.model.data'].xmlid_to_res_id('mail.mt_comment') note_id = self.env['ir.model.data'].xmlid_to_res_id('mail.mt_note') - for message in message_values: - message_sudo = message_tree[message['id']] + for vals in vals_list: + message_sudo = self.browse(vals['id']).sudo().with_prefetch(self.ids) notifs = message_sudo.notification_ids.filtered(lambda n: n.res_partner_id) - message.update({ + vals.update({ 'needaction_partner_ids': notifs.filtered(lambda n: not n.is_read).res_partner_id.ids, 'history_partner_ids': notifs.filtered(lambda n: n.is_read).res_partner_id.ids, 'is_note': message_sudo.subtype_id.id == note_id, 'is_discussion': message_sudo.subtype_id.id == com_id, - 'subtype_description': message_sudo.subtype_id.description + 'subtype_description': message_sudo.subtype_id.description, + 'is_notification': vals['message_type'] == 'user_notification', }) - message['is_notification'] = message['message_type'] == 'user_notification' - - if message['model'] and self.env[message['model']]._original_module: - message['module_icon'] = modules.module.get_module_icon(self.env[message['model']]._original_module) - return message_values + if vals['model'] and self.env[vals['model']]._original_module: + vals['module_icon'] = modules.module.get_module_icon(self.env[vals['model']]._original_module) + return vals_list def _get_message_format_fields(self): return [ diff --git a/addons/portal/models/mail_message.py b/addons/portal/models/mail_message.py index cfda6d5eaa0..91d40d7a891 100644 --- a/addons/portal/models/mail_message.py +++ b/addons/portal/models/mail_message.py @@ -1,7 +1,7 @@ # -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. -from odoo import api, models +from odoo import models class MailMessage(models.Model): @@ -15,12 +15,10 @@ class MailMessage(models.Model): ]) def _portal_message_format(self, fields_list): - message_values = self.read(fields_list) - message_tree = dict((m.id, m) for m in self.sudo()) - self._message_read_dict_postprocess(message_values, message_tree) + vals_list = self._message_format(fields_list) IrAttachmentSudo = self.env['ir.attachment'].sudo() - for message in message_values: - for attachment in message.get('attachment_ids', []): + for vals in vals_list: + for attachment in vals.get('attachment_ids', []): if not attachment.get('access_token'): attachment['access_token'] = IrAttachmentSudo.browse(attachment['id']).generate_access_token()[0] - return message_values + return vals_list diff --git a/addons/portal_rating/models/mail_message.py b/addons/portal_rating/models/mail_message.py index 9f2be5d1a25..20716960503 100644 --- a/addons/portal_rating/models/mail_message.py +++ b/addons/portal_rating/models/mail_message.py @@ -13,16 +13,16 @@ class MailMessage(models.Model): field_list += ['rating_value'] return super(MailMessage, self)._portal_message_format(field_list) - def _message_read_dict_postprocess(self, message_values, message_tree): + def _message_format(self, fnames): """ Override the method to add information about a publisher comment on each rating messages if requested, and compute a plaintext value of it. """ - res = super(MailMessage, self)._message_read_dict_postprocess(message_values, message_tree) + vals_list = super(MailMessage, self)._message_format(fnames) 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 values in message_values: - values["rating"] = mid_rating_tree.get(values['id'], {}) - return res + for vals in vals_list: + vals["rating"] = mid_rating_tree.get(vals['id'], {}) + return vals_list diff --git a/addons/test_mail/tests/test_performance.py b/addons/test_mail/tests/test_performance.py index 61c2b96a61a..b952fe73388 100644 --- a/addons/test_mail/tests/test_performance.py +++ b/addons/test_mail/tests/test_performance.py @@ -3,7 +3,7 @@ import base64 from odoo.addons.base.tests.common import TransactionCaseWithUserDemo -from odoo.tests.common import TransactionCase, users, warmup +from odoo.tests.common import users, warmup from odoo.tests import tagged from odoo.tools import mute_logger, formataddr @@ -267,7 +267,7 @@ class TestMailAPIPerformance(BaseMailPerformance): 'partner_ids': [(4, customer_id)], }) - with self.assertQueryCount(__system__=33, emp=38): + with self.assertQueryCount(__system__=31, emp=37): composer.send_mail() @users('__system__', 'emp') @@ -286,7 +286,7 @@ class TestMailAPIPerformance(BaseMailPerformance): }).create({}) composer.onchange_template_id_wrapper() - with self.assertQueryCount(__system__=41, emp=45): + with self.assertQueryCount(__system__=39, emp=44): composer.send_mail() # remove created partner to ensure tests are the same each run @@ -307,7 +307,7 @@ class TestMailAPIPerformance(BaseMailPerformance): @warmup def test_message_assignation_inbox(self): record = self.env['mail.test.track'].create({'name': 'Test'}) - with self.assertQueryCount(__system__=22, emp=24): + with self.assertQueryCount(__system__=19, emp=22): record.write({ 'user_id': self.user_test.id, }) @@ -363,7 +363,7 @@ class TestMailAPIPerformance(BaseMailPerformance): def test_message_post_one_inbox_notification(self): record = self.env['mail.test.simple'].create({'name': 'Test'}) - with self.assertQueryCount(__system__=17, emp=19): + with self.assertQueryCount(__system__=15, emp=18): record.message_post( body='

Test Post Performances with an inbox ping

', partner_ids=self.user_test.partner_id.ids, @@ -700,9 +700,9 @@ class TestMailComplexPerformance(BaseMailPerformance): @users('emp') @warmup def test_message_format(self): - """Test performance of `_message_read_dict_postprocess` and of - `message_format` with multiple messages with multiple attachments, - different authors, various notifications, and different tracking values. + """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. """ @@ -794,7 +794,7 @@ class TestMailComplexPerformance(BaseMailPerformance): ] }]) - with self.assertQueryCount(emp=9): + with self.assertQueryCount(emp=6): res = messages.message_format() self.assertEqual(len(res), 2) for message in res: @@ -928,7 +928,7 @@ class TestMailHeavyPerformancePost(BaseMailPerformance): ] self.attachements = self.env['ir.attachment'].with_user(self.env.user).create(self.vals) attachement_ids = self.attachements.ids - with self.assertQueryCount(emp=88): + with self.assertQueryCount(emp=86): self.cr.sql_log = self.warm and self.cr.sql_log_count record.with_context({}).message_post( body='

Test body

', diff --git a/odoo/models.py b/odoo/models.py index bbec718b5a3..a5d8ddb7900 100644 --- a/odoo/models.py +++ b/odoo/models.py @@ -2932,11 +2932,20 @@ Fields: stored_fields.add(f.name) self._read(stored_fields) - # retrieve results from records; this takes values from the cache and - # computes remaining fields + return self._read_format(fnames=fields, load=load) + + def _read_format(self, fnames, load='_classic_read'): + """Returns a list of dictionaries mapping field names to their values, + with one dictionary per record that exists. + + The output format is similar to the one expected from the `read` method. + + The current method is different from `read` because it retrieves its + values from the cache without doing a query when it is avoidable. + """ data = [(record, {'id': record._ids[0]}) for record in self] use_name_get = (load == '_classic_read') - for name in fields: + for name in fnames: convert = self._fields[name].convert_to_read for record, vals in data: # missing records have their vals empty