From ee2631cd514d8339d40dbab38b714371c428ff7b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thibault=20Delavall=C3=A9e?= Date: Fri, 30 Nov 2018 11:08:45 +0000 Subject: [PATCH 01/15] [REF] survey: clean survey routes definitions Purpose of this commit is to ease understanding or routes. In this commit we make token a parameter where it is optional and force is where it is not; we remove unnecessary list on routes; we rename some controller methods to ease understanding; Code calling those URLs is adapted accordingly. No functional change should occur with this commit. Some routes are json-http-mixed as a result of old implementation. Those will be updated in future tasks when cleaning the templates and routes. This commit is linked to task ID 1911586 and PR #28986. --- addons/survey/controllers/main.py | 87 ++++++++++-------------- addons/survey/models/survey_survey.py | 4 +- addons/survey/models/survey_user.py | 2 +- addons/survey/tests/test_survey_flow.py | 8 +-- addons/survey/views/survey_templates.xml | 24 ++++--- 5 files changed, 59 insertions(+), 66 deletions(-) diff --git a/addons/survey/controllers/main.py b/addons/survey/controllers/main.py index 80ee03d0dcf..28a5ff721a7 100644 --- a/addons/survey/controllers/main.py +++ b/addons/survey/controllers/main.py @@ -58,13 +58,11 @@ class Survey(http.Controller): 'survey_id': survey.id, 'test_entry': True }) - return request.redirect('/survey/start/%s/%s' % (survey.id, user_input.token)) + return request.redirect('/survey/start/%s?token=%s' % (survey.id, user_input.token)) return werkzeug.utils.redirect('/') - @http.route(['/survey/start/', - '/survey/start//'], - type='http', auth='public', website=True) - def survey_start(self, survey, token=None, **post): + @http.route('/survey/start/', type='http', auth='public', website=True) + def survey_start(self, survey, token=None): """ Start a survey by providing a token linked to an answer or generate a new token if access is allowed """ UserInput = request.env['survey.user_input'] @@ -97,12 +95,8 @@ class Survey(http.Controller): else: return request.redirect('/survey/fill/%s/%s' % (survey.id, user_input.token)) - # Survey displaying - @http.route(['/survey/fill//', - '/survey/fill///'], - type='http', auth='public', website=True) - def fill_survey(self, survey, token, prev=None, **post): - '''Display and validates a survey''' + @http.route('/survey/fill//', type='http', auth='public', website=True) + def survey_display_page(self, survey, token, prev=None): Survey = request.env['survey.survey'] UserInput = request.env['survey.user_input'] @@ -131,8 +125,8 @@ class Survey(http.Controller): return request.render('survey.survey', data) elif user_input.state == 'done': # Display success message return request.render('survey.sfinished', {'survey': survey, - 'token': token, - 'user_input': user_input}) + 'token': token, + 'user_input': user_input}) elif user_input.state == 'skip': flag = (True if prev and prev == 'prev' else False) page, page_nr, last = Survey.next_page(user_input, user_input.last_displayed_page_id.id, go_back=flag) @@ -148,17 +142,15 @@ class Survey(http.Controller): else: return request.render("survey.403", {'survey': survey}) - # AJAX prefilling of a survey - @http.route(['/survey/prefill//', - '/survey/prefill///'], - type='http', auth='public', website=True) - def prefill(self, survey, token, page=None, **post): + @http.route('/survey/prefill//', type='http', auth='public', website=True) + def survey_get_answers(self, survey, token, page_id=None): + """ TDE NOTE: original comment: # AJAX prefilling of a survey -> AJAX / http ?? """ UserInputLine = request.env['survey.user_input_line'] ret = {} # Fetch previous answers - if page: - previous_answers = UserInputLine.sudo().search([('user_input_id.token', '=', token), ('page_id', '=', page.id)]) + if page_id: + previous_answers = UserInputLine.sudo().search([('user_input_id.token', '=', token), ('page_id', '=', page_id)]) else: previous_answers = UserInputLine.sudo().search([('user_input_id.token', '=', token)]) @@ -190,10 +182,9 @@ 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) - # AJAX scores loading for quiz correction mode - @http.route(['/survey/scores//'], - type='http', auth='public', website=True) - def get_scores(self, survey, token, page=None, **post): + @http.route('/survey/scores//', type='http', auth='public', website=True) + def survey_get_scores(self, survey, token, page_id=None): + """ TDE NOTE: original comment: # AJAX scores loading for quiz correction mode -> AJAX / http ?? """ ret = {} # Fetch answers @@ -205,9 +196,9 @@ class Survey(http.Controller): ret.update({answer.question_id.id: tmp_score + answer.quizz_mark}) return json.dumps(ret) - # AJAX submission of a page - @http.route(['/survey/submit/'], type='http', methods=['POST'], auth='public', website=True) - def submit(self, survey, **post): + @http.route('/survey/submit//', type='http', methods=['POST'], auth='public', website=True) + def survey_submit(self, survey, token, **post): + """ TDE NOTE: original comment: # AJAX submission of a page -> AJAX / http ?? """ _logger.debug('Incoming data: %s', post) page_id = int(post['page_id']) questions = request.env['survey.question'].search([('page_id', '=', page_id)]) @@ -225,7 +216,7 @@ class Survey(http.Controller): else: # Store answers into database try: - user_input = request.env['survey.user_input'].sudo().search([('token', '=', post['token'])], limit=1) + user_input = request.env['survey.user_input'].sudo().search([('token', '=', token)], limit=1) except KeyError: # Invalid token return request.render("survey.403", {'survey': survey}) user_id = request.env.user.id if user_input.input_type != 'link' else SUPERUSER_ID @@ -242,27 +233,23 @@ class Survey(http.Controller): else: vals.update({'state': 'skip'}) user_input.sudo(user=user_id).write(vals) - ret['redirect'] = '/survey/fill/%s/%s' % (survey.id, post['token']) + ret['redirect'] = '/survey/fill/%s/%s' % (survey.id, token) if go_back: - ret['redirect'] += '/prev' + ret['redirect'] += '?prev=prev' return json.dumps(ret) - # Printing routes - @http.route(['/survey/print/', - '/survey/print//'], - type='http', auth='public', website=True) - def print_survey(self, survey, token=None, **post): + @http.route('/survey/print/', type='http', auth='public', website=True) + def survey_print(self, survey, token=None, **post): '''Display an survey in printable view; if is set, it will grab the answers of the user_input_id that has .''' - return request.render('survey.survey_print', - {'survey': survey, - 'token': token, - 'page_nr': 0, - 'quizz_correction': True if survey.quizz_mode and token else False}) + return request.render('survey.survey_print', { + 'survey': survey, + 'token': token, + 'page_nr': 0, + 'quizz_correction': True if survey.quizz_mode and token else False}) - @http.route(['/survey/results/'], - type='http', auth='user', website=True) - def survey_reporting(self, survey, token=None, **post): + @http.route('/survey/results/', type='http', auth='user', website=True) + def survey_report(self, survey, token=None, **post): '''Display survey Results & Statistics for given survey.''' result_template = 'survey.result' current_filters = [] @@ -275,12 +262,12 @@ class Survey(http.Controller): post.pop('finished') filter_finish = True if post or filter_finish: - filter_data = self.get_filter_data(post) + filter_data = self._get_filter_data(post) current_filters = survey.filter_input_ids(filter_data, filter_finish) filter_display_data = survey.get_filter_display_data(filter_data) return request.render(result_template, {'survey': survey, - 'survey_dict': self.prepare_result_dict(survey, current_filters), + 'survey_dict': self._prepare_result_dict(survey, current_filters), 'page_range': self.page_range, 'current_filters': current_filters, 'filter_display_data': filter_display_data, @@ -324,7 +311,7 @@ class Survey(http.Controller): # filter_finish: boolean => only finished surveys or not # - def prepare_result_dict(self, survey, current_filters=None): + def _prepare_result_dict(self, survey, current_filters=None): """Returns dictionary having values for rendering template""" current_filters = current_filters if current_filters else [] Survey = request.env['survey.survey'] @@ -336,14 +323,14 @@ class Survey(http.Controller): 'question': question, 'input_summary': Survey.get_input_summary(question, current_filters), 'prepare_result': Survey.prepare_result(question, current_filters), - 'graph_data': self.get_graph_data(question, current_filters), + 'graph_data': self._get_graph_data(question, current_filters), } page_dict['question_ids'].append(question_dict) result['page_ids'].append(page_dict) return result - def get_filter_data(self, post): + def _get_filter_data(self, post): """Returns data used for filtering the result""" filters = [] for ids in post: @@ -360,9 +347,9 @@ class Survey(http.Controller): total = ceil(total_record / float(limit)) return range(1, int(total + 1)) - def get_graph_data(self, question, current_filters=None): + def _get_graph_data(self, question, current_filters=None): '''Returns formatted data required by graph library on basis of filter''' - # TODO refactor this terrible method and merge it with prepare_result_dict + # TODO refactor this terrible method and merge it with _prepare_result_dict current_filters = current_filters if current_filters else [] Survey = request.env['survey.survey'] result = [] diff --git a/addons/survey/models/survey_survey.py b/addons/survey/models/survey_survey.py index 01e0b7b8eec..2e8e90b48fa 100644 --- a/addons/survey/models/survey_survey.py +++ b/addons/survey/models/survey_survey.py @@ -268,7 +268,7 @@ class Survey(models.Model): """ Open the website page with the survey form """ self.ensure_one() token = self.env.context.get('survey_token') - trail = "/%s" % token if token else "" + trail = "?token=%s" % token if token else "" return { 'type': 'ir.actions.act_url', 'name': "Start Survey", @@ -312,7 +312,7 @@ 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 if token else "" + trail = "?token=%s" % token if token else "" return { 'type': 'ir.actions.act_url', 'name': "Print Survey", diff --git a/addons/survey/models/survey_user.py b/addons/survey/models/survey_user.py index 32250825bd3..4efe74a9c43 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': '%s/%s' % (self.print_url, self.token) + 'url': '%s?token=%s' % (self.print_url, self.token) } @api.multi diff --git a/addons/survey/tests/test_survey_flow.py b/addons/survey/tests/test_survey_flow.py index 12230d49194..537ab1d46d4 100644 --- a/addons/survey/tests/test_survey_flow.py +++ b/addons/survey/tests/test_survey_flow.py @@ -17,8 +17,8 @@ class TestSurveyFlow(common.SurveyCase, HttpCase): def _access_page(self, survey, token): return self.url_open('/survey/fill/%s/%s' % (survey.id, token)) - def _access_submit(self, survey, post_data): - return self.url_open('/survey/submit/%s' % survey.id, data=post_data) + def _access_submit(self, survey, token, post_data): + return self.url_open('/survey/submit/%s/%s' % (survey.id, 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) @@ -108,7 +108,7 @@ class TestSurveyFlow(common.SurveyCase, HttpCase): page0_q1.id: {'value': [44.0]}, } post_data = self._format_submission_data(page_0, answer_data, {'csrf_token': csrf_token, 'token': answer_token, 'button_submit': 'next'}) - r = self._access_submit(survey, post_data) + r = self._access_submit(survey, answer_token, post_data) self.assertResponse(r, 200) answers.invalidate_cache() # TDE note: necessary as lots of sudo in controllers messing with cache @@ -127,7 +127,7 @@ class TestSurveyFlow(common.SurveyCase, HttpCase): page1_q0.id: {'value': [page1_q0.labels_ids.ids[0], page1_q0.labels_ids.ids[1]]}, } post_data = self._format_submission_data(page_1, answer_data, {'csrf_token': csrf_token, 'token': answer_token, 'button_submit': 'next'}) - r = self._access_submit(survey, post_data) + r = self._access_submit(survey, answer_token, post_data) self.assertResponse(r, 200) answers.invalidate_cache() # TDE note: necessary as lots of sudo in controllers messing with cache diff --git a/addons/survey/views/survey_templates.xml b/addons/survey/views/survey_templates.xml index 203c7f77d3b..2b8149d37f0 100644 --- a/addons/survey/views/survey_templates.xml +++ b/addons/survey/views/survey_templates.xml @@ -26,7 +26,7 @@

Thank you!

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

Login required

-

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

+

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

@@ -167,7 +169,11 @@
-
+ @@ -455,20 +461,20 @@ - + -
+
-
+
-
+
@@ -489,7 +495,7 @@ - +
@@ -673,7 +679,7 @@ - +
From b74252dd1d2a38aae0ee969f443fac342e8e9f74 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thibault=20Delavall=C3=A9e?= Date: Tue, 4 Dec 2018 12:01:45 +0000 Subject: [PATCH 02/15] [REF] survey: clean models by removing unnecessary fields This commit holds several improvements on survey models. [IMP] re-order fields in survey models and update strings / helps Purpose is to ease understanding and future modifications of survey models by reordering fields in survey, question and user input models. Those are the main models we want to clean in future updates. Some comments have been moved into help and some labels and helps have been updated to clarify a bit the use of various fields, notably in question model. No functional or technical change occur with this commit. It is just pure code move. Reordering them allow to already understand a bit more the logic of this module. [REF] survey: remove unnecessary/unused fields on answer models Purpose of this commit is to remove unnecessary and/or unused fields on survey answer line model. In this commit we * remove useless date_create field on answer and answer lines models. This field is basically a renamed create_date. Let us remove this dummy field as it adds nothing to the answer and answer lines models; * remove useless URL-related fields and actions on answer and answer lines models. Having those fields actually bloats the model without really helping understanding survey models. As changing routes should not occur frequently hiding routes in fields does not really help. Templates are update to directly include links instead of fields; We also remove a button on user input that redirects to the survey global results. Surveys results are available on survey kanban and form views. It makes no sense to have that kind of button on a specific answer. [REF] survey: remove unnecessary/unused fields on survey models Purpose of this commit is to remove unnecessary and/or unused fields on survey model. In this commit we * remove useless is_designed field on survey model. We remove is_designed field that is based on having page and/or questions. It is just used to make some links inactive in kanban view. Having fields just for that purpose bloats the model and is not necessary. Testing void surveys will just lead to a void page; * remove useless email_template_id field on survey model. This field is completely not used anywhere in survey codebase; * remove useless urls field in survey model. In this commit we remove support of both relative and global urls computation. It was used only in invite wizard but was actually not really necessary as knowing the survey access url is the only really necessary thing to display; * Print and results URLs are removed from survey model. Having them inlined directly in code is simpler that actually hiding them behind field. As changing routes should not occur often it is not a big deal to write them when necessary. We therefore remove public_url_html, print_url and result_url fields; This commit is linked to task ID 1911586 and PR #28986. --- addons/survey/models/survey_page.py | 12 +-- addons/survey/models/survey_question.py | 95 ++++++++------------ addons/survey/models/survey_stage.py | 2 - addons/survey/models/survey_survey.py | 56 +++++------- addons/survey/models/survey_user.py | 48 ++++------ addons/survey/tests/test_survey.py | 16 ---- addons/survey/views/survey_survey_views.xml | 8 +- addons/survey/views/survey_templates.xml | 12 +-- addons/survey/views/survey_user_views.xml | 17 ++-- addons/survey/wizard/survey_invite.py | 3 - addons/survey/wizard/survey_invite_views.xml | 2 - 11 files changed, 93 insertions(+), 178 deletions(-) diff --git a/addons/survey/models/survey_page.py b/addons/survey/models/survey_page.py index b518ff01f16..bda0f551dbf 100644 --- a/addons/survey/models/survey_page.py +++ b/addons/survey/models/survey_page.py @@ -5,21 +5,13 @@ from odoo import fields, models class SurveyPage(models.Model): - """ A page for a survey. - - Pages are essentially containers, allowing to group questions by ordered - screens. - - .. note:: - A page should be deleted if the survey it belongs to is deleted. - """ + """ A page for a survey. Pages are essentially containers, allowing to group questions by ordered + screens. """ _name = 'survey.page' _description = 'Survey Page' _rec_name = 'title' _order = 'sequence,id' - # Model Fields # - title = fields.Char('Page Title', required=True, translate=True) survey_id = fields.Many2one('survey.survey', string='Survey', ondelete='cascade', required=True) question_ids = fields.One2many('survey.question', 'page_id', string='Questions', copy=True) diff --git a/addons/survey/models/survey_question.py b/addons/survey/models/survey_question.py index 3f207c2719e..b018a7a8b5a 100644 --- a/addons/survey/models/survey_question.py +++ b/addons/survey/models/survey_question.py @@ -20,72 +20,56 @@ def dict_keys_startswith(dictionary, string): class SurveyQuestion(models.Model): - """ Questions that will be asked in a survey. - - Each question can have one of more suggested answers (eg. in case of - dropdown choices, multi-answer checkboxes, radio buttons...). - """ - + """ Questions that will be asked in a survey. Each question can have one of more suggested answers (eg. in case of + dropdown choices, multi-answer checkboxes, radio buttons...). """ _name = 'survey.question' _description = 'Survey Question' _rec_name = 'question' _order = 'sequence,id' - # Model fields # - # Question metadata - page_id = fields.Many2one('survey.page', string='Survey page', - ondelete='cascade', required=True, default=lambda self: self.env.context.get('page_id')) + page_id = fields.Many2one( + 'survey.page', string='Survey page', + ondelete='cascade', required=True, default=lambda self: self.env.context.get('page_id')) survey_id = fields.Many2one('survey.survey', related='page_id.survey_id', string='Survey', readonly=False) sequence = fields.Integer('Sequence', default=10) - # Question question = fields.Char('Question Name', required=True, translate=True) - description = fields.Html('Description', help="Use this field to add \ - additional explanations about your question", translate=True, - oldname='descriptive_text') - - # Answer + description = fields.Html('Description', help="Use this field to add additional explanations about your question", translate=True) question_type = fields.Selection([ - ('free_text', 'Multiple Lines Text Box'), - ('textbox', 'Single Line Text Box'), - ('numerical_box', 'Numerical Value'), - ('date', 'Date'), - ('simple_choice', 'Multiple choice: only one answer'), - ('multiple_choice', 'Multiple choice: multiple answers allowed'), - ('matrix', 'Matrix')], string='Type of Question', default='free_text', required=True, oldname='type') - matrix_subtype = fields.Selection([('simple', 'One choice per row'), + ('free_text', 'Multiple Lines Text Box'), + ('textbox', 'Single Line Text Box'), + ('numerical_box', 'Numerical Value'), + ('date', 'Date'), + ('simple_choice', 'Multiple choice: only one answer'), + ('multiple_choice', 'Multiple choice: multiple answers allowed'), + ('matrix', 'Matrix')], string='Type of Question', + default='free_text', required=True, oldname='type') + # simple choice / multiple choice / matrix + labels_ids = fields.One2many( + 'survey.label', 'question_id', string='Types of answers', copy=True, + help='Labels used for proposed choices: simple choice, multiple choice and columns of matrix') + # matrix + matrix_subtype = fields.Selection([ + ('simple', 'One choice per row'), ('multiple', 'Multiple choices per row')], string='Matrix Type', default='simple') - labels_ids = fields.One2many('survey.label', 'question_id', string='Types of answers', oldname='answer_choice_ids', copy=True) - labels_ids_2 = fields.One2many('survey.label', 'question_id_2', string='Rows of the Matrix', copy=True) - # labels are used for proposed choices - # if question.type == simple choice | multiple choice - # -> only labels_ids is used - # if question.type == matrix - # -> labels_ids are the columns of the matrix - # -> labels_ids_2 are the rows of the matrix - + labels_ids_2 = fields.One2many( + 'survey.label', 'question_id_2', string='Rows of the Matrix', copy=True, + help='Labels used for proposed choices: rows of matrix') # Display options - column_nb = fields.Selection([('12', '1'), - ('6', '2'), - ('4', '3'), - ('3', '4'), - ('2', '6')], - 'Number of columns', default='12') - # These options refer to col-xx-[12|6|4|3|2] classes in Bootstrap - display_mode = fields.Selection([('columns', 'Radio Buttons'), - ('dropdown', 'Selection Box')], - default='columns') - + column_nb = fields.Selection([ + ('12', '1'), ('6', '2'), ('4', '3'), ('3', '4'), ('2', '6')], + string='Number of columns', default='12', + help='These options refer to col-xx-[12|6|4|3|2] classes in Bootstrap for dropdown-based simple and multiple choice questions.') + display_mode = fields.Selection( + [('columns', 'Radio Buttons'), ('dropdown', 'Selection Box')], + string='Display Mode', default='columns', help='Display mode of simple choice questions.') # Comments - comments_allowed = fields.Boolean('Show Comments Field', - oldname="allow_comment") + comments_allowed = fields.Boolean('Show Comments Field') comments_message = fields.Char('Comment Message', translate=True, default=lambda self: _("If other, please specify:")) - comment_count_as_answer = fields.Boolean('Comment Field is an Answer Choice', - oldname='make_comment_field') - + comment_count_as_answer = fields.Boolean('Comment Field is an Answer Choice') # Validation - validation_required = fields.Boolean('Validate entry', oldname='is_validation_require') + validation_required = fields.Boolean('Validate entry') validation_email = fields.Boolean('Input must be an email') validation_length_min = fields.Integer('Minimum Text Length') validation_length_max = fields.Integer('Maximum Text Length') @@ -93,12 +77,11 @@ class SurveyQuestion(models.Model): validation_max_float_value = fields.Float('Maximum value') validation_min_date = fields.Date('Minimum Date') validation_max_date = fields.Date('Maximum Date') - validation_error_msg = fields.Char('Validation Error message', oldname='validation_valid_err_msg', - translate=True, default=lambda self: _("The answer you entered has an invalid format.")) - + validation_error_msg = fields.Char('Validation Error message', translate=True, default=lambda self: _("The answer you entered has an invalid format.")) # Constraints on number of answers (matrices) - constr_mandatory = fields.Boolean('Mandatory Answer', oldname="is_require_answer") - constr_error_msg = fields.Char('Error message', oldname='req_error_msg', translate=True, default=lambda self: _("This question requires an answer.")) + constr_mandatory = fields.Boolean('Mandatory Answer') + constr_error_msg = fields.Char('Error message', translate=True, default=lambda self: _("This question requires an answer.")) + # Answer user_input_line_ids = fields.One2many('survey.user_input_line', 'question_id', string='Answers', domain=[('skipped', '=', False)]) _sql_constraints = [ @@ -277,9 +260,9 @@ class SurveyQuestion(models.Model): errors.update({answer_tag: self.constr_error_msg}) return errors + class SurveyLabel(models.Model): """ A suggested answer for a question """ - _name = 'survey.label' _rec_name = 'value' _order = 'sequence,id' diff --git a/addons/survey/models/survey_stage.py b/addons/survey/models/survey_stage.py index b73e28fbc51..f2d415340fd 100644 --- a/addons/survey/models/survey_stage.py +++ b/addons/survey/models/survey_stage.py @@ -5,8 +5,6 @@ from odoo import fields, models class SurveyStage(models.Model): - """Stages for Kanban view of surveys""" - _name = 'survey.stage' _description = 'Survey Stage' _order = 'sequence,id' diff --git a/addons/survey/models/survey_survey.py b/addons/survey/models/survey_survey.py index 2e8e90b48fa..a88cec640bd 100644 --- a/addons/survey/models/survey_survey.py +++ b/addons/survey/models/survey_survey.py @@ -11,11 +11,8 @@ from odoo.exceptions import UserError class Survey(models.Model): - """ Settings for a multi-page/multi-question survey. - Each survey can have one or more attached pages, and each page can display - one or more questions. - """ - + """ Settings for a multi-page/multi-question survey. Each survey can have one or more attached pages + and each page can display one or more questions. """ _name = 'survey.survey' _description = 'Survey' _rec_name = 'title' @@ -24,36 +21,27 @@ class Survey(models.Model): def _default_stage(self): return self.env['survey.stage'].search([], limit=1).id + # description title = fields.Char('Title', required=True, translate=True) - page_ids = fields.One2many('survey.page', 'survey_id', string='Pages', copy=True) - stage_id = fields.Many2one('survey.stage', string="Stage", default=_default_stage, - ondelete="restrict", copy=False, group_expand='_read_group_stage_ids') - auth_required = fields.Boolean('Login required', help="Users with a public link will be requested to login before taking part to the survey", - oldname="authenticate") - users_can_go_back = fields.Boolean('Users can go back', help="If checked, users can go back to previous pages.") - tot_sent_survey = fields.Integer("Number of sent surveys", compute="_compute_survey_statistic") - 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") description = fields.Html("Description", translate=True, help="A long description of the purpose of the survey") color = fields.Integer('Color Index', default=0) - user_input_ids = fields.One2many('survey.user_input', 'survey_id', string='User responses', readonly=True) - designed = fields.Boolean("Is designed?", compute="_is_designed") - public_url = fields.Char("Public link", compute="_compute_survey_url") - public_url_html = fields.Char("Public link (html version)", compute="_compute_survey_url") - print_url = fields.Char("Print link", compute="_compute_survey_url") - result_url = fields.Char("Results link", compute="_compute_survey_url") - email_template_id = fields.Many2one('mail.template', string='Email Template', ondelete='set null') 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, + ondelete="restrict", copy=False, group_expand='_read_group_stage_ids') is_closed = fields.Boolean("Is closed", related='stage_id.closed', readonly=False) - - def _is_designed(self): - for survey in self: - if not survey.page_ids or not [page.question_ids for page in survey.page_ids if page.question_ids]: - survey.designed = False - else: - survey.designed = True + # content + page_ids = fields.One2many('survey.page', 'survey_id', string='Pages', copy=True) + user_input_ids = fields.One2many('survey.user_input', 'survey_id', string='User responses', readonly=True) + # security / access + auth_required = fields.Boolean('Login required', help="Users with a public link will be requested to login before taking part to the survey") + users_can_go_back = fields.Boolean('Users can go back', help="If checked, users can go back to previous pages.") + public_url = fields.Char("Public link", compute="_compute_survey_url") + # statistics + tot_sent_survey = fields.Integer("Number of sent surveys", compute="_compute_survey_statistic") + 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") @api.multi def _compute_survey_statistic(self): @@ -70,13 +58,9 @@ class Survey(models.Model): def _compute_survey_url(self): """ Computes a public URL for the survey """ - base_url = '/' if self.env.context.get('relative_url') else \ - self.env['ir.config_parameter'].sudo().get_param('web.base.url') + 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" % (slug(survey))) - survey.print_url = urls.url_join(base_url, "survey/print/%s" % (slug(survey))) - survey.result_url = urls.url_join(base_url, "survey/results/%s" % (slug(survey))) - survey.public_url_html = '%s' % (survey.public_url, _("Click here to start survey")) @api.model def _read_group_stage_ids(self, stages, domain, order): @@ -273,7 +257,7 @@ class Survey(models.Model): 'type': 'ir.actions.act_url', 'name': "Start Survey", 'target': 'self', - 'url': self.with_context(relative_url=True).public_url + trail + 'url': self.public_url + trail } @api.multi @@ -317,7 +301,7 @@ class Survey(models.Model): 'type': 'ir.actions.act_url', 'name': "Print Survey", 'target': 'self', - 'url': self.with_context(relative_url=True).print_url + trail + 'url': '/survey/print/%s%s' % (self.id, trail) } @api.multi @@ -328,7 +312,7 @@ class Survey(models.Model): 'type': 'ir.actions.act_url', 'name': "Results of the Survey", 'target': 'self', - 'url': self.with_context(relative_url=True).result_url + 'url': '/survey/results/%s' % self.id } @api.multi diff --git a/addons/survey/models/survey_user.py b/addons/survey/models/survey_user.py index 4efe74a9c43..edc903c85fd 100644 --- a/addons/survey/models/survey_user.py +++ b/addons/survey/models/survey_user.py @@ -25,33 +25,28 @@ class SurveyUserInput(models.Model): """ Metadata for a set of one user's answers to a particular survey """ _name = "survey.user_input" - _rec_name = 'date_create' + _rec_name = 'survey_id' _description = 'Survey User Input' + # description survey_id = fields.Many2one('survey.survey', string='Survey', required=True, readonly=True, ondelete='restrict') - date_create = fields.Datetime('Creation Date', default=fields.Datetime.now, required=True, readonly=True, copy=False) - deadline = fields.Datetime('Deadline', help="Date by which the person can open the survey and submit answers", oldname="date_deadline") - input_type = fields.Selection([('manually', 'Manually'), ('link', 'Link')], string='Answer Type', default='manually', required=True, readonly=True, oldname="type") + input_type = fields.Selection([ + ('manually', 'Manually'), ('link', 'Link')], + string='Answer Type', default='manually', required=True, readonly=True, + oldname="type") state = fields.Selection([ ('new', 'Not started yet'), ('skip', 'Partially completed'), ('done', 'Completed')], string='Status', default='new', readonly=True) test_entry = fields.Boolean(readonly=True) + # identification and access token = fields.Char('Identification token', default=lambda self: str(uuid.uuid4()), readonly=True, required=True, copy=False) - - # Optional Identification data partner_id = fields.Many2one('res.partner', string='Partner', readonly=True) email = fields.Char('E-mail', readonly=True) - - # Displaying data - last_displayed_page_id = fields.Many2one('survey.page', string='Last displayed page') - # The answers ! + # answers user_input_line_ids = fields.One2many('survey.user_input_line', 'user_input_id', string='Answers', copy=True) - - # URLs used to display the answers - result_url = fields.Char("Public link to the survey results", related='survey_id.result_url', readonly=False) - print_url = fields.Char("Public link to the empty survey", related='survey_id.print_url', readonly=False) - + deadline = fields.Datetime('Deadline', help="Date by which the person can open the survey and submit answers") + last_displayed_page_id = fields.Many2one('survey.page', string='Last displayed page') quizz_score = fields.Float("Score for the quiz", compute="_compute_quizz_score", default=0.0) @api.depends('user_input_line_ids.quizz_mark') @@ -69,8 +64,9 @@ class SurveyUserInput(models.Model): (used as a cronjob declared in data/survey_cron.xml) """ an_hour_ago = fields.Datetime.to_string(datetime.datetime.now() - datetime.timedelta(hours=1)) - self.search([('input_type', '=', 'manually'), ('state', '=', 'new'), - ('date_create', '<', an_hour_ago)]).unlink() + self.search([('input_type', '=', 'manually'), + ('state', '=', 'new'), + ('create_date', '<', an_hour_ago)]).unlink() @api.multi def action_survey_resend(self): @@ -85,38 +81,26 @@ class SurveyUserInput(models.Model): return self.survey_id.with_context(local_context).action_send_survey() @api.multi - def action_view_answers(self): + def action_print_answers(self): """ Open the website page with the survey form """ self.ensure_one() return { 'type': 'ir.actions.act_url', 'name': "View Answers", 'target': 'self', - 'url': '%s?token=%s' % (self.print_url, self.token) - } - - @api.multi - def action_survey_results(self): - """ Open the website page with the survey results """ - self.ensure_one() - return { - 'type': 'ir.actions.act_url', - 'name': "Survey Results", - 'target': 'self', - 'url': self.result_url + 'url': '/survey/print/%s?token=%s' % (self.survey_id.id, self.token) } class SurveyUserInputLine(models.Model): _name = 'survey.user_input_line' _description = 'Survey User Input Line' - _rec_name = 'date_create' + _rec_name = 'user_input_id' user_input_id = fields.Many2one('survey.user_input', string='User Input', ondelete='cascade', required=True) question_id = fields.Many2one('survey.question', string='Question', ondelete='restrict', required=True) page_id = fields.Many2one(related='question_id.page_id', string="Page", readonly=False) survey_id = fields.Many2one(related='user_input_id.survey_id', string='Survey', store=True, readonly=False) - date_create = fields.Datetime('Create Date', default=fields.Datetime.now, required=True) skipped = fields.Boolean('Skipped') answer_type = fields.Selection([ ('text', 'Text'), diff --git a/addons/survey/tests/test_survey.py b/addons/survey/tests/test_survey.py index 0ce7c5d612d..b962a4fa1dd 100644 --- a/addons/survey/tests/test_survey.py +++ b/addons/survey/tests/test_survey.py @@ -175,19 +175,3 @@ class TestSurveyInternals(common.SurveyCase): result = self.env['survey.survey'].prepare_result(question) for key in exresult: self.assertEqual(result[key], exresult[key]) - - @users('survey_manager') - def test_survey_urls(self): - base_url = self.env['ir.config_parameter'].sudo().get_param('web.base.url') - urltypes = {'public': 'start', 'print': 'print', 'result': 'results'} - for urltype, urltxt in urltypes.items(): - survey_url = getattr(self.survey, urltype + '_url') - survey_url_relative = getattr(self.survey.with_context({'relative_url': True}), urltype + '_url') - url = "survey/%s/%s" % (urltxt, slug(self.survey)) - full_url = urls.url_join(base_url, url) - self.assertEqual(full_url, survey_url) - self.assertEqual('/' + url, survey_url_relative) - if urltype == 'public': - url_html = 'Click here to start survey' - self.assertEqual(url_html % full_url, getattr(self.survey, urltype + '_url_html'), msg="Public URL is incorrect") - self.assertEqual(url_html % ('/' + url), getattr(self.survey.with_context({'relative_url': True}), urltype + '_url_html')) diff --git a/addons/survey/views/survey_survey_views.xml b/addons/survey/views/survey_survey_views.xml index 6e10e60398f..7c0d391b4af 100644 --- a/addons/survey/views/survey_survey_views.xml +++ b/addons/survey/views/survey_survey_views.xml @@ -80,14 +80,12 @@ - - @@ -111,12 +109,10 @@
  • - Test - Test + Test
  • - Share &amp; Invite - Share &amp; Invite + Share &amp; Invite
  • Analyze Answers () diff --git a/addons/survey/views/survey_templates.xml b/addons/survey/views/survey_templates.xml index 2b8149d37f0..37795950cfb 100644 --- a/addons/survey/views/survey_templates.xml +++ b/addons/survey/views/survey_templates.xml @@ -461,20 +461,20 @@ - + -
    +
    -
    +
    -
    +
    @@ -495,7 +495,7 @@ - +
    @@ -679,7 +679,7 @@ - +
    diff --git a/addons/survey/views/survey_user_views.xml b/addons/survey/views/survey_user_views.xml index 1e35a182e06..919daed1deb 100644 --- a/addons/survey/views/survey_user_views.xml +++ b/addons/survey/views/survey_user_views.xml @@ -30,8 +30,7 @@
    @@ -39,7 +38,7 @@ - + @@ -57,7 +56,7 @@ - + @@ -71,7 +70,7 @@ - + @@ -89,7 +88,7 @@ - + @@ -105,7 +104,7 @@
- +
@@ -155,7 +154,7 @@ - + @@ -181,7 +180,7 @@ - + diff --git a/addons/survey/wizard/survey_invite.py b/addons/survey/wizard/survey_invite.py index 7aa47259262..9190d67b6b7 100644 --- a/addons/survey/wizard/survey_invite.py +++ b/addons/survey/wizard/survey_invite.py @@ -64,7 +64,6 @@ class SurveyInvite(models.TransientModel): ('email_private', 'Send private invitation to your audience (only one response per recipient and per invitation).')], string='Share options', default='public_link', required=True) public_url = fields.Char(compute="_compute_survey_url", string="Public url") - public_url_html = fields.Char(compute="_compute_survey_url", string="Public HTML web link") date_deadline = fields.Date(string="Deadline to which the invitation to respond is valid", help="Deadline to which the invitation to respond for this survey is valid. If the field is empty,\ the invitation is still valid.") @@ -73,7 +72,6 @@ class SurveyInvite(models.TransientModel): def _compute_survey_url(self): for wizard in self: wizard.public_url = wizard.survey_id.public_url - wizard.public_url_html = wizard.survey_id.public_url_html @api.model def default_get(self, fields): @@ -219,7 +217,6 @@ class SurveyInvite(models.TransientModel): survey_user_input = SurveyUserInput.create({ 'survey_id': wizard.survey_id.id, 'deadline': wizard.date_deadline, - 'date_create': fields.Datetime.now(), 'input_type': 'link', 'state': 'new', 'token': token, diff --git a/addons/survey/wizard/survey_invite_views.xml b/addons/survey/wizard/survey_invite_views.xml index 942714e0f43..2d0edef97e7 100644 --- a/addons/survey/wizard/survey_invite_views.xml +++ b/addons/survey/wizard/survey_invite_views.xml @@ -17,8 +17,6 @@
Copy, paste and share the web link below to your audience.
-
Copy and paste the HTML code below to add this web link to any webpage.
- From e3d2957f98597fa5028c6662d6bbb6c659c5ff57 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thibault=20Delavall=C3=A9e?= Date: Fri, 23 Nov 2018 15:01:34 +0000 Subject: [PATCH 03/15] [REF] survey: update ACLs and limit access to survey models Purpose of this commit is to rewrite all access rights and rules of survey models. Indeed this module is quite old and rules may give some strange permissions to some people and does not enforce the use of real survey user groups. Specifications * survey manager: can CRUD everything; * survey users: can CRUD their own surveys, read all; * regular users: cannot access anything and will use dedicated controllers and routes; * portal, public: cannot access anything and will use dedicated controllers and routes; This commit alone breaks some use of survey, notably its embedded portal. Future commit will update code to use those new ACLs. It is done in several commits in order to have smaller diff and avoid having too large diff in a single commit. This commit is linked to task ID 1911586 and PR #28986. --- addons/survey/models/survey_question.py | 4 +- addons/survey/models/survey_survey.py | 2 +- addons/survey/security/ir.model.access.csv | 49 +++-- addons/survey/security/survey_security.xml | 238 +++++++++++++++++---- 4 files changed, 227 insertions(+), 66 deletions(-) diff --git a/addons/survey/models/survey_question.py b/addons/survey/models/survey_question.py index b018a7a8b5a..5014c96e017 100644 --- a/addons/survey/models/survey_question.py +++ b/addons/survey/models/survey_question.py @@ -82,7 +82,9 @@ class SurveyQuestion(models.Model): constr_mandatory = fields.Boolean('Mandatory Answer') constr_error_msg = fields.Char('Error message', translate=True, default=lambda self: _("This question requires an answer.")) # Answer - user_input_line_ids = fields.One2many('survey.user_input_line', 'question_id', string='Answers', domain=[('skipped', '=', False)]) + user_input_line_ids = fields.One2many( + 'survey.user_input_line', 'question_id', string='Answers', + domain=[('skipped', '=', False)], groups='survey.group_survey_user') _sql_constraints = [ ('positive_len_min', 'CHECK (validation_length_min >= 0)', 'A length must be positive!'), diff --git a/addons/survey/models/survey_survey.py b/addons/survey/models/survey_survey.py index a88cec640bd..ea7e74ca4b8 100644 --- a/addons/survey/models/survey_survey.py +++ b/addons/survey/models/survey_survey.py @@ -33,7 +33,7 @@ class Survey(models.Model): is_closed = fields.Boolean("Is closed", related='stage_id.closed', readonly=False) # content page_ids = fields.One2many('survey.page', 'survey_id', string='Pages', copy=True) - user_input_ids = fields.One2many('survey.user_input', 'survey_id', string='User responses', readonly=True) + user_input_ids = fields.One2many('survey.user_input', 'survey_id', string='User responses', readonly=True, groups='survey.group_survey_user') # security / access auth_required = fields.Boolean('Login required', help="Users with a public link will be requested to login before taking part to the survey") users_can_go_back = fields.Boolean('Users can go back', help="If checked, users can go back to previous pages.") diff --git a/addons/survey/security/ir.model.access.csv b/addons/survey/security/ir.model.access.csv index 09b5ae48178..e57a716cd23 100644 --- a/addons/survey/security/ir.model.access.csv +++ b/addons/survey/security/ir.model.access.csv @@ -1,22 +1,29 @@ id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink -access_survey_public,survey.survey public,model_survey_survey,,1,0,0,0 -access_survey_page_public,survey.page public,model_survey_page,,1,0,0,0 -access_survey_question_public,survey.question public,model_survey_question,,1,0,0,0 -access_survey_label_public,survey.label public,model_survey_label,,1,0,0,0 -access_survey_user_input_public,survey.user_input public,model_survey_user_input,,1,1,1,0 -access_survey_user_input_line_public,survey.user_input_line public,model_survey_user_input_line,,1,1,1,0 -access_survey_stage_public,survey.stage public,model_survey_stage,,1,0,0,0 -access_survey_user,survey.survey user,model_survey_survey,group_survey_user,1,0,0,0 -access_survey_page_user,survey.page user,model_survey_page,group_survey_user,1,0,0,0 -access_survey_question_user,survey.question user,model_survey_question,group_survey_user,1,0,0,0 -access_survey_label_user,survey.label user,model_survey_label,group_survey_user,1,0,0,0 -access_survey_user_input_user,survey.user_input user,model_survey_user_input,group_survey_user,1,1,1,0 -access_survey_user_input_line_user,survey.user_input_line user,model_survey_user_input_line,group_survey_user,1,1,1,0 -access_survey_stage_user,survey.stage user,model_survey_stage,group_survey_user,1,0,0,0 -access_survey_manager,survey.survey manager,model_survey_survey,group_survey_manager,1,1,1,1 -access_survey_page_manager,survey.page manager,model_survey_page,group_survey_manager,1,1,1,1 -access_survey_question_manager,survey.question manager,model_survey_question,group_survey_manager,1,1,1,1 -access_survey_label_manager,survey.label manager,model_survey_label,group_survey_manager,1,1,1,1 -access_survey_user_input_manager,survey.user_input manager,model_survey_user_input,group_survey_manager,1,1,1,1 -access_survey_user_input_line_manager,survey.user_input_line manager,model_survey_user_input_line,group_survey_manager,1,1,1,1 -access_survey_stage_manager,survey.stage manager,model_survey_stage,group_survey_manager,1,1,1,1 \ No newline at end of file +access_survey_all,survey.survey.all,model_survey_survey,,0,0,0,0 +access_survey_user,survey.survey.user,model_survey_survey,base.group_user,0,0,0,0 +access_survey_survey_user,survey.survey.survey.user,model_survey_survey,group_survey_user,1,1,1,1 +access_survey_survey_manager,survey.survey.survey.manager,model_survey_survey,group_survey_manager,1,1,1,1 +access_survey_stage_all,survey.stage.all,model_survey_stage,,0,0,0,0 +access_survey_stage_user,survey.stage.user,model_survey_stage,base.group_user,0,0,0,0 +access_survey_stage_survey_user,survey.stage.survey.user,model_survey_stage,group_survey_user,1,0,0,0 +access_survey_stage_survey_manager,survey.stage.survey.manager,model_survey_stage,group_survey_manager,1,1,1,1 +access_survey_page_all,survey.page.all,model_survey_page,,0,0,0,0 +access_survey_page_user,survey.page.user,model_survey_page,base.group_user,0,0,0,0 +access_survey_page_survey_user,survey.page.survey.user,model_survey_page,group_survey_user,1,1,1,1 +access_survey_page_survey_manager,survey.page.survey.manager,model_survey_page,group_survey_manager,1,1,1,1 +access_survey_question_all,survey.question.all,model_survey_question,,0,0,0,0 +access_survey_question_user,survey.question.user,model_survey_question,base.group_user,0,0,0,0 +access_survey_question_survey_user,survey.question.survey.user,model_survey_question,group_survey_user,1,1,1,1 +access_survey_question_survey_manager,survey.question.survey.manager,model_survey_question,group_survey_manager,1,1,1,1 +access_survey_label_all,survey.label.all,model_survey_label,,0,0,0,0 +access_survey_label_user,survey.label.user,model_survey_label,base.group_user,0,0,0,0 +access_survey_label_survey_user,survey.label.survey.user,model_survey_label,group_survey_user,1,1,1,1 +access_survey_label_survey_manager,survey.label.survey.manager,model_survey_label,group_survey_manager,1,1,1,1 +access_survey_user_input_all,survey.user_input.all,model_survey_user_input,,0,0,0,0 +access_survey_user_input_user,survey.user_input.user,model_survey_user_input,base.group_user,0,0,0,0 +access_survey_user_input_survey_user,survey.user_input.survey.user,model_survey_user_input,group_survey_user,1,1,1,1 +access_survey_user_input_survey_manager,survey.user_input.survey.manager,model_survey_user_input,group_survey_manager,1,1,1,1 +access_survey_user_input_line_all,survey.user_input_line.all,model_survey_user_input_line,,0,0,0,0 +access_survey_user_input_line_user,survey.user_input_line.user,model_survey_user_input_line,base.group_user,0,0,0,0 +access_survey_user_input_line_survey_user,survey.user_input_line.survey.user,model_survey_user_input_line,group_survey_user,1,1,1,1 +access_survey_user_input_line_survey_manager,survey.user_input_line.survey.manager,model_survey_user_input_line,group_survey_manager,1,1,1,1 diff --git a/addons/survey/security/survey_security.xml b/addons/survey/security/survey_security.xml index 5d79c515d26..cf4e02e9183 100644 --- a/addons/survey/security/survey_security.xml +++ b/addons/survey/security/survey_security.xml @@ -19,61 +19,213 @@ - - - Access to survey for regular users - - [('stage_id.closed', '=', False)] - - - - - - - - - Survey Manager access rights + + + Survey: manager: all [(1, '=', 1)] - - - - + + + + - - - Public access to user_input - + + Survey: officer: read all + + [(1, '=', 1)] + + + + + + + + Survey: officer: create/write/unlink own only + [('create_uid', '=', user.id)] - - - - - - - - - Access to user_input for regular users - - ['|', ('create_uid', '=', user.id), ('partner_id', '=', user.partner_id.id)] - - - - + + + + - - Survey Manager access rights + + Survey page: manager: all + + [(1, '=', 1)] + + + + + + + + Survey page: officer: read all + + [(1, '=', 1)] + + + + + + + + Survey page: officer: create/write/unlink linked to own survey only + + [('survey_id.create_uid', '=', user.id)] + + + + + + + + + Survey stage: manager: all + + [(1, '=', 1)] + + + + + + + + Survey stage: officer: read all + + [(1, '=', 1)] + + + + + + + + + Survey question: manager: all + + [(1, '=', 1)] + + + + + + + + Survey question: officer: read all + + [(1, '=', 1)] + + + + + + + + Survey question: officer: create/write/unlink linked to own survey only + + [('survey_id.create_uid', '=', user.id)] + + + + + + + + + Survey label: manager: all + + [(1, '=', 1)] + + + + + + + + Survey label: officer: read all + + [(1, '=', 1)] + + + + + + + + Survey label: officer: create/write/unlink linked to own survey only + + [('survey_id.create_uid', '=', user.id)] + + + + + + + + + + Survey user input: manager: all [(1, '=', 1)] - - - - + + + + + + + Survey user input: officer: read all + + [(1, '=', 1)] + + + + + + + + Survey user input: officer: create/write/unlink linked to own survey only + + [('survey_id.create_uid', '=', user.id)] + + + + + + + Survey user input line: manager: all + + [(1, '=', 1)] + + + + + + + + Survey user input line: officer: read all + + [(1, '=', 1)] + + + + + + + + Survey user input line: officer: create/write/unlink linked to own survey only + + [('user_input_id.survey_id.create_uid', '=', user.id)] + + + + + + From 598d84783e29bd5c5d9dde0748234ea544cda380 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thibault=20Delavall=C3=A9e?= Date: Fri, 23 Nov 2018 16:16:18 +0000 Subject: [PATCH 04/15] [FIX] survey: allow to delete surveys with questions and answers Purpose of this commit is to allow to unlink surveys even if answers have been given. Currently an ondelete restrict on answers exists that prevent from unlinking surveys. As unlink rights are now given to survey users only they should be able to completely remove surveys if necessary. This commit is linked to task ID 1911586 and PR #28986. --- addons/survey/models/survey_user.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/addons/survey/models/survey_user.py b/addons/survey/models/survey_user.py index edc903c85fd..21e5cfff446 100644 --- a/addons/survey/models/survey_user.py +++ b/addons/survey/models/survey_user.py @@ -29,7 +29,7 @@ class SurveyUserInput(models.Model): _description = 'Survey User Input' # description - survey_id = fields.Many2one('survey.survey', string='Survey', required=True, readonly=True, ondelete='restrict') + survey_id = fields.Many2one('survey.survey', string='Survey', required=True, readonly=True, ondelete='cascade') input_type = fields.Selection([ ('manually', 'Manually'), ('link', 'Link')], string='Answer Type', default='manually', required=True, readonly=True, @@ -98,7 +98,7 @@ class SurveyUserInputLine(models.Model): _rec_name = 'user_input_id' user_input_id = fields.Many2one('survey.user_input', string='User Input', ondelete='cascade', required=True) - question_id = fields.Many2one('survey.question', string='Question', ondelete='restrict', required=True) + question_id = fields.Many2one('survey.question', string='Question', ondelete='cascade', required=True) page_id = fields.Many2one(related='question_id.page_id', string="Page", readonly=False) survey_id = fields.Many2one(related='user_input_id.survey_id', string='Survey', store=True, readonly=False) skipped = fields.Boolean('Skipped') From 5878579b10240eb0a3e8c5a6e54da82914959a59 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thibault=20Delavall=C3=A9e?= Date: Tue, 18 Dec 2018 13:25:42 +0000 Subject: [PATCH 05/15] [FIX] survey: is_closed field on survey should be readonly Otherwise having this value coming from a view triggers a write on the survey stage model. This write is unnecessary ad is_closed should really be readonly. It may also causes access rights issues notably for survey users that cannot update stages. This commit is linked to task ID 1911586 and PR #28986. --- addons/survey/models/survey_survey.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/addons/survey/models/survey_survey.py b/addons/survey/models/survey_survey.py index ea7e74ca4b8..e6e8aebed4a 100644 --- a/addons/survey/models/survey_survey.py +++ b/addons/survey/models/survey_survey.py @@ -30,7 +30,7 @@ class Survey(models.Model): active = fields.Boolean("Active", default=True) stage_id = fields.Many2one('survey.stage', string="Stage", default=_default_stage, ondelete="restrict", copy=False, group_expand='_read_group_stage_ids') - is_closed = fields.Boolean("Is closed", related='stage_id.closed', readonly=False) + is_closed = fields.Boolean("Is closed", related='stage_id.closed', readonly=True) # content page_ids = fields.One2many('survey.page', 'survey_id', string='Pages', copy=True) user_input_ids = fields.One2many('survey.user_input', 'survey_id', string='User responses', readonly=True, groups='survey.group_survey_user') From dfc37ecede7125872cbd2887a677de5d36a370d8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thibault=20Delavall=C3=A9e?= Date: Mon, 3 Dec 2018 09:01:53 +0000 Subject: [PATCH 06/15] [REF] survey: adapt controllers and portal flow to new ACLs Purpose of this commit is to adapt controllers and routes to new ACLs. As survey access is now controlled routes have to use survey ID and browse records as sudo when access is granted. This commit is linked to task ID 1911586 and PR #28986. --- addons/survey/controllers/main.py | 322 ++++++++++++++--------- addons/survey/models/survey_survey.py | 54 +++- addons/survey/tests/test_survey_ui.py | 6 +- addons/survey/views/survey_templates.xml | 14 +- 4 files changed, 260 insertions(+), 136 deletions(-) diff --git a/addons/survey/controllers/main.py b/addons/survey/controllers/main.py index 28a5ff721a7..9a6b8b57337 100644 --- a/addons/survey/controllers/main.py +++ b/addons/survey/controllers/main.py @@ -8,6 +8,7 @@ from datetime import datetime from math import ceil from odoo import fields, http, SUPERUSER_ID +from odoo.exceptions import UserError from odoo.http import request from odoo.tools import ustr @@ -16,145 +17,206 @@ _logger = logging.getLogger(__name__) class Survey(http.Controller): - def _check_bad_cases(self, survey, token=None): - # In case of bad survey, redirect to surveys list - if not survey.sudo().exists(): - return werkzeug.utils.redirect("/survey/") + def _fetch_from_access_token(self, survey_id, access_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: + 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) + ], limit=1) + return survey_sudo, answer_sudo - # In case of auth required, block public user - if survey.auth_required and request.env.user._is_public(): - return request.render("survey.auth_required", {'survey': survey, 'token': token}) + def _check_validity(self, survey_id, access_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 - # In case of non open surveys - if survey.stage_id.closed: + * survey_wrong: survey does not exist; + * survey_auth: authentication is required; + * survey_closed: survey is closed and does not accept input anymore; + * survey_void: survey is void and should not be taken; + * token_wrong: given token not recognized; + * token_required: no token given although it is necessary to access the + survey; + * answer_deadline: token linked to an expired answer; + + :param ensure_token: whether user input existence based on given access token + 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) + + if not survey_sudo.exists(): + return 'survey_wrong' + + if access_token and not answer_sudo: + return 'token_wrong' + + if not answer_sudo and ensure_token: + return 'token_required' + + if survey_sudo.auth_required and request.env.user._is_public(): + return 'survey_auth' + + if survey_sudo.is_closed or not survey_sudo.active: + return 'survey_closed' + + if not survey_sudo.page_ids: + return 'survey_void' + + # In case of delayed deadline # TDE FIXME + if answer_sudo and answer_sudo.deadline: + dt_now = datetime.now() + if dt_now > answer_sudo.deadline: + return 'answer_deadline' + + return True + + def _get_access_data(self, survey_id, access_token, ensure_token=True): + """ Get back data related to survey and user input, given the ID and access + token provided by the route. + + : param ensure_token: whether user input existence should be enforced or not(see ``_check_validity``) + """ + 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) + if validity_code != 'survey_wrong': + survey_sudo, answer_sudo = self._fetch_from_access_token(survey_id, access_token) + try: + survey_user = survey_sudo.sudo(request.env.user) + survey_user.check_access_rights(self, 'read', raise_exception=True) + survey_user.check_access_rule(self, 'read') + except: + pass + else: + has_survey_access = True + can_answer = bool(answer_sudo) + if not can_answer: + can_answer = survey_sudo.auth_required is False + + return { + 'survey_sudo': survey_sudo, + 'answer_sudo': answer_sudo, + 'has_survey_access': has_survey_access, + 'can_answer': can_answer, + 'validity_code': validity_code, + } + + def _redirect_with_error(self, access_data, error_key): + survey_sudo = access_data['survey_sudo'] + answer_sudo = access_data['answer_sudo'] + + if error_key == 'survey_void' and access_data['can_answer']: + return request.render("survey.nopages", {'survey': survey_sudo}) + elif error_key == 'survey_closed' and access_data['can_answer']: + return request.render("survey.notopen") + elif error_key == 'survey_auth' and answer_sudo.token: + return request.render("survey.auth_required", {'survey': survey_sudo, 'token': answer_sudo.token}) + elif error_key == 'answer_deadline' and answer_sudo.token: return request.render("survey.notopen") - # If there is no pages - if not survey.page_ids: - return request.render("survey.nopages", {'survey': survey}) + return werkzeug.utils.redirect("/") - # Everything seems to be ok - return None - - def _check_deadline(self, user_input): - '''Prevent opening of the survey if the deadline has turned out - - ! This will NOT disallow access to users who have already partially filled the survey !''' - deadline = user_input.deadline - if deadline: - dt_deadline = fields.Datetime.from_string(deadline) - dt_now = datetime.now() - if dt_now > dt_deadline: # survey is not open anymore - return request.render("survey.notopen") - return None - - @http.route('/survey/test/', type='http', auth='user', website=True) - def survey_test(self, survey, token=None, **kwargs): + @http.route('/survey/test/', type='http', auth='user', website=True) + def survey_test(self, survey_id, token=None, **kwargs): """ Test mode for surveys: create a test answer, only for managers or officers testing their surveys """ - if request.env.user.has_group('survey.group_survey_manager') or \ - request.env.user.has_group('survey.group_survey_user') and survey.create_uid == request.env.user: - user_input = request.env['survey.user_input'].create({ - 'survey_id': survey.id, - 'test_entry': True - }) - return request.redirect('/survey/start/%s?token=%s' % (survey.id, user_input.token)) - return werkzeug.utils.redirect('/') + survey_sudo = request.env['survey.survey'].sudo().browse(survey_id) + 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?token=%s' % (survey_sudo.id, answer_sudo.token)) - @http.route('/survey/start/', type='http', auth='public', website=True) - def survey_start(self, survey, token=None): + @http.route('/survey/start/', type='http', auth='public', website=True) + def survey_start(self, survey_id, token=None, email=False): """ Start a survey by providing a token linked to an answer or generate a new token if access is allowed """ - UserInput = request.env['survey.user_input'] + access_data = self._get_access_data(survey_id, token, ensure_token=False) + if access_data['validity_code'] is not True: + return self._redirect_with_error(access_data, access_data['validity_code']) - # Controls if the survey can be displayed - errpage = self._check_bad_cases(survey, token=token) - if errpage: - return errpage + survey_sudo, answer_sudo = access_data['survey_sudo'], access_data['answer_sudo'] + if not answer_sudo: + try: + answer_sudo = survey_sudo._create_answer(user=request.env.user, email=email) + except UserError: + answer_sudo = False - # Manual surveying - if not token: - vals = {'survey_id': survey.id} - if not request.env.user._is_public(): - vals['partner_id'] = request.env.user.partner_id.id - user_input = UserInput.create(vals) - else: - user_input = UserInput.sudo().search([('token', '=', token)], limit=1) - if not user_input: - return request.render("survey.403", {'survey': survey}) - - # Do not open expired survey - errpage = self._check_deadline(user_input) - if errpage: - return errpage + if not answer_sudo: + try: + survey_sudo.sudo(request.env.user).check_access_rights('read') + survey_sudo.sudo(request.env.user).check_access_rule('read') + except: + return werkzeug.utils.redirect("/") + else: + return request.render("survey.403", {'survey': survey_sudo}) # Select the right page - if user_input.state == 'new': # Intro page - data = {'survey': survey, 'page': None, 'token': user_input.token} + if answer_sudo.state == 'new': # Intro page + data = {'survey': survey_sudo, 'page': None, 'token': answer_sudo.token} return request.render('survey.survey_init', data) else: - return request.redirect('/survey/fill/%s/%s' % (survey.id, user_input.token)) + return request.redirect('/survey/fill/%s/%s' % (survey_sudo.id, answer_sudo.token)) - @http.route('/survey/fill//', type='http', auth='public', website=True) - def survey_display_page(self, survey, token, prev=None): - Survey = request.env['survey.survey'] - UserInput = request.env['survey.user_input'] + @http.route('/survey/fill//', type='http', auth='public', website=True) + def survey_display_page(self, survey_id, token, prev=None): + access_data = self._get_access_data(survey_id, token, ensure_token=True) + if access_data['validity_code'] is not True: + return self._redirect_with_error(access_data, access_data['validity_code']) - # Controls if the survey can be displayed - errpage = self._check_bad_cases(survey) - if errpage: - return errpage - - # Load the user_input - user_input = UserInput.sudo().search([('token', '=', token)], limit=1) - if not user_input: # Invalid token - return request.render("survey.403", {'survey': survey}) - - # Do not display expired survey (even if some pages have already been - # displayed -- There's a time for everything!) - errpage = self._check_deadline(user_input) - if errpage: - return errpage + survey_sudo, answer_sudo = access_data['survey_sudo'], access_data['answer_sudo'] # Select the right page - if user_input.state == 'new': # First page - page, page_nr, last = Survey.next_page(user_input, 0, go_back=False) - data = {'survey': survey, 'page': page, 'page_nr': page_nr, 'token': user_input.token} + 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} if last: data.update({'last': True}) return request.render('survey.survey', data) - elif user_input.state == 'done': # Display success message - return request.render('survey.sfinished', {'survey': survey, + elif answer_sudo.state == 'done': # Display success message + return request.render('survey.sfinished', {'survey': survey_sudo, 'token': token, - 'user_input': user_input}) - elif user_input.state == 'skip': + 'user_input': answer_sudo}) + elif answer_sudo.state == 'skip': flag = (True if prev and prev == 'prev' else False) - page, page_nr, last = Survey.next_page(user_input, user_input.last_displayed_page_id.id, go_back=flag) + page, page_nr, last = survey_sudo.next_page(answer_sudo, answer_sudo.last_displayed_page_id.id, go_back=flag) #special case if you click "previous" from the last page, then leave the survey, then reopen it from the URL, avoid crash if not page: - page, page_nr, last = Survey.next_page(user_input, user_input.last_displayed_page_id.id, go_back=True) + page, page_nr, last = survey_sudo.next_page(answer_sudo, answer_sudo.last_displayed_page_id.id, go_back=True) - data = {'survey': survey, 'page': page, 'page_nr': page_nr, 'token': user_input.token} + data = {'survey': survey_sudo, 'page': page, 'page_nr': page_nr, 'token': answer_sudo.token} if last: data.update({'last': True}) return request.render('survey.survey', data) else: - return request.render("survey.403", {'survey': survey}) + return request.render("survey.403", {'survey': survey_sudo}) - @http.route('/survey/prefill//', type='http', auth='public', website=True) - def survey_get_answers(self, survey, token, page_id=None): + @http.route('/survey/prefill//', type='http', auth='public', website=True) + def survey_get_answers(self, survey_id, token, page_id=None): """ TDE NOTE: original comment: # AJAX prefilling of a survey -> AJAX / http ?? """ - UserInputLine = request.env['survey.user_input_line'] - ret = {} + access_data = self._get_access_data(survey_id, token, ensure_token=True) + if access_data['validity_code'] is not True: + return {} + + survey_sudo, answer_sudo = access_data['survey_sudo'], access_data['answer_sudo'] # Fetch previous answers if page_id: - previous_answers = UserInputLine.sudo().search([('user_input_id.token', '=', token), ('page_id', '=', page_id)]) + previous_answers = answer_sudo.user_input_line_ids.filtered(lambda line: line.page_id.id == page_id) else: - previous_answers = UserInputLine.sudo().search([('user_input_id.token', '=', token)]) + previous_answers = answer_sudo.user_input_line_ids # Return non empty answers in a JSON compatible format + ret = {} for answer in previous_answers: if not answer.skipped: answer_tag = '%s_%s_%s' % (answer.survey_id.id, answer.page_id.id, answer.question_id.id) @@ -182,31 +244,41 @@ 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, token, page_id=None): + @http.route('/survey/scores//', type='http', auth='public', website=True) + def survey_get_scores(self, survey_id, token, page_id=None): """ TDE NOTE: original comment: # AJAX scores loading for quiz correction mode -> AJAX / http ?? """ - ret = {} + access_data = self._get_access_data(survey_id, token, ensure_token=True) + if access_data['validity_code'] is not True: + return {} - # Fetch answers - previous_answers = request.env['survey.user_input_line'].sudo().search([('user_input_id.token', '=', token)]) + survey_sudo, answer_sudo = access_data['survey_sudo'], access_data['answer_sudo'] # Compute score for each question - for answer in previous_answers: + ret = {} + for answer in answer_sudo.user_input_line_ids: tmp_score = ret.get(answer.question_id.id, 0.0) 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, token, **post): + @http.route('/survey/submit//', type='http', methods=['POST'], auth='public', website=True) + def survey_submit(self, survey_id, token, **post): """ TDE NOTE: original comment: # AJAX submission of a page -> AJAX / http ?? """ - _logger.debug('Incoming data: %s', post) + access_data = self._get_access_data(survey_id, token, ensure_token=True) + if access_data['validity_code'] is not True: + return {} + + survey_sudo, answer_sudo = access_data['survey_sudo'], access_data['answer_sudo'] + page_id = int(post['page_id']) - questions = request.env['survey.question'].search([('page_id', '=', page_id)]) + questions = request.env['survey.question'].sudo().search([ + ('survey_id', '=', survey_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_id, page_id, question.id) errors.update(question.validate_question(post, answer_tag)) ret = {} @@ -214,39 +286,39 @@ class Survey(http.Controller): # Return errors messages to webpage ret['errors'] = errors else: - # Store answers into database - try: - user_input = request.env['survey.user_input'].sudo().search([('token', '=', token)], limit=1) - except KeyError: # Invalid token - return request.render("survey.403", {'survey': survey}) - user_id = request.env.user.id if user_input.input_type != 'link' else SUPERUSER_ID - for question in questions: - answer_tag = "%s_%s_%s" % (survey.id, page_id, question.id) - request.env['survey.user_input_line'].sudo(user=user_id).save_lines(user_input.id, question, post, answer_tag) + answer_tag = "%s_%s_%s" % (survey_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' - next_page, _, last = request.env['survey.survey'].next_page(user_input, page_id, go_back=go_back) + next_page, _, last = request.env['survey.survey'].next_page(answer_sudo, page_id, go_back=go_back) vals = {'last_displayed_page_id': page_id} if next_page is None and not go_back: vals.update({'state': 'done'}) else: vals.update({'state': 'skip'}) - user_input.sudo(user=user_id).write(vals) - ret['redirect'] = '/survey/fill/%s/%s' % (survey.id, token) + answer_sudo.write(vals) + ret['redirect'] = '/survey/fill/%s/%s' % (survey_sudo.id, 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, token=None, **post): + @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) + 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']) + + survey_sudo, answer_sudo = access_data['survey_sudo'], access_data['answer_sudo'] + return request.render('survey.survey_print', { - 'survey': survey, + 'survey': survey_sudo, 'token': token, 'page_nr': 0, - 'quizz_correction': True if survey.quizz_mode and token else False}) + '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): diff --git a/addons/survey/models/survey_survey.py b/addons/survey/models/survey_survey.py index e6e8aebed4a..38da4f40277 100644 --- a/addons/survey/models/survey_survey.py +++ b/addons/survey/models/survey_survey.py @@ -60,7 +60,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" % (slug(survey))) + survey.public_url = urls.url_join(base_url, "survey/start/%s" % (survey.id)) @api.model def _read_group_stage_ids(self, stages, domain, order): @@ -76,6 +76,58 @@ class Survey(models.Model): default = dict(default or {}, title=title) return super(Survey, self).copy_data(default) + @api.multi + def _create_answer(self, user=False, partner=False, email=False, test_entry=False, **additional_vals): + """ Main entry point to get a token back or create a new one. This method + does check for current user access in order to explicitely validate + security. + + :param user: target user asking for a token; it might be void or a + public user in which case an email is welcomed; + :param email: email of the person asking the token is no user exists; + """ + self.check_access_rights('read') + self.check_access_rule('read') + + tokens = self.env['survey.user_input'] + for survey in self: + if partner and not user and partner.user_ids: + user = partner.user_ids[0] + + survey._check_answer_creation(user, partner, email, test_entry=test_entry) + answer_vals = { + 'survey_id': survey.id, + 'test_entry': test_entry, + } + if user and not user._is_public: + answer_vals['partner_id'] = user.partner_id.id + answer_vals['email'] = user.email + elif partner: + answer_vals['partner_id'] = partner.id + answer_vals['email'] = partner.email + else: + answer_vals['email'] = email + + answer_vals.update(additional_vals) + tokens += tokens.create(answer_vals) + + return tokens + + @api.multi + def _check_answer_creation(self, user, partner, email, test_entry=False): + """ Ensure conditions to create new tokens are met. """ + self.ensure_one() + if test_entry: + if not user.has_group('survey.group_survey_manager') or not user.has_group('survey.group_survey_user'): + raise UserError(_('Creating test token is not allowed for you.')) + else: + if not self.active: + raise UserError(_('Creating token for archived surveys is not allowed.')) + elif self.is_closed: + raise UserError(_('Creating token for closed surveys is not allowed.')) + if self.auth_required and (not user or user._is_public()): + raise UserError(_('Creating token for external users is not allowed for surveys requesting authentication.')) + @api.model def next_page(self, user_input, page_id, go_back=False): """ The next page to display to the user, knowing that page_id is the id diff --git a/addons/survey/tests/test_survey_ui.py b/addons/survey/tests/test_survey_ui.py index 3e489ca7be7..5005ac71bcf 100644 --- a/addons/survey/tests/test_survey_ui.py +++ b/addons/survey/tests/test_survey_ui.py @@ -6,10 +6,10 @@ import odoo.tests class TestUi(odoo.tests.HttpCase): def test_01_admin_survey_tour(self): - self.phantom_js("/survey/start/user-feedback-form-1", "odoo.__DEBUG__.services['web_tour.tour'].run('test_survey')", "odoo.__DEBUG__.services['web_tour.tour'].tours.test_survey.ready", login="admin") + 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") def test_02_demo_survey_tour(self): - self.phantom_js("/survey/start/user-feedback-form-1", "odoo.__DEBUG__.services['web_tour.tour'].run('test_survey')", "odoo.__DEBUG__.services['web_tour.tour'].tours.test_survey.ready", login="demo") + 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") def test_03_public_survey_tour(self): - self.phantom_js("/survey/start/user-feedback-form-1", "odoo.__DEBUG__.services['web_tour.tour'].run('test_survey')", "odoo.__DEBUG__.services['web_tour.tour'].tours.test_survey.ready") + 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") diff --git a/addons/survey/views/survey_templates.xml b/addons/survey/views/survey_templates.xml index 37795950cfb..8e8ccf61385 100644 --- a/addons/survey/views/survey_templates.xml +++ b/addons/survey/views/survey_templates.xml @@ -26,7 +26,7 @@

Thank you!

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

@@ -170,10 +170,10 @@

+ t-att-action="'/survey/fill/%s/%s' % (survey.id, token)" + t-att-data-prefill="'/survey/prefill/%s/%s?page_id=%s' % (survey.id, token, page.id)" + t-att-data-validate="'/survey/validate/%s/%s' % (survey.id, token)" + t-att-data-submit="'/survey/submit/%s/%s' % (survey.id, token)"> @@ -342,7 +342,7 @@

-
+

From 66e00f642afccebf2bd67a1aad1bc8bef64439fb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thibault=20Delavall=C3=A9e?= Date: Wed, 21 Nov 2018 09:44:38 +0000 Subject: [PATCH 07/15] [IMP] survey: add access rights tests Purpose of this commit is to improve test coverage of survey module. This commit adds tests related to current access and serve as a base for future modification. Some tests are commented as access will evolve for this application. This commit is linked to task ID 1911586 and PR #28986. --- addons/survey/tests/__init__.py | 1 + addons/survey/tests/test_survey_security.py | 325 ++++++++++++++++++++ 2 files changed, 326 insertions(+) create mode 100644 addons/survey/tests/test_survey_security.py diff --git a/addons/survey/tests/__init__.py b/addons/survey/tests/__init__.py index b345311e36b..0f0a3bd31ca 100644 --- a/addons/survey/tests/__init__.py +++ b/addons/survey/tests/__init__.py @@ -5,4 +5,5 @@ from . import common from . import test_survey from . import test_survey_flow from . import test_survey_invite +from . import test_survey_security from . import test_survey_ui diff --git a/addons/survey/tests/test_survey_security.py b/addons/survey/tests/test_survey_security.py new file mode 100644 index 00000000000..7ac3db41382 --- /dev/null +++ b/addons/survey/tests/test_survey_security.py @@ -0,0 +1,325 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from odoo.addons.survey.tests import common +from odoo.exceptions import AccessError, UserError +from odoo.tests import tagged +from odoo.tests.common import users +from odoo.tools import mute_logger + + +@tagged('security') +class TestAccess(common.SurveyCase): + + def setUp(self): + super(TestAccess, self).setUp() + + self.answer_0 = self._add_answer(self.survey, self.customer) + self.answer_0_0 = self._add_answer_line(self.question_ft, self.answer_0, 'Test Answer') + self.answer_0_1 = self._add_answer_line(self.question_num, self.answer_0, 5) + + @mute_logger('odoo.addons.base.models.ir_model') + @users('user_emp') + def test_access_survey_employee(self): + # Create: nope + with self.assertRaises(AccessError): + self.env['survey.survey'].create({'title': 'Test Survey 2'}) + with self.assertRaises(AccessError): + self.env['survey.page'].create({'title': 'My Page', 'survey_id': self.survey.id}) + with self.assertRaises(AccessError): + self.env['survey.question'].create({'question': 'My Question', 'page_id': self.page_0.id}) + + # Read: nope + with self.assertRaises(AccessError): + self.env['survey.survey'].search([('title', 'ilike', 'Test')]) + with self.assertRaises(AccessError): + self.survey.sudo(self.env.user).read(['title']) + + # Write: nope + with self.assertRaises(AccessError): + self.survey.sudo(self.env.user).write({'title': 'New Title'}) + with self.assertRaises(AccessError): + self.page_0.sudo(self.env.user).write({'title': 'New Title'}) + with self.assertRaises(AccessError): + self.question_ft.sudo(self.env.user).write({'question': 'New Title'}) + + # Unlink: nope + with self.assertRaises(AccessError): + self.survey.sudo(self.env.user).unlink() + with self.assertRaises(AccessError): + self.page_0.sudo(self.env.user).unlink() + with self.assertRaises(AccessError): + self.question_ft.sudo(self.env.user).unlink() + + @mute_logger('odoo.addons.base.models.ir_model') + @users('user_portal') + def test_access_survey_portal(self): + # Create: nope + with self.assertRaises(AccessError): + self.env['survey.survey'].create({'title': 'Test Survey 2'}) + with self.assertRaises(AccessError): + self.env['survey.page'].create({'title': 'My Page', 'survey_id': self.survey.id}) + with self.assertRaises(AccessError): + self.env['survey.question'].create({'question': 'My Question', 'page_id': self.page_0.id}) + + # Read: nope + with self.assertRaises(AccessError): + self.env['survey.survey'].search([('title', 'ilike', 'Test')]) + with self.assertRaises(AccessError): + self.survey.sudo(self.env.user).read(['title']) + + # Write: nope + with self.assertRaises(AccessError): + self.survey.sudo(self.env.user).write({'title': 'New Title'}) + with self.assertRaises(AccessError): + self.page_0.sudo(self.env.user).write({'title': 'New Title'}) + with self.assertRaises(AccessError): + self.question_ft.sudo(self.env.user).write({'question': 'New Title'}) + + # Unlink: nope + with self.assertRaises(AccessError): + self.survey.sudo(self.env.user).unlink() + with self.assertRaises(AccessError): + self.page_0.sudo(self.env.user).unlink() + with self.assertRaises(AccessError): + self.question_ft.sudo(self.env.user).unlink() + + @mute_logger('odoo.addons.base.models.ir_model') + @users('user_public') + def test_access_survey_public(self): + # Create: nope + with self.assertRaises(AccessError): + self.env['survey.survey'].create({'title': 'Test Survey 2'}) + with self.assertRaises(AccessError): + self.env['survey.page'].create({'title': 'My Page', 'survey_id': self.survey.id}) + with self.assertRaises(AccessError): + self.env['survey.question'].create({'question': 'My Question', 'page_id': self.page_0.id}) + + # Read: nope + with self.assertRaises(AccessError): + self.env['survey.survey'].search([('title', 'ilike', 'Test')]) + with self.assertRaises(AccessError): + self.survey.sudo(self.env.user).read(['title']) + + # Write: nope + with self.assertRaises(AccessError): + self.survey.sudo(self.env.user).write({'title': 'New Title'}) + with self.assertRaises(AccessError): + self.page_0.sudo(self.env.user).write({'title': 'New Title'}) + with self.assertRaises(AccessError): + self.question_ft.sudo(self.env.user).write({'question': 'New Title'}) + + # Unlink: nope + with self.assertRaises(AccessError): + self.survey.sudo(self.env.user).unlink() + with self.assertRaises(AccessError): + self.page_0.sudo(self.env.user).unlink() + with self.assertRaises(AccessError): + self.question_ft.sudo(self.env.user).unlink() + + @users('survey_manager') + def test_access_survey_survey_manager(self): + # Create: all + survey = self.env['survey.survey'].create({'title': 'Test Survey 2'}) + page = self.env['survey.page'].create({'title': 'My Page', 'survey_id': survey.id}) + self.env['survey.question'].create({'question': 'My Question', 'page_id': page.id}) + + # Read: all + surveys = self.env['survey.survey'].search([('title', 'ilike', 'Test')]) + self.assertEqual(surveys, self.survey | survey) + surveys.read(['title']) + + # Write: all + (self.survey | survey).write({'title': 'New Title'}) + + # Unlink: all + (self.survey | survey).unlink() + + @mute_logger('odoo.addons.base.models.ir_model') + @users('survey_user') + def test_access_survey_survey_user(self): + # Create: own only + survey = self.env['survey.survey'].create({'title': 'Test Survey 2'}) + page = self.env['survey.page'].create({'title': 'My Page', 'survey_id': survey.id}) + self.env['survey.question'].create({'question': 'My Question', 'page_id': page.id}) + + # Read: all + surveys = self.env['survey.survey'].search([('title', 'ilike', 'Test')]) + self.assertEqual(surveys, self.survey | survey) + surveys.read(['title']) + + # Write: own only + survey.write({'title': 'New Title'}) + with self.assertRaises(AccessError): + self.survey.sudo(self.env.user).write({'title': 'New Title'}) + + # Unlink: own only + survey.unlink() + with self.assertRaises(AccessError): + self.survey.sudo(self.env.user).unlink() + + @mute_logger('odoo.addons.base.models.ir_model') + @users('user_emp') + def test_access_answers_employee(self): + # Create: nope + with self.assertRaises(AccessError): + self.env['survey.user_input'].create({'survey_id': self.survey.id}) + with self.assertRaises(AccessError): + self.env['survey.user_input_line'].create({'question_id': self.question_num.id, 'answer_type': 'number', 'value_number': 3, 'user_input_id': self.answer_0.id}) + + # Read: nope + with self.assertRaises(AccessError): + self.env['survey.user_input'].search([('survey_id', 'in', [self.survey.id])]) + with self.assertRaises(AccessError): + self.env['survey.user_input_line'].search([('survey_id', 'in', [self.survey.id])]) + with self.assertRaises(AccessError): + self.env['survey.user_input'].browse(self.answer_0.ids).read(['state']) + with self.assertRaises(AccessError): + self.env['survey.user_input_line'].browse(self.answer_0_0.ids).read(['value_number']) + + # Write: nope + with self.assertRaises(AccessError): + self.answer_0.sudo(self.env.user).write({'state': 'done'}) + + # Unlink: nope + with self.assertRaises(AccessError): + self.answer_0.sudo(self.env.user).unlink() + with self.assertRaises(AccessError): + self.answer_0_0.sudo(self.env.user).unlink() + + @mute_logger('odoo.addons.base.models.ir_model') + @users('user_portal') + def test_access_answers_portal(self): + # Create: nope + with self.assertRaises(AccessError): + self.env['survey.user_input'].create({'survey_id': self.survey.id}) + with self.assertRaises(AccessError): + self.env['survey.user_input_line'].create({'question_id': self.question_num.id, 'answer_type': 'number', 'value_number': 3, 'user_input_id': self.answer_0.id}) + + # Read: nope + with self.assertRaises(AccessError): + self.env['survey.user_input'].search([('survey_id', 'in', [self.survey.id])]) + with self.assertRaises(AccessError): + self.env['survey.user_input_line'].search([('survey_id', 'in', [self.survey.id])]) + with self.assertRaises(AccessError): + self.env['survey.user_input'].browse(self.answer_0.ids).read(['state']) + with self.assertRaises(AccessError): + self.env['survey.user_input_line'].browse(self.answer_0_0.ids).read(['value_number']) + + # Write: nope + with self.assertRaises(AccessError): + self.answer_0.sudo(self.env.user).write({'state': 'done'}) + + # Unlink: nope + with self.assertRaises(AccessError): + self.answer_0.sudo(self.env.user).unlink() + with self.assertRaises(AccessError): + self.answer_0_0.sudo(self.env.user).unlink() + + @mute_logger('odoo.addons.base.models.ir_model') + @users('user_public') + def test_access_answers_public(self): + # Create: nope + with self.assertRaises(AccessError): + self.env['survey.user_input'].create({'survey_id': self.survey.id}) + with self.assertRaises(AccessError): + self.env['survey.user_input_line'].create({'question_id': self.question_num.id, 'answer_type': 'number', 'value_number': 3, 'user_input_id': self.answer_0.id}) + + # Read: nope + with self.assertRaises(AccessError): + self.env['survey.user_input'].search([('survey_id', 'in', [self.survey.id])]) + with self.assertRaises(AccessError): + self.env['survey.user_input_line'].search([('survey_id', 'in', [self.survey.id])]) + with self.assertRaises(AccessError): + self.env['survey.user_input'].browse(self.answer_0.ids).read(['state']) + with self.assertRaises(AccessError): + self.env['survey.user_input_line'].browse(self.answer_0_0.ids).read(['value_number']) + + # Write: nope + with self.assertRaises(AccessError): + self.answer_0.sudo(self.env.user).write({'state': 'done'}) + + # Unlink: nope + with self.assertRaises(AccessError): + self.answer_0.sudo(self.env.user).unlink() + with self.assertRaises(AccessError): + self.answer_0_0.sudo(self.env.user).unlink() + + @mute_logger('odoo.addons.base.models.ir_model') + @users('survey_user') + def test_access_answers_survey_user(self): + survey_own = self.env['survey.survey'].create({'title': 'Other'}) + page_own = self.env['survey.page'].create({'title': 'Other', 'survey_id': survey_own.id}) + question_own = self.env['survey.question'].create({'question': 'Other Question', 'page_id': page_own.id}) + + # Create: own survey only + answer_own = self.env['survey.user_input'].create({'survey_id': survey_own.id}) + answer_line_own = self.env['survey.user_input_line'].create({'question_id': question_own.id, 'answer_type': 'number', 'value_number': 3, 'user_input_id': answer_own.id}) + + # Read: always + answers = self.env['survey.user_input'].search([('survey_id', 'in', [survey_own.id, self.survey.id])]) + self.assertEqual(answers, answer_own | self.answer_0) + + answer_lines = self.env['survey.user_input_line'].search([('survey_id', 'in', [survey_own.id, self.survey.id])]) + self.assertEqual(answer_lines, answer_line_own | self.answer_0_0 | self.answer_0_1) + + self.env['survey.user_input'].browse(answer_own.ids).read(['state']) + self.env['survey.user_input'].browse(self.answer_0.ids).read(['state']) + + self.env['survey.user_input_line'].browse(answer_line_own.ids).read(['value_number']) + self.env['survey.user_input_line'].browse(self.answer_0_0.ids).read(['value_number']) + + # Create: own survey only (moved after read because DB not correctly rollbacked with assertRaises) + with self.assertRaises(AccessError): + answer_other = self.env['survey.user_input'].create({'survey_id': self.survey.id}) + with self.assertRaises(AccessError): + answer_line_other = self.env['survey.user_input_line'].create({'question_id': self.question_num.id, 'answer_type': 'number', 'value_number': 3, 'user_input_id': self.answer_0.id}) + + # Write: own survey only + answer_own.write({'state': 'done'}) + with self.assertRaises(AccessError): + self.answer_0.sudo(self.env.user).write({'state': 'done'}) + + # Unlink: own survey only + answer_own.unlink() + answer_line_own.unlink() + with self.assertRaises(AccessError): + self.answer_0.sudo(self.env.user).unlink() + with self.assertRaises(AccessError): + self.answer_0_0.sudo(self.env.user).unlink() + + @users('survey_manager') + def test_access_answers_survey_manager(self): + admin = self.env.ref('base.user_admin') + with self.sudo(admin): + survey_other = self.env['survey.survey'].create({'title': 'Other'}) + page_other = self.env['survey.page'].create({'title': 'Other', 'survey_id': survey_other.id}) + question_other = self.env['survey.question'].create({'question': 'Other Question', 'page_id': page_other.id}) + self.assertEqual(survey_other.create_uid, admin) + self.assertEqual(question_other.create_uid, admin) + + # Create: always + answer_own = self.env['survey.user_input'].create({'survey_id': self.survey.id}) + answer_other = self.env['survey.user_input'].create({'survey_id': survey_other.id}) + answer_line_own = self.env['survey.user_input_line'].create({'question_id': self.question_num.id, 'answer_type': 'number', 'value_number': 3, 'user_input_id': answer_own.id}) + answer_line_other = self.env['survey.user_input_line'].create({'question_id': question_other.id, 'answer_type': 'number', 'value_number': 3, 'user_input_id': answer_other.id}) + + # Read: always + answers = self.env['survey.user_input'].search([('survey_id', 'in', [survey_other.id, self.survey.id])]) + self.assertEqual(answers, answer_own | answer_other | self.answer_0) + + answer_lines = self.env['survey.user_input_line'].search([('survey_id', 'in', [survey_other.id, self.survey.id])]) + self.assertEqual(answer_lines, answer_line_own | answer_line_other | self.answer_0_0 | self.answer_0_1) + + self.env['survey.user_input'].browse(answer_own.ids).read(['state']) + self.env['survey.user_input'].browse(self.answer_0.ids).read(['state']) + + self.env['survey.user_input_line'].browse(answer_line_own.ids).read(['value_number']) + self.env['survey.user_input_line'].browse(self.answer_0_0.ids).read(['value_number']) + + # Write: always + answer_own.write({'state': 'done'}) + answer_other.write({'partner_id': self.env.user.partner_id.id}) + + # Unlink: always + (answer_own | answer_other | self.answer_0).unlink() From 7c50807bece0ded1333c44fb996405d09f7d40e0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thibault=20Delavall=C3=A9e?= Date: Tue, 4 Dec 2018 13:42:38 +0000 Subject: [PATCH 08/15] [REF] survey: improve access mode options and survey form view In this commit we extend access option on survey model. Access mode is now defined as * public: everyone can access this survey. People having a link to the survey can create answers and fill survey. Those are used for example as satisfaction surveys; * login required: only users can access this survey. Depending on auth signup configuration this either limits survey access or forces people to create an account; * employees only: only users belonging to base.group_user can access this survey. Those are used for example for internal polls; * on invitation only: only people receiving a token can access this survey. They do not need to be an user, having a token is sufficient. Those are used for example for appraisals; Note that survey officers and managers can always create and update answers according to their ACLs. In this commit we also improve a bit survey form view while changing the access field. Surveys users now have access to all buttons and some labels have been updated to better fit current Odoo style. Archive button is now always displayed as it is considered as a standard way of managing surveys. This commit is linked to task ID 1911586 and PR #28986. --- .../data/survey_demo.xml | 2 +- addons/survey/controllers/main.py | 7 ++- addons/survey/data/survey_demo_feedback.xml | 4 +- addons/survey/models/survey_survey.py | 25 +++++++-- addons/survey/tests/common.py | 2 +- addons/survey/tests/test_survey_flow.py | 2 +- addons/survey/views/survey_survey_views.xml | 53 ++++++++----------- addons/survey/views/survey_templates.xml | 2 +- addons/survey/views/survey_user_views.xml | 2 +- 9 files changed, 57 insertions(+), 42 deletions(-) diff --git a/addons/hr_recruitment_survey/data/survey_demo.xml b/addons/hr_recruitment_survey/data/survey_demo.xml index 089b5df481b..2f98596d8db 100644 --- a/addons/hr_recruitment_survey/data/survey_demo.xml +++ b/addons/hr_recruitment_survey/data/survey_demo.xml @@ -3,7 +3,7 @@ Recruitment Form - + token

diff --git a/addons/survey/controllers/main.py b/addons/survey/controllers/main.py index 9a6b8b57337..bf569e733e4 100644 --- a/addons/survey/controllers/main.py +++ b/addons/survey/controllers/main.py @@ -59,8 +59,11 @@ class Survey(http.Controller): if not answer_sudo and ensure_token: return 'token_required' + if not answer_sudo and survey_sudo.access_mode == 'token': + return 'token_required' - if survey_sudo.auth_required and request.env.user._is_public(): + # Public -> no auth required; Token -> token check hereabove + if survey_sudo.access_mode not in ['public', 'token'] and request.env.user._is_public(): return 'survey_auth' if survey_sudo.is_closed or not survey_sudo.active: @@ -99,7 +102,7 @@ class Survey(http.Controller): has_survey_access = True can_answer = bool(answer_sudo) if not can_answer: - can_answer = survey_sudo.auth_required is False + can_answer = survey_sudo.access_mode == 'public' return { 'survey_sudo': survey_sudo, diff --git a/addons/survey/data/survey_demo_feedback.xml b/addons/survey/data/survey_demo_feedback.xml index 24aa3890ac4..8d24117ab47 100644 --- a/addons/survey/data/survey_demo_feedback.xml +++ b/addons/survey/data/survey_demo_feedback.xml @@ -4,7 +4,7 @@ User Feedback Form - + public

This survey allows you to give a feedback about your experience with our eCommerce solution. @@ -229,4 +229,4 @@ skip - + \ No newline at end of file diff --git a/addons/survey/models/survey_survey.py b/addons/survey/models/survey_survey.py index 38da4f40277..0f0f788f029 100644 --- a/addons/survey/models/survey_survey.py +++ b/addons/survey/models/survey_survey.py @@ -35,14 +35,26 @@ class Survey(models.Model): page_ids = fields.One2many('survey.page', 'survey_id', string='Pages', copy=True) user_input_ids = fields.One2many('survey.user_input', 'survey_id', string='User responses', readonly=True, groups='survey.group_survey_user') # security / access - auth_required = fields.Boolean('Login required', help="Users with a public link will be requested to login before taking part to the survey") + access_mode = fields.Selection([ + ('public', 'Everyone'), + ('authentication', 'Login Required'), + ('internal', 'Employees Only'), + ('token', 'Invitation only')], string='Access Mode', + default='authentication', required=True) 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") # statistics tot_sent_survey = fields.Integer("Number of sent surveys", compute="_compute_survey_statistic") 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") + @api.multi + def _compute_users_can_signup(self): + signup_allowed = self.env['res.users'].sudo()._get_signup_invitation_scope() == 'b2c' + for survey in self: + survey.users_can_signup = signup_allowed + @api.multi def _compute_survey_statistic(self): UserInput = self.env['survey.user_input'] @@ -125,8 +137,15 @@ class Survey(models.Model): raise UserError(_('Creating token for archived surveys is not allowed.')) elif self.is_closed: raise UserError(_('Creating token for closed surveys is not allowed.')) - if self.auth_required and (not user or user._is_public()): - raise UserError(_('Creating token for external users is not allowed for surveys requesting authentication.')) + if self.access_mode == 'authentication': + # signup possible -> should have at least a partner to create an account + if self.users_can_signup and not user and not partner: + raise UserError(_('Creating token for external people is not allowed for surveys requesting authentication.')) + # no signup possible -> should be a not public user (employee or portal users) + if not self.users_can_signup and (not user or user._is_public()): + raise UserError(_('Creating token for external people is not allowed for surveys requesting authentication.')) + if self.access_mode == 'internal' and (not user or not user.has_group('base.group_user')): + raise UserError(_('Creating token for anybody else than employees is not allowed for internal surveys.')) @api.model def next_page(self, user_input, page_id, go_back=False): diff --git a/addons/survey/tests/common.py b/addons/survey/tests/common.py index fbe03f3f24b..4235739ba14 100644 --- a/addons/survey/tests/common.py +++ b/addons/survey/tests/common.py @@ -64,7 +64,7 @@ class SurveyCase(common.SavepointCase): self.survey = self.env['survey.survey'].sudo(self.survey_manager).create({ 'title': 'Test Survey', - 'auth_required': True, + 'access_mode': 'authentication', 'users_can_go_back': False, }) self.page_0 = self.env['survey.page'].sudo(self.survey_manager).create({ diff --git a/addons/survey/tests/test_survey_flow.py b/addons/survey/tests/test_survey_flow.py index 537ab1d46d4..11d9ed2a57c 100644 --- a/addons/survey/tests/test_survey_flow.py +++ b/addons/survey/tests/test_survey_flow.py @@ -47,7 +47,7 @@ class TestSurveyFlow(common.SurveyCase, HttpCase): with self.sudo(self.survey_manager): survey = self.env['survey.survey'].create({ 'title': 'Public Survey for Tarte Al Djotte', - 'auth_required': False, + 'access_mode': 'public', }) # First page is about customer data diff --git a/addons/survey/views/survey_survey_views.xml b/addons/survey/views/survey_survey_views.xml index 7c0d391b4af..c6292b549a9 100644 --- a/addons/survey/views/survey_survey_views.xml +++ b/addons/survey/views/survey_survey_views.xml @@ -9,12 +9,11 @@ -

-
@@ -25,7 +24,7 @@ icon="fa-pencil-square-o"> -

@@ -33,24 +32,20 @@
- - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + +
@@ -85,7 +80,7 @@ - + @@ -108,7 +103,7 @@
    -
  • +
  • Test
  • @@ -165,12 +160,10 @@

    You can create surveys for different purposes: customer opinion, services feedback, recruitment interviews, employee's periodical evaluations, marketing campaigns, etc.

    - Design easily your survey, send invitations to answer by email and analyse answers. + Design easily your survey, send invitations to answer by email and analyze answers.

    - - diff --git a/addons/survey/views/survey_templates.xml b/addons/survey/views/survey_templates.xml index 8e8ccf61385..530bcdeffc3 100644 --- a/addons/survey/views/survey_templates.xml +++ b/addons/survey/views/survey_templates.xml @@ -55,7 +55,7 @@

    Login required

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

diff --git a/addons/survey/views/survey_user_views.xml b/addons/survey/views/survey_user_views.xml index 919daed1deb..5344823ed42 100644 --- a/addons/survey/views/survey_user_views.xml +++ b/addons/survey/views/survey_user_views.xml @@ -30,7 +30,7 @@
From 1c5371a966632eef2b6eaf67b755d13f529e7988 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thibault=20Delavall=C3=A9e?= Date: Fri, 21 Dec 2018 12:52:00 +0000 Subject: [PATCH 09/15] [IMP] survey: improve some survey pages Survey has several side pages related to corner cases: closed surveys, void surveys, ... those templates need a bit of polishing. Other main templates are left untouched and will probably be improved in other tasks. This commit is linked to task ID 1911586 and PR #28986. --- addons/survey/controllers/main.py | 6 +++--- addons/survey/views/survey_templates.xml | 16 ++++++++++------ 2 files changed, 13 insertions(+), 9 deletions(-) diff --git a/addons/survey/controllers/main.py b/addons/survey/controllers/main.py index bf569e733e4..712edc23908 100644 --- a/addons/survey/controllers/main.py +++ b/addons/survey/controllers/main.py @@ -117,13 +117,13 @@ class Survey(http.Controller): answer_sudo = access_data['answer_sudo'] if error_key == 'survey_void' and access_data['can_answer']: - return request.render("survey.nopages", {'survey': survey_sudo}) + return request.render("survey.survey_void", {'survey': survey_sudo}) elif error_key == 'survey_closed' and access_data['can_answer']: - return request.render("survey.notopen") + return request.render("survey.survey_expired", {'survey': survey_sudo}) elif error_key == 'survey_auth' and answer_sudo.token: return request.render("survey.auth_required", {'survey': survey_sudo, 'token': answer_sudo.token}) elif error_key == 'answer_deadline' and answer_sudo.token: - return request.render("survey.notopen") + return request.render("survey.survey_expired", {'survey': survey_sudo}) return werkzeug.utils.redirect("/") diff --git a/addons/survey/views/survey_templates.xml b/addons/survey/views/survey_templates.xml index 530bcdeffc3..4bcd81ba462 100644 --- a/addons/survey/views/survey_templates.xml +++ b/addons/survey/views/survey_templates.xml @@ -34,13 +34,13 @@ -