summaryrefslogtreecommitdiff
path: root/addons/survey/static
diff options
context:
space:
mode:
authorAurélien Warnon <awa@odoo.com>2020-01-21 10:42:16 +0000
committerAurélien Warnon <awa@odoo.com>2020-01-31 14:35:48 +0000
commite973bd5519295ca737b57de6d50b021ffb5ec6c4 (patch)
tree0e14b576c2c68c2fb0af218a3e809ebe91090a18 /addons/survey/static
parent5e6c09a1aee33a4e30b6c861998d0c4d4c5900fa (diff)
[IMP] survey: add a 'live session' mode to the survey app
PURPOSE Add a "live session" mode for survey that allows the host to interact with its audience. He controls the pace of the survey and the audience answers questions one at a time. Results for each question can be displayed by the host to adapt his speech in real time. A gamification component even allows to give points to the attendees, based on the quickness of their answer, and create ranking to keep everyone's attention. SPECIFICATIONS This whole feature is deeply integrated with survey. It adds a layer of "survey session" on top if it that gathers attendees and answers during the lifespan of the session. The attendee identity is filled in by a special question that is marked as "Save as user nickname". If the host has not configured that question or if the user doesn't answer, they are marked as "Anonymous" in the rankings. The answer score computation is also altered during the session to take the speed of the answer into account. To ease the access to the survey to attendees, we have introduced two new routes in the survey main controller: - '/s/123456' That uses the 6 first characters of the token to give quick access to the survey and allow typing the URL manually somewhat conveniently. - '/s' that renders a view that asks the attendee to enter the survey 'code', which then redirects to the previous route. This commit also fixes a few bugs: - A bugged use case with the "enter key" listener. When the survey is done, we display a "result template" that shows the user his score and allows him to retry if possible. When this template is displayed, we don't want to listen to the "enter" key that allows to submit the survey form. - We now correctly remove the timer and initialize the result widget when the user presses 'enter' on the last page / question. SURVEY SESSION FLOW HOST point of view: - On the Survey Form, the host configures: - "Reward quick answers" that gives more points to attendees if they answer quickly - Question Time Limit, on each question, that defines the time limit for that specific question, allowing to have different (or no) timer for each question. - From the Survey Form, the host can start a "live session" Only one session per survey can be running at a time. - When he starts the session, the session_state is marked as 'ready', meaning all attendees landing on the survey page will be part of the session. - The host will land on a page showing the current number of attendees as well as the link for the attendees to join that session. - The host starts the session, activating the first question of this session. We keep an active reference to the "current_question_id" and use the bus to trigger an event that will refresh the survey page for all attendees, showing them the question and allowing them to answer. - The host gets a "question management screen", from which he can: - See the current question text & suggested answers - See the current question timer - See the number of answers received for the question - Display the answers of the question (same view as the survey "results" page, but only for the question) - Display the ranking of attendees (if "competitive mode" is enabled) - The host controls the pace of the survey by moving to the next question until it's the last one of the survey, then ends the current session. When the session is closed, we mark the answers of the attendees as "done". - The host lands on a screen where he sees the survey results and the ranking of all attendees. ATTENDEE point of view: - He reaches the survey when a session and open, and gets a screen asking him to wait until the host decides to start the session. - When the sessions starts, he's automatically redirected to the first question (see above). - If the session is already started when he reaches the link, he lands on the current question (and NOT on the first one) - If the host has configured a time limit, the attendee sees the countdown while he's answering. - When the attendee answers the question, the screen tells him the answer is registered and he has to wait for the host to go to the next question, which will happen automatically. - If the timer reaches 0 and he has not submitted his answer, it's too late and he waits for the next one. - It continues like that until the end of the session. - At the end of the session, the attendee gets a screen with his global results (same as a regular scored survey). PR #43568 Task 1972640 Co-authored-by: David Beguin <dbe@odoo.com>
Diffstat (limited to 'addons/survey/static')
-rw-r--r--addons/survey/static/src/js/survey_form.js198
-rw-r--r--addons/survey/static/src/js/survey_quick_access.js66
-rw-r--r--addons/survey/static/src/js/survey_result.js16
-rw-r--r--addons/survey/static/src/js/survey_session_manage.js323
-rw-r--r--addons/survey/static/src/js/survey_timer.js3
-rw-r--r--addons/survey/static/src/scss/survey_form.scss189
6 files changed, 729 insertions, 66 deletions
diff --git a/addons/survey/static/src/js/survey_form.js b/addons/survey/static/src/js/survey_form.js
index 6c51b0e2d8c..f270949903d 100644
--- a/addons/survey/static/src/js/survey_form.js
+++ b/addons/survey/static/src/js/survey_form.js
@@ -33,8 +33,9 @@ publicWidget.registry.SurveyFormWidget = publicWidget.Widget.extend({
this.fadeInOutDelay = 400;
return this._super.apply(this, arguments).then(function () {
self.options = self.$target.find('form').data();
+ self.readonly = self.options.readonly;
// Init fields
- if (!self.options.isStartScreen) {
+ if (!self.options.isStartScreen && !self.readonly) {
self._initTimer();
self._initBreadcrumb();
}
@@ -45,9 +46,14 @@ publicWidget.registry.SurveyFormWidget = publicWidget.Widget.extend({
self._initTextArea();
self._focusOnFirstInput();
// Init event listener
- if (!self.options.readonly) {
+ if (!self.readonly) {
$(document).on('keypress', self._onKeyPress.bind(self));
}
+ if (self.options.sessionInProgress &&
+ (self.options.isStartScreen || self.options.hasAnswered)) {
+ self.preventEnterSubmit = true;
+ }
+ self._initSessionManagement();
});
},
@@ -71,7 +77,10 @@ publicWidget.registry.SurveyFormWidget = publicWidget.Widget.extend({
// Handle Start / Next / Submit
if (keyCode === 13) { // Enter : go Next
event.preventDefault();
- this._submitForm({});
+ if (!this.preventEnterSubmit) {
+ var isFinish = this.$('button[value="finish"]').length !== 0;
+ this._submitForm({isFinish: isFinish});
+ }
} else if (self.options.questionsLayout === 'page_per_question'
&& letter.match(/[a-z]/i)) {
var $choiceInput = this.$(`input[data-selection-key=${letter}]`);
@@ -131,14 +140,16 @@ publicWidget.registry.SurveyFormWidget = publicWidget.Widget.extend({
},
_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');
- }
+ if (this.readonly) {
+ return;
+ }
+
+ 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');
}
},
@@ -161,6 +172,75 @@ publicWidget.registry.SurveyFormWidget = publicWidget.Widget.extend({
this._submitForm({'previousPageId': event.data.previousPageId});
},
+ /**
+ * We listen to 'next_question' and 'end_session' events to load the next
+ * page of the survey automatically, based on the host pacing.
+ *
+ * If the trigger is 'next_question', we handle some extra computation to find
+ * a suitable "fadeInOutDelay" based on the delay between the time of the question
+ * change by the host and the time of reception of the event.
+ * This will allow us to account for a little bit of server lag (up to 2 seconds)
+ * while giving everyone a fair experience on the quiz.
+ *
+ * e.g 1:
+ * - The host switches the question
+ * - We receive the event 500 ms later due to server lag
+ * - -> The fadeInOutDelay will be 750 ms (500ms delay + 750ms * 2 fade in fade out)
+ *
+ * e.g 2:
+ * - The host switches the question
+ * - We receive the event 1500 ms later due to bigger server lag
+ * - -> The fadeInOutDelay will be 250ms (1500ms delay + 250ms * 2 fade in fade out)
+ *
+ * @private
+ * @param {Array[]} notifications structured as specified by the bus feature
+ */
+ _onNotification: function (notifications) {
+ var nextPageEvent = false;
+ if (notifications && notifications.length !== 0) {
+ notifications.forEach(function (notification) {
+ if (notification.length >= 2) {
+ var event = notification[1];
+ if (event.type === 'next_question' ||
+ event.type === 'end_session') {
+ nextPageEvent = event;
+ }
+ }
+ });
+ }
+
+ if (this.options.isStartScreen && nextPageEvent.type === 'end_session') {
+ // can happen when triggering the same survey session multiple times
+ // we received an "old" end_session event that needs to be ignored
+ return;
+ }
+
+ if (nextPageEvent) {
+ if (nextPageEvent.type === 'next_question') {
+ var serverDelayMS = moment.utc().valueOf() - moment.unix(nextPageEvent.question_start).utc().valueOf();
+ if (serverDelayMS < 0) {
+ serverDelayMS = 0;
+ } else if (serverDelayMS > 2000) {
+ serverDelayMS = 2000;
+ }
+ this.fadeInOutDelay = (2000 - serverDelayMS) / 2;
+ } else {
+ this.fadeInOutDelay = 400;
+ }
+
+ this.preventEnterSubmit = false;
+ this.readonly = false;
+ this._nextScreen(
+ this._rpc({
+ route: `/survey/next_question/${this.options.surveyToken}/${this.options.answerToken}`,
+ }), {
+ initTimer: true,
+ isFinish: nextPageEvent.type === 'end_session'
+ }
+ );
+ }
+ },
+
// SUBMIT
// -------------------------------------------------------------------------
@@ -174,6 +254,8 @@ publicWidget.registry.SurveyFormWidget = publicWidget.Widget.extend({
* @param {Array} [options]
* @param {Integer} [options.previousPageId] navigates to page id
* @param {Boolean} [options.skipValidation] skips JS validation
+ * @param {Boolean} [options.initTime] will force the re-init of the timer after next
+ * screen transition
* @param {Boolean} [options.isFinish] fades out breadcrumb and timer
* @private
*/
@@ -201,6 +283,32 @@ publicWidget.registry.SurveyFormWidget = publicWidget.Widget.extend({
this._prepareSubmitValues(formData, params);
}
+ // prevent user from submitting more times using enter key
+ this.preventEnterSubmit = true;
+
+ if (this.options.sessionInProgress) {
+ // reset the fadeInOutDelay when attendee is submitting form
+ this.fadeInOutDelay = 400;
+ // prevent user from clicking on matrix options when form is submitted
+ this.readonly = true;
+ }
+
+ var submitPromise = self._rpc({
+ route: _.str.sprintf('%s/%s/%s', route, self.options.surveyToken, self.options.answerToken),
+ params: params,
+ });
+ this._nextScreen(submitPromise, options);
+ },
+
+ /**
+ * Will fade out / fade in the next screen based on passed promise and options.
+ *
+ * @param {Promise} nextScreenPromise
+ * @param {Object} options see '_submitForm' for details
+ */
+ _nextScreen: function (nextScreenPromise, options) {
+ var self = this;
+
var resolveFadeOut;
var fadeOutPromise = new Promise(function (resolve, reject) {resolveFadeOut = resolve;});
@@ -211,33 +319,41 @@ publicWidget.registry.SurveyFormWidget = publicWidget.Widget.extend({
self.$(selectorsToFadeout.join(',')).fadeOut(this.fadeInOutDelay, function () {
resolveFadeOut();
});
- var submitPromise = self._rpc({
- route: _.str.sprintf('%s/%s/%s', route, self.options.surveyToken, self.options.answerToken),
- params: params,
- });
- Promise.all([fadeOutPromise, submitPromise]).then(function (results) {
- return self._onSubmitDone(results[1], options.isFinish);
+
+ Promise.all([fadeOutPromise, nextScreenPromise]).then(function (results) {
+ return self._onNextScreenDone(results[1], options);
});
},
/**
- * Follow the submit and handle the transition from one screen to another
- * Also handle server side validation and displays eventual error messages.
- */
- _onSubmitDone: function (result, isFinish) {
+ * Handle server side validation and display eventual error messages.
+ *
+ * @param {string} result the HTML result of the screen to display
+ * @param {Object} options see '_submitForm' for details
+ */
+ _onNextScreenDone: function (result, options) {
var self = this;
+ if (!(options && options.isFinish)
+ && !this.options.sessionInProgress) {
+ this.preventEnterSubmit = false;
+ }
+
if (result && !result.error) {
this.$(".o_survey_form_content").empty();
this.$(".o_survey_form_content").html(result);
this.$('div.o_survey_form_date').each(function () {
self._initDateTimePicker($(this));
});
- if (this.options.isStartScreen) {
+ if (this.options.isStartScreen || (options && options.initTimer)) {
this._initTimer();
this.options.isStartScreen = false;
+ } else {
+ if (this.options.sessionInProgress && this.surveyTimerWidget) {
+ this.surveyTimerWidget.destroy();
+ }
}
- if (isFinish) {
+ if (options && options.isFinish) {
this._initResultWidget();
if (this.surveyBreadcrumbWidget) {
this.$('.o_survey_breadcrumb_container').addClass('d-none');
@@ -588,12 +704,37 @@ publicWidget.registry.SurveyFormWidget = publicWidget.Widget.extend({
}
},
+ /**
+ * Will handle bus specific behavior for survey 'sessions'
+ *
+ * @private
+ */
+ _initSessionManagement: function () {
+ if (this.options.surveyToken && this.options.sessionInProgress) {
+ this.call('bus_service', 'addChannel', this.options.surveyToken);
+ this.call('bus_service', 'startPolling');
+
+ this.call('bus_service', 'onNotification', this, this._onNotification);
+ }
+ },
+
_initTimer: function () {
+ if (this.surveyTimerWidget) {
+ this.surveyTimerWidget.destroy();
+ }
+
var self = this;
- var $timer = this.$('.o_survey_timer');
- if ($timer.length) {
- var timeLimitMinutes = this.options.timeLimitMinutes;
- var timer = this.options.timer;
+ var $timerData = this.$('.o_survey_form_content_data');
+ var questionTimeLimitReached = $timerData.data('questionTimeLimitReached');
+ var timeLimitMinutes = $timerData.data('timeLimitMinutes');
+ var hasAnswered = $timerData.data('hasAnswered');
+
+ if (!questionTimeLimitReached && !hasAnswered && timeLimitMinutes) {
+ var timer = $timerData.data('timer');
+ var $timer = $('<span>', {
+ class: 'o_survey_timer'
+ });
+ this.$('.o_survey_timer_container').append($timer);
this.surveyTimerWidget = new publicWidget.registry.SurveyTimerWidget(this, {
'timer': timer,
'timeLimitMinutes': timeLimitMinutes
@@ -602,10 +743,9 @@ publicWidget.registry.SurveyFormWidget = publicWidget.Widget.extend({
this.surveyTimerWidget.on('time_up', this, function (ev) {
self._submitForm({
'skipValidation': true,
- 'isFinish': true
+ 'isFinish': !this.options.sessionInProgress
});
});
- $timer.removeClass('d-none');
}
},
diff --git a/addons/survey/static/src/js/survey_quick_access.js b/addons/survey/static/src/js/survey_quick_access.js
new file mode 100644
index 00000000000..0534aa41878
--- /dev/null
+++ b/addons/survey/static/src/js/survey_quick_access.js
@@ -0,0 +1,66 @@
+odoo.define('survey.quick.access', function (require) {
+'use strict';
+
+var publicWidget = require('web.public.widget');
+
+publicWidget.registry.SurveyQuickAccessWidget = publicWidget.Widget.extend({
+ selector: '.o_survey_quick_access',
+ events: {
+ 'click button[type="submit"]': '_onSubmit',
+ },
+
+ //--------------------------------------------------------------------------
+ // Widget
+ //--------------------------------------------------------------------------
+
+ /**
+ * @override
+ */
+ start: function () {
+ var self = this;
+ return this._super.apply(this, arguments).then(function () {
+ // Init event listener
+ if (!self.readonly) {
+ $(document).on('keypress', self._onKeyPress.bind(self));
+ }
+ });
+ },
+
+ // -------------------------------------------------------------------------
+ // Private
+ // -------------------------------------------------------------------------
+
+ // Handlers
+ // -------------------------------------------------------------------------
+
+ _onKeyPress: function (event) {
+ if (event.keyCode === 13) { // Enter
+ event.preventDefault();
+ this._submitCode();
+ }
+ },
+
+ _onSubmit: function (event) {
+ event.preventDefault();
+ this._submitCode();
+ },
+
+ _submitCode: function () {
+ var self = this;
+ this.$('.o_survey_error').addClass("d-none");
+ var $accessCodeInput = this.$('input#access_code');
+ this._rpc({
+ route: `/survey/check_access_code/${$accessCodeInput.val()}`,
+ }).then(function (response) {
+ if (response.survey_url) {
+ window.location = response.survey_url;
+ } else {
+ self.$('.o_survey_error').removeClass("d-none");
+ }
+ });
+ },
+});
+
+return publicWidget.registry.SurveyQuickAccessWidget;
+
+});
diff --git a/addons/survey/static/src/js/survey_result.js b/addons/survey/static/src/js/survey_result.js
index dd7e1d5773d..c27fc343a43 100644
--- a/addons/survey/static/src/js/survey_result.js
+++ b/addons/survey/static/src/js/survey_result.js
@@ -325,17 +325,25 @@ publicWidget.registry.SurveyResultWidget = publicWidget.Widget.extend({
start: function () {
var self = this;
return this._super.apply(this, arguments).then(function () {
+ var allPromises = [];
+
self.$('.pagination').each(function (){
var questionId = $(this).data("question_id");
- new publicWidget.registry.SurveyResultPagination(self, {
+ allPromises.push(new publicWidget.registry.SurveyResultPagination(self, {
'questionsEl': self.$('#survey_table_question_'+ questionId)
- }).attachTo($(this));
+ }).attachTo($(this)));
});
self.$('.survey_graph').each(function () {
- new publicWidget.registry.SurveyResultChart(self)
- .attachTo($(this));
+ allPromises.push(new publicWidget.registry.SurveyResultChart(self)
+ .attachTo($(this)));
});
+
+ if (allPromises.length !== 0) {
+ return Promise.all(allPromises);
+ } else {
+ return Promise.resolve();
+ }
});
},
diff --git a/addons/survey/static/src/js/survey_session_manage.js b/addons/survey/static/src/js/survey_session_manage.js
new file mode 100644
index 00000000000..daa6979b4ab
--- /dev/null
+++ b/addons/survey/static/src/js/survey_session_manage.js
@@ -0,0 +1,323 @@
+odoo.define('survey.session_manage', function (require) {
+'use strict';
+
+var publicWidget = require('web.public.widget');
+var core = require('web.core');
+var _t = core._t;
+
+publicWidget.registry.SurveySessionManage = publicWidget.Widget.extend({
+ selector: '.o_survey_session_manage',
+ events: {
+ 'click .o_survey_session_copy': '_onCopySessionLink',
+ 'click .o_survey_session_next, .o_survey_session_start': '_onNextQuestionClick',
+ 'click .o_survey_session_end': '_onEndSessionClick',
+ 'click .o_survey_session_results_href': '_onResultsTabClick',
+ 'click .o_survey_session_ranking_href': '_onRankingTabClick',
+ 'click .o_survey_session_refresh_results': '_onRefreshResultsClick',
+ 'click .o_survey_session_refresh_ranking': '_onRefreshRankingClick',
+ },
+
+ /**
+ * Overridden to set a few properties that come from the python template rendering.
+ *
+ * We also handle the timer IF we're not "transitioning", meaning a fade out of the previous
+ * $el to the next question (the fact that we're transitioning is in the isRpcCall data).
+ * If we're transitioning, the timer is handled manually at the end of the transition.
+ */
+ start: function () {
+ var self = this;
+ return this._super.apply(this, arguments).then(function () {
+ self.surveyId = self.$el.data('surveyId');
+ self.surveyAccessToken = self.$el.data('surveyAccessToken');
+ self.isStartScreen = self.$el.data('isStartScreen');
+
+ var isRpcCall = self.$el.data('isRpcCall');
+ if (!isRpcCall) {
+ self._startTimer();
+ }
+
+ if (self.isStartScreen) {
+ self._refreshAttendeesCount();
+ } else if (!self.refreshingAnswers) {
+ self.stopRefreshingAttendees = true;
+ self._refreshReceivedAnswers();
+ }
+ });
+ },
+
+ //--------------------------------------------------------------------------
+ // Handlers
+ //--------------------------------------------------------------------------
+
+ /**
+ * Copies the survey URL link to the clipboard.
+ * We use 'ClipboardJS' to avoid having to print the URL in a standard text input
+ *
+ * @param {MouseEvent} ev
+ */
+ _onCopySessionLink: function (ev) {
+ var self = this;
+ ev.preventDefault();
+
+ var $clipboardBtn = this.$('.o_survey_session_copy');
+
+ $clipboardBtn.popover({
+ placement: 'right',
+ container: 'body',
+ offset: '0, 3',
+ content: function () {
+ return _t("Copied !");
+ }
+ });
+
+ var clipboard = new ClipboardJS('.o_survey_session_copy', {
+ text: function () {
+ return self.$('.o_survey_session_copy_url').val();
+ },
+ container: this.el
+ });
+
+ clipboard.on('success', function () {
+ clipboard.destroy();
+ $clipboardBtn.popover('show');
+ _.delay(function () {
+ $clipboardBtn.popover('hide');
+ }, 800);
+ });
+
+ clipboard.on('error', function (e) {
+ clipboard.destroy();
+ });
+ },
+
+ /**
+ * We use a fade in/out mechanism to display the next question of the session.
+ *
+ * The fade out happens at the same moment as the _rpc to get the new question template.
+ * When they're both finished, we update the HTML of this widget with the new template and then
+ * fade in the updated question to the user.
+ *
+ * The timer (if configured) starts at the end of the fade in animation.
+ *
+ * @param {MouseEvent} ev
+ * @private
+ */
+ _onNextQuestionClick: function (ev) {
+ var self = this;
+ ev.preventDefault();
+ this.isStartScreen = false;
+
+ var resolveFadeOut;
+ var fadeOutPromise = new Promise(function (resolve, reject) { resolveFadeOut = resolve; });
+ this.$el.fadeOut(1000, function () {
+ resolveFadeOut();
+ });
+
+ var nextQuestionPromise = this._rpc({
+ route: _.str.sprintf('/survey/session/next_question/%s', self.surveyAccessToken)
+ });
+
+ Promise.all([fadeOutPromise, nextQuestionPromise]).then(function (results) {
+ var $renderedTemplate = $(results[1]);
+ self.$el.replaceWith($renderedTemplate);
+ self.attachTo($renderedTemplate);
+ self.$el.fadeIn(1000, function () {
+ self._startTimer();
+ });
+ });
+ },
+
+ /**
+ * Marks this session as 'done' and redirects the user to the results.
+ *
+ * @param {MouseEvent} ev
+ * @private
+ */
+ _onEndSessionClick: function (ev) {
+ var self = this;
+ ev.preventDefault();
+
+ this._rpc({
+ model: 'survey.survey',
+ method: 'action_end_session',
+ args: [[this.surveyId]],
+ }).then(function () {
+ document.location = _.str.sprintf(
+ '/survey/results/%s',
+ self.surveyId
+ );
+ });
+ },
+
+ _onResultsTabClick: function (ev) {
+ this._onRefreshResultsClick(ev, true);
+ },
+
+ _onRankingTabClick: function (ev) {
+ this._onRefreshRankingClick(ev, true);
+ },
+
+ _onRefreshResultsClick: function (ev, preventSpin) {
+ ev.preventDefault();
+
+ if (this.$('.o_survey_session_refresh_results').hasClass('fa-spin')) {
+ return;
+ }
+
+ var refreshPromise = this._refreshResults();
+ if (!preventSpin) {
+ this._spinIcon(this.$('.o_survey_session_refresh_results'), refreshPromise);
+ }
+ },
+
+ _onRefreshRankingClick: function (ev, preventSpin) {
+ ev.preventDefault();
+
+ if (this.$('.o_survey_session_refresh_ranking').hasClass('fa-spin')) {
+ return;
+ }
+
+ var refreshPromise = this._refreshRanking();
+
+ if (!preventSpin) {
+ this._spinIcon(this.$('.o_survey_session_refresh_ranking'), refreshPromise);
+ }
+ },
+
+ //--------------------------------------------------------------------------
+ // Private
+ //--------------------------------------------------------------------------
+
+ /**
+ * Will start the question timer so that the host may know when the question is done to display
+ * the results and the ranking.
+ */
+ _startTimer: function () {
+ var $timer = this.$('.o_survey_timer');
+
+ if ($timer.length) {
+ var timeLimitMinutes = this.$el.data('timeLimitMinutes');
+ var timer = this.$el.data('timer');
+ this.surveyTimerWidget = new publicWidget.registry.SurveyTimerWidget(this, {
+ 'timer': timer,
+ 'timeLimitMinutes': timeLimitMinutes
+ });
+ this.surveyTimerWidget.attachTo($timer);
+ }
+ },
+
+ /**
+ * Refreshes the question results on screen.
+ */
+ _refreshResults: function () {
+ var self = this;
+
+ return this._rpc({
+ route: _.str.sprintf('/survey/session/results/%s', this.surveyAccessToken)
+ }).then(function (renderedTemplate) {
+ var $renderedTemplate = $(renderedTemplate).addClass('py-3');
+ self.$('.o_survey_result').empty().append($renderedTemplate);
+ return new publicWidget.registry.SurveyResultWidget().attachTo($renderedTemplate);
+ });
+ },
+
+ /**
+ * Refreshes the question ranking on screen.
+ * We set the width of the progress bars after the rendering to enable a width css animation.
+ */
+ _refreshRanking: function () {
+ var self = this;
+
+ return this._rpc({
+ route: _.str.sprintf('/survey/session/ranking/%s', this.surveyAccessToken)
+ }).then(function (renderedTemplate) {
+ var $renderedTemplate = $(renderedTemplate).addClass('py-3');
+ self.$('.o_survey_session_ranking_container').empty().append($renderedTemplate);
+ // delay by 200ms to account for fade out / in
+ setTimeout(function () {
+ self.$('.o_survey_session_ranking_bar').each(function () {
+ $(this).css('width', `calc(calc(100% - 10rem) * ${$(this).data('widthRatio')})`);
+ });
+ }, 200);
+
+ return Promise.resolve();
+ });
+ },
+
+ /**
+ * We refresh the attendees count every 2 seconds while the user is on the start screen.
+ * When he leaves the start screen, the "stopRefreshingAttendees" becomes true and we stop
+ * querying the answer_count.
+ */
+ _refreshAttendeesCount: function () {
+ var self = this;
+
+ setTimeout(function () {
+ self._rpc({
+ model: 'survey.survey',
+ method: 'read',
+ args: [[self.surveyId], ['answer_count']],
+ }).then(function (result) {
+ if (result && result.length === 1){
+ self.$('.o_survey_session_attendees_count').text(
+ result[0].answer_count
+ );
+ }
+ if (!self.stopRefreshingAttendees) {
+ self._refreshAttendeesCount();
+ }
+ });
+ }, 2000);
+ },
+
+ /**
+ * We refresh the received answers count every 2 seconds for the current question.
+ * This allows the host to know what he can move on to the next question.
+ * (Without having to manually refresh the page).
+ */
+ _refreshReceivedAnswers: function () {
+ var self = this;
+
+ this.refreshingAnswers = true;
+ setTimeout(function () {
+ self._rpc({
+ model: 'survey.survey',
+ method: 'read',
+ args: [[self.surveyId], ['session_question_answer_count']],
+ }).then(function (result) {
+ if (result && result.length === 1){
+ self.$('.o_survey_session_answer_count').text(
+ result[0].session_question_answer_count
+ );
+ }
+ self._refreshReceivedAnswers();
+ });
+ }, 2000);
+ },
+
+ /**
+ * We need the refresh icon to spin for at least 1s to make the user feel like it's
+ * refreshing something.
+ * If we only wait for the 'refreshPromise', it can be so fast that the icon doesn't even move.
+ *
+ * @param {$.Element} $target
+ * @param {Promise} refreshPromise
+ */
+ _spinIcon: function ($target, refreshPromise) {
+ $target.addClass('fa-spin');
+
+ var minimumSpinResolve;
+ var minimumSpinPromise = new Promise(function (resolve) { minimumSpinResolve = resolve;});
+ setTimeout(function () {
+ minimumSpinResolve();
+ }, 1000);
+
+ Promise.all([minimumSpinPromise, refreshPromise]).then(function () {
+ $target.removeClass('fa-spin');
+ });
+ },
+});
+
+return publicWidget.registry.SurveySessionManage;
+
+});
diff --git a/addons/survey/static/src/js/survey_timer.js b/addons/survey/static/src/js/survey_timer.js
index 664871e4e7e..d0a38c88324 100644
--- a/addons/survey/static/src/js/survey_timer.js
+++ b/addons/survey/static/src/js/survey_timer.js
@@ -31,7 +31,6 @@ publicWidget.registry.SurveyTimerWidget = publicWidget.Widget.extend({
if (this.timeLimitMinutes <= 0 || this.countDownDate.diff(moment.utc(), 'seconds') < 0) {
this.trigger_up('time_up');
} else {
- this.$timer = this.$el.find('.timer');
this._updateTimer(this);
this.surveyTimerInterval = setInterval(this._updateTimer.bind(this), 1000);
}
@@ -57,7 +56,7 @@ publicWidget.registry.SurveyTimerWidget = publicWidget.Widget.extend({
if (timeLeft >= 0) {
var timeLeftMinutes = parseInt(timeLeft / 60);
var timeLeftSeconds = timeLeft - (timeLeftMinutes * 60);
- this.$timer.html(this._formatTime(timeLeftMinutes) + ':' + this._formatTime(timeLeftSeconds));
+ this.$el.text(this._formatTime(timeLeftMinutes) + ':' + this._formatTime(timeLeftSeconds));
} else {
clearInterval(this.surveyTimerInterval);
this.trigger_up('time_up');
diff --git a/addons/survey/static/src/scss/survey_form.scss b/addons/survey/static/src/scss/survey_form.scss
index 262a0532130..f9f98c9ea03 100644
--- a/addons/survey/static/src/scss/survey_form.scss
+++ b/addons/survey/static/src/scss/survey_form.scss
@@ -6,7 +6,7 @@
Common Style
**********************************************************/
-.o_survey_form, .o_survey_print {
+.o_survey_form, .o_survey_print, .o_survey_session_manage, .o_survey_quick_access {
.o_survey_question_error {
height: 0px;
transition: height .5s ease;
@@ -16,6 +16,15 @@
}
}
+ fieldset[disabled] {
+ .o_survey_question_text_box,
+ .o_survey_question_date,
+ .o_survey_question_datetime,
+ .o_survey_question_numerical_box {
+ padding-left: 0px;
+ }
+ }
+
.o_survey_question_text_box,
.o_survey_question_date,
.o_survey_question_datetime,
@@ -88,28 +97,12 @@
}
}
-/**********************************************************
- Form Specific Style
- **********************************************************/
-
-.o_survey_form {
+.o_survey_form, .o_survey_session_manage {
min-height: 100%;
.o_survey_choice_btn {
background-color: rgba($primary, 0.5);
border: 2px solid $primary;
- cursor: pointer;
-
- &.o_survey_selected {
- background-color: $primary;
- }
-
- &:hover {
- background-color: rgba($primary, 0.8);
- .o_survey_choice_key span.o_survey_key {
- opacity: 1;
- }
- }
}
.o_survey_choice_key {
@@ -120,14 +113,7 @@
font-size: smaller;
top: -2px;
&.o_survey_key {
- width: inherit;
- right: 10px;
- border: 2px solid $primary;
- border-right: 0px;
- height: 25px;
opacity: 0;
- transition: opacity 0.4s ease;
- white-space: nowrap;
}
}
}
@@ -138,19 +124,160 @@
}
td {
background-color: rgba($primary, 0.2);
+ }
+ }
+
+ .o_survey_progress {
+ height:0.5em;
+ }
+}
+
+/**********************************************************
+ Form Specific Style
+ **********************************************************/
+
+.o_survey_form {
+ .o_survey_choice_btn {
+ cursor: pointer;
+
+ &.o_survey_selected {
+ background-color: $primary;
+ }
+
+ &:hover {
+ background-color: rgba($primary, 0.8);
+ .o_survey_choice_key span.o_survey_key {
+ opacity: 1;
+ }
+ }
+ }
+
+ .o_survey_choice_key span.o_survey_key {
+ width: inherit;
+ right: 10px;
+ border: 2px solid $primary;
+ border-right: 0px;
+ height: 25px;
+ transition: opacity 0.4s ease;
+ white-space: nowrap;
+ }
+
+ .o_survey_question_matrix td:hover {
+ background-color: rgba($primary, 0.5);
+ cursor: pointer;
+ .o_survey_choice_key span.o_survey_key {
+ opacity: 1;
+ }
+ }
+}
+
+/**********************************************************
+ Survey Session Specific Style
+ **********************************************************/
+
+.o_survey_session_manage {
+ .o_survey_manage_fontsize_14 {
+ font-size: 1.4rem;
+ }
+
+ .o_survey_session_manage_container {
+ border: solid 1px #999999;
+ box-shadow: 0px 1px 1px 0px #999999;
+ min-height: 75vh;
+
+ .js_question-wrapper {
+ padding-bottom: 0px;
+ }
+ }
+
+ .tab-content > .o_survey_session_manage_question.tab-pane {
+ display: none;
+ }
+
+ .tab-content > .o_survey_session_manage_question.active {
+ display: flex;
+ }
+
+ .o_survey_session_copy {
+ cursor: pointer;
+ opacity: .75;
+ transition: opacity .3s ease;
+ &:hover {
+ opacity: 1;
+ }
+ }
+
+ .btn {
+ font-variant: small-caps;
+ &.o_survey_session_end {
+ right: 0px;
+ }
+ }
+ .o_survey_session_results, .o_survey_session_ranking {
+ .fa {
+ font-size: .9rem;
+ opacity: .75;
&:hover {
- background-color: rgba($primary, 0.5);
+ transition: opacity .4s ease;
+ opacity: 1;
cursor: pointer;
- .o_survey_choice_key span.o_survey_key {
- opacity: 1;
- }
}
}
+
+ .o_survey_result, .o_survey_session_ranking_container {
+ overflow: hidden;
+ }
+
+ .survey_filter {
+ display: none;
+ }
}
- .o_survey_progress {
- height:0.5em;
+ .o_survey_result {
+ .nav-tabs .nav-link.active {
+ background-color: transparent;
+ border-color: #dee2e6 #dee2e6 transparent;
+ }
+ }
+
+ .nav-tabs {
+ border-bottom: 0px;
+ .nav-item .nav-link {
+ border-radius: 0px;
+ background-color: transparent;
+ border: solid 1px transparent;
+
+ &.active {
+ border: solid 1px #999999;
+ border-bottom: 0px;
+ }
+ }
+ }
+}
+
+.o_survey_session_ranking {
+ .o_survey_session_ranking_item {
+ line-height: 1.4rem;
+
+ .o_survey_session_ranking_index {
+ width: 1.5rem;
+ }
+
+ .o_survey_session_ranking_bar {
+ min-width: 90px;
+ height: 1.4rem;
+ color: white;
+ background-color: #007A77;
+ transition: width ease 1.5s;
+ }
+
+ .o_survey_session_ranking_name {
+ width: 7.5rem;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ }
}
}