summaryrefslogtreecommitdiff
path: root/addons/survey/static/src/js/survey_session_manage.js
diff options
context:
space:
mode:
authorstephanchrst <stephanchrst@gmail.com>2022-05-10 21:51:50 +0700
committerstephanchrst <stephanchrst@gmail.com>2022-05-10 21:51:50 +0700
commit3751379f1e9a4c215fb6eb898b4ccc67659b9ace (patch)
treea44932296ef4a9b71d5f010906253d8c53727726 /addons/survey/static/src/js/survey_session_manage.js
parent0a15094050bfde69a06d6eff798e9a8ddf2b8c21 (diff)
initial commit 2
Diffstat (limited to 'addons/survey/static/src/js/survey_session_manage.js')
-rw-r--r--addons/survey/static/src/js/survey_session_manage.js588
1 files changed, 588 insertions, 0 deletions
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 00000000..dc9898e7
--- /dev/null
+++ b/addons/survey/static/src/js/survey_session_manage.js
@@ -0,0 +1,588 @@
+odoo.define('survey.session_manage', function (require) {
+'use strict';
+
+var publicWidget = require('web.public.widget');
+var SurveySessionChart = require('survey.session_chart');
+var SurveySessionTextAnswers = require('survey.session_text_answers');
+var SurveySessionLeaderBoard = require('survey.session_leaderboard');
+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_navigation_next, .o_survey_session_start': '_onNext',
+ 'click .o_survey_session_navigation_previous': '_onBack',
+ 'click .o_survey_session_close': '_onEndSessionClick',
+ },
+
+ /**
+ * 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;
+ this.fadeInOutTime = 500;
+ return this._super.apply(this, arguments).then(function () {
+ // general survey props
+ self.surveyId = self.$el.data('surveyId');
+ self.surveyAccessToken = self.$el.data('surveyAccessToken');
+ self.isStartScreen = self.$el.data('isStartScreen');
+ self.isLastQuestion = self.$el.data('isLastQuestion');
+ // scoring props
+ self.isScoredQuestion = self.$el.data('isScoredQuestion');
+ self.sessionShowLeaderboard = self.$el.data('sessionShowLeaderboard');
+ self.hasCorrectAnswers = self.$el.data('hasCorrectAnswers');
+ // display props
+ self.showBarChart = self.$el.data('showBarChart');
+ self.showTextAnswers = self.$el.data('showTextAnswers');
+
+ var isRpcCall = self.$el.data('isRpcCall');
+ if (!isRpcCall) {
+ self._startTimer();
+ $(document).on('keydown', self._onKeyDown.bind(self));
+ }
+
+ self._setupIntervals();
+ self._setupCurrentScreen();
+ var setupPromises = [];
+ setupPromises.push(self._setupTextAnswers());
+ setupPromises.push(self._setupChart());
+ setupPromises.push(self._setupLeaderboard());
+
+ return Promise.all(setupPromises);
+ });
+ },
+
+ //--------------------------------------------------------------------------
+ // 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();
+ });
+ },
+
+ /**
+ * Listeners for keyboard arrow / spacebar keys.
+ *
+ * - 39 = arrow-right
+ * - 32 = spacebar
+ * - 37 = arrow-left
+ *
+ * @param {KeyboardEvent} ev
+ */
+ _onKeyDown: function (ev) {
+ var keyCode = ev.keyCode;
+
+ if (keyCode === 39 || keyCode === 32) {
+ this._onNext(ev);
+ } else if (keyCode === 37) {
+ this._onBack(ev);
+ }
+ },
+
+ /**
+ * Handles the "next screen" behavior.
+ * It happens when the host uses the keyboard key / button to go to the next screen.
+ * The result depends on the current screen we're on.
+ *
+ * Possible values of the "next screen" to display are:
+ * - 'userInputs' when going from a question to the display of attendees' survey.user_input.line
+ * for that question.
+ * - 'results' when going from the inputs to the actual correct / incorrect answers of that
+ * question. Only used for scored simple / multiple choice questions.
+ * - 'leaderboard' (or 'leaderboardFinal') when going from the correct answers of a question to
+ * the leaderboard of attendees. Only used for scored simple / multiple choice questions.
+ * - If it's not one of the above: we go to the next question, or end the session if we're on
+ * the last question of this session.
+ *
+ * See '_getNextScreen' for a detailed logic.
+ *
+ * @param {Event} ev
+ */
+ _onNext: function (ev) {
+ ev.preventDefault();
+
+ var screenToDisplay = this._getNextScreen();
+
+ if (screenToDisplay === 'userInputs') {
+ this._setShowInputs(true);
+ this.$('.o_survey_session_navigation_previous').removeClass('d-none');
+ } else if (screenToDisplay === 'results') {
+ this._setShowAnswers(true);
+ // when showing results, stop refreshing answers
+ clearInterval(this.resultsRefreshInterval);
+ delete this.resultsRefreshInterval;
+ this.$('.o_survey_session_navigation_previous').removeClass('d-none');
+ } else if (['leaderboard', 'leaderboardFinal'].includes(screenToDisplay)
+ && !['leaderboard', 'leaderboardFinal'].includes(this.currentScreen)) {
+ if (this.isLastQuestion) {
+ this.$('.o_survey_session_navigation_next').addClass('d-none');
+ }
+ this.leaderBoard.showLeaderboard(true, this.isScoredQuestion);
+ } else {
+ if (!this.isLastQuestion) {
+ this._nextQuestion();
+ } else if (!this.sessionShowLeaderboard) {
+ // If we have no leaderboard to show, directly end the session
+ this.$('.o_survey_session_close').click();
+ }
+ }
+
+ this.currentScreen = screenToDisplay;
+ },
+
+ /**
+ * Reverse behavior of '_onNext'.
+ *
+ * @param {Event} ev
+ */
+ _onBack: function (ev) {
+ ev.preventDefault();
+
+ var screenToDisplay = this._getPreviousScreen();
+
+ if (screenToDisplay === 'question') {
+ this._setShowInputs(false);
+ this.$('.o_survey_session_navigation_previous').addClass('d-none');
+ } else if (screenToDisplay === 'userInputs') {
+ this._setShowAnswers(false);
+ // resume refreshing answers if necessary
+ if (!this.resultsRefreshInterval) {
+ this.resultsRefreshInterval = setInterval(this._refreshResults.bind(this), 2000);
+ }
+ } else if (screenToDisplay === 'results') {
+ this.leaderBoard.hideLeaderboard();
+ // when showing results, stop refreshing answers
+ clearInterval(this.resultsRefreshInterval);
+ delete this.resultsRefreshInterval;
+ }
+
+ this.currentScreen = screenToDisplay;
+ },
+
+ /**
+ * Marks this session as 'done' and redirects the user to the results based on the clicked link.
+ *
+ * @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 () {
+ if ($(ev.currentTarget).data('showResults')) {
+ document.location = _.str.sprintf(
+ '/survey/results/%s',
+ self.surveyId
+ );
+ } else {
+ window.history.back();
+ }
+ });
+ },
+
+ //--------------------------------------------------------------------------
+ // Private
+ //--------------------------------------------------------------------------
+
+ /**
+ * Business logic that determines the 'next screen' based on the current screen and the question
+ * configuration.
+ *
+ * Breakdown of use cases:
+ * - If we're on the 'question' screen, and the question is scored, we move to the 'userInputs'
+ * - If we're on the 'question' screen and it's NOT scored, then we move to
+ * - 'results' if the question has correct / incorrect answers
+ * (but not scored, which is kind of a corner case)
+ * - 'nextQuestion' otherwise
+ * - If we're on the 'userInputs' screen and the question has answers, we move to the 'results'
+ * - If we're on the 'results' and the question is scored, we move to the 'leaderboard'
+ * - In all other cases, we show the next question
+ * - (Small exception for the last question: we show the "final leaderboard")
+ *
+ * (For details about which screen shows what, see '_onNext')
+ */
+ _getNextScreen: function () {
+ if (this.currentScreen === 'question' && this.isScoredQuestion) {
+ return 'userInputs';
+ } else if (this.hasCorrectAnswers && ['question', 'userInputs'].includes(this.currentScreen)) {
+ return 'results';
+ } else if (this.sessionShowLeaderboard) {
+ if (['question', 'userInputs', 'results'].includes(this.currentScreen) && this.isScoredQuestion) {
+ return 'leaderboard';
+ } else if (this.isLastQuestion) {
+ return 'leaderboardFinal';
+ }
+ }
+ return 'nextQuestion';
+ },
+
+ /**
+ * Reverse behavior of '_getNextScreen'.
+ *
+ * @param {Event} ev
+ */
+ _getPreviousScreen: function () {
+ if (this.currentScreen === 'userInputs' && this.isScoredQuestion) {
+ return 'question';
+ } else if (this.currentScreen === 'results' ||
+ (this.currentScreen === 'leaderboard' && !this.isScoredQuestion)) {
+ return 'userInputs';
+ } else if (this.currentScreen === 'leaderboard' && this.isScoredQuestion) {
+ return 'results';
+ }
+
+ return this.currentScreen;
+ },
+
+ /**
+ * 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
+ */
+ _nextQuestion: function () {
+ var self = this;
+
+ this.isStartScreen = false;
+ if (this.surveyTimerWidget) {
+ this.surveyTimerWidget.destroy();
+ }
+
+ var resolveFadeOut;
+ var fadeOutPromise = new Promise(function (resolve, reject) { resolveFadeOut = resolve; });
+ this.$el.fadeOut(this.fadeInOutTime, function () {
+ resolveFadeOut();
+ });
+
+ var nextQuestionPromise = this._rpc({
+ route: _.str.sprintf('/survey/session/next_question/%s', self.surveyAccessToken)
+ });
+
+ // avoid refreshing results while transitioning
+ if (this.resultsRefreshInterval) {
+ clearInterval(this.resultsRefreshInterval);
+ delete this.resultsRefreshInterval;
+ }
+
+ Promise.all([fadeOutPromise, nextQuestionPromise]).then(function (results) {
+ if (results[1]) {
+ var $renderedTemplate = $(results[1]);
+ self.$el.replaceWith($renderedTemplate);
+ self.attachTo($renderedTemplate);
+ self.$el.fadeIn(self.fadeInOutTime, function () {
+ self._startTimer();
+ });
+ } else if (self.sessionShowLeaderboard) {
+ // Display last screen if leaderboard activated
+ self.isLastQuestion = true;
+ self._setupLeaderboard().then(function () {
+ self.$('.o_survey_session_leaderboard_title').text(_t('Final Leaderboard'));
+ self.$('.o_survey_session_navigation_next').addClass('d-none');
+ self.$('.o_survey_leaderboard_buttons').removeClass('d-none');
+ self.leaderBoard.showLeaderboard(false, false);
+ });
+ } else {
+ self.$('.o_survey_session_close').click();
+ }
+ });
+ },
+
+ /**
+ * Will start the question timer so that the host may know when the question is done to display
+ * the results and the leaderboard.
+ *
+ * If the question is scored, the timer ending triggers the display of attendees inputs.
+ */
+ _startTimer: function () {
+ var self = this;
+ 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);
+ this.surveyTimerWidget.on('time_up', this, function () {
+ if (self.currentScreen === 'question' && this.isScoredQuestion) {
+ self.$('.o_survey_session_navigation_next').click();
+ }
+ });
+ }
+ },
+
+ /**
+ * Refreshes the question results.
+ *
+ * What we get from this call:
+ * - The 'question statistics' used to display the bar chart when appropriate
+ * - The 'user input lines' that are used to display text/date/datetime answers on the screen
+ * - The number of answers, useful for refreshing the progress bar
+ */
+ _refreshResults: function () {
+ var self = this;
+
+ return this._rpc({
+ route: _.str.sprintf('/survey/session/results/%s', self.surveyAccessToken)
+ }).then(function (questionResults) {
+ if (questionResults) {
+ self.attendeesCount = questionResults.attendees_count;
+
+ if (self.resultsChart && questionResults.question_statistics_graph) {
+ self.resultsChart.updateChart(JSON.parse(questionResults.question_statistics_graph));
+ } else if (self.textAnswers) {
+ self.textAnswers.updateTextAnswers(questionResults.input_line_values);
+ }
+
+ var max = self.attendeesCount > 0 ? self.attendeesCount : 1;
+ var percentage = Math.min(Math.round((questionResults.answer_count / max) * 100), 100);
+ self.$('.progress-bar').css('width', `${percentage}%`);
+
+ if (self.attendeesCount && self.attendeesCount > 0) {
+ var answerCount = Math.min(questionResults.answer_count, self.attendeesCount);
+ self.$('.o_survey_session_answer_count').text(answerCount);
+ self.$('.progress-bar.o_survey_session_progress_small span').text(
+ `${answerCount} / ${self.attendeesCount}`
+ );
+ }
+ }
+
+ return Promise.resolve();
+ }, function () {
+ // on failure, stop refreshing
+ clearInterval(self.resultsRefreshInterval);
+ delete self.resultsRefreshInterval;
+ });
+ },
+
+ /**
+ * We refresh the attendees count every 2 seconds while the user is on the start screen.
+ *
+ */
+ _refreshAttendeesCount: function () {
+ var self = this;
+
+ return self._rpc({
+ model: 'survey.survey',
+ method: 'read',
+ args: [[self.surveyId], ['session_answer_count']],
+ }).then(function (result) {
+ if (result && result.length === 1){
+ self.$('.o_survey_session_attendees_count').text(
+ result[0].session_answer_count
+ );
+ }
+ }, function () {
+ // on failure, stop refreshing
+ clearInterval(self.attendeesRefreshInterval);
+ });
+ },
+
+ /**
+ * For simple/multiple choice questions, we display a bar chart with:
+ *
+ * - answers of attendees
+ * - correct / incorrect answers when relevant
+ *
+ * see SurveySessionChart widget doc for more information.
+ *
+ */
+ _setupChart: function () {
+ if (this.resultsChart) {
+ this.resultsChart.setElement(null);
+ this.resultsChart.destroy();
+ delete this.resultsChart;
+ }
+
+ if (!this.isStartScreen && this.showBarChart) {
+ this.resultsChart = new SurveySessionChart(this, {
+ questionType: this.$el.data('questionType'),
+ answersValidity: this.$el.data('answersValidity'),
+ hasCorrectAnswers: this.hasCorrectAnswers,
+ questionStatistics: this.$el.data('questionStatistics'),
+ showInputs: this.showInputs
+ });
+
+ return this.resultsChart.attachTo(this.$('.o_survey_session_chart'));
+ } else {
+ return Promise.resolve();
+ }
+ },
+
+ /**
+ * Leaderboard of all the attendees based on their score.
+ * see SurveySessionLeaderBoard widget doc for more information.
+ *
+ */
+ _setupLeaderboard: function () {
+ if (this.leaderBoard) {
+ this.leaderBoard.setElement(null);
+ this.leaderBoard.destroy();
+ delete this.leaderBoard;
+ }
+
+ if (this.isScoredQuestion || this.isLastQuestion) {
+ this.leaderBoard = new SurveySessionLeaderBoard(this, {
+ surveyAccessToken: this.surveyAccessToken,
+ sessionResults: this.$('.o_survey_session_results')
+ });
+
+ return this.leaderBoard.attachTo(this.$('.o_survey_session_leaderboard'));
+ } else {
+ return Promise.resolve();
+ }
+ },
+
+ /**
+ * Shows attendees answers for char_box/date and datetime questions.
+ * see SurveySessionTextAnswers widget doc for more information.
+ *
+ */
+ _setupTextAnswers: function () {
+ if (this.textAnswers) {
+ this.textAnswers.setElement(null);
+ this.textAnswers.destroy();
+ delete this.textAnswers;
+ }
+
+ if (!this.isStartScreen && this.showTextAnswers) {
+ this.textAnswers = new SurveySessionTextAnswers(this, {
+ questionType: this.$el.data('questionType')
+ });
+
+ return this.textAnswers.attachTo(this.$('.o_survey_session_text_answers_container'));
+ } else {
+ return Promise.resolve();
+ }
+ },
+
+ /**
+ * Setup the 2 refresh intervals of 2 seconds for our widget:
+ * - The refresh of attendees count (only on the start screen)
+ * - The refresh of results (used for chart/text answers/progress bar)
+ */
+ _setupIntervals: function () {
+ this.attendeesCount = this.$el.data('attendeesCount') ? this.$el.data('attendeesCount') : 0;
+
+ if (this.isStartScreen) {
+ this.attendeesRefreshInterval = setInterval(this._refreshAttendeesCount.bind(this), 2000);
+ } else {
+ if (this.attendeesRefreshInterval) {
+ clearInterval(this.attendeesRefreshInterval);
+ }
+
+ if (!this.resultsRefreshInterval) {
+ this.resultsRefreshInterval = setInterval(this._refreshResults.bind(this), 2000);
+ }
+ }
+ },
+
+ /**
+ * Setup current screen based on question properties.
+ * If it's a non-scored question with a chart, we directly display the user inputs.
+ */
+ _setupCurrentScreen: function () {
+ if (this.isStartScreen) {
+ this.currentScreen = 'startScreen';
+ } else if (!this.isScoredQuestion && this.showBarChart) {
+ this.currentScreen = 'userInputs';
+ } else {
+ this.currentScreen = 'question';
+ }
+
+ this._setShowInputs(this.currentScreen === 'userInputs');
+ },
+
+ /**
+ * When we go from the 'question' screen to the 'userInputs' screen, we toggle this boolean
+ * and send the information to the chart.
+ * The chart will show attendees survey.user_input.lines.
+ *
+ * @param {Boolean} showInputs
+ */
+ _setShowInputs(showInputs) {
+ this.showInputs = showInputs;
+
+ if (this.resultsChart) {
+ this.resultsChart.setShowInputs(showInputs);
+ this.resultsChart.updateChart();
+ }
+ },
+
+ /**
+ * When we go from the 'userInputs' screen to the 'results' screen, we toggle this boolean
+ * and send the information to the chart.
+ * The chart will show the question survey.question.answers.
+ * (Only used for simple / multiple choice questions).
+ *
+ * @param {Boolean} showAnswers
+ */
+ _setShowAnswers(showAnswers) {
+ this.showAnswers = showAnswers;
+
+ if (this.resultsChart) {
+ this.resultsChart.setShowAnswers(showAnswers);
+ this.resultsChart.updateChart();
+ }
+ }
+});
+
+return publicWidget.registry.SurveySessionManage;
+
+});