[IMP] survey: improve session "short link" management
PURPOSE
Improve the various sessions flow to make it easier for attendees to reach the
session and to allow better interactions between the host and his audience.
This comes with small usability improvements as well as increased
"beautifulness" by using custom layouts & animations.
SPECS
- For live session only:
Currently, the "short link" feature is good but it still needs to be improved
with the following points:
- Make the default "code" shorter (4) and digit only.
- Allow to customize the "code"
- Check for potential collisions
Added at the same time:
- input element is focused when you land on the '/s' page.
LINKS
PR #46768
Task 2208574
This commit is contained in:
@@ -40,16 +40,6 @@ class Survey(http.Controller):
|
||||
], limit=1)
|
||||
return survey_sudo, answer_sudo
|
||||
|
||||
def _fetch_from_access_code(self, survey_short_token):
|
||||
if survey_short_token and len(survey_short_token) == 6:
|
||||
matching_survey = request.env['survey.survey'].sudo().search([
|
||||
('state', '=', 'open'),
|
||||
('access_token', 'like', f'{survey_short_token}%'),
|
||||
])
|
||||
if len(matching_survey) == 1:
|
||||
return matching_survey
|
||||
return False
|
||||
|
||||
def _check_validity(self, survey_token, answer_token, ensure_token=True):
|
||||
""" Check survey is open and can be taken. This does not checks for
|
||||
security rules, only functional / business rules. It returns a string key
|
||||
@@ -150,30 +140,6 @@ class Survey(http.Controller):
|
||||
|
||||
return werkzeug.utils.redirect("/")
|
||||
|
||||
# ------------------------------------------------------------
|
||||
# QUICK ACCESS SURVEY ROUTES
|
||||
# ------------------------------------------------------------
|
||||
|
||||
@http.route('/s', type='http', auth='public', website=True, sitemap=False)
|
||||
def survey_access_code(self, **post):
|
||||
""" Renders the survey access code page route.
|
||||
This page allows the user to enter the short code of the survey.
|
||||
It's mainly used in 'session mode' when attendees have to manually type the
|
||||
URL, to ease survey access. """
|
||||
return request.render("survey.survey_access_code")
|
||||
|
||||
@http.route('/s/<string:survey_short_token>', type='http', auth='public', website=True)
|
||||
def survey_start_short(self, survey_short_token):
|
||||
"""" Redirects to 'survey_start' route using a shortened link & token.
|
||||
We match the 6 first characters of the token for open surveys.
|
||||
This route is mostly used in survey sessions where we need short links for people to type. """
|
||||
|
||||
survey = self._fetch_from_access_code(survey_short_token)
|
||||
if survey:
|
||||
return werkzeug.utils.redirect("/survey/start/%s" % survey.access_token)
|
||||
|
||||
return werkzeug.utils.redirect("/")
|
||||
|
||||
# ------------------------------------------------------------
|
||||
# TEST / RETRY SURVEY ROUTES
|
||||
# ------------------------------------------------------------
|
||||
@@ -492,17 +458,6 @@ class Survey(http.Controller):
|
||||
|
||||
return self._prepare_question_html(survey_sudo, answer_sudo)
|
||||
|
||||
@http.route('/survey/check_access_code/<string:access_code>', type='json', auth='public', website=True)
|
||||
def survey_access_code_check(self, access_code):
|
||||
""" Checks if the given code is matching a survey access_token.
|
||||
If yes, redirect to /s/code route.
|
||||
If not, return error. The user is invited to type again the code. """
|
||||
survey = self._fetch_from_access_code(access_code)
|
||||
if survey:
|
||||
return {"survey_url": "/survey/start/%s" % survey.access_token}
|
||||
|
||||
return {"error": "survey_wrong"}
|
||||
|
||||
def _extract_comment_from_answers(self, question, answers):
|
||||
""" Answers is a custom structure depending of the question type
|
||||
that can contain question answers but also comments that need to be
|
||||
|
||||
@@ -3,10 +3,10 @@
|
||||
|
||||
import datetime
|
||||
import json
|
||||
import werkzeug
|
||||
|
||||
from dateutil.relativedelta import relativedelta
|
||||
from werkzeug.exceptions import NotFound
|
||||
from werkzeug.urls import url_join
|
||||
|
||||
from odoo import fields, http
|
||||
from odoo.http import request
|
||||
@@ -19,6 +19,27 @@ class UserInputSession(http.Controller):
|
||||
Unlike the regular survey controller, user trying to access the survey must have full access rights! """
|
||||
return request.env['survey.survey'].search([('access_token', '=', survey_token)])
|
||||
|
||||
def _fetch_from_session_code(self, session_code):
|
||||
""" Matches a survey against a passed session_code.
|
||||
We force the session_state to be reachable (ready / in_progress) to avoid people
|
||||
using this route to access other (private) surveys.
|
||||
We limit to sessions opened within the last 7 days to avoid potential abuses. """
|
||||
if session_code:
|
||||
matching_survey = request.env['survey.survey'].sudo().search([
|
||||
('state', '=', 'open'),
|
||||
('session_state', 'in', ['ready', 'in_progress']),
|
||||
('session_start_time', '>', fields.Datetime.now() - relativedelta(days=7)),
|
||||
('session_code', '=', session_code),
|
||||
], limit=1)
|
||||
if matching_survey:
|
||||
return matching_survey
|
||||
|
||||
return False
|
||||
|
||||
# ------------------------------------------------------------
|
||||
# SURVEY SESSION MANAGEMENT
|
||||
# ------------------------------------------------------------
|
||||
|
||||
@http.route('/survey/session/manage/<string:survey_token>', type='http', auth='user', website=True)
|
||||
def survey_session_manage(self, survey_token, **kwargs):
|
||||
""" Main route used by the host to 'manager' the session.
|
||||
@@ -37,8 +58,7 @@ class UserInputSession(http.Controller):
|
||||
|
||||
if survey.session_state == 'ready':
|
||||
return request.render('survey.user_input_session_open', {
|
||||
'survey': survey,
|
||||
'survey_url': url_join(survey.get_base_url(), survey.get_start_short_url())
|
||||
'survey': survey
|
||||
})
|
||||
else:
|
||||
template_values = self._prepare_manage_session_values(survey)
|
||||
@@ -119,6 +139,40 @@ class UserInputSession(http.Controller):
|
||||
'leaderboard': survey._prepare_leaderboard_values()
|
||||
})
|
||||
|
||||
# ------------------------------------------------------------
|
||||
# QUICK ACCESS SURVEY ROUTES
|
||||
# ------------------------------------------------------------
|
||||
|
||||
@http.route('/s', type='http', auth='public', website=True, sitemap=False)
|
||||
def survey_session_code(self, **post):
|
||||
""" Renders the survey session code page route.
|
||||
This page allows the user to enter the session code of the survey.
|
||||
It is mainly used to ease survey access for attendees in session mode. """
|
||||
return request.render("survey.survey_session_code")
|
||||
|
||||
@http.route('/s/<string:session_code>', type='http', auth='public', website=True)
|
||||
def survey_start_short(self, session_code):
|
||||
"""" Redirects to 'survey_start' route using a shortened link & token.
|
||||
We match the session_code for open surveys.
|
||||
This route is used in survey sessions where we need short links for people to type. """
|
||||
|
||||
survey = self._fetch_from_session_code(session_code)
|
||||
if survey:
|
||||
return werkzeug.utils.redirect("/survey/start/%s" % survey.access_token)
|
||||
|
||||
return werkzeug.utils.redirect("/s")
|
||||
|
||||
@http.route('/survey/check_session_code/<string:session_code>', type='json', auth='public', website=True)
|
||||
def survey_check_session_code(self, session_code):
|
||||
""" Checks if the given code is matching a survey session_code.
|
||||
If yes, redirect to /s/code route.
|
||||
If not, return error. The user is invited to type again the code. """
|
||||
survey = self._fetch_from_session_code(session_code)
|
||||
if survey:
|
||||
return {"survey_url": "/survey/start/%s" % survey.access_token}
|
||||
|
||||
return {"error": "survey_wrong"}
|
||||
|
||||
def _prepare_manage_session_values(self, survey):
|
||||
is_last_question = False
|
||||
if survey.question_ids:
|
||||
@@ -127,7 +181,6 @@ class UserInputSession(http.Controller):
|
||||
values = {
|
||||
'survey': survey,
|
||||
'is_last_question': is_last_question,
|
||||
'survey_url': url_join(survey.get_base_url(), survey.get_start_short_url()),
|
||||
}
|
||||
|
||||
values.update(self._prepare_question_results_values(survey, request.env['survey.user_input.line']))
|
||||
|
||||
@@ -23,6 +23,27 @@ class Survey(models.Model):
|
||||
def _get_default_access_token(self):
|
||||
return str(uuid.uuid4())
|
||||
|
||||
def _get_default_session_code(self):
|
||||
""" Attempt to generate a session code for our survey.
|
||||
The method will first try to generate 20 codes with 4 digits each and check if any are colliding.
|
||||
If we have at least one non-colliding code, we use it.
|
||||
If all 20 generated codes are colliding, we try with 20 codes of 5 digits,
|
||||
then 6, ... up to 10 digits. """
|
||||
|
||||
for digits_count in range(4, 10):
|
||||
range_lower_bound = 1 * (10 ** (digits_count - 1))
|
||||
range_upper_bound = (range_lower_bound * 10) - 1
|
||||
code_candidates = set([str(random.randint(range_lower_bound, range_upper_bound)) for i in range(20)])
|
||||
colliding_codes = self.sudo().search_read(
|
||||
[('session_code', 'in', list(code_candidates))],
|
||||
['session_code']
|
||||
)
|
||||
code_candidates -= set([colliding_code['session_code'] for colliding_code in colliding_codes])
|
||||
if code_candidates:
|
||||
return list(code_candidates)[0]
|
||||
|
||||
return False # could not generate a code
|
||||
|
||||
# description
|
||||
title = fields.Char('Survey Title', required=True, translate=True)
|
||||
color = fields.Integer('Color Index', default=0)
|
||||
@@ -113,6 +134,9 @@ class Survey(models.Model):
|
||||
('ready', 'Ready'),
|
||||
('in_progress', 'In Progress'),
|
||||
], string="Session State", copy=False)
|
||||
session_code = fields.Char('Session Code', default=lambda self: self._get_default_session_code(), copy=False,
|
||||
help="This code will be used by your attendees to reach your session. Feel free to customize it however you like!")
|
||||
session_link = fields.Char('Session Link', compute='_compute_session_link')
|
||||
# live sessions - current question fields
|
||||
session_question_id = fields.Many2one('survey.question', string="Current Question", copy=False,
|
||||
help="The current question of the survey session.")
|
||||
@@ -130,6 +154,7 @@ class Survey(models.Model):
|
||||
|
||||
_sql_constraints = [
|
||||
('access_token_unique', 'unique(access_token)', 'Access token should be unique'),
|
||||
('session_code_unique', 'unique(session_code)', 'Session code should be unique'),
|
||||
('certification_check', "CHECK( scoring_type!='no_scoring' OR certification=False )",
|
||||
'You can only create certifications for surveys that have a scoring mechanism.'),
|
||||
('time_limit_check', "CHECK( (is_time_limited=False) OR (time_limit is not null AND time_limit > 0) )",
|
||||
@@ -192,10 +217,14 @@ class Survey(models.Model):
|
||||
""" We have to loop since our result is dependent of the survey.session_start_time.
|
||||
This field is currently used to display the count about a single survey, in the
|
||||
context of sessions, so it should not matter too much. """
|
||||
|
||||
for survey in self:
|
||||
answer_count = 0
|
||||
input_count = self.env['survey.user_input'].read_group(
|
||||
[('survey_id', '=', survey.id), ('create_date', '>=', survey.session_start_time)],
|
||||
[('survey_id', '=', survey.id),
|
||||
('is_session_answer', '=', True),
|
||||
('state', '!=', 'done'),
|
||||
('create_date', '>=', survey.session_start_time)],
|
||||
['create_uid:count'],
|
||||
['survey_id'],
|
||||
)
|
||||
@@ -224,6 +253,18 @@ class Survey(models.Model):
|
||||
|
||||
survey.session_question_answer_count = answer_count
|
||||
|
||||
@api.depends('session_code')
|
||||
def _compute_session_link(self):
|
||||
for survey in self:
|
||||
if survey.session_code:
|
||||
survey.session_link = werkzeug.urls.url_join(
|
||||
survey.get_base_url(),
|
||||
'/s/%s' % survey.session_code)
|
||||
else:
|
||||
survey.session_link = werkzeug.urls.url_join(
|
||||
survey.get_base_url(),
|
||||
survey.get_start_url())
|
||||
|
||||
@api.depends('scoring_type', 'question_and_page_ids.save_as_nickname')
|
||||
def _compute_session_show_leaderboard(self):
|
||||
for survey in self:
|
||||
|
||||
@@ -23,6 +23,8 @@ publicWidget.registry.SurveyQuickAccessWidget = publicWidget.Widget.extend({
|
||||
if (!self.readonly) {
|
||||
$(document).on('keypress', self._onKeyPress.bind(self));
|
||||
}
|
||||
|
||||
self.$('input').focus();
|
||||
});
|
||||
},
|
||||
|
||||
@@ -48,9 +50,9 @@ publicWidget.registry.SurveyQuickAccessWidget = publicWidget.Widget.extend({
|
||||
_submitCode: function () {
|
||||
var self = this;
|
||||
this.$('.o_survey_error').addClass("d-none");
|
||||
var $accessCodeInput = this.$('input#access_code');
|
||||
var $sessionCodeInput = this.$('input#session_code');
|
||||
this._rpc({
|
||||
route: `/survey/check_access_code/${$accessCodeInput.val()}`,
|
||||
route: `/survey/check_session_code/${$sessionCodeInput.val()}`,
|
||||
}).then(function (response) {
|
||||
if (response.survey_url) {
|
||||
window.location = response.survey_url;
|
||||
|
||||
@@ -135,6 +135,14 @@
|
||||
</group>
|
||||
<group string="Live Session">
|
||||
<field name="session_speed_rating" />
|
||||
<div class="o_td_label">
|
||||
<label for="session_code" string="Session Code" class="oe_edit_only o_form_label" />
|
||||
<label for="session_link" string="Session Link" class="oe_read_only o_form_label" />
|
||||
</div>
|
||||
<div class="d-flex">
|
||||
<field name="session_code" nolabel="1" class="mr-2 oe_edit_only" />
|
||||
<field name="session_link" nolabel="1" widget="CopyClipboardChar" class="oe_read_only" />
|
||||
</div>
|
||||
</group>
|
||||
</group>
|
||||
</page>
|
||||
|
||||
@@ -110,7 +110,7 @@
|
||||
<div class="wrap o_survey_start pb256">
|
||||
<div class='mt32 mb32'>
|
||||
<div t-field='survey.description' class="oe_no_empty"/>
|
||||
<t t-if="survey.session_state == 'ready'">
|
||||
<t t-if="answer.is_session_answer">
|
||||
<div class="font-weight-bold">
|
||||
The session will begin automatically when the host starts.
|
||||
</div>
|
||||
|
||||
@@ -101,9 +101,9 @@
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- Survey Home page - Access Code
|
||||
Mainly used in 'session mode' to give an easy access to the survey through the '/s' route. -->
|
||||
<template id="survey_access_code" name="Survey: Access Code page">
|
||||
<!-- Survey Home page - Session Code
|
||||
Used in 'session mode' to give an easy access to the survey through the '/s' route. -->
|
||||
<template id="survey_session_code" name="Survey: Access Code page">
|
||||
<t t-call="survey.layout">
|
||||
<div class="wrap o_survey_wrap pb16 d-flex">
|
||||
<div class="container o_survey_quick_access d-flex flex-column">
|
||||
@@ -114,7 +114,7 @@
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="offset-md-4 col-md-4 text-center">
|
||||
<input id="access_code" type="text" placeholder="ABC123"
|
||||
<input id="session_code" type="text" placeholder="4812"
|
||||
class="form-control o_survey_question_text_box font-weight-bold bg-transparent text-primary text-center rounded-0 p-2 w-100"/>
|
||||
</div>
|
||||
<div class="col-3 text-center o_survey_error text-danger d-none pt-2" role="alert">
|
||||
|
||||
@@ -32,9 +32,9 @@
|
||||
<div class="text-center">
|
||||
<h1 class="mb-4" t-field="survey.title" />
|
||||
<h2 class="mb-5 font-weight-normal">
|
||||
<span>Go to <a t-att-href="survey_url" t-esc="survey_url" target="_blank" /></span>
|
||||
<span>Go to <a t-att-href="survey.session_link" t-esc="survey.session_link" target="_blank" /></span>
|
||||
<i class="fa fa-copy font-weight-normal ml-3 o_survey_session_copy" />
|
||||
<input class="o_survey_session_copy_url d-none" type="text" t-att-value="survey_url" />
|
||||
<input class="o_survey_session_copy_url d-none" type="text" t-att-value="survey.session_link" />
|
||||
</h2>
|
||||
<h2 class="font-weight-normal"><span>Waiting for attendees...</span>
|
||||
<span class="ml-1 font-weight-bold o_survey_session_attendees_count" t-esc="survey.session_answer_count" />
|
||||
@@ -78,9 +78,9 @@
|
||||
t-att-data-show-text-answers="show_text_answers">
|
||||
<div class="p-4 d-flex flex-column o_survey_session_manage_container min-w-75 pt-6">
|
||||
<h3 class="o_survey_session_link_reminder">
|
||||
<span>Go to <a t-att-href="survey_url" t-esc="survey_url" target="_blank" /></span>
|
||||
<span>Go to <a t-att-href="survey.session_link" t-esc="survey.session_link" target="_blank" /></span>
|
||||
<i class="fa fa-copy font-weight-normal ml-3 mr-1 o_survey_session_copy" />
|
||||
<input class="o_survey_session_copy_url d-none" type="text" t-att-value="survey_url" />
|
||||
<input class="o_survey_session_copy_url d-none" type="text" t-att-value="survey.session_link" />
|
||||
</h3>
|
||||
<a role="button"
|
||||
class="font-weight-bold fa fa-chevron-right o_survey_session_navigation o_survey_session_navigation_next p-3" />
|
||||
|
||||
Reference in New Issue
Block a user