From aedeccbeeaafe87ec0f80228a05b45dbc7140f72 Mon Sep 17 00:00:00 2001 From: David Beguin Date: Tue, 10 Dec 2019 12:30:21 +0000 Subject: [IMP] survey : redesign frontend layout In this commit, all the questions type have been redesigned to look more fancy. (impacted question types : radio and checkboxes, text boxes (textarea and text inputs, date and datetime inputs and matrix) For radio and checkboxes, both design have been aligned to work the same way (except checkboxes can still have more than one selected option). Selection by key have been applied on those two question types if the number of option is under 26 (to use all the alphabeat character for selection). This selection by key is only available on page per question layout. Tests have been adapted accordingly to the redesign (typically for choice and matrix inputs) * Progress Bar This commit adds a progress bar to the survey to inform the user where he is in the survey filling process. There are two progress modes: - Number : will display the number of the current page on the total number of pages (or questions if layout mode is question_per_page) - Percentage : will display the percentage of page or question already done So this leads to, on last page: - in Number mode : the progress bar div is 100% filled 3 / 3 pages [===] - in percentage mode : the progress bar is aligned to percentage 66% completed [== ] * Print Widget A widget is added for survey print mode in order to resize all textarea to fit their content, instead of showing a scroll bar. This can be usefull if user wants to print the results. He will get the entire content of the 'textarea answers' instead of only the two first lines. * Misc This commit also : - adds background image to survey. - redo quizz correction and add some data to illustrate non scored questions rendering in print template - review breadcrumb style Note : readonly data option on survey form widget is not set anywhere yet but the usage is done in prevention of the future work on presenter view for survey session mode. Task ID: '2150291' PR #43237 --- addons/survey/controllers/main.py | 53 ++-- addons/survey/data/survey_demo_certification.xml | 1 + addons/survey/data/survey_demo_quiz.xml | 42 +++ addons/survey/models/survey_survey.py | 5 + addons/survey/static/src/img/survey_background.jpg | Bin 0 -> 166837 bytes .../survey/static/src/img/survey_background_2.jpg | Bin 0 -> 86252 bytes addons/survey/static/src/js/survey_form.js | 115 +++++++- addons/survey/static/src/js/survey_print.js | 31 +++ addons/survey/static/src/scss/survey_form.scss | 193 +++++++++++++ .../survey/static/src/scss/survey_templates.scss | 28 -- .../static/src/xml/survey_breadcrumb_templates.xml | 10 +- .../static/tests/tours/certification_failure.js | 24 +- .../static/tests/tours/certification_success.js | 24 +- addons/survey/static/tests/tours/survey.js | 16 +- addons/survey/static/tests/tours/survey_prefill.js | 16 +- addons/survey/views/assets.xml | 4 +- addons/survey/views/survey_survey_views.xml | 2 + addons/survey/views/survey_templates.xml | 305 +++++++++++++++------ .../survey/views/survey_templates_management.xml | 2 +- addons/survey/views/survey_templates_print.xml | 6 +- .../tests/tours/slides_certification_member.js | 18 +- 21 files changed, 700 insertions(+), 195 deletions(-) create mode 100644 addons/survey/static/src/img/survey_background.jpg create mode 100644 addons/survey/static/src/img/survey_background_2.jpg create mode 100644 addons/survey/static/src/js/survey_print.js create mode 100644 addons/survey/static/src/scss/survey_form.scss delete mode 100644 addons/survey/static/src/scss/survey_templates.scss diff --git a/addons/survey/controllers/main.py b/addons/survey/controllers/main.py index 94ced8b83f8..7b9491d3ed2 100644 --- a/addons/survey/controllers/main.py +++ b/addons/survey/controllers/main.py @@ -15,6 +15,8 @@ from odoo.http import request, content_disposition from odoo.osv import expression from odoo.tools import format_datetime, format_date +from odoo.addons.web.controllers.main import Binary + _logger = logging.getLogger(__name__) @@ -283,6 +285,24 @@ class Survey(http.Controller): return request.render('survey.survey_page_fill', self._prepare_survey_data(access_data['survey_sudo'], access_data['answer_sudo'], **post)) + @http.route('/survey/get_background_image//', type='http', auth="public", website=True, sitemap=False) + def survey_get_background(self, survey_token, answer_token): + access_data = self._get_access_data(survey_token, answer_token, ensure_token=True) + if access_data['validity_code'] is not True: + return werkzeug.exceptions.Forbidden() + + survey_sudo, answer_sudo = access_data['survey_sudo'], access_data['answer_sudo'] + + status, headers, image_base64 = request.env['ir.http'].sudo().binary_content( + model='survey.survey', id=survey_sudo.id, field='background_image', + default_mimetype='image/png') + + return Binary._content_image_get_response(status, headers, image_base64) + + # ---------------------------------------------------------------- + # JSON ROUTES to begin / continue survey (ajax navigation) + Tools + # ---------------------------------------------------------------- + @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. """ @@ -427,6 +447,7 @@ class Survey(http.Controller): 'review': review, 'survey': survey_sudo, 'answer': answer_sudo, + 'scoring_display_correction': survey_sudo.scoring_type == 'scoring_with_answers' and answer_sudo, 'format_datetime': lambda dt: format_datetime(request.env, dt, dt_format=False), 'format_date': lambda date: format_date(request.env, date) }) @@ -464,23 +485,8 @@ class Survey(http.Controller): return self._generate_report(succeeded_attempt, download=True) - def _generate_report(self, user_input, download=True): - report = request.env.ref('survey.certification_report').sudo().render_qweb_pdf([user_input.id], data={'report_type': 'pdf'})[0] - - report_content_disposition = content_disposition('Certification.pdf') - if not download: - content_split = report_content_disposition.split(';') - content_split[0] = 'inline' - report_content_disposition = ';'.join(content_split) - - return request.make_response(report, headers=[ - ('Content-Type', 'application/pdf'), - ('Content-Length', len(report)), - ('Content-Disposition', report_content_disposition), - ]) - # ------------------------------------------------------------ - # REPORTING SURVEY ROUTES + # REPORTING SURVEY ROUTES AND TOOLS # ------------------------------------------------------------ @http.route('/survey/results/', type='http', auth='user', website=True) @@ -509,6 +515,21 @@ class Survey(http.Controller): 'search_finished': post.get('finished') == 'true', }) + def _generate_report(self, user_input, download=True): + report = request.env.ref('survey.certification_report').sudo().render_qweb_pdf([user_input.id], data={'report_type': 'pdf'})[0] + + report_content_disposition = content_disposition('Certification.pdf') + if not download: + content_split = report_content_disposition.split(';') + content_split[0] = 'inline' + report_content_disposition = ';'.join(content_split) + + return request.make_response(report, headers=[ + ('Content-Type', 'application/pdf'), + ('Content-Length', len(report)), + ('Content-Disposition', report_content_disposition), + ]) + def _extract_filters_data(self, survey, post): search_filters = [] line_filter_domain, line_choices = [], [] diff --git a/addons/survey/data/survey_demo_certification.xml b/addons/survey/data/survey_demo_certification.xml index 5ea4351605a..e2f438b3c92 100644 --- a/addons/survey/data/survey_demo_certification.xml +++ b/addons/survey/data/survey_demo_certification.xml @@ -26,6 +26,7 @@

Test your vendor skills!

True + diff --git a/addons/survey/data/survey_demo_quiz.xml b/addons/survey/data/survey_demo_quiz.xml index c4202ef081a..7a6f5347d7a 100644 --- a/addons/survey/data/survey_demo_quiz.xml +++ b/addons/survey/data/survey_demo_quiz.xml @@ -12,6 +12,7 @@ page_per_section

This small quiz will test your knowledge about our Company. Be prepared !

+
@@ -279,4 +280,45 @@ 4 Winter + + + + Your feeling + + 26 + + + +

We may be interested by your input.

+
+ + + 27 + What do you think about this survey ? + If you don't like us, please try to be as objective as possible. + simple_choice + + + + + + + 1 + Good + + + + 2 + Not Good, Not Bad + + + + 3 + Iznogoud + + + + 4 + I have no idea, I'm a dog! + diff --git a/addons/survey/models/survey_survey.py b/addons/survey/models/survey_survey.py index f12a34ad5fe..6c6d36168a8 100644 --- a/addons/survey/models/survey_survey.py +++ b/addons/survey/models/survey_survey.py @@ -30,6 +30,7 @@ class Survey(models.Model): description_done = fields.Html( "End Message", translate=True, help="This message will be displayed when survey is completed") + background_image = fields.Binary("Background Image") active = fields.Boolean("Active", default=True) state = fields.Selection(selection=[ ('draft', 'Draft'), ('open', 'In Progress'), ('closed', 'Closed') @@ -49,6 +50,10 @@ class Survey(models.Model): ('random', 'Randomized per section')], string="Selection", required=True, default='all', help="If randomized is selected, add the number of random questions next to the section.") + progression_mode = fields.Selection([ + ('percent', 'Percentage'), + ('number', 'Number')], string='Progression Mode', default='percent', + help="If Number is selected, it will display the number of questions answered on the total number of question to answer.") # attendees user_input_ids = fields.One2many('survey.user_input', 'survey_id', string='User responses', readonly=True, groups='survey.group_survey_user') # security / access diff --git a/addons/survey/static/src/img/survey_background.jpg b/addons/survey/static/src/img/survey_background.jpg new file mode 100644 index 00000000000..ba7c4f0263a Binary files /dev/null and b/addons/survey/static/src/img/survey_background.jpg differ diff --git a/addons/survey/static/src/img/survey_background_2.jpg b/addons/survey/static/src/img/survey_background_2.jpg new file mode 100644 index 00000000000..b2e076ed2ba Binary files /dev/null and b/addons/survey/static/src/img/survey_background_2.jpg differ diff --git a/addons/survey/static/src/js/survey_form.js b/addons/survey/static/src/js/survey_form.js index 02f9e567365..eb8ac3a95be 100644 --- a/addons/survey/static/src/js/survey_form.js +++ b/addons/survey/static/src/js/survey_form.js @@ -6,11 +6,13 @@ var publicWidget = require('web.public.widget'); var time = require('web.time'); var core = require('web.core'); var _t = core._t; +var dom = require('web.dom'); publicWidget.registry.SurveyFormWidget = publicWidget.Widget.extend({ selector: '.o_survey_form', events: { 'change .o_survey_form_choice_item': '_onChangeChoiceItem', + 'click .o_survey_matrix_btn': '_onMatrixBtnClick', 'click button[type="submit"]': '_onSubmit', }, custom_events: { @@ -29,6 +31,7 @@ publicWidget.registry.SurveyFormWidget = publicWidget.Widget.extend({ this.fadeInOutDelay = 400; return this._super.apply(this, arguments).then(function () { self.options = self.$target.find('form').data(); + // Init fields if (!self.options.isStartScreen) { self._initTimer(); self._initBreadcrumb(); @@ -36,6 +39,13 @@ publicWidget.registry.SurveyFormWidget = publicWidget.Widget.extend({ self.$('div.o_survey_form_date').each(function () { self._initDateTimePicker($(this)); }); + self._initChoiceItems(); + self._initTextArea(); + self._focusOnFirstInput(); + // Init event listener + if (!self.options.readonly) { + $(document).on('keypress', self._onKeyPress.bind(self)); + } }); }, @@ -46,6 +56,35 @@ publicWidget.registry.SurveyFormWidget = publicWidget.Widget.extend({ // Handlers // ------------------------------------------------------------------------- + _onKeyPress: function (event) { + // If user is answering a textarea, do not handle keyPress + if (this.$("textarea").is(":focus")) { + return; + } + + var self = this; + var keyCode = event.keyCode; + var letter = String.fromCharCode(keyCode).toUpperCase(); + + // Handle Start / Next / Submit + if (keyCode === 13) { // Enter : go Next + event.preventDefault(); + this._submitForm({}); + } else if (self.options.questionsLayout === 'page_per_question' + && letter.match(/[a-z]/i)) { + var $choiceInput = this.$(`input[data-selection-key=${letter}]`); + if ($choiceInput.length === 1) { + if ($choiceInput.attr('type') === 'radio') { + $choiceInput.prop("checked", true).trigger('change'); + } else { + $choiceInput.prop("checked", !$choiceInput.prop("checked")).trigger('change'); + } + // Avoid selection key to be typed into the textbox if 'other' is selected by key + event.preventDefault(); + } + } + }, + /** * Checks, if the 'other' choice is checked. Applies only if the comment count as answer. * If not checked : Clear the comment textarea and disable it @@ -55,7 +94,8 @@ publicWidget.registry.SurveyFormWidget = publicWidget.Widget.extend({ * @param {Event} event */ _onChangeChoiceItem: function (event) { - var $choiceItemGroup = $(event.currentTarget).parents('.o_survey_form_choice'); + var $target = $(event.currentTarget); + var $choiceItemGroup = $target.closest('.o_survey_form_choice'); var $otherItem = $choiceItemGroup.find('.o_survey_js_form_other_comment'); var $commentInput = $choiceItemGroup.find('textarea[type="text"]'); @@ -68,6 +108,36 @@ publicWidget.registry.SurveyFormWidget = publicWidget.Widget.extend({ $commentInput.val(''); $commentInput.enable(false); } + + var $matrixBtn = $target.closest('.o_survey_matrix_btn'); + if ($target.attr('type') === 'radio') { + if ($matrixBtn.length > 0) { + $matrixBtn.closest('tr').find('td').removeClass('o_survey_selected'); + $matrixBtn.addClass('o_survey_selected'); + } else { + $choiceItemGroup.find('label').removeClass('o_survey_selected'); + $target.closest('label').addClass('o_survey_selected'); + } + } else { // $target.attr('type') === 'checkbox' + if ($matrixBtn.length > 0) { + $matrixBtn.toggleClass('o_survey_selected', !$matrixBtn.hasClass('o_survey_selected')); + } else { + var $label = $target.closest('label'); + $label.toggleClass('o_survey_selected', !$label.hasClass('o_survey_selected')); + } + } + }, + + _onMatrixBtnClick: function (event) { + if (!this.options.readonly) { + var $target = $(event.currentTarget); + var $input = $target.find('input'); + if ($input.attr('type') === 'radio') { + $input.prop("checked", true).trigger('change'); + } else { + $input.prop("checked", !$input.prop("checked")).trigger('change'); + } + } }, _onSubmit: function (event) { @@ -177,8 +247,11 @@ publicWidget.registry.SurveyFormWidget = publicWidget.Widget.extend({ } else { this._updateBreadcrumb(); } + self._initChoiceItems(); + self._initTextArea(); this.$('.o_survey_form_content').fadeIn(this.fadeInOutDelay); $("html, body").animate({ scrollTop: 0 }, this.fadeInOutDelay); + self._focusOnFirstInput(); } else if (result && result.fields && result.error === 'validation') { this.$('.o_survey_form_content').fadeIn(0); @@ -321,7 +394,7 @@ publicWidget.registry.SurveyFormWidget = publicWidget.Widget.extend({ */ _prepareSubmitValues: function (formData, params) { var self = this; - formData.forEach(function (value, key){ + formData.forEach(function (value, key) { switch (key) { case 'csrf_token': case 'token': @@ -463,6 +536,25 @@ publicWidget.registry.SurveyFormWidget = publicWidget.Widget.extend({ // INIT FIELDS TOOLS // ------------------------------------------------------------------------- + /** + * Will allow the textarea to resize on carriage return instead of showing scrollbar. + */ + _initTextArea: function () { + this.$('textarea').each(function () { + dom.autoresize($(this)); + }); + }, + + _initChoiceItems: function () { + this.$("input[type='radio'],input[type='checkbox']").each(function () { + var matrixBtn = $(this).parents('.o_survey_matrix_btn'); + if ($(this).prop("checked")) { + var $target = matrixBtn.length > 0 ? matrixBtn : $(this).closest('label'); + $target.addClass('o_survey_selected'); + } + }); + }, + /** * Will initialize the breadcrumb widget that handles navigation to a previously filled in page. * @@ -573,6 +665,17 @@ publicWidget.registry.SurveyFormWidget = publicWidget.Widget.extend({ } }, + /** + * Will automatically focus on the first input to allow the user to complete directly the survey, + * without having to manually get the focus (only if the input has the right type - can write something inside -) + */ + _focusOnFirstInput: function () { + var $inputs = this.$("input[type='text'],input[type='number'],textarea").not('.o_survey_comment'); + if ($inputs.length > 0) { + $inputs.first().focus(); + } + }, + // ERRORS TOOLS // ------------------------------------------------------------------------- @@ -591,10 +694,12 @@ publicWidget.registry.SurveyFormWidget = publicWidget.Widget.extend({ var scrollLocation = $target.offset().top; var navbarHeight = $('.o_main_navbar').height(); if (navbarHeight) { - scrollLocation -= navbarHeight; + // In overflow auto, scrollLocation of target can be negative if target is out of screen (up side) + scrollLocation = scrollLocation >= 0 ? scrollLocation - navbarHeight : scrollLocation + navbarHeight; } - $('html, body').animate({ - scrollTop: scrollLocation + var scrollinside = $("#wrapwrap").scrollTop(); + $('#wrapwrap').animate({ + scrollTop: scrollinside + scrollLocation }, 500); }, diff --git a/addons/survey/static/src/js/survey_print.js b/addons/survey/static/src/js/survey_print.js new file mode 100644 index 00000000000..5e4b8118aec --- /dev/null +++ b/addons/survey/static/src/js/survey_print.js @@ -0,0 +1,31 @@ +odoo.define('survey.print', function (require) { +'use strict'; + +var publicWidget = require('web.public.widget'); +var dom = require('web.dom'); + +publicWidget.registry.SurveyPrintWidget = publicWidget.Widget.extend({ + selector: '.o_survey_print', + + //-------------------------------------------------------------------------- + // Widget + //-------------------------------------------------------------------------- + + /** + * @override + */ + start: function () { + var self = this; + return this._super.apply(this, arguments).then(function () { + // Will allow the textarea to resize if any carriage return instead of showing scrollbar. + self.$('textarea').each(function () { + dom.autoresize($(this)); + }); + }); + }, + +}); + +return publicWidget.registry.SurveyPrintWidget; + +}); diff --git a/addons/survey/static/src/scss/survey_form.scss b/addons/survey/static/src/scss/survey_form.scss new file mode 100644 index 00000000000..7c54bed6738 --- /dev/null +++ b/addons/survey/static/src/scss/survey_form.scss @@ -0,0 +1,193 @@ +.o_survey_wrap { + min-height: 100%; +} + +/********************************************************** + Common Style + **********************************************************/ + +.o_survey_form, .o_survey_print { + .o_survey_question_error { + height: 0px; + transition: height .5s ease; + line-height: 4rem; + &.slide_in { + height: 4rem; + } + } + + .o_survey_question_text_box, + .o_survey_question_date, + .o_survey_question_datetime, + .o_survey_question_numerical_box { + border: 0px; + border-bottom: 2px solid $o-brand-primary; + &:disabled { + color: black !important; + border-color: $gray-600; + border-bottom: 2px solid $gray-600; + } + &:focus { + border-color: $o-brand-primary; + } + } + + .o_survey_form_date .input-group-append { + right: 0; + .input-group-text i { + font-size: large; + } + } + + .o_survey_choice_btn { + transition: background-color 0.3s ease; + flex: 1 0 300px; + + span { + line-height: 25px; + } + i { + top: 0px; + font-size: large; + &.fa-check-circle,&.fa-check-square { + display: none; + } + } + + &.o_survey_selected i { + display: none; + &.fa-check-circle,&.fa-check-square { + display: inline; + } + } + } + + .o_survey_question_matrix { + td { + min-width: 100px; + i { + font-size: 22px; + display: none; + &.o_survey_matrix_empty_checkbox { + display: inline; + } + } + .o_survey_choice_key > span > span { + top: 0px; + } + + &.o_survey_selected { + i { + display: inline; + &.o_survey_matrix_empty_checkbox { + display: none; + } + } + } + } + } +} + +/********************************************************** + Form Specific Style + **********************************************************/ + +.o_survey_form { + min-height: 100%; + + .o_survey_choice_btn { + background-color: rgba($o-brand-primary, 0.5); + border: 2px solid $o-brand-primary; + cursor: pointer; + + &.o_survey_selected { + background-color: $o-brand-primary; + } + + &:hover { + background-color: rgba($o-brand-primary, 0.8); + .o_survey_choice_key span.o_survey_key { + opacity: 1; + } + } + } + + .o_survey_choice_key { + width: 25px; + height: 25px; + border: 2px solid $o-brand-primary; + span { + font-size: smaller; + top: -2px; + &.o_survey_key { + width: inherit; + right: 10px; + border: 2px solid $o-brand-primary; + border-right: 0px; + height: 25px; + opacity: 0; + transition: opacity 0.4s ease; + white-space: nowrap; + } + } + } + + .o_survey_question_matrix { + th { + background-color: $o-brand-primary; + } + td { + background-color: rgba($o-brand-primary, 0.2); + + &:hover { + background-color: rgba($o-brand-primary, 0.5); + cursor: pointer; + .o_survey_choice_key span.o_survey_key { + opacity: 1; + } + } + } + } + + .o_survey_progress { + height:0.5em; + } +} + +/********************************************************** + Print Specific Style + **********************************************************/ + +.o_survey_print { + .o_survey_choice_btn { + background-color: $gray-500; + border-color: transparent; + cursor: default; + + &.bg-success, &.bg-danger { + opacity: 0.6; + } + &.o_survey_selected { + background-color: $gray-600; + opacity: 1; + } + i.fa-square, i.fa-circle { + display: none; + } + } + .o_survey_question_matrix { + th { + /* important needed to force override bg-primary set on th in the template */ + background-color: $gray-600 !important; + } + td { + background-color: $gray-200; + &:hover { + cursor: default; + } + } + i.fa-check-square, i.fa-check-circle, i.o_survey_matrix_empty_checkbox { + color: $gray-600; + } + } +} diff --git a/addons/survey/static/src/scss/survey_templates.scss b/addons/survey/static/src/scss/survey_templates.scss deleted file mode 100644 index 4a53c4ac0af..00000000000 --- a/addons/survey/static/src/scss/survey_templates.scss +++ /dev/null @@ -1,28 +0,0 @@ -.o_survey_header { - padding-top: 15px; - padding-bottom: 25px; -} - -.o_survey_title { - padding-bottom: 15px; -} - -.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 index 8893d711815..1df7f54a88a 100644 --- a/addons/survey/static/src/xml/survey_breadcrumb_templates.xml +++ b/addons/survey/static/src/xml/survey_breadcrumb_templates.xml @@ -2,10 +2,11 @@ -