19.0 vanilla

This commit is contained in:
Ernad Husremovic 2026-03-09 09:30:27 +01:00
parent d1963a3c3a
commit 2d3ee4855a
7430 changed files with 2687981 additions and 2965473 deletions

View file

@ -1,14 +1,57 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
import datetime
from freezegun import freeze_time
from odoo import _, Command, fields
from odoo.addons.mail.tests.common import MailCase
from odoo.addons.survey.tests import common
from odoo.tests.common import users
class TestSurveyInternals(common.TestSurveyCommon):
class TestSurveyInternals(common.TestSurveyCommon, MailCase):
@users('survey_manager')
def test_allowed_triggering_question_ids(self):
# Create 2 surveys, each with 3 questions, each with 2 suggested answers
survey_1, survey_2 = self.env['survey.survey'].create([
{'title': 'Test Survey 1'},
{'title': 'Test Survey 2'}
])
self.env['survey.question'].create([
{
'survey_id': survey_id,
'title': f'Question {question_idx}',
'question_type': 'simple_choice',
'suggested_answer_ids': [
Command.create({
'value': f'Answer {answer_idx}',
}) for answer_idx in range(2)],
}
for question_idx in range(3)
for survey_id in (survey_1 | survey_2).ids
])
survey_1_q_1, survey_1_q_2, _ = survey_1.question_ids
survey_2_q_1, survey_2_q_2, _ = survey_2.question_ids
with self.subTest('Editing existing questions'):
# Only previous questions from the same survey
self.assertFalse(bool(survey_1_q_2.allowed_triggering_question_ids & survey_2_q_2.allowed_triggering_question_ids))
self.assertEqual(survey_1_q_2.allowed_triggering_question_ids, survey_1_q_1)
self.assertEqual(survey_2_q_2.allowed_triggering_question_ids, survey_2_q_1)
survey_1_new_question = self.env['survey.question'].new({'survey_id': survey_1})
survey_2_new_question = self.env['survey.question'].new({'survey_id': survey_2})
with self.subTest('New questions'):
# New questions should be allowed to use any question with choices from the same survey
self.assertFalse(
bool(survey_1_new_question.allowed_triggering_question_ids & survey_2_new_question.allowed_triggering_question_ids)
)
self.assertEqual(survey_1_new_question.allowed_triggering_question_ids.ids, survey_1.question_ids.ids)
self.assertEqual(survey_2_new_question.allowed_triggering_question_ids.ids, survey_2.question_ids.ids)
def test_answer_attempts_count(self):
""" As 'attempts_number' and 'attempts_count' are computed using raw SQL queries, let us
@ -44,6 +87,7 @@ class TestSurveyInternals(common.TestSurveyCommon):
self.assertEqual(fourth_attempt['attempts_count'], 4)
@freeze_time("2020-02-15 18:00")
@users('survey_manager')
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.
@ -67,7 +111,7 @@ class TestSurveyInternals(common.TestSurveyCommon):
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')
self.assertEqual(question_answer.display_name, '2020-02-15 19: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')
@ -83,6 +127,9 @@ class TestSurveyInternals(common.TestSurveyCommon):
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')
elif question.question_type == 'scale':
question_answer = self._add_answer_line(question, user_input, '3')
self.assertEqual(question_answer.display_name, '3')
@users('survey_manager')
def test_answer_validation_mandatory(self):
@ -100,7 +147,7 @@ class TestSurveyInternals(common.TestSurveyCommon):
validation_min_date='2015-03-20', validation_max_date='2015-03-25', validation_error_msg='ValidationError')
self.assertEqual(
question.validate_question('Is Alfred an answer ?'),
question.validate_question('Is Alfred an answer?'),
{question.id: _('This is not a date')}
)
@ -126,7 +173,7 @@ class TestSurveyInternals(common.TestSurveyCommon):
validation_min_float_value=2.2, validation_max_float_value=3.3, validation_error_msg='ValidationError')
self.assertEqual(
question.validate_question('Is Alfred an answer ?'),
question.validate_question('Is Alfred an answer?'),
{question.id: _('This is not a number')}
)
@ -180,6 +227,86 @@ class TestSurveyInternals(common.TestSurveyCommon):
{}
)
@users('survey_manager')
def test_simple_choice_validation_multiple_answers(self):
"""
Check that a 'simple_choice' question fails validation if more than one
valid answer is provided.
"""
question = self._add_question(
self.page_0, 'Simple Choice Constraint Test', 'simple_choice',
constr_mandatory=True,
comments_allowed=True,
comment_count_as_answer=True,
labels=[{'value': 'Choice X'}, {'value': 'Choice Y'}]
)
answer_choice_x_id = question.suggested_answer_ids[0].id
answer_choice_y_id = question.suggested_answer_ids[1].id
scenarios = [
(
'Two selected choices should not be allowed',
[answer_choice_x_id, answer_choice_y_id],
None,
True,
),
(
'One choice and one comment that counts as an answer',
answer_choice_x_id,
'This is my comment, which is also an answer.',
True,
),
(
'A single valid answer should pass validation',
answer_choice_x_id,
None,
False,
),
(
'A single valid comment should pass validation',
'',
'This is my comment, which is also an answer.',
False,
),
]
for case_description, answers, comment, is_multiple_answers in scenarios:
with self.subTest(answers=answers, comment=comment):
self.assertEqual(
question.validate_question(answers, comment),
{question.id: 'For this question, you can only select one answer.'} if is_multiple_answers else {},
case_description,
)
@users('survey_manager')
def test_answer_validation_comment(self):
""" Check that a comment validates a mandatory question based on 'comment_count_as_answer'. """
# Scenario 1: A comment counts as a valid answer.
question_ok = self._add_question(
self.page_0, 'Q_OK', 'multiple_choice',
constr_mandatory=True, validation_error_msg='ValidationError',
comments_allowed=True,
comment_count_as_answer=True,
labels=[{'value': 'Choice A'}, {'value': 'Choice B'}])
self.assertEqual(
question_ok.validate_question(answer='', comment='This comment is a valid answer.'),
{}
)
# Scenario 2: A comment does NOT count as a valid answer.
question_fail = self._add_question(
self.page_0, 'Q_FAIL', 'multiple_choice',
constr_mandatory=True, validation_error_msg='ValidationError',
comments_allowed=True,
comment_count_as_answer=False,
labels=[{'value': 'Choice A'}, {'value': 'Choice B'}])
self.assertEqual(
question_fail.validate_question(answer='', comment='This comment is not enough.'),
{question_fail.id: 'TestError'}
)
def test_partial_scores_simple_choice(self):
"""" Check that if partial scores are given for partially correct answers, in the case of a multiple
choice question with single choice, choosing the answer with max score gives 100% of points. """
@ -220,6 +347,83 @@ class TestSurveyInternals(common.TestSurveyCommon):
self.assertEqual(user_input.scoring_percentage, 100)
self.assertTrue(user_input.scoring_success)
def test_session_code_generation(self):
surveys = self.env['survey.survey'].create([{
'title': f'Survey {i}'
} for i in range(30)])
survey_codes = surveys.mapped('session_code')
self.assertEqual(len(survey_codes), 30)
for code in survey_codes:
self.assertTrue(bool(code))
self.assertEqual(
len(surveys.filtered(lambda survey: survey.session_code == code)),
1,
f"Each code should be unique, found multiple occurrences of: {code}"
)
def test_simple_choice_question_answer_result(self):
test_survey = self.env['survey.survey'].create({
'title': 'Test This Survey',
'scoring_type': 'scoring_with_answers',
'scoring_success_min': 80.0,
})
[a_01, a_02, a_03, a_04] = self.env['survey.question.answer'].create([{
'value': 'In Europe',
'answer_score': 0.0,
'is_correct': False
}, {
'value': 'In Asia',
'answer_score': 5.0,
'is_correct': True
}, {
'value': 'In South Asia',
'answer_score': 10.0,
'is_correct': True
}, {
'value': 'On Globe',
'answer_score': 5.0,
'is_correct': False
}])
q_01 = self.env['survey.question'].create({
'survey_id': test_survey.id,
'title': 'Where is india?',
'sequence': 1,
'question_type': 'simple_choice',
'suggested_answer_ids': [(6, 0, (a_01 | a_02 | a_03 | a_04).ids)]
})
user_input = self.env['survey.user_input'].create({'survey_id': test_survey.id})
user_input_line = self.env['survey.user_input.line'].create({
'user_input_id': user_input.id,
'question_id': q_01.id,
'answer_type': 'suggestion',
'suggested_answer_id': a_01.id
})
def assert_answer_status(expected_answer_status, questions_statistics):
"""Assert counts for 'Correct', 'Partially', 'Incorrect', 'Unanswered' are 0, and 1 for our expected answer status"""
for status, count in [(total['text'], total['count']) for total in questions_statistics['totals']]:
self.assertEqual(count, 1 if status == expected_answer_status else 0)
# this answer is incorrect with no score: should be considered as incorrect
statistics = user_input._prepare_statistics()[user_input]
assert_answer_status('Incorrect', statistics)
# this answer is correct with a positive score (even if not the maximum): should be considered as correct
user_input_line.suggested_answer_id = a_02.id
statistics = user_input._prepare_statistics()[user_input]
assert_answer_status('Correct', statistics)
# this answer is correct with the best score: should be considered as correct
user_input_line.suggested_answer_id = a_03.id
statistics = user_input._prepare_statistics()[user_input]
assert_answer_status('Correct', statistics)
# this answer is incorrect but has a score: should be considered as "partially"
user_input_line.suggested_answer_id = a_04.id
statistics = user_input._prepare_statistics()[user_input]
assert_answer_status('Partially', statistics)
@users('survey_manager')
def test_skipped_values(self):
""" Create one question per type of questions.
@ -231,11 +435,35 @@ class TestSurveyInternals(common.TestSurveyCommon):
for question in questions:
answer = '' if question.question_type in ['char_box', 'text_box'] else None
survey_user.save_lines(question, answer)
survey_user._save_lines(question, answer)
for question in questions:
self._assert_skipped_question(question, survey_user)
@users('survey_manager')
def test_multiple_choice_comment_not_skipped(self):
""" Test that a multiple choice question with only a comment is not marked as skipped. """
survey_user = self.survey._create_answer(user=self.survey_user)
question = self._add_question(
self.page_0, 'MCQ with Comment', 'multiple_choice',
comments_allowed=True,
comment_count_as_answer=True,
labels=[{'value': 'Choice A'}, {'value': 'Choice B'}]
)
# Save an answer with no selected choice but with a comment.
survey_user._save_lines(question, answer=[], comment='This is only a comment')
answer_line = self.env['survey.user_input.line'].search([
('user_input_id', '=', survey_user.id),
('question_id', '=', question.id)
])
self.assertEqual(len(answer_line), 1)
self.assertFalse(answer_line.skipped)
self.assertEqual(answer_line.answer_type, 'char_box')
self.assertEqual(answer_line.value_char_box, 'This is only a comment')
@users('survey_manager')
def test_copy_conditional_question_settings(self):
""" Create a survey with conditional layout, clone it and verify that the cloned survey has the same conditional
@ -246,22 +474,21 @@ class TestSurveyInternals(common.TestSurveyCommon):
return survey.question_ids.filtered(lambda q: q.title == title)[0]
# Create the survey questions (! texts of the questions must be unique as they are used to query them)
q_is_vegetarian_text = 'Are you vegetarian ?'
q_is_vegetarian_text = 'Are you vegetarian?'
q_is_vegetarian = self._add_question(
self.page_0, q_is_vegetarian_text, 'multiple_choice', survey_id=self.survey.id,
sequence=100, labels=[{'value': 'Yes'}, {'value': 'No'}])
sequence=100, labels=[{'value': 'Yes'}, {'value': 'No'}, {'value': 'Sometimes'}])
q_food_vegetarian_text = 'Choose your green meal'
self._add_question(self.page_0, q_food_vegetarian_text, 'multiple_choice',
is_conditional=True, sequence=101,
triggering_question_id=q_is_vegetarian.id,
triggering_answer_id=q_is_vegetarian.suggested_answer_ids[0].id,
sequence=101,
triggering_answer_ids=[q_is_vegetarian.suggested_answer_ids[0].id,
q_is_vegetarian.suggested_answer_ids[2].id],
survey_id=self.survey.id,
labels=[{'value': 'Vegetarian pizza'}, {'value': 'Vegetarian burger'}])
q_food_not_vegetarian_text = 'Choose your meal'
q_food_not_vegetarian_text = 'Choose your meal in case we serve meet/fish'
self._add_question(self.page_0, q_food_not_vegetarian_text, 'multiple_choice',
is_conditional=True, sequence=102,
triggering_question_id=q_is_vegetarian.id,
triggering_answer_id=q_is_vegetarian.suggested_answer_ids[1].id,
sequence=102,
triggering_answer_ids=q_is_vegetarian.suggested_answer_ids[1].ids,
survey_id=self.survey.id,
labels=[{'value': 'Steak with french fries'}, {'value': 'Fish'}])
@ -273,29 +500,25 @@ class TestSurveyInternals(common.TestSurveyCommon):
q_food_vegetarian_cloned = get_question_by_title(survey_clone, q_food_vegetarian_text)
q_food_not_vegetarian_cloned = get_question_by_title(survey_clone, q_food_not_vegetarian_text)
self.assertFalse(q_is_vegetarian_cloned.is_conditional)
self.assertFalse(bool(q_is_vegetarian_cloned.triggering_answer_ids))
# Vegetarian choice
self.assertTrue(q_food_vegetarian_cloned)
self.assertTrue(bool(q_food_vegetarian_cloned))
# Correct conditional layout
self.assertEqual(q_food_vegetarian_cloned.triggering_question_id.id, q_is_vegetarian_cloned.id)
self.assertEqual(q_food_vegetarian_cloned.triggering_answer_id.id,
q_is_vegetarian_cloned.suggested_answer_ids[0].id)
self.assertEqual(q_food_vegetarian_cloned.triggering_answer_ids.ids,
[q_is_vegetarian_cloned.suggested_answer_ids[0].id, q_is_vegetarian_cloned.suggested_answer_ids[2].id])
# Doesn't reference the original survey
self.assertNotEqual(q_food_vegetarian_cloned.triggering_question_id.id, q_is_vegetarian.id)
self.assertNotEqual(q_food_vegetarian_cloned.triggering_answer_id.id,
q_is_vegetarian.suggested_answer_ids[0].id)
self.assertNotEqual(q_food_vegetarian_cloned.triggering_answer_ids.ids,
[q_is_vegetarian.suggested_answer_ids[0].id, q_is_vegetarian.suggested_answer_ids[2].id])
# Not vegetarian choice
self.assertTrue(q_food_not_vegetarian_cloned.is_conditional)
self.assertTrue(bool(q_food_not_vegetarian_cloned.triggering_answer_ids))
# Correct conditional layout
self.assertEqual(q_food_not_vegetarian_cloned.triggering_question_id.id, q_is_vegetarian_cloned.id)
self.assertEqual(q_food_not_vegetarian_cloned.triggering_answer_id.id,
q_is_vegetarian_cloned.suggested_answer_ids[1].id)
self.assertEqual(q_food_not_vegetarian_cloned.triggering_answer_ids.ids,
q_is_vegetarian_cloned.suggested_answer_ids[1].ids)
# Doesn't reference the original survey
self.assertNotEqual(q_food_not_vegetarian_cloned.triggering_question_id.id, q_is_vegetarian.id)
self.assertNotEqual(q_food_not_vegetarian_cloned.triggering_answer_id.id,
q_is_vegetarian.suggested_answer_ids[1].id)
self.assertNotEqual(q_food_not_vegetarian_cloned.triggering_answer_ids.ids,
q_is_vegetarian.suggested_answer_ids[1].ids)
@users('survey_manager')
def test_copy_conditional_question_with_sequence_changed(self):
@ -318,11 +541,7 @@ class TestSurveyInternals(common.TestSurveyCommon):
q_2.write({'sequence': 100})
# Set a conditional question on the first question
q_1.write({
'is_conditional': True,
'triggering_question_id': q_2.id,
'triggering_answer_id': q_2.suggested_answer_ids[0].id,
})
q_1.write({'triggering_answer_ids': [Command.set([q_2.suggested_answer_ids[0].id])]})
(q_1 | q_2).invalidate_recordset()
@ -334,8 +553,159 @@ class TestSurveyInternals(common.TestSurveyCommon):
self.assertEqual(get_question_by_title(cloned_survey, 'Q2').sequence, q_2.sequence)
# Check that the conditional question is correctly copied to the right question
self.assertEqual(get_question_by_title(cloned_survey, 'Q1').triggering_question_id.title, q_1.triggering_question_id.title)
self.assertFalse(get_question_by_title(cloned_survey, 'Q2').triggering_question_id)
self.assertEqual(
get_question_by_title(cloned_survey, 'Q1').triggering_answer_ids[0].value, q_1.triggering_answer_ids[0].value
)
self.assertFalse(bool(get_question_by_title(cloned_survey, 'Q2').triggering_answer_ids))
@users('survey_manager')
def test_matrix_rows_display_name(self):
"""Check that matrix rows' display name is not changed."""
# A case's shape is: (question title, row value, expected row display names)
cases = [
(
'Question 1',
'Row A is short, so what?',
'Row A is short, so what?',
), (
'Question 2',
'Row B is a very long question, but it is shown by itself so there shouldn\'t be any change',
'Row B is a very long question, but it is shown by itself so there shouldn\'t be any change',
),
]
for question_title, row_value, exp_display_name in cases:
question = self.env['survey.question'].create({
'title': question_title,
'matrix_row_ids': [Command.create({'value': row_value})],
})
with self.subTest(question=question_title, row=row_value):
self.assertEqual(question.matrix_row_ids[0].display_name, exp_display_name)
@users('survey_manager')
def test_suggested_answer_display_name(self):
"""Check that answers' display name is not too long and allows to identify the question & answer.
When a matrix answer though, simply show the value as the question and row should be made
clear via the survey.user.input.line context."""
# A case's shape is: (question title, answer value, expected display name, additional create values)
cases = [
(
'Question 1',
'Answer A is short',
'Question 1 : Answer A is short',
{}
), (
'Question 2',
'Answer B is a very long answer, so it should itself be shortened or we would go too far',
'Question 2 : Answer B is a very long answer, so it should itself be shortened or we...',
{}
), (
'Question 3 is a very long question, so what can we do?',
'Answer A is short',
'Question 3 is a very long question, so what can we do? : Answer A is short',
{}
), (
'Question 4 is a very long question, so what can we do?',
'Answer B is a bit too long for Q4 now',
'Question 4 is a very long question, so what can... : Answer B is a bit too long for Q4 now',
{}
), (
'Question 5 is a very long question, so what can we do?',
'Answer C is so long that both the question and the answer will be shortened',
'Question 5 is a very long... : Answer C is so long that both the question and the...',
{}
), (
'Question 6',
'Answer A is short, so what?',
'Answer A is short, so what?',
{'question_type': 'matrix'},
), (
'Question 7',
'Answer B is a very long answer, but it is shown by itself so there shouldn\'t be any change',
'Answer B is a very long answer, but it is shown by itself so there shouldn\'t be any change',
{'question_type': 'matrix'},
),
]
for question_title, answer_value, exp_display_name, other_values in cases:
question = self.env['survey.question'].create({
'title': question_title,
'suggested_answer_ids': [Command.create({'value': answer_value})],
**other_values
})
with self.subTest(question=question_title, answer=answer_value):
self.assertEqual(question.suggested_answer_ids[0].display_name, exp_display_name)
@users('survey_manager')
def test_unlink_triggers(self):
# Create the survey questions
q_is_vegetarian_text = 'Are you vegetarian?'
q_is_vegetarian = self._add_question(
self.page_0, q_is_vegetarian_text, 'simple_choice', survey_id=self.survey.id, sequence=100,
labels=[{'value': 'Yes'}, {'value': 'No'}, {'value': 'It depends'}], constr_mandatory=True,
)
q_is_kinda_vegetarian_text = 'Would you prefer a veggie meal if possible?'
q_is_kinda_vegetarian = self._add_question(
self.page_0, q_is_kinda_vegetarian_text, 'simple_choice', survey_id=self.survey.id, sequence=101,
labels=[{'value': 'Yes'}, {'value': 'No'}], constr_mandatory=True, triggering_answer_ids=[
Command.link(q_is_vegetarian.suggested_answer_ids[1].id), # It depends
],
)
q_food_vegetarian_text = 'Choose your green meal'
veggie_question = self._add_question(
self.page_0, q_food_vegetarian_text, 'simple_choice', survey_id=self.survey.id, sequence=102,
labels=[{'value': 'Vegetarian pizza'}, {'value': 'Vegetarian burger'}], constr_mandatory=True,
triggering_answer_ids=[
Command.link(q_is_vegetarian.suggested_answer_ids[0].id), # Veggie
Command.link(q_is_kinda_vegetarian.suggested_answer_ids[0].id), # Would prefer veggie
])
q_food_not_vegetarian_text = 'Choose your meal'
not_veggie_question = self._add_question(
self.page_0, q_food_not_vegetarian_text, 'simple_choice', survey_id=self.survey.id, sequence=103,
labels=[{'value': 'Steak with french fries'}, {'value': 'Fish'}], constr_mandatory=True,
triggering_answer_ids=[
Command.link(q_is_vegetarian.suggested_answer_ids[1].id), # Not a veggie
Command.link(q_is_kinda_vegetarian.suggested_answer_ids[1].id), # Would not prefer veggie
],
)
q_is_kinda_vegetarian.unlink()
# Deleting one trigger but maintaining another keeps conditional behavior
self.assertTrue(bool(veggie_question.triggering_answer_ids))
q_is_vegetarian.suggested_answer_ids[0].unlink()
# Deleting answer Yes makes the following question always visible
self.assertFalse(bool(veggie_question.triggering_answer_ids))
# But the other is still conditional
self.assertEqual(not_veggie_question.triggering_answer_ids[0].id, q_is_vegetarian.suggested_answer_ids[0].id)
q_is_vegetarian.unlink()
# Now it will also be always visible
self.assertFalse(bool(not_veggie_question.triggering_answer_ids))
def test_get_correct_answers(self):
questions = self._create_one_question_per_type_with_scoring()
qtype_mapping = {q.question_type: q for q in questions}
expected_correct_answer = {
qtype_mapping['numerical_box'].id: 5,
qtype_mapping['date'].id: '10/16/2023',
qtype_mapping['datetime'].id: '11/17/2023 08:00:00 AM',
qtype_mapping['simple_choice'].id:
qtype_mapping['simple_choice'].suggested_answer_ids.filtered_domain([('value', '=', 'SChoice0')]).ids,
qtype_mapping['multiple_choice'].id:
qtype_mapping['multiple_choice'].suggested_answer_ids.filtered_domain([('value', 'in', ['MChoice0', 'MChoice1'])]).ids,
}
self.assertEqual(questions._get_correct_answers(), expected_correct_answer)
def test_get_pages_and_questions_to_show(self):
"""
@ -345,20 +715,15 @@ class TestSurveyInternals(common.TestSurveyCommon):
Structure of the test survey:
sequence | type | trigger | validity
sequence | type | trigger | validity
----------------------------------------------------------------------
1 | page, no description | / | X
2 | text_box | trigger is 6 | X
3 | numerical_box | trigger is 2 | X
4 | simple_choice | / | V
5 | page, description | / | V
6 | multiple_choice | / | V
7 | multiple_choice, no answers | / | V
8 | text_box | trigger is 6 | V
9 | matrix | trigger is 5 | X
10 | simple_choice | trigger is 7 | X
11 | simple_choice, no answers | trigger is 8 | X
12 | text_box | trigger is 11 | X
1 | page, no description | / | X
2 | simple_choice | trigger is 5 | X
3 | simple_choice | trigger is 2 | X
4 | page, description | / | V
5 | multiple_choice | / | V
6 | text_box | triggers are 5+7 | V
7 | multiple_choice | | V
"""
my_survey = self.env['survey.survey'].create({
@ -369,17 +734,12 @@ class TestSurveyInternals(common.TestSurveyCommon):
})
[
page_without_description,
text_box_1,
numerical_box,
_simple_choice_1,
page_with_description,
multiple_choice_1,
multiple_choice_2,
text_box_2,
matrix,
simple_choice_1,
simple_choice_2,
simple_choice_3,
text_box_3,
_page_with_description,
multiple_choice_1,
text_box_2,
multiple_choice_2,
] = self.env['survey.question'].create([{
'title': 'no desc',
'survey_id': my_survey.id,
@ -388,86 +748,51 @@ class TestSurveyInternals(common.TestSurveyCommon):
'is_page': True,
'description': False,
}, {
'title': 'text_box with invalid trigger',
'title': 'simple choice with invalid trigger',
'survey_id': my_survey.id,
'sequence': 2,
'is_page': False,
'question_type': 'simple_choice',
'suggested_answer_ids': [(0, 0, {'value': 'a'})],
}, {
'title': 'numerical box with trigger that is invalid',
'title': 'simple_choice with chained invalid trigger',
'survey_id': my_survey.id,
'sequence': 3,
'is_page': False,
'question_type': 'numerical_box',
}, {
'title': 'valid simple_choice',
'survey_id': my_survey.id,
'sequence': 4,
'is_page': False,
'question_type': 'simple_choice',
'suggested_answer_ids': [(0, 0, {'value': 'a'})],
}, {
'title': 'with desc',
'survey_id': my_survey.id,
'sequence': 5,
'sequence': 4,
'is_page': True,
'question_type': False,
'description': 'This page has a description',
}, {
'title': 'multiple choice not conditional',
'survey_id': my_survey.id,
'sequence': 6,
'sequence': 5,
'is_page': False,
'question_type': 'multiple_choice',
'suggested_answer_ids': [(0, 0, {'value': 'a'})]
}, {
'title': 'multiple_choice with no answers',
'title': 'text_box with valid trigger',
'survey_id': my_survey.id,
'sequence': 6,
'is_page': False,
'question_type': 'text_box',
}, {
'title': 'valid multiple_choice',
'survey_id': my_survey.id,
'sequence': 7,
'is_page': False,
'question_type': 'multiple_choice',
}, {
'title': 'text_box with valid trigger',
'survey_id': my_survey.id,
'sequence': 8,
'is_page': False,
'question_type': 'text_box',
}, {
'title': 'matrix with invalid trigger (page)',
'survey_id': my_survey.id,
'sequence': 9,
'is_page': False,
'question_type': 'matrix',
}, {
'title': 'simple choice w/ invalid trigger (no suggested_answer_ids)',
'survey_id': my_survey.id,
'sequence': 10,
'is_page': False,
'question_type': 'simple_choice',
}, {
'title': 'text_box w/ invalid trigger (not a mcq)',
'survey_id': my_survey.id,
'sequence': 11,
'is_page': False,
'question_type': 'simple_choice',
'suggested_answer_ids': False,
}, {
'title': 'text_box w/ invalid trigger (suggested_answer_ids is False)',
'survey_id': my_survey.id,
'sequence': 12,
'is_page': False,
'question_type': 'text_box',
'suggested_answer_ids': [(0, 0, {'value': 'a'})]
}])
text_box_1.write({'is_conditional': True, 'triggering_question_id': multiple_choice_1.id})
numerical_box.write({'is_conditional': True, 'triggering_question_id': text_box_1.id})
text_box_2.write({'is_conditional': True, 'triggering_question_id': multiple_choice_1.id})
matrix.write({'is_conditional': True, 'triggering_question_id': page_with_description.id})
simple_choice_2.write({'is_conditional': True, 'triggering_question_id': multiple_choice_2.id})
simple_choice_3.write({'is_conditional': True, 'triggering_question_id': text_box_2.id})
text_box_3.write({'is_conditional': True, 'triggering_question_id': simple_choice_3.id})
invalid_records = page_without_description + text_box_1 + numerical_box \
+ matrix + simple_choice_2 + simple_choice_3 + text_box_3
simple_choice_1.write({'triggering_answer_ids': multiple_choice_1.suggested_answer_ids})
simple_choice_2.write({'triggering_answer_ids': multiple_choice_1.suggested_answer_ids})
text_box_2.write({'triggering_answer_ids': (multiple_choice_1 | multiple_choice_2).suggested_answer_ids})
invalid_records = page_without_description + simple_choice_1 + simple_choice_2
question_and_page_ids = my_survey.question_and_page_ids
returned_questions_and_pages = my_survey._get_pages_and_questions_to_show()
@ -475,7 +800,7 @@ class TestSurveyInternals(common.TestSurveyCommon):
def test_survey_session_leaderboard(self):
"""Check leaderboard rendering with small (max) scores values."""
start_time = fields.datetime(2023, 7, 7, 12, 0, 0)
start_time = datetime.datetime(2023, 7, 7, 12, 0, 0)
test_survey = self.env['survey.survey'].create({
'title': 'Test This Survey',
'scoring_type': 'scoring_with_answers',
@ -507,3 +832,177 @@ class TestSurveyInternals(common.TestSurveyCommon):
'animate': True,
'leaderboard': test_survey._prepare_leaderboard_values()
})
def test_notify_subscribers(self):
"""Check that messages are posted only if there are participation followers"""
survey_2 = self.survey.copy()
survey_participation_subtype = self.env.ref('survey.mt_survey_survey_user_input_completed')
user_input_participation_subtype = self.env.ref('survey.mt_survey_user_input_completed')
# Make survey_user (group_survey_user) follow participation to survey (they follow), not survey 2 (no followers)
self.survey.message_subscribe(partner_ids=self.survey_user.partner_id.ids, subtype_ids=survey_participation_subtype.ids)
# Complete a participation for both surveys, only one should trigger a notification for followers
user_inputs = self.env['survey.user_input'].create([{'survey_id': survey.id} for survey in (self.survey, survey_2)])
with self.mock_mail_app():
user_inputs._mark_done()
self.assertEqual(len(self._new_msgs), 1)
self.assertMessageFields(
self._new_msgs,
{
'model': 'survey.user_input',
'subtype_id': user_input_participation_subtype,
'res_id': user_inputs[0].id,
'notified_partner_ids': self.survey_user.partner_id
},
)
def test_survey_session_speed_reward_config_propagation(self):
"""Check the speed rating time limit propagation to non time-customized questions."""
test_survey = self.env['survey.survey'].create({
'title': 'Test This Survey',
'scoring_type': 'scoring_with_answers',
'question_and_page_ids': [
Command.create({
'is_time_customized': True,
'is_time_limited': True,
'time_limit': 30,
'title': 'Question A',
}), Command.create({
'is_time_customized': True,
'is_time_limited': True,
'time_limit': 40,
'title': 'Question B',
}), Command.create({
'time_limit': 11, # left-over somehow
'title': 'Question C',
}),
],
})
self.assertFalse(test_survey.session_speed_rating)
test_survey.write({'session_speed_rating': True, 'session_speed_rating_time_limit': 30})
self.assertEqual(test_survey.session_speed_rating_time_limit, 30)
self.assertSetEqual({*test_survey.question_ids.mapped('is_time_limited')}, {True})
self.assertListEqual(test_survey.question_ids.mapped('time_limit'), [30, 40, 30])
self.assertListEqual(test_survey.question_ids.mapped('is_time_customized'), [False, True, False])
test_survey.session_speed_rating_time_limit = 40
self.assertSetEqual({*test_survey.question_ids.mapped('time_limit')}, {40})
self.assertSetEqual({*test_survey.question_ids.mapped('is_time_customized')}, {False})
test_survey.question_ids[:2].write({
"is_time_limited": False,
'is_time_customized': True, # As would the client do
})
self.assertListEqual(test_survey.question_ids.mapped('is_time_limited'), [False, False, True])
self.assertListEqual(test_survey.question_ids.mapped('is_time_customized'), [True, True, False])
test_survey.session_speed_rating_time_limit = 20
self.assertListEqual(test_survey.question_ids.mapped('is_time_limited'), [False, False, True])
self.assertListEqual(test_survey.question_ids.mapped('is_time_customized'), [True, True, False])
self.assertEqual(test_survey.question_ids[2].time_limit, 20)
test_survey.session_speed_rating = False
self.assertSetEqual({*test_survey.question_ids.mapped('is_time_limited')}, {False})
self.assertSetEqual({*test_survey.question_ids.mapped('is_time_customized')}, {False})
# test update in batch
test_survey.write({'session_speed_rating': True, 'session_speed_rating_time_limit': 30})
self.assertSetEqual({*test_survey.question_ids.mapped('is_time_limited')}, {True})
self.assertSetEqual({*test_survey.question_ids.mapped('time_limit')}, {30})
self.assertSetEqual({*test_survey.question_ids.mapped('is_time_customized')}, {False})
def test_survey_session_speed_reward_default_applied(self):
"""Check that new questions added to a survey with speed reward will apply defaults."""
test_survey = self.env['survey.survey'].create({
'title': 'Test This Survey',
'scoring_type': 'scoring_with_answers',
'session_speed_rating': True,
'session_speed_rating_time_limit': 60,
})
question_1, question_2, question_3, question_4 = self.env['survey.question'].create([{
'is_time_limited': True, # from client, unedited time limits (from default_get)
'question_type': 'numerical_box',
'survey_id': test_survey.id,
'time_limit': 60,
'title': 'Question 1',
}, {
'survey_id': test_survey.id, # simple values (via rpc for example), will be updated to is_time_customized
'question_type': 'numerical_box',
'title': 'Question 2',
}, {
'is_time_customized': True,
'is_time_limited': False,
'question_type': 'numerical_box',
'survey_id': test_survey.id,
'title': 'Question 3',
}, {
'is_time_customized': True, # override in client
'is_time_limited': True,
'question_type': 'numerical_box',
'survey_id': test_survey.id,
'time_limit': 30,
'title': 'Question 4',
},
])
self.assertTrue(question_1.is_time_limited)
self.assertEqual(question_1.time_limit, 60)
self.assertFalse(question_1.is_time_customized)
self.assertFalse(question_2.is_time_limited)
self.assertFalse(question_2.time_limit)
self.assertTrue(question_2.is_time_customized)
self.assertFalse(question_3.is_time_limited)
self.assertTrue(question_3.is_time_customized)
self.assertFalse(question_2.time_limit)
self.assertTrue(question_4.is_time_limited)
self.assertEqual(question_4.time_limit, 30)
self.assertTrue(question_4.is_time_customized)
def test_survey_time_limits_results(self):
"""Check that speed-related scores awarded are correctly computed."""
start_time = datetime.datetime(2023, 7, 7, 12, 0, 0)
test_survey = self.env['survey.survey'].create({
'title': 'Test This Survey',
'scoring_type': 'scoring_with_answers',
'scoring_success_min': 80.0,
'session_speed_rating': True,
'session_speed_rating_time_limit': 30,
'session_question_start_time': start_time,
})
q_01 = self.env['survey.question'].create([{
'is_time_customized': True,
'is_time_limited': True,
'question_type': 'simple_choice',
'suggested_answer_ids': [
Command.create({'value': 'In Asia', 'answer_score': 5.0, 'is_correct': True}),
Command.create({'value': 'In Europe', 'answer_score': 0., 'is_correct': False}),
],
'survey_id': test_survey.id,
'time_limit': 60,
'title': 'Where is india?',
}])
test_survey.session_question_id = q_01
answer_correct, answer_incorrect = q_01.suggested_answer_ids
user_input = self.env['survey.user_input'].create({'survey_id': test_survey.id, 'is_session_answer': True})
for (seconds_since_start, answer), expected_score in zip(
[
(61, answer_correct), # time limit elapsed
(61, answer_incorrect),
(31, answer_correct), # half of time limit elapsed
(31, answer_incorrect),
(2, answer_correct), # end of max_score_delay
(2, answer_incorrect),
], [2.5, 0.0, 3.75, 0.0, 5.0, 0.0], # 2.5 if succeeded + up to 2.5 depending on time to answer
):
with (self.subTest(elapsed=seconds_since_start, is_correct=answer.is_correct),
freeze_time(start_time + datetime.timedelta(seconds=seconds_since_start))):
user_input_line = self.env['survey.user_input.line'].create({
'user_input_id': user_input.id,
'question_id': q_01.id,
'answer_type': 'suggestion',
'suggested_answer_id': answer.id,
})
self.assertEqual(user_input_line.answer_score, expected_score)