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',