diff options
| author | stephanchrst <stephanchrst@gmail.com> | 2022-05-10 21:51:50 +0700 |
|---|---|---|
| committer | stephanchrst <stephanchrst@gmail.com> | 2022-05-10 21:51:50 +0700 |
| commit | 3751379f1e9a4c215fb6eb898b4ccc67659b9ace (patch) | |
| tree | a44932296ef4a9b71d5f010906253d8c53727726 /addons/survey/static/src/js/survey_session_manage.js | |
| parent | 0a15094050bfde69a06d6eff798e9a8ddf2b8c21 (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.js | 588 |
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; + +}); |
