diff --git a/addons/survey/__manifest__.py b/addons/survey/__manifest__.py index 49693cf373c..09c79a0f211 100644 --- a/addons/survey/__manifest__.py +++ b/addons/survey/__manifest__.py @@ -36,6 +36,7 @@ sent mails with personal token for the invitation of the survey. 'views/survey_question_views.xml', 'views/survey_templates.xml', 'views/survey_templates_management.xml', + 'views/survey_templates_print.xml', 'views/survey_templates_statistics.xml', 'views/gamification_badge_views.xml', 'wizard/survey_invite_views.xml', diff --git a/addons/survey/controllers/main.py b/addons/survey/controllers/main.py index 11b2e287e04..94ced8b83f8 100644 --- a/addons/survey/controllers/main.py +++ b/addons/survey/controllers/main.py @@ -5,7 +5,7 @@ import json import logging import werkzeug -from datetime import datetime +from datetime import datetime, timedelta from dateutil.relativedelta import relativedelta from odoo import fields, http, _ @@ -50,7 +50,6 @@ class Survey(http.Controller): * token_wrong: given token not recognized; * token_required: no token given although it is necessary to access the survey; - * answer_done: token linked to a finished answer; * answer_deadline: token linked to an expired answer; :param ensure_token: whether user input existence based on given access token @@ -79,9 +78,6 @@ class Survey(http.Controller): if (not survey_sudo.page_ids and survey_sudo.questions_layout == 'page_per_section') or not survey_sudo.question_ids: return 'survey_void' - if answer_sudo and answer_sudo.state == 'done': - return 'answer_done' - if answer_sudo and answer_sudo.deadline and answer_sudo.deadline < datetime.now(): return 'answer_deadline' @@ -139,8 +135,6 @@ class Survey(http.Controller): return request.render("survey.survey_auth_required", {'survey': survey_sudo, 'redirect_url': redirect_url}) elif error_key == 'answer_deadline' and answer_sudo.access_token: return request.render("survey.survey_closed_expired", {'survey': survey_sudo}) - elif error_key == 'answer_done' and answer_sudo.access_token: - return request.render("survey.survey_closed_finished", self._prepare_survey_finished_values(survey_sudo, answer_sudo, token=answer_sudo.access_token)) return werkzeug.utils.redirect("/") @@ -160,7 +154,7 @@ class Survey(http.Controller): """ This route is called whenever the user has attempts left and hits the 'Retry' button after failing the survey.""" access_data = self._get_access_data(survey_token, answer_token, ensure_token=True) - if access_data['validity_code'] is not True and access_data['validity_code'] != 'answer_done': + if access_data['validity_code'] is not True: return self._redirect_with_error(access_data, access_data['validity_code']) survey_sudo, answer_sudo = access_data['survey_sudo'], access_data['answer_sudo'] @@ -223,22 +217,20 @@ class Survey(http.Controller): else: return request.render("survey.survey_403_page", {'survey': survey_sudo}) - # Select the right page - if answer_sudo.state == 'new': # Intro page - data = {'survey': survey_sudo, 'answer': answer_sudo, 'page': 0} - return request.render('survey.survey_page_start', data) - else: - return request.redirect('/survey/fill/%s/%s' % (survey_sudo.access_token, answer_sudo.access_token)) - - @http.route('/survey/fill//', type='http', auth='public', website=True) - def survey_display_page(self, survey_token, answer_token, **post): - access_data = self._get_access_data(survey_token, answer_token, ensure_token=True) - if access_data['validity_code'] is not True: - return self._redirect_with_error(access_data, access_data['validity_code']) - - survey_sudo, answer_sudo = access_data['survey_sudo'], access_data['answer_sudo'] + return request.redirect('/survey/%s/%s' % (survey_sudo.access_token, answer_sudo.access_token)) + def _prepare_survey_data(self, survey_sudo, answer_sudo, **post): + """ This method prepares all the data needed for template rendering, in function of the survey user input state. + :param post: + - previous_page_id : come from the breadcrumb or the back button and force the next questions to load + to be the previous ones. """ data = { + 'survey': survey_sudo, + 'answer': answer_sudo, + 'breadcrumb_pages': [{ + 'id': page.id, + 'title': page.title, + } for page in survey_sudo.page_ids], 'format_datetime': lambda dt: format_datetime(request.env, dt, dt_format=False), 'format_date': lambda date: format_date(request.env, date) } @@ -250,48 +242,64 @@ class Survey(http.Controller): previous_page_or_question_id = int(post['previous_page_id']) new_previous_id = survey_sudo._previous_page_or_question_id(answer_sudo, previous_page_or_question_id) data.update({ - 'survey': survey_sudo, page_or_question_key: request.env['survey.question'].sudo().browse(previous_page_or_question_id), - 'answer': answer_sudo, 'previous_page_id': new_previous_id }) - return request.render('survey.survey_page_main', data) + return data - if survey_sudo.is_time_limited and not answer_sudo.start_datetime: - # init start date when user starts filling in the survey - answer_sudo.write({ - 'start_datetime': fields.Datetime.now() - }) - - # Select the right page - if answer_sudo.state == 'new': # First page - page_or_question_id, is_last = survey_sudo.next_page_or_question(answer_sudo, 0) - data.update({ - 'survey': survey_sudo, - page_or_question_key: page_or_question_id, - 'answer': answer_sudo, - }) - if is_last: - data.update({'last': True}) - return request.render('survey.survey_page_main', data) - elif answer_sudo.state == 'done': # Display success message - return request.render('survey.survey_closed_finished', self._prepare_survey_finished_values(survey_sudo, answer_sudo)) - elif answer_sudo.state == 'skip': - page_or_question_id, is_last = survey_sudo.next_page_or_question(answer_sudo, answer_sudo.last_displayed_page_id.id) - previous_id = survey_sudo._previous_page_or_question_id(answer_sudo, page_or_question_id.id) + if answer_sudo.state == 'in_progress': + page_or_question_id, is_last = survey_sudo.next_page_or_question( + answer_sudo, + answer_sudo.last_displayed_page_id.id if answer_sudo.last_displayed_page_id else 0) data.update({ - 'survey': survey_sudo, page_or_question_key: page_or_question_id, - 'answer': answer_sudo, - 'previous_page_id': previous_id }) + if survey_sudo.questions_layout != 'one_page': + data.update({ + 'previous_page_id': survey_sudo._previous_page_or_question_id(answer_sudo, page_or_question_id.id) + }) if is_last: data.update({'last': True}) + elif answer_sudo.state == 'done' or answer_sudo.is_time_limit_reached: # Display success message + return self._prepare_survey_finished_values(survey_sudo, answer_sudo) - return request.render('survey.survey_page_main', data) - else: - return request.render("survey.survey_403_page", {'survey': survey_sudo}) + return data + + def _prepare_question_html(self, survey_sudo, answer_sudo, **post): + """ Survey page navigation is done in AJAX. This function prepare the 'next page' to display in html + and send back this html to the survey_form widget that will inject it into the page.""" + data = self._prepare_survey_data(survey_sudo, answer_sudo, **post) + if answer_sudo.state == 'done': + return request.env.ref('survey.survey_fill_form_done').render(data).decode('UTF-8') + return request.env.ref('survey.survey_fill_form_in_progress').render(data).decode('UTF-8') + + @http.route('/survey//', type='http', auth='public', website=True) + def survey_display_page(self, survey_token, answer_token, **post): + access_data = self._get_access_data(survey_token, answer_token, ensure_token=True) + if access_data['validity_code'] is not True: + return self._redirect_with_error(access_data, access_data['validity_code']) + + return request.render('survey.survey_page_fill', + self._prepare_survey_data(access_data['survey_sudo'], access_data['answer_sudo'], **post)) + + @http.route('/survey/begin//', type='json', auth='public', website=True) + def survey_begin(self, survey_token, answer_token, **post): + """ Route used to start the survey user input and display the first survey page. """ + access_data = self._get_access_data(survey_token, answer_token, ensure_token=True) + if access_data['validity_code'] is not True: + return {'error': access_data['validity_code']} + survey_sudo, answer_sudo = access_data['survey_sudo'], access_data['answer_sudo'] + + if answer_sudo.state != "new": + return {'error': _("The survey has already started.")} + + # init start date when user starts filling in the survey + answer_sudo.write({ + 'start_datetime': fields.Datetime.now(), + 'state': 'in_progress' + }) + return self._prepare_question_html(survey_sudo, answer_sudo, **post) @http.route('/survey/submit//', type='json', auth='public', website=True) def survey_submit(self, survey_token, answer_token, **post): @@ -302,47 +310,42 @@ class Survey(http.Controller): # Survey Validation access_data = self._get_access_data(survey_token, answer_token, ensure_token=True) if access_data['validity_code'] is not True: - return { - 'error': access_data['validity_code'], - } + return {'error': access_data['validity_code']} survey_sudo, answer_sudo = access_data['survey_sudo'], access_data['answer_sudo'] + if answer_sudo.state == 'done': + return {'error': 'unauthorized'} + questions, page_or_question_id = survey_sudo._get_survey_questions(answer=answer_sudo, page_id=post.get('page_id'), question_id=post.get('question_id')) if not answer_sudo.test_entry and not survey_sudo._has_attempts_left(answer_sudo.partner_id, answer_sudo.email, answer_sudo.invite_token): # prevent cheating with users creating multiple 'user_input' before their last attempt - return {} + return {'error': 'unauthorized'} - if not answer_sudo.is_time_limit_reached: - # Prepare answers and comment by question - prepared_questions = {} - for question in questions: - answer_full = post.get(str(question.id)) - answer_without_comment, comment = self._extract_comment_from_answers(question, answer_full) - prepared_questions[question.id] = {'answer': answer_without_comment, 'comment': comment} + if answer_sudo.is_time_limit_reached: + time_limit = answer_sudo.start_datetime + timedelta(minutes=survey_sudo.time_limit) + if fields.Datetime.now() > (time_limit + timedelta(seconds=10)): + # prevent cheating with users blocking the JS timer and taking all their time to answer + return {'error': 'unauthorized'} - # Questions Validation - errors = {} - for question in questions: - answer = prepared_questions[question.id]['answer'] - comment = prepared_questions[question.id]['comment'] - errors.update(question.validate_question(answer, comment)) - if errors: - return {'error': 'validation', 'fields': errors} - - # Submitting questions - for question in questions: - answer = prepared_questions[question.id]['answer'] - comment = prepared_questions[question.id]['comment'] + errors = {} + # Prepare answers / comment by question, validate and save answers + for question in questions: + answer, comment = self._extract_comment_from_answers(question, post.get(str(question.id))) + errors.update(question.validate_question(answer, comment)) + if not errors.get(question.id): answer_sudo.save_lines(question, answer, comment) + if errors and not answer_sudo.is_time_limit_reached: + return {'error': 'validation', 'fields': errors} + if answer_sudo.is_time_limit_reached or survey_sudo.questions_layout == 'one_page': answer_sudo._mark_done() elif 'previous_page_id' in post: # Go back to specific page using the breadcrumb. Lines are saved and survey continues - return '/survey/fill/%s/%s?previous_page_id=%s' % (survey_sudo.access_token, answer_token, post['previous_page_id']) + return self._prepare_question_html(survey_sudo, answer_sudo, **post) else: next_page, unused = request.env['survey.survey'].next_page_or_question(answer_sudo, page_or_question_id) vals = {'last_displayed_page_id': page_or_question_id} @@ -350,11 +353,11 @@ class Survey(http.Controller): if next_page is None: answer_sudo._mark_done() else: - vals.update({'state': 'skip'}) + vals.update({'state': 'in_progress'}) answer_sudo.write(vals) - return '/survey/fill/%s/%s' % (survey_sudo.access_token, answer_token) + return self._prepare_question_html(survey_sudo, answer_sudo) def _extract_comment_from_answers(self, question, answers): """ Answers is a custom structure depending of the question type @@ -412,7 +415,7 @@ class Survey(http.Controller): access_data = self._get_access_data(survey_token, answer_token, ensure_token=False) if access_data['validity_code'] is not True and ( access_data['has_survey_access'] or - access_data['validity_code'] not in ['token_required', 'survey_closed', 'survey_void', 'answer_done']): + access_data['validity_code'] not in ['token_required', 'survey_closed', 'survey_void']): return self._redirect_with_error(access_data, access_data['validity_code']) survey_sudo, answer_sudo = access_data['survey_sudo'], access_data['answer_sudo'] diff --git a/addons/survey/data/survey_demo_certification.xml b/addons/survey/data/survey_demo_certification.xml index 389b8278e71..5ea4351605a 100644 --- a/addons/survey/data/survey_demo_certification.xml +++ b/addons/survey/data/survey_demo_certification.xml @@ -42,7 +42,6 @@ 2 Do we sell Acoustic Bloc Screens? simple_choice - dropdown @@ -145,7 +144,6 @@ 5 How many versions of the Corner Desk do we have? simple_choice - dropdown @@ -192,7 +190,6 @@ 8 How much do we sell our Cable Management Box? simple_choice - dropdown @@ -280,7 +277,6 @@ 10 What do you think about our prices (not rated)? simple_choice - dropdown diff --git a/addons/survey/data/survey_demo_feedback.xml b/addons/survey/data/survey_demo_feedback.xml index 6aff000dcdd..8a86b8e295f 100644 --- a/addons/survey/data/survey_demo_feedback.xml +++ b/addons/survey/data/survey_demo_feedback.xml @@ -42,7 +42,6 @@ 4 How frequently do you buy products online ? simple_choice - dropdown @@ -217,7 +216,7 @@ Martin Tamarre <martin@example.com> - skip + in_progress diff --git a/addons/survey/data/survey_demo_quiz.xml b/addons/survey/data/survey_demo_quiz.xml index 88bb3c7f694..c4202ef081a 100644 --- a/addons/survey/data/survey_demo_quiz.xml +++ b/addons/survey/data/survey_demo_quiz.xml @@ -89,7 +89,6 @@ Which category does a tomato belong to "Red" is not a category, I know what you are trying to do ;) simple_choice - dropdown @@ -118,7 +117,6 @@ 22 Which of the following would you use to pollinate simple_choice - columns diff --git a/addons/survey/data/survey_demo_quiz_userinput.xml b/addons/survey/data/survey_demo_quiz_userinput.xml index 66bdee2b811..15822b9c9cb 100644 --- a/addons/survey/data/survey_demo_quiz_userinput.xml +++ b/addons/survey/data/survey_demo_quiz_userinput.xml @@ -28,7 +28,7 @@ colleen.diaz83@example.com - skip + in_progress diff --git a/addons/survey/models/survey_question.py b/addons/survey/models/survey_question.py index 486c3d2d95d..24f55e00133 100644 --- a/addons/survey/models/survey_question.py +++ b/addons/survey/models/survey_question.py @@ -14,7 +14,7 @@ class SurveyQuestion(models.Model): """ Questions that will be asked in a survey. Each question can have one of more suggested answers (eg. in case of - dropdown choices, multi-answer checkboxes, radio buttons...). + multi-answer checkboxes, radio buttons...). Technical note: @@ -98,9 +98,6 @@ class SurveyQuestion(models.Model): ('12', '1'), ('6', '2'), ('4', '3'), ('3', '4'), ('2', '6')], string='Number of columns', default='12', help='These options refer to col-xx-[12|6|4|3|2] classes in Bootstrap for dropdown-based simple and multiple choice questions.') - display_mode = fields.Selection( - [('columns', 'Radio Buttons'), ('dropdown', 'Selection Box')], - string='Display Mode', default='columns', help='Display mode of simple choice questions.') # -- comments (simple choice, multiple choice, matrix (without count as an answer)) comments_allowed = fields.Boolean('Show Comments Field') comments_message = fields.Char('Comment Message', translate=True, default=lambda self: _("If other, please specify:")) diff --git a/addons/survey/models/survey_user.py b/addons/survey/models/survey_user.py index 8db25a544ea..56b4ad2d5df 100644 --- a/addons/survey/models/survey_user.py +++ b/addons/survey/models/survey_user.py @@ -25,7 +25,7 @@ class SurveyUserInput(models.Model): deadline = fields.Datetime('Deadline', help="Datetime until customer can open the survey and submit answers") state = fields.Selection([ ('new', 'Not started yet'), - ('skip', 'Partially completed'), + ('in_progress', 'In Progress'), ('done', 'Completed')], string='Status', default='new', readonly=True) test_entry = fields.Boolean(readonly=True) last_displayed_page_id = fields.Many2one('survey.question', string='Last displayed question/page') @@ -72,8 +72,11 @@ class SurveyUserInput(models.Model): def _compute_is_time_limit_reached(self): """ Checks that the user_input is not exceeding the survey's time limit. """ for user_input in self: - user_input.is_time_limit_reached = user_input.survey_id.is_time_limited and fields.Datetime.now() \ - > user_input.start_datetime + relativedelta(minutes=user_input.survey_id.time_limit) + if user_input.start_datetime and user_input.survey_id.is_time_limited: + datetime_limit = user_input.start_datetime + relativedelta(minutes=user_input.survey_id.time_limit) + user_input.is_time_limit_reached = fields.Datetime.now() > datetime_limit + else: + user_input.is_time_limit_reached = False @api.depends('state', 'test_entry', 'survey_id.is_attempts_limited', 'partner_id', 'email', 'invite_token') def _compute_attempts_number(self): @@ -261,7 +264,7 @@ class SurveyUserInput(models.Model): return vals if answer_type == 'suggestion': - vals['suggested_answer_id'] = answer + vals['suggested_answer_id'] = int(answer) elif answer_type == 'numerical_box': vals['value_numerical_box'] = float(answer) else: diff --git a/addons/survey/static/src/js/survey_breadcrumb.js b/addons/survey/static/src/js/survey_breadcrumb.js new file mode 100644 index 00000000000..4cc3796f9cb --- /dev/null +++ b/addons/survey/static/src/js/survey_breadcrumb.js @@ -0,0 +1,50 @@ +odoo.define('survey.breadcrumb', function (require) { +'use strict'; + +var publicWidget = require('web.public.widget'); + +publicWidget.registry.SurveyBreadcrumbWidget = publicWidget.Widget.extend({ + xmlDependencies: ['/survey/static/src/xml/survey_breadcrumb_templates.xml'], + template: "survey.survey_breadcrumb_template", + events: { + 'click .breadcrumb-item a': '_onBreadcrumbClick', + }, + + /** + * @override + */ + init: function (parent, options) { + this._super.apply(this, arguments); + this.canGoBack = options.canGoBack; + this.currentPageId = options.currentPageId; + this.pages = options.pages; + }, + + // Handlers + // ------------------------------------------------------------------- + + _onBreadcrumbClick: function (event) { + event.preventDefault(); + this.trigger_up('breadcrumb_click', { + 'previousPageId': this.$(event.currentTarget) + .closest('.breadcrumb-item') + .data('pageId') + }); + }, + + // PUBLIC METHODS + // ------------------------------------------------------------------- + + updateBreadcrumb: function (pageId) { + if (pageId) { + this.currentPageId = pageId; + this.renderElement(); + } else { + this.$('.breadcrumb').addClass('d-none'); + } + }, +}); + +return publicWidget.registry.SurveyBreadcrumbWidget; + +}); diff --git a/addons/survey/static/src/js/survey_form.js b/addons/survey/static/src/js/survey_form.js index c28cbebf8ec..02f9e567365 100644 --- a/addons/survey/static/src/js/survey_form.js +++ b/addons/survey/static/src/js/survey_form.js @@ -1,16 +1,20 @@ -odoo.define('survey.form', function(require) { +odoo.define('survey.form', function (require) { 'use strict'; var field_utils = require('web.field_utils'); var publicWidget = require('web.public.widget'); var time = require('web.time'); +var core = require('web.core'); +var _t = core._t; publicWidget.registry.SurveyFormWidget = publicWidget.Widget.extend({ selector: '.o_survey_form', events: { 'change .o_survey_form_choice_item': '_onChangeChoiceItem', 'click button[type="submit"]': '_onSubmit', - 'click .o_survey_header .breadcrumb-item a': '_onBreadcrumbClick', + }, + custom_events: { + 'breadcrumb_click': '_onBreadcrumbClick', }, //-------------------------------------------------------------------------- @@ -22,20 +26,12 @@ publicWidget.registry.SurveyFormWidget = publicWidget.Widget.extend({ */ start: function () { var self = this; - return this._super.apply(this, arguments).then(function() { + this.fadeInOutDelay = 400; + return this._super.apply(this, arguments).then(function () { self.options = self.$target.find('form').data(); - var $timer = $('.o_survey_timer'); - if ($timer.length) { - var timeLimitMinutes = self.options.timeLimitMinutes; - var timer = self.options.timer; - self.surveyTimerWidget = new publicWidget.registry.SurveyTimerWidget(self, { - 'timer': timer, - 'timeLimitMinutes': timeLimitMinutes - }); - self.surveyTimerWidget.attachTo($timer); - self.surveyTimerWidget.on('time_up', self, function (ev) { - self.$el.find('button[type="submit"]').click(); - }); + if (!self.options.isStartScreen) { + self._initTimer(); + self._initBreadcrumb(); } self.$('div.o_survey_form_date').each(function () { self._initDateTimePicker($(this)); @@ -50,12 +46,8 @@ publicWidget.registry.SurveyFormWidget = publicWidget.Widget.extend({ // Handlers // ------------------------------------------------------------------------- - /* + /** * Checks, if the 'other' choice is checked. Applies only if the comment count as answer. - * In case of dropdownlist: - * If not checked : Clear the comment textarea and hide it - * If checked : show the comment textarea and focus on it - * In case of radio buttons * If not checked : Clear the comment textarea and disable it * If checked : enable the comment textarea and focus on it * @@ -67,93 +59,260 @@ publicWidget.registry.SurveyFormWidget = publicWidget.Widget.extend({ var $otherItem = $choiceItemGroup.find('.o_survey_js_form_other_comment'); var $commentInput = $choiceItemGroup.find('textarea[type="text"]'); - // Handle dropdownlist - var $dropdownlist = $choiceItemGroup.find('select'); - var isDropdown = $dropdownlist.length > 0; - - var isOtherSelected = (isDropdown && $dropdownlist.val() === $otherItem.val()) || $otherItem.prop('checked'); - - if (isOtherSelected || $commentInput.hasClass('o_survey_comment')) { - if (isDropdown) { - $commentInput.toggleClass('d-none', false); - } - if (isOtherSelected) { - $commentInput.enable(); + if ($otherItem.prop('checked') || $commentInput.hasClass('o_survey_comment')) { + $commentInput.enable(); + if ($otherItem.prop('checked')) { $commentInput.focus(); } } else { - if (isDropdown) { - $commentInput.toggleClass('d-none', true); - } $commentInput.val(''); $commentInput.enable(false); } }, - /* - * When clicking on a breadcrumb enabled item, redirect to the target page. - * Uses the submit flow to validate and save the answer before going to the specified page. - * - * @private - * @param {Event} event - */ - _onBreadcrumbClick: function (event) { - event.preventDefault(); - this._submitForm({'previous_page_id': $(event.currentTarget).data('previousPageId')}); - }, - _onSubmit: function (event) { event.preventDefault(); - if ($(event.currentTarget).val() === 'previous') { - this._submitForm({'previous_page_id': $(event.currentTarget).data('previousPageId')}); - } else { - this._submitForm({}); + var options = {}; + var $target = $(event.currentTarget); + if ($target.val() === 'previous') { + options.previousPageId = $target.data('previousPageId'); + } else if ($target.val() === 'finish') { + options.isFinish = true; } + this._submitForm(options); }, - _submitForm: function (params) { + // Custom Events + // ------------------------------------------------------------------------- + + _onBreadcrumbClick: function (event) { + this._submitForm({'previousPageId': event.data.previousPageId}); + }, + + // SUBMIT + // ------------------------------------------------------------------------- + + /** + * This function will send a json rpc call to the server to + * - start the survey (if we are on start screen) + * - submit the answers of the current page + * Before submitting the answers, they are first validated to avoid latency from the server + * and allow a fade out/fade in transition of the next question. + * + * @param {Array} [options] + * @param {Integer} [options.previousPageId] navigates to page id + * @param {Boolean} [options.skipValidation] skips JS validation + * @param {Boolean} [options.isFinish] fades out breadcrumb and timer + * @private + */ + _submitForm: function (options) { var self = this; - var $form = this.$('form'); - var formData = new FormData($form[0]); + var params = {}; + if (options.previousPageId) { + params.previous_page_id = options.previousPageId; + } + var route = "/survey/submit"; - this._prepareSubmitValues(formData, params); + if (this.options.isStartScreen) { + route = "/survey/begin"; + } else { + var $form = this.$('form'); + var formData = new FormData($form[0]); - this._resetErrors(); + if (!options.skipValidation) { + // Validation pre submit + if (!this._validateForm($form, formData)) { + return; + } + } - return self._rpc({ - route: '/survey/submit/' + self.options.surveyToken + '/' + self.options.answerToken , + this._prepareSubmitValues(formData, params); + } + + var resolveFadeOut; + var fadeOutPromise = new Promise(function (resolve, reject) {resolveFadeOut = resolve;}); + + var selectorsToFadeout = ['.o_survey_form_content']; + if (options.isFinish) { + selectorsToFadeout.push('.breadcrumb', '.o_survey_timer'); + } + self.$(selectorsToFadeout.join(',')).fadeOut(this.fadeInOutDelay, function () { + resolveFadeOut(); + }); + var submitPromise = self._rpc({ + route: _.str.sprintf('%s/%s/%s', route, self.options.surveyToken, self.options.answerToken), params: params, - }).then(function (result) { - return self._onSubmitDone(result, params); + }); + Promise.all([fadeOutPromise, submitPromise]).then(function (results) { + return self._onSubmitDone(results[1], options.isFinish); }); }, - _onSubmitDone: function (result, params) { + /** + * Follow the submit and handle the transition from one screen to another + * Also handle server side validation and displays eventual error messages. + */ + _onSubmitDone: function (result, isFinish) { var self = this; + if (result && !result.error) { - window.location = result; + this.$(".o_survey_form_content").empty(); + this.$(".o_survey_form_content").html(result); + this.$('div.o_survey_form_date').each(function () { + self._initDateTimePicker($(this)); + }); + if (this.options.isStartScreen) { + this._initTimer(); + this.options.isStartScreen = false; + } + if (isFinish) { + this._initResultWidget(); + if (this.surveyBreadcrumbWidget) { + this.$('.o_survey_breadcrumb_container').addClass('d-none'); + this.surveyBreadcrumbWidget.destroy(); + } + if (this.surveyTimerWidget) { + this.surveyTimerWidget.destroy(); + } + } else { + this._updateBreadcrumb(); + } + this.$('.o_survey_form_content').fadeIn(this.fadeInOutDelay); + $("html, body").animate({ scrollTop: 0 }, this.fadeInOutDelay); } else if (result && result.fields && result.error === 'validation') { - var fieldKeys = _.keys(result.fields); - _.each(fieldKeys, function (key) { - self.$("#" + key + '>.o_survey_question_error').append($('

', {text: result.fields[key]})).toggleClass('d-none', false); - if (fieldKeys[fieldKeys.length - 1] === key) { - self._scrollToError(self.$('.o_survey_question_error:visible:first').closest('.js_question-wrapper')); - } - }); - return false; - } - else { - var $target = self.$('.o_survey_error'); - $target.toggleClass('d-none', false); - self._scrollToError($target); + this.$('.o_survey_form_content').fadeIn(0); + this._showErrors(result.fields); + } else { + var $errorTarget = this.$('.o_survey_error'); + $errorTarget.removeClass("d-none"); + this._scrollToError($errorTarget); + } + }, + + // VALIDATION TOOLS + // ------------------------------------------------------------------------- + /** + * Validation is done in frontend before submit to avoid latency from the server. + * If the validation is incorrect, the errors are displayed before submitting and + * fade in / out of submit is avoided. + * + * Each question type gets its own validation process. + * + * There is a special use case for the 'required' questions, where we use the constraint + * error message that comes from the question configuration ('constr_error_msg' field). + * + * @private + */ + _validateForm: function ($form, formData) { + var self = this; + var errors = {}; + var validationEmailMsg = _t("This answer must be an email address."); + var validationDateMsg = _t("This is not a date"); + + this._resetErrors(); + + var data = {}; + formData.forEach(function (value, key) { + data[key] = value; + }); + + $form.find('[data-question-type]').each(function () { + var $input = $(this); + var $questionWrapper = $input.closest(".js_question-wrapper"); + var constrErrorMsg = $questionWrapper.data('constrErrorMsg'); + var validationErrorMsg = $questionWrapper.data('validationErrorMsg'); + var questionId = $questionWrapper.attr('id'); + var questionRequired = $questionWrapper.data('required'); + switch ($input.data('questionType')) { + case 'char_box': + if (questionRequired && !$input.val()) { + errors[questionId] = constrErrorMsg; + } else if ($input.val() && $input.attr('type') === 'email' && !self._validateEmail($input.val())) { + errors[questionId] = validationEmailMsg; + } else { + var lengthMin = $input.data('validationLengthMin'); + var lengthMax = $input.data('validationLengthMax'); + var length = $input.val().length; + if (lengthMin && (lengthMin > length || length > lengthMax)) { + errors[questionId] = validationErrorMsg; + } + } + break; + case 'numerical_box': + if (questionRequired && !data[questionId]) { + errors[questionId] = constrErrorMsg; + } else { + var floatMin = $input.data('validationFloatMin'); + var floatMax = $input.data('validationFloatMax'); + var value = parseFloat($input.val()); + if (floatMin && (floatMin > value || value > floatMax)) { + errors[questionId] = validationErrorMsg; + } + } + break; + case 'date': + case 'datetime': + if (questionRequired && !data[questionId]) { + errors[questionId] = constrErrorMsg; + } else if (data[questionId]) { + var momentDate = moment($input.val()); + if (!momentDate.isValid()) { + errors[questionId] = validationDateMsg; + } else { + var $dateDiv = $questionWrapper.find('.o_survey_form_date'); + var maxDate = $dateDiv.data('maxdate'); + var minDate = $dateDiv.data('mindate'); + if ((maxDate && momentDate.isAfter(moment(maxDate))) + || (minDate && momentDate.isBefore(moment(minDate)))) { + errors[questionId] = validationErrorMsg; + } + } + } + break; + case 'simple_choice_radio': + case 'multiple_choice': + if (questionRequired) { + var $textarea = $questionWrapper.find('textarea'); + if (!data[questionId]) { + errors[questionId] = constrErrorMsg; + } else if (data[questionId] === '-1' && !$textarea.val()) { + // if other has been checked and value is null + errors[questionId] = constrErrorMsg; + } + } + break; + case 'matrix': + if (questionRequired) { + var subQuestionsIds = $questionWrapper.find('table').data('subQuestions'); + subQuestionsIds.forEach(function (id) { + if (!((questionId + '_' + id) in data)) { + errors[questionId] = constrErrorMsg; + } + }); + } + break; + } + }); + if (_.keys(errors).length > 0) { + this._showErrors(errors); return false; } + return true; + }, + + /** + * Check if the email has an '@', a left part and a right part + * @private + */ + _validateEmail: function (email) { + var emailParts = email.split('@'); + return emailParts.length === 2 && emailParts[0] && emailParts[1]; }, // PREPARE SUBMIT TOOLS // ------------------------------------------------------------------------- - /* + /** * For each type of question, extract the answer from inputs or textarea (comment or answer) * * @@ -162,8 +321,7 @@ publicWidget.registry.SurveyFormWidget = publicWidget.Widget.extend({ */ _prepareSubmitValues: function (formData, params) { var self = this; - // Get all context params -- TODO : Use formData instead (test if input with no name are in formData) - formData.forEach(function(value, key){ + formData.forEach(function (value, key){ switch (key) { case 'csrf_token': case 'token': @@ -188,12 +346,9 @@ publicWidget.registry.SurveyFormWidget = publicWidget.Widget.extend({ case 'datetime': params = self._prepareSubmitDates(params, this.name, this.value, true); break; - case 'simple_choice_dropdown': - params = self._prepareSubmitChoices(params, $(this), $(this).data('name'), 'option:selected'); - break; case 'simple_choice_radio': case 'multiple_choice': - params = self._prepareSubmitChoices(params, $(this), $(this).data('name'), 'input:checked'); + params = self._prepareSubmitChoices(params, $(this), $(this).data('name')); break; case 'matrix': params = self._prepareSubmitAnswersMatrix(params, $(this)); @@ -207,7 +362,7 @@ publicWidget.registry.SurveyFormWidget = publicWidget.Widget.extend({ * Convert date value from client current timezone to UTC Date to correspond to the server format. * return params = { 'dateQuestionId' : '2019-05-23', 'datetimeQuestionId' : '2019-05-23 14:05:12' } */ - _prepareSubmitDates: function(params, questionId, value, isDateTime) { + _prepareSubmitDates: function (params, questionId, value, isDateTime) { var momentDate = isDateTime ? field_utils.parse.datetime(value, null, {timezone: true}) : field_utils.parse.date(value); var formattedDate = momentDate ? momentDate.toJSON() : ''; params[questionId] = formattedDate; @@ -219,9 +374,9 @@ publicWidget.registry.SurveyFormWidget = publicWidget.Widget.extend({ * If the answer is not the 'comment selection' (=Other), calls the _prepareSubmitAnswer method to add the answer to the params * If there is a comment linked to that question, calls the _prepareSubmitComment method to add the comment to the params */ - _prepareSubmitChoices: function(params, $parent, questionId, selector) { + _prepareSubmitChoices: function (params, $parent, questionId) { var self = this; - $parent.find(selector).each(function () { + $parent.find('input:checked').each(function () { if (this.value !== '-1') { params = self._prepareSubmitAnswer(params, questionId, this.value); } @@ -271,7 +426,7 @@ publicWidget.registry.SurveyFormWidget = publicWidget.Widget.extend({ * Lonely answer are directly assigned to questionId. Multiple answers are regrouped in an array: * params = { 'questionId1' : lonelyAnswer, 'questionId2' : [multipleAnswer1, multipleAnswer2, ...] } */ - _prepareSubmitAnswer: function(params, questionId, value) { + _prepareSubmitAnswer: function (params, questionId, value) { if (questionId in params) { if (params[questionId].constructor === Array) { params[questionId].push(value); @@ -290,7 +445,7 @@ publicWidget.registry.SurveyFormWidget = publicWidget.Widget.extend({ * with the new value. At the end, the result looks like : * params = { 'questionId1' : {'comment': commentValue}, 'questionId2' : [multipleAnswer1, {'comment': commentValue}, ...] } */ - _prepareSubmitComment: function(params, $parent, questionId, isMatrix) { + _prepareSubmitComment: function (params, $parent, questionId, isMatrix) { var self = this; $parent.find('textarea').each(function () { if (this.value) { @@ -308,6 +463,58 @@ publicWidget.registry.SurveyFormWidget = publicWidget.Widget.extend({ // INIT FIELDS TOOLS // ------------------------------------------------------------------------- + /** + * Will initialize the breadcrumb widget that handles navigation to a previously filled in page. + * + * @private + */ + _initBreadcrumb: function () { + var $breadcrumb = this.$('.o_survey_breadcrumb_container'); + var pageId = this.$('input[name=page_id]').val(); + if ($breadcrumb.length) { + this.surveyBreadcrumbWidget = new publicWidget.registry.SurveyBreadcrumbWidget(this, { + 'canGoBack': $breadcrumb.data('canGoBack'), + 'currentPageId': pageId ? parseInt(pageId) : 0, + 'pages': $breadcrumb.data('pages'), + }); + this.surveyBreadcrumbWidget.appendTo($breadcrumb); + $breadcrumb.removeClass('d-none'); // hidden by default to avoid having ghost div in start screen + } + }, + + /** + * Called after survey submit to update the breadcrumb to the right page. + */ + _updateBreadcrumb: function () { + if (this.surveyBreadcrumbWidget) { + var pageId = this.$('input[name=page_id]').val(); + this.surveyBreadcrumbWidget.updateBreadcrumb(parseInt(pageId)); + } else { + this._initBreadcrumb(); + } + }, + + _initTimer: function () { + var self = this; + var $timer = this.$('.o_survey_timer'); + if ($timer.length) { + var timeLimitMinutes = this.options.timeLimitMinutes; + var timer = this.options.timer; + this.surveyTimerWidget = new publicWidget.registry.SurveyTimerWidget(this, { + 'timer': timer, + 'timeLimitMinutes': timeLimitMinutes + }); + this.surveyTimerWidget.attachTo($timer); + this.surveyTimerWidget.on('time_up', this, function (ev) { + self._submitForm({ + 'skipValidation': true, + 'isFinish': true + }); + }); + $timer.removeClass('d-none'); + } + }, + /** * Initialize datetimepicker in correct format and with constraints */ @@ -319,7 +526,7 @@ publicWidget.registry.SurveyFormWidget = publicWidget.Widget.extend({ var maxDateData = $dateGroup.data('maxdate'); var maxDate = maxDateData ? this._formatDateTime(maxDateData) : moment().add(200, "y"); - var datetimepickerFormat = time.getLangDateFormat() + var datetimepickerFormat = time.getLangDateFormat(); if ($dateGroup.find('input').data('questionType') === 'datetime') { datetimepickerFormat = time.getLangDatetimeFormat(); } else { @@ -353,13 +560,33 @@ publicWidget.registry.SurveyFormWidget = publicWidget.Widget.extend({ }); }, - _formatDateTime: function (datetimeValue){ + _formatDateTime: function (datetimeValue) { return field_utils.format.datetime(moment(datetimeValue), null, {timezone: true}); }, + _initResultWidget: function () { + var $result = this.$('.o_survey_result'); + if ($result.length) { + this.surveyResultWidget = new publicWidget.registry.SurveyResultWidget(this); + this.surveyResultWidget.attachTo($result); + $result.fadeIn(this.fadeInOutDelay); + } + }, + // ERRORS TOOLS // ------------------------------------------------------------------------- + _showErrors: function (errors) { + var self = this; + var errorKeys = _.keys(errors); + _.each(errorKeys, function (key) { + self.$("#" + key + '>.o_survey_question_error').append($('

', {text: errors[key]})).addClass("slide_in"); + if (errorKeys[0] === key) { + self._scrollToError(self.$('.js_question-wrapper#' + key)); + } + }); + }, + _scrollToError: function ($target) { var scrollLocation = $target.offset().top; var navbarHeight = $('.o_main_navbar').height(); @@ -375,8 +602,8 @@ publicWidget.registry.SurveyFormWidget = publicWidget.Widget.extend({ * Clean all form errors in order to clean DOM before a new validation */ _resetErrors: function () { - this.$('.o_survey_question_error').empty().toggleClass("d-none", true); - this.$('.o_survey_error').toggleClass("d-none", true); + this.$('.o_survey_question_error').empty().removeClass('slide_in'); + this.$('.o_survey_error').addClass('d-none'); }, }); diff --git a/addons/survey/static/src/js/survey_timer.js b/addons/survey/static/src/js/survey_timer.js index 79f8b07c8dd..664871e4e7e 100644 --- a/addons/survey/static/src/js/survey_timer.js +++ b/addons/survey/static/src/js/survey_timer.js @@ -3,10 +3,6 @@ odoo.define('survey.timer', function (require) { var publicWidget = require('web.public.widget'); -var interval = null; -var $timer = null; -var countDownDate = null; - publicWidget.registry.SurveyTimerWidget = publicWidget.Widget.extend({ //-------------------------------------------------------------------------- // Widget @@ -17,8 +13,9 @@ publicWidget.registry.SurveyTimerWidget = publicWidget.Widget.extend({ */ init: function (parent, params) { this._super.apply(this, arguments); - this.$timer = params.timer; - this.$timeLimitMinutes = params.timeLimitMinutes; + this.timer = params.timer; + this.timeLimitMinutes = params.timeLimitMinutes; + this.surveyTimerInterval = null; }, @@ -30,13 +27,13 @@ publicWidget.registry.SurveyTimerWidget = publicWidget.Widget.extend({ start: function () { var superDef = this._super.apply(this, arguments); - countDownDate = moment.utc(this.$timer).add(this.$timeLimitMinutes, 'minutes'); - if (this.$timeLimitMinutes <= 0 || countDownDate.diff(moment.utc(), 'seconds') < 0) { + this.countDownDate = moment.utc(this.timer).add(this.timeLimitMinutes, 'minutes'); + if (this.timeLimitMinutes <= 0 || this.countDownDate.diff(moment.utc(), 'seconds') < 0) { this.trigger_up('time_up'); } else { - $timer = this.$el.find('.timer'); + this.$timer = this.$el.find('.timer'); this._updateTimer(this); - interval = setInterval(this._updateTimer.bind(this), 1000); + this.surveyTimerInterval = setInterval(this._updateTimer.bind(this), 1000); } return superDef; @@ -55,14 +52,14 @@ publicWidget.registry.SurveyTimerWidget = publicWidget.Widget.extend({ * When the time runs out, it triggers a 'time_up' event to notify the parent widget. */ _updateTimer: function () { - var timeLeft = countDownDate.diff(moment.utc(), 'seconds'); + var timeLeft = this.countDownDate.diff(moment.utc(), 'seconds'); if (timeLeft >= 0) { var timeLeftMinutes = parseInt(timeLeft / 60); var timeLeftSeconds = timeLeft - (timeLeftMinutes * 60); - $timer.html(this._formatTime(timeLeftMinutes) + ':' + this._formatTime(timeLeftSeconds)); + this.$timer.html(this._formatTime(timeLeftMinutes) + ':' + this._formatTime(timeLeftSeconds)); } else { - clearInterval(interval); + clearInterval(this.surveyTimerInterval); this.trigger_up('time_up'); } }, diff --git a/addons/survey/static/src/scss/survey_templates.scss b/addons/survey/static/src/scss/survey_templates.scss index 606db7b8621..4a53c4ac0af 100644 --- a/addons/survey/static/src/scss/survey_templates.scss +++ b/addons/survey/static/src/scss/survey_templates.scss @@ -10,3 +10,19 @@ .js_question-wrapper { padding-bottom: 25px; } + +.o_survey_breadcrumb_container { + min-height: 3rem; +} + +.o_survey_question_error { + overflow: hidden; + height: 0px; + border: 0px; + padding: 0rem 0.75rem; + transition: height .5s ease; + line-height: 4rem; + &.slide_in { + height: 4rem; + } +} diff --git a/addons/survey/static/src/xml/survey_breadcrumb_templates.xml b/addons/survey/static/src/xml/survey_breadcrumb_templates.xml new file mode 100644 index 00000000000..8893d711815 --- /dev/null +++ b/addons/survey/static/src/xml/survey_breadcrumb_templates.xml @@ -0,0 +1,29 @@ + + + + +

+ + + diff --git a/addons/survey/static/tests/tours/certification_failure.js b/addons/survey/static/tests/tours/certification_failure.js index 5b5fb1c9ea0..fa6849e63e9 100644 --- a/addons/survey/static/tests/tours/certification_failure.js +++ b/addons/survey/static/tests/tours/certification_failure.js @@ -1,6 +1,17 @@ odoo.define('survey.tour_test_certification_failure', function (require) { 'use strict'; +var SurveyFormWidget = require('survey.form'); +/** + * Speed up fade-in fade-out to avoid useless delay in tests. + */ +SurveyFormWidget.include({ + _submitForm: function () { + this.fadeInOutDelay = 0; + return this._super.apply(this, arguments); + } +}); + /** * This tour will test that, for the demo certification allowing 2 attempts, a user can * try and fail twice and will no longer be able to take the certification. @@ -10,11 +21,10 @@ var tour = require('web_tour.tour'); var failSteps = [{ // Page-1 content: "Clicking on Start Certification", - trigger: 'a.btn.btn-primary.btn-lg:contains("Start Certification")', + trigger: 'button.btn.btn-primary.btn-lg:contains("Start Certification")', }, { // Question: Do we sell Acoustic Bloc Screens? content: "Selecting answer 'No'", - trigger: 'div.js_question-wrapper:contains("Do we sell Acoustic Bloc Screens") select', - run: 'text No', + trigger: 'div.js_question-wrapper:contains("Do we sell Acoustic Bloc Screens") label:contains("No") input', }, { // Question: Select all the existing products content: "Ticking answer 'Fanta'", trigger: 'div.js_question-wrapper:contains("Select all the existing products") label:contains("Fanta") input' @@ -32,19 +42,14 @@ var failSteps = [{ // Page-1 trigger: 'div.js_question-wrapper:contains("Select all the available customizations for our Customizable Desk") label:contains("Height") input' }, { // Question: How many versions of the Corner Desk do we have? content: "Selecting answer '2'", - trigger: 'div.js_question-wrapper:contains("How many versions of the Corner Desk do we have") select', - run: 'text 2', + trigger: 'div.js_question-wrapper:contains("How many versions of the Corner Desk do we have") label:contains("2") input', }, { // Question: Do you think we have missing products in our catalog? (not rated) content: "Missing products", trigger: 'div.js_question-wrapper:contains("Do you think we have missing products in our catalog") textarea', run: "text I don't know products enough to be able to answer that", }, { // Page-2 Question: How much do we sell our Cable Management Box? content: "Selecting answer '80$'", - trigger: 'div.js_question-wrapper:contains("How much do we sell our Cable Management Box") select', - run: function () { - var $select = $('div.js_question-wrapper:contains("How much do we sell our Cable Management Box") select'); - $select.val($('option:contains("80$")').val()); - } + trigger: 'div.js_question-wrapper:contains("How much do we sell our Cable Management Box") label:contains("80$") input', }, { // Question: Select all the the products that sell for 100$ or more content: "Ticking answer 'Corner Desk Right Sit'", trigger: 'div.js_question-wrapper:contains("Select all the the products that sell for 100$ or more") label:contains("Corner Desk Right Sit") input' @@ -55,11 +60,7 @@ var failSteps = [{ // Page-1 content: "Ticking answer 'Office Chair Black'", trigger: 'div.js_question-wrapper:contains("Select all the the products that sell for 100$ or more") label:contains("Office Chair Black") input' }, { // Question: What do you think about our prices (not rated)? - trigger: 'div.js_question-wrapper:contains("What do you think about our prices") select', - run: function () { - var $select = $('div.js_question-wrapper:contains("What do you think about our prices") select'); - $select.val($('option:contains("Correctly priced")').val()); - } + trigger: 'div.js_question-wrapper:contains("What do you think about our prices") label:contains("Correctly priced") input', }, { content: "Finish Survey", trigger: 'button[type="submit"]', diff --git a/addons/survey/static/tests/tours/certification_success.js b/addons/survey/static/tests/tours/certification_success.js index 03d3a22a9d7..584c357f7ba 100644 --- a/addons/survey/static/tests/tours/certification_success.js +++ b/addons/survey/static/tests/tours/certification_success.js @@ -1,6 +1,17 @@ odoo.define('survey.tour_test_certification_success', function (require) { 'use strict'; +var SurveyFormWidget = require('survey.form'); +/** + * Speed up fade-in fade-out to avoid useless delay in tests. + */ +SurveyFormWidget.include({ + _submitForm: function () { + this.fadeInOutDelay = 0; + return this._super.apply(this, arguments); + } +}); + var tour = require('web_tour.tour'); tour.register('test_certification_success', { @@ -9,11 +20,10 @@ tour.register('test_certification_success', { }, [{ // Page-1 content: "Clicking on Start Certification", - trigger: 'a.btn.btn-primary.btn-lg:contains("Start Certification")', + trigger: 'button.btn.btn-primary.btn-lg:contains("Start Certification")', }, { // Question: Do we sell Acoustic Bloc Screens? content: "Selecting answer 'Yes'", - trigger: 'div.js_question-wrapper:contains("Do we sell Acoustic Bloc Screens") select', - run: 'text Yes', + trigger: 'div.js_question-wrapper:contains("Do we sell Acoustic Bloc Screens") label:contains("Yes") input', }, { // Question: Select all the existing products content: "Ticking answer 'Chair floor protection'", trigger: 'div.js_question-wrapper:contains("Select all the existing products") label:contains("Chair floor protection") input' @@ -31,19 +41,14 @@ tour.register('test_certification_success', { trigger: 'div.js_question-wrapper:contains("Select all the available customizations for our Customizable Desk") label:contains("Legs") input' }, { // Question: How many versions of the Corner Desk do we have? content: "Selecting answer '2'", - trigger: 'div.js_question-wrapper:contains("How many versions of the Corner Desk do we have") select', - run: 'text 2', + trigger: 'div.js_question-wrapper:contains("How many versions of the Corner Desk do we have") label:contains("2") input', }, { // Question: Do you think we have missing products in our catalog? (not rated) content: "Missing products", trigger: 'div.js_question-wrapper:contains("Do you think we have missing products in our catalog") textarea', run: "text I think we should make more versions of the customizable desk, it's such an amazing product!", }, { // Page-2 Question: How much do we sell our Cable Management Box? content: "Selecting answer '80$' (wrong one)", - trigger: 'div.js_question-wrapper:contains("How much do we sell our Cable Management Box") select', - run: function () { - var $select = $('div.js_question-wrapper:contains("How much do we sell our Cable Management Box") select'); - $select.val($('option:contains("80$")').val()); - } + trigger: 'div.js_question-wrapper:contains("How much do we sell our Cable Management Box") label:contains("80$") input', }, { // Question: Select all the the products that sell for 100$ or more content: "Ticking answer 'Corner Desk Right Sit'", trigger: 'div.js_question-wrapper:contains("Select all the the products that sell for 100$ or more") label:contains("Corner Desk Right Sit") input' @@ -55,11 +60,7 @@ tour.register('test_certification_success', { trigger: 'div.js_question-wrapper:contains("Select all the the products that sell for 100$ or more") label:contains("Large Desk") input' }, { // Question: What do you think about our prices (not rated)? content: "Selecting answer 'Underpriced'", - trigger: 'div.js_question-wrapper:contains("What do you think about our prices") select', - run: function () { - var $select = $('div.js_question-wrapper:contains("What do you think about our prices") select'); - $select.val($('option:contains("Underpriced")').val()); - } + trigger: 'div.js_question-wrapper:contains("What do you think about our prices") label:contains("Underpriced") input', }, { content: "Finish Survey", trigger: 'button[type="submit"]', diff --git a/addons/survey/static/tests/tours/survey.js b/addons/survey/static/tests/tours/survey.js index e4540fba1ab..fce408f9cbe 100644 --- a/addons/survey/static/tests/tours/survey.js +++ b/addons/survey/static/tests/tours/survey.js @@ -10,7 +10,7 @@ tour.register('test_survey', { // Page-1 { content: 'Click on Start', - trigger: 'a.btn:contains("Start")', + trigger: 'button.btn:contains("Start")', }, { content: 'Answer Where do you live', trigger: 'div.js_question-wrapper:contains("Where do you live") input', @@ -21,11 +21,7 @@ tour.register('test_survey', { run: 'text 05/05/1980', }, { content: 'Answer How frequently do you buy products online', - trigger: 'div.js_question-wrapper:contains("How frequently do you buy products online") select', - run: function () { - var $select = $('div.js_question-wrapper:contains("How frequently do you buy products online") select'); - $select.val($('option:contains("Once a month")').val()); - } + trigger: 'div.js_question-wrapper:contains("How frequently do you buy products online") label:contains("Once a month") input', }, { content: 'Answer How many times did you order products on our website', trigger: 'div.js_question-wrapper:contains("How many times did you order products on our website") input', diff --git a/addons/survey/static/tests/tours/survey_prefill.js b/addons/survey/static/tests/tours/survey_prefill.js index d93f3d96210..89242a0a5d0 100644 --- a/addons/survey/static/tests/tours/survey_prefill.js +++ b/addons/survey/static/tests/tours/survey_prefill.js @@ -8,7 +8,7 @@ tour.register('test_survey_prefill', { url: '/survey/start/b137640d-14d4-4748-9ef6-344caaaaaae' }, [{ // Page-1 - trigger: 'a.btn.btn-primary.btn-lg:contains("Start Survey")', + trigger: 'button.btn.btn-primary.btn-lg:contains("Start Survey")', }, { // Question: Where do you live ? trigger: 'div.js_question-wrapper:contains("Where do you live ?") input', run: 'text Grand-Rosiere', @@ -16,11 +16,7 @@ tour.register('test_survey_prefill', { trigger: 'div.js_question-wrapper:contains("When is your date of birth ?") input', run: 'text 05/05/1980', }, { // Question: How frequently do you buy products online ? - trigger: 'div.js_question-wrapper:contains("How frequently do you buy products online ?") select', - run: function() { - var optionID = $('option:contains("Once a week")').val(); - $('div.js_question-wrapper:contains("How frequently do you buy products online ?") select').val(optionID); - }, + trigger: 'div.js_question-wrapper:contains("How frequently do you buy products online ?") label:contains("Once a week") input', }, { // Question: How many times did you order products on our website ? trigger: 'div.js_question-wrapper:contains("How many times did you order products on our website ?") input', run: 'text 42', @@ -80,8 +76,8 @@ tour.register('test_survey_prefill', { return; } - var $selectQ3 = $('div.js_question-wrapper:contains("How frequently do you buy products online ?") select'); - if ($selectQ3.val() !== $('option:contains("Once a week")').val()) { + var $inputQ3 = $('div.js_question-wrapper:contains("How frequently do you buy products online ?") label:contains("Once a week") input'); + if (!$inputQ3.is(':checked')) { return; } diff --git a/addons/survey/tests/common.py b/addons/survey/tests/common.py index 83680b5c37a..640556491f0 100644 --- a/addons/survey/tests/common.py +++ b/addons/survey/tests/common.py @@ -214,7 +214,7 @@ class TestSurveyCommon(common.SavepointCase): return self.url_open('/survey/start/%s' % survey.access_token) def _access_page(self, survey, token): - return self.url_open('/survey/fill/%s/%s' % (survey.access_token, token)) + return self.url_open('/survey/%s/%s' % (survey.access_token, token)) def _access_submit(self, survey, token, post_data): base_url = self.env['ir.config_parameter'].sudo().get_param('web.base.url') diff --git a/addons/survey/tests/test_survey_flow.py b/addons/survey/tests/test_survey_flow.py index ef51ba2c717..534b3d29a59 100644 --- a/addons/survey/tests/test_survey_flow.py +++ b/addons/survey/tests/test_survey_flow.py @@ -96,7 +96,7 @@ class TestSurveyFlow(common.TestSurveyCommon, HttpCase): answers.invalidate_cache() # TDE note: necessary as lots of sudo in controllers messing with cache # -> this should have generated answer lines - self.assertAnswer(answers, 'skip', page_0) + self.assertAnswer(answers, 'in_progress', page_0) self.assertAnswerLines(page_0, answers, answer_data) # Customer is redirected on second page and begins filling it diff --git a/addons/survey/tests/test_survey_ui_certification.py b/addons/survey/tests/test_survey_ui_certification.py index 8beca6113e2..da8d3b74658 100644 --- a/addons/survey/tests/test_survey_ui_certification.py +++ b/addons/survey/tests/test_survey_ui_certification.py @@ -37,7 +37,6 @@ class TestUiCertification(HttpCaseWithUserDemo): 'title': 'Do we sell Acoustic Bloc Screens?', 'sequence': 2, 'question_type': 'simple_choice', - 'display_mode': 'dropdown', 'constr_mandatory': True, 'suggested_answer_ids': [ (0, 0, { @@ -116,7 +115,6 @@ class TestUiCertification(HttpCaseWithUserDemo): 'title': 'How many versions of the Corner Desk do we have?', 'sequence': 5, 'question_type': 'simple_choice', - 'display_mode': 'dropdown', 'constr_mandatory': True, 'suggested_answer_ids': [ (0, 0, { @@ -149,7 +147,6 @@ class TestUiCertification(HttpCaseWithUserDemo): 'title': 'How much do we sell our Cable Management Box?', 'sequence': 8, 'question_type': 'simple_choice', - 'display_mode': 'dropdown', 'constr_mandatory': True, 'suggested_answer_ids': [ (0, 0, { @@ -213,7 +210,6 @@ class TestUiCertification(HttpCaseWithUserDemo): 'title': 'What do you think about our prices (not rated)?', 'sequence': 10, 'question_type': 'simple_choice', - 'display_mode': 'dropdown', 'constr_mandatory': True, 'suggested_answer_ids': [ (0, 0, { diff --git a/addons/survey/tests/test_survey_ui_feedback.py b/addons/survey/tests/test_survey_ui_feedback.py index 60ab7c5b297..1778881a409 100644 --- a/addons/survey/tests/test_survey_ui_feedback.py +++ b/addons/survey/tests/test_survey_ui_feedback.py @@ -40,7 +40,6 @@ class TestUiFeedback(HttpCaseWithUserDemo): 'title': 'How frequently do you buy products online ?', 'sequence': 4, 'question_type': 'simple_choice', - 'display_mode': 'dropdown', 'comments_allowed': True, 'comment_count_as_answer': True, 'constr_mandatory': True, diff --git a/addons/survey/views/assets.xml b/addons/survey/views/assets.xml index 3135c064bb0..07f2d902d5b 100644 --- a/addons/survey/views/assets.xml +++ b/addons/survey/views/assets.xml @@ -8,6 +8,7 @@