diff options
| author | Jérome Maes <jem@openerp.com> | 2015-07-08 16:39:00 +0200 |
|---|---|---|
| committer | Jérome Maes <jem@openerp.com> | 2015-08-27 09:36:35 +0200 |
| commit | a960eb9ff599fcbdea1371488beb6ebff4f35d87 (patch) | |
| tree | a7c54c8d90b9642e8665af7f818b2ab686c78683 /addons | |
| parent | 97cc5b4dc5dbb1ed6b98594d1fab631d8079cc4e (diff) | |
[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.
Diffstat (limited to 'addons')
| -rw-r--r-- | addons/rating/models/rating.py | 66 | ||||
| -rw-r--r-- | addons/rating/static/src/css/rating_common.css | 48 | ||||
| -rw-r--r-- | addons/rating/static/src/js/rating_common.js | 127 | ||||
| -rw-r--r-- | addons/rating/static/src/xml/rating_common.xml | 21 | ||||
| -rw-r--r-- | addons/rating/views/rating_template.xml | 89 |
5 files changed, 338 insertions, 13 deletions
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 @@ + +<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> 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 @@ <?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> |
