From 06363470992fb5315976b11cd72ee627a89c75e8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thibault=20Delavall=C3=A9e?= Date: Thu, 29 Aug 2019 12:39:54 +0000 Subject: [PATCH] [IMP] survey: reorder some code to ease understanding PURPOSE Perform a back2basics review of survey to improve daily use, new user onboarding and labelling. SPECIFICATIONS Reorder some code to better understand module organization. LINKS Task 2039068 PR #36229 --- addons/survey/controllers/main.py | 12 + addons/survey/models/survey_survey.py | 334 ++++++++++++---------- addons/survey/models/survey_user.py | 4 +- addons/survey/views/survey_user_views.xml | 26 +- 4 files changed, 204 insertions(+), 172 deletions(-) diff --git a/addons/survey/controllers/main.py b/addons/survey/controllers/main.py index 045661c5d75..9ab1f366ac3 100644 --- a/addons/survey/controllers/main.py +++ b/addons/survey/controllers/main.py @@ -20,6 +20,10 @@ _logger = logging.getLogger(__name__) class Survey(http.Controller): + # ------------------------------------------------------------ + # ACCESS + # ------------------------------------------------------------ + def _fetch_from_access_token(self, survey_token, answer_token): """ Check that given token matches an answer from the given survey_id. Returns a sudo-ed browse record of survey in order to avoid access rights @@ -182,6 +186,10 @@ class Survey(http.Controller): 'deadline': answer.deadline, } + # ------------------------------------------------------------ + # TAKING SURVEY ROUTES + # ------------------------------------------------------------ + @http.route('/survey/start/', type='http', auth='public', website=True) def survey_start(self, survey_token, answer_token=None, email=False, **post): """ Start a survey by providing @@ -423,6 +431,10 @@ class Survey(http.Controller): return json.dumps(ret) + # ------------------------------------------------------------ + # COMPLETED SURVEY ROUTES + # ------------------------------------------------------------ + @http.route('/survey/print/', type='http', auth='public', website=True) def survey_print(self, survey_token, review=False, answer_token=None, **post): '''Display an survey in printable view; if is set, it will diff --git a/addons/survey/models/survey_survey.py b/addons/survey/models/survey_survey.py index 22d03ad9308..d3ea6462a5a 100644 --- a/addons/survey/models/survey_survey.py +++ b/addons/survey/models/survey_survey.py @@ -8,7 +8,7 @@ from itertools import product from werkzeug import urls import random -from odoo import api, fields, models, SUPERUSER_ID, _ +from odoo import api, fields, models, _ from odoo.exceptions import UserError from odoo.osv import expression @@ -188,7 +188,10 @@ class Survey(models.Model): if not self.users_login_required or not self.certificate: self.certification_give_badge = False + # ------------------------------------------------------------ # CRUD + # ------------------------------------------------------------ + @api.model def create(self, vals): survey = super(Survey, self).create(vals) @@ -202,12 +205,15 @@ class Survey(models.Model): return self.sudo()._handle_certification_badges(vals) return result - # Public methods # def copy_data(self, default=None): title = _("%s (copy)") % (self.title) default = dict(default or {}, title=title) return super(Survey, self).copy_data(default) + # ------------------------------------------------------------ + # TECHNICAL + # ------------------------------------------------------------ + def _create_answer(self, user=False, partner=False, email=False, test_entry=False, check_attempts=True, **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 @@ -278,6 +284,60 @@ class Survey(models.Model): if check_attempts and not self._has_attempts_left(partner or (user and user.partner_id), email, invite_token): raise UserError(_('No attempts left.')) + def _prepare_answer_questions(self): + """ Will generate the questions for a randomized survey. + It uses the random_questions_count of every sections of the survey to + pick a random number of questions and returns the merged recordset """ + self.ensure_one() + + questions = self.env['survey.question'] + + for page in self.page_ids: + if self.questions_selection == 'all': + questions |= page.question_ids + else: + if page.random_questions_count > 0 and len(page.question_ids) > page.random_questions_count: + questions = questions.concat(*random.sample(page.question_ids, page.random_questions_count)) + else: + questions |= page.question_ids + + if not questions: + questions = self.question_ids + + return questions + + def _has_attempts_left(self, partner, email, invite_token): + self.ensure_one() + + if (self.access_mode != 'public' or self.users_login_required) and self.is_attempts_limited: + return self._get_number_of_attempts_lefts(partner, email, invite_token) > 0 + + return True + + def _get_number_of_attempts_lefts(self, partner, email, invite_token): + """ Returns the number of attempts left. """ + self.ensure_one() + + domain = [ + ('survey_id', '=', self.id), + ('test_entry', '=', False), + ('state', '=', 'done') + ] + + if partner: + domain = expression.AND([domain, [('partner_id', '=', partner.id)]]) + else: + domain = expression.AND([domain, [('email', '=', email)]]) + + if invite_token: + domain = expression.AND([domain, [('invite_token', '=', invite_token)]]) + + return self.attempts_limit - self.env['survey.user_input'].search_count(domain) + + # ------------------------------------------------------------ + # ACTIONS + # ------------------------------------------------------------ + @api.model def next_page_or_question(self, user_input, page_or_question_id, go_back=False): """ The next page to display to the user, knowing that page_id is the id @@ -329,6 +389,115 @@ class Survey(models.Model): else: return (pages_or_questions[current_page_index + 1][1], False) + def action_draft(self): + self.write({'state': 'draft'}) + + def action_open(self): + self.write({'state': 'open'}) + + def action_close(self): + self.write({'state': 'closed'}) + + def action_start_survey(self): + """ Open the website page with the survey form """ + self.ensure_one() + token = self.env.context.get('survey_token') + trail = "?answer_token=%s" % token if token else "" + return { + 'type': 'ir.actions.act_url', + 'name': "Start Survey", + 'target': 'self', + 'url': self.public_url + trail + } + + def action_send_survey(self): + """ Open a window to compose an email, pre-filled with the survey message """ + # Ensure that this survey has at least one page with at least one question. + if (not self.page_ids and self.questions_layout == 'page_per_section') or not self.question_ids: + raise UserError(_('You cannot send an invitation for a survey that has no questions.')) + + if self.state == 'closed': + raise UserError(_("You cannot send invitations for closed surveys.")) + + template = self.env.ref('survey.mail_template_user_input_invite', raise_if_not_found=False) + + local_context = dict( + self.env.context, + default_survey_id=self.id, + default_use_template=bool(template), + default_template_id=template and template.id or False, + notif_layout='mail.mail_notification_light', + ) + return { + 'type': 'ir.actions.act_window', + 'view_mode': 'form', + 'res_model': 'survey.invite', + 'target': 'new', + 'context': local_context, + } + + def action_print_survey(self): + """ Open the website page with the survey printable view """ + self.ensure_one() + token = self.env.context.get('survey_token') + trail = "?answer_token=%s" % token if token else "" + return { + 'type': 'ir.actions.act_url', + 'name': "Print Survey", + 'target': 'self', + 'url': '/survey/print/%s%s' % (self.access_token, trail) + } + + def action_result_survey(self): + """ Open the website page with the survey results view """ + self.ensure_one() + return { + 'type': 'ir.actions.act_url', + 'name': "Results of the Survey", + 'target': 'self', + 'url': '/survey/results/%s' % self.id + } + + def action_test_survey(self): + ''' Open the website page with the survey form into test mode''' + self.ensure_one() + return { + 'type': 'ir.actions.act_url', + 'name': "Test Survey", + 'target': 'self', + 'url': '/survey/test/%s' % self.access_token, + } + + def action_survey_user_input_completed(self): + action_rec = self.env.ref('survey.action_survey_user_input_notest') + action = action_rec.read()[0] + ctx = dict(self.env.context) + ctx.update({'search_default_survey_id': self.ids[0], + 'search_default_completed': 1}) + action['context'] = ctx + return action + + def action_survey_user_input_certified(self): + action_rec = self.env.ref('survey.action_survey_user_input_notest') + action = action_rec.read()[0] + ctx = dict(self.env.context) + ctx.update({'search_default_survey_id': self.ids[0], + 'search_default_quizz_passed': 1}) + action['context'] = ctx + return action + + def action_survey_user_input(self): + action_rec = self.env.ref('survey.action_survey_user_input_notest') + action = action_rec.read()[0] + ctx = dict(self.env.context) + ctx.update({'search_default_survey_id': self.ids[0]}) + action['context'] = ctx + return action + + # ------------------------------------------------------------ + # GRAPH / RESULTS + # ------------------------------------------------------------ + def filter_input_ids(self, filters, finished=False): """If user applies any filters, then this function returns list of filtered user_input_id and label's strings for display data in web. @@ -453,6 +622,10 @@ class Survey(models.Model): return result + # ------------------------------------------------------------ + # GAMIFICATION / BADGES + # ------------------------------------------------------------ + def _create_certification_badge_trigger(self): self.ensure_one() goal = self.env['gamification.goal.definition'].create({ @@ -502,160 +675,3 @@ class Survey(models.Model): # delete all challenges and goals because not needed anymore (challenge lines are deleted in cascade) challenges_to_delete.unlink() goals_to_delete.unlink() - - # Actions - - def action_draft(self): - self.write({'state': 'draft'}) - - def action_open(self): - self.write({'state': 'open'}) - - def action_close(self): - self.write({'state': 'closed'}) - - def action_start_survey(self): - """ Open the website page with the survey form """ - self.ensure_one() - token = self.env.context.get('survey_token') - trail = "?answer_token=%s" % token if token else "" - return { - 'type': 'ir.actions.act_url', - 'name': "Start Survey", - 'target': 'self', - 'url': self.public_url + trail - } - - def action_send_survey(self): - """ Open a window to compose an email, pre-filled with the survey message """ - # Ensure that this survey has at least one page with at least one question. - if (not self.page_ids and self.questions_layout == 'page_per_section') or not self.question_ids: - raise UserError(_('You cannot send an invitation for a survey that has no questions.')) - - if self.state == 'closed': - raise UserError(_("You cannot send invitations for closed surveys.")) - - template = self.env.ref('survey.mail_template_user_input_invite', raise_if_not_found=False) - - local_context = dict( - self.env.context, - default_survey_id=self.id, - default_use_template=bool(template), - default_template_id=template and template.id or False, - notif_layout='mail.mail_notification_light', - ) - return { - 'type': 'ir.actions.act_window', - 'view_mode': 'form', - 'res_model': 'survey.invite', - 'target': 'new', - 'context': local_context, - } - - def action_print_survey(self): - """ Open the website page with the survey printable view """ - self.ensure_one() - token = self.env.context.get('survey_token') - trail = "?answer_token=%s" % token if token else "" - return { - 'type': 'ir.actions.act_url', - 'name': "Print Survey", - 'target': 'self', - 'url': '/survey/print/%s%s' % (self.access_token, trail) - } - - def action_result_survey(self): - """ Open the website page with the survey results view """ - self.ensure_one() - return { - 'type': 'ir.actions.act_url', - 'name': "Results of the Survey", - 'target': 'self', - 'url': '/survey/results/%s' % self.id - } - - def action_test_survey(self): - ''' Open the website page with the survey form into test mode''' - self.ensure_one() - return { - 'type': 'ir.actions.act_url', - 'name': "Test Survey", - 'target': 'self', - 'url': '/survey/test/%s' % self.access_token, - } - - def action_survey_user_input_completed(self): - action_rec = self.env.ref('survey.action_survey_user_input_notest') - action = action_rec.read()[0] - ctx = dict(self.env.context) - ctx.update({'search_default_survey_id': self.ids[0], - 'search_default_completed': 1}) - action['context'] = ctx - return action - - def action_survey_user_input_certified(self): - action_rec = self.env.ref('survey.action_survey_user_input_notest') - action = action_rec.read()[0] - ctx = dict(self.env.context) - ctx.update({'search_default_survey_id': self.ids[0], - 'search_default_quizz_passed': 1}) - action['context'] = ctx - return action - - def action_survey_user_input(self): - action_rec = self.env.ref('survey.action_survey_user_input_notest') - action = action_rec.read()[0] - ctx = dict(self.env.context) - ctx.update({'search_default_survey_id': self.ids[0]}) - action['context'] = ctx - return action - - def _has_attempts_left(self, partner, email, invite_token): - self.ensure_one() - - if (self.access_mode != 'public' or self.users_login_required) and self.is_attempts_limited: - return self._get_number_of_attempts_lefts(partner, email, invite_token) > 0 - - return True - - def _get_number_of_attempts_lefts(self, partner, email, invite_token): - """ Returns the number of attempts left. """ - self.ensure_one() - - domain = [ - ('survey_id', '=', self.id), - ('test_entry', '=', False), - ('state', '=', 'done') - ] - - if partner: - domain = expression.AND([domain, [('partner_id', '=', partner.id)]]) - else: - domain = expression.AND([domain, [('email', '=', email)]]) - - if invite_token: - domain = expression.AND([domain, [('invite_token', '=', invite_token)]]) - - return self.attempts_limit - self.env['survey.user_input'].search_count(domain) - - def _prepare_answer_questions(self): - """ Will generate the questions for a randomized survey. - It uses the random_questions_count of every sections of the survey to - pick a random number of questions and returns the merged recordset """ - self.ensure_one() - - questions = self.env['survey.question'] - - for page in self.page_ids: - if self.questions_selection == 'all': - questions |= page.question_ids - else: - if page.random_questions_count > 0 and len(page.question_ids) > page.random_questions_count: - questions = questions.concat(*random.sample(page.question_ids, page.random_questions_count)) - else: - questions |= page.question_ids - - if not questions: - questions = self.question_ids - - return questions diff --git a/addons/survey/models/survey_user.py b/addons/survey/models/survey_user.py index 3dd913f2ff8..b7fa5f1f92a 100644 --- a/addons/survey/models/survey_user.py +++ b/addons/survey/models/survey_user.py @@ -200,10 +200,12 @@ class SurveyUserInputLine(models.Model): _description = 'Survey User Input Line' _rec_name = 'user_input_id' + # survey data user_input_id = fields.Many2one('survey.user_input', string='User Input', ondelete='cascade', required=True) + survey_id = fields.Many2one(related='user_input_id.survey_id', string='Survey', store=True, readonly=False) question_id = fields.Many2one('survey.question', string='Question', ondelete='cascade', required=True) page_id = fields.Many2one(related='question_id.page_id', string="Section", readonly=False) - survey_id = fields.Many2one(related='user_input_id.survey_id', string='Survey', store=True, readonly=False) + # answer skipped = fields.Boolean('Skipped') answer_type = fields.Selection([ ('text', 'Text'), diff --git a/addons/survey/views/survey_user_views.xml b/addons/survey/views/survey_user_views.xml index 0725db4ba88..fd7249da958 100644 --- a/addons/survey/views/survey_user_views.xml +++ b/addons/survey/views/survey_user_views.xml @@ -2,8 +2,8 @@ - - survey_user_input_search + + survey.user_input.view.search survey.user_input @@ -26,8 +26,9 @@ - - survey_user_input_form + + + survey.user_input.view.form survey.user_input
@@ -76,8 +77,9 @@
- - survey_user_input_tree + + + survey.user_input.view.tree survey.user_input @@ -96,8 +98,8 @@ - - survey.user_input.kanban + + survey.user_input.view.kanban survey.user_input @@ -135,8 +137,8 @@ Answers survey.user_input tree,kanban,form - - + + {'search_default_group_by_survey': True}

@@ -148,8 +150,8 @@ Answers survey.user_input tree,kanban,form - - + + {'search_default_group_by_survey': True} [('test_entry', '=', False)]