[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.
This commit is contained in:
Aurélien Warnon
2019-02-06 13:20:52 +00:00
parent 445541989f
commit 99bb017f68
5 changed files with 45 additions and 13 deletions
+5 -2
View File
@@ -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 {}
+19 -7
View File
@@ -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
+6
View File
@@ -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']
+1 -1
View File
@@ -33,7 +33,7 @@
<t t-else="">
<div>Unfortunately, you have failed the test.</div>
<t t-if="survey.is_attempts_limited">
<t t-set="attempts_left" t-value="survey._get_number_of_attempts_lefts(answer.partner_id, answer.email)" />
<t t-set="attempts_left" t-value="survey._get_number_of_attempts_lefts(answer.partner_id, answer.email, answer.invite_token)" />
<t t-if="attempts_left > 0">
<p><span>Number of attemps left</span>: <span t-esc="attempts_left"></span></p>
<p>
+14 -3
View File
@@ -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