From 99bb017f68fa2bbf8f6519e514c6f719d4325153 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aur=C3=A9lien=20Warnon?= Date: Fri, 1 Feb 2019 07:43:17 +0000 Subject: [PATCH] [IMP] survey: add invite_token to user_input to reset the attempts count Task #1902306 Purpose ======= When you invite people to take a certification multiple times, the user will have his "attempts" count reset with every invitation. This is important when there is a limited number of attempts available. ex: A user invited twice will have 2 x allowed_attempts_count to succeed the survey. Attempts are now separated in different pools using a "invite_token" field on the survey.user_input. --- addons/survey/controllers/main.py | 7 +++++-- addons/survey/models/survey_survey.py | 26 +++++++++++++++++------- addons/survey/models/survey_user.py | 6 ++++++ addons/survey/views/survey_templates.xml | 2 +- addons/survey/wizard/survey_invite.py | 17 +++++++++++++--- 5 files changed, 45 insertions(+), 13 deletions(-) diff --git a/addons/survey/controllers/main.py b/addons/survey/controllers/main.py index 5fcb8bdcbd7..488250ce4a1 100644 --- a/addons/survey/controllers/main.py +++ b/addons/survey/controllers/main.py @@ -165,7 +165,10 @@ class Survey(http.Controller): return werkzeug.utils.redirect("/") try: - retry_answer_sudo = survey_sudo._create_answer(user=request.env.user, partner=answer_sudo.partner_id, email=answer_sudo.email) + retry_answer_sudo = survey_sudo._create_answer(user=request.env.user, partner=answer_sudo.partner_id, email=answer_sudo.email, invite_token=answer_sudo.invite_token, **{ + 'input_type': answer_sudo.input_type, + 'deadline': answer_sudo.deadline, + }) except: return werkzeug.utils.redirect("/") return request.redirect('/survey/start/%s?%s' % (survey_sudo.access_token, keep_query('*', answer_token=retry_answer_sudo.token))) @@ -356,7 +359,7 @@ class Survey(http.Controller): return {} survey_sudo, answer_sudo = access_data['survey_sudo'], access_data['answer_sudo'] - if not survey_sudo._has_attempts_left(answer_sudo.partner_id, answer_sudo.email): + if not survey_sudo._has_attempts_left(answer_sudo.partner_id, answer_sudo.email, answer_sudo.invite_token): # prevent cheating with users creating multiple 'user_input' before their last attempt return {} diff --git a/addons/survey/models/survey_survey.py b/addons/survey/models/survey_survey.py index 8cd5823735d..a08f735bd82 100644 --- a/addons/survey/models/survey_survey.py +++ b/addons/survey/models/survey_survey.py @@ -171,7 +171,7 @@ class Survey(models.Model): return super(Survey, self).copy_data(default) @api.multi - def _create_answer(self, user=False, partner=False, email=False, test_entry=False, **additional_vals): + 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 security. @@ -188,7 +188,8 @@ class Survey(models.Model): 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) + invite_token = additional_vals.pop('invite_token', False) + survey._check_answer_creation(user, partner, email, test_entry=test_entry, check_attempts=check_attempts, invite_token=invite_token) answer_vals = { 'survey_id': survey.id, 'test_entry': test_entry, @@ -203,13 +204,21 @@ class Survey(models.Model): else: answer_vals['email'] = email + if invite_token: + answer_vals['invite_token'] = invite_token + elif survey.is_attempts_limited and survey.access_mode != 'public': + # attempts limited: create a new invite_token + # exception made for 'public' access_mode since the attempts pool is global because answers are + # created every time the user lands on '/start' + answer_vals['invite_token'] = self.env['survey.user_input']._generate_invite_token() + answer_vals.update(additional_vals) answers += answers.create(answer_vals) return answers @api.multi - def _check_answer_creation(self, user, partner, email, test_entry=False): + def _check_answer_creation(self, user, partner, email, test_entry=False, check_attempts=True, invite_token=False): """ Ensure conditions to create new tokens are met. """ self.ensure_one() if test_entry: @@ -229,7 +238,7 @@ class Survey(models.Model): 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.')) - if not self._has_attempts_left(partner or (user and user.partner_id), email): + if check_attempts and not self._has_attempts_left(partner or (user and user.partner_id), email, invite_token): raise UserError(_('No attempts left.')) @api.model @@ -505,16 +514,16 @@ class Survey(models.Model): return action @api.multi - def _has_attempts_left(self, partner, email): + 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) > 0 + return self._get_number_of_attempts_lefts(partner, email, invite_token) > 0 return True @api.multi - def _get_number_of_attempts_lefts(self, partner, email): + def _get_number_of_attempts_lefts(self, partner, email, invite_token): """ Returns the number of attempts left. """ self.ensure_one() @@ -529,6 +538,9 @@ class Survey(models.Model): 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) @api.multi diff --git a/addons/survey/models/survey_user.py b/addons/survey/models/survey_user.py index 69844b49c7b..bf79968b381 100644 --- a/addons/survey/models/survey_user.py +++ b/addons/survey/models/survey_user.py @@ -45,6 +45,8 @@ class SurveyUserInput(models.Model): 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) + # no unique constraint, as it identifies a pool of attempts + invite_token = fields.Char('Invite token', readonly=True, copy=False) partner_id = fields.Many2one('res.partner', string='Partner', readonly=True) email = fields.Char('E-mail', readonly=True) @@ -95,6 +97,10 @@ class SurveyUserInput(models.Model): ('state', '=', 'new'), ('create_date', '<', an_hour_ago)]).unlink() + @api.model + def _generate_invite_token(self): + return str(uuid.uuid4()) + @api.multi def action_resend(self): partners = self.env['res.partner'] diff --git a/addons/survey/views/survey_templates.xml b/addons/survey/views/survey_templates.xml index f5a78622cfb..e5f13cf766f 100644 --- a/addons/survey/views/survey_templates.xml +++ b/addons/survey/views/survey_templates.xml @@ -33,7 +33,7 @@
Unfortunately, you have failed the test.
- +

Number of attemps left:

diff --git a/addons/survey/wizard/survey_invite.py b/addons/survey/wizard/survey_invite.py index 5630d071f73..305b85b39d8 100644 --- a/addons/survey/wizard/survey_invite.py +++ b/addons/survey/wizard/survey_invite.py @@ -170,12 +170,23 @@ class SurveyInvite(models.TransientModel): if self.existing_mode == 'resend': partners_done = existing_answers.mapped('partner_id') emails_done = existing_answers.mapped('email') - answers |= existing_answers + + # only add the last answer for each user of each type (partner_id & email) + # to have only one mail sent per user + for partner_done in partners_done: + answers |= next(existing_answer for existing_answer in + existing_answers.sorted(lambda answer: answer.create_date, reverse=True) + if existing_answer.partner_id == partner_done) + + for email_done in emails_done: + answers |= next(existing_answer for existing_answer in + existing_answers.sorted(lambda answer: answer.create_date, reverse=True) + if existing_answer.email == email_done) for new_partner in partners - partners_done: - answers |= self.survey_id._create_answer(partner=new_partner, **self._get_answers_values()) + answers |= self.survey_id._create_answer(partner=new_partner, check_attempts=False, **self._get_answers_values()) for new_email in [email for email in emails if email not in emails_done]: - answers |= self.survey_id._create_answer(email=new_email, **self._get_answers_values()) + answers |= self.survey_id._create_answer(email=new_email, check_attempts=False, **self._get_answers_values()) return answers