diff options
| author | Jérome Maes <jem@openerp.com> | 2017-08-17 09:38:16 +0200 |
|---|---|---|
| committer | jem-odoo <jem@openerp.com> | 2017-12-12 14:06:29 +0100 |
| commit | d7d30f69d78e75cb859ec6b5e70b91d78c6e4eaf (patch) | |
| tree | 11e537f68c4f71e7afd82cfb9ddccebea9e971da /addons/project | |
| parent | 93adbdbb208a29b0a61b97a502bbbadb16e8e60a (diff) | |
[REM] website_rating_project: move feature in project
Displaying the rating of project is moved directly
in project module, since it does not depends on website.
User may want to display rating of its support project
even without website.
Diffstat (limited to 'addons/project')
| -rw-r--r-- | addons/project/__manifest__.py | 3 | ||||
| -rw-r--r-- | addons/project/controllers/__init__.py | 1 | ||||
| -rw-r--r-- | addons/project/controllers/rating.py | 92 | ||||
| -rw-r--r-- | addons/project/models/project.py | 15 | ||||
| -rw-r--r-- | addons/project/static/src/js/portal_rating.js | 27 | ||||
| -rw-r--r-- | addons/project/static/src/less/portal_rating.less | 34 | ||||
| -rw-r--r-- | addons/project/views/project_assets.xml (renamed from addons/project/views/project_templates.xml) | 8 | ||||
| -rw-r--r-- | addons/project/views/project_rating_templates.xml | 203 | ||||
| -rw-r--r-- | addons/project/views/project_views.xml | 1 |
9 files changed, 380 insertions, 4 deletions
diff --git a/addons/project/__manifest__.py b/addons/project/__manifest__.py index 43b36689d88..e80837b5631 100644 --- a/addons/project/__manifest__.py +++ b/addons/project/__manifest__.py @@ -29,8 +29,9 @@ 'views/project_views.xml', 'views/res_partner_views.xml', 'views/res_config_settings_views.xml', - 'views/project_templates.xml', + 'views/project_assets.xml', 'views/project_portal_templates.xml', + 'views/project_rating_templates.xml', 'data/project_mail_template_data.xml', 'wizard/project_task_merge_wizard_views.xml', ], diff --git a/addons/project/controllers/__init__.py b/addons/project/controllers/__init__.py index 903b755e71e..10825353c81 100644 --- a/addons/project/controllers/__init__.py +++ b/addons/project/controllers/__init__.py @@ -2,3 +2,4 @@ # Part of Odoo. See LICENSE file for full copyright and licensing details. from . import portal +from . import rating diff --git a/addons/project/controllers/rating.py b/addons/project/controllers/rating.py new file mode 100644 index 00000000000..3343674c0e8 --- /dev/null +++ b/addons/project/controllers/rating.py @@ -0,0 +1,92 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from werkzeug.exceptions import NotFound + +from odoo import http +from odoo.http import request + + +class RatingProject(http.Controller): + + @http.route(['/project/rating/'], type='http', auth="public", website=True) + def index(self, **kw): + projects = request.env['project.project'].sudo().search([('rating_status', '!=', 'no'), ('portal_show_rating', '=', True)]) + values = {'projects': projects} + return request.render('project.rating_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'): + raise NotFound() + + return request.render('project.rating_project_rating_page', { + 'project': project, + '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/project/models/project.py b/addons/project/models/project.py index c1e802839da..1df6bbad540 100644 --- a/addons/project/models/project.py +++ b/addons/project/models/project.py @@ -235,9 +235,11 @@ class Project(models.Model): "- Periodical Rating: Email will be sent periodically\n\n" "Don't forget to set up the mail templates on the stages for which you want to get the customer's feedbacks.", default="no", required=True) rating_status_period = fields.Selection([ - ('daily', 'Daily'), ('weekly', 'Weekly'), ('bimonthly', 'Twice a Month'), - ('monthly', 'Once a Month'), ('quarterly', 'Quarterly'), ('yearly', 'Yearly') - ], 'Rating Frequency') + ('daily', 'Daily'), ('weekly', 'Weekly'), ('bimonthly', 'Twice a Month'), + ('monthly', 'Once a Month'), ('quarterly', 'Quarterly'), ('yearly', 'Yearly') + ], 'Rating Frequency') + + portal_show_rating = fields.Boolean('Rating visible publicly', copy=False, oldname='website_published') _sql_constraints = [ ('project_date_greater', 'check(date >= date_start)', 'Error! project start-date must be lower than project end-date.') @@ -405,6 +407,13 @@ class Project(models.Model): @api.multi def action_view_all_rating(self): """ return the action to see all the rating of the project, and activate default filters """ + if self.portal_show_rating: + return { + 'type': 'ir.actions.act_url', + 'name': "Redirect to the Website Projcet Rating Page", + 'target': 'self', + 'url': "/project/rating/%s" % (self.id,) + } action = self.env['ir.actions.act_window'].for_xml_id('project', 'rating_rating_action_view_project_rating') action['name'] = _('Ratings of %s') % (self.name,) action_context = safe_eval(action['context']) if action['context'] else {} diff --git a/addons/project/static/src/js/portal_rating.js b/addons/project/static/src/js/portal_rating.js new file mode 100644 index 00000000000..12740b9bbba --- /dev/null +++ b/addons/project/static/src/js/portal_rating.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_portal_project_rating').length) { + return $.Deferred().reject("DOM doesn't contain '.o_portal_project_rating'"); + } + + /** + * Rating popover with some informations + */ + $('.o_portal_project_rating .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/project/static/src/less/portal_rating.less b/addons/project/static/src/less/portal_rating.less new file mode 100644 index 00000000000..4ae3290a75b --- /dev/null +++ b/addons/project/static/src/less/portal_rating.less @@ -0,0 +1,34 @@ +.o_portal_project_rating { + .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/project/views/project_templates.xml b/addons/project/views/project_assets.xml index 9b6d13d2582..7a2c4647984 100644 --- a/addons/project/views/project_templates.xml +++ b/addons/project/views/project_assets.xml @@ -8,6 +8,14 @@ <link rel="stylesheet" href="/project/static/src/less/project_dashboard.less"/> </xpath> </template> + + <template id="assets_frontend" inherit_id="web.assets_frontend" name="Rating Project Assets"> + <xpath expr="." position="inside"> + <link rel="stylesheet" href="/project/static/src/less/portal_rating.less"/> + <script type="text/javascript" src="/project/static/src/js/portal_rating.js"/> + </xpath> + </template> + <template id="qunit_suite" name="project_test" inherit_id="web.qunit_suite"> <xpath expr="//t[@t-set='head']" position="inside"> <script type="text/javascript" src="/project/static/tests/project_tests.js"></script> diff --git a/addons/project/views/project_rating_templates.xml b/addons/project/views/project_rating_templates.xml new file mode 100644 index 00000000000..7ac2e9463ba --- /dev/null +++ b/addons/project/views/project_rating_templates.xml @@ -0,0 +1,203 @@ +<?xml version="1.0" encoding="utf-8"?> +<odoo> + <data> + + <!-- Page : List of project --> + <template id="rating_index" name="Project Rating List"> + <t t-call="portal.frontend_layout"> + <t t-set="additional_title">Project Satisfaction</t> + <div id="wrap"> + <div class="oe_structure"/> + <div class="container o_portal_project_rating"> + <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"> + <div> + <div class="caption"> + <span class="label label-default pull-right"><t t-esc="project.privacy_visibility"/></span> + <h4><t t-esc="project.name"/></h4> + <p t-if="project.date" class="text-muted"> + <i class="fa fa-calendar"/> End date : <t t-esc="project.date"/> + </p> + <p t-if="project.alias_name and project.alias_domain" class="text-muted"> + <i class="fa fa-envelope"/> Email : <t t-esc="project.alias_name"/>@<t t-esc="project.alias_domain"/> + </p> + <div class="row"> + <div class="col-md-6 text-center"> + <h2><t t-esc="len(project.task_ids)"/></h2> + <p><t t-esc="project.label_tasks"/></p> + </div> + </div> + </div> + <div class="caption"> + <p> + <a t-att-href="'/project/rating/%s' % project.id" class="btn btn-primary btn-lg btn-block"> + <i class="fa fa-arrow-circle-right "/> See the feedbacks + </a> + </p> + </div> + </div> + </div> + </div> + </t> + </div> + </div> + <div class="oe_structure"/> + </div> + </t> + </template> + + <!-- 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> + <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> + <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="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> + </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="row o_top_partner_feedback" t-if="rating.feedback"> + <div class="col-md-12"> + <t t-esc="rating.feedback" /> + </div> + </div> + </div> + </template> + + <template id="rating_project_rating_page" name="Project Rating Page"> + <t t-call="portal.frontend_layout"> + <div id="wrap"> + <div class="oe_structure"/> + <div class="container o_project o_portal_project_rating"> + <h1 t-esc="project.name" class="text-center"/> + <div class="row mb32"> + <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="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="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="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="project.portal_project_rating_progressbar"> + <t t-set="stats" t-value="statistics['period_statistics']['days_90']"/> + </t> + </div> + <div> + <t t-call="project.portal_project_rating_partner_stat"> + <t t-set="partner_stats" t-value="statistics['partner_statistics']"/> + </t> + </div> + </div> + </div> + </div> + </div> + </div> + <div class="oe_structure"/> + </div> + </t> + </template> + + </data> +</odoo> diff --git a/addons/project/views/project_views.xml b/addons/project/views/project_views.xml index 3fc53cf4892..cf6ef36411c 100644 --- a/addons/project/views/project_views.xml +++ b/addons/project/views/project_views.xml @@ -152,6 +152,7 @@ </p> </div> <field name="rating_status_period" class="oe_inline" attrs="{'required': [('rating_status','=','periodic')], 'invisible': [('rating_status','!=','periodic')]}" groups="project.group_project_rating"/> + <field name="portal_show_rating" attrs="{'invisible': [('rating_status','==','no')]}" groups="project.group_project_rating"/> </group> <group name="misc"> <group string="Configuration" groups="base.group_no_one"> |
