From 1a8c1fd8ce4487e0ea8037e7009965b797c43e8f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thibault=20Delavall=C3=A9e?= Date: Thu, 21 Nov 2019 12:44:11 +0000 Subject: [PATCH 1/3] [MOV] survey: reorganize a bit code and files PURPOSE Purpose is to perform some cleaning in survey models: naming, ordering, dead code [0]. As new features are about to land in survey, notably live interactions [1] and new survey building [2] performing a pre cleaning is necessary [3]. SPECIFICATIONS Code move main points * move fields to have them grouped by main use; * move methods to have them grouped by main purpose; * reorganize some line breaks to ease reading files; Globally this code move is done according to our famous guidelines. No functional changes should occur with this commit. It contains only code move, not any change at all. LINKS [0] Related to Task ID 2144393 (surveys split / cleaning) [1] Task ID 1972640 (live interactions) [2] Task ID 2119587 (new frontend for building surveys) [3] Task ID 2061901 (survey models cleaning and preparation) PR #40623 --- addons/survey/models/survey_question.py | 99 ++++++++++---------- addons/survey/models/survey_survey.py | 44 +++++---- addons/survey/models/survey_user.py | 117 ++++++++++++------------ addons/survey/wizard/survey_invite.py | 4 +- 4 files changed, 128 insertions(+), 136 deletions(-) diff --git a/addons/survey/models/survey_question.py b/addons/survey/models/survey_question.py index 7271e8c88a5..3130004d617 100644 --- a/addons/survey/models/survey_question.py +++ b/addons/survey/models/survey_question.py @@ -50,7 +50,6 @@ class SurveyQuestion(models.Model): That makes the use and display of these information at view and controller levels easier to understand. """ - _name = 'survey.question' _description = 'Survey Question' _rec_name = 'question' @@ -63,23 +62,24 @@ class SurveyQuestion(models.Model): defaults['question_type'] = False if defaults.get('is_page') == True else 'free_text' return defaults - # Question metadata + # question generic data + title = fields.Char('Title', required=True, translate=True) + question = fields.Char('Question', related="title") + description = fields.Html('Description', help="Use this field to add additional explanations about your question", translate=True) survey_id = fields.Many2one('survey.survey', string='Survey', ondelete='cascade') - page_id = fields.Many2one('survey.question', string='Page', compute="_compute_page_id", store=True) - question_ids = fields.One2many('survey.question', string='Questions', compute="_compute_question_ids") scoring_type = fields.Selection(related='survey_id.scoring_type', string='Scoring Type', readonly=True) sequence = fields.Integer('Sequence', default=10) - # Question + # page specific is_page = fields.Boolean('Is a page?') + question_ids = fields.One2many('survey.question', string='Questions', compute="_compute_question_ids") questions_selection = fields.Selection( related='survey_id.questions_selection', readonly=True, help="If randomized is selected, add the number of random questions next to the section.") random_questions_count = fields.Integer( 'Random questions count', default=1, help="Used on randomized sections to take X random questions from all the questions of that section.") - title = fields.Char('Title', required=True, translate=True) - question = fields.Char('Question', related="title") - description = fields.Html('Description', help="Use this field to add additional explanations about your question", translate=True) + # question specific + page_id = fields.Many2one('survey.question', string='Page', compute="_compute_page_id", store=True) question_type = fields.Selection([ ('free_text', 'Multiple Lines Text Box'), ('textbox', 'Single Line Text Box'), @@ -89,18 +89,18 @@ class SurveyQuestion(models.Model): ('simple_choice', 'Multiple choice: only one answer'), ('multiple_choice', 'Multiple choice: multiple answers allowed'), ('matrix', 'Matrix')], string='Question Type') - # simple choice / multiple choice / matrix + # -- 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 matrix_subtype = fields.Selection([ ('simple', 'One choice per row'), ('multiple', 'Multiple choices per row')], string='Matrix Type', default='simple') 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 + # -- display options column_nb = fields.Selection([ ('12', '1'), ('6', '2'), ('4', '3'), ('3', '4'), ('2', '6')], string='Number of columns', default='12', @@ -108,11 +108,11 @@ class SurveyQuestion(models.Model): display_mode = fields.Selection( [('columns', 'Radio Buttons'), ('dropdown', 'Selection Box')], string='Display Mode', default='columns', help='Display mode of simple choice questions.') - # Comments + # -- comments 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') - # Validation + # question validation validation_required = fields.Boolean('Validate entry') validation_email = fields.Boolean('Input must be an email') validation_length_min = fields.Integer('Minimum Text Length') @@ -124,10 +124,9 @@ class SurveyQuestion(models.Model): validation_min_datetime = fields.Datetime('Minimum Datetime') validation_max_datetime = fields.Datetime('Maximum Datetime') validation_error_msg = fields.Char('Validation Error message', translate=True, default=lambda self: _("The answer you entered is not valid.")) - # Constraints on number of answers (matrices) constr_mandatory = fields.Boolean('Mandatory Answer') constr_error_msg = fields.Char('Error message', translate=True, default=lambda self: _("This question requires an answer.")) - # Answer + # answers user_input_line_ids = fields.One2many( 'survey.user_input_line', 'question_id', string='Answers', domain=[('skipped', '=', False)], groups='survey.group_survey_user') @@ -151,6 +150,39 @@ class SurveyQuestion(models.Model): if self.is_page: self.question_type = False + @api.depends('survey_id.question_and_page_ids.is_page', 'survey_id.question_and_page_ids.sequence') + def _compute_question_ids(self): + """Will take all questions of the survey for which the index is higher than the index of this page + and lower than the index of the next page.""" + for question in self: + if question.is_page: + next_page_index = False + for page in question.survey_id.page_ids: + if page._index() > question._index(): + next_page_index = page._index() + break + + question.question_ids = question.survey_id.question_ids.filtered(lambda q: + q._index() > question._index() and (not next_page_index or q._index() < next_page_index)) + else: + question.question_ids = self.env['survey.question'] + + @api.depends('survey_id.question_and_page_ids.is_page', 'survey_id.question_and_page_ids.sequence') + def _compute_page_id(self): + """Will find the page to which this question belongs to by looking inside the corresponding survey""" + for question in self: + if question.is_page: + question.page_id = None + else: + question.page_id = next( + (iter(question + .survey_id + .question_and_page_ids + .filtered(lambda q: q.is_page and q.sequence < question.sequence) + .sorted(reverse=True))), + None + ) + # Validation methods def validate_question(self, answer, comment=None): """ Validate question, depending on question type and parameters @@ -249,39 +281,6 @@ class SurveyQuestion(models.Model): return {self.id: self.constr_error_msg} return {} - @api.depends('survey_id.question_and_page_ids.is_page', 'survey_id.question_and_page_ids.sequence') - def _compute_question_ids(self): - """Will take all questions of the survey for which the index is higher than the index of this page - and lower than the index of the next page.""" - for question in self: - if question.is_page: - next_page_index = False - for page in question.survey_id.page_ids: - if page._index() > question._index(): - next_page_index = page._index() - break - - question.question_ids = question.survey_id.question_ids.filtered(lambda q: - q._index() > question._index() and (not next_page_index or q._index() < next_page_index)) - else: - question.question_ids = self.env['survey.question'] - - @api.depends('survey_id.question_and_page_ids.is_page', 'survey_id.question_and_page_ids.sequence') - def _compute_page_id(self): - """Will find the page to which this question belongs to by looking inside the corresponding survey""" - for question in self: - if question.is_page: - question.page_id = None - else: - question.page_id = next( - (iter(question - .survey_id - .question_and_page_ids - .filtered(lambda q: q.is_page and q.sequence < question.sequence) - .sorted(reverse=True))), - None - ) - def _index(self): """We would normally just use the 'sequence' field of questions BUT, if the pages and questions are created without ever moving records around, the sequence field can be set to 0 for all the questions. @@ -295,6 +294,7 @@ class SurveyQuestion(models.Model): return self.labels_ids.filtered(lambda label: label.is_correct) + class SurveyLabel(models.Model): """ A suggested answer for a question """ _name = 'survey.label' @@ -307,8 +307,7 @@ class SurveyLabel(models.Model): sequence = fields.Integer('Label Sequence order', default=10) value = fields.Char('Suggested value', translate=True, required=True) is_correct = fields.Boolean('Is a correct answer') - answer_score = fields.Float('Score for this choice', - help="A positive score indicates a correct choice; a negative or null score indicates a wrong answer") + answer_score = fields.Float('Score for this choice', help="A positive score indicates a correct choice; a negative or null score indicates a wrong answer") @api.constrains('question_id', 'question_id_2') def _check_question_not_empty(self): diff --git a/addons/survey/models/survey_survey.py b/addons/survey/models/survey_survey.py index 92030be9cf8..3d8731c937f 100644 --- a/addons/survey/models/survey_survey.py +++ b/addons/survey/models/survey_survey.py @@ -26,23 +26,24 @@ class Survey(models.Model): # description title = fields.Char('Survey Title', required=True, translate=True) - description = fields.Html("Description", translate=True, + description = fields.Html( + "Description", translate=True, help="The description will be displayed on the home page of the survey. You can use this to give the purpose and guidelines to your candidates before they start it.") color = fields.Integer('Color Index', default=0) thank_you_message = fields.Html("Thanks Message", translate=True, help="This message will be displayed when survey is completed") active = fields.Boolean("Active", default=True) + state = fields.Selection(selection=[ + ('draft', 'Draft'), ('open', 'In Progress'), ('closed', 'Closed') + ], string="Survey Stage", default='draft', required=True, + group_expand='_read_group_states') + category = fields.Selection([ + ('default', 'Generic Survey')], string='Category', + default='default', required=True, + help='Category is used to know in which context the survey is used. Various apps may define their own categories when they use survey like jobs recruitment or employee appraisal surveys.') + # questions question_and_page_ids = fields.One2many('survey.question', 'survey_id', string='Sections and Questions', copy=True) page_ids = fields.One2many('survey.question', string='Pages', compute="_compute_page_and_question_ids") question_ids = fields.One2many('survey.question', string='Questions', compute="_compute_page_and_question_ids") - state = fields.Selection( - string="Survey Stage", - selection=[ - ('draft', 'Draft'), - ('open', 'In Progress'), - ('closed', 'Closed'), - ], default='draft', required=True, - group_expand='_read_group_states' - ) questions_layout = fields.Selection([ ('one_page', 'One page with all the questions'), ('page_per_section', 'One page per section'), @@ -53,12 +54,7 @@ class Survey(models.Model): ('random', 'Randomized per section')], string="Selection", required=True, default='all', help="If randomized is selected, add the number of random questions next to the section.") - - category = fields.Selection([ - ('default', 'Generic Survey')], string='Category', - default='default', required=True, - help='Category is used to know in which context the survey is used. Various apps may define their own categories when they use survey like jobs recruitment or employee appraisal surveys.') - # content + # attendees user_input_ids = fields.One2many('survey.user_input', 'survey_id', string='User responses', readonly=True, groups='survey.group_survey_user') # security / access access_mode = fields.Selection([ @@ -76,15 +72,14 @@ class Survey(models.Model): answer_score_avg = fields.Float("Avg Score %", compute="_compute_survey_statistic") success_count = fields.Integer("Success", compute="_compute_survey_statistic") success_ratio = fields.Integer("Success Ratio", compute="_compute_survey_statistic") - # scoring and certification fields + # scoring / certification scoring_type = fields.Selection([ ('no_scoring', 'No scoring'), ('scoring_with_answers', 'Scoring with answers at the end'), ('scoring_without_answers', 'Scoring without answers at the end')], string="Scoring", required=True, default='no_scoring') passing_score = fields.Float('Passing score (%)', required=True, default=80.0) - is_attempts_limited = fields.Boolean('Limited number of attempts', - help="Check this option if you want to limit the number of attempts per user") + is_attempts_limited = fields.Boolean('Limited number of attempts', help="Check this option if you want to limit the number of attempts per user") attempts_limit = fields.Integer('Number of attempts', default=1) is_time_limited = fields.Boolean('The survey is limited in time') time_limit = fields.Float("Time limit (minutes)") @@ -93,7 +88,6 @@ class Survey(models.Model): 'mail.template', 'Email Template', domain="[('model', '=', 'survey.user_input')]", help="Automated email sent to the user when he succeeds the certification, containing his certification document.") - # Certification badge # certification_badge_id_dummy is used to have two different behaviours in the form view : # - If the certification badge is not set, show certification_badge_id and only display create option in the m2o @@ -218,7 +212,7 @@ class Survey(models.Model): return super(Survey, self).copy_data(default) # ------------------------------------------------------------ - # TECHNICAL + # ANSWER MANAGEMENT # ------------------------------------------------------------ def _create_answer(self, user=False, partner=False, email=False, test_entry=False, check_attempts=True, **additional_vals): @@ -346,8 +340,9 @@ class Survey(models.Model): return self.attempts_limit - self.env['survey.user_input'].search_count(domain) # ------------------------------------------------------------ - # ACTIONS + # QUESTIONS MANAGEMENT # ------------------------------------------------------------ + @api.model def _get_pages_or_questions(self, user_input): if self.questions_layout == 'one_page': @@ -375,7 +370,6 @@ class Survey(models.Model): return previous_page_id - @api.model def next_page_or_question(self, user_input, page_or_question_id): """ The next page to display to the user, knowing that page_id is the id @@ -433,6 +427,10 @@ class Survey(models.Model): questions = questions & answer.question_ids return questions, page_or_question_id + # ------------------------------------------------------------ + # ACTIONS + # ------------------------------------------------------------ + def action_draft(self): self.write({'state': 'draft'}) diff --git a/addons/survey/models/survey_user.py b/addons/survey/models/survey_user.py index 37e822da1ba..3d02ea50093 100644 --- a/addons/survey/models/survey_user.py +++ b/addons/survey/models/survey_user.py @@ -25,18 +25,15 @@ def dict_keys_startswith(dictionary, string): class SurveyUserInput(models.Model): """ Metadata for a set of one user's answers to a particular survey """ - _name = "survey.user_input" _rec_name = 'survey_id' _description = 'Survey User Input' - # description + # answer description survey_id = fields.Many2one('survey.survey', string='Survey', required=True, readonly=True, ondelete='cascade') scoring_type = fields.Selection(string="Scoring", related="survey_id.scoring_type") - is_attempts_limited = fields.Boolean("Limited number of attempts", related='survey_id.is_attempts_limited') - attempts_limit = fields.Integer("Number of attempts", related='survey_id.attempts_limit') start_datetime = fields.Datetime('Start date and time', readonly=True) - is_time_limit_reached = fields.Boolean("Is time limit reached?", compute='_compute_is_time_limit_reached') + deadline = fields.Datetime('Deadline', help="Datetime until customer can open the survey and submit answers") input_type = fields.Selection([ ('manually', 'Manual'), ('link', 'Invitation')], string='Answer Type', default='manually', required=True, readonly=True) @@ -45,24 +42,26 @@ class SurveyUserInput(models.Model): ('skip', 'Partially completed'), ('done', 'Completed')], string='Status', default='new', readonly=True) test_entry = fields.Boolean(readonly=True) - # identification and access + last_displayed_page_id = fields.Many2one('survey.question', string='Last displayed question/page') + # attempts management + is_attempts_limited = fields.Boolean("Limited number of attempts", related='survey_id.is_attempts_limited') + attempts_limit = fields.Integer("Number of attempts", related='survey_id.attempts_limit') + attempt_number = fields.Integer("Attempt n°", compute='_compute_attempt_number') + is_time_limit_reached = fields.Boolean("Is time limit reached?", compute='_compute_is_time_limit_reached') + # identification / access token = fields.Char('Identification token', default=lambda self: str(uuid.uuid4()), readonly=True, required=True, copy=False) - # no unique constraint, as it identifies a pool of attempts - invite_token = fields.Char('Invite token', readonly=True, copy=False) + invite_token = fields.Char('Invite token', readonly=True, copy=False) # no unique constraint, as it identifies a pool of attempts partner_id = fields.Many2one('res.partner', string='Partner', readonly=True) email = fields.Char('E-mail', readonly=True) - attempt_number = fields.Integer("Attempt n°", compute='_compute_attempt_number') - - # Displaying data - last_displayed_page_id = fields.Many2one('survey.question', string='Last displayed question/page') - # answers + # questions / answers user_input_line_ids = fields.One2many('survey.user_input_line', 'user_input_id', string='Answers', copy=True) - # Pre-defined questions question_ids = fields.Many2many('survey.question', string='Predefined Questions', readonly=True) - deadline = fields.Datetime('Deadline', help="Datetime until customer can open the survey and submit answers") - # Stored for performance reasons while displaying results page - quizz_score = fields.Float("Score (%)", compute="_compute_quizz_score", store=True, compute_sudo=True) - quizz_passed = fields.Boolean('Quizz Passed', compute='_compute_quizz_passed', store=True, compute_sudo=True) + quizz_score = fields.Float("Score (%)", compute="_compute_quizz_score", store=True, compute_sudo=True) # stored for perf reasons + quizz_passed = fields.Boolean('Quizz Passed', compute='_compute_quizz_passed', store=True, compute_sudo=True) # stored for perf reasons + + _sql_constraints = [ + ('unique_token', 'UNIQUE (token)', 'A token must be unique!'), + ] @api.depends('user_input_line_ids.answer_score', 'user_input_line_ids.question_id') def _compute_quizz_score(self): @@ -83,49 +82,6 @@ class SurveyUserInput(models.Model): for user_input in self: user_input.quizz_passed = user_input.quizz_score >= user_input.survey_id.passing_score - _sql_constraints = [ - ('unique_token', 'UNIQUE (token)', 'A token must be unique!'), - ] - - @api.model - def do_clean_emptys(self): - """ Remove empty user inputs that have been created manually - (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'), - ('create_date', '<', an_hour_ago)]).unlink() - - @api.model - def _generate_invite_token(self): - return str(uuid.uuid4()) - - def action_resend(self): - partners = self.env['res.partner'] - emails = [] - for user_answer in self: - if user_answer.partner_id: - partners |= user_answer.partner_id - elif user_answer.email: - emails.append(user_answer.email) - - return self.survey_id.with_context( - default_existing_mode='resend', - default_partner_ids=partners.ids, - default_emails=','.join(emails) - ).action_send_survey() - - 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': '/survey/print/%s?answer_token=%s' % (self.survey_id.access_token, self.token) - } - @api.depends('start_datetime', 'survey_id.is_time_limited', 'survey_id.time_limit') def _compute_is_time_limit_reached(self): """ Checks that the user_input is not exceeding the survey's time limit. """ @@ -167,6 +123,45 @@ class SurveyUserInput(models.Model): user_input.attempt_number = attempt_number + @api.model + def do_clean_emptys(self): + """ Remove empty user inputs that have been created manually + (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'), + ('create_date', '<', an_hour_ago)]).unlink() + + def action_resend(self): + partners = self.env['res.partner'] + emails = [] + for user_answer in self: + if user_answer.partner_id: + partners |= user_answer.partner_id + elif user_answer.email: + emails.append(user_answer.email) + + return self.survey_id.with_context( + default_existing_mode='resend', + default_partner_ids=partners.ids, + default_emails=','.join(emails) + ).action_send_survey() + + 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': '/survey/print/%s?answer_token=%s' % (self.survey_id.access_token, self.token) + } + + @api.model + def _generate_invite_token(self): + return str(uuid.uuid4()) + def _mark_done(self): """ This method will: 1. mark the state as 'done' diff --git a/addons/survey/wizard/survey_invite.py b/addons/survey/wizard/survey_invite.py index 48a6ee57c31..52d6987efdc 100644 --- a/addons/survey/wizard/survey_invite.py +++ b/addons/survey/wizard/survey_invite.py @@ -150,9 +150,9 @@ class SurveyInvite(models.TransientModel): values['body'] = template.body_html return super(SurveyInvite, self).create(values) - #------------------------------------------------------ + # ------------------------------------------------------ # Wizard validation and send - #------------------------------------------------------ + # ------------------------------------------------------ def _prepare_answers(self, partners, emails): answers = self.env['survey.user_input'] From b6bbe5171c91bf82876843d5f0646b727a3d11b6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thibault=20Delavall=C3=A9e?= Date: Thu, 21 Nov 2019 12:55:56 +0000 Subject: [PATCH 2/3] [REF] survey: remove dead or duplicated code PURPOSE Purpose is to perform some cleaning in survey models: naming, ordering, dead code [0]. As new features are about to land in survey, notably live interactions [1] and new survey building [2] performing a pre cleaning is necessary [3]. SPECIFICATIONS Remove dead code : * old code bits not used anymore since several code cleanings have already been performed; * use tools when possible instead of manually checking stuff like email validation; * clean unused imports; No functional changes should occur with this commit. It contains only code move, not any change at all. LINKS [0] Related to Task ID 2144393 (surveys split / cleaning) [1] Task ID 1972640 (live interactions) [2] Task ID 2119587 (new frontend for building surveys) [3] Task ID 2061901 (survey models cleaning and preparation) PR #40623 --- addons/survey/models/survey_question.py | 17 +---------------- addons/survey/models/survey_survey.py | 2 +- addons/survey/models/survey_user.py | 14 ++------------ 3 files changed, 4 insertions(+), 29 deletions(-) diff --git a/addons/survey/models/survey_question.py b/addons/survey/models/survey_question.py index 3130004d617..db6f8f43e6f 100644 --- a/addons/survey/models/survey_question.py +++ b/addons/survey/models/survey_question.py @@ -1,24 +1,9 @@ # -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. -import logging -import re -import datetime - from odoo import api, fields, models, tools, _ from odoo.exceptions import ValidationError -email_validator = re.compile(r"[^@]+@[^@]+\.[^@]+") -_logger = logging.getLogger(__name__) - - -def dict_keys_startswith(dictionary, string): - """Returns a dictionary containing the elements of whose keys start with . - .. note:: - This function uses dictionary comprehensions (Python >= 2.7) - """ - return {k: v for k, v in dictionary.items() if k.startswith(string)} - class SurveyQuestion(models.Model): """ Questions that will be asked in a survey. @@ -220,7 +205,7 @@ class SurveyQuestion(models.Model): # Email format validation # all the strings of the form "@." will be accepted if self.validation_email: - if not email_validator.match(answer): + if not tools.email_normalize(answer): return {self.id: _('This answer must be an email address')} # Answer validation (if properly defined) diff --git a/addons/survey/models/survey_survey.py b/addons/survey/models/survey_survey.py index 3d8731c937f..a63f12451e9 100644 --- a/addons/survey/models/survey_survey.py +++ b/addons/survey/models/survey_survey.py @@ -1,12 +1,12 @@ # -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. +import random import uuid from collections import Counter, OrderedDict from itertools import product from werkzeug import urls -import random from odoo import api, fields, models, _ from odoo.exceptions import UserError diff --git a/addons/survey/models/survey_user.py b/addons/survey/models/survey_user.py index 3d02ea50093..926221ed758 100644 --- a/addons/survey/models/survey_user.py +++ b/addons/survey/models/survey_user.py @@ -3,26 +3,16 @@ import datetime import logging -import re import uuid +from dateutil.relativedelta import relativedelta + from odoo import api, fields, models, _ from odoo.exceptions import ValidationError -from dateutil.relativedelta import relativedelta - -email_validator = re.compile(r"[^@]+@[^@]+\.[^@]+") _logger = logging.getLogger(__name__) -def dict_keys_startswith(dictionary, string): - """Returns a dictionary containing the elements of whose keys start with . - .. note:: - This function uses dictionary comprehensions (Python >= 2.7) - """ - return {k: v for k, v in dictionary.items() if k.startswith(string)} - - class SurveyUserInput(models.Model): """ Metadata for a set of one user's answers to a particular survey """ _name = "survey.user_input" From ca1c975d78e3ba0f157a8fabb43787d9b584f920 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thibault=20Delavall=C3=A9e?= Date: Fri, 22 Nov 2019 09:42:47 +0000 Subject: [PATCH 3/3] [REF] survey: split templates into main files and rename templates PURPOSE Purpose is to perform some cleaning in survey models: naming, ordering, dead code [0]. As new features are about to land in survey, notably live interactions [1] and new survey building [2] performing a pre cleaning is necessary [3]. SPECIFICATIONS Remove dead code : * split templates into 3 main files * survey_templates: holding main templates when taking a survey; * survey_templates_statistics: graph and statistics (results) of a survey; * survey_templates_management: templates linked to corner cases and error management (403, closed survey, ...) as well as tools and utility templates; * rename template ids to ease code understanding; No functional changes should occur with this commit. It contains only code move, not any change at all. LINKS [0] Related to Task ID 2144393 (surveys split / cleaning) [1] Task ID 1972640 (live interactions) [2] Task ID 2119587 (new frontend for building surveys) [3] Task ID 2061901 (survey models cleaning and preparation) PR #40623 --- addons/survey/__manifest__.py | 2 + addons/survey/controllers/main.py | 31 +- addons/survey/views/survey_templates.xml | 738 +++--------------- .../views/survey_templates_management.xml | 159 ++++ .../views/survey_templates_statistics.xml | 363 +++++++++ .../views/survey_templates.xml | 2 +- 6 files changed, 652 insertions(+), 643 deletions(-) create mode 100644 addons/survey/views/survey_templates_management.xml create mode 100644 addons/survey/views/survey_templates_statistics.xml diff --git a/addons/survey/__manifest__.py b/addons/survey/__manifest__.py index f23302b5d84..99b1d6717fb 100644 --- a/addons/survey/__manifest__.py +++ b/addons/survey/__manifest__.py @@ -35,6 +35,8 @@ sent mails with personal token for the invitation of the survey. 'views/survey_user_views.xml', 'views/survey_question_views.xml', 'views/survey_templates.xml', + 'views/survey_templates_management.xml', + 'views/survey_templates_statistics.xml', 'views/gamification_badge_views.xml', 'wizard/survey_invite_views.xml', 'views/res_partner_views.xml', diff --git a/addons/survey/controllers/main.py b/addons/survey/controllers/main.py index ba124dc9632..83e957d8b1e 100644 --- a/addons/survey/controllers/main.py +++ b/addons/survey/controllers/main.py @@ -124,9 +124,9 @@ class Survey(http.Controller): answer_sudo = access_data['answer_sudo'] if error_key == 'survey_void' and access_data['can_answer']: - return request.render("survey.survey_void", {'survey': survey_sudo, 'answer': answer_sudo}) + return request.render("survey.survey_void_content", {'survey': survey_sudo, 'answer': answer_sudo}) elif error_key == 'survey_closed' and access_data['can_answer']: - return request.render("survey.survey_expired", {'survey': survey_sudo}) + return request.render("survey.survey_closed_expired", {'survey': survey_sudo}) elif error_key == 'survey_auth' and answer_sudo.token: if answer_sudo.partner_id and (answer_sudo.partner_id.user_ids or survey_sudo.users_can_signup): if answer_sudo.partner_id.user_ids: @@ -136,11 +136,11 @@ class Survey(http.Controller): redirect_url = answer_sudo.partner_id._get_signup_url_for_action(url='/survey/start/%s?answer_token=%s' % (survey_sudo.access_token, answer_sudo.token))[answer_sudo.partner_id.id] else: redirect_url = '/web/login?redirect=%s' % ('/survey/start/%s?answer_token=%s' % (survey_sudo.access_token, answer_sudo.token)) - return request.render("survey.auth_required", {'survey': survey_sudo, 'redirect_url': redirect_url}) + return request.render("survey.survey_auth_required", {'survey': survey_sudo, 'redirect_url': redirect_url}) elif error_key == 'answer_deadline' and answer_sudo.token: - return request.render("survey.survey_expired", {'survey': survey_sudo}) + return request.render("survey.survey_closed_expired", {'survey': survey_sudo}) elif error_key == 'answer_done' and answer_sudo.token: - return request.render("survey.sfinished", self._prepare_survey_finished_values(survey_sudo, answer_sudo, token=answer_sudo.token)) + return request.render("survey.survey_closed_finished", self._prepare_survey_finished_values(survey_sudo, answer_sudo, token=answer_sudo.token)) return werkzeug.utils.redirect("/") @@ -214,12 +214,12 @@ class Survey(http.Controller): except: return werkzeug.utils.redirect("/") else: - return request.render("survey.403", {'survey': survey_sudo}) + return request.render("survey.survey_403_page", {'survey': survey_sudo}) # Select the right page if answer_sudo.state == 'new': # Intro page data = {'survey': survey_sudo, 'answer': answer_sudo, 'page': 0} - return request.render('survey.survey_init', data) + return request.render('survey.survey_page_start', data) else: return request.redirect('/survey/fill/%s/%s' % (survey_sudo.access_token, answer_sudo.token)) @@ -248,7 +248,7 @@ class Survey(http.Controller): 'answer': answer_sudo, 'previous_page_id': new_previous_id }) - return request.render('survey.survey', data) + return request.render('survey.survey_page_main', data) if survey_sudo.is_time_limited and not answer_sudo.start_datetime: # init start date when user starts filling in the survey @@ -266,9 +266,9 @@ class Survey(http.Controller): }) if is_last: data.update({'last': True}) - return request.render('survey.survey', data) + return request.render('survey.survey_page_main', data) elif answer_sudo.state == 'done': # Display success message - return request.render('survey.sfinished', self._prepare_survey_finished_values(survey_sudo, answer_sudo)) + return request.render('survey.survey_closed_finished', self._prepare_survey_finished_values(survey_sudo, answer_sudo)) elif answer_sudo.state == 'skip': page_or_question_id, is_last = survey_sudo.next_page_or_question(answer_sudo, answer_sudo.last_displayed_page_id.id) previous_id = survey_sudo._previous_page_or_question_id(answer_sudo, page_or_question_id.id) @@ -282,9 +282,9 @@ class Survey(http.Controller): if is_last: data.update({'last': True}) - return request.render('survey.survey', data) + return request.render('survey.survey_page_main', data) else: - return request.render("survey.403", {'survey': survey_sudo}) + return request.render("survey.survey_403_page", {'survey': survey_sudo}) @http.route('/survey/submit//', type='json', auth='public', website=True) def survey_submit(self, survey_token, answer_token, **post): @@ -366,9 +366,9 @@ class Survey(http.Controller): survey_sudo, answer_sudo = access_data['survey_sudo'], access_data['answer_sudo'] if survey_sudo.scoring_type == 'scoring_without_answers': - return request.render("survey.403", {'survey': survey_sudo}) + return request.render("survey.survey_403_page", {'survey': survey_sudo}) - return request.render('survey.survey_print', { + return request.render('survey.survey_page_print', { 'review': review, 'survey': survey_sudo, 'answer': answer_sudo, @@ -379,7 +379,6 @@ class Survey(http.Controller): @http.route('/survey/results/', type='http', auth='user', website=True) def survey_report(self, survey, answer_token=None, **post): '''Display survey Results & Statistics for given survey.''' - result_template = 'survey.result' current_filters = [] filter_display_data = [] @@ -389,7 +388,7 @@ class Survey(http.Controller): 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, + return request.render('survey.survey_page_statistics', {'survey': survey, 'answers': answers, 'survey_dict': self._prepare_result_dict(survey, current_filters), diff --git a/addons/survey/views/survey_templates.xml b/addons/survey/views/survey_templates.xml index c24a45addba..b11c32d909c 100644 --- a/addons/survey/views/survey_templates.xml +++ b/addons/survey/views/survey_templates.xml @@ -1,154 +1,7 @@ - - - - - - - - - - - - - - - - - - - - - + -