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/website_event_track_quiz/static/src | |
| parent | 0a15094050bfde69a06d6eff798e9a8ddf2b8c21 (diff) | |
initial commit 2
Diffstat (limited to 'addons/website_event_track_quiz/static/src')
4 files changed, 516 insertions, 0 deletions
diff --git a/addons/website_event_track_quiz/static/src/js/event_quiz.js b/addons/website_event_track_quiz/static/src/js/event_quiz.js new file mode 100644 index 00000000..c6adddc3 --- /dev/null +++ b/addons/website_event_track_quiz/static/src/js/event_quiz.js @@ -0,0 +1,323 @@ +odoo.define('website_event_track_quiz.event.quiz', function (require) { + +'use strict'; + +var publicWidget = require('web.public.widget'); +var core = require('web.core'); +var session = require('web.session'); +var utils = require('web.utils'); + +var QWeb = core.qweb; +var _t = core._t; + +/** + * This widget is responsible of displaying quiz questions and propositions. Submitting the quiz will fetch the + * correction and decorate the answers according to the result. Error message can be displayed. + * + * This widget can be attached to DOM rendered server-side by `gamification_quiz.` + * + */ +var Quiz = publicWidget.Widget.extend({ + template: 'quiz.main', + xmlDependencies: ['/website_event_track_quiz/static/src/xml/quiz_templates.xml'], + events: { + "click .o_quiz_quiz_answer": '_onAnswerClick', + "click .o_quiz_js_quiz_submit": '_submitQuiz', + "click .o_quiz_js_quiz_reset": '_onClickReset', + }, + + /** + * @override + * @param {Object} parent + * @param {Object} data holding all the container information + * @param {Object} quizData : quiz data to display + */ + init: function (parent, data, quizData) { + this._super.apply(this, arguments); + this.track = _.defaults(data, { + id: 0, + name: '', + eventId: '', + completed: false, + isMember: false, + progressBar: false, + isManager: false + }); + this.quiz = quizData || false; + if (this.quiz) { + this.quiz.questionsCount = quizData.questions.length; + } + this.isMember = data.isMember || false; + this.userId = session.user_id; + this.redirectURL = encodeURIComponent(document.URL); + }, + + /** + * @override + */ + willStart: function () { + var defs = [this._super.apply(this, arguments)]; + if (!this.quiz) { + defs.push(this._fetchQuiz()); + } + return Promise.all(defs); + }, + + /** + * Overridden to add custom rendering behavior upon start of the widget. + * + * If the user has answered the quiz before having joined the course, we check + * his answers (saved into his session) here as well. + * + * @override + */ + start: function () { + var self = this; + return this._super.apply(this, arguments).then(function () { + self._renderValidationInfo(); + }); + }, + + //-------------------------------------------------------------------------- + // Private + //-------------------------------------------------------------------------- + + _alertShow: function (alertCode) { + var message = _t('There was an error validating this quiz.'); + if (alertCode === 'quiz_incomplete') { + message = _t('All questions must be answered !'); + } else if (alertCode === 'quiz_done') { + message = _t('This quiz is already done. Retaking it is not possible.'); + } + + this.displayNotification({ + type: 'warning', + title: _t('Quiz validation error'), + message: message, + sticky: true + }); + }, + + /** + * Get all the questions ID from the displayed Quiz + * @returns {Array} + * @private + */ + _getQuestionsIds: function () { + return this.$('.o_quiz_js_quiz_question').map(function () { + return $(this).data('question-id'); + }).get(); + }, + + /** + * @private + * Decorate the answers according to state + */ + _disableAnswers: function () { + var self = this; + this.$('.o_quiz_js_quiz_question').addClass('completed-disabled'); + this.$('input[type=radio]').each(function () { + $(this).prop('disabled', self.track.completed); + }); + }, + + /** + * Decorate the answer inputs according to the correction and adds the answer comment if + * any. + * + * @private + */ + _renderAnswersHighlightingAndComments: function () { + var self = this; + this.$('.o_quiz_js_quiz_question').each(function () { + var $question = $(this); + var questionId = $question.data('questionId'); + var isCorrect = self.quiz.answers[questionId].is_correct; + $question.find('a.o_quiz_quiz_answer').each(function () { + var $answer = $(this); + $answer.find('i.fa').addClass('d-none'); + if ($answer.find('input[type=radio]')[0].checked) { + if (isCorrect) { + $answer.removeClass('list-group-item-danger').addClass('list-group-item-success'); + $answer.find('i.fa-check-circle').removeClass('d-none'); + } else { + $answer.removeClass('list-group-item-success').addClass('list-group-item-danger'); + $answer.find('i.fa-times-circle').removeClass('d-none'); + $answer.find('label input').prop('checked', false); + } + } else { + $answer.removeClass('list-group-item-danger list-group-item-success'); + $answer.find('i.fa-circle').removeClass('d-none'); + } + }); + var comment = self.quiz.answers[questionId].comment; + if (comment) { + $question.find('.o_quiz_quiz_answer_info').removeClass('d-none'); + $question.find('.o_quiz_quiz_answer_comment').text(comment); + } + }); + }, + + /* + * @private + * Update validation box (karma, buttons) according to widget state + */ + _renderValidationInfo: function () { + var $validationElem = this.$('.o_quiz_js_quiz_validation'); + $validationElem.html( + QWeb.render('quiz.validation', {'widget': this}) + ); + }, + + /** + * Get the quiz answers filled in by the User + * + * @private + */ + _getQuizAnswers: function () { + return this.$('input[type=radio]:checked').map(function (index, element) { + return parseInt($(element).val()); + }).get(); + }, + + /** + * Submit a quiz and get the correction. It will display messages + * according to quiz result. + * + * @private + */ + _submitQuiz: function () { + var self = this; + + return this._rpc({ + route: '/event_track/quiz/submit', + params: { + event_id: self.track.eventId, + track_id: self.track.id, + answer_ids: this._getQuizAnswers(), + } + }).then(function (data) { + if (data.error) { + self._alertShow(data.error); + } else { + self.quiz = _.extend(self.quiz, data); + self.quiz.quizPointsGained = data.quiz_points; + if (data.quiz_completed) { + self._disableAnswers(); + self.track.completed = data.quiz_completed; + } + self._renderAnswersHighlightingAndComments(); + self._renderValidationInfo(); + if (data.visitor_uuid) { + utils.set_cookie('visitor_uuid', data.visitor_uuid); + } + } + + return Promise.resolve(data); + }); + }, + + //-------------------------------------------------------------------------- + // Handlers + //-------------------------------------------------------------------------- + + /** + * When clicking on an answer, this one should be marked as "checked". + * + * @private + * @param OdooEvent ev + */ + _onAnswerClick: function (ev) { + ev.preventDefault(); + if (!this.track.completed) { + $(ev.currentTarget).find('input[type=radio]').prop('checked', true); + } + }, + + /** + * Resets the completion of the track so the user can take + * the quiz again + * + * @private + */ + _onClickReset: function () { + this._rpc({ + route: '/event_track/quiz/reset', + params: { + event_id: this.track.eventId, + track_id: this.track.id + } + }).then(function () { + window.location.reload(); + }); + }, + +}); + +publicWidget.registry.Quiz = publicWidget.Widget.extend({ + selector: '.o_quiz_main', + + //---------------------------------------------------------------------- + // Public + //---------------------------------------------------------------------- + + /** + * @override + * @param {Object} parent + */ + start: function () { + var self = this; + this.quizWidgets = []; + var defs = [this._super.apply(this, arguments)]; + this.$('.o_quiz_js_quiz').each(function () { + var data = $(this).data(); + data.quizData = { + questions: self._extractQuestionsAndAnswers(), + sessionAnswers: data.sessionAnswers || [], + quizKarmaMax: data.quizKarmaMax, + quizKarmaWon: data.quizKarmaWon, + quizKarmaGain: data.quizKarmaGain, + quizPointsGained: data.quizPointsGained, + quizAttemptsCount: data.quizAttemptsCount, + }; + defs.push(new Quiz(self, data, data.quizData).attachTo($(this))); + }); + return Promise.all(defs); + }, + + //---------------------------------------------------------------------- + // Private + //--------------------------------------------------------------------- + + /** + * Extract data from exiting DOM rendered server-side, to have the list of questions with their + * relative answers. + * This method should return the same format as /gamification_quiz/quiz/get controller. + * + * @return {Array<Object>} list of questions with answers + */ + _extractQuestionsAndAnswers: function () { + var questions = []; + this.$('.o_quiz_js_quiz_question').each(function () { + var $question = $(this); + var answers = []; + $question.find('.o_quiz_quiz_answer').each(function () { + var $answer = $(this); + answers.push({ + id: $answer.data('answerId'), + text: $answer.data('text'), + }); + }); + questions.push({ + id: $question.data('questionId'), + title: $question.data('title'), + answer_ids: answers, + }); + }); + return questions; + }, +}); + +return Quiz; + +}); diff --git a/addons/website_event_track_quiz/static/src/js/event_quiz_leaderboard.js b/addons/website_event_track_quiz/static/src/js/event_quiz_leaderboard.js new file mode 100644 index 00000000..f624a852 --- /dev/null +++ b/addons/website_event_track_quiz/static/src/js/event_quiz_leaderboard.js @@ -0,0 +1,34 @@ +odoo.define('website_event_track_quiz.event_leaderboard', function (require) { + +'use strict'; + +var publicWidget = require('web.public.widget'); + +publicWidget.registry.EventLeaderboard = publicWidget.Widget.extend({ + selector: '.o_wevent_quiz_leaderboard', + + /** + * Basic override to scroll to current visitor's position. + */ + start: function () { + var self = this; + return this._super(...arguments).then(function () { + var $scrollTo = self.$('.o_wevent_quiz_scroll_to'); + if ($scrollTo.length !== 0) { + var offset = $('.o_header_standard').height(); + var $appMenu = $('.o_main_navbar'); + if ($appMenu.length !== 0) { + offset += $appMenu.height(); + } + window.scrollTo({ + top: $scrollTo.offset().top - offset, + behavior: 'smooth' + }); + } + }); + } +}); + +return publicWidget.registry.EventLeaderboard; + +}); diff --git a/addons/website_event_track_quiz/static/src/scss/event_quiz.scss b/addons/website_event_track_quiz/static/src/scss/event_quiz.scss new file mode 100644 index 00000000..ca441d4d --- /dev/null +++ b/addons/website_event_track_quiz/static/src/scss/event_quiz.scss @@ -0,0 +1,92 @@ +// Retrive the tab's height by summ its properties +$o-wslides-tabs-height: ($nav-link-padding-y*2) + ($font-size-base * $line-height-base); + +// Overal page bg-color: Blend it 'over' the color chosen by the user +// ($body-bg), rather than force it replacing the variable's value. +$o-wslides-color-bg: mix($body-bg, #efeff4); + +$o-wslides-color-dark1: #47525f; +$o-wslides-color-dark2: #1f262d; +$o-wslides-color-dark3: #101216; +$o-wslides-fs-side-width: 300px; + + +// Common to new slides pages +// ************************************************** +.o_quiz_gradient { + background-image: linear-gradient(120deg, #875A7B, darken(#875A7B, 10%)); +} + +.o_quiz_main { + background-color: $o-wslides-color-bg; + + .o_wslides_home_nav { + top: -40px; + + @include media-breakpoint-up(lg) { + font-size: 1rem; + + .o_wslides_nav_navbar_right { + padding-left: $spacer; + margin-left: auto; + border-left: 1px solid $border-color; + } + } + } + + .o_quiz_js_quiz { + i.o_quiz_js_quiz_icon { + cursor: pointer; + } + + i.o_quiz_js_quiz_icon:hover { + color: black !important; + } + } + + .o_quiz_js_quiz_question { + .list-group-item { + font-size: 1rem; + + input:checked + i.fa-circle { + color: $primary !important; + } + } + + &.disabled { + opacity: 0.5; + pointer-events: none; + } + + &.completed-disabled{ + pointer-events: none; + } + } + + a.o_quiz_js_quiz_is_correct { + color: black; + input:checked + i.fa-check-circle-o { + color: $primary !important; + } + } + + .o_quiz_js_quiz_sequence_highlight { + background-color: #1252F3; + height: 1px; + z-index: 3; + + &:before, &:after { + content: ""; + @include size(6px); + display: block; + border-radius: 100%; + background-color: inherit; + @include o-position-absolute(-2px, -2px); + } + + &:after { + right: auto; + left: -2px; + } + } +} diff --git a/addons/website_event_track_quiz/static/src/xml/quiz_templates.xml b/addons/website_event_track_quiz/static/src/xml/quiz_templates.xml new file mode 100644 index 00000000..fe800c7f --- /dev/null +++ b/addons/website_event_track_quiz/static/src/xml/quiz_templates.xml @@ -0,0 +1,67 @@ +<?xml version="1.0" encoding="UTF-8"?> +<templates xml:space="preserve"> + <t t-name="quiz.main"> + <div class="h-100 w-100 overflow-auto px-2 py-2"> + <div class="container"> + <div t-foreach="widget.quiz.questions" t-as="question" + t-attf-class="o_quiz_js_quiz_question mt-3 mb-4 #{widget.track.completed ? 'completed-disabled' : ''}" + t-att-data-question-id="question.id" t-att-data-title="question.question"> + <div class="h4"> + <small class="text-muted"><span t-esc="question_index+1"/>. </small> <span t-esc="question.question"/> + </div> + <div class="list-group"> + <t t-foreach="question.answer_ids" t-as="answer"> + <a t-att-data-answer-id="answer.id" href="#" + t-att-data-text="answer.text_value" + t-attf-class="o_quiz_quiz_answer list-group-item d-flex align-items-center list-group-item-action #{widget.track.completed && answer.is_correct ? 'list-group-item-success' : '' }"> + + <label class="my-0 d-flex align-items-center justify-content-center mr-2"> + <input type="radio" + t-att-name="question.id" + t-att-value="answer.id" + class="d-none"/> + <i t-att-class="'fa fa-circle text-400' + (!(widget.track.completed && answer.is_correct) ? '' : ' d-none')"></i> + <i class="fa fa-times-circle text-danger d-none"></i> + <i t-att-class="'fa fa-check-circle text-success' + (widget.track.completed && answer.is_correct ? '' : ' d-none')"></i> + </label> + <span t-esc="answer.text_value"/> + </a> + </t> + <div class="o_quiz_quiz_answer_info list-group-item list-group-item-info d-none"> + <i class="fa fa-info-circle"/> + <span class="o_quiz_quiz_answer_comment"/> + </div> + </div> + </div> + <div t-if="!widget.track.completed" class="o_quiz_js_quiz_validation border-top pt-3"/> + <div t-else="" class="row"> + <div class="o_quiz_js_quiz_validation col py-2 bg-100 mb-2 border-bottom"/> + </div> + </div> + </div> + </t> + + <t t-name="quiz.validation"> + <div id="validation"> + <div class="d-flex align-items-center justify-content-between"> + <div t-att-class="'d-flex align-items-center' + (widget.track.completed ? ' alert alert-success my-0 py-1 px-3' : '')"> + <button t-if="!widget.track.completed" role="button" title="Check answers" aria-label="Check answers" + class="btn btn-primary text-uppercase font-weight-bold o_quiz_js_quiz_submit">Check your answers</button> + <b t-else="" class="my-0 h5">Done !</b> + <span class="my-0 h5" style="line-height: 1"> + <span t-if="widget.track.completed" role="button" title="Succeed and gain karma" aria-label="Succeed and gain karma" class="badge badge-pill badge-warning text-white font-weight-bold ml-3 px-2"> + + <t t-esc="widget.quiz.quizPointsGained || 0"/> Points + </span> + </span> + </div> + <div class="flex-grow-1 text-right"> + <button t-if="widget.track.isManager" + class="d-none d-md-inline-block btn btn-light border o_quiz_js_quiz_reset"> + Reset + </button> + </div> + </div> + </div> + </t> + +</templates> |
