[IMP] portal,website_rating: new popup composer to submit rating

Rating document available on a website will become more common, but
we want to do it with different UI widget. This commit introduces a
new "popup rating composer": the idea is the rating average is
displayed with stars, and when clicking on it, a popup with the
composer appears. The user can so submit its review.

To do so, we factorize the Portal Composer into a dedicated widget
(instead of natively inside Portal Chatter).

Task-1902304
This commit is contained in:
jem-odoo
2019-02-06 09:55:13 +00:00
parent 8c840f7539
commit 4cb228d823
7 changed files with 326 additions and 126 deletions
+63 -23
View File
@@ -12,6 +12,43 @@ var time = require('web.time');
var qweb = core.qweb;
var _t = core._t;
/**
* Widget PortalComposer
*
* Display the composer (according to access right)
*
*/
var PortalComposer = Widget.extend({
template: 'portal.Composer',
events: {
'click .o_portal_chatter_composer_btn': '_onSubmitButtonClick',
},
init: function(parent, options){
this._super.apply(this, arguments);
this.options = _.defaults(options || {}, {
'allow_composer': true,
'display_composer': false,
'csrf_token': odoo.csrf_token,
'token': false,
'res_model': false,
'res_id': false,
});
// TODO simplify this using the 'async' keyword in the events
// property definition as soon as this widget is converted in
// frontend widget.
this._onSubmitButtonClick = dom.makeButtonHandler(this._onSubmitButtonClick);
},
/**
* @private
*/
_onSubmitButtonClick: function () {
return $.Deferred();
},
});
/**
* Widget PortalChatter
*
@@ -20,15 +57,23 @@ var _t = core._t;
* - Provider API to filter displayed messages
*/
var PortalChatter = Widget.extend({
template: 'portal.chatter',
template: 'portal.Chatter',
events: {
"click .o_portal_chatter_pager_btn": '_onClickPager',
'click .o_portal_chatter_composer_btn': '_onSubmitButtonClick',
},
xmlDependencies: ['/portal/static/src/xml/portal_chatter.xml'],
init: function(parent, options){
var self = this;
this.options = {};
this._super.apply(this, arguments);
this.options = _.defaults(options || {}, {
// underscorize the camelcased option keys
_.each(options, function(val, key) {
self.options[_.str.underscored(key)] = val;
});
// set default options
this.options = _.defaults(this.options, {
'allow_composer': true,
'display_composer': false,
'csrf_token': odoo.csrf_token,
@@ -45,20 +90,16 @@ var PortalChatter = Widget.extend({
this.set('pager', {});
this.set('domain', this.options['domain']);
this._current_page = this.options['pager_start'];
// TODO simplify this using the 'async' keyword in the events
// property definition as soon as this widget is converted in
// frontend widget.
this._onSubmitButtonClick = dom.makeButtonHandler(this._onSubmitButtonClick);
},
willStart: function(){
var self = this;
// load qweb template and init data
return $.when(
this._super.apply(this, arguments),
rpc.query({
route: '/mail/chatter_init',
params: this._messageFetchPrepareParams()
}), this._loadTemplates()
}),
).then(function(result){
self.result = result;
self.options = _.extend(self.options, self.result['options'] || {});
@@ -80,7 +121,18 @@ var PortalChatter = Widget.extend({
// set options and parameters
this.set('message_count', this.options['message_count']);
this.set('messages', this.preprocessMessages(this.result['messages']));
return this._super.apply(this, arguments);
var defs = [];
defs.push(this._super.apply(this, arguments));
// instanciate and insert composer widget
if (this.options['display_composer']) {
this._composer = new PortalComposer(this, this.options);
defs.push(this._composer.replace(this.$('.o_portal_chatter_composer')));
}
return $.when.apply($, defs);
},
//--------------------------------------------------------------------------
@@ -134,13 +186,6 @@ var PortalChatter = Widget.extend({
var d = domain ? domain : _.clone(this.get('domain'));
this.set('domain', d); // trigger fetch message
},
/**
* @private
* @returns {Deferred}
*/
_loadTemplates: function(){
return ajax.loadXML('/portal/static/src/xml/portal_chatter.xml', qweb);
},
_messageFetchPrepareParams: function(){
var self = this;
var data = {
@@ -234,12 +279,6 @@ var PortalChatter = Widget.extend({
var page = $(ev.currentTarget).data('page');
this._changeCurrentPage(page);
},
/**
* @private
*/
_onSubmitButtonClick: function () {
return $.Deferred();
},
});
base.ready().then(function () {
@@ -251,6 +290,7 @@ base.ready().then(function () {
});
return {
PortalComposer: PortalComposer,
PortalChatter: PortalChatter,
};
@@ -15,7 +15,12 @@
</div>
</t>
<t t-name="portal.chatter_composer">
<!--
Widget PortalComposer (standalone)
required many options: token, res_model, res_id, ...
-->
<t t-name="portal.Composer">
<div class="o_portal_chatter_composer" t-if="widget.options['allow_composer']">
<t t-if="!widget.options['display_composer']">
<h4>Leave a comment</h4>
@@ -45,13 +50,16 @@
</div>
</div>
</t>
<hr/>
</div>
</t>
<!--
Widget PortalChatter, and subtemplates
-->
<t t-name="portal.chatter_messages">
<div class="o_portal_chatter_messages">
<t t-foreach="widget.get('messages')" t-as="message">
<t t-foreach="widget.get('messages') || []" t-as="message">
<div class="media o_portal_chatter_message" t-att-id="'message-' + message.id">
<img class="o_portal_chatter_avatar" t-att-src="message.author_avatar_url" alt="avatar"/>
<div class="media-body">
@@ -96,13 +104,14 @@
</div>
</t>
<t t-name="portal.chatter">
<t t-name="portal.Chatter">
<div class="o_portal_chatter p-0">
<div class="o_portal_chatter_header">
<t t-call="portal.chatter_message_count"/>
</div>
<hr t-if="widget.options['allow_composer']"/>
<div class="o_portal_chatter_composer"/>
<hr/>
<t t-call="portal.chatter_composer"/>
<t t-call="portal.chatter_messages"/>
<div class="o_portal_chatter_footer">
<t t-call="portal.pager"/>
@@ -1,27 +1,122 @@
odoo.define('website_rating.thread', function(require) {
'use strict';
var base = require('web_editor.base');
var ajax = require('web.ajax');
var core = require('web.core');
var Widget = require('web.Widget');
var rootWidget = require('web_editor.root_widget');
var session = require('web.session');
var qweb = core.qweb;
var _t = core._t;
var PortalComposer = require('portal.chatter').PortalComposer;
var PortalChatter = require('portal.chatter').PortalChatter;
/**
* Extends Frontend Chatter to handle rating
* PortalComposer
*
* Extends Frontend Composer to handle rating submission
*/
PortalChatter.include({
events: _.extend({}, PortalChatter.prototype.events, {
PortalComposer.include({
events: _.extend({}, PortalComposer.prototype.events, {
"mousemove .stars i" : "_onMoveStar",
"mouseleave .stars i" : "_onMoveOutStar",
"click .stars" : "_onClickStar",
"mouseleave .stars" : "_onMouseleaveStar",
}),
init: function(parent, options){
this._super.apply(this, arguments);
// star input widget
this.labels = {
'0': "",
'1': _t("I hate it"),
'2': _t("I don't like it"),
'3': _t("It's okay"),
'4': _t("I like it"),
'5': _t("I love it"),
};
this.user_click = false; // user has click or not
this.set("star_value", this.options.rating_default_value);
this.on("change:star_value", this, this._onChangeStarValue);
},
start: function(){
var self = this;
return this._super.apply(this, arguments).then(function(){
// rating stars
self.$input = self.$('input[name="rating_value"]');
self.$star_list = self.$('.stars').find('i');
self.set("star_value", self.options.rating_default_value); // set the default value to trigger the display of star widget
});
},
//--------------------------------------------------------------------------
// Handlers
//--------------------------------------------------------------------------
/**
* @private
* @param {MouseEvent} event
*/
_onClickStar: function(e){
this.user_click = true;
this.$input.val(this.get("star_value"));
},
/**
* @private
*/
_onChangeStarValue: function(){
var val = this.get("star_value");
var index = Math.floor(val);
var decimal = val - index;
// reset the stars
this.$star_list.removeClass('fa-star fa-star-half-o').addClass('fa-star-o');
this.$('.stars').find("i:lt("+index+")").removeClass('fa-star-o fa-star-half-o').addClass('fa-star');
if(decimal){
this.$('.stars').find("i:eq("+(index)+")").removeClass('fa-star-o fa-star fa-star-half-o').addClass('fa-star-half-o');
}
this.$('.rate_text .label').text(this.labels[index]);
},
_onMouseleaveStar: function(e){
this.$('.rate_text').hide();
},
/**
* @private
* @param {MouseEvent} event
*/
_onMoveStar: function(e){
var index = this.$('.stars i').index(e.currentTarget);
this.$('.rate_text').show();
this.set("star_value", index+1);
},
/**
* @private
*/
_onMoveOutStar: function(){
if(!this.user_click){
this.set("star_value", parseInt(this.$input.val()));
}
this.user_click = false;
},
});
/**
* PortalChatter
*
* Extends Frontend Chatter to handle rating
*/
PortalChatter.include({
events: _.extend({}, PortalChatter.prototype.events, {
"click .o_website_rating_select": "_onClickStarDomain",
"click .o_website_rating_select_text": "_onClickStarDomainReset",
}),
xmlDependencies: (PortalChatter.prototype.xmlDependencies || []).concat(['/website_rating/static/src/xml/website_mail.xml', '/website_rating/static/src/xml/portal_chatter.xml']),
init: function(parent, options){
this._super.apply(this, arguments);
@@ -36,18 +131,6 @@ odoo.define('website_rating.thread', function(require) {
this.set('rating_card_values', {});
this.set('rating_value', false);
this.on("change:rating_value", this, this._onChangeRatingDomain);
// rating star
this.labels = {
'0': "",
'1': _t("I hate it"),
'2': _t("I don't like it"),
'3': _t("It's okay"),
'4': _t("I like it"),
'5': _t("I love it"),
};
this.user_click = false; // user has click or not
this.set("star_value", this.options.rating_default_value);
this.on("change:star_value", this, this._onChangeStarValue);
},
willStart: function(){
var self = this;
@@ -70,15 +153,6 @@ odoo.define('website_rating.thread', function(require) {
}
});
},
start: function(){
var self = this;
return this._super.apply(this, arguments).then(function(){
// rating stars
self.$input = self.$('input[name="rating_value"]');
self.$star_list = self.$('.stars').find('i');
self.set("star_value", self.options.rating_default_value); // set the default value to trigger the display of star widget
});
},
//--------------------------------------------------------------------------
// Public
@@ -129,9 +203,6 @@ odoo.define('website_rating.thread', function(require) {
// Private
//--------------------------------------------------------------------------
_loadTemplates: function(){
return $.when(this._super(), ajax.loadXML('/website_rating/static/src/xml/website_mail.xml', qweb));
},
_messageFetchPrepareParams: function(){
var params = this._super.apply(this, arguments);
if(this.options['display_rating']){
@@ -144,14 +215,6 @@ odoo.define('website_rating.thread', function(require) {
// Handlers
//--------------------------------------------------------------------------
/**
* @private
* @param {MouseEvent} event
*/
_onClickStar: function(e){
this.user_click = true;
this.$input.val(this.get("star_value"));
},
/**
* @private
* @param {MouseEvent} event
@@ -179,6 +242,7 @@ odoo.define('website_rating.thread', function(require) {
*/
_onClickStarDomainReset: function(e){
e.stopPropagation();
e.preventDefault();
this.set('rating_value', false);
this.$('.o_website_rating_select_text').css('visibility', 'hidden');
this.$('.o_website_rating_select').css({
@@ -195,43 +259,62 @@ odoo.define('website_rating.thread', function(require) {
}
this._changeCurrentPage(1, domain);
},
/**
* @private
*/
_onChangeStarValue: function(){
var val = this.get("star_value");
var index = Math.floor(val);
var decimal = val - index;
// reset the stars
this.$star_list.removeClass('fa-star fa-star-half-o').addClass('fa-star-o');
});
this.$('.stars').find("i:lt("+index+")").removeClass('fa-star-o fa-star-half-o').addClass('fa-star');
if(decimal){
this.$('.stars').find("i:eq("+(index)+")").removeClass('fa-star-o fa-star fa-star-half-o').addClass('fa-star-half-o');
}
this.$('.rate_text .label').text(this.labels[index]);
},
_onMouseleaveStar: function(e){
this.$('.rate_text').hide();
/**
* RatingPopupComposer
*
* Display the rating average with a static star widget, and open
* a popup with the portal composer when clicking on it.
**/
var RatingPopupComposer = Widget.extend({
template: 'website_rating.PopupComposer',
xmlDependencies: [
'/portal/static/src/xml/portal_chatter.xml',
'/website_rating/static/src/xml/website_mail.xml',
'/website_rating/static/src/xml/portal_chatter.xml',
],
init: function(parent, options){
this._super.apply(this, arguments);
this.rating_avg = Math.round(options['ratingAvg'] * 100) / 100 || 0.0;
this.rating_total = options['ratingTotal'] || 0.0;
this.options = _.defaults({}, options, {
'token': false,
'res_model': false,
'res_id': false,
'pid': 0,
'display_composer': !session.is_website_user,
'display_rating': true,
'csrf_token': odoo.csrf_token,
'user_id': session.user_id,
});
},
/**
* @private
* @param {MouseEvent} event
* @override
*/
_onMoveStar: function(e){
var index = this.$('.stars i').index(e.currentTarget);
this.$('.rate_text').show();
this.set("star_value", index+1);
},
/**
* @private
*/
_onMoveOutStar: function(){
if(!this.user_click){
this.set("star_value", parseInt(this.$input.val()));
}
this.user_click = false;
start: function () {
var defs = [];
defs.push(this._super.apply(this, arguments));
// instanciate and insert composer widget
this._composer = new PortalComposer(this, this.options);
defs.push(this._composer.replace(this.$('.o_portal_chatter_composer')));
return $.when.apply($, defs);
},
});
base.ready().then(function () {
$('.o_rating_popup_composer').each(function (index) {
var $elem = $(this);
var rating_popup = new RatingPopupComposer(rootWidget, $elem.data());
rating_popup.appendTo($elem);
});
});
});
@@ -68,3 +68,15 @@ $o-w-rating-star-color: #FACC2E;
display: inline-block;
}
}
/* Rating Popup Composer */
.o_rating_popup_composer {
.o_rating_clickable {
cursor: pointer;
}
.o_portal_chatter_avatar {
margin-right: 10px;
}
}
@@ -0,0 +1,71 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates id="template" xml:space="preserve">
<!--
Inherited templates from portal to custom chatter with rating
-->
<t t-extend="portal.Composer">
<t t-jquery=".o_portal_chatter_composer_form input[name='csrf_token']" t-operation="after">
<t t-call="website_rating.rating_star_input"/>
</t>
</t>
<t t-extend="portal.chatter_messages">
<t t-jquery="t[t-raw='message.body']" t-operation="before">
<t t-if="message['rating_value']">
<t t-call="website_rating.rating_stars_static">
<t t-set="val" t-value="message.rating_value"/>
</t>
</t>
</t>
</t>
<t t-extend="portal.Chatter">
<t t-jquery="t[t-call='portal.chatter_message_count']" t-operation="replace">
<t t-if="widget.options['display_rating']">
<t t-call="website_rating.rating_card"/>
</t>
<t t-if="!widget.options['display_rating']">
<t t-call="portal.chatter_message_count"/>
</t>
</t>
</t>
<!--
Popup Rating Composer Widget
-->
<t t-name="website_rating.PopupComposer">
<div>
<button t-if="widget.options['display_composer']" type="button" class="btn btn-primary" data-toggle="modal" data-target="#ratingpopupcomposer">
<t t-if="widget.options['display_composer']">Add a review</t>
</button>
<div>
<t t-call="website_rating.rating_stars_static">
<t t-set="val" t-value="widget.rating_avg || 0"/>
<t t-set="inline_mode" t-value="true"/>
</t>
<t t-if="widget.rating_total">
<t t-esc="widget.rating_avg"/> (<t t-esc="widget.rating_total"/>)
</t>
</div>
</div>
<div t-if="widget.options['display_composer']" class="modal fade" id="ratingpopupcomposer" tabindex="-1" role="dialog" aria-labelledby="ratingpopupcomposerlabel" aria-hidden="true">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="ratingpopupcomposerlabel">Add a review</h5>
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span>&amp;times;</span>
</button>
</div>
<div class="modal-body">
<div class="o_portal_chatter_composer"/>
</div>
</div>
</div>
</div>
</t>
</templates>
@@ -5,7 +5,7 @@
<t t-set="val_integer" t-value="Math.floor(val)"/>
<t t-set="val_decimal" t-value="val - val_integer"/>
<t t-set="empty_star" t-value="5 - (val_integer+Math.ceil(val_decimal))"/>
<div class="o_website_rating_static">
<div class="o_website_rating_static" t-att-style="inline_mode ? 'display:inline' : ''">
<t t-foreach="_.range(0, val_integer)" t-as="num">
<i class="fa fa-star" role="img" aria-label="One star" title="One star"></i>
</t>
@@ -75,34 +75,4 @@
</div>
</t>
<!--
Inherited templates from portal to custom chatter with rating
-->
<t t-extend="portal.chatter_composer">
<t t-jquery=".o_portal_chatter_composer_form input[name='csrf_token']" t-operation="after">
<t t-call="website_rating.rating_star_input"/>
</t>
</t>
<t t-extend="portal.chatter_messages">
<t t-jquery="t[t-raw='message.body']" t-operation="before">
<t t-if="message['rating_value']">
<t t-call="website_rating.rating_stars_static">
<t t-set="val" t-value="message.rating_value"/>
</t>
</t>
</t>
</t>
<t t-extend="portal.chatter">
<t t-jquery="t[t-call='portal.chatter_message_count']" t-operation="replace">
<t t-if="widget.options['display_rating']">
<t t-call="website_rating.rating_card"/>
</t>
<t t-if="!widget.options['display_rating']">
<t t-call="portal.chatter_message_count"/>
</t>
</t>
</t>
</templates>
@@ -38,4 +38,19 @@
</div>
</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.
To use this template, you need to call it after setting the following variable in your template or in your controller:
:float rating_avg : average rating to be displayed with star widget
:object browserecord : the mail_thread object
:token string (optional): if you want your chatter to be available for non-logged user,
you can use a token to verify the identity of the user;
the message will be posted with the identity of the partner_id of the object
-->
<template id="rating_stars_static_popup_composer" name="Rating: rating composer in popup">
<div class="row d-print-none o_rating_popup_composer o_not_editable p-0" t-att-data-rating-avg="rating_avg or 0.0" t-att-data-rating-total="rating_total or 0.0" t-att-data-token="token" t-att-data-hash="hash" t-att-data-pid="pid" t-att-data-res_model="object._name" t-att-data-res_id="object.id" t-att-data-partner_id="request.env.user.partner_id.id">
</div>
</template>
</odoo>