From c3873cb1e1ffcd38d322e4b1e43fbbdaf45c57d3 Mon Sep 17 00:00:00 2001 From: Pierre-Yves Dufays Date: Wed, 19 Oct 2022 06:40:29 +0000 Subject: [PATCH] [IMP] mail, rating, various: support base rating feature in mail.thread Various: im_livechat, rating, test_mail_full, website_{sale|slides} Allows to rate any record extending mail_thread. This change was needed because rating template could be created for any models, even those not inheriting from rating.mixin. If sent on a record of such model, it was crashing when the controllers using rating mixin features were used. This is no longer the case. This commit moves the code from the mixin rating.mixin to mail.thread and adapts the test for testing rating submission with and without the mixin. Base behavior accepts rating and provides an access to ratings through the 'rating_ids' field. Rating.mixin inherits now from mail.thread to ease computation, and adds statistics and some advanced capabilities. Inheritance of some model have been reordered now that rating is build on top of mail.thread. Task-2674649 Part-of: odoo/odoo#103966 --- addons/im_livechat/models/mail_channel.py | 2 +- addons/rating/models/mail_thread.py | 165 +++++++++++++- addons/rating/models/rating_mixin.py | 146 +----------- .../models/test_mail_models_mail.py | 41 +++- .../security/ir.model.access.csv | 3 + addons/test_mail_full/tests/test_rating.py | 208 +++++++++++------- .../website_sale/models/product_template.py | 2 +- addons/website_slides/models/slide_channel.py | 2 +- 8 files changed, 329 insertions(+), 240 deletions(-) diff --git a/addons/im_livechat/models/mail_channel.py b/addons/im_livechat/models/mail_channel.py index 9476b58b8b6..b62a7c570aa 100644 --- a/addons/im_livechat/models/mail_channel.py +++ b/addons/im_livechat/models/mail_channel.py @@ -14,7 +14,7 @@ class MailChannel(models.Model): """ _name = 'mail.channel' - _inherit = ['mail.channel', 'rating.mixin'] + _inherit = ['rating.mixin', 'mail.channel'] anonymous_name = fields.Char('Anonymous Name') channel_type = fields.Selection(selection_add=[('livechat', 'Livechat Conversation')], ondelete={'livechat': 'cascade'}) diff --git a/addons/rating/models/mail_thread.py b/addons/rating/models/mail_thread.py index c9196018c6c..e33f6e0c334 100644 --- a/addons/rating/models/mail_thread.py +++ b/addons/rating/models/mail_thread.py @@ -1,12 +1,175 @@ # -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. -from odoo import api, models, _ +import datetime +import markupsafe + +from odoo import _, api, fields, models, tools class MailThread(models.AbstractModel): _inherit = 'mail.thread' + rating_ids = fields.One2many('rating.rating', 'res_id', string='Ratings', groups='base.group_user', + domain=lambda self: [('res_model', '=', self._name)], auto_join=True) + + # MAIL OVERRIDES + # -------------------------------------------------- + + def unlink(self): + """ When removing a record, its rating should be deleted too. """ + record_ids = self.ids + result = super().unlink() + self.env['rating.rating'].sudo().search([('res_model', '=', self._name), ('res_id', 'in', record_ids)]).unlink() + return result + + def _message_create(self, values_list): + """ Force usage of rating-specific methods and API allowing to delegate + computation to records. Keep methods optimized and skip rating_ids + support to simplify MailThrad main API. """ + if not isinstance(values_list, list): + values_list = [values_list] + if any(values.get('rating_ids') for values in values_list): + raise ValueError(_("Posting a rating should be done using message post API.")) + return super()._message_create(values_list) + + # RATING CONFIGURATION + # -------------------------------------------------- + + def _rating_apply_get_default_subtype_id(self): + return self.env['ir.model.data']._xmlid_to_res_id("mail.mt_comment") + + def _rating_get_operator(self): + """ Return the operator (partner) that is the person who is rated. + + :return record: res.partner singleton + """ + if 'user_id' in self and self.user_id.partner_id: + return self.user_id.partner_id + return self.env['res.partner'] + + def _rating_get_partner(self): + """ Return the customer (partner) that performs the rating. + + :return record: res.partner singleton + """ + if 'partner_id' in self and self.partner_id: + return self.partner_id + return self.env['res.partner'] + + # RATING SUPPORT + # -------------------------------------------------- + + def _rating_get_access_token(self, partner=None): + """ Return access token linked to existing ratings, or create a new rating + that will create the asked token. An explicit call to access rights is + performed as sudo is used afterwards as this method could be used from + different sources, notably templates. """ + self.check_access_rights('read') + self.check_access_rule('read') + if not partner: + partner = self._rating_get_partner() + rated_partner = self._rating_get_operator() + rating = next( + (r for r in self.rating_ids.sudo() + if r.partner_id.id == partner.id and not r.consumed), + None) + if not rating: + rating = self.env['rating.rating'].sudo().create({ + 'partner_id': partner.id, + 'rated_partner_id': rated_partner.id, + 'res_model_id': self.env['ir.model']._get_id(self._name), + 'res_id': self.id, + 'is_internal': False, + }) + return rating.access_token + + # EXPOSED API + # -------------------------------------------------- + + def rating_send_request(self, template, lang=False, force_send=True): + """ This method send rating request by email, using a template given in parameter. + + :param record template: a mail.template record used to compute the message body; + :param str lang: optional lang; it can also be specified directly on the template + itself in the lang field; + :param bool force_send: whether to send the request directly or use the mail + queue cron (preferred option); + """ + if lang: + template = template.with_context(lang=lang) + self.with_context(mail_notify_force_send=force_send).message_post_with_source( + template, + email_layout_xmlid='mail.mail_notification_light', + force_send=force_send, + subtype_xmlid='mail.mt_note', + ) + + def rating_apply(self, rate, token=None, rating=None, feedback=None, + subtype_xmlid=None, notify_delay_send=False): + """ Apply a rating to the record. This rating can either be linked to a + token (customer flow) or directly a rating record (code flow). + + If the current model inherits from mail.thread mixin a message is posted + on its chatter. User going through this method should have at least + employee rights as well as rights on the current record because of rating + manipulation and chatter post (either employee, either sudo-ed in public + controllers after security check granting access). + + :param float rate: the rating value to apply (from 0 to 5); + :param string token: access token to fetch the rating to apply (optional); + :param record rating: rating.rating to apply (if no token); + :param string feedback: additional feedback (plaintext); + :param string subtype_xmlid: xml id of a valid mail.message.subtype used + to post the message (if it applies). If not given a classic comment is + posted; + :param notify_delay_send: Delay the sending by 2 hours of the email so the user + can still change his feedback. If False, the email will be sent immediately. + + :returns rating: rating.rating record + """ + if rate < 0 or rate > 5: + raise ValueError(_('Wrong rating value. A rate should be between 0 and 5 (received %d).', rate)) + if token: + rating = self.env['rating.rating'].search([('access_token', '=', token)], limit=1) + if not rating: + raise ValueError(_('Invalid token or rating.')) + + rating.write({'rating': rate, 'feedback': feedback, 'consumed': True}) + if issubclass(type(self), self.env.registry['mail.thread']): + if subtype_xmlid is None: + subtype_id = self._rating_apply_get_default_subtype_id() + else: + subtype_id = False + feedback = tools.plaintext2html(feedback or '') + + scheduled_datetime = ( + fields.Datetime.now() + datetime.timedelta(hours=2) + if notify_delay_send else None + ) + rating_body = ( + markupsafe.Markup( + ":%s/5%s" + ) % (rating.rating_image_url, rate, feedback) + ) + + if rating.message_id: + self._message_update_content( + rating.message_id, rating_body, + scheduled_date=scheduled_datetime, + strict=False + ) + else: + self.message_post( + author_id=rating.partner_id.id or None, # None will set the default author in mail/mail_thread.py + body=rating_body, + rating_id=rating.id, + scheduled_date=scheduled_datetime, + subtype_id=subtype_id, + subtype_xmlid=subtype_xmlid, + ) + return rating + @api.returns('mail.message', lambda value: value.id) def message_post(self, **kwargs): rating_id = kwargs.pop('rating_id', False) diff --git a/addons/rating/models/rating_mixin.py b/addons/rating/models/rating_mixin.py index b06797afd2e..f9dabc1fafa 100644 --- a/addons/rating/models/rating_mixin.py +++ b/addons/rating/models/rating_mixin.py @@ -1,20 +1,18 @@ # -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. -import datetime -import markupsafe - -from odoo import api, fields, models, tools +from odoo import api, fields, models from odoo.addons.rating.models import rating_data from odoo.osv import expression from odoo.tools.float_utils import float_compare, float_round class RatingMixin(models.AbstractModel): + """This mixin adds rating statistics to mail.thread that already support ratings.""" _name = 'rating.mixin' _description = "Rating Mixin" + _inherit = 'mail.thread' - rating_ids = fields.One2many('rating.rating', 'res_id', string='Rating', groups='base.group_user', domain=lambda self: [('res_model', '=', self._name)], auto_join=True) rating_last_value = fields.Float('Rating Last Value', groups='base.group_user', compute='_compute_rating_last_value', compute_sudo=True, store=True) rating_last_feedback = fields.Text('Rating Last Feedback', groups='base.group_user', related='rating_ids.feedback') rating_last_image = fields.Binary('Rating Last Image', groups='base.group_user', related='rating_ids.rating_image') @@ -110,16 +108,8 @@ class RatingMixin(models.AbstractModel): return result - def unlink(self): - """ When removing a record, its rating should be deleted too. """ - record_ids = self.ids - result = super(RatingMixin, self).unlink() - self.env['rating.rating'].sudo().search([('res_model', '=', self._name), ('res_id', 'in', record_ids)]).unlink() - return result - def _rating_get_parent_field_name(self): - """Return the parent relation field name - Should return a Many2One""" + """Return the parent relation field name. Should return a Many2One""" return None def _rating_domain(self): @@ -128,134 +118,6 @@ class RatingMixin(models.AbstractModel): """ return ['&', '&', ('res_model', '=', self._name), ('res_id', 'in', self.ids), ('consumed', '=', True)] - def _rating_get_partner(self): - """ Return the customer (partner) that performs the rating. - - :return record: res.partner singleton - """ - if hasattr(self, 'partner_id') and self.partner_id: - return self.partner_id - return self.env['res.partner'] - - def _rating_get_operator(self): - """ Return the operator (partner) that is the person who is rated. - - :return record: res.partner singleton - """ - if hasattr(self, 'user_id') and self.user_id.partner_id: - return self.user_id.partner_id - return self.env['res.partner'] - - def _rating_get_access_token(self, partner=None): - """ Return access token linked to existing ratings, or create a new rating - that will create the asked token. An explicit call to access rights is - performed as sudo is used afterwards as this method could be used from - different sources, notably templates. """ - self.check_access_rights('read') - self.check_access_rule('read') - if not partner: - partner = self._rating_get_partner() - rated_partner = self._rating_get_operator() - ratings = self.rating_ids.sudo().filtered(lambda x: x.partner_id.id == partner.id and not x.consumed) - if not ratings: - rating = self.env['rating.rating'].sudo().create({ - 'partner_id': partner.id, - 'rated_partner_id': rated_partner.id, - 'res_model_id': self.env['ir.model']._get_id(self._name), - 'res_id': self.id, - 'is_internal': False, - }) - else: - rating = ratings[0] - return rating.access_token - - def rating_send_request(self, template, lang=False, force_send=True): - """ This method send rating request by email, using a template given - in parameter. - - :param record template: a mail.template record used to compute the message body; - :param str lang: optional lang; it can also be specified directly on the template - itself in the lang field; - :param bool force_send: whether to send the request directly or use the mail - queue cron (preferred option); - """ - if lang: - template = template.with_context(lang=lang) - self.with_context(mail_notify_force_send=force_send).message_post_with_source( - template, - email_layout_xmlid='mail.mail_notification_light', - force_send=force_send, - subtype_xmlid='mail.mt_note', - ) - - def rating_apply(self, rate, token=None, rating=None, feedback=None, - subtype_xmlid=None, notify_delay_send=False): - """ Apply a rating to the record. This rating can either be linked to a - token (customer flow) or directly a rating record (code flow). - - If the current model inherits from mail.thread mixin a message is posted - on its chatter. User going through this method should have at least - employee rights as well as rights on the current record because of rating - manipulation and chatter post (either employee, either sudo-ed in public - controllers after security check granting access). - - :param float rate: the rating value to apply (from 0 to 5); - :param string token: access token to fetch the rating to apply (optional); - :param record rating: rating.rating to apply (if no token); - :param string feedback: additional feedback (plaintext); - :param string subtype_xmlid: xml id of a valid mail.message.subtype used - to post the message (if it applies). If not given a classic comment is - posted; - :param notify_delay_send: Delay the sending by 2 hours of the email so the user - can still change his feedback. If False, the email will be sent immediately. - - :returns rating: rating.rating record - """ - if rate < 0 or rate > 5: - raise ValueError('Wrong rating value. A rate should be between 0 and 5 (received %d).' % rate) - if token: - rating = self.env['rating.rating'].search([('access_token', '=', token)], limit=1) - if not rating: - raise ValueError('Invalid token or rating.') - - rating.write({'rating': rate, 'feedback': feedback, 'consumed': True}) - if issubclass(type(self), self.env.registry['mail.thread']): - if subtype_xmlid is None: - subtype_id = self._rating_apply_get_default_subtype_id() - else: - subtype_id = False - feedback = tools.plaintext2html(feedback or '') - - scheduled_datetime = ( - fields.Datetime.now() + datetime.timedelta(hours=2) - if notify_delay_send else None - ) - rating_body = ( - markupsafe.Markup( - ":%s/5%s" - ) % (rating.rating_image_url, rate, feedback) - ) - - if rating.message_id: - self._message_update_content( - rating.message_id, rating_body, - scheduled_date=scheduled_datetime, - strict=False - ) - else: - self.message_post( - author_id=rating.partner_id.id or None, # None will set the default author in mail_thread.py - body=rating_body, - rating_id=rating.id, - scheduled_date=scheduled_datetime, - subtype_id=subtype_id, - subtype_xmlid=subtype_xmlid, - ) - return rating - - def _rating_apply_get_default_subtype_id(self): - return self.env['ir.model.data']._xmlid_to_res_id("mail.mt_comment") - def _rating_get_repartition(self, add_stats=False, domain=None): """ get the repatition of rating grade for the given res_ids. :param add_stats : flag to add stat to the result diff --git a/addons/test_mail_full/models/test_mail_models_mail.py b/addons/test_mail_full/models/test_mail_models_mail.py index dba1e9a5137..12db1ac29c8 100644 --- a/addons/test_mail_full/models/test_mail_models_mail.py +++ b/addons/test_mail_full/models/test_mail_models_mail.py @@ -14,7 +14,7 @@ class MailTestPortal(models.Model): 'mail.thread', ] - name = fields.Char() + name = fields.Char('Name') partner_id = fields.Many2one('res.partner', 'Customer') user_id = fields.Many2one(comodel_name='res.users', string="Salesperson") @@ -42,26 +42,25 @@ class MailTestPortalNoPartner(models.Model): class MailTestRating(models.Model): - """ A model inheriting from mail.thread with some fields used for SMS + """ A model inheriting from rating.mixin (which inherits from mail.thread) with some fields used for SMS gateway, like a partner, a specific mobile phone, ... """ _description = 'Rating Model (ticket-like)' _name = 'mail.test.rating' _inherit = [ - 'mail.thread', - 'mail.activity.mixin', 'rating.mixin', + 'mail.activity.mixin', 'portal.mixin', ] _mailing_enabled = True _order = 'name asc, id asc' - name = fields.Char() - subject = fields.Char() + name = fields.Char('Name') + subject = fields.Char('Subject') company_id = fields.Many2one('res.company', 'Company') customer_id = fields.Many2one('res.partner', 'Customer') - email_from = fields.Char(compute='_compute_email_from', precompute=True, readonly=False, store=True) - mobile_nbr = fields.Char(compute='_compute_mobile_nbr', precompute=True, readonly=False, store=True) - phone_nbr = fields.Char(compute='_compute_phone_nbr', precompute=True, readonly=False, store=True) + email_from = fields.Char('From', compute='_compute_email_from', precompute=True, readonly=False, store=True) + mobile_nbr = fields.Char('Mobile', compute='_compute_mobile_nbr', precompute=True, readonly=False, store=True) + phone_nbr = fields.Char('Phone Number', compute='_compute_phone_nbr', precompute=True, readonly=False, store=True) user_id = fields.Many2one('res.users', 'Responsible', tracking=1) @api.depends('customer_id') @@ -96,3 +95,27 @@ class MailTestRating(models.Model): def _rating_get_partner(self): return self.customer_id + + +class MailTestRatingThread(models.Model): + """A model inheriting from mail.thread with minimal fields for testing + rating submission without the rating mixin but with the same test code: + + - 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' + _order = 'name asc, id asc' + + name = fields.Char('Name') + customer_id = fields.Many2one('res.partner', 'Customer') + user_id = fields.Many2one('res.users', 'Responsible', tracking=1) + + def _mail_get_partner_fields(self): + return ['customer_id'] + + def _rating_get_partner(self): + return self.customer_id or super()._rating_get_partner() diff --git a/addons/test_mail_full/security/ir.model.access.csv b/addons/test_mail_full/security/ir.model.access.csv index c16510115dd..89a38134b72 100644 --- a/addons/test_mail_full/security/ir.model.access.csv +++ b/addons/test_mail_full/security/ir.model.access.csv @@ -6,3 +6,6 @@ access_mail_test_portal_no_partner_user,mail.test.portal.no.partner.user,model_m access_mail_test_rating_all,mail.test.rating.all,model_mail_test_rating,,0,0,0,0 access_mail_test_rating_portal,mail.test.rating.portal,model_mail_test_rating,base.group_portal,1,0,0,0 access_mail_test_rating_user,mail.test.rating.user,model_mail_test_rating,base.group_user,1,1,1,1 +access_mail_test_rating_thread_all,mail.test.rating.thread.all,model_mail_test_rating_thread,,0,0,0,0 +access_mail_test_rating_thread_portal,mail.test.rating.thread.portal,model_mail_test_rating_thread,base.group_portal,1,0,0,0 +access_mail_test_rating_thread_user,mail.test.rating.thread.user,model_mail_test_rating_thread,base.group_user,1,1,1,1 diff --git a/addons/test_mail_full/tests/test_rating.py b/addons/test_mail_full/tests/test_rating.py index 546a1aa7ecf..d0a16a30ecb 100644 --- a/addons/test_mail_full/tests/test_rating.py +++ b/addons/test_mail_full/tests/test_rating.py @@ -20,88 +20,104 @@ class TestRatingCommon(TestMailFullCommon, TestSMSRecipients): 'name': 'Test Rating', 'user_id': cls.user_admin.id, }) + cls.record_rating_thread = cls.env['mail.test.rating.thread'].create({ + 'customer_id': cls.partner_1.id, + 'name': 'Test rating without rating mixin', + 'user_id': cls.user_admin.id, + }) @tagged('rating') class TestRatingFlow(TestRatingCommon): def test_initial_values(self): - record_rating = self.record_rating.with_env(self.env) - self.assertFalse(record_rating.rating_ids) - self.assertEqual(record_rating.message_partner_ids, self.partner_admin) - self.assertEqual(len(record_rating.message_ids), 1) + for record_rating in [self.record_rating, self.record_rating_thread]: + record_rating = record_rating.with_env(self.env) + self.assertFalse(record_rating.rating_ids) + self.assertEqual(record_rating.message_partner_ids, self.partner_admin) + self.assertEqual(len(record_rating.message_ids), 1) @users('employee') @mute_logger('odoo.addons.mail.models.mail_mail') def test_rating_prepare(self): - record_rating = self.record_rating.with_env(self.env) + for record_rating, desc in ((self.record_rating, 'With rating mixin'), + (self.record_rating_thread, 'Without rating mixin')): + with self.subTest(desc): + record_rating = record_rating.with_env(self.env) - # prepare rating token - access_token = record_rating._rating_get_access_token() + # prepare rating token + access_token = record_rating._rating_get_access_token() - # check rating creation - rating = record_rating.rating_ids - self.assertEqual(rating.access_token, access_token) - self.assertFalse(rating.consumed) - self.assertFalse(rating.is_internal) - self.assertEqual(rating.partner_id, self.partner_1) - self.assertEqual(rating.rated_partner_id, self.user_admin.partner_id) - self.assertFalse(rating.rating) + # check rating creation + rating = record_rating.rating_ids + self.assertEqual(rating.access_token, access_token) + self.assertFalse(rating.consumed) + self.assertFalse(rating.is_internal) + self.assertEqual(rating.partner_id, self.partner_1) + self.assertEqual(rating.rated_partner_id, self.user_admin.partner_id) + self.assertFalse(rating.rating) @users('employee') @mute_logger('odoo.addons.mail.models.mail_mail') def test_rating_rating_apply(self): - record_rating = self.record_rating.with_env(self.env) - record_messages = record_rating.message_ids + for record_rating, expected_subtype, is_rating_mixin_test in ( + (self.record_rating_thread, self.env.ref('mail.mt_comment'), False), + (self.record_rating, self.env.ref('test_mail_full.mt_mail_test_rating_rating_done'), True), + ): + with self.subTest('With rating mixin' if is_rating_mixin_test else 'Without rating mixin'): + record_rating = record_rating.with_env(self.env) + record_messages = record_rating.message_ids - # prepare rating token - access_token = record_rating._rating_get_access_token() + # prepare rating token + access_token = record_rating._rating_get_access_token() - # simulate an email click: notification should be delayed - with self.mock_mail_gateway(mail_unlink_sent=False), self.mock_mail_app(): - record_rating.rating_apply(5, token=access_token, feedback='Top Feedback', notify_delay_send=True) - message = record_rating.message_ids[0] - rating = record_rating.rating_ids + # simulate an email click: notification should be delayed + with self.mock_mail_gateway(mail_unlink_sent=False), self.mock_mail_app(): + record_rating.rating_apply(5, token=access_token, feedback='Top Feedback', notify_delay_send=True) + message = record_rating.message_ids[0] + rating = record_rating.rating_ids - # check posted message - self.assertEqual(record_rating.message_ids, record_messages + message) - self.assertIn('Top Feedback', message.body) - self.assertIn('/rating/static/src/img/rating_5.png', message.body) - self.assertEqual(message.author_id, self.partner_1) - self.assertEqual(message.rating_ids, rating) - self.assertFalse(message.notified_partner_ids) - self.assertEqual(message.subtype_id, self.env.ref('test_mail_full.mt_mail_test_rating_rating_done')) + # check posted message + self.assertEqual(record_rating.message_ids, record_messages + message) + self.assertIn('Top Feedback', message.body) + self.assertIn('/rating/static/src/img/rating_5.png', message.body) + self.assertEqual(message.author_id, self.partner_1) + self.assertEqual(message.rating_ids, rating) + self.assertFalse(message.notified_partner_ids) + self.assertEqual(message.subtype_id, expected_subtype) - # check rating update - self.assertTrue(rating.consumed) - self.assertEqual(rating.feedback, 'Top Feedback') - self.assertEqual(rating.message_id, message) - self.assertEqual(rating.rating, 5) - self.assertEqual(record_rating.rating_last_value, 5) + # check rating update + self.assertTrue(rating.consumed) + self.assertEqual(rating.feedback, 'Top Feedback') + self.assertEqual(rating.message_id, message) + self.assertEqual(rating.rating, 5) + if is_rating_mixin_test: + self.assertEqual(record_rating.rating_last_value, 5) - # give a feedback: send notifications (notify_delay_send set to False) - with self.mock_mail_gateway(mail_unlink_sent=False), self.mock_mail_app(): - record_rating.rating_apply(1, token=access_token, feedback='Bad Feedback') + # give a feedback: send notifications (notify_delay_send set to False) + with self.mock_mail_gateway(mail_unlink_sent=False), self.mock_mail_app(): + record_rating.rating_apply(1, token=access_token, feedback='Bad Feedback') - # check posted message: message is updated - update_message = record_rating.message_ids[0] - self.assertEqual(update_message, message, 'Should update first message') - self.assertEqual(record_rating.message_ids, record_messages + update_message) - self.assertIn('Bad Feedback', update_message.body) - self.assertIn('/rating/static/src/img/rating_1.png', update_message.body) - self.assertEqual(update_message.author_id, self.partner_1) - self.assertEqual(update_message.rating_ids, rating) - self.assertEqual(update_message.notified_partner_ids, self.partner_admin) - self.assertEqual(update_message.subtype_id, self.env.ref("test_mail_full.mt_mail_test_rating_rating_done")) + # check posted message: message is updated + update_message = record_rating.message_ids[0] + self.assertEqual(update_message, message, 'Should update first message') + self.assertEqual(record_rating.message_ids, record_messages + update_message) + self.assertIn('Bad Feedback', update_message.body) + self.assertIn('/rating/static/src/img/rating_1.png', update_message.body) + self.assertEqual(update_message.author_id, self.partner_1) + self.assertEqual(update_message.rating_ids, rating) + self.assertEqual(update_message.notified_partner_ids, self.partner_admin) + self.assertEqual(update_message.subtype_id, expected_subtype) - # check rating update - new_rating = record_rating.rating_ids - self.assertEqual(new_rating, rating, 'Should update first rating') - self.assertTrue(new_rating.consumed) - self.assertEqual(new_rating.feedback, 'Bad Feedback') - self.assertEqual(new_rating.message_id, update_message) - self.assertEqual(new_rating.rating, 1) - self.assertEqual(record_rating.rating_last_value, 1) + # check rating update + new_rating = record_rating.rating_ids + self.assertEqual(new_rating, rating, 'Should update first rating') + self.assertTrue(new_rating.consumed) + self.assertEqual(new_rating.feedback, 'Bad Feedback') + self.assertEqual(new_rating.message_id, update_message) + self.assertEqual(new_rating.rating, 1) + if is_rating_mixin_test: + self.assertEqual(record_rating.rating_last_value, 1) @tagged('rating') @@ -131,37 +147,55 @@ class TestRatingMixin(TestRatingCommon): @tagged('rating', 'mail_performance', 'post_install', '-at_install') class TestRatingPerformance(TestRatingCommon): + @classmethod + def setUpClass(cls): + super().setUpClass() + cls.RECORD_COUNT = 100 + cls.partners = cls.env['res.partner'].sudo().create([ + {'name': 'Jean-Luc %s' % (idx), 'email': 'jean-luc-%s@opoo.com' % (idx)} + for idx in range(cls.RECORD_COUNT)]) + + def apply_ratings(self, rate): + for record in self.record_ratings: + access_token = record._rating_get_access_token() + record.rating_apply(rate, token=access_token) + self.flush_tracking() + + def create_ratings(self, model): + self.record_ratings = self.env[model].create([{ + 'customer_id': self.partners[idx].id, + 'name': 'Test Rating', + 'user_id': self.user_admin.id, + } for idx in range(self.RECORD_COUNT)]) + self.flush_tracking() @users('employee') @warmup def test_rating_last_value_perfs(self): - RECORD_COUNT = 100 - partners = self.env['res.partner'].sudo().create([ - {'name': 'Jean-Luc %s' % (idx), 'email': 'jean-luc-%s@opoo.com' % (idx)} for idx in range(RECORD_COUNT)]) + with self.assertQueryCount(employee=1213): # tmf 1213 / com 1217 + self.create_ratings('mail.test.rating.thread') - with self.assertQueryCount(employee=1514): # tmf 1514 / com 1514 - record_ratings = self.env['mail.test.rating'].create([{ - 'customer_id': partners[idx].id, - 'name': 'Test Rating', - 'user_id': self.user_admin.id, - } for idx in range(RECORD_COUNT)]) - self.flush_tracking() + with self.assertQueryCount(employee=1801): # tmf 1801 + self.apply_ratings(1) + + with self.assertQueryCount(employee=1700): # tmf 1700 + self.apply_ratings(5) + + @users('employee') + @warmup + def test_rating_last_value_perfs_with_rating_mixin(self): + with self.assertQueryCount(employee=1312): # tmf 1312 / com 1312 + self.create_ratings('mail.test.rating') with self.assertQueryCount(employee=2004): # tmf 2004 - for record in record_ratings: - access_token = record._rating_get_access_token() - record.rating_apply(1, token=access_token) - self.flush_tracking() + self.apply_ratings(1) with self.assertQueryCount(employee=2003): # tmf 2003 - for record in record_ratings: - access_token = record._rating_get_access_token() - record.rating_apply(5, token=access_token) - self.flush_tracking() + self.apply_ratings(5) with self.assertQueryCount(employee=1): - record_ratings._compute_rating_last_value() - vals = [val == 5 for val in record_ratings.mapped('rating_last_value')] + self.record_ratings._compute_rating_last_value() + vals = (val == 5 for val in self.record_ratings.mapped('rating_last_value')) self.assertTrue(all(vals), "The last rating is kept.") @@ -169,10 +203,14 @@ class TestRatingPerformance(TestRatingCommon): class TestRatingRoutes(HttpCase, TestRatingCommon): def test_open_rating_route(self): - access_token = self.record_rating._rating_get_access_token() - self.url_open(f"/rate/{access_token}/5") + for record_rating, is_rating_mixin_test in ((self.record_rating_thread, False), + (self.record_rating, True)): + with self.subTest('With rating mixin' if is_rating_mixin_test else 'Without rating mixin'): + access_token = record_rating._rating_get_access_token() + self.url_open(f"/rate/{access_token}/5") - rating = self.record_rating.rating_ids - self.assertTrue(rating.consumed) - self.assertEqual(rating.rating, 5) - self.assertEqual(self.record_rating.rating_last_value, 5) + rating = record_rating.rating_ids + self.assertTrue(rating.consumed) + self.assertEqual(rating.rating, 5) + if is_rating_mixin_test: + self.assertEqual(record_rating.rating_last_value, 5) diff --git a/addons/website_sale/models/product_template.py b/addons/website_sale/models/product_template.py index 7d9946a28b0..88961ad0531 100644 --- a/addons/website_sale/models/product_template.py +++ b/addons/website_sale/models/product_template.py @@ -10,11 +10,11 @@ from odoo.osv import expression class ProductTemplate(models.Model): _inherit = [ + 'rating.mixin', "product.template", "website.seo.metadata", 'website.published.multi.mixin', 'website.searchable.mixin', - 'rating.mixin', ] _name = 'product.template' _mail_post_access = 'read' diff --git a/addons/website_slides/models/slide_channel.py b/addons/website_slides/models/slide_channel.py index 6530d7ef615..2e8e89fd9ba 100644 --- a/addons/website_slides/models/slide_channel.py +++ b/addons/website_slides/models/slide_channel.py @@ -187,7 +187,7 @@ class Channel(models.Model): _name = 'slide.channel' _description = 'Course' _inherit = [ - 'mail.thread', 'rating.mixin', + 'rating.mixin', 'mail.activity.mixin', 'image.mixin', 'website.cover_properties.mixin',