PURPOSE
Add a "live session" mode for survey that allows the host to interact with its
audience.
He controls the pace of the survey and the audience answers questions one at a
time.
Results for each question can be displayed by the host to adapt his speech in
real time.
A gamification component even allows to give points to the attendees, based on
the quickness of their answer, and create ranking to keep everyone's attention.
SPECIFICATIONS
This whole feature is deeply integrated with survey.
It adds a layer of "survey session" on top if it that gathers attendees and
answers during the lifespan of the session.
The attendee identity is filled in by a special question that is marked as
"Save as user nickname".
If the host has not configured that question or if the user doesn't answer,
they are marked as "Anonymous" in the rankings.
The answer score computation is also altered during the session to take the
speed of the answer into account.
To ease the access to the survey to attendees, we have introduced two new
routes in the survey main controller:
- '/s/123456'
That uses the 6 first characters of the token to give quick access to the
survey and allow typing the URL manually somewhat conveniently.
- '/s' that renders a view that asks the attendee to enter the survey 'code',
which then redirects to the previous route.
This commit also fixes a few bugs:
- A bugged use case with the "enter key" listener.
When the survey is done, we display a "result template" that shows the user
his score and allows him to retry if possible.
When this template is displayed, we don't want to listen to the "enter" key
that allows to submit the survey form.
- We now correctly remove the timer and initialize the result
widget when the user presses 'enter' on the last page / question.
SURVEY SESSION FLOW
HOST point of view:
- On the Survey Form, the host configures:
- "Reward quick answers" that gives more points to attendees if they answer
quickly
- Question Time Limit, on each question, that defines the time limit for
that specific question, allowing to have different (or no) timer for each
question.
- From the Survey Form, the host can start a "live session"
Only one session per survey can be running at a time.
- When he starts the session, the session_state is marked as 'ready', meaning
all attendees landing on the survey page will be part of the session.
- The host will land on a page showing the current number of attendees as well
as the link for the attendees to join that session.
- The host starts the session, activating the first question of this session.
We keep an active reference to the "current_question_id" and use the bus to
trigger an event that will refresh the survey page for all attendees, showing
them the question and allowing them to answer.
- The host gets a "question management screen", from which he can:
- See the current question text & suggested answers
- See the current question timer
- See the number of answers received for the question
- Display the answers of the question (same view as the survey "results"
page, but only for the question)
- Display the ranking of attendees (if "competitive mode" is enabled)
- The host controls the pace of the survey by moving to the next question until
it's the last one of the survey, then ends the current session.
When the session is closed, we mark the answers of the attendees as "done".
- The host lands on a screen where he sees the survey results and the ranking
of all attendees.
ATTENDEE point of view:
- He reaches the survey when a session and open, and gets a screen asking him
to wait until the host decides to start the session.
- When the sessions starts, he's automatically redirected to the first question
(see above).
- If the session is already started when he reaches the link, he lands on the
current question (and NOT on the first one)
- If the host has configured a time limit, the attendee sees the countdown
while he's answering.
- When the attendee answers the question, the screen tells him the answer is
registered and he has to wait for the host to go to the next question, which
will happen automatically.
- If the timer reaches 0 and he has not submitted his answer, it's too late and
he waits for the next one.
- It continues like that until the end of the session.
- At the end of the session, the attendee gets a screen with his global results
(same as a regular scored survey).
PR #43568
Task 1972640
Co-authored-by: David Beguin <dbe@odoo.com>
145 lines
7.4 KiB
Python
145 lines
7.4 KiB
Python
# -*- coding: utf-8 -*-
|
|
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
|
|
|
from unittest.mock import patch
|
|
|
|
from odoo.addons.base.models.ir_mail_server import IrMailServer
|
|
from odoo.addons.survey.tests import common
|
|
from odoo.tests import tagged
|
|
from odoo.tests.common import HttpCase
|
|
|
|
|
|
@tagged('-at_install', 'post_install', 'functional')
|
|
class TestCertificationFlow(common.TestSurveyCommon, HttpCase):
|
|
def _answer_question(self, question, answer, answer_token, csrf_token, button_submit='next'):
|
|
# Employee submits the question answer
|
|
post_data = self._format_submission_data(question, answer, {'csrf_token': csrf_token, 'token': answer_token, 'button_submit': button_submit})
|
|
response = self._access_submit(question.survey_id, answer_token, post_data)
|
|
self.assertResponse(response, 200)
|
|
|
|
# Employee is redirected on next question
|
|
response = self._access_page(question.survey_id, answer_token)
|
|
self.assertResponse(response, 200)
|
|
|
|
def _format_submission_data(self, question, answer, additional_post_data):
|
|
post_data = {}
|
|
post_data['question_id'] = question.id
|
|
post_data.update(self._prepare_post_data(question, answer, post_data))
|
|
post_data.update(**additional_post_data)
|
|
return post_data
|
|
|
|
def test_flow_certification(self):
|
|
# Step: survey user creates the certification
|
|
# --------------------------------------------------
|
|
with self.with_user(self.survey_user):
|
|
certification = self.env['survey.survey'].create({
|
|
'title': 'User Certification for SO lines',
|
|
'access_mode': 'public',
|
|
'users_login_required': True,
|
|
'questions_layout': 'page_per_question',
|
|
'users_can_go_back': True,
|
|
'scoring_type': 'scoring_with_answers',
|
|
'scoring_success_min': 85.0,
|
|
'certification': True,
|
|
'certification_mail_template_id': self.env.ref('survey.mail_template_certification').id,
|
|
'is_time_limited': True,
|
|
'time_limit': 10,
|
|
'state': 'open',
|
|
})
|
|
|
|
q01 = self._add_question(
|
|
None, 'When do you know it\'s the right time to use the SO line model?', 'simple_choice',
|
|
sequence=1,
|
|
constr_mandatory=True, constr_error_msg='Please select an answer', survey_id=certification.id,
|
|
labels=[
|
|
{'value': 'Please stop'},
|
|
{'value': 'Only on the SO form'},
|
|
{'value': 'Only on the Survey form'},
|
|
{'value': 'Easy, all the time!!!', 'is_correct': True, 'answer_score': 2.0}
|
|
])
|
|
|
|
q02 = self._add_question(
|
|
None, 'On average, how many lines of code do you need when you use SO line widgets?', 'simple_choice',
|
|
sequence=2,
|
|
constr_mandatory=True, constr_error_msg='Please select an answer', survey_id=certification.id,
|
|
labels=[
|
|
{'value': '1'},
|
|
{'value': '5', 'is_correct': True, 'answer_score': 2.0},
|
|
{'value': '100'},
|
|
{'value': '1000'}
|
|
])
|
|
|
|
q03 = self._add_question(
|
|
None, 'What do you think about SO line widgets (not rated)?', 'text_box',
|
|
sequence=3,
|
|
constr_mandatory=True, constr_error_msg='Please tell us what you think', survey_id=certification.id)
|
|
|
|
q04 = self._add_question(
|
|
None, 'On a scale of 1 to 10, how much do you like SO line widgets (not rated)?', 'simple_choice',
|
|
sequence=4,
|
|
constr_mandatory=True, constr_error_msg='Please tell us what you think', survey_id=certification.id,
|
|
labels=[
|
|
{'value': '-1'},
|
|
{'value': '0'},
|
|
{'value': '100'}
|
|
])
|
|
|
|
q05 = self._add_question(
|
|
None, 'Select all the correct "types" of SO lines', 'multiple_choice',
|
|
sequence=5,
|
|
constr_mandatory=False, survey_id=certification.id,
|
|
labels=[
|
|
{'value': 'sale_order', 'is_correct': True, 'answer_score': 1.0},
|
|
{'value': 'survey_page', 'is_correct': True, 'answer_score': 1.0},
|
|
{'value': 'survey_question', 'is_correct': True, 'answer_score': 1.0},
|
|
{'value': 'a_future_and_yet_unknown_model', 'is_correct': True, 'answer_score': 1.0},
|
|
{'value': 'none', 'answer_score': -1.0}
|
|
])
|
|
|
|
# Step: employee takes the certification
|
|
# --------------------------------------------------
|
|
self.authenticate('user_emp', 'user_emp')
|
|
|
|
# Employee opens start page
|
|
response = self._access_start(certification)
|
|
self.assertResponse(response, 200, [certification.title, 'Time limit for this survey', '10 minutes'])
|
|
|
|
# -> this should have generated a new user_input with a token
|
|
user_inputs = self.env['survey.user_input'].search([('survey_id', '=', certification.id)])
|
|
self.assertEqual(len(user_inputs), 1)
|
|
self.assertEqual(user_inputs.partner_id, self.user_emp.partner_id)
|
|
answer_token = user_inputs.access_token
|
|
|
|
# Employee begins survey with first page
|
|
response = self._access_page(certification, answer_token)
|
|
self.assertResponse(response, 200)
|
|
csrf_token = self._find_csrf_token(response.text)
|
|
|
|
r = self._access_begin(certification, answer_token)
|
|
self.assertResponse(r, 200)
|
|
|
|
with patch.object(IrMailServer, 'connect'):
|
|
self._answer_question(q01, q01.suggested_answer_ids.ids[3], answer_token, csrf_token)
|
|
self._answer_question(q02, q02.suggested_answer_ids.ids[1], answer_token, csrf_token)
|
|
self._answer_question(q03, "I think they're great!", answer_token, csrf_token)
|
|
self._answer_question(q04, q04.suggested_answer_ids.ids[0], answer_token, csrf_token, button_submit='previous')
|
|
self._answer_question(q03, "Just kidding, I don't like it...", answer_token, csrf_token)
|
|
self._answer_question(q04, q04.suggested_answer_ids.ids[0], answer_token, csrf_token)
|
|
self._answer_question(q05, [q05.suggested_answer_ids.ids[0], q05.suggested_answer_ids.ids[1], q05.suggested_answer_ids.ids[3]], answer_token, csrf_token)
|
|
|
|
user_inputs.invalidate_cache()
|
|
# Check that certification is successfully passed
|
|
self.assertEqual(user_inputs.scoring_percentage, 87.5)
|
|
self.assertTrue(user_inputs.scoring_success)
|
|
|
|
# Check answer correction is taken into account
|
|
self.assertNotIn("I think they're great!", user_inputs.mapped('user_input_line_ids.value_text_box'))
|
|
self.assertIn("Just kidding, I don't like it...", user_inputs.mapped('user_input_line_ids.value_text_box'))
|
|
|
|
certification_email = self.env['mail.mail'].sudo().search([], limit=1, order="create_date desc")
|
|
# Check certification email correctly sent and contains document
|
|
self.assertIn("User Certification for SO lines", certification_email.subject)
|
|
self.assertIn("employee@example.com", certification_email.email_to)
|
|
self.assertEqual(len(certification_email.attachment_ids), 1)
|
|
self.assertEqual(certification_email.attachment_ids[0].name, 'Certification Document.html')
|