diff --git a/addons/survey/models/survey_user_input.py b/addons/survey/models/survey_user_input.py
index 5c1ae626a1f..29faf9dc5a8 100644
--- a/addons/survey/models/survey_user_input.py
+++ b/addons/survey/models/survey_user_input.py
@@ -2,6 +2,7 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
import logging
+import textwrap
import uuid
from dateutil.relativedelta import relativedelta
@@ -19,6 +20,7 @@ class SurveyUserInput(models.Model):
_description = "Survey User Input"
_rec_name = "survey_id"
_order = "create_date desc"
+ _inherit = ['mail.thread', 'mail.activity.mixin']
# answer description
survey_id = fields.Many2one('survey.survey', string='Survey', required=True, readonly=True, ondelete='cascade')
@@ -35,12 +37,13 @@ class SurveyUserInput(models.Model):
# 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')
- attempts_number = fields.Integer("Attempt n°", compute='_compute_attempts_number')
+ attempts_count = fields.Integer("Attempts Count", compute='_compute_attempts_info')
+ attempts_number = fields.Integer("Attempt n°", compute='_compute_attempts_info')
survey_time_limit_reached = fields.Boolean("Survey Time Limit Reached", compute='_compute_survey_time_limit_reached')
# identification / access
access_token = fields.Char('Identification token', default=lambda self: str(uuid.uuid4()), readonly=True, required=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)
+ partner_id = fields.Many2one('res.partner', string='Contact', readonly=True)
email = fields.Char('Email', readonly=True)
nickname = fields.Char('Nickname', help="Attendee nickname, mainly used to identify him in the survey session leaderboard.")
# questions / answers
@@ -116,38 +119,47 @@ class SurveyUserInput(models.Model):
user_input.question_time_limit_reached = False
@api.depends('state', 'test_entry', 'survey_id.is_attempts_limited', 'partner_id', 'email', 'invite_token')
- def _compute_attempts_number(self):
+ def _compute_attempts_info(self):
attempts_to_compute = self.filtered(
lambda user_input: user_input.state == 'done' and not user_input.test_entry and user_input.survey_id.is_attempts_limited
)
for user_input in (self - attempts_to_compute):
+ user_input.attempts_count = 1
user_input.attempts_number = 1
if attempts_to_compute:
- self.env.cr.execute("""SELECT user_input.id, (COUNT(previous_user_input.id) + 1) AS attempts_number
+ self.env['survey.user_input'].flush()
+
+ self.env.cr.execute("""
+ SELECT user_input.id,
+ COUNT(all_attempts_user_input.id) AS attempts_count,
+ COUNT(CASE WHEN all_attempts_user_input.id < user_input.id THEN all_attempts_user_input.id END) + 1 AS attempts_number
FROM survey_user_input user_input
- LEFT OUTER JOIN survey_user_input previous_user_input
- ON user_input.survey_id = previous_user_input.survey_id
- AND previous_user_input.state = 'done'
- AND previous_user_input.test_entry IS NOT TRUE
- AND previous_user_input.id < user_input.id
- AND (user_input.invite_token IS NULL OR user_input.invite_token = previous_user_input.invite_token)
- AND (user_input.partner_id = previous_user_input.partner_id OR user_input.email = previous_user_input.email)
+ LEFT OUTER JOIN survey_user_input all_attempts_user_input
+ ON user_input.survey_id = all_attempts_user_input.survey_id
+ AND all_attempts_user_input.state = 'done'
+ AND all_attempts_user_input.test_entry IS NOT TRUE
+ AND (user_input.invite_token IS NULL OR user_input.invite_token = all_attempts_user_input.invite_token)
+ AND (user_input.partner_id = all_attempts_user_input.partner_id OR user_input.email = all_attempts_user_input.email)
WHERE user_input.id IN %s
GROUP BY user_input.id;
""", (tuple(attempts_to_compute.ids),))
- attempts_count_results = self.env.cr.dictfetchall()
+ attempts_number_results = self.env.cr.dictfetchall()
+
+ attempts_number_results = {
+ attempts_number_result['id']: {
+ 'attempts_number': attempts_number_result['attempts_number'],
+ 'attempts_count': attempts_number_result['attempts_count'],
+ }
+ for attempts_number_result in attempts_number_results
+ }
for user_input in attempts_to_compute:
- attempts_number = 1
- for attempts_count_result in attempts_count_results:
- if attempts_count_result['id'] == user_input.id:
- attempts_number = attempts_count_result['attempts_number']
- break
-
- user_input.attempts_number = attempts_number
+ attempts_number_result = attempts_number_results.get(user_input.id, {})
+ user_input.attempts_number = attempts_number_result.get('attempts_number', 1)
+ user_input.attempts_count = attempts_number_result.get('attempts_count', 1)
@api.model_create_multi
def create(self, vals_list):
@@ -187,6 +199,23 @@ class SurveyUserInput(models.Model):
'url': '/survey/print/%s?answer_token=%s' % (self.survey_id.access_token, self.access_token)
}
+ def action_redirect_to_attempts(self):
+ self.ensure_one()
+
+ action = self.env['ir.actions.act_window']._for_xml_id('survey.action_survey_user_input')
+ context = dict(self.env.context or {})
+
+ context['create'] = False
+ context['search_default_survey_id'] = self.survey_id.id
+ context['search_default_group_by_survey'] = False
+ if self.partner_id:
+ context['search_default_partner_id'] = self.partner_id.id
+ elif self.email:
+ context['search_default_email'] = self.email
+
+ action['context'] = context
+ return action
+
@api.model
def _generate_invite_token(self):
return str(uuid.uuid4())
@@ -562,6 +591,21 @@ class SurveyUserInput(models.Model):
inactive_questions = self._get_inactive_conditional_questions()
return survey.question_ids - inactive_questions
+ # ------------------------------------------------------------
+ # MESSAGING
+ # ------------------------------------------------------------
+
+ def _message_get_suggested_recipients(self):
+ recipients = super()._message_get_suggested_recipients()
+ for user_input in self:
+ if user_input.partner_id:
+ user_input._message_add_suggested_recipient(
+ recipients,
+ partner=user_input.partner_id,
+ reason=_('Survey Participant')
+ )
+ return recipients
+
class SurveyUserInputLine(models.Model):
_name = 'survey.user_input.line'
@@ -595,6 +639,30 @@ class SurveyUserInputLine(models.Model):
answer_score = fields.Float('Score')
answer_is_correct = fields.Boolean('Correct')
+ @api.depends('answer_type')
+ def _compute_display_name(self):
+ for line in self:
+ if line.answer_type == 'char_box':
+ line.display_name = line.value_char_box
+ elif line.answer_type == 'text_box' and line.value_text_box:
+ line.display_name = textwrap.shorten(line.value_text_box, width=50, placeholder=" [...]")
+ elif line.answer_type == 'numerical_box':
+ line.display_name = line.value_numerical_box
+ elif line.answer_type == 'date':
+ line.display_name = fields.Date.to_string(line.value_date)
+ elif line.answer_type == 'datetime':
+ line.display_name = fields.Datetime.to_string(line.value_datetime)
+ elif line.answer_type == 'suggestion':
+ if line.matrix_row_id:
+ line.display_name = '%s: %s' % (
+ line.suggested_answer_id.value,
+ line.matrix_row_id.value)
+ else:
+ line.display_name = line.suggested_answer_id.value
+
+ if not line.display_name:
+ line.display_name = _('Skipped')
+
@api.constrains('skipped', 'answer_type')
def _check_answer_type_skipped(self):
for line in self:
diff --git a/addons/survey/tests/common.py b/addons/survey/tests/common.py
index 1103b699422..01db7d8843e 100644
--- a/addons/survey/tests/common.py
+++ b/addons/survey/tests/common.py
@@ -24,6 +24,7 @@ class SurveyCase(common.TransactionCase):
'char_box': ('char_box', 'value_char_box'),
'numerical_box': ('numerical_box', 'value_numerical_box'),
'date': ('date', 'value_date'),
+ 'datetime': ('datetime', 'value_datetime'),
'simple_choice': ('suggestion', 'suggested_answer_id'), # TDE: still unclear
'multiple_choice': ('suggestion', 'suggested_answer_id'), # TDE: still unclear
'matrix': ('suggestion', ('suggested_answer_id', 'matrix_row_id')), # TDE: still unclear
@@ -135,6 +136,8 @@ class SurveyCase(common.TransactionCase):
qtype = self._type_match.get(question.question_type, (False, False))
answer_type = kwargs.pop('answer_type', qtype[0])
answer_fname = kwargs.pop('answer_fname', qtype[1])
+ if question.question_type == 'matrix':
+ answer_fname = qtype[1][0]
base_alvals = {
'user_input_id': answer.id,
@@ -143,6 +146,10 @@ class SurveyCase(common.TransactionCase):
'answer_type': answer_type,
}
base_alvals[answer_fname] = answer_value
+ if 'answer_value_row' in kwargs:
+ answer_value_row = kwargs.pop('answer_value_row')
+ base_alvals[qtype[1][1]] = answer_value_row
+
base_alvals.update(kwargs)
return self.env['survey.user_input.line'].create(base_alvals)
@@ -238,7 +245,7 @@ class SurveyCase(common.TransactionCase):
if question_type == 'multiple_choice':
kwargs['labels'] = [{'value': 'MChoice0'}, {'value': 'MChoice1'}]
elif question_type == 'simple_choice':
- kwargs['labels'] = []
+ kwargs['labels'] = [{'value': 'SChoice0'}, {'value': 'SChoice1'}]
elif question_type == 'matrix':
kwargs['labels'] = [{'value': 'Column0'}, {'value': 'Column1'}]
kwargs['labels_2'] = [{'value': 'Row0'}, {'value': 'Row1'}]
diff --git a/addons/survey/tests/test_survey.py b/addons/survey/tests/test_survey.py
index 17d7d3ade00..9ecf4b8574e 100644
--- a/addons/survey/tests/test_survey.py
+++ b/addons/survey/tests/test_survey.py
@@ -1,13 +1,89 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
-from odoo import _
+from freezegun import freeze_time
+
+from odoo import _, fields
from odoo.addons.survey.tests import common
from odoo.tests.common import users
class TestSurveyInternals(common.TestSurveyCommon):
+ def test_answer_attempts_count(self):
+ """ As 'attempts_number' and 'attempts_count' are computed using raw SQL queries, let us
+ test the results. """
+
+ test_survey = self.env['survey.survey'].create({
+ 'title': 'Test Survey',
+ 'is_attempts_limited': True,
+ 'attempts_limit': 4,
+ })
+
+ all_attempts = self.env['survey.user_input']
+ for _i in range(4):
+ all_attempts |= self._add_answer(test_survey, self.survey_user.partner_id, state='done')
+
+ # read both fields at once to allow computing their values in batch
+ attempts_results = all_attempts.read(['attempts_number', 'attempts_count'])
+ first_attempt = attempts_results[0]
+ second_attempt = attempts_results[1]
+ third_attempt = attempts_results[2]
+ fourth_attempt = attempts_results[3]
+
+ self.assertEqual(first_attempt['attempts_number'], 1)
+ self.assertEqual(first_attempt['attempts_count'], 4)
+
+ self.assertEqual(second_attempt['attempts_number'], 2)
+ self.assertEqual(second_attempt['attempts_count'], 4)
+
+ self.assertEqual(third_attempt['attempts_number'], 3)
+ self.assertEqual(third_attempt['attempts_count'], 4)
+
+ self.assertEqual(fourth_attempt['attempts_number'], 4)
+ self.assertEqual(fourth_attempt['attempts_count'], 4)
+
+ @freeze_time("2020-02-15 18:00")
+ def test_answer_display_name(self):
+ """ The "display_name" field in a survey.user_input.line is a computed field that will
+ display the answer label for any type of question.
+ Let us test the various question types. """
+
+ questions = self._create_one_question_per_type()
+ user_input = self._add_answer(self.survey, self.survey_user.partner_id)
+
+ for question in questions:
+ if question.question_type == 'char_box':
+ question_answer = self._add_answer_line(question, user_input, 'Char box answer')
+ self.assertEqual(question_answer.display_name, 'Char box answer')
+ elif question.question_type == 'text_box':
+ question_answer = self._add_answer_line(question, user_input, 'Text box answer')
+ self.assertEqual(question_answer.display_name, 'Text box answer')
+ elif question.question_type == 'numerical_box':
+ question_answer = self._add_answer_line(question, user_input, 7)
+ self.assertEqual(question_answer.display_name, '7.0')
+ elif question.question_type == 'date':
+ question_answer = self._add_answer_line(question, user_input, fields.Datetime.now())
+ self.assertEqual(question_answer.display_name, '2020-02-15')
+ elif question.question_type == 'datetime':
+ question_answer = self._add_answer_line(question, user_input, fields.Datetime.now())
+ self.assertEqual(question_answer.display_name, '2020-02-15 18:00:00')
+ elif question.question_type == 'simple_choice':
+ question_answer = self._add_answer_line(question, user_input, question.suggested_answer_ids[0].id)
+ self.assertEqual(question_answer.display_name, 'SChoice0')
+ elif question.question_type == 'multiple_choice':
+ question_answer_1 = self._add_answer_line(question, user_input, question.suggested_answer_ids[0].id)
+ self.assertEqual(question_answer_1.display_name, 'MChoice0')
+ question_answer_2 = self._add_answer_line(question, user_input, question.suggested_answer_ids[1].id)
+ self.assertEqual(question_answer_2.display_name, 'MChoice1')
+ elif question.question_type == 'matrix':
+ question_answer_1 = self._add_answer_line(question, user_input,
+ question.suggested_answer_ids[0].id, **{'answer_value_row': question.matrix_row_ids[0].id})
+ self.assertEqual(question_answer_1.display_name, 'Column0: Row0')
+ question_answer_2 = self._add_answer_line(question, user_input,
+ question.suggested_answer_ids[0].id, **{'answer_value_row': question.matrix_row_ids[1].id})
+ self.assertEqual(question_answer_2.display_name, 'Column0: Row1')
+
@users('survey_manager')
def test_answer_validation_mandatory(self):
""" For each type of question check that mandatory questions correctly check for complete answers """
diff --git a/addons/survey/views/survey_user_views.xml b/addons/survey/views/survey_user_views.xml
index eb0d773bd5e..bb4df68ebac 100644
--- a/addons/survey/views/survey_user_views.xml
+++ b/addons/survey/views/survey_user_views.xml
@@ -6,10 +6,13 @@
survey.user_input.view.search
survey.user_input
-
-
-
+
+
+
@@ -39,11 +42,27 @@
-
+
+
+
+
+
+
+
+
@@ -51,31 +70,44 @@
/
-
+
-
+
+
+
+
+
+ %
+
-
-
-
-
+
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
@@ -85,16 +117,19 @@
survey.user_input
-
-
+
-
+
+