summaryrefslogtreecommitdiff
path: root/addons/website_event_track_quiz/static/src
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/website_event_track_quiz/static/src
parent0a15094050bfde69a06d6eff798e9a8ddf2b8c21 (diff)
initial commit 2
Diffstat (limited to 'addons/website_event_track_quiz/static/src')
-rw-r--r--addons/website_event_track_quiz/static/src/js/event_quiz.js323
-rw-r--r--addons/website_event_track_quiz/static/src/js/event_quiz_leaderboard.js34
-rw-r--r--addons/website_event_track_quiz/static/src/scss/event_quiz.scss92
-rw-r--r--addons/website_event_track_quiz/static/src/xml/quiz_templates.xml67
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 &amp;&amp; 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 &amp;&amp; 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 &amp;&amp; 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>