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 @@ -