[SEC] rating: escape feedback, text2html to avoid js injection

[IMP] rating: website rating page: reviewed design, rate on click, update on submit
[IMP] rating_project: rating_status: 'no' instead of False
[IMP] rating_project/issue: smileys insteaf of thumbs for consistency in kanban
[IMP] rating: allow to update an existing rating (feedback + rate)
[IMP] rating: consistency on rating names (statisfied, not satisfied, higly dissatisfed)
[IMP] rating: avoid deadend after rating, button to go to Odoo
[IMP] rating: res_config better sentence
This commit is contained in:
Fabien Pinckaers
2016-07-02 18:33:55 +02:00
parent 580129522f
commit fb289e0b5d
11 changed files with 88 additions and 69 deletions
+1 -1
View File
@@ -17,7 +17,7 @@ class ProjectConfiguration(models.TransientModel):
'-This installs the module pad.')
module_rating_project = fields.Selection([
(0, "No customer rating"),
(1, 'Allow activating customer rating on projects, at issue completion')
(1, 'Allow customer ratings on tasks or issues')
], string="Rating",
help="This allows customers to give rating on provided services")
generate_project_alias = fields.Selection([
+22 -14
View File
@@ -4,28 +4,36 @@ import werkzeug
from openerp import http
from openerp.http import request
from openerp.tools.translate import _
class Rating(http.Controller):
@http.route('/rating/<string:token>/<int:rate>', type='http', auth="public")
def open_rating(self, token, rate, **kwargs):
assert rate in (1, 5, 10), "Incorrect rating"
rating = request.env['rating.rating'].sudo().search([('access_token', '=', token)])
if not rating.consumed:
return request.render('rating.rating_external_page_submit', {'rating': rate, 'token': token})
else:
return request.render('rating.rating_external_page_view', {'is_rated': True})
if not rating:
return request.not_found()
rate_names={
5: _("not satisfied"),
1: _("highly dissatisfied"),
10: _("satisfied")
}
rating.sudo().write({'rating': rate, 'consumed': True})
return request.render('rating.rating_external_page_submit', {
'rating': rating, 'token': token,
'rate_name': rate_names[rate], 'rate': rate
})
return request.not_found()
@http.route(['/rating/<string:token>/<int:rate>/submit_feedback'], type="http", auth="public", method=['post'])
def submit_rating(self, token, rate, **kwargs):
rating = request.env['rating.rating'].sudo().search([('access_token', '=', token)])
if not rating.consumed:
record_sudo = request.env[rating.res_model].sudo().browse(rating.res_id)
record_sudo.rating_apply(rate, token=token, feedback=kwargs.get('feedback'))
# redirect to the form view if logged person
if request.session.uid:
return werkzeug.utils.redirect('/web#model=%s&id=%s&view_type=form' % (record_sudo._name, record_sudo.id))
return request.render('rating.rating_external_page_view', {'is_public': True})
else:
return request.render('rating.rating_external_page_view', {'is_rated': True})
return request.not_found()
if not rating:
return request.not_found()
record_sudo = request.env[rating.res_model].sudo().browse(rating.res_id)
record_sudo.rating_apply(rate, token=token, feedback=kwargs.get('feedback'))
# redirect to the form view if logged person
if request.session.uid:
return werkzeug.utils.redirect('/web#model=%s&id=%s&view_type=form' % (record_sudo._name, record_sudo.id))
return request.render('rating.rating_external_page_view')
+9 -5
View File
@@ -1,6 +1,9 @@
# -*- coding: utf-8 -*-
import uuid
from odoo import api, fields, models
from openerp import tools
class Rating(models.Model):
@@ -105,22 +108,23 @@ class RatingMixin(models.AbstractModel):
"""
Rating, rating = self.env['rating.rating'], None
if token:
rating = self.env['rating.rating'].search([('access_token', '=', token), ('consumed', '=', False)], limit=1)
rating = self.env['rating.rating'].search([('access_token', '=', token)], limit=1)
else:
rating = Rating.search([('res_model', '=', self._name), ('res_id', '=', self.ids[0]), ('consumed', '=', False)], limit=1)
rating = Rating.search([('res_model', '=', self._name), ('res_id', '=', self.ids[0])], limit=1)
if rating:
rating.write({'rating': rate, 'feedback': feedback, 'consumed': True})
if hasattr(self, 'message_post'):
feedback = tools.plaintext2html(feedback or '')
self.message_post(
body="<img src='/rating/static/src/img/rating_%s.png' style='width:20px;height:20px'/>%s"
% (rate, '<br/>' + feedback if feedback else ''),
body="<img src='/rating/static/src/img/rating_%s.png' style='width:20px;height:20px;float:left;margin-right: 5px;'/>%s"
% (rate, feedback),
subtype=subtype or "mail.mt_comment",
author_id=rating.partner_id and rating.partner_id.id or None # None will set the default author in mail_thread.py
)
if hasattr(self, 'stage_id') and self.stage_id and hasattr(self.stage_id, 'auto_validation_kanban_state') and self.stage_id.auto_validation_kanban_state:
if rating.rating > 5:
self.write({'kanban_state': 'done'})
else:
if rating.rating < 5:
self.write({'kanban_state': 'blocked'})
return rating
+23 -21
View File
@@ -101,13 +101,12 @@
<link rel='stylesheet' href='/web/static/lib/fontawesome/css/font-awesome.css'/>
</head>
<div class="container">
<div clas="row" t-if="is_rated">
<h1 class="text-center"><i class="fa fa-frown-o fa-2x text-danger" /></h1>
<h1 class="text-center">Sorry you have already voted!</h1>
<div class="text-center" style="margin-top:128px">
<i class="fa fa-check-circle fa-5x text-success" />
</div>
<div clas="row" t-if="is_public">
<h1 class="text-center"><i class="fa fa-check-circle fa-2x text-success" /></h1>
<h1 class="text-center">Thank you for submitting your feedback!</h1>
<h2 class="text-center">We appreciate your feedback!</h2>
<div class="text-center">
<a href="https://www.odoo.com" class="btn btn-primary">Go to Odoo</a>
</div>
</div>
</template>
@@ -119,21 +118,24 @@
</head>
<div class="container">
<div clas="row">
<h1 class="text-center">Please submit your rating</h1>
<h4 class="text-center">You selected <img t-attf-src='/rating/static/src/img/rating_#{rating}.png'/></h4>
<form class="form-horizontal" t-attf-action="/rating/#{token}/#{rating}/submit_feedback" method="post">
<div class="form-group">
<div class="col-md-6 col-md-offset-3">
<h5>Do you want to give additional explanation? (Optional)</h5>
<input type="hidden" name="csrf_token" t-att-value="request.csrf_token()"/>
<textarea class="form-control" name="feedback" rows="3"></textarea>
</div>
</div>
<div class="form-group">
<div class="col-md-6 col-md-offset-3">
<button type="submit" class="btn btn-primary">Submit</button>
</div>
</div>
<h1 class="text-center">Thanks! We appreciate your feedback.</h1>
<h4 class="text-center text-muted" style="margin-bottom: 32px;">Your rating has been submitted.</h4>
<div class="pull-left">
<img t-attf-src='/rating/static/src/img/rating_#{rate}.png'/>
</div>
<div style="margin-left: 80px;">
you are <b t-esc="rate_name"></b><br/>
on our services on "<b t-esc="rating.res_name"></b>"<br/>
<t t-if="rating.rated_partner_id">by <b t-esc="rating.rated_partner_id.name"></b>.</t>
</div>
<div class="clearfix"></div>
<p style="margin-top:32px;">
Would be great if you can provide more informaion:
</p>
<form class="form-horizontal" t-attf-action="/rating/#{token}/#{rate}/submit_feedback" method="post">
<input type="hidden" name="csrf_token" t-att-value="request.csrf_token()"/>
<textarea class="form-control" name="feedback" rows="8" t-esc="rating.feedback"></textarea>
<button type="submit" class="btn btn-primary" style="margin-top:8px;">Send Feedback</button>
</form>
</div>
</div>
+6 -5
View File
@@ -57,8 +57,9 @@
<tr>
<td style="text-align:center;">
<h2 style="font-weight:300;font-size:18px;">
Tell us how you feel about our service
Tell us how you feel about our service:
</h2>
<div style="text-color: #888888">(click on one of these smileys)</div>
</td>
</tr>
<tr>
@@ -67,17 +68,17 @@
<tr>
<td>
<a href="/rating/${access_token}/10">
<img alt="Great" src="/rating/static/src/img/rating_10.png" title="It was great"/>
<img alt="Satisfied" src="/rating/static/src/img/rating_10.png" title="Satisfied"/>
</a>
</td>
<td>
<a href="/rating/${access_token}/5">
<img alt="Okay" src="/rating/static/src/img/rating_5.png" title="It was okay"/>
<img alt="Not satisfied" src="/rating/static/src/img/rating_5.png" title="Not satisfied"/>
</a>
</td>
<td>
<a href="/rating/${access_token}/1">
<img alt="Bad" src="/rating/static/src/img/rating_1.png" title="It wasn't good"/>
<img alt="Highly Dissatisfied" src="/rating/static/src/img/rating_1.png" title="Highly Dissatisfied"/>
</a>
</td>
</tr>
@@ -96,7 +97,7 @@
<p>This customer survey has been sent because your task has been moved to the stage <b>${object.stage_id.name}</b></p>
% endif
% if object.project_id.rating_status == 'periodic':
<p>This customer survey has been sent <b>${object.project_id.rating_status_period}</b> as long as the task is in the <b>${object.stage_id.name}</b> stage.
<p>This customer survey is send <b>${object.project_id.rating_status_period}</b> as long as the task is in the <b>${object.stage_id.name}</b> stage.
% endif
<p>Email automatically sent by <a href="https://www.odoo.com/page/project-management" style="color:#a24689;text-decoration:none;">Odoo Project</a> for <a href="${object.project_id.company_id.website}" style="color:#a24689;text-decoration:none;">${object.project_id.company_id.name}</a></p>
</td></tr>
+3 -3
View File
@@ -78,10 +78,10 @@ class Project(models.Model):
percentage_satisfaction_project = fields.Integer(
compute="_compute_percentage_satisfaction_project", string="Happy % on Project", store=True, default=-1)
rating_request_deadline = fields.Datetime(compute='_compute_rating_request_deadline', store=True)
rating_status = fields.Selection([('stage', 'Rating on Stage'), ('periodic', 'Periodical Rating')], 'Customer(s) Ratings', help="How to get the customer's feedbacks?\n"
"- Rating on stage: Email will be sent when a task/issue is pulled in another stage\n"
rating_status = fields.Selection([('stage', 'Rating when changing stage'), ('periodic', 'Periodical Rating'), ('no','No rating')], 'Customer(s) Ratings', help="How to get the customer's feedbacks?\n"
"- Rating when changing stage: Email will be sent when a task/issue is pulled in another stage\n"
"- 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.")
"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')
@@ -10,11 +10,11 @@
</field>
<xpath expr="//div[contains(@class, 'o_kanban_primary_left')]" position="inside">
<div t-if="record.rating_status.raw_value" class="mt8 text-primary" title="Percentage of happy ratings over the past 30 days. Get rating details from the More menu.">
<div t-if="record.rating_status.raw_value != 'no'" class="mt8 text-primary" title="Percentage of happy ratings over the past 30 days. Get rating details from the More menu.">
<b>
<i class="fa fa-smile-o"/>
<t t-if="record.percentage_satisfaction_project.value == -1">
No rating
No rating yet
</t>
<t t-if="record.percentage_satisfaction_project.value != -1">
<t t-esc="record.percentage_satisfaction_project.value"/>%
@@ -23,7 +23,7 @@
</div>
</xpath>
<xpath expr="//div[contains(@class, 'o_kanban_card_manage_section')]" position="inside">
<div t-if="record.rating_status.raw_value">
<div t-if="record.rating_status.raw_value != 'no'">
<a name="action_view_all_rating" type="object">
Customers Ratings
</a>
@@ -32,4 +32,4 @@
</field>
</record>
</odoo>
</odoo>
+13 -9
View File
@@ -21,6 +21,7 @@
<field name="arch" type="xml">
<xpath expr="//div[@name='button_box']" position="inside">
<button name="%(rating_rating_action_task)d" type="action"
attrs="{'invisible': [('rating_count', '=', 0)]}"
class="oe_stat_button" icon="fa-smile-o">
<field name="rating_count" string="Rating" widget="statinfo"/>
</button>
@@ -39,9 +40,9 @@
</xpath>
<xpath expr="//div[@class='oe_kanban_bottom_left']" position="inside">
<b t-if="record.rating_ids.raw_value.length">
<span class="fa fa-fw mt4 fa-thumbs-up text-success" t-if="record.rating_last_value.value == 10" title="Latest Rating: Happy"/>
<span class="fa fa-fw mt4 fa-thumbs-down text-warning" t-if="record.rating_last_value.value == 5" title="Latest Rating: Average"/>
<span class="fa fa-fw mt4 fa-thumbs-down text-danger" t-if="record.rating_last_value.value == 1" title="Latest Rating: Not Happy"/>
<span style="font-weight:bold;" class="fa fa-fw mt4 fa-smile-o text-success" t-if="record.rating_last_value.value == 10" title="Latest Rating: Satisfied"/>
<span style="font-weight:bold;" class="fa fa-fw mt4 fa-meh-o text-warning" t-if="record.rating_last_value.value == 5" title="Latest Rating: Not Satisfied"/>
<span style="font-weight:bold;" class="fa fa-fw mt4 fa-frown-o text-danger" t-if="record.rating_last_value.value == 1" title="Latest Rating: Higly Dissatisfied"/>
</b>
</xpath>
</field>
@@ -56,7 +57,7 @@
<field name="arch" type="xml">
<div name="button_box" position="inside">
<button name="action_view_task_rating"
attrs="{'invisible': ['|', '|', ('rating_status', '=', False), ('use_tasks','=', False), ('percentage_satisfaction_task', '=', -1)]}"
attrs="{'invisible': ['|', '|', ('rating_status', '=', 'no'), ('use_tasks','=', False), ('percentage_satisfaction_task', '=', -1)]}"
class="oe_stat_button oe_percent"
type="object"
icon="fa-smile-o">
@@ -66,11 +67,14 @@
<xpath expr="//field[@name='partner_id']" position="after">
<label for="rating_status"/>
<div>
<field name="rating_status" class="oe_inline"/>
<span attrs="{'invisible': [('rating_status','!=','periodic')]}"> -
<field name="rating_status_period" attrs="{'required': [('rating_status','=','periodic')]}" class="oe_inline"/>
</span>
<field name="rating_status" widget="radio"/>
<p attrs="{'invisible': [('rating_status','not in',('periodic','stage'))]}" class="text-muted oe_edit_only">
Edit project's stages and set an email template
on the stages on which you want to activate the rating.
</p>
</div>
<field name="rating_status_period" class="oe_inline"
attrs="{'required': [('rating_status','=','periodic')], 'invisible': [('rating_status','!=','periodic')]}"/>
</xpath>
</field>
</record>
@@ -103,4 +107,4 @@
<menuitem id="rating_rating_menu_project" action="rating_rating_action_project" parent="base.menu_project_report" sequence="5"/>
</data>
</openerp>
</openerp>
@@ -39,9 +39,9 @@
</xpath>
<xpath expr="//div[@class='oe_kanban_bottom_left']" position="inside">
<b t-if="record.rating_ids.raw_value.length">
<span class="fa fa-fw mt4 fa-thumbs-up text-success" t-if="record.rating_last_value.value == 10" title="Latest Rating: Happy"/>
<span class="fa fa-fw mt4 fa-thumbs-down text-warning" t-if="record.rating_last_value.value == 5" title="Latest Rating: Average"/>
<span class="fa fa-fw mt4 fa-thumbs-down text-danger" t-if="record.rating_last_value.value == 1" title="Latest Rating: Not Happy"/>
<span style="font-weight:bold;" class="fa fa-fw mt4 fa-smile-o text-success" t-if="record.rating_last_value.value == 10" title="Latest Rating: Happy"/>
<span style="font-weight:bold;" class="fa fa-fw mt4 fa-meh-o text-warning" t-if="record.rating_last_value.value == 5" title="Latest Rating: Average"/>
<span style="font-weight:bold;" class="fa fa-fw mt4 fa-frown-o text-danger" t-if="record.rating_last_value.value == 1" title="Latest Rating: Not Happy"/>
</b>
</xpath>
</field>
@@ -56,7 +56,7 @@
<field name="arch" type="xml">
<div name="button_box" position="inside">
<button name="action_view_issue_rating"
attrs="{'invisible': ['|', '|', ('use_issues','=', False), ('rating_status', '=', False), ('percentage_satisfaction_issue', '=', -1)]}"
attrs="{'invisible': ['|', '|', ('use_issues','=', False), ('rating_status', '=', 'no'), ('percentage_satisfaction_issue', '=', -1)]}"
class="oe_stat_button oe_percent"
type="object"
icon="fa-smile-o">
@@ -10,7 +10,7 @@ class WebsiteRatingProject(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', '!=', False), ('website_published', '=', True)])
projects = request.env['project.project'].sudo().search([('rating_status', '!=', 'no'), ('website_published', '=', True)])
values = {'projects': projects}
return request.website.render('website_rating_project_issue.index', values)
@@ -20,7 +20,7 @@ class WebsiteRatingProject(http.Controller):
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 and project.website_published) and not user.sudo(user).has_group('project.group_project_manager'):
if not ((project.rating_status<>'no') and project.website_published) and not user.sudo(user).has_group('project.group_project_manager'):
raise NotFound()
values = {
'project': project,
@@ -6,7 +6,7 @@
<field name="arch" type="xml">
<xpath expr='//div[@name="button_box"]/*[1]' position='before'>
<button class="oe_stat_button" name="website_publish_button"
type="object" icon="fa-globe" attrs="{'invisible': [('rating_status', '=', False)]}">
type="object" icon="fa-globe" attrs="{'invisible': [('rating_status', '=', 'no')]}">
<field name="website_published" widget="website_button"/>
</button>
</xpath>