From a960eb9ff599fcbdea1371488beb6ebff4f35d87 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9rome=20Maes?= Date: Wed, 8 Jul 2015 16:39:00 +0200 Subject: [PATCH] [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. --- addons/rating/models/rating.py | 66 +++++++-- .../rating/static/src/css/rating_common.css | 48 +++++++ addons/rating/static/src/js/rating_common.js | 127 ++++++++++++++++++ .../rating/static/src/xml/rating_common.xml | 21 +++ addons/rating/views/rating_template.xml | 89 +++++++++++- 5 files changed, 338 insertions(+), 13 deletions(-) create mode 100644 addons/rating/static/src/css/rating_common.css create mode 100644 addons/rating/static/src/js/rating_common.js create mode 100644 addons/rating/static/src/xml/rating_common.xml diff --git a/addons/rating/models/rating.py b/addons/rating/models/rating.py index 44d446b4b0c..7a21a2ea77d 100644 --- a/addons/rating/models/rating.py +++ b/addons/rating/models/rating.py @@ -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 diff --git a/addons/rating/static/src/css/rating_common.css b/addons/rating/static/src/css/rating_common.css new file mode 100644 index 00000000000..ff84353a8eb --- /dev/null +++ b/addons/rating/static/src/css/rating_common.css @@ -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; +} \ No newline at end of file diff --git a/addons/rating/static/src/js/rating_common.js b/addons/rating/static/src/js/rating_common.js new file mode 100644 index 00000000000..67994e72695 --- /dev/null +++ b/addons/rating/static/src/js/rating_common.js @@ -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, + }; +}); \ No newline at end of file diff --git a/addons/rating/static/src/xml/rating_common.xml b/addons/rating/static/src/xml/rating_common.xml new file mode 100644 index 00000000000..fb703785ae7 --- /dev/null +++ b/addons/rating/static/src/xml/rating_common.xml @@ -0,0 +1,21 @@ + + + +
+
+ + + + + +
+
+ +
+ +
+
+
diff --git a/addons/rating/views/rating_template.xml b/addons/rating/views/rating_template.xml index 7ff1c3b80e2..8f84562265e 100644 --- a/addons/rating/views/rating_template.xml +++ b/addons/rating/views/rating_template.xml @@ -1,6 +1,15 @@ + + + + - + + + + + + + +