diff --git a/addons/survey/controllers/main.py b/addons/survey/controllers/main.py index d12348a33a7..2f556e72192 100644 --- a/addons/survey/controllers/main.py +++ b/addons/survey/controllers/main.py @@ -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/', 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/', 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 diff --git a/addons/survey/controllers/survey_session_manage.py b/addons/survey/controllers/survey_session_manage.py index b93a31d94ff..159f43234c8 100644 --- a/addons/survey/controllers/survey_session_manage.py +++ b/addons/survey/controllers/survey_session_manage.py @@ -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/', 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/', 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/', 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'])) diff --git a/addons/survey/models/survey_survey.py b/addons/survey/models/survey_survey.py index 41022901c9e..5aa8a8e56f6 100644 --- a/addons/survey/models/survey_survey.py +++ b/addons/survey/models/survey_survey.py @@ -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: diff --git a/addons/survey/static/src/js/survey_quick_access.js b/addons/survey/static/src/js/survey_quick_access.js index 0534aa41878..ff0f3e88473 100644 --- a/addons/survey/static/src/js/survey_quick_access.js +++ b/addons/survey/static/src/js/survey_quick_access.js @@ -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; diff --git a/addons/survey/views/survey_survey_views.xml b/addons/survey/views/survey_survey_views.xml index 90dde68c632..af42249dd5b 100644 --- a/addons/survey/views/survey_survey_views.xml +++ b/addons/survey/views/survey_survey_views.xml @@ -135,6 +135,14 @@ +
+
+
+ + +
diff --git a/addons/survey/views/survey_templates.xml b/addons/survey/views/survey_templates.xml index b8addaaab7f..57e56103ef9 100644 --- a/addons/survey/views/survey_templates.xml +++ b/addons/survey/views/survey_templates.xml @@ -110,7 +110,7 @@
- +
The session will begin automatically when the host starts.
diff --git a/addons/survey/views/survey_templates_management.xml b/addons/survey/views/survey_templates_management.xml index c8af8b020af..acd63612969 100644 --- a/addons/survey/views/survey_templates_management.xml +++ b/addons/survey/views/survey_templates_management.xml @@ -101,9 +101,9 @@
- -