[IMP] rating : add generic star widget
- star widget and rating card to post and display rating - Linked rating.rating to mail.message - method to extract more statistics about the rating of an object.
This commit is contained in:
@@ -31,6 +31,8 @@ class Rating(models.Model):
|
||||
feedback = fields.Text('Feedback reason', help="Reason of the rating")
|
||||
access_token = fields.Char(string='Security Token', default=new_access_token, help="Access token to set the rating of the value")
|
||||
|
||||
message_id = fields.Many2one('mail.message', string="Linked message", help="Associated message when posting a review. Mainly used in website addons.", index=True)
|
||||
|
||||
@api.model
|
||||
def apply_rating(self, rate, res_model=None, res_id=None, token=None):
|
||||
""" apply a rating for given res_model/res_id or token. If the res_model is a mail.thread
|
||||
@@ -100,10 +102,10 @@ class RatingMixin(models.AbstractModel):
|
||||
for record in self:
|
||||
res_id = record.id
|
||||
values = {
|
||||
'res_model' : res_model,
|
||||
'res_id' : res_id,
|
||||
'partner_id' : partner_id.id,
|
||||
'rated_partner_id' : rated_partner_id.id
|
||||
'res_model': res_model,
|
||||
'res_id': res_id,
|
||||
'partner_id': partner_id.id,
|
||||
'rated_partner_id': rated_partner_id.id
|
||||
}
|
||||
if reuse_rating:
|
||||
# search the existing rating for the given res_model/res_id
|
||||
@@ -118,25 +120,46 @@ class RatingMixin(models.AbstractModel):
|
||||
template.send_mail(rating.id, force_send=True)
|
||||
|
||||
@api.multi
|
||||
def rating_get_stats(self):
|
||||
def rating_get_repartition(self, add_stats=False, domain=None):
|
||||
""" get the repatition of rating grade for the given res_ids.
|
||||
:return dictionnary where the key is the rating value (the note), and the value, the number of object (res_model, res_id) having the value
|
||||
:param add_stats : flag to add stat to the result
|
||||
:type add_stats : boolean
|
||||
:param domain : optional extra domain of the rating to include/exclude in repartition
|
||||
:return dictionnary
|
||||
if not add_stats, the dict is like
|
||||
- key is the rating value (integer)
|
||||
- value is the number of object (res_model, res_id) having the value
|
||||
otherwise, key is the value of the information (string) : either stat name (avg, total, ...) or 'repartition'
|
||||
containing the same dict if add_stats was False.
|
||||
"""
|
||||
data = self.env['rating.rating'].read_group([('res_model', '=', self._name), ('res_id', 'in', self.ids), ('rating', '>=', 0)], ['rating'], ['rating', 'res_id'])
|
||||
base_domain = [('res_model', '=', self._name), ('res_id', 'in', self.ids), ('rating', '>=', 0)]
|
||||
if domain:
|
||||
base_domain += domain
|
||||
data = self.env['rating.rating'].read_group(base_domain, ['rating'], ['rating', 'res_id'])
|
||||
# init dict with all posible rate value, except -1 (no value for the rating)
|
||||
res = dict.fromkeys(range(11), 0)
|
||||
res.update((d['rating'], d['rating_count']) for d in data)
|
||||
return res
|
||||
values = dict.fromkeys(range(11), 0)
|
||||
values.update((d['rating'], d['rating_count']) for d in data)
|
||||
# add other stats
|
||||
if add_stats:
|
||||
rating_number = sum(values.values())
|
||||
result = {
|
||||
'repartition': values,
|
||||
'avg': sum([float(key*values[key]) for key in values])/rating_number if rating_number > 0 else 0,
|
||||
'total': reduce(lambda x, y: y['rating_count']+x, data, 0),
|
||||
}
|
||||
return result
|
||||
return values
|
||||
|
||||
@api.multi
|
||||
def rating_get_grades(self):
|
||||
def rating_get_grades(self, domain=None):
|
||||
""" get the repatition of rating grade for the given res_ids.
|
||||
:param domain : optional domain of the rating to include/exclude in grades computation
|
||||
:return dictionnary where the key is the grade (great, okay, bad), and the value, the number of object (res_model, res_id) having the grade
|
||||
the grade are compute as 0-30% : Bad
|
||||
31-69%: Okay
|
||||
70-100%: Great
|
||||
"""
|
||||
data = self.rating_get_stats()
|
||||
data = self.rating_get_repartition(domain=domain)
|
||||
res = dict.fromkeys(['great', 'okay', 'bad'], 0)
|
||||
for key in data:
|
||||
if key >= 7:
|
||||
@@ -146,3 +169,22 @@ class RatingMixin(models.AbstractModel):
|
||||
else:
|
||||
res['bad'] += data[key]
|
||||
return res
|
||||
|
||||
@api.multi
|
||||
def rating_get_stats(self, domain=None):
|
||||
""" get the statistics of the rating repatition
|
||||
:param domain : optional domain of the rating to include/exclude in statistic computation
|
||||
:return dictionnary where
|
||||
- key is the the name of the information (stat name)
|
||||
- value is statistic value : 'percent' contains the repartition in percentage, 'avg' is the average rate
|
||||
and 'total' is the number of rating
|
||||
"""
|
||||
data = self.rating_get_repartition(domain=domain, add_stats=True)
|
||||
result = {
|
||||
'avg': data['avg'],
|
||||
'total': data['total'],
|
||||
'percent': dict.fromkeys(range(11), 0),
|
||||
}
|
||||
for rate in data['repartition']:
|
||||
result['percent'][rate] = (data['repartition'][rate] * 100) / data['total'] if data['total'] > 0 else 0
|
||||
return result
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
/* Star Widget */
|
||||
.o_rating_star_card{
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.o_rating_star_card .stars {
|
||||
display: inline-block;
|
||||
color: #FACC2E;
|
||||
margin-right: 15px;
|
||||
}
|
||||
|
||||
.o_rating_star_card .stars i {
|
||||
margin-right: -3px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.o_rating_star_card .stars.enabled{
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.o_rating_star_card .rate_text{
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
|
||||
/* Rating Card */
|
||||
.o_rating_card .progress {
|
||||
position: relative;
|
||||
height: 25px;
|
||||
margin-bottom: 3px;
|
||||
}
|
||||
.o_rating_card .progress-bar {
|
||||
background-color: #FACC2E;
|
||||
}
|
||||
.o_rating_card .progress > .progress_type {
|
||||
position: absolute;
|
||||
left: 0px;
|
||||
font-weight: 800;
|
||||
padding: 3px 10px 2px 10px;
|
||||
color: rgb(120, 100, 100);
|
||||
width: 100%;
|
||||
}
|
||||
.o_rating_card .progress > .progress_completed {
|
||||
position: absolute;
|
||||
right: 0px;
|
||||
font-weight: 800;
|
||||
padding: 3px 10px 2px;
|
||||
}
|
||||
@@ -0,0 +1,127 @@
|
||||
odoo.define('rating.rating', function (require) {
|
||||
"use strict";
|
||||
|
||||
var Widget = require('web.Widget');
|
||||
var translation = require('web.translation');
|
||||
var _t = translation._t;
|
||||
|
||||
// Star Widget
|
||||
var RatingStarWidget = Widget.extend({
|
||||
template: 'rating.rating_star_card',
|
||||
events: {
|
||||
"mousemove .stars i" : "moveOnStars",
|
||||
"mouseleave .stars i" : "moveOut",
|
||||
"click .stars" : "clickOnStar",
|
||||
"mouseleave .stars" : "moveOutStars",
|
||||
},
|
||||
init: function(parent, options){
|
||||
this._super.apply(this, arguments);
|
||||
this.options = _.defaults(options || {}, {
|
||||
'rating_default_value': 0,
|
||||
'rating_disabled': 0,
|
||||
});
|
||||
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", 0);
|
||||
this.on("change:star_value", this, this.changeStars);
|
||||
},
|
||||
start: function(){
|
||||
this.$input = this.$('input');
|
||||
this.star_list = this.$('.stars').find('i');
|
||||
// set the default value
|
||||
this.set("star_value", this.options.rating_default_value);
|
||||
this.is_editable = !this.options.rating_disabled;
|
||||
},
|
||||
attachTo: function(el){
|
||||
this._super.apply(this, arguments);
|
||||
// set the default value and bind event
|
||||
var default_value = this.$('input').data('default');
|
||||
default_value = this.roundToHalf(default_value);
|
||||
this.set('star_value', default_value);
|
||||
// is_editable value from DOM
|
||||
this.is_editable = !this.$input.data('is_disabled');
|
||||
},
|
||||
changeStars: 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.$input.val(val);
|
||||
this.$('.rate_text .label').text(this.labels[index]);
|
||||
},
|
||||
moveOut: function(){
|
||||
if(!this.user_click && this.is_editable){
|
||||
this.set("star_value", 0);
|
||||
}
|
||||
this.user_click = false;
|
||||
},
|
||||
moveOnStars: function(e){
|
||||
if(this.is_editable){
|
||||
this.$('.rate_text').show();
|
||||
var index = this.$('.stars i').index(e.currentTarget);
|
||||
this.set("star_value", index+1);
|
||||
}
|
||||
},
|
||||
clickOnStar: function(e){
|
||||
if(this.is_editable){
|
||||
this.user_click = true;
|
||||
}
|
||||
},
|
||||
moveOutStars: function(e){
|
||||
this.$('.rate_text').hide();
|
||||
},
|
||||
roundToHalf: function(value) {
|
||||
var converted = parseFloat(value); // Make sure we have a number
|
||||
var decimal = (converted - parseInt(converted, 10));
|
||||
decimal = Math.round(decimal * 10);
|
||||
if(decimal == 5){
|
||||
return (parseInt(converted, 10)+0.5);
|
||||
}
|
||||
if((decimal < 3) || (decimal > 7)){
|
||||
return Math.round(converted);
|
||||
}else{
|
||||
return (parseInt(converted, 10)+0.5);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
var page_widgets = {};
|
||||
|
||||
$(document).ready(function(){
|
||||
// Rating Card
|
||||
$('[data-toggle="rating-popover"]').popover({
|
||||
html : true,
|
||||
trigger: 'hover',
|
||||
title: function() {
|
||||
return $(this).parent().find('.rating_popover').find('.popover-title').html();
|
||||
},
|
||||
content: function() {
|
||||
return $(this).parent().find('.rating_popover').find('.popover-content').html();
|
||||
}
|
||||
});
|
||||
// Rating Star Widget instances
|
||||
$('.o_rating_star_card').each(function(index, elem){
|
||||
page_widgets[index] = new RatingStarWidget();
|
||||
page_widgets[index].attachTo(elem);
|
||||
})
|
||||
});
|
||||
|
||||
return {
|
||||
RatingStarWidget: RatingStarWidget,
|
||||
rating_star_widgets: page_widgets,
|
||||
};
|
||||
});
|
||||
@@ -0,0 +1,21 @@
|
||||
|
||||
<templates id="template" xml:space="preserve">
|
||||
<t t-name="rating.rating_star_card">
|
||||
<div class="o_rating_star_card">
|
||||
<div t-att-class="'stars ' + (widget.options.rating_disabled ? 'disabled' : 'enabled')">
|
||||
<i class="fa fa-star-o"></i>
|
||||
<i class="fa fa-star-o"></i>
|
||||
<i class="fa fa-star-o"></i>
|
||||
<i class="fa fa-star-o"></i>
|
||||
<i class="fa fa-star-o"></i>
|
||||
</div>
|
||||
<div t-if="!widget.options.rating_disabled" class="rate_text">
|
||||
<span class="label label-info"></span>
|
||||
</div>
|
||||
<input type="hidden" readonly="readonly"
|
||||
name="rating"
|
||||
t-att-data-default="widget.options.rating_default_value || 0.0"
|
||||
t-att-data-is_disabled="widget.options.rating_disabled ? 1 : 0"/>
|
||||
</div>
|
||||
</t>
|
||||
</templates>
|
||||
@@ -1,6 +1,15 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<openerp>
|
||||
<data>
|
||||
|
||||
<!-- Bundle for Rating Widget (in assets common to be used in website modules) -->
|
||||
<template id="assets_common" inherit_id="web.assets_common" name="Rating common assets">
|
||||
<xpath expr='.' position="inside">
|
||||
<script src="/rating/static/src/js/rating_common.js"></script>
|
||||
<link href="/rating/static/src/css/rating_common.css" rel="stylesheet"/>
|
||||
</xpath>
|
||||
</template>
|
||||
|
||||
<!-- backend assets -->
|
||||
<template id="assets_backend" name="rating assets" inherit_id="web.assets_backend">
|
||||
<xpath expr="." position="inside">
|
||||
@@ -8,7 +17,84 @@
|
||||
</xpath>
|
||||
</template>
|
||||
|
||||
<!-- Thanks page -->
|
||||
<!-- Rating Star Widget (Generic widget)
|
||||
:param String rating_input_name : the name of the input field to send the rating
|
||||
:param Float rating_default_value : the rate to initialize the widget with
|
||||
:param Boolean rating_disabled : is the widget disabled (False), or it is just used to display information (True). Default value is False
|
||||
-->
|
||||
<template id="rating_star_card" name="Rating Star Widget">
|
||||
<div class="o_rating_star_card">
|
||||
<div t-attf-class="stars #{rating_disabled and 'disabled' or 'enabled'}">
|
||||
<i class="fa fa-star-o"></i>
|
||||
<i class="fa fa-star-o"></i>
|
||||
<i class="fa fa-star-o"></i>
|
||||
<i class="fa fa-star-o"></i>
|
||||
<i class="fa fa-star-o"></i>
|
||||
</div>
|
||||
<div t-if="not rating_disabled" class="rate_text">
|
||||
<span class="label label-info"></span>
|
||||
</div>
|
||||
<input type="hidden" readonly="readonly"
|
||||
t-att-name="rating_input_name or 'rating'"
|
||||
t-att-data-default="rating_default_value or 0.0"
|
||||
t-att-data-is_disabled="rating_disabled and 1 or 0"/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- Rating card : display the result (avg, total, ...) for rating
|
||||
:param dict rating_stat : a dictionnary with
|
||||
'avg' : containing the rating note average
|
||||
'total' : the number of rating for the object
|
||||
'percent' : a dict where the key is each possible rate, and the value is
|
||||
the percent of rating having this rate.
|
||||
:param float max_rate : the upper bound of the rate to display (in the popover)
|
||||
:param float min_rate : the lower bound of the rate to display (in the popover)
|
||||
-->
|
||||
<template id="rating_card" name="Rating Card">
|
||||
<div class="o_rating_card">
|
||||
<div class="text-center well well-sm">
|
||||
<!-- popover 'button' -->
|
||||
<div class="rating"
|
||||
data-placement="top"
|
||||
data-toggle="rating-popover">
|
||||
<t t-call="rating.rating_star_card">
|
||||
<t t-set="rating_default_value" t-value="'%.2f' % (rating_stat.get('avg', 0.0),)"/>
|
||||
<t t-set="rating_disabled" t-value="True"/>
|
||||
</t>
|
||||
<i class="fa fa-user"> <t t-esc="rating_stat.get('total', 0)"/></i>
|
||||
</div>
|
||||
<!-- popover core -->
|
||||
<div class="popover rating_popover" role="tooltip">
|
||||
<div class="arrow"></div>
|
||||
<div class="popover-content">
|
||||
<t t-foreach="rating_stat.get('percent')" t-as="percent">
|
||||
<t t-if="min_rate <= percent and percent <= max_rate">
|
||||
<div class="progress" style="width:200px;">
|
||||
<div class="progress-bar" role="progressbar"
|
||||
aria-valuemin="0"
|
||||
aria-valuemax="100"
|
||||
t-att-aria-valuenow="rating_stat['percent'][percent]"
|
||||
t-attf-style="width: #{rating_stat['percent'][percent]}%">
|
||||
<span class="sr-only">
|
||||
<t t-esc="rating_stat['percent'][percent]"/>%
|
||||
</span>
|
||||
</div>
|
||||
<span class="progress_type">
|
||||
<t t-esc="percent"/> <i class="fa fa-star"/>
|
||||
</span>
|
||||
<span class="progress_completed">
|
||||
<t t-esc="rating_stat['percent'][percent]"/>%
|
||||
</span>
|
||||
</div>
|
||||
</t>
|
||||
</t>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- External page : thanks message -->
|
||||
<template id="rating_external_page_view">
|
||||
<head>
|
||||
<link rel='stylesheet' href='/web/static/lib/bootstrap/css/bootstrap.css'/>
|
||||
@@ -24,5 +110,6 @@
|
||||
<h1>Sorry you have already voted !</h1>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
</data>
|
||||
</openerp>
|
||||
|
||||
Reference in New Issue
Block a user