[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:
committed by
Thibault Delavallée
parent
796882b546
commit
1b48730446
@@ -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">
|
||||
|
||||
@@ -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]
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user