diff --git a/addons/survey/controllers/main.py b/addons/survey/controllers/main.py index 5b085d8181a..398579dca0b 100644 --- a/addons/survey/controllers/main.py +++ b/addons/survey/controllers/main.py @@ -18,21 +18,21 @@ _logger = logging.getLogger(__name__) class Survey(http.Controller): - def _fetch_from_access_token(self, survey_id, access_token): + def _fetch_from_access_token(self, survey_token, answer_token): """ Check that given token matches an answer from the given survey_id. Returns a sudo-ed browse record of survey in order to avoid access rights issues now that access is granted through token. """ - survey_sudo = request.env['survey.survey'].with_context(active_test=False).sudo().browse(survey_id) - if not access_token: + survey_sudo = request.env['survey.survey'].with_context(active_test=False).sudo().search([('access_token', '=', survey_token)]) + if not answer_token: answer_sudo = request.env['survey.user_input'].sudo() else: answer_sudo = request.env['survey.user_input'].sudo().search([ ('survey_id', '=', survey_sudo.id), - ('token', '=', access_token) + ('token', '=', answer_token) ], limit=1) return survey_sudo, answer_sudo - def _check_validity(self, survey_id, access_token, ensure_token=True): + def _check_validity(self, survey_token, answer_token, ensure_token=True): """ Check survey is open and can be taken. This does not checks for security rules, only functional / business rules. It returns a string key allowing further manipulation of validity issues @@ -50,12 +50,12 @@ class Survey(http.Controller): should be enforced or not, depending on the route requesting a token or allowing external world calls; """ - survey_sudo, answer_sudo = self._fetch_from_access_token(survey_id, access_token) + survey_sudo, answer_sudo = self._fetch_from_access_token(survey_token, answer_token) if not survey_sudo.exists(): return 'survey_wrong' - if access_token and not answer_sudo: + if answer_token and not answer_sudo: return 'token_wrong' if not answer_sudo and ensure_token: @@ -81,7 +81,7 @@ class Survey(http.Controller): return True - def _get_access_data(self, survey_id, access_token, ensure_token=True): + def _get_access_data(self, survey_token, answer_token, ensure_token=True): """ Get back data related to survey and user input, given the ID and access token provided by the route. @@ -90,9 +90,9 @@ class Survey(http.Controller): survey_sudo, answer_sudo = request.env['survey.survey'].sudo(), request.env['survey.user_input'].sudo() has_survey_access, can_answer = False, False - validity_code = self._check_validity(survey_id, access_token, ensure_token=ensure_token) + validity_code = self._check_validity(survey_token, answer_token, ensure_token=ensure_token) if validity_code != 'survey_wrong': - survey_sudo, answer_sudo = self._fetch_from_access_token(survey_id, access_token) + survey_sudo, answer_sudo = self._fetch_from_access_token(survey_token, answer_token) try: survey_user = survey_sudo.sudo(request.env.user) survey_user.check_access_rights(self, 'read', raise_exception=True) @@ -128,22 +128,24 @@ class Survey(http.Controller): return werkzeug.utils.redirect("/") - @http.route('/survey/test/', type='http', auth='user', website=True) - def survey_test(self, survey_id, token=None, **kwargs): + @http.route('/survey/test/', type='http', auth='user', website=True) + def survey_test(self, survey_token, **kwargs): """ Test mode for surveys: create a test answer, only for managers or officers testing their surveys """ - survey_sudo = request.env['survey.survey'].sudo().browse(survey_id) + survey_sudo, dummy = self._fetch_from_access_token(survey_token, False) try: answer_sudo = survey_sudo._create_answer(user=request.env.user, test_entry=True) except: return werkzeug.utils.redirect('/') - return request.redirect('/survey/start/%s?%s' % (survey_sudo.id, keep_query('*', token=answer_sudo.token))) + return request.redirect('/survey/start/%s?%s' % (survey_sudo.access_token, keep_query('*', answer_token=answer_sudo.token))) - @http.route('/survey/start/', type='http', auth='public', website=True) - def survey_start(self, survey_id, token=None, email=False, **post): - """ Start a survey by providing a token linked to an answer or generate - a new token if access is allowed """ - access_data = self._get_access_data(survey_id, token, ensure_token=False) + @http.route('/survey/start/', type='http', auth='public', website=True) + def survey_start(self, survey_token, answer_token=None, email=False, **post): + """ Start a survey by providing + * a token linked to a survey; + * a token linked to an answer or generate a new token if access is allowed; + """ + access_data = self._get_access_data(survey_token, answer_token, ensure_token=False) if access_data['validity_code'] is not True: return self._redirect_with_error(access_data, access_data['validity_code']) @@ -165,14 +167,14 @@ class Survey(http.Controller): # Select the right page if answer_sudo.state == 'new': # Intro page - data = {'survey': survey_sudo, 'page': None, 'token': answer_sudo.token, 'test_entry': answer_sudo.test_entry} + data = {'survey': survey_sudo, 'page': None, 'answer': answer_sudo} return request.render('survey.survey_init', data) else: - return request.redirect('/survey/fill/%s/%s' % (survey_sudo.id, answer_sudo.token)) + return request.redirect('/survey/fill/%s/%s' % (survey_sudo.access_token, answer_sudo.token)) - @http.route('/survey/fill//', type='http', auth='public', website=True) - def survey_display_page(self, survey_id, token, prev=None, **post): - access_data = self._get_access_data(survey_id, token, ensure_token=True) + @http.route('/survey/fill//', type='http', auth='public', website=True) + def survey_display_page(self, survey_token, answer_token, prev=None, **post): + access_data = self._get_access_data(survey_token, answer_token, ensure_token=True) if access_data['validity_code'] is not True: return self._redirect_with_error(access_data, access_data['validity_code']) @@ -181,13 +183,12 @@ class Survey(http.Controller): # Select the right page if answer_sudo.state == 'new': # First page page, page_nr, last = survey_sudo.next_page(answer_sudo, 0, go_back=False) - data = {'survey': survey_sudo, 'page': page, 'page_nr': page_nr, 'token': answer_sudo.token, 'test_entry': answer_sudo.test_entry} + data = {'survey': survey_sudo, 'page': page, 'page_nr': page_nr, 'answer': answer_sudo} if last: data.update({'last': True}) return request.render('survey.survey', data) elif answer_sudo.state == 'done': # Display success message return request.render('survey.sfinished', {'survey': survey_sudo, - 'token': token, 'user_input': answer_sudo}) elif answer_sudo.state == 'skip': flag = (True if prev and prev == 'prev' else False) @@ -197,17 +198,17 @@ class Survey(http.Controller): if not page: page, page_nr, last = survey_sudo.next_page(answer_sudo, answer_sudo.last_displayed_page_id.id, go_back=True) - data = {'survey': survey_sudo, 'page': page, 'page_nr': page_nr, 'token': answer_sudo.token, 'test_entry': answer_sudo.test_entry} + data = {'survey': survey_sudo, 'page': page, 'page_nr': page_nr, 'answer': answer_sudo} if last: data.update({'last': True}) return request.render('survey.survey', data) else: return request.render("survey.403", {'survey': survey_sudo}) - @http.route('/survey/prefill//', type='http', auth='public', website=True) - def survey_get_answers(self, survey_id, token, page_id=None, **post): + @http.route('/survey/prefill//', type='http', auth='public', website=True) + def survey_get_answers(self, survey_token, answer_token, page_id=None, **post): """ TDE NOTE: original comment: # AJAX prefilling of a survey -> AJAX / http ?? """ - access_data = self._get_access_data(survey_id, token, ensure_token=True) + access_data = self._get_access_data(survey_token, answer_token, ensure_token=True) if access_data['validity_code'] is not True: return {} @@ -250,10 +251,10 @@ class Survey(http.Controller): _logger.warning("[survey] No answer has been found for question %s marked as non skipped" % answer_tag) return json.dumps(ret, default=str) - @http.route('/survey/scores//', type='http', auth='public', website=True) - def survey_get_scores(self, survey_id, token, page_id=None, **post): + @http.route('/survey/scores//', type='http', auth='public', website=True) + def survey_get_scores(self, survey_id, answer_token, page_id=None, **post): """ TDE NOTE: original comment: # AJAX scores loading for quiz correction mode -> AJAX / http ?? """ - access_data = self._get_access_data(survey_id, token, ensure_token=True) + access_data = self._get_access_data(survey_id, answer_token, ensure_token=True) if access_data['validity_code'] is not True: return {} @@ -266,10 +267,10 @@ class Survey(http.Controller): ret.update({answer.question_id.id: tmp_score + answer.quizz_mark}) return json.dumps(ret) - @http.route('/survey/submit//', type='http', methods=['POST'], auth='public', website=True) - def survey_submit(self, survey_id, token, **post): + @http.route('/survey/submit//', type='http', methods=['POST'], auth='public', website=True) + def survey_submit(self, survey_token, answer_token, **post): """ TDE NOTE: original comment: # AJAX submission of a page -> AJAX / http ?? """ - access_data = self._get_access_data(survey_id, token, ensure_token=True) + access_data = self._get_access_data(survey_token, answer_token, ensure_token=True) if access_data['validity_code'] is not True: return {} @@ -277,14 +278,14 @@ class Survey(http.Controller): page_id = int(post['page_id']) questions = request.env['survey.question'].sudo().search([ - ('survey_id', '=', survey_id), + ('survey_id', '=', survey_sudo.id), ('page_id', '=', page_id) ]) # Answer validation errors = {} for question in questions: - answer_tag = "%s_%s_%s" % (survey_id, page_id, question.id) + answer_tag = "%s_%s_%s" % (survey_sudo.id, page_id, question.id) errors.update(question.validate_question(post, answer_tag)) ret = {} @@ -293,7 +294,7 @@ class Survey(http.Controller): ret['errors'] = errors else: for question in questions: - answer_tag = "%s_%s_%s" % (survey_id, page_id, question.id) + answer_tag = "%s_%s_%s" % (survey_sudo.id, page_id, question.id) request.env['survey.user_input_line'].sudo().save_lines(answer_sudo.id, question, post, answer_tag) go_back = post['button_submit'] == 'previous' @@ -304,16 +305,16 @@ class Survey(http.Controller): else: vals.update({'state': 'skip'}) answer_sudo.write(vals) - ret['redirect'] = '/survey/fill/%s/%s' % (survey_sudo.id, token) + ret['redirect'] = '/survey/fill/%s/%s' % (survey_sudo.access_token, answer_token) if go_back: ret['redirect'] += '?prev=prev' return json.dumps(ret) - @http.route('/survey/print/', type='http', auth='public', website=True) - def survey_print(self, survey_id, token=None, **post): - '''Display an survey in printable view; if is set, it will - grab the answers of the user_input_id that has .''' - access_data = self._get_access_data(survey_id, token, ensure_token=False) + @http.route('/survey/print/', type='http', auth='public', website=True) + def survey_print(self, survey_token, answer_token=None, **post): + '''Display an survey in printable view; if is set, it will + grab the answers of the user_input_id that has .''' + access_data = self._get_access_data(survey_token, answer_token, ensure_token=False) if access_data['validity_code'] is not True and ( not access_data['has_survey_access'] or access_data['validity_code'] not in ['token_required', 'survey_closed', 'survey_void']): return self._redirect_with_error(access_data, access_data['validity_code']) @@ -322,13 +323,12 @@ class Survey(http.Controller): return request.render('survey.survey_print', { 'survey': survey_sudo, - 'token': token, - 'test_entry': answer_sudo.test_entry, + 'answer': answer_sudo, 'page_nr': 0, - 'quizz_correction': True if survey_sudo.quizz_mode and token else False}) + }) @http.route('/survey/results/', type='http', auth='user', website=True) - def survey_report(self, survey, token=None, **post): + def survey_report(self, survey, answer_token=None, **post): '''Display survey Results & Statistics for given survey.''' result_template = 'survey.result' current_filters = [] diff --git a/addons/survey/data/mail_template_data.xml b/addons/survey/data/mail_template_data.xml index 84d8ea69358..ededcfb9bcc 100644 --- a/addons/survey/data/mail_template_data.xml +++ b/addons/survey/data/mail_template_data.xml @@ -11,7 +11,7 @@ Dear ${object.partner_id.name or 'participant'}

We are conducting a survey and your response would be appreciated.
- Start Survey diff --git a/addons/survey/data/survey_demo_feedback.xml b/addons/survey/data/survey_demo_feedback.xml index 8d24117ab47..39fb5f77a47 100644 --- a/addons/survey/data/survey_demo_feedback.xml +++ b/addons/survey/data/survey_demo_feedback.xml @@ -3,6 +3,7 @@ User Feedback Form + b137640d-14d4-4748-9ef6-344ca256531e public diff --git a/addons/survey/models/survey_survey.py b/addons/survey/models/survey_survey.py index 1a893011c3f..3c60d6c343a 100644 --- a/addons/survey/models/survey_survey.py +++ b/addons/survey/models/survey_survey.py @@ -1,12 +1,13 @@ # -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. +import uuid + from collections import Counter, OrderedDict from itertools import product from werkzeug import urls from odoo import api, fields, models, SUPERUSER_ID, _ -from odoo.addons.http_routing.models.ir_http import slug from odoo.exceptions import UserError from odoo.osv import expression @@ -19,9 +20,12 @@ class Survey(models.Model): _rec_name = 'title' _inherit = ['mail.thread', 'mail.activity.mixin'] - def _default_stage(self): + def _get_default_stage_id(self): return self.env['survey.stage'].search([], limit=1).id + def _get_default_access_token(self): + return str(uuid.uuid4()) + # description title = fields.Char('Title', required=True, translate=True) description = fields.Html("Description", translate=True, help="A long description of the purpose of the survey") @@ -29,7 +33,7 @@ class Survey(models.Model): thank_you_message = fields.Html("Thanks Message", translate=True, help="This message will be displayed when survey is completed") quizz_mode = fields.Boolean("Quizz Mode") active = fields.Boolean("Active", default=True) - stage_id = fields.Many2one('survey.stage', string="Stage", default=_default_stage, + stage_id = fields.Many2one('survey.stage', string="Stage", default=lambda self: self._get_default_stage_id(), ondelete="restrict", copy=False, group_expand='_read_group_stage_ids') is_closed = fields.Boolean("Is closed", related='stage_id.closed', readonly=True) category = fields.Selection([ @@ -46,6 +50,7 @@ class Survey(models.Model): ('internal', 'Employees Only'), ('token', 'Invitation only')], string='Access Mode', default='authentication', required=True) + access_token = fields.Char('Access Token', default=lambda self: self._get_default_access_token()) users_can_go_back = fields.Boolean('Users can go back', help="If checked, users can go back to previous pages.") users_can_signup = fields.Boolean('Users can signup', compute='_compute_users_can_signup') public_url = fields.Char("Public link", compute="_compute_survey_url") @@ -54,6 +59,10 @@ class Survey(models.Model): tot_start_survey = fields.Integer("Number of started surveys", compute="_compute_survey_statistic") tot_comp_survey = fields.Integer("Number of completed surveys", compute="_compute_survey_statistic") + _sql_constraints = [ + ('access_token_unique', 'unique(access_token)', 'Access token should be unique') + ] + @api.multi def _compute_users_can_signup(self): signup_allowed = self.env['res.users'].sudo()._get_signup_invitation_scope() == 'b2c' @@ -78,7 +87,7 @@ class Survey(models.Model): """ Computes a public URL for the survey """ base_url = self.env['ir.config_parameter'].sudo().get_param('web.base.url') for survey in self: - survey.public_url = urls.url_join(base_url, "survey/start/%s" % (survey.id)) + survey.public_url = urls.url_join(base_url, "survey/start/%s" % (survey.access_token)) @api.model def _read_group_stage_ids(self, stages, domain, order): @@ -326,7 +335,7 @@ class Survey(models.Model): """ Open the website page with the survey form """ self.ensure_one() token = self.env.context.get('survey_token') - trail = "?token=%s" % token if token else "" + trail = "?answer_token=%s" % token if token else "" return { 'type': 'ir.actions.act_url', 'name': "Start Survey", @@ -367,12 +376,12 @@ class Survey(models.Model): """ Open the website page with the survey printable view """ self.ensure_one() token = self.env.context.get('survey_token') - trail = "?token=%s" % token if token else "" + trail = "?answer_token=%s" % token if token else "" return { 'type': 'ir.actions.act_url', 'name': "Print Survey", 'target': 'self', - 'url': '/survey/print/%s%s' % (self.id, trail) + 'url': '/survey/print/%s%s' % (self.access_token, trail) } @api.multi @@ -394,7 +403,7 @@ class Survey(models.Model): 'type': 'ir.actions.act_url', 'name': "Test Survey", 'target': 'self', - 'url': '/survey/test/%s' % self.id, + 'url': '/survey/test/%s' % self.access_token, } @api.multi diff --git a/addons/survey/models/survey_user.py b/addons/survey/models/survey_user.py index 1bdcba0c1c2..35ae87c7c47 100644 --- a/addons/survey/models/survey_user.py +++ b/addons/survey/models/survey_user.py @@ -92,7 +92,7 @@ class SurveyUserInput(models.Model): 'type': 'ir.actions.act_url', 'name': "View Answers", 'target': 'self', - 'url': '/survey/print/%s?token=%s' % (self.survey_id.id, self.token) + 'url': '/survey/print/%s?answer_token=%s' % (self.survey_id.access_token, self.token) } diff --git a/addons/survey/static/src/js/tour_test_survey.js b/addons/survey/static/src/js/tour_test_survey.js index 8b2425d6be4..2ce01b8dc41 100644 --- a/addons/survey/static/src/js/tour_test_survey.js +++ b/addons/survey/static/src/js/tour_test_survey.js @@ -5,7 +5,7 @@ var tour = require('web_tour.tour'); tour.register('test_survey', { test: true, - url: '/survey/start/1', + url: '/survey/start/b137640d-14d4-4748-9ef6-344ca256531e', }, [ // Page-1 { diff --git a/addons/survey/tests/test_survey_flow.py b/addons/survey/tests/test_survey_flow.py index 11d9ed2a57c..396b56f32b9 100644 --- a/addons/survey/tests/test_survey_flow.py +++ b/addons/survey/tests/test_survey_flow.py @@ -12,13 +12,13 @@ from odoo.tests.common import HttpCase class TestSurveyFlow(common.SurveyCase, HttpCase): def _access_start(self, survey): - return self.url_open('/survey/start/%s' % survey.id) + return self.url_open('/survey/start/%s' % survey.access_token) def _access_page(self, survey, token): - return self.url_open('/survey/fill/%s/%s' % (survey.id, token)) + return self.url_open('/survey/fill/%s/%s' % (survey.access_token, token)) def _access_submit(self, survey, token, post_data): - return self.url_open('/survey/submit/%s/%s' % (survey.id, token), data=post_data) + return self.url_open('/survey/submit/%s/%s' % (survey.access_token, token), data=post_data) def _find_csrf_token(self, text): csrf_token_re = re.compile("(input.+csrf_token.+value=\")([_a-zA-Z0-9]{51})", re.MULTILINE) diff --git a/addons/survey/tests/test_survey_ui.py b/addons/survey/tests/test_survey_ui.py index 5005ac71bcf..adcf821c396 100644 --- a/addons/survey/tests/test_survey_ui.py +++ b/addons/survey/tests/test_survey_ui.py @@ -6,10 +6,13 @@ import odoo.tests class TestUi(odoo.tests.HttpCase): def test_01_admin_survey_tour(self): - self.phantom_js("/survey/start/1", "odoo.__DEBUG__.services['web_tour.tour'].run('test_survey')", "odoo.__DEBUG__.services['web_tour.tour'].tours.test_survey.ready", login="admin") + access_token = self.env.ref('survey.survey_feedback').access_token + self.phantom_js("/survey/start/%s" % access_token, "odoo.__DEBUG__.services['web_tour.tour'].run('test_survey')", "odoo.__DEBUG__.services['web_tour.tour'].tours.test_survey.ready", login="admin") def test_02_demo_survey_tour(self): - self.phantom_js("/survey/start/1", "odoo.__DEBUG__.services['web_tour.tour'].run('test_survey')", "odoo.__DEBUG__.services['web_tour.tour'].tours.test_survey.ready", login="demo") + access_token = self.env.ref('survey.survey_feedback').access_token + self.phantom_js("/survey/start/%s" % access_token, "odoo.__DEBUG__.services['web_tour.tour'].run('test_survey')", "odoo.__DEBUG__.services['web_tour.tour'].tours.test_survey.ready", login="demo") def test_03_public_survey_tour(self): - self.phantom_js("/survey/start/1", "odoo.__DEBUG__.services['web_tour.tour'].run('test_survey')", "odoo.__DEBUG__.services['web_tour.tour'].tours.test_survey.ready") + access_token = self.env.ref('survey.survey_feedback').access_token + self.phantom_js("/survey/start/%s" % access_token, "odoo.__DEBUG__.services['web_tour.tour'].run('test_survey')", "odoo.__DEBUG__.services['web_tour.tour'].tours.test_survey.ready") diff --git a/addons/survey/views/survey_templates.xml b/addons/survey/views/survey_templates.xml index 1aca736513a..db9f9c0480d 100644 --- a/addons/survey/views/survey_templates.xml +++ b/addons/survey/views/survey_templates.xml @@ -29,7 +29,7 @@

Thank you!

You scored points.
-
If you wish, you can review your answers.
+
If you wish, you can review your answers.
@@ -58,7 +58,7 @@

Login required

This survey is open only to registered people. Please - log in. + log in.

@@ -140,14 +140,14 @@
-
+
This is a test survey. Your answer will not be registered in the answers.

@@ -174,7 +174,7 @@