diff options
| author | fda-odoo <fda@odoo.com> | 2017-12-06 09:57:34 +0100 |
|---|---|---|
| committer | fda-odoo <fda@odoo.com> | 2017-12-06 16:02:29 +0100 |
| commit | 411afe54bed23e783070c7c5d5390d612887a629 (patch) | |
| tree | 0edd21d0f030b2b1b01a2c7ad5b4cd8c89695216 /addons | |
| parent | ebc4704b21b68f68175d924148f722341aef01fe (diff) | |
[IMP] website_rating_project: redesign project rating page
This commit improves and cleans the project rating page;
- statistics of top 5 rated in last 15 days in right column of the page
- move progress bar for tasks on right side of the page
- show the detail of a rating when mouse hovering on the smiley
Since the page is refreshed, the statistics computation is done
with a single SQL request to extract all raw data to calculate
period and partner stats to display.
Thanks to @ror-odoo for the first implementation and to @jem-odoo for its review
Diffstat (limited to 'addons')
5 files changed, 252 insertions, 66 deletions
diff --git a/addons/website_rating_project/controllers/main.py b/addons/website_rating_project/controllers/main.py index 4d6b3ecb65e..98c6cacc057 100644 --- a/addons/website_rating_project/controllers/main.py +++ b/addons/website_rating_project/controllers/main.py @@ -3,8 +3,6 @@ from werkzeug.exceptions import NotFound -import datetime - from odoo import http from odoo.http import request @@ -17,34 +15,78 @@ class WebsiteRatingProject(http.Controller): values = {'projects': projects} return request.render('website_rating_project.index', values) + def _calculate_period_partner_stats(self, project_id): + # get raw data: number of rating by rated partner, by rating value, by period + request.env.cr.execute(""" + SELECT + rated_partner_id, + rating, + COUNT(rating) as rating_count, + CASE + WHEN now()::date - create_date::date BETWEEN 0 AND 6 Then 'days_06' + WHEN now()::date - create_date::date BETWEEN 0 AND 15 Then 'days_15' + WHEN now()::date - create_date::date BETWEEN 0 AND 30 Then 'days_30' + WHEN now()::date - create_date::date BETWEEN 0 AND 90 Then 'days_90' + END AS period + FROM + rating_rating + WHERE + parent_res_model = 'project.project' + AND parent_res_id = %s + AND res_model = 'project.task' + AND rated_partner_id IS NOT NULL + AND create_date >= current_date - interval '90' day + AND rating IN (1,5,10) + GROUP BY + rated_partner_id, rating, period + """ % (project_id)) + + raw_data = request.env.cr.dictfetchall() + + # periodical statistics + default_period_dict = {'rating_10': 0, 'rating_5': 0, 'rating_1': 0, 'total': 0} + period_statistics = { + 'days_06': dict(default_period_dict), + 'days_15': dict(default_period_dict), + 'days_30': dict(default_period_dict), + 'days_90': dict(default_period_dict), + } + for period_statistics_key in period_statistics.keys(): + for row in raw_data: + if row['period'] <= period_statistics_key: + period_statistics[period_statistics_key]['rating_%s' % (int(row['rating']),)] += row['rating_count'] + period_statistics[period_statistics_key]['total'] += row['rating_count'] + + # partner statistics + default_partner_dict = {'rating_10': 0, 'rating_5': 0, 'rating_1': 0, 'total': 0, 'rated_partner': None, 'percentage_happy': 0.0} + partner_statistics = {} + for row in raw_data: + if row['period'] <= 'days_15': + if row['rated_partner_id'] not in partner_statistics: + partner_statistics[row['rated_partner_id']] = dict(default_partner_dict) + partner_statistics[row['rated_partner_id']]['rated_partner'] = request.env['res.partner'].sudo().browse(row['rated_partner_id']) + partner_statistics[row['rated_partner_id']]['rating_%s' % (int(row['rating']),)] += row['rating_count'] + partner_statistics[row['rated_partner_id']]['total'] += row['rating_count'] + + for partner_id, stat_values in partner_statistics.items(): + stat_values['percentage_happy'] = (stat_values['rating_10'] / float(stat_values['total'])) * 100 if stat_values['total'] else 0.0 + + return { + 'partner_statistics': partner_statistics, + 'period_statistics': period_statistics, + } + @http.route(['/project/rating/<int:project_id>'], type='http', auth="public", website=True) def page(self, project_id=None, **kw): user = request.env.user project = request.env['project.project'].sudo().browse(project_id) # to avoid giving any access rights on projects to the public user, let's use sudo # and check if the user should be able to view the project (project managers only if it's unpublished or has no rating) - if not ((project.rating_status!='no') and project.portal_show_rating) and not user.sudo(user).has_group('project.group_project_manager'): + if not ((project.rating_status != 'no') and project.portal_show_rating) and not user.sudo(user).has_group('project.group_project_manager'): raise NotFound() - values = { + + return request.render('website_rating_project.project_rating_page', { 'project': project, - 'task_data': self._calculate_rating(project.id), - } - return request.render('website_rating_project.project_rating_page', values) - - def _calculate_rating(self, project_id): - # Calculate rating for Tasks - records = request.env["project.task"].sudo().search([('project_id', '=', project_id)]) - domain = [('res_model', '=', "project.task"), ('res_id', 'in', records.ids), ('consumed', '=', True)] - ratings = request.env['rating.rating'].search(domain, order="id desc", limit=100) - - yesterday = (datetime.date.today() - datetime.timedelta(days=-1)).strftime('%Y-%m-%d 23:59:59') - stats = {} - for x in (7, 30, 90): - todate = (datetime.date.today() - datetime.timedelta(days=x)).strftime('%Y-%m-%d 00:00:00') - domdate = domain + [('create_date', '<=', yesterday), ('create_date', '>=', todate)] - stats[x] = {1: 0, 5: 0, 10: 0} - rating_stats = request.env['rating.rating'].read_group(domdate, [], ['rating']) - total = sum(st['rating_count'] for st in rating_stats) - for rate in rating_stats: - stats[x][rate['rating']] = float("%.2f" % (rate['rating_count'] * 100.0 / total)) - return {'ratings': ratings, 'stats': stats} + 'ratings': request.env['rating.rating'].sudo().search([('consumed', '=', True), ('parent_res_model', '=', 'project.project'), ('parent_res_id', '=', project_id)], order='write_date DESC', limit=50), + 'statistics': self._calculate_period_partner_stats(project_id), + }) diff --git a/addons/website_rating_project/static/src/css/website_rating_project.css b/addons/website_rating_project/static/src/css/website_rating_project.css deleted file mode 100644 index 6f5c549cc9a..00000000000 --- a/addons/website_rating_project/static/src/css/website_rating_project.css +++ /dev/null @@ -1,3 +0,0 @@ -.oe_website_rating_project .thumbnail{ - height: 355px; -} diff --git a/addons/website_rating_project/static/src/js/website_rating_project.js b/addons/website_rating_project/static/src/js/website_rating_project.js new file mode 100644 index 00000000000..44c91980ae7 --- /dev/null +++ b/addons/website_rating_project/static/src/js/website_rating_project.js @@ -0,0 +1,27 @@ +odoo.define('website_rating_project.rating', function (require) { +'use strict'; + + var time = require('web.time'); + require('web.dom_ready'); + + if(!$('.o_website_rating_project').length) { + return $.Deferred().reject("DOM doesn't contain '.o_website_rating_project'"); + } + + /** + * Rating popover with some informations + */ + $('.o_website_rating_project .o_rating_image').popover({ + placement: 'bottom', + trigger: 'hover', + html: 'true', + content: function () { + var id = $(this).data('id'); + var rating_date = $(this).data('rating-date'); + var base_date = time.auto_str_to_date(rating_date); + var duration = moment(base_date).fromNow(); + $("#rating_"+ id).find(".rating_timeduration").text(duration); + return $("#rating_"+ id).html(); + } + }); +}); diff --git a/addons/website_rating_project/static/src/less/website_rating_project.less b/addons/website_rating_project/static/src/less/website_rating_project.less new file mode 100644 index 00000000000..e95b1dbfac7 --- /dev/null +++ b/addons/website_rating_project/static/src/less/website_rating_project.less @@ -0,0 +1,34 @@ +.o_website_rating_project { + .thumbnail{ + height: 240px; + } + .o_top_partner_rating_image { + height: 15px; + } + .o_top_partner_image { + height: 30px; + width: 30px; + } + .o_top_partner_feedback{ + word-wrap: break-word; + } + .o_vertical_separator { + border-left: 1px solid #eeeeee + } + .o_rating_progress { + margin-bottom: 10px; + } + .o_rating_count { + display: inline-block; + min-width: 22px + } + .o_smiley_no_padding_left { + padding-left: 0; + } + .o_smiley_no_padding_right { + padding-right: 0; + } + .o_lighter_smileys { + opacity: 0.4 + } +}
\ No newline at end of file diff --git a/addons/website_rating_project/views/website_rating_project.xml b/addons/website_rating_project/views/website_rating_project.xml index 74f5f48fb91..c3376dfe51f 100644 --- a/addons/website_rating_project/views/website_rating_project.xml +++ b/addons/website_rating_project/views/website_rating_project.xml @@ -4,7 +4,8 @@ <!-- CSS Style Bundle --> <template id="assets_frontend" inherit_id="website.assets_frontend" name="Website Rating Project Assets"> <xpath expr="." position="inside"> - <link rel="stylesheet" href="/website_rating_project/static/src/css/website_rating_project.css"/> + <link rel="stylesheet" href="/website_rating_project/static/src/less/website_rating_project.less"/> + <script type="text/javascript" src="/website_rating_project/static/src/js/website_rating_project.js"/> </xpath> </template> @@ -21,12 +22,17 @@ <t t-set="additional_title">Project Satisfaction</t> <div id="wrap"> <div class="oe_structure"/> - <div class="container oe_website_rating_project"> + <div class="container o_website_rating_project"> <div class="row text-center"> <h2>Projects</h2> </div> <div class="oe_structure"/> <div class="row"> + <t t-if="not projects"> + <div class="mt64 text-center"> + No project rating puslished for now. + </div> + </t> <t t-foreach="projects" t-as="project"> <div t-if="project.percentage_satisfaction_project != -1" class="col-sm-6 col-xs-4 col-md-4 col-lg-4"> <div class="thumbnail"> @@ -65,60 +71,140 @@ </t> </template> - <!-- Page : Rating of a particular project --> - <template id="project_rating_data" name="Project Rating Page"> - <div class="progress"> - <div class="progress-bar progress-bar-success" t-attf-style="width: #{stats[duration][10]}%;" title="Happy"> - <t t-esc="int(stats[duration][10])"/>% + <!-- Page : Project Rating Page (and widgets) --> + <template id="portal_project_rating_progressbar"> + <div class="progress o_rating_progress"> + <t t-set="percent_rating_10" t-value="(stats['rating_10'] / float(stats['total'])) * 100 if stats['total'] else 0.0"/> + <div class="progress-bar progress-bar-success" t-attf-style="width: #{percent_rating_10}%;" title="Happy"> + <t t-esc="percent_rating_10"/>% </div> - <div class="progress-bar progress-bar-warning" t-attf-style="width: #{stats[duration][5]}%;" title="Average"> - <t t-esc="int(stats[duration][5])"/>% + <t t-set="percent_rating_5" t-value="(stats['rating_5'] / float(stats['total'])) * 100 if stats['total'] else 0.0"/> + <div class="progress-bar progress-bar-warning" t-attf-style="width: #{percent_rating_5}%;" title="Average"> + <t t-esc="percent_rating_5"/>% </div> - <div class="progress-bar progress-bar-danger" t-attf-style="width: #{stats[duration][1]}%;" title="Bad"> - <t t-esc="int(stats[duration][1])"/>% + <t t-set="percent_rating_1" t-value="(stats['rating_1'] / float(stats['total'])) * 100 if stats['total'] else 0.0"/> + <div class="progress-bar progress-bar-danger" t-attf-style="width: #{percent_rating_1}%;" title="Bad"> + <t t-esc="percent_rating_1"/>% </div> </div> </template> - <template id="project_data_satisfaction"> - <div class="row"> - <div class="col-sm-4"> - <h5>Last 7 days</h5> - <t t-set="duration" t-value="7"/> - <t t-set="stats" t-value="data['stats']"/> - <t t-call="website_rating_project.project_rating_data"/> + <template id="portal_project_rating_partner_stat"> + <t t-foreach="partner_stats.values()" t-as="partner_stat"> + <div class="row mt8" t-if="partner_stat['total']" title="Top 5 partner ratings of last 15 days."> + <div class="col-sm-1"> + <img t-if="partner_stat['rated_partner'].image" class="o_top_partner_image" t-attf-src="data:image/png;base64,#{partner_stat['rated_partner'].image}"/> + <img t-if="not partner_stat['rated_partner'].image" class="o_top_partner_image" src='/web/static/src/img/placeholder.png'/> + </div> + <div class="col-sm-4 o_smiley_no_padding_right"> + <span class="ml8" t-esc="partner_stat['rated_partner'].name"/> + </div> + <div class="col-sm-4 o_smiley_no_padding_right"> + <div class="col-sm-4 o_smiley_no_padding_right o_smiley_no_padding_left"> + <span t-if="partner_stat['rating_10']"> + <img title="happy" class="o_top_partner_rating_image" src='/rating/static/src/img/rating_10.png'/> + <span class="o_rating_count" t-esc="partner_stat['rating_10']"/> + </span> + <span t-if="not partner_stat['rating_10']"> + <img title="happy" class="o_top_partner_rating_image o_lighter_smileys" src='/rating/static/src/img/rating_10.png'/> + <span class="o_rating_count o_lighter_smileys" t-esc="partner_stat['rating_10']"/> + </span> + </div> + <div class="col-sm-4 o_smiley_no_padding_right o_smiley_no_padding_left"> + <span t-if="partner_stat['rating_5']"> + <img title="happy" class="o_top_partner_rating_image" src='/rating/static/src/img/rating_5.png'/> + <span class="o_rating_count" t-esc="partner_stat['rating_5']"/> + </span> + <span t-if="not partner_stat['rating_5']"> + <img title="happy" class="o_top_partner_rating_image o_lighter_smileys" src='/rating/static/src/img/rating_5.png'/> + <span class="o_rating_count o_lighter_smileys" t-esc="partner_stat['rating_5']"/> + </span> + </div> + <div class="col-sm-4 o_smiley_no_padding_right o_smiley_no_padding_left"> + <span t-if="partner_stat['rating_1']"> + <img title="happy" class="o_top_partner_rating_image" src='/rating/static/src/img/rating_1.png'/> + <span class="o_rating_count" t-esc="partner_stat['rating_1']"/> + </span> + <span t-if="not partner_stat['rating_1']"> + <img title="happy" class="o_top_partner_rating_image o_lighter_smileys" src='/rating/static/src/img/rating_1.png'/> + <span class="o_rating_count o_lighter_smileys" t-esc="partner_stat['rating_1']"/> + </span> + </div> + </div> + <div class="col-sm-3 o_smiley_no_padding_left"> + <strong class="pull-right"><t t-esc="partner_stat['percentage_happy']"/>%</strong> + </div> </div> - <div class="col-sm-4"> - <h5>Last 30 days</h5> - <t t-set="duration" t-value="30"/> - <t t-set="stats" t-value="data['stats']"/> - <t t-call="website_rating_project.project_rating_data"/> + </t> + </template> + + <template id="portal_project_rating_popover"> + <div t-attf-id="rating_#{rating.id}" class="container hidden"> + <div class="row"> + <div class="col-md-2"> + <img t-if="rating.partner_id.image" class="o_top_partner_image" t-attf-src="data:image/png;base64,#{rating.partner_id.image}"/> + <img t-if="not rating.partner_id.image" class="o_top_partner_image" src='/web/static/src/img/placeholder.png'/> + </div> + <div class="col-md-10"> + <div class="mt4"> + <strong><t t-esc="rating.rated_partner_id.name"/></strong> - <span class="rating_timeduration"><t t-esc="rating.create_date"/></span> + </div> + <strong>Rated by: </strong><t t-esc="rating.partner_id.name" /> + </div> </div> - <div class="col-sm-4"> - <h5>Last 3 months</h5> - <t t-set="duration" t-value="90"/> - <t t-set="stats" t-value="data['stats']"/> - <t t-call="website_rating_project.project_rating_data"/> + <div class="row o_top_partner_feedback" t-if="rating.feedback"> + <div class="col-md-12"> + <t t-esc="rating.feedback" /> + </div> </div> - </div> - <h5 class="page-header">Latest Feedbacks</h5> - <t t-foreach="data['ratings']" t-as="rating" class="media"> - <img t-attf-src='/rating/static/src/img/rating_#{int(rating.rating)}.png' t-att-alt="rating.res_name" t-att-title="rating.res_name"/> - </t> </template> <template id="project_rating_page" name="Project Rating Page"> <t t-call="website.layout"> <div id="wrap"> <div class="oe_structure"/> - <div class="container oe_website_rating_project"> + <div class="container o_website_rating_project"> <h1 t-esc="project.name" class="text-center"/> <div class="row mb32"> - <div class="col-md-8"> - <t t-set="type" t-value="'Tasks'"/> - <t t-set="data" t-value="task_data"/> - <t t-call="website_rating_project.project_data_satisfaction"/> + <div> + <h3 class="page-header">Customer Ratings</h3> + <div class="row"> + <div class="col-md-7"> + <t t-foreach="ratings" t-as="rating"> + <img t-attf-src='/rating/static/src/img/rating_#{int(rating.rating)}.png' + class="mt4 o_rating_image" + t-att-data-id="rating.id" + t-att-data-rating-date="rating.write_date"/> + <t t-call="website_rating_project.portal_project_rating_popover"/> + </t> + </div> + <div class="col-md-5 o_vertical_separator"> + <div t-if="statistics['period_statistics']['days_06']"> + <h5>Last 7 days</h5> + <t t-call="website_rating_project.portal_project_rating_progressbar"> + <t t-set="stats" t-value="statistics['period_statistics']['days_06']"/> + </t> + </div> + <div t-if="statistics['period_statistics']['days_30']"> + <h5>Last 30 days</h5> + <t t-call="website_rating_project.portal_project_rating_progressbar"> + <t t-set="stats" t-value="statistics['period_statistics']['days_30']"/> + </t> + </div> + <div t-if="statistics['period_statistics']['days_90']"> + <h5>Last 3 months</h5> + <t t-call="website_rating_project.portal_project_rating_progressbar"> + <t t-set="stats" t-value="statistics['period_statistics']['days_90']"/> + </t> + </div> + <div> + <t t-call="website_rating_project.portal_project_rating_partner_stat"> + <t t-set="partner_stats" t-value="statistics['partner_statistics']"/> + </t> + </div> + </div> + </div> </div> </div> </div> |
