[IMP] website_rating : allow to comment message-based ratings

PURPOSE

Allow publishers to add a comment on frontend reviews with ratings. Purpose
is to allow to give some feedback or context on published reviews.

SPECIFICATIONS

In order to answer to a rating message (review) of a course (eLearning) or a
product (eCommerce) we add a way to the publisher to directly comment a review
inside the website rating chatter.

Any publisher can add/edit/remove comment of a rating message from the website.

The goal is not to enter into a discussion but just to give a feedback. This
comment is not sent or propagated to anyone, just displayed for information
purpose.

In portal chatter

* add 1 button below a review "Comment" (for publishers of the website only)
* only one comment possible by review/rating
* when the comment is added, display

* author name, date, comment
* 2 buttons, only visible by the website publisher: edit and delete

LINKS

Task ID : 2026911

closes odoo/odoo#35956

Signed-off-by: Thibault Delavallee (tde) <tde@openerp.com>
This commit is contained in:
ryv-odoo
2019-08-22 13:58:15 +00:00
committed by Thibault Delavallée
parent 796882b546
commit 1b48730446
11 changed files with 335 additions and 25 deletions
+1 -1
View File
@@ -21,7 +21,7 @@
<field name="name">rating.rating.form</field>
<field name="model">rating.rating</field>
<field name="arch" type="xml">
<form string="Rating" create="false" edit="false">
<form string="Rating" create="false">
<sheet>
<div class="oe_button_box" name="button_box">
<button class="oe_stat_button" type="object" name="action_open_rated_object" icon="fa-file">
+1
View File
@@ -11,6 +11,7 @@ Bridge module for rating on website stuff.
'depends': ['rating', 'website_mail'],
'data': [
'views/assets.xml',
'views/rating_views.xml',
'views/portal_templates.xml',
'views/rating_templates.xml',
],
@@ -2,3 +2,4 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from . import portal_chatter
from . import portal_rating
@@ -0,0 +1,17 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import http, _
from odoo.http import request
class PortalRating(http.Controller):
@http.route(['/website/rating/comment'], type='json', auth="user", method=['POST'], website=True)
def publish_rating_comment(self, rating_id, publisher_comment):
rating = request.env['rating.rating'].search([('id', '=', int(rating_id))])
if not rating:
return {'error': _('Invalid rating')}
rating.write({'publisher_comment': publisher_comment})
# return to the front-end the created/updated publisher comment
return rating.read(['publisher_comment', 'publisher_id', 'publisher_datetime'])[0]
+15 -1
View File
@@ -1,7 +1,7 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import api, fields, models
from odoo import models
class MailMessage(models.Model):
@@ -12,3 +12,17 @@ class MailMessage(models.Model):
if self._context.get('rating_include'):
field_list += ['rating_value']
return super(MailMessage, self)._portal_message_format(field_list)
def _message_read_dict_postprocess(self, message_values, message_tree):
""" 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)
if self._context.get('rating_include'):
infos = ["id", "publisher_comment", "publisher_id", "publisher_datetime", "message_id"]
related_rating = self.env['rating.rating'].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
+17 -2
View File
@@ -1,10 +1,25 @@
# -*- coding: utf-8 -*-
from odoo import fields, models
from odoo import fields, models, exceptions, _
class Rating(models.Model):
_inherit = 'rating.rating'
# Add this related field to mail.message for performance reason
website_published = fields.Boolean(related='message_id.website_published', store=True, readonly=False)
# Adding information for comment a rating message
publisher_comment = fields.Text("Publisher Comment")
publisher_id = fields.Many2one('res.partner', 'Commented by',
ondelete='set null', readonly=True)
publisher_datetime = fields.Datetime("Commented on", readonly=True)
def write(self, values):
if values.get('publisher_comment'):
if not self.env.user.has_group("website.group_website_publisher"):
raise exceptions.AccessError(_("Only the publisher of the website can change the rating comment"))
if not values.get('publisher_datetime'):
values['publisher_datetime'] = fields.Datetime.now()
if not values.get('publisher_id'):
values['publisher_id'] = self.env.user.partner_id.id
return super(Rating, self).write(values)
@@ -1,12 +1,15 @@
odoo.define('rating.portal.chatter', function (require) {
'use strict';
var core = require('web.core');
var portalChatter = require('portal.chatter');
var utils = require('web.utils');
var time = require('web.time');
var _t = core._t;
var PortalChatter = portalChatter.PortalChatter;
var STAR_RATING_RATIO = 2; // conversion factor from the star (1-5) to the db rating range (1-10)
var qweb = core.qweb;
var STAR_RATING_RATIO = 2; // conversion factor from the star (1-5) to the db rating range (1-10)
/**
* PortalChatter
@@ -15,8 +18,15 @@ var STAR_RATING_RATIO = 2; // conversion factor from the star (1-5) to the db r
*/
PortalChatter.include({
events: _.extend({}, PortalChatter.prototype.events, {
"click .o_website_rating_select": "_onClickStarDomain",
"click .o_website_rating_select_text": "_onClickStarDomainReset",
// star based control
'click .o_website_rating_select': '_onClickStarDomain',
'click .o_website_rating_select_text': '_onClickStarDomainReset',
// publisher comments
'click .o_wrating_js_publisher_comment_btn': '_onClickPublisherComment',
'click .o_wrating_js_publisher_comment_edit': '_onClickPublisherComment',
'click .o_wrating_js_publisher_comment_delete': '_onClickPublisherCommentDelete',
'click .o_wrating_js_publisher_comment_submit': '_onClickPublisherCommentSubmit',
'click .o_wrating_js_publisher_comment_cancel': '_onClickPublisherCommentCancel',
}),
xmlDependencies: (PortalChatter.prototype.xmlDependencies || [])
.concat([
@@ -56,10 +66,13 @@ PortalChatter.include({
var self = this;
messages = this._super.apply(this, arguments);
if (this.options['display_rating']) {
_.each(messages, function (m) {
m['rating_value'] = self.roundToHalf(m['rating_value'] / STAR_RATING_RATIO);
_.each(messages, function (m, i) {
m.rating_value = self.roundToHalf(m['rating_value'] / STAR_RATING_RATIO);
m.rating = self._preprocessCommentData(m.rating, i);
});
}
// save messages in the widget to process correctly the publisher comment templates
this.messages = messages;
return messages;
},
/**
@@ -126,16 +139,77 @@ PortalChatter.include({
return params;
},
/**
* Default rating data for publisher comment qweb template
* @private
* @param {Integer} messageIndex
*/
_newPublisherCommentData: function (messageIndex) {
return {
mes_index: messageIndex,
publisher_id: this.options.partner_id,
publisher_avatar: _.str.sprintf('/web/image/%s/%s/image_64/50x50', 'res.partner', this.options.partner_id),
publisher_name: _t("Write your comment"),
publisher_datetime: '',
publisher_comment: '',
};
},
/**
* preprocess the rating data comming from /website/rating/comment or the chatter_init
* Can be also use to have new rating data for a new publisher comment
* @param {JSON} rawRating
* @returns {JSON} the process rating data
*/
_preprocessCommentData: function (rawRating, messageIndex) {
var ratingData = {
id: rawRating.id,
mes_index: messageIndex,
publisher_datetime: rawRating.publisher_datetime ? moment(time.str_to_datetime(rawRating.publisher_datetime)).format('MMMM Do YYYY, h:mm:ss a') : "",
publisher_comment: rawRating.publisher_comment ? rawRating.publisher_comment : '',
};
// split array (id, display_name) of publisher_id into publisher_id and publisher_name
if (rawRating.publisher_id && rawRating.publisher_id.length >= 2) {
ratingData.publisher_id = rawRating.publisher_id[0];
ratingData.publisher_name = rawRating.publisher_id[1];
ratingData.publisher_avatar = _.str.sprintf('/web/image/%s/%s/image_64/50x50', 'res.partner', ratingData.publisher_id);
}
var commentData = _.extend(this._newPublisherCommentData(messageIndex), ratingData);
return commentData;
},
/** ---------------
* Selection of elements for the publisher comment feature
* Only available from a source in a publisher_comment or publisher_comment_form template
*/
_getCommentContainer: function ($source) {
return $source.parents(".o_wrating_publisher_container").first().find(".o_wrating_publisher_comment").first();
},
_getCommentButton: function ($source) {
return $source.parents(".o_wrating_publisher_container").first().find(".o_wrating_js_publisher_comment_btn").first();
},
_getCommentTextarea: function ($source) {
return $source.parents(".o_wrating_publisher_container").first().find(".o_portal_rating_comment_input").first();
},
_focusTextComment: function ($source) {
this._getCommentTextarea($source).focus();
},
//--------------------------------------------------------------------------
// Handlers
//--------------------------------------------------------------------------
/**
* @private
* @param {MouseEvent} event
* @param {MouseEvent} ev
*/
_onClickStarDomain: function (e) {
var $tr = this.$(e.currentTarget);
_onClickStarDomain: function (ev) {
var $tr = this.$(ev.currentTarget);
var num = $tr.data('star');
if ($tr.css('opacity') === '1') {
this.set('rating_value', num);
@@ -153,17 +227,120 @@ PortalChatter.include({
},
/**
* @private
* @param {MouseEvent} event
* @param {MouseEvent} ev
*/
_onClickStarDomainReset: function (e) {
e.stopPropagation();
e.preventDefault();
_onClickStarDomainReset: function (ev) {
ev.stopPropagation();
ev.preventDefault();
this.set('rating_value', false);
this.$('.o_website_rating_select_text').css('visibility', 'hidden');
this.$('.o_website_rating_select').css({
'opacity': 1,
});
},
/**
* @private
* @param {MouseEvent} ev
*/
_onClickPublisherComment: function (ev) {
var $source = this.$(ev.currentTarget);
// If the form is already present => like cancel remove the form
if (this._getCommentTextarea($source).length === 1) {
this._getCommentContainer($source).empty();
return;
}
var messageIndex = $source.data("mes_index");
var data = {is_publisher: this.options['is_user_publisher']};
data.rating = this._newPublisherCommentData(messageIndex);
var oldRating = this.messages[messageIndex].rating;
data.rating.publisher_comment = oldRating.publisher_comment ? oldRating.publisher_comment : '';
this._getCommentContainer($source).html($(qweb.render("website_rating.chatter_rating_publisher_form", data)));
this._focusTextComment($source);
},
/**
* @private
* @param {MouseEvent} ev
*/
_onClickPublisherCommentDelete: function (ev) {
var self = this;
var $source = this.$(ev.currentTarget);
var messageIndex = $source.data("mes_index");
var ratingId = this.messages[messageIndex].rating.id;
this._rpc({
route: '/website/rating/comment',
params: {
"rating_id": ratingId,
"publisher_comment": '' // Empty publisher comment means no comment
}
}).then(function (res) {
self.messages[messageIndex].rating = self._preprocessCommentData(res, messageIndex);
self._getCommentButton($source).removeClass("d-none");
self._getCommentContainer($source).empty();
});
},
/**
* @private
* @param {MouseEvent} ev
*/
_onClickPublisherCommentSubmit: function (ev) {
var self = this;
var $source = this.$(ev.currentTarget);
var messageIndex = $source.data("mes_index");
var comment = this._getCommentTextarea($source).val();
var ratingId = this.messages[messageIndex].rating.id;
this._rpc({
route: '/website/rating/comment',
params: {
"rating_id": ratingId,
"publisher_comment": comment
}
}).then(function (res) {
// Modify the related message
self.messages[messageIndex].rating = self._preprocessCommentData(res, messageIndex);
if (self.messages[messageIndex].rating.publisher_comment !== '') {
// Remove the button comment if exist and render the comment
self._getCommentButton($source).addClass('d-none');
self._getCommentContainer($source).html($(qweb.render("website_rating.chatter_rating_publisher_comment", {
rating: self.messages[messageIndex].rating,
is_publisher: self.options.is_user_publisher
})));
} else {
// Empty string or false considers as no comment
self._getCommentButton($source).removeClass("d-none");
self._getCommentContainer($source).empty();
}
});
},
/**
* @private
* @param {MouseEvent} ev
*/
_onClickPublisherCommentCancel: function (ev) {
var $source = this.$(ev.currentTarget);
var messageIndex = $source.data("mes_index");
var comment = this.messages[messageIndex].rating.publisher_comment;
if (comment) {
var data = {
rating: this.messages[messageIndex].rating,
is_publisher: this.options.is_user_publisher
};
this._getCommentContainer($source).html($(qweb.render("website_rating.chatter_rating_publisher_comment", data)));
} else {
this._getCommentContainer($source).empty();
}
},
/**
* @private
*/
@@ -24,6 +24,15 @@
</t>
</t>
</t>
<t t-jquery=".o_portal_chatter_attachments" t-operation="after">
<!--Only possible if a rating is link to the message, for now we can't comment if no rating
is link to the message (because publisher comment data
is on the rating.rating model - one comment max) -->
<t t-if="message.rating and message.rating.id" t-call="website_rating.chatter_rating_publisher">
<t t-set="is_publisher" t-value="widget.options['is_user_publisher']"/>
<t t-set="rating" t-value="message.rating"/>
</t>
</t>
</t>
<t t-extend="portal.Chatter">
@@ -36,4 +45,72 @@
</t>
</t>
</t>
<!--
New templates specific of rating in Chatter
-->
<t t-name="website_rating.chatter_rating_publisher">
<div class="o_wrating_publisher_container">
<button t-if="is_publisher"
t-attf-class="btn px-2 btn-sm border o_wrating_js_publisher_comment_btn {{ rating.publisher_comment !== '' ? 'd-none' : '' }}"
t-att-data-mes_index="rating.mes_index">
<i class="fa fa-comment text-muted mr-1"/>Comment
</button>
<div class="o_wrating_publisher_comment mt-2 mb-2">
<t t-if="rating.publisher_comment" t-call="website_rating.chatter_rating_publisher_comment"/>
</div>
</div>
</t>
<t t-name="website_rating.chatter_rating_publisher_comment">
<div class="media o_portal_chatter_message">
<img class="o_portal_chatter_avatar" t-att-src="rating.publisher_avatar" alt="avatar"/>
<div class="media-body">
<div class="o_portal_chatter_message_title">
<div class="d-inline-block">
<h5><t t-esc="rating.publisher_name"/></h5>
</div>
<div t-if="is_publisher" class="dropdown d-inline-block">
<button class="btn py-0" type="button" id="dropdownMenuButton" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
<i class="fa fa-ellipsis-v"/>
</button>
<div class="dropdown-menu" aria-labelledby="dropdownMenuButton">
<button class="dropdown-item o_wrating_js_publisher_comment_edit" t-att-data-mes_index="rating.mes_index">
<i class="fa fa-fw fa-pencil mr-1"/>Edit
</button>
<button class="dropdown-item o_wrating_js_publisher_comment_delete" t-att-data-mes_index="rating.mes_index">
<i class="fa fa-fw fa-trash-o mr-1"/>Delete
</button>
</div>
</div>
<p>Published on <t t-esc="rating.publisher_datetime"/></p>
</div>
<t t-raw="rating.publisher_comment"/>
</div>
</div>
</t>
<t t-name="website_rating.chatter_rating_publisher_form">
<div t-if="is_publisher" class="media o_portal_chatter_message shadow bg-white rounded px-3 py-3 my-1">
<img class="o_portal_chatter_avatar" t-att-src="rating.publisher_avatar" alt="avatar"/>
<div class="media-body">
<div class="o_portal_chatter_message_title">
<h5><t t-esc="rating.publisher_name"/></h5>
<p>Published on <t t-esc="rating.publisher_datetime"/></p>
</div>
<textarea rows="3" class="form-control o_portal_rating_comment_input"><t t-esc="rating.publisher_comment"/></textarea>
<div>
<button class="btn btn-primary mt-2 o_wrating_js_publisher_comment_submit" t-att-data-mes_index="rating.mes_index">
<t t-if="rating.publisher_comment === ''">
Post comment
</t><t t-else="">
Update comment
</t>
</button>
<button class="border btn btn-light mt-2 bg-white o_wrating_js_publisher_comment_cancel" t-att-data-mes_index="rating.mes_index">
Cancel
</button>
</div>
</div>
</div>
</t>
</templates>
@@ -1,12 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<template id="message_thread" inherit_id="portal.message_thread">
<xpath expr="//div[@id='discussion']" position="attributes">
<attribute name="t-att-data-display_rating">display_rating or False</attribute>
</xpath>
</template>
<!--
Display static star widget, and open rating composer on click
This template provide the container of the Popup Rating Composer. The rest is done in js.
@@ -34,5 +27,4 @@
t-att-data-disable_composer="disable_composer">
</div>
</template>
</odoo>
@@ -0,0 +1,13 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="rating_rating_view_form" model="ir.ui.view">
<field name="name">rating.rating.view.form</field>
<field name="model">rating.rating</field>
<field name="inherit_id" ref="rating.rating_rating_view_form"/>
<field name="arch" type="xml">
<xpath expr="//field[@name='feedback']" position="after">
<field name="publisher_comment" string="Publisher comment"/>
</xpath>
</field>
</record>
</odoo>
@@ -191,6 +191,9 @@
<field name="rating">6</field>
<field name="feedback">Interesting. Could be great to include more examples.</field>
<field name="message_id" ref="website_slides.message_channel_1_portal"/>
<field name="publisher_comment">Thanks for the feedback! We just updated first lessons to better include newcomers.</field>
<field name="publisher_id" ref="base.partner_admin"/>
<field name="publisher_datetime" eval="(DateTime.today() - relativedelta(days=1)).strftime('%Y-%m-%d %H:%M')"/>
</record>
<function model="slide.channel" name="message_subscribe"