[MERGE][IMP] survey: redesign frontend views

PURPOSE
=========

In 3 words, make survey beautiful.

SPECIFICATIONS
==============

- Center the question in Question per page mode
- When last question or page of the survey, Continue becomes Finish
- Add Press key capabilities to speed up the survey completion
    - ENTER : go to next question (or next page)
    - A to Z : to select answers in choice and matrix question type
      (only in question per page mode to avoid mismatch and complexity with multiple questions
- New layout for single / multiple choice
    - Custom radio buttons and checkboxes. Unified style for both except we keep circle icon for
      simple choice and square icon for multiple choice.
- Add a progress bar (2 modes : % and number)
- Add Background image
- Re-do quiz correction mode that were removed from controller during survey refactoring.
- Review breadcrumb style (make it more integrated to the new design)

See sub commits for more details.

LINKS
=====

Follow the survey refactoring done in #42908.
Task ID: '2150291'

closes odoo/odoo#43237

Signed-off-by: Thibault Delavallee (tde) <tde@openerp.com>
This commit is contained in:
Odoo's Mergebot
2020-01-17 17:24:12 +01:00
committed by GitHub
26 changed files with 728 additions and 214 deletions
-15
View File
@@ -24,21 +24,6 @@ $o-portal-bg-color: desaturate($gray-200, 100%);
$o-portal-use-default-colors: $body-bg == $o-portal-default-body-bg;
// Frontend general
#wrapwrap {
position: relative;
display: flex;
flex-flow: column nowrap;
width: 100%;
min-height: 100%;
> * {
flex: 0 0 auto;
}
> main {
flex: 1 0 auto;
}
}
header {
.navbar-brand {
flex: 0 0 auto;
+1 -1
View File
@@ -27,7 +27,7 @@ class CustomerPortal(CustomerPortal):
#
def resize_to_48(b64source):
if not b64source:
b64source = base64.b64encode(Binary().placeholder())
b64source = base64.b64encode(Binary.placeholder())
return image_process(b64source, size=(48, 48))
values = {
+37 -16
View File
@@ -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/<string:survey_token>/<string:answer_token>', 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/<string:survey_token>/<string:answer_token>', 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/<model("survey.survey"):survey>', 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 = [], []
@@ -26,6 +26,7 @@
<field name="description" type="html"><p>Test your vendor skills!</p></field>
<field name="certification_give_badge">True</field>
<field name="certification_badge_id" ref="vendor_certification_badge"/>
<field name="background_image" type="base64" file="survey/static/src/img/survey_background_2.jpg"/>
</record>
<!-- Page 1 -->
<record model="survey.question" id="vendor_certification_page_1">
+42
View File
@@ -12,6 +12,7 @@
<field name="questions_layout">page_per_section</field>
<field name="description" type="html">
<p>This small quiz will test your knowledge about our Company. Be prepared !</p></field>
<field name="background_image" type="base64" file="survey/static/src/img/survey_background.jpg"/>
</record>
<!-- Page 1: general informations -->
@@ -279,4 +280,45 @@
<field name="sequence">4</field>
<field name="value">Winter</field>
</record>
<!-- Page 4: Feedback - non scored question -->
<record id="survey_demo_quiz_p4" model="survey.question">
<field name="title">Your feeling</field>
<field name="survey_id" ref="survey_demo_quiz"/>
<field name="sequence">26</field>
<field name="question_type" eval="False"/>
<field name="is_page" eval="True"/>
<field name="description" type="html">
<p>We may be interested by your input.</p></field>
</record>
<record id="survey_demo_quiz_p4_q1" model="survey.question">
<field name="survey_id" ref="survey_demo_quiz"/>
<field name="sequence">27</field>
<field name="title">What do you think about this survey ?</field>
<field name="description" type="html"><span>If you don't like us, please try to be as objective as possible.</span></field>
<field name="question_type">simple_choice</field>
<field name="comments_allowed" eval="True"/>
<field name="comment_count_as_answer" eval="True"/>
<field name="constr_mandatory" eval="False"/>
</record>
<record id="survey_demo_quiz_p4_q1_sug1" model="survey.question.answer">
<field name="question_id" ref="survey_demo_quiz_p4_q1"/>
<field name="sequence">1</field>
<field name="value">Good</field>
</record>
<record id="survey_demo_quiz_p4_q1_sug2" model="survey.question.answer">
<field name="question_id" ref="survey_demo_quiz_p4_q1"/>
<field name="sequence">2</field>
<field name="value">Not Good, Not Bad</field>
</record>
<record id="survey_demo_quiz_p4_q1_sug3" model="survey.question.answer">
<field name="question_id" ref="survey_demo_quiz_p4_q1"/>
<field name="sequence">3</field>
<field name="value">Iznogoud</field>
</record>
<record id="survey_demo_quiz_p4_q1_sug4" model="survey.question.answer">
<field name="question_id" ref="survey_demo_quiz_p4_q1"/>
<field name="sequence">4</field>
<field name="value">I have no idea, I'm a dog!</field>
</record>
</data></odoo>
+5
View File
@@ -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
Binary file not shown.

After

Width:  |  Height:  |  Size: 163 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 84 KiB

+110 -5
View File
@@ -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);
},
@@ -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;
});
@@ -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;
}
}
}
@@ -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;
}
}
@@ -2,10 +2,11 @@
<templates id="template" xml:space="preserve">
<t t-name="survey.survey_breadcrumb_template">
<ol class="breadcrumb justify-content-end">
<ol class="breadcrumb justify-content-end bg-transparent">
<t t-set="canGoBack" t-value="widget.canGoBack"/>
<t t-foreach="widget.pages" t-as="page">
<li t-att-class="'breadcrumb-item' + (page.id === widget.currentPageId ? ' active' : '')"
<t t-set="isActivePage" t-value="page.id === widget.currentPageId"/>
<li t-att-class="'breadcrumb-item' + (isActivePage ? ' active font-weight-bold' : '')"
t-att-data-page-id="page.id"
t-att-data-page-title="page.title">
<t t-if="widget.currentPageId === page.id">
@@ -14,12 +15,13 @@
<t t-set="canGoBack" t-value="false" />
</t>
<t t-if="canGoBack">
<a href="#">
<a class="text-primary" href="#">
<span t-esc="page.title" />
</a>
</t>
<t t-else="">
<span t-esc="page.title" />
<span t-att-class="(isActivePage ? 'text-black' : 'text-muted')"
t-esc="page.title" />
</t>
</li>
</t>
@@ -24,43 +24,43 @@ var failSteps = [{ // Page-1
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") label:contains("No") input',
trigger: 'div.js_question-wrapper:contains("Do we sell Acoustic Bloc Screens") label:contains("No")',
}, { // 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'
trigger: 'div.js_question-wrapper:contains("Select all the existing products") label:contains("Fanta")'
}, {
content: "Ticking answer 'Drawer'",
trigger: 'div.js_question-wrapper:contains("Select all the existing products") label:contains("Drawer") input'
trigger: 'div.js_question-wrapper:contains("Select all the existing products") label:contains("Drawer")'
}, {
content: "Ticking answer 'Conference chair'",
trigger: 'div.js_question-wrapper:contains("Select all the existing products") label:contains("Conference chair") input'
trigger: 'div.js_question-wrapper:contains("Select all the existing products") label:contains("Conference chair")'
}, { // Question: Select all the available customizations for our Customizable Desk
content: "Ticking answer 'Color'",
trigger: 'div.js_question-wrapper:contains("Select all the available customizations for our Customizable Desk") label:contains("Color") input'
trigger: 'div.js_question-wrapper:contains("Select all the available customizations for our Customizable Desk") label:contains("Color")'
}, {
content: "Ticking answer 'Height'",
trigger: 'div.js_question-wrapper:contains("Select all the available customizations for our Customizable Desk") label:contains("Height") input'
trigger: 'div.js_question-wrapper:contains("Select all the available customizations for our Customizable Desk") label:contains("Height")'
}, { // 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") label:contains("2") input',
trigger: 'div.js_question-wrapper:contains("How many versions of the Corner Desk do we have") label:contains("2")',
}, { // 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") label:contains("80$") input',
trigger: 'div.js_question-wrapper:contains("How much do we sell our Cable Management Box") label:contains("80$")',
}, { // 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'
trigger: 'div.js_question-wrapper:contains("Select all the the products that sell for 100$ or more") label:contains("Corner Desk Right Sit")'
}, {
content: "Ticking answer 'Desk Combination'",
trigger: 'div.js_question-wrapper:contains("Select all the the products that sell for 100$ or more") label:contains("Desk Combination") input'
trigger: 'div.js_question-wrapper:contains("Select all the the products that sell for 100$ or more") label:contains("Desk Combination")'
}, {
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'
trigger: 'div.js_question-wrapper:contains("Select all the the products that sell for 100$ or more") label:contains("Office Chair Black")'
}, { // Question: What do you think about our prices (not rated)?
trigger: 'div.js_question-wrapper:contains("What do you think about our prices") label:contains("Correctly priced") input',
trigger: 'div.js_question-wrapper:contains("What do you think about our prices") label:contains("Correctly priced")',
}, {
content: "Finish Survey",
trigger: 'button[type="submit"]',
@@ -23,44 +23,44 @@ tour.register('test_certification_success', {
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") label:contains("Yes") input',
trigger: 'div.js_question-wrapper:contains("Do we sell Acoustic Bloc Screens") label:contains("Yes")',
}, { // 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'
trigger: 'div.js_question-wrapper:contains("Select all the existing products") label:contains("Chair floor protection")'
}, {
content: "Ticking answer 'Drawer'",
trigger: 'div.js_question-wrapper:contains("Select all the existing products") label:contains("Drawer") input'
trigger: 'div.js_question-wrapper:contains("Select all the existing products") label:contains("Drawer")'
}, {
content: "Ticking answer 'Conference chair'",
trigger: 'div.js_question-wrapper:contains("Select all the existing products") label:contains("Conference chair") input'
trigger: 'div.js_question-wrapper:contains("Select all the existing products") label:contains("Conference chair")'
}, { // Question: Select all the available customizations for our Customizable Desk
content: "Ticking answer 'Color'",
trigger: 'div.js_question-wrapper:contains("Select all the available customizations for our Customizable Desk") label:contains("Color") input'
trigger: 'div.js_question-wrapper:contains("Select all the available customizations for our Customizable Desk") label:contains("Color")'
}, {
content: "Ticking answer 'Legs'",
trigger: 'div.js_question-wrapper:contains("Select all the available customizations for our Customizable Desk") label:contains("Legs") input'
trigger: 'div.js_question-wrapper:contains("Select all the available customizations for our Customizable Desk") label:contains("Legs")'
}, { // 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") label:contains("2") input',
trigger: 'div.js_question-wrapper:contains("How many versions of the Corner Desk do we have") label:contains("2")',
}, { // 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") label:contains("80$") input',
trigger: 'div.js_question-wrapper:contains("How much do we sell our Cable Management Box") label:contains("80$")',
}, { // 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'
trigger: 'div.js_question-wrapper:contains("Select all the the products that sell for 100$ or more") label:contains("Corner Desk Right Sit")'
}, {
content: "Ticking answer 'Desk Combination'",
trigger: 'div.js_question-wrapper:contains("Select all the the products that sell for 100$ or more") label:contains("Desk Combination") input'
trigger: 'div.js_question-wrapper:contains("Select all the the products that sell for 100$ or more") label:contains("Desk Combination")'
}, {
content: "Ticking answer 'Large Desk'",
trigger: 'div.js_question-wrapper:contains("Select all the the products that sell for 100$ or more") label:contains("Large Desk") input'
trigger: 'div.js_question-wrapper:contains("Select all the the products that sell for 100$ or more") label:contains("Large Desk")'
}, { // 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") label:contains("Underpriced") input',
trigger: 'div.js_question-wrapper:contains("What do you think about our prices") label:contains("Underpriced")',
}, {
content: "Finish Survey",
trigger: 'button[type="submit"]',
+8 -8
View File
@@ -21,7 +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") label:contains("Once a month") input',
trigger: 'div.js_question-wrapper:contains("How frequently do you buy products online") label:contains("Once a month")',
}, {
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',
@@ -33,25 +33,25 @@ tour.register('test_survey', {
// Page-2
{
content: 'Answer Which of the following words would you use to describe our products (High Quality)',
trigger: 'div.js_question-wrapper:contains("Which of the following words would you use to describe our products") label:contains("High quality") input',
trigger: 'div.js_question-wrapper:contains("Which of the following words would you use to describe our products") label:contains("High quality")',
}, {
content: 'Answer Which of the following words would you use to describe our products (Good value for money)',
trigger: 'div.js_question-wrapper:contains("Which of the following words would you use to describe our products") label:contains("Good value for money") input',
trigger: 'div.js_question-wrapper:contains("Which of the following words would you use to describe our products") label:contains("Good value for money")',
}, {
content: 'Answer What do your think about our new eCommerce (The new layout and design is fresh and up-to-date)',
trigger: 'div.js_question-wrapper:contains("What do your think about our new eCommerce") tr:contains("The new layout and design is fresh and up-to-date") input:first',
trigger: 'div.js_question-wrapper:contains("What do your think about our new eCommerce") tr:contains("The new layout and design is fresh and up-to-date") td:first',
}, {
content: 'Answer What do your think about our new eCommerce (It is easy to find the product that I want)',
trigger: 'div.js_question-wrapper:contains("What do your think about our new eCommerce") tr:contains("It is easy to find the product that I want") input:eq(2)',
trigger: 'div.js_question-wrapper:contains("What do your think about our new eCommerce") tr:contains("It is easy to find the product that I want") td:eq(2)',
}, {
content: 'Answer What do your think about our new eCommerce (The tool to compare the products is useful to make a choice)',
trigger: 'div.js_question-wrapper:contains("What do your think about our new eCommerce") tr:contains("The tool to compare the products is useful to make a choice") input:eq(3)',
trigger: 'div.js_question-wrapper:contains("What do your think about our new eCommerce") tr:contains("The tool to compare the products is useful to make a choice") td:eq(3)',
}, {
content: 'Answer What do your think about our new eCommerce (The checkout process is clear and secure)',
trigger: 'div.js_question-wrapper:contains("What do your think about our new eCommerce") tr:contains("The checkout process is clear and secure") input:eq(2)',
trigger: 'div.js_question-wrapper:contains("What do your think about our new eCommerce") tr:contains("The checkout process is clear and secure") td:eq(2)',
}, {
content: 'Answer What do your think about our new eCommerce (I have added products to my wishlist)',
trigger: 'div.js_question-wrapper:contains("What do your think about our new eCommerce") tr:contains("I have added products to my wishlist") input:last',
trigger: 'div.js_question-wrapper:contains("What do your think about our new eCommerce") tr:contains("I have added products to my wishlist") td:last',
}, {
content: 'Answer Do you have any other comments, questions, or concerns',
trigger: 'div.js_question-wrapper:contains("Do you have any other comments, questions, or concerns") textarea',
@@ -16,7 +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 ?") label:contains("Once a week") input',
trigger: 'div.js_question-wrapper:contains("How frequently do you buy products online ?") label:contains("Once a week")',
}, { // 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',
@@ -27,25 +27,25 @@ tour.register('test_survey_prefill', {
// Page-2
{ // Question: Which of the following words would you use to describe our products ?
content: 'Answer Which of the following words would you use to describe our products (High Quality)',
trigger: 'div.js_question-wrapper:contains("Which of the following words would you use to describe our products") label:contains("High quality") input',
trigger: 'div.js_question-wrapper:contains("Which of the following words would you use to describe our products") label:contains("High quality")',
}, {
content: 'Answer Which of the following words would you use to describe our products (Good value for money)',
trigger: 'div.js_question-wrapper:contains("Which of the following words would you use to describe our products") label:contains("Good value for money") input',
trigger: 'div.js_question-wrapper:contains("Which of the following words would you use to describe our products") label:contains("Good value for money")',
}, {
content: 'Answer What do your think about our new eCommerce (The new layout and design is fresh and up-to-date)',
trigger: 'div.js_question-wrapper:contains("What do your think about our new eCommerce") tr:contains("The new layout and design is fresh and up-to-date") input:first',
trigger: 'div.js_question-wrapper:contains("What do your think about our new eCommerce") tr:contains("The new layout and design is fresh and up-to-date") td:first',
}, {
content: 'Answer What do your think about our new eCommerce (It is easy to find the product that I want)',
trigger: 'div.js_question-wrapper:contains("What do your think about our new eCommerce") tr:contains("It is easy to find the product that I want") input:eq(2)',
trigger: 'div.js_question-wrapper:contains("What do your think about our new eCommerce") tr:contains("It is easy to find the product that I want") td:eq(2)',
}, {
content: 'Answer What do your think about our new eCommerce (The tool to compare the products is useful to make a choice)',
trigger: 'div.js_question-wrapper:contains("What do your think about our new eCommerce") tr:contains("The tool to compare the products is useful to make a choice") input:eq(3)',
trigger: 'div.js_question-wrapper:contains("What do your think about our new eCommerce") tr:contains("The tool to compare the products is useful to make a choice") td:eq(3)',
}, {
content: 'Answer What do your think about our new eCommerce (The checkout process is clear and secure)',
trigger: 'div.js_question-wrapper:contains("What do your think about our new eCommerce") tr:contains("The checkout process is clear and secure") input:eq(2)',
trigger: 'div.js_question-wrapper:contains("What do your think about our new eCommerce") tr:contains("The checkout process is clear and secure") td:eq(2)',
}, {
content: 'Answer What do your think about our new eCommerce (I have added products to my wishlist)',
trigger: 'div.js_question-wrapper:contains("What do your think about our new eCommerce") tr:contains("I have added products to my wishlist") input:last',
trigger: 'div.js_question-wrapper:contains("What do your think about our new eCommerce") tr:contains("I have added products to my wishlist") td:last',
}, {
content: 'Answer Do you have any other comments, questions, or concerns',
trigger: 'div.js_question-wrapper:contains("Do you have any other comments, questions, or concerns") textarea',
+3 -1
View File
@@ -10,11 +10,13 @@
<script type="text/javascript" src="/survey/static/src/js/survey_timer.js" />
<script type="text/javascript" src="/survey/static/src/js/survey_breadcrumb.js" />
<script type="text/javascript" src="/survey/static/src/js/survey_form.js" />
<script type="text/javascript" src="/survey/static/src/js/survey_print.js" />
<script type="text/javascript" src="/survey/static/src/js/survey_result.js" />
<t t-call="web._assets_helpers"/>
<link href="/survey/static/src/css/survey_print.css" rel="stylesheet" type="text/css"/>
<link href="/survey/static/src/css/survey_result.css" rel="stylesheet" type="text/css"></link>
<link rel="stylesheet" type="text/scss" href="/survey/static/src/scss/survey_templates.scss"/>
<link rel="stylesheet" type="text/scss" href="/survey/static/src/scss/survey_form.scss"/>
</template>
<template id="survey_report_assets_pdf" inherit_id="web.report_assets_pdf">
@@ -42,6 +42,7 @@
</button>
</div>
<widget name="web_ribbon" title="Archived" bg_color="bg-danger" attrs="{'invisible': [('active', '=', True)]}"/>
<field name="background_image" widget="image" class="oe_avatar"/>
<div class="oe_title" style="width: 100%;">
<label for="title" class="oe_edit_only"/>
<h1><field name="title" placeholder="e.g. Satisfaction Survey"/></h1>
@@ -75,6 +76,7 @@
<group name="options">
<group string="Questions" name="questions">
<field name="questions_layout" widget="radio" />
<field name="progression_mode" widget="radio" />
<label for="is_time_limited" string="Time Limit"/>
<div>
<field name="is_time_limited" nolabel="1"/>
+218 -89
View File
@@ -3,6 +3,9 @@
<data>
<!-- Main survey layout -->
<template id="survey.layout" name="Survey Layout" inherit_id="web.frontend_layout" primary="True">
<xpath expr="//div[@id='wrapwrap']" position="attributes">
<attribute name="t-att-style" add="('height: 100%; overflow: auto; background: url(' + '/survey/get_background_image/%s/%s' % (survey.access_token, answer.access_token) + ') no-repeat fixed center; box-shadow: inset 0 0 0 10000px rgba(255,255,255,.7); background-size: cover;') if survey and survey.background_image and answer else 'height: 100%; overflow: auto;'"/>
</xpath>
<xpath expr="//head/t[@t-call-assets][last()]" position="after">
<t t-call-assets="survey.survey_assets" lazy_load="True"/>
</xpath>
@@ -17,8 +20,8 @@
<template id="survey_page_fill" name="Survey: main page (take survey)">
<t t-call="survey.layout">
<div class="wrap">
<div class="container o_survey_form">
<div class="wrap o_survey_wrap mt16 pb16 d-flex">
<div class="container o_survey_form d-flex flex-column">
<t t-if="answer.test_entry" t-call="survey.survey_button_form_view" />
<t t-call="survey.survey_fill_header" />
@@ -29,7 +32,7 @@
</template>
<template id="survey_fill_header" name="Survey: main page header">
<div class="o_page_header o_survey_header">
<div class="o_survey_nav pt16 pb24">
<div class="container m-0 p-0">
<div class="row">
<div class="col-lg-8"><h1 t-esc="survey.title"></h1></div>
@@ -38,7 +41,7 @@
</div>
</div>
</div>
<div t-att-class="'o_survey_breadcrumb_container mt8 bg-200' + (' d-none ' if answer.state != 'in_progress' else '')"
<div t-att-class="'o_survey_breadcrumb_container mt8' + (' d-none ' if answer.state != 'in_progress' else '')"
t-if="survey.questions_layout == 'page_per_section' and answer.state != 'done'"
t-att-data-can-go-back="survey.users_can_go_back"
t-att-data-pages="json.dumps(breadcrumb_pages)" />
@@ -48,19 +51,21 @@
<template id="survey_fill_form" name="Survey: main page content">
<t t-set="survey_form_readonly" t-value="false"/>
<form role="form" method="post" t-att-name="survey.id"
class="d-flex flex-grow-1 align-items-center"
t-att-data-answer-token="answer.access_token"
t-att-data-survey-token="survey.access_token"
t-att-data-timer="answer.start_datetime.isoformat() if survey.is_time_limited
and answer.start_datetime and answer.state != 'done' else False"
t-att-data-time-limit-minutes="survey.time_limit"
t-att-data-is-start-screen="answer.state == 'new'">
t-att-data-is-start-screen="answer.state == 'new'"
t-att-data-questions-layout="survey.questions_layout">
<input type="hidden" name="csrf_token" t-att-value="request.csrf_token()"/>
<input type="hidden" name="token" t-att-value="answer.access_token" />
<div class="o_survey_error alert alert-danger d-none" role="alert">
<p>There was an error during the validation of the survey.</p>
</div>
<div class="o_survey_form_content">
<div class="o_survey_form_content w-100">
<t t-if="answer.state == 'new'" t-call="survey.survey_fill_form_start"/>
<t t-elif="answer.state == 'in_progress'" t-call="survey.survey_fill_form_in_progress" />
<t t-else="" t-call="survey.survey_fill_form_done"/>
@@ -83,7 +88,7 @@
</template>
<template id="survey_fill_form_start" name="Survey: start form content">
<div class="wrap o_survey_start">
<div class="wrap o_survey_start pb256">
<div class='mt32 mb32'>
<div t-field='survey.description' class="oe_no_empty"/>
<div t-if="survey.is_time_limited">
@@ -99,7 +104,7 @@
<t t-else="">
Start Survey
</t>
</button>
</button><span class="font-weight-bold ml-2 text-primary d-none d-md-inline">or press Enter</span>
</div>
</div>
</template>
@@ -107,17 +112,18 @@
<template id="survey_fill_form_in_progress" name="Survey: form with questions">
<t t-if="survey.questions_layout == 'one_page'">
<t t-foreach='survey.question_and_page_ids' t-as='question'>
<h2 t-if="question.is_page" t-field='question.title' class="o_survey_title" />
<h2 t-if="question.is_page" t-field='question.title' class="o_survey_title pb16" />
<t t-if="not question.is_page and question in answer.predefined_question_ids" t-call="survey.question_container"/>
</t>
<div class="text-center mt16 mb256">
<button type="submit" value="finish" class="btn btn-primary">Submit Survey</button>
<button type="submit" value="finish" class="btn btn-primary">
Submit</button><span class="font-weight-bold ml-2 text-primary d-none d-md-inline">or press Enter</span>
</div>
</t>
<t t-if="survey.questions_layout == 'page_per_section'">
<h2 t-field='page.title' class="o_survey_title" />
<h2 t-field='page.title' class="o_survey_title pb16" />
<div t-field='page.description' class="oe_no_empty"/>
<input type="hidden" name="page_id" t-att-value="page.id" />
@@ -125,12 +131,19 @@
<t t-if="question in answer.predefined_question_ids" t-call="survey.question_container"/>
</t>
<div class="text-center mt16 mb256">
<button t-if="survey.users_can_go_back and page != survey.page_ids[0]" type="submit" class="btn btn-secondary"
name="button_submit" value="previous" t-att-data-previous-page-id="previous_page_id">Previous page</button>
<button type="submit" t-att-value="'next' if not last else 'finish'" class="btn btn-primary">
<t t-esc="'Next Page' if not last else 'Submit Survey'"/>
</button>
<div class="row">
<div class="offset-3 col-6 text-center mt16 mb128">
<button t-if="survey.users_can_go_back and page != survey.page_ids[0]" type="submit" class="btn btn-secondary"
name="button_submit" value="previous" t-att-data-previous-page-id="previous_page_id">Back</button>
<button type="submit" t-att-value="'next' if not last else 'finish'" class="btn btn-primary">
Continue</button><span class="font-weight-bold ml-2 text-primary d-none d-md-inline"> or press Enter</span>
</div>
<div class="col-3">
<t t-call="survey.survey_progression">
<t t-set="page_ids" t-value="survey.page_ids.ids"/>
<t t-set="page_number" t-value="page_ids.index(page.id) + (1 if survey.progression_mode == 'number' else 0)"/>
</t>
</div>
</div>
</t>
@@ -138,12 +151,19 @@
<input type="hidden" name="question_id" t-att-value="question.id" />
<t t-call="survey.question_container"/>
<div class="text-center mt16 mb256">
<button t-if="survey.users_can_go_back and question != answer.predefined_question_ids[0]" type="submit" class="btn btn-secondary"
name="button_submit" value="previous" t-att-data-previous-page-id="previous_page_id">Previous page</button>
<button type="submit" t-att-value="'next' if not last else 'finish'" class="btn btn-primary">
<t t-esc="'Next Page' if not last else 'Submit Survey'"/>
</button>
<div class="row">
<div class="offset-3 col-6 text-center mt16 mb128">
<button t-if="survey.users_can_go_back and question != answer.predefined_question_ids[0]" type="submit" class="btn btn-secondary"
name="button_submit" value="previous" t-att-data-previous-page-id="previous_page_id">Back</button>
<button type="submit" t-att-value="'next' if not last else 'finish'" class="btn btn-primary">
Continue</button><span class="font-weight-bold ml-2 text-primary d-none d-md-inline">or press Enter</span>
</div>
<div class="col-3">
<t t-call="survey.survey_progression">
<t t-set="page_ids" t-value="survey.question_ids.ids"/>
<t t-set="page_number" t-value="page_ids.index(question.id)"/>
</t>
</div>
</div>
</t>
</template>
@@ -151,7 +171,7 @@
<!-- Finished (taken and finished) survey page -->
<template id="survey_fill_form_done" name="Survey: finished">
<div class="wrap">
<div class="o_survey_finished mt32 mb32">
<div class="o_survey_finished mt32 mb32 pb256">
<h1>Thank you!</h1>
<div t-field="survey.description_done" class="oe_no_empty" />
<div class="row">
@@ -204,7 +224,10 @@
<!-- Question widgets -->
<template id="question_container" name="Survey: question container">
<t t-set="answer_lines" t-value="answer.user_input_line_ids.filtered(lambda line: line.question_id == question)"/>
<div class="js_question-wrapper" t-att-id="question.id" t-att-data-required="question.constr_mandatory"
<!--Use Key selection if number of choices is < 26 to keep Z for other choice if any-->
<t t-set="letters" t-value="'ABCDEFGHIJKLMNOPQRSTUVWXYZ'"/>
<t t-set="useKeySelection" t-value="len(question.suggested_answer_ids) &lt; len(letters) and survey.questions_layout == 'page_per_question'"/>
<div class="js_question-wrapper pb-4" t-att-id="question.id" t-att-data-required="question.constr_mandatory"
t-att-data-constr-error-msg="question.constr_error_msg"
t-att-data-validation-error-msg="question.validation_error_msg">
<h4>
@@ -220,26 +243,30 @@
<t t-if="question.question_type == 'simple_choice'"><t t-call="survey.question_simple_choice"/></t>
<t t-if="question.question_type == 'multiple_choice'"><t t-call="survey.question_multiple_choice"/></t>
<t t-if="question.question_type == 'matrix'"><t t-call="survey.question_matrix"/></t>
<div class="o_survey_question_error alert alert-danger" role="alert"></div>
<div class="o_survey_question_error overflow-hidden border-0 py-0 px-3 alert alert-danger" role="alert"></div>
</div>
</template>
<template id="question_text_box" name="Question: free text box">
<textarea class="form-control o_survey_question_text_box" rows="3" t-att-name="question.id"
t-att-data-question-type="question.question_type"><t t-if="answer_lines" t-esc="answer_lines[0].value_text_box or None"/></textarea>
<div class="o_survey_comment_container py-0 px-1">
<textarea class="form-control o_survey_question_text_box font-weight-bold bg-transparent text-primary rounded-0" rows="3" t-att-name="question.id"
t-att-data-question-type="question.question_type"><t t-if="answer_lines" t-esc="answer_lines[0].value_text_box or None"/></textarea>
</div>
</template>
<template id="question_char_box" name="Question: text box">
<input t-att-type="'email' if question.validation_email else 'text'"
class="form-control o_survey_question_text_box" t-att-name="question.id"
<div class="o_survey_comment_container py-0 px-1">
<input t-att-type="'email' if question.validation_email else 'text'"
class="form-control o_survey_question_text_box font-weight-bold bg-transparent text-primary rounded-0" t-att-name="question.id"
t-att-value="answer_lines[0].value_char_box if answer_lines else None"
t-att-data-question-type="question.question_type"
t-att-data-validation-length-min="question.validation_length_min if question.validation_required else False"
t-att-data-validation-length-max="question.validation_length_max if question.validation_required else False"/>
</div>
</template>
<template id="question_numerical_box" name="Question: numerical box">
<input type="number" step="any" class="form-control o_survey_question_numerical_box" t-att-name="question.id" t-att-value="answer_lines[0].value_numerical_box if answer_lines else None"
<input type="number" step="any" class="form-control o_survey_question_numerical_box font-weight-bold bg-transparent text-primary rounded-0" t-att-name="question.id" t-att-value="answer_lines[0].value_numerical_box if answer_lines else None"
t-att-data-question-type="question.question_type"
t-att-data-validation-float-min="question.validation_min_float_value if question.validation_required else False"
t-att-data-validation-float-max="question.validation_max_float_value if question.validation_required else False"/>
@@ -249,12 +276,12 @@
<div class="input-group o_survey_form_date" t-attf-id="datetimepicker_#{question.id}" data-target-input="nearest"
t-att-data-mindate="question.validation_min_date"
t-att-data-maxdate="question.validation_max_date">
<input type="text" class="form-control datetimepicker-input o_survey_question_date"
<input type="text" class="form-control datetimepicker-input o_survey_question_date font-weight-bold bg-transparent text-primary rounded-0"
t-attf-data-target="#datetimepicker_#{question.id}" t-att-name="question.id"
t-att-value="format_date(answer_lines[0].value_date) if answer_lines else None"
t-att-data-question-type="question.question_type"/>
<div t-if="not survey_form_readonly" class="input-group-append" t-attf-data-target="#datetimepicker_#{question.id}" data-toggle="datetimepicker">
<div class="input-group-text"><i class="fa fa-calendar"></i></div>
<div t-if="not survey_form_readonly" class="input-group-append position-absolute" t-attf-data-target="#datetimepicker_#{question.id}" data-toggle="datetimepicker">
<div class="input-group-text text-primary border-0 bg-transparent"><i class="fa fa-calendar"></i></div>
</div>
</div>
</template>
@@ -263,12 +290,12 @@
<div class="input-group o_survey_form_date" t-attf-id="datetimepicker_#{question.id}" data-target-input="nearest"
t-att-data-mindate="question.validation_min_datetime"
t-att-data-maxdate="question.validation_max_datetime">
<input type="text" class="form-control datetimepicker-input o_survey_question_datetime"
<input type="text" class="form-control datetimepicker-input o_survey_question_datetime font-weight-bold bg-transparent text-primary rounded-0"
t-attf-data-target="#datetimepicker_#{question.id}" t-att-name="question.id"
t-att-value="format_datetime(answer_lines[0].value_datetime) if answer_lines else None"
t-att-data-question-type="question.question_type"/>
<div t-if="not survey_form_readonly" class="input-group-append" t-attf-data-target="#datetimepicker_#{question.id}" data-toggle="datetimepicker">
<div class="input-group-text"><i class="fa fa-calendar"></i></div>
<div t-if="not survey_form_readonly" class="input-group-append position-absolute" t-attf-data-target="#datetimepicker_#{question.id}" data-toggle="datetimepicker">
<div class="input-group-text text-primary border-0 bg-transparent"><i class="fa fa-calendar"></i></div>
</div>
</div>
</template>
@@ -279,27 +306,58 @@
<div class="row o_survey_form_choice"
t-att-data-name="question.id"
data-question-type="simple_choice_radio">
<div t-foreach='question.suggested_answer_ids' t-as='label' t-attf-class="col-lg-#{question.column_nb}">
<label t-att-class="' bg-success ' if quizz_correction and label.answer_score > 0.0 else None">
<input type="radio" t-att-value='label.id' class="o_survey_form_choice_item"
t-att-name='question.id'
t-att-checked="answer_line and answer_line.suggested_answer_id.id == label.id and 'checked' or None"/>
<span t-field='label.value'/>
</label>
<t t-set="item_idx" t-value="0"/>
<div class="d-flex flex-wrap font-weight-bold col-lg-12">
<t t-set="has_correct_answer" t-value="scoring_display_correction and any(label.is_correct for label in question.suggested_answer_ids)"/>
<t t-foreach='question.suggested_answer_ids' t-as='label'>
<t t-set="item_idx" t-value="label_index"/>
<t t-set="answer_selected" t-value="answer_line and answer_line.suggested_answer_id.id == label.id"/>
<t t-set="is_correct" t-value="label.answer_score > 0.0"/>
<!--Used for print mode with corrections -->
<t t-set="answer_class" t-if="not has_correct_answer" t-value="''" />
<t t-set="answer_class" t-elif="is_correct" t-value="'bg-success'" />
<t t-set="answer_class" t-elif="not is_correct" t-value="'bg-danger'" />
<label t-att-for="str(question.id) + '_' + str(label.id)"
t-att-class="'o_survey_choice_btn m-1 py-1 px-3 text-white rounded %s %s' % (answer_class, 'o_survey_selected' if answer_selected else '')">
<t t-call="survey.survey_selection_key">
<t t-set="selection_key_class" t-value="'o_survey_radio_btn float-left d-flex'"/>
</t>
<span class="ml-2" t-field='label.value'/>
<input t-att-id="str(question.id) + '_' + str(label.id)" type="radio" t-att-value='label.id' class="o_survey_form_choice_item invisible position-absolute"
t-att-name='question.id'
t-att-checked="answer_line and answer_line.suggested_answer_id.id == label.id and 'checked' or None"
t-att-data-selection-key="letters[item_idx] if useKeySelection else ''"/>
<i class="fa fa-check-circle float-right mt-1 position-relative"></i>
<i class="fa fa-circle float-right mt-1 position-relative"></i>
</label>
</t>
</div>
<div t-if='question.comments_allowed and question.comment_count_as_answer' class="js_comments col-lg-12" >
<label>
<input type="radio" class="o_survey_form_choice_item o_survey_js_form_other_comment" value="-1"
t-att-name='question.id'
t-att-checked="comment_line and 'checked' or None"/>
<span t-field="question.comments_message" />
</label>
<textarea type="text" class="form-control"
t-attf-disabled="#{'' if comment_line else 'disabled'}"><t t-esc="comment_line.value_char_box if comment_line else ''"/></textarea>
<div class="d-flex flex-wrap font-weight-bold">
<label t-att-class="'o_survey_choice_btn m-1 py-1 px-3 text-white rounded %s' % ('o_survey_selected' if comment_line else '')">
<t t-set="item_idx" t-value="item_idx + 1"/>
<t t-call="survey.survey_selection_key">
<t t-set="selection_key_class" t-value="'o_survey_radio_btn float-left d-flex'"/>
</t>
<input type="radio" class="o_survey_form_choice_item o_survey_js_form_other_comment invisible position-absolute" value="-1"
t-att-name='question.id'
t-att-checked="comment_line and 'checked' or None"
t-att-data-selection-key="letters[item_idx] if useKeySelection else ''"/>
<span class="ml-2" t-field="question.comments_message" />
<i class="fa fa-check-circle float-right mt-1 position-relative"></i>
<i class="fa fa-circle float-right mt-1 position-relative"></i>
</label>
</div>
<div class="o_survey_comment_container py-0 px-1">
<textarea type="text" class="form-control o_survey_question_text_box font-weight-bold bg-transparent text-primary rounded-0"
t-att-disabled="None if comment_line else 'disabled'"><t t-esc="comment_line.value_char_box if comment_line else ''"/></textarea>
</div>
</div>
<div t-if='question.comments_allowed and not question.comment_count_as_answer' class="col-lg-12">
<span t-field="question.comments_message"/>
<textarea type="text" class="form-control o_survey_comment"><t t-esc="comment_line.value_char_box if comment_line else ''"/></textarea>
<div t-if='question.comments_allowed and not question.comment_count_as_answer' class="col-lg-12 o_survey_comment_container mx-1 py-0 pl-3 pr-4">
<textarea type="text" class="form-control o_survey_comment o_survey_question_text_box font-weight-bold bg-transparent text-primary rounded-0"
t-att-placeholder="question.comments_message"><t t-esc="comment_line.value_char_box if comment_line else ''"/></textarea>
</div>
</div>
</template>
@@ -309,61 +367,132 @@
<div class="row o_survey_form_choice o_survey_question_multiple_choice"
t-att-data-name="question.id"
t-att-data-question-type="question.question_type">
<div t-foreach='question.suggested_answer_ids' t-as='label' t-attf-class="col-lg-#{question.column_nb}">
<t t-set="answer_line" t-value="answer_lines.filtered(lambda line: line.suggested_answer_id == label)"/>
<label t-att-class="' bg-success ' if quizz_correction and label.answer_score > 0.0 else None">
<input type="checkbox" t-att-value='label.id' class="o_survey_form_choice_item"
t-att-name="question.id"
t-att-checked="'checked' if answer_line else None"/>
<span t-field='label.value'/>
</label>
</div>
<t t-set="item_idx" t-value="0"/>
<div class="d-flex flex-wrap font-weight-bold col-lg-12">
<t t-set="has_correct_answer" t-value="scoring_display_correction and any(label.is_correct for label in question.suggested_answer_ids)"/>
<t t-foreach='question.suggested_answer_ids' t-as='label'>
<t t-set="item_idx" t-value="label_index"/>
<t t-set="answer_line" t-value="answer_lines.filtered(lambda line: line.suggested_answer_id == label)"/>
<t t-set="answer_selected" t-value="answer_line and answer_line.suggested_answer_id.id == label.id"/>
<t t-set="is_correct" t-value="label.answer_score > 0.0"/>
<div t-if='question.comments_allowed and question.comment_count_as_answer' class="js_ck_comments col-lg-12" >
<label>
<input type="checkbox" class="o_survey_form_choice_item o_survey_js_form_other_comment" value="-1"
t-att-name="question.id"
t-att-checked="comment_line and 'checked' or None"/>
<span t-field="question.comments_message" />
</label>
<textarea type="text" class="form-control"
t-attf-disabled="#{'' if comment_line else 'disabled'}"><t t-esc="comment_line.value_char_box if comment_line else ''"/></textarea>
<!--Used for print mode with corrections -->
<t t-set="answer_class" t-if="not has_correct_answer" t-value="''" />
<t t-set="answer_class" t-elif="is_correct" t-value="'bg-success'" />
<t t-set="answer_class" t-elif="not is_correct" t-value="'bg-danger'" />
<label t-att-class="'o_survey_choice_btn m-1 py-1 px-3 text-white rounded %s %s' % (answer_class, 'o_survey_selected' if answer_line else '')">
<t t-call="survey.survey_selection_key">
<t t-set="selection_key_class" t-value="'float-left d-flex'"/>
</t>
<input type="checkbox" t-att-value='label.id' class="o_survey_form_choice_item invisible position-absolute"
t-att-name="question.id"
t-att-checked="'checked' if answer_line else None"
t-att-data-selection-key="letters[item_idx] if useKeySelection else ''"/>
<span class="ml-2" t-field='label.value'/>
<i class="fa fa-check-square float-right mt-1 position-relative"></i>
<i class="fa fa-square float-right mt-1 position-relative"></i>
</label>
</t>
</div>
<div t-if='question.comments_allowed and not question.comment_count_as_answer' class="col-lg-12">
<span t-field="question.comments_message"/>
<textarea type="text" class="form-control o_survey_comment"><t t-esc="comment_line.value_char_box if comment_line else ''"/></textarea>
<div t-if='question.comments_allowed and question.comment_count_as_answer' class="js_ck_comments col-lg-12" >
<div class="d-flex flex-wrap font-weight-bold">
<label t-att-class="'o_survey_choice_btn m-1 py-1 px-3 text-white rounded %s' % ('o_survey_selected' if comment_line else '')">
<t t-set="item_idx" t-value="item_idx + 1"/>
<t t-call="survey.survey_selection_key">
<t t-set="selection_key_class" t-value="'float-left d-flex'"/>
</t>
<input type="checkbox" class="o_survey_form_choice_item o_survey_js_form_other_comment invisible position-absolute" value="-1"
t-att-name="question.id"
t-att-checked="comment_line and 'checked' or None"
t-att-data-selection-key="letters[item_idx] if useKeySelection else ''"/>
<span class="ml-2" t-field="question.comments_message" />
<i class="fa fa-check-square float-right mt-1 position-relative"></i>
<i class="fa fa-square float-right mt-1 position-relative"></i>
</label>
</div>
<div class="o_survey_comment_container py-0 px-1">
<textarea type="text" class="form-control o_survey_question_text_box font-weight-bold bg-transparent text-primary rounded-0"
t-att-disabled="None if comment_line else 'disabled'"><t t-esc="comment_line.value_char_box if comment_line else ''"/></textarea>
</div>
</div>
<div t-if='question.comments_allowed and not question.comment_count_as_answer' class="col-lg-12 o_survey_comment_container mx-1 py-0 pl-3 pr-4">
<textarea type="text" class="form-control o_survey_comment o_survey_question_text_box font-weight-bold bg-transparent text-primary rounded-0"
t-att-placeholder="question.comments_message"><t t-esc="comment_line.value_char_box if comment_line else ''"/></textarea>
</div>
</div>
</template>
<template id="question_matrix" name="Question: matrix">
<t t-set="comment_line" t-value="answer_lines.filtered(lambda line: line.value_char_box)"/>
<table class="table table-hover o_survey_question_matrix"
<table class="table table-hover o_survey_question_matrix text-white text-center mb-0"
t-att-data-name="question.id"
t-att-data-question-type="question.question_type"
t-att-data-sub-questions="question.matrix_row_ids.ids">
<thead>
<tr>
<th> </th>
<th t-foreach="question.suggested_answer_ids" t-as="col_label"><span t-field="col_label.value" /></th>
<th class="border-0"> </th>
<th class="border-0" t-foreach="question.suggested_answer_ids" t-as="col_label"><span t-field="col_label.value" /></th>
</tr>
</thead>
<tbody>
<tr t-foreach="question.matrix_row_ids" t-as="row_label">
<th><span t-field="row_label.value" /></th>
<td t-foreach="question.suggested_answer_ids" t-as="col_label">
<t t-set="item_idx" t-value="0"/>
<tr class="bg-white text-white" t-foreach="question.matrix_row_ids" t-as="row_label">
<th class="border-0"><span t-field="row_label.value" /></th>
<t t-foreach="question.suggested_answer_ids" t-as="col_label">
<t t-set="answer" t-value="answer_lines.filtered(lambda line: line.suggested_answer_id == col_label and line.matrix_row_id == row_label)"/>
<input t-att-type="'checkbox' if question.matrix_subtype == 'multiple' else 'radio'"
t-att-name="'%s_%s' % (question.id, row_label.id)" t-att-value='col_label.id'
t-att-checked="'checked' if answer else None"
t-att-data-row-id="row_label.id"/>
</td>
<td t-att-class="'o_survey_matrix_btn text-primary border-0 %s'
% ('o_survey_selected' if answer else '')">
<input t-att-type="'checkbox' if question.matrix_subtype == 'multiple' else 'radio'"
t-att-name="'%s_%s' % (question.id, row_label.id)" t-att-value='col_label.id'
t-att-checked="'checked' if answer else None"
t-att-data-row-id="row_label.id"
t-att-data-selection-key="letters[item_idx] if useKeySelection else ''"
class="o_survey_form_choice_item d-none"/>
<i t-att-class="'o_survey_matrix_empty_checkbox fa fa-%s position-relative'
% ('square-o' if question.matrix_subtype == 'multiple' else 'circle-thin')"></i>
<i t-att-class="'fa fa-check-%s position-relative'
% ('square' if question.matrix_subtype == 'multiple' else 'circle')"></i>
<t t-call="survey.survey_selection_key">
<t t-set="selection_key_class"
t-value="'float-right font-weight-bold %s' % ('o_survey_radio_btn' if question.matrix_subtype != 'multiple' else '')"/>
</t>
<t t-set="item_idx" t-value="item_idx + 1"/>
</td>
</t>
</tr>
</tbody>
</table>
<div t-if='question.comments_allowed'>
<span t-field="question.comments_message"/>
<textarea type="text" class="form-control" t-att-name="'%s_%s' % (question.id, 'comment')"><t t-esc="comment_line.value_char_box if comment_line else ''"/></textarea>
<textarea type="text" class="form-control o_survey_question_text_box o_survey_comment font-weight-bold bg-transparent text-primary rounded-0"
t-att-placeholder="question.comments_message"
t-att-name="'%s_%s' % (question.id, 'comment')"><t t-esc="comment_line.value_char_box if comment_line else ''"/></textarea>
</div>
</template>
<template id="survey_selection_key">
<div t-if="useKeySelection" t-att-class="'o_survey_choice_key bg-white position-relative rounded %s' % selection_key_class">
<span class="o_survey_key text-center position-absolute bg-white rounded-left py-0 pr-4 pl-2"><span class="text-primary text-center text-center w-100 position-relative">Key</span></span>
<span class="text-primary text-center w-100 position-relative" t-esc="letters[item_idx]"/>
</div>
</template>
<template id="survey_progression" name="Survey: Progression">
<div class="float-right">
<t t-if="len(page_ids) > 1">
<t t-set="percentage" t-value="round(100*(page_number/len(page_ids)))"/>
<t t-if="survey.progression_mode == 'percent'">
<span class="o_survey_progress_percent" t-esc="percentage"/> % completed
</t>
<t t-else="">
<span class="o_survey_progress_number" t-esc="page_number"/> of <span t-esc="len(page_ids)"/>
<span t-if="survey.questions_layout == 'page_per_question'">answered</span>
<span t-else="">pages</span>
</t>
<div class="o_survey_progress progress flex-grow-1">
<div class="progress-bar bg-primary" t-att-style="'width: ' + str(percentage) + '%'"/>
</div>
</t>
</div>
</template>
</data>
@@ -74,7 +74,7 @@
<!-- ============================================================ -->
<template id="survey_button_form_view" name="Survey: back to form view">
<div groups="survey.group_survey_manager" t-ignore="true" class="alert alert-info alert-dismissible rounded-0 mt16 fade show d-print-none css_editable_mode_hidden">
<div groups="survey.group_survey_manager" t-ignore="true" class="alert alert-info alert-dismissible rounded-0 fade show d-print-none css_editable_mode_hidden">
<div t-ignore="true" class="text-center">
<a t-attf-href="/web#view_type=form&amp;model=survey.survey&amp;id=#{survey.id}&amp;action=survey.action_survey_form"><i class="fa fa-fw fa-arrow-right"/><span t-if="answer and answer.test_entry">This is a test survey. </span>Edit Survey.</a>
</div>
@@ -6,7 +6,7 @@
<t t-call="survey.layout">
<t t-set="survey_form_readonly" t-value="true"/>
<div class="wrap">
<div class="container">
<div class="o_survey_print container">
<t t-if="answer.test_entry" t-call="survey.survey_button_form_view" />
<div class='jumbotron mt32'>
<h1><span t-field='survey.title'/></h1>
@@ -27,7 +27,7 @@
<h2>
<span t-field='question.title'/>
<span t-if="question.constr_mandatory" class="text-danger">*</span>
<span t-if="quizz_correction" class="badge badge-pill" t-att-data-score-question="question.id"></span>
<span t-if="scoring_display_correction" class="badge badge-pill" t-att-data-score-question="question.id"></span>
</h2>
<t t-if="question.description"><div class="text-muted oe_no_empty" t-field='question.description'/></t>
<t t-if="question.question_type == 'text_box'"><t t-call="survey.question_text_box"/></t>
@@ -38,7 +38,7 @@
<t t-if="question.question_type == 'simple_choice'"><t t-call="survey.question_simple_choice"/></t>
<t t-if="question.question_type == 'multiple_choice'"><t t-call="survey.question_multiple_choice"/></t>
<t t-if="question.question_type == 'matrix'"><t t-call="survey.question_matrix"/></t>
<div class="o_survey_question_error alert alert-danger" role="alert"></div>
<div class="o_survey_question_error overflow-hidden border-0 py-0 px-3 alert alert-danger" role="alert"></div>
</div>
</t>
</t>
@@ -89,16 +89,16 @@ var failCertificationSteps = [{
trigger: 'button:contains("Start Certification")'
}, { // Question: What type of wood is the best for furniture?
content: 'Survey: selecting answer "Fir"',
trigger: 'div.js_question-wrapper:contains("What type of wood is the best for furniture") label:contains("Fir") input',
trigger: 'div.js_question-wrapper:contains("What type of wood is the best for furniture") label:contains("Fir")'
}, { // Question: Select all the furniture shown in the video
content: 'Survey: ticking answer "Table"',
trigger: 'div.js_question-wrapper:contains("Select all the furniture shown in the video") label:contains("Table") input'
trigger: 'div.js_question-wrapper:contains("Select all the furniture shown in the video") label:contains("Table")'
}, {
content: 'Survey: ticking answer "Bed"',
trigger: 'div.js_question-wrapper:contains("Select all the furniture shown in the video") label:contains("Bed") input'
trigger: 'div.js_question-wrapper:contains("Select all the furniture shown in the video") label:contains("Bed")'
}, {
content: 'Survey: submitting the certification with wrong answers',
trigger: 'button:contains("Submit Survey")'
trigger: 'button:contains("Submit")'
}];
var retrySteps = [{
@@ -111,19 +111,19 @@ var succeedCertificationSteps = [{
trigger: 'button:contains("Start Certification")'
}, { // Question: What type of wood is the best for furniture?
content: 'Survey: selecting answer "Oak"',
trigger: 'div.js_question-wrapper:contains("What type of wood is the best for furniture") label:contains("Oak") input',
trigger: 'div.js_question-wrapper:contains("What type of wood is the best for furniture") label:contains("Oak")',
}, { // Question: Select all the furniture shown in the video
content: 'Survey: ticking answer "Chair"',
trigger: 'div.js_question-wrapper:contains("Select all the furniture shown in the video") label:contains("Chair") input'
trigger: 'div.js_question-wrapper:contains("Select all the furniture shown in the video") label:contains("Chair")'
}, {
content: 'Survey: ticking answer "Shelve"',
trigger: 'div.js_question-wrapper:contains("Select all the furniture shown in the video") label:contains("Shelve") input'
trigger: 'div.js_question-wrapper:contains("Select all the furniture shown in the video") label:contains("Shelve")'
}, {
content: 'Survey: ticking answer "Desk"',
trigger: 'div.js_question-wrapper:contains("Select all the furniture shown in the video") label:contains("Desk") input'
trigger: 'div.js_question-wrapper:contains("Select all the furniture shown in the video") label:contains("Desk")'
}, {
content: 'Survey: submitting the certification with correct answers',
trigger: 'button:contains("Submit Survey")'
trigger: 'button:contains("Submit")'
}];
var certificationCompletionSteps = [{
+10 -2
View File
@@ -1365,7 +1365,8 @@ class View(http.Controller):
class Binary(http.Controller):
def placeholder(self, image='placeholder.png'):
@staticmethod
def placeholder(image='placeholder.png'):
with tools.file_open(get_resource_path('web', 'static/src/img', image), 'rb') as fd:
return fd.read()
@@ -1442,10 +1443,17 @@ class Binary(http.Controller):
filename_field=filename_field, download=download, mimetype=mimetype,
default_mimetype='image/png', access_token=access_token)
return Binary._content_image_get_response(status, headers, image_base64, field=field, download=download,
width=width, height=height, crop=crop, quality=quality,
placeholder=placeholder)
@staticmethod
def _content_image_get_response(status, headers, image_base64, field='datas', download=None,
width=0, height=0, crop=False, quality=0, placeholder='placeholder.png'):
if status in [301, 304] or (status != 200 and download):
return request.env['ir.http']._response_by_status(status, headers, image_base64)
if not image_base64:
image_base64 = base64.b64encode(self.placeholder(image=placeholder))
image_base64 = base64.b64encode(Binary.placeholder(image=placeholder))
if not (width or height):
width, height = odoo.tools.image_guess_size_from_field_name(field)
@@ -0,0 +1,15 @@
// Frontend general
#wrapwrap {
position: relative;
display: flex;
flex-flow: column nowrap;
width: 100%;
min-height: 100%;
> * {
flex: 0 0 auto;
}
> main {
flex: 1 0 auto;
}
}
+1
View File
@@ -399,6 +399,7 @@
</t>
<t t-call="web._assets_bootstrap"/>
<link rel="stylesheet" type="text/scss" href="/web/static/src/scss/base_frontend.scss"/>
<link rel="stylesheet" type="text/scss" href="/web/static/src/scss/lazyloader.scss"/>
<link rel="stylesheet" type="text/scss" href="/web/static/src/scss/navbar_mobile.scss"/>
<link rel="stylesheet" type="text/scss" href="/web/static/src/scss/notification.scss"/>