diff --git a/addons/im_livechat/data/im_livechat_channel_demo.xml b/addons/im_livechat/data/im_livechat_channel_demo.xml
index 360a4646a42..6027cf3e7f9 100644
--- a/addons/im_livechat/data/im_livechat_channel_demo.xml
+++ b/addons/im_livechat/data/im_livechat_channel_demo.xml
@@ -25,13 +25,15 @@
Visitor #234, Mitchell Admin
+ LIVECHAT_1
-
+
mail.channel
@@ -81,13 +83,15 @@
Visitor #323, Marc Demo
+ LIVECHAT_2
-
+
mail.channel
@@ -137,13 +141,16 @@
Joel Willis, Mitchell Admin
+ LIVECHAT_3
-
+
+
mail.channel
@@ -185,13 +192,16 @@
Joel Willis, Marc Demo
+ LIVECHAT_4
-
+
+
mail.channel
@@ -225,13 +235,15 @@
Visitor #532, Mitchell Admin
+ LIVECHAT_5
-
+
mail.channel
@@ -273,13 +285,15 @@
Visitor #649, Mitchell Admin
+ LIVECHAT_6
-
+
mail.channel
@@ -321,13 +335,15 @@
Joel Willis, Mitchell Admin
+ LIVECHAT_7
-
+
mail.channel
@@ -369,13 +385,15 @@
Visitor #722, Marc Demo
+ LIVECHAT_8
-
+
mail.channel
@@ -470,14 +488,15 @@
mail.channel
+ LIVECHAT_9
-
-
+
diff --git a/addons/mail/models/mail_thread.py b/addons/mail/models/mail_thread.py
index a28122f38cc..b59ea4a8d7e 100644
--- a/addons/mail/models/mail_thread.py
+++ b/addons/mail/models/mail_thread.py
@@ -2930,9 +2930,6 @@ class MailThread(models.AbstractModel):
# DISCORDUSS API
# ------------------------------------------------------
- def _message_update_content_after_hook(self, message):
- """ Hook to add custom behavior after having updated the message content. """
-
def _message_add_reaction_after_hook(self, message, content):
""" Hook to add custom behavior after having added a reaction to a message. """
@@ -2940,7 +2937,7 @@ class MailThread(models.AbstractModel):
""" Hook to add custom behavior after having removed a reaction from a message. """
# ------------------------------------------------------
- # WRAPPERS AND TOOLS
+ # THREAD MESSAGE UPDATE
# ------------------------------------------------------
def message_change_thread(self, new_thread, new_parent_message=False):
@@ -2983,6 +2980,9 @@ class MailThread(models.AbstractModel):
msg_not_comment.write(msg_vals)
return True
+ def _message_update_content_after_hook(self, message):
+ """ Hook to add custom behavior after having updated the message content. """
+
# ------------------------------------------------------
# CONTROLLERS
# ------------------------------------------------------
diff --git a/addons/project/data/project_demo.xml b/addons/project/data/project_demo.xml
index f4b1d689012..79ed148921a 100644
--- a/addons/project/data/project_demo.xml
+++ b/addons/project/data/project_demo.xml
@@ -1227,36 +1227,44 @@ Send it ASAP, its urgent.
+ PROJECT_1
-
+
+ PROJECT_2
-
+
+ PROJECT_3
-
+
+ PROJECT_4
-
+
diff --git a/addons/project/models/project.py b/addons/project/models/project.py
index c6a2abbdd64..6d80e90a3f9 100644
--- a/addons/project/models/project.py
+++ b/addons/project/models/project.py
@@ -9,6 +9,7 @@ from datetime import timedelta, datetime, time
from random import randint
from odoo import api, Command, fields, models, tools, SUPERUSER_ID, _, _lt
+from odoo.addons.rating.models.rating_data import RATING_LIMIT_OK
from odoo.exceptions import UserError, ValidationError, AccessError
from odoo.osv.expression import OR, AND
from odoo.tools.misc import get_lang
@@ -2278,8 +2279,15 @@ class Task(models.Model):
return self.project_id.partner_id
return res
- def rating_apply(self, rate, token=None, feedback=None, subtype_xmlid=None):
- return super(Task, self).rating_apply(rate, token=token, feedback=feedback, subtype_xmlid="project.mt_task_rating")
+ def rating_apply(self, rate, token=None, rating=None, feedback=None, subtype_xmlid=None):
+ rating = super(Task, self).rating_apply(rate, token=token, rating=rating, feedback=feedback, subtype_xmlid=subtype_xmlid)
+ if self.stage_id and self.stage_id.auto_validation_kanban_state:
+ kanban_state = 'done' if rating.rating >= RATING_LIMIT_OK else 'blocked'
+ self.write({'kanban_state': kanban_state})
+ return rating
+
+ def _rating_apply_get_default_subtype_id(self):
+ return self.env['ir.model.data']._xmlid_to_res_id("project.mt_task_rating")
def _rating_get_parent_field_name(self):
return 'project_id'
diff --git a/addons/rating/__manifest__.py b/addons/rating/__manifest__.py
index c2588e06b8d..6752803e432 100644
--- a/addons/rating/__manifest__.py
+++ b/addons/rating/__manifest__.py
@@ -3,7 +3,7 @@
{
'name': 'Customer Rating',
- 'version': '1.0',
+ 'version': '1.1',
'category': 'Productivity',
'description': """
This module allows a customer to give rating.
diff --git a/addons/rating/controllers/main.py b/addons/rating/controllers/main.py
index 7f075f7b0d4..e3b3bca5e24 100644
--- a/addons/rating/controllers/main.py
+++ b/addons/rating/controllers/main.py
@@ -2,6 +2,7 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
import logging
+import werkzeug
from odoo import http
from odoo.http import request
@@ -20,31 +21,51 @@ class Rating(http.Controller):
@http.route('/rate//', type='http', auth="public", website=True)
def action_open_rating(self, token, rate, **kwargs):
- assert rate in (1, 3, 5), "Incorrect rating"
+ if rate not in (1, 3, 5):
+ raise ValueError(_("Incorrect rating: should be 1, 3 or 5 (received %d)"), rate)
+
rating = request.env['rating.rating'].sudo().search([('access_token', '=', token)])
if not rating:
- return request.not_found()
- rate_names = {
- 5: _("Satisfied"),
- 3: _("Okay"),
- 1: _("Dissatisfied")
- }
+ return werkzeug.exceptions.Forbidden()
+
+ record_sudo = request.env[rating.res_model].sudo().browse(rating.res_id)
+ if not record_sudo.exists():
+ return werkzeug.exceptions.Forbidden()
+
rating.write({'rating': rate, 'consumed': True})
+
lang = rating.partner_id.lang or get_lang(request.env).code
return request.env['ir.ui.view'].with_context(lang=lang)._render_template('rating.rating_external_page_submit', {
- 'rating': rating, 'token': token,
- 'rate_names': rate_names, 'rate': rate
+ 'rating': rating,
+ 'token': token,
+ 'rate_names': {
+ 5: _("Satisfied"),
+ 3: _("Okay"),
+ 1: _("Dissatisfied"),
+ },
+ 'rate': rate,
})
@http.route(['/rate//submit_feedback'], type="http", auth="public", methods=['post'], website=True)
- def action_submit_rating(self, token, **kwargs):
- rate = int(kwargs.get('rate'))
- assert rate in (1, 3, 5), "Incorrect rating"
+ def action_submit_rating(self, token, rate, **kwargs):
+ rate = int(rate)
+ if rate not in (1, 3, 5):
+ raise ValueError(_("Incorrect rating: should be 1, 3 or 5 (received %d)"), rate)
+
rating = request.env['rating.rating'].sudo().search([('access_token', '=', token)])
if not rating:
- return request.not_found()
+ return werkzeug.exceptions.Forbidden()
+
record_sudo = request.env[rating.res_model].sudo().browse(rating.res_id)
- record_sudo.rating_apply(rate, token=token, feedback=kwargs.get('feedback'))
+ if not record_sudo.exists():
+ return werkzeug.exceptions.Forbidden()
+
+ record_sudo.rating_apply(rate,
+ rating=rating,
+ feedback=kwargs.get('feedback'),
+ subtype_xmlid=None, # force default subtype choice
+ )
+
lang = rating.partner_id.lang or get_lang(request.env).code
return request.env['ir.ui.view'].with_context(lang=lang)._render_template('rating.rating_external_page_view', {
'web_base_url': rating.get_base_url(),
diff --git a/addons/rating/models/mail_thread.py b/addons/rating/models/mail_thread.py
index 8725bc81c44..eacfc11931e 100644
--- a/addons/rating/models/mail_thread.py
+++ b/addons/rating/models/mail_thread.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 api, models, _
class MailThread(models.AbstractModel):
@@ -9,6 +9,7 @@ class MailThread(models.AbstractModel):
@api.returns('mail.message', lambda value: value.id)
def message_post(self, **kwargs):
+ rating_id = kwargs.pop('rating_id', False)
rating_value = kwargs.pop('rating_value', False)
rating_feedback = kwargs.pop('rating_feedback', False)
message = super(MailThread, self).message_post(**kwargs)
@@ -25,4 +26,17 @@ class MailThread(models.AbstractModel):
'consumed': True,
'partner_id': self.env.user.partner_id.id,
})
+ elif rating_id:
+ self.env['rating.rating'].browse(rating_id).write({'message_id': message.id})
+
return message
+
+ 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)
diff --git a/addons/rating/models/rating.py b/addons/rating/models/rating.py
index 5cae372d02e..dcab6718b2a 100644
--- a/addons/rating/models/rating.py
+++ b/addons/rating/models/rating.py
@@ -42,6 +42,7 @@ class Rating(models.Model):
partner_id = fields.Many2one('res.partner', string='Customer', help="Author of the rating")
rating = fields.Float(string="Rating Value", group_operator="avg", default=0, help="Rating value: 0=Unhappy, 5=Happy")
rating_image = fields.Binary('Image', compute='_compute_rating_image')
+ rating_image_url = fields.Char('Image URL', compute='_compute_rating_image')
rating_text = fields.Selection(rating_data.RATING_TEXT, string='Rating', store=True, compute='_compute_rating_text', readonly=True)
feedback = fields.Text('Comment', help="Reason of the rating")
message_id = fields.Many2one(
@@ -91,13 +92,17 @@ class Rating(models.Model):
self.ensure_one()
return 'rating_%s.png' % rating_data._rating_to_threshold(self.rating)
+ @api.depends('rating')
def _compute_rating_image(self):
+ self.rating_image_url = False
+ self.rating_image = False
for rating in self:
try:
image_path = get_resource_path('rating', 'static/src/img', rating._get_rating_image_filename())
+ rating.rating_image_url = '/rating/static/src/img/%s' % rating._get_rating_image_filename()
rating.rating_image = base64.b64encode(open(image_path, 'rb').read()) if image_path else False
except (IOError, OSError):
- rating.rating_image = False
+ pass
@api.depends('rating')
def _compute_rating_text(self):
diff --git a/addons/rating/models/rating_mixin.py b/addons/rating/models/rating_mixin.py
index ecc661f376b..7001a59592a 100644
--- a/addons/rating/models/rating_mixin.py
+++ b/addons/rating/models/rating_mixin.py
@@ -173,42 +173,52 @@ class RatingMixin(models.AbstractModel):
subtype_id=subtype_id
)
- def rating_apply(self, rate, token=None, feedback=None, subtype_xmlid=None):
- """ Apply a rating given a token. 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 because of rating
- manipulation (either employee, either sudo-ed in public controllers after
- security check granting access).
+ def rating_apply(self, rate, token=None, rating=None, feedback=None, subtype_xmlid=None):
+ """ Apply a rating to the record. This rating can either be linked to a
+ token (customer flow) or directly a rating record (code flow).
- :param float rate : the rating value to apply
- :param string token : access token
- :param string feedback : additional feedback
- :param string subtype_xmlid : xml id of a valid mail.message.subtype
+ 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).
- :returns rating.rating record
+ :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;
+
+ :returns rating: rating.rating record
"""
- rating = None
+ 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)
- else:
- rating = self.env['rating.rating'].search([('res_model', '=', self._name), ('res_id', '=', self.ids[0])], limit=1)
- if rating:
- rating.write({'rating': rate, 'feedback': feedback, 'consumed': True})
- if hasattr(self, 'message_post'):
- feedback = tools.plaintext2html(feedback or '')
- self.message_post(
- body="
%s"
- % (rate, rate, feedback),
- subtype_xmlid=subtype_xmlid or "mail.mt_comment",
- author_id=rating.partner_id and rating.partner_id.id or None # None will set the default author in mail_thread.py
- )
- if hasattr(self, 'stage_id') and self.stage_id and hasattr(self.stage_id, 'auto_validation_kanban_state') and self.stage_id.auto_validation_kanban_state:
- if rating.rating > 2:
- self.write({'kanban_state': 'done'})
- else:
- self.write({'kanban_state': 'blocked'})
+ if not rating:
+ raise ValueError('Invalid token or rating.')
+
+ rating.write({'rating': rate, 'feedback': feedback, 'consumed': True})
+ if hasattr(self, 'message_post'):
+ if subtype_xmlid is None:
+ subtype_id = self._rating_apply_get_default_subtype_id()
+ else:
+ subtype_id = self.env['ir.model.data']._xmlid_to_res_id(subtype_xmlid)
+ feedback = tools.plaintext2html(feedback or '')
+ self.message_post(
+ author_id=rating.partner_id.id or None, # None will set the default author in mail_thread.py
+ body="
%s"
+ % (rating.rating_image_url, rate, feedback),
+ rating_id=rating.id,
+ subtype_id=subtype_id,
+ )
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/sale_timesheet/data/sale_service_demo.xml b/addons/sale_timesheet/data/sale_service_demo.xml
index 6590e55aaf0..66532b53668 100644
--- a/addons/sale_timesheet/data/sale_service_demo.xml
+++ b/addons/sale_timesheet/data/sale_service_demo.xml
@@ -1045,21 +1045,24 @@
+ TS_0
-
+
+ TS_1
-
-
+
diff --git a/addons/test_mail_full/__manifest__.py b/addons/test_mail_full/__manifest__.py
index 0fdf6b0212d..c0c97caefd4 100644
--- a/addons/test_mail_full/__manifest__.py
+++ b/addons/test_mail_full/__manifest__.py
@@ -24,6 +24,7 @@ real applications. """,
'sms',
],
'data': [
+ 'data/mail_message_subtype_data.xml',
'security/ir.model.access.csv',
'security/ir_rule_data.xml',
],
diff --git a/addons/test_mail_full/data/mail_message_subtype_data.xml b/addons/test_mail_full/data/mail_message_subtype_data.xml
new file mode 100644
index 00000000000..ad8c172a6d2
--- /dev/null
+++ b/addons/test_mail_full/data/mail_message_subtype_data.xml
@@ -0,0 +1,10 @@
+
+
+
+ Rating Done
+ Rating Done
+ mail.test.rating
+
+
+
+
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 80bdc88dad7..3c254a0c25d 100644
--- a/addons/test_mail_full/models/test_mail_models_mail.py
+++ b/addons/test_mail_full/models/test_mail_models_mail.py
@@ -51,5 +51,8 @@ class MailTestRating(models.Model):
elif not rating.phone_nbr:
rating.phone_nbr = False
+ def _rating_apply_get_default_subtype_id(self):
+ return self.env['ir.model.data']._xmlid_to_res_id("test_mail_full.mt_mail_test_rating_rating_done")
+
def rating_get_partner_id(self):
return self.customer_id
diff --git a/addons/test_mail_full/tests/test_rating.py b/addons/test_mail_full/tests/test_rating.py
index a0e867e73e2..097ac9ff9b3 100644
--- a/addons/test_mail_full/tests/test_rating.py
+++ b/addons/test_mail_full/tests/test_rating.py
@@ -48,27 +48,51 @@ class TestRatingFlow(TestMailFullCommon, TestMailFullRecipients):
@mute_logger('odoo.addons.mail.models.mail_mail')
def test_rating_rating_apply(self):
record_rating = self.env['mail.test.rating'].browse(self.record_rating.ids)
+ record_messages = record_rating.message_ids
# prepare rating token
access_token = record_rating.rating_get_access_token()
record_rating.invalidate_cache(fnames=['rating_ids'])
- # apply a rate
+ # apply a rate as note (first click)
with self.mock_mail_gateway(mail_unlink_sent=False), self.mock_mail_app():
- record_rating.rating_apply(5, token=access_token, feedback='Top Feedback')
+ record_rating.rating_apply(5, token=access_token, feedback='Top Feedback', subtype_xmlid='mail.mt_note')
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.assertIn('/rating/static/src/img/rating_5.png', message.body)
self.assertEqual(message.author_id, self.partner_1)
- self.assertFalse(message.rating_ids)
- self.assertEqual(message.notified_partner_ids, self.partner_admin)
- self.assertEqual(message.subtype_id, self.env.ref('mail.mt_comment'))
+ self.assertEqual(message.rating_ids, rating)
+ self.assertFalse(message.notified_partner_ids)
+ self.assertEqual(message.subtype_id, self.env.ref('mail.mt_note'))
# check rating update
self.assertTrue(rating.consumed)
self.assertEqual(rating.feedback, 'Top Feedback')
- self.assertFalse(rating.message_id)
+ self.assertEqual(rating.message_id, message)
self.assertEqual(rating.rating, 5)
+
+ # apply a rate again (second click with feedback)
+ 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: a new message is posted with default subtype
+ update_message = record_rating.message_ids[0]
+ self.assertNotEqual(update_message, message)
+ self.assertEqual(record_rating.message_ids, record_messages + message + 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 rating update
+ rating = record_rating.rating_ids
+ self.assertTrue(rating.consumed)
+ self.assertEqual(rating.feedback, 'Bad Feedback')
+ self.assertEqual(rating.message_id, update_message)
+ self.assertEqual(rating.rating, 1)