diff --git a/addons/website_slides/controllers/main.py b/addons/website_slides/controllers/main.py index 5b915c582f37a2d920657664802f17fc6bf5ab51..bdfb0d49f8854cf73477102e2f99c6438e464360 100644 --- a/addons/website_slides/controllers/main.py +++ b/addons/website_slides/controllers/main.py @@ -51,7 +51,7 @@ class WebsiteSlides(WebsiteProfile): return {'error': 'slide_access'} return {'slide': slide} - def _set_viewed_slide(self, slide): + def _set_viewed_slide(self, slide, quiz_attempts_inc=False): if request.env.user._is_public() or not slide.website_published or not slide.channel_id.is_member: viewed_slides = request.session.setdefault('viewed_slides', list()) if slide.id not in viewed_slides: @@ -59,12 +59,12 @@ class WebsiteSlides(WebsiteProfile): viewed_slides.append(slide.id) request.session['viewed_slides'] = viewed_slides else: - slide.action_set_viewed() + slide.action_set_viewed(quiz_attempts_inc=quiz_attempts_inc) return True - def _set_completed_slide(self, slide): + def _set_completed_slide(self, slide, quiz_attempts_inc=False): if slide.website_published and slide.channel_id.is_member: - slide.action_set_completed() + slide.action_set_completed(quiz_attempts_inc=quiz_attempts_inc) return True def _get_slide_detail(self, slide): @@ -104,11 +104,29 @@ class WebsiteSlides(WebsiteProfile): 'message_post_pid': request.env.user.partner_id.id, }) + if slide.question_ids: + values.update(self._get_slide_quiz_info(slide)) + return values - def _get_quiz_points(self, slide, attempt_count): - possible_points = [slide.quiz_first_attempt_reward,slide.quiz_second_attempt_reward,slide.quiz_third_attempt_reward, slide.quiz_fourth_attempt_reward] - return possible_points[attempt_count] if attempt_count < len(possible_points) else possible_points[-1] + def _get_slide_quiz_info(self, slide, quiz_done=False): + gains = [slide.quiz_first_attempt_reward, + slide.quiz_second_attempt_reward, + slide.quiz_third_attempt_reward, + slide.quiz_fourth_attempt_reward] + result = { + 'quiz_karma_max': gains[0], # what could be gained if succeed at first try + 'quiz_karma_gain': gains[0], # what would be gained at next test + 'quiz_karma_won': 0, # what has been gained + 'quiz_attempts_count': 0, # number of attempts + } + if slide.user_membership_id: + if slide.user_membership_id.quiz_attempts_count: + result['quiz_karma_gain'] = gains[slide.user_membership_id.quiz_attempts_count] if slide.user_membership_id.quiz_attempts_count <= len(gains) else gains[-1] + result['quiz_attempts_count'] = slide.user_membership_id.quiz_attempts_count + if quiz_done or slide.user_membership_id.completed: + result['quiz_karma_won'] = gains[slide.user_membership_id.quiz_attempts_count-1] if slide.user_membership_id.quiz_attempts_count < len(gains) else gains[-1] + return result # CHANNEL UTILITIES # -------------------------------------------------- @@ -513,6 +531,7 @@ class WebsiteSlides(WebsiteProfile): self._set_viewed_slide(slide) values = self._get_slide_detail(slide) + values['channel_progress'] = self._get_channel_progress(slide.channel_id) if 'fullscreen' in kwargs: return request.render("website_slides.slide_fullscreen", values) @@ -569,9 +588,10 @@ class WebsiteSlides(WebsiteProfile): } @http.route('/slides/slide/<model("slide.slide"):slide>/set_completed', website=True, type="http", auth="user") - def slide_set_completed_and_redirect(self, slide, next_slide=None): + def slide_set_completed_and_redirect(self, slide, next_slide_id=None): self._set_completed_slide(slide) - return werkzeug.utils.redirect("/slides/slide/%s" % (next_slide if next_slide else slide.id)) + next_slide = request.env['slide.slide'].browse(next_slide_id) if next_slide_id else None + return werkzeug.utils.redirect("/slides/slide/%s" % (slug(next_slide) if next_slide else slug(slide))) @http.route('/slides/slide/set_completed', website=True, type="json", auth="public") def slide_set_completed(self, slide_id): @@ -624,68 +644,70 @@ class WebsiteSlides(WebsiteProfile): # QUIZZ SECTION # -------------------------------------------------- - @http.route('/slide/quiz/get', type="json", auth="public", website=True) - def get_quiz(self, **kw): - if 'slide_id' in kw: - slide = request.env['slide.slide'].browse(kw['slide_id']) - slide_partner = request.env['slide.slide.partner'].search([('slide_id', '=', slide.id), ('partner_id', '=', request.env.user.partner_id.id)]) - possible_points = [slide.quiz_first_attempt_reward,slide.quiz_second_attempt_reward,slide.quiz_third_attempt_reward, slide.quiz_fourth_attempt_reward] - points = 0 - if slide_partner.quiz_attempts_count < len(possible_points): - points = possible_points[slide_partner.quiz_attempts_count] - else: - points = possible_points[len(possible_points)-1] - res = { - 'questions':[ - {'title': question.question, - 'id': question.id, - 'answers': [{'text': answer.text_value, 'correct':answer.is_correct,'id': answer.id} for answer in question.answer_ids] - } for question in slide.question_ids - ], - 'nb_attempts': slide_partner.quiz_attempts_count if slide_partner else 0, - 'possible_rewards': possible_points, - 'reward': points - } - return res - - @http.route('/slide/quiz/submit', type="json", auth="user", website=True) - def submit_quiz(self, slide_id, answer_ids,**kw): - slide = request.env['slide.slide'].browse(slide_id) - good_answers = request.env['slide.answer'].search([('id', 'in', answer_ids), ('is_correct', '=', True)]) - bad_answers = request.env['slide.answer'].browse(answer_ids) - good_answers - slide_partner = request.env['slide.slide.partner'].search([('slide_id', '=', slide_id), ('partner_id', '=', request.env.user.partner_id.id)]) - points = 0 - if not slide_partner: - slide.action_set_viewed() - if not slide_partner.completed: - points = self._get_quiz_points(slide, slide_partner.quiz_attempts_count) - slide_partner.sudo().write({ - 'quiz_attempts_count': slide_partner.quiz_attempts_count if not bad_answers else slide_partner.quiz_attempts_count + 1, - 'points_won': points if not bad_answers else 0, - 'completed': not bad_answers - }) - user = {} - if not bad_answers: - request.env.user.sudo().add_karma(points) - lower_bound = request.env.user.rank_id.karma_min - upper_bound = request.env.user.next_rank_id.karma_min - user= { - 'lower_bound': lower_bound, - 'upper_bound': upper_bound, - 'karma': request.env.user.karma, - 'progress_bar_percentage': 100 * ((request.env.user.karma - lower_bound) / (upper_bound - lower_bound)) - } - return { - 'goodAnswers': [good_answer.id for good_answer in good_answers], - 'badAnswers': [bad_answer.id for bad_answer in bad_answers], - 'passed': not bad_answers, - 'points': points if not bad_answers else 0, - 'attempts_count': slide_partner.quiz_attempts_count if slide_partner else 0, - 'channel_completion': slide.channel_id.completion, - 'user': user + @http.route('/slides/slide/quiz/get', type="json", auth="public", website=True) + def slide_quiz_get(self, slide_id): + fetch_res = self._fetch_slide(slide_id) + if fetch_res.get('error'): + return fetch_res + slide = fetch_res['slide'] + quiz_info = self._get_slide_quiz_info(slide) + return { + 'questions': [{ + 'id': question.id, + 'question': question.question, + 'answers': [{ + 'id': answer.id, + 'text_value': answer.text_value, + 'is_correct': answer.is_correct, + } for answer in question.answer_ids], + } for question in slide.question_ids], + 'quizAttemptsCount': quiz_info['quiz_attempts_count'], + 'quizKarmaGain': quiz_info['quiz_karma_gain'], + 'quizKarmaWon': quiz_info['quiz_karma_won'], + } + + @http.route('/slides/slide/quiz/submit', type="json", auth="user", website=True) + def slide_quiz_submit(self, slide_id, answer_ids): + fetch_res = self._fetch_slide(slide_id) + if fetch_res.get('error'): + return fetch_res + slide = fetch_res['slide'] + + if slide.user_membership_id.completed: + return {'error': 'slide_quiz_done'} + + all_questions = request.env['slide.question'].sudo().search([('slide_id', '=', slide.id)]) + + user_answers = request.env['slide.answer'].sudo().search([('id', 'in', answer_ids)]) + if user_answers.mapped('question_id') != all_questions: + return {'error': 'slide_quiz_incomplete'} + + user_bad_answers = user_answers.filtered(lambda answer: not answer.is_correct) + user_good_answers = user_answers - user_bad_answers + + self._set_viewed_slide(slide, quiz_attempts_inc=True) + quiz_info = self._get_slide_quiz_info(slide, quiz_done=True) + + rank_progress = {} + if not user_bad_answers: + slide._action_set_quiz_done() + lower_bound = request.env.user.rank_id.karma_min + upper_bound = request.env.user.next_rank_id.karma_min + rank_progress = { + 'lowerBound': lower_bound, + 'upperBound': upper_bound, + 'currentKarma': request.env.user.karma, + 'motivational': request.env.user.next_rank_id.description_motivational, + 'progress': 100 * ((request.env.user.karma - lower_bound) / (upper_bound - lower_bound)) } return { - 'error': "You already passed this quiz" + 'goodAnswers': user_good_answers.ids, + 'badAnswers': user_bad_answers.ids, + 'completed': not user_bad_answers, + 'quizKarmaWon': quiz_info['quiz_karma_won'], + 'quizKarmaGain': quiz_info['quiz_karma_gain'], + 'quizAttemptsCount': quiz_info['quiz_attempts_count'], + 'rankProgress': rank_progress } # -------------------------------------------------- diff --git a/addons/website_slides/models/slide_slide.py b/addons/website_slides/models/slide_slide.py index 6f2bb6feac5aa20e6513f2481918fd7effb991fa..5990a6dc30ef2cbacbdb84cad01b47c01cbbaf01 100644 --- a/addons/website_slides/models/slide_slide.py +++ b/addons/website_slides/models/slide_slide.py @@ -427,52 +427,83 @@ class Slide(models.Model): }) self.env.user.add_karma(new_slide.channel_id.karma_gen_slide_vote) - def action_set_viewed(self): + def action_set_viewed(self, quiz_attempts_inc=False): if not all(slide.channel_id.is_member for slide in self): raise UserError(_('You cannot mark a slide as viewed if you are not among its members.')) - return bool(self._action_set_viewed(self.env.user.partner_id)) + return bool(self._action_set_viewed(self.env.user.partner_id, quiz_attempts_inc=quiz_attempts_inc)) - def _action_set_viewed(self, target_partner): + def _action_set_viewed(self, target_partner, quiz_attempts_inc=False): self_sudo = self.sudo() SlidePartnerSudo = self.env['slide.slide.partner'].sudo() existing_sudo = SlidePartnerSudo.search([ ('slide_id', 'in', self.ids), ('partner_id', '=', target_partner.id) ]) + if quiz_attempts_inc: + for exsting_slide in existing_sudo: + exsting_slide.write({ + 'quiz_attempts_count': exsting_slide.quiz_attempts_count + 1 + }) new_slides = self_sudo - existing_sudo.mapped('slide_id') return SlidePartnerSudo.create([{ 'slide_id': new_slide.id, 'channel_id': new_slide.channel_id.id, 'partner_id': target_partner.id, + 'quiz_attempts_count': 1 if quiz_attempts_inc else 0, 'vote': 0} for new_slide in new_slides]) - def action_set_completed(self): + def action_set_completed(self, quiz_attempts_inc=False): if not all(slide.channel_id.is_member for slide in self): raise UserError(_('You cannot mark a slide as completed if you are not among its members.')) - return self._action_set_completed(self.env.user.partner_id) + return self._action_set_completed(self.env.user.partner_id, quiz_attempts_inc=quiz_attempts_inc) - def _action_set_completed(self, target_partner): + def _action_set_completed(self, target_partner, quiz_attempts_inc=False): self_sudo = self.sudo() SlidePartnerSudo = self.env['slide.slide.partner'].sudo() existing_sudo = SlidePartnerSudo.search([ ('slide_id', 'in', self.ids), ('partner_id', '=', target_partner.id) ]) - existing_sudo.write({'completed': True}) + if quiz_attempts_inc: + for existing_slide in existing_sudo: + existing_slide.write({ + 'completed': True, + 'quiz_attempts_count': existing_slide.quiz_attempts_count + 1 + }) + else: + existing_sudo.write({'completed': True}) new_slides = self_sudo - existing_sudo.mapped('slide_id') SlidePartnerSudo.create([{ 'slide_id': new_slide.id, 'channel_id': new_slide.channel_id.id, 'partner_id': target_partner.id, + 'quiz_attempts_count': 1 if quiz_attempts_inc else 0, 'vote': 0, 'completed': True} for new_slide in new_slides]) return True + def _action_set_quiz_done(self): + if not all(slide.channel_id.is_member for slide in self): + raise UserError(_('You cannot mark a slide quiz as completed if you are not among its members.')) + + points = 0 + for slide in self: + if not slide.user_membership_id or slide.user_membership_id.completed or not slide.user_membership_id.quiz_attempts_count: + continue + + gains = [slide.quiz_first_attempt_reward, + slide.quiz_second_attempt_reward, + slide.quiz_third_attempt_reward, + slide.quiz_fourth_attempt_reward] + points += gains[slide.user_membership_id.quiz_attempts_count-1] if slide.user_membership_id.quiz_attempts_count <= len(gains) else gains[-1] + + return self.env.user.sudo().add_karma(points) + # -------------------------------------------------- # Parsing methods # -------------------------------------------------- diff --git a/addons/website_slides/static/src/js/slides_course_fullscreen_player.js b/addons/website_slides/static/src/js/slides_course_fullscreen_player.js index df8e57bf481995a6964e4c9a0f1a49a8c9e62c71..b7165e92d4de9676da94043b37de89e97487fd48 100644 --- a/addons/website_slides/static/src/js/slides_course_fullscreen_player.js +++ b/addons/website_slides/static/src/js/slides_course_fullscreen_player.js @@ -17,6 +17,10 @@ odoo.define('website_slides.fullscreen', function (require) { var Fullscreen = Widget.extend({ + custom_events: { + quiz_next_slide: '_goToNextSlide', + quiz_completed: '_onQuizCompleted' + }, /** * @override * @param {Object} el @@ -36,11 +40,11 @@ odoo.define('website_slides.fullscreen', function (require) { this.activetab = undefined; this.player = undefined; this.goToQuiz = false; - this.answeredQuestions = []; this.slideTitle = undefined; return this._super.apply(this,arguments); }, start: function (){ + var self = this; this.url = window.location.pathname; this.urlToSmallScreen = this.url.replace('/fullscreen',''); this._getSlides(); @@ -58,46 +62,50 @@ odoo.define('website_slides.fullscreen', function (require) { */ _renderPlayer: function (){ var self = this; - var embed_url; - if (self.slide.slide_type !== 'webpage' || self.slide.htmlContent){ - if ((self.slide.slide_type === "quiz" || self.slide.has_quiz) && !self.slide.quiz){ - self._fetchQuiz(); - } else { - embed_url = $(this.slide.embed_code).attr('src'); - if (self.slide.slide_type === "video"){ + + var def = $.Deferred(); + if (this.slide.slide_type === 'webpage') { + this._fetchHtmlContent().then(function () { + self._renderWebpage(); + def.resolve(); + }); + } else { + if (this.slide.slide_type !== 'quiz') { + // no RPC, get slide data from existing DOM + var embed_url = $(this.slide.embed_code).attr('src'); + if (this.slide.slide_type === "video"){ embed_url = "https://" + embed_url + "&rel=0&autoplay=1&enablejsapi=1&origin=" + window.location.origin; } $('.o_wslides_fs_player').html(QWeb.render('website.slides.fullscreen', { slide: self.slide, nextSlide: self.nextSlide, questions: self.slide.quiz ? self.slide.quiz.questions: '', - reward: self.slide.quiz ? self.slide.quiz.nb_attempts < 3 ? self.slide.quiz.possible_rewards[self.slide.quiz.nb_attempts] : self.slide.quiz.possible_rewards[3]: self.slide.maxPoints, embed_url: embed_url, question_count: self.slide.quiz ? self.slide.quiz.questions.length : '', letters: self.slide.quiz ? self.letters : '', showMiniQuiz: self.goToQuiz })); - if (self.slide.slide_type === "video"){ - self._renderYoutubeIframe(); - } - if (self.slide.slide_type === 'webpage'){ - self._renderWebpage(); - } - if ((self.slide.slide_type === "presentation" || self.slide.slide_type === "document" || self.slide.slide_type === "infographic" || self.slide.slide_type === "webpage") && !self.slide.quiz){ - self._setSlideStateAsDone(); - } - if ((self.slide.quiz && self.slide.slide_type === "quiz") || self.goToQuiz){ - self._renderQuiz(); + + if(this.slide.slide_type === "video"){ + this._renderYoutubeIframe(); } + def.resolve(); } - } else { - self._fetchHtmlContent(); } - self._renderTitle(); + + if (this.slide.slide_type === "quiz" || this.goToQuiz) { + this._renderQuiz().then(function() { + def.resolve(); + }); + } + + return def.then(function() { + self._renderTitle(); + }); }, _renderYoutubeIframe: function (){ var self = this; - /** + /** * Due to issues of synchronization between the youtube api script and the widget's instanciation. */ try { @@ -114,18 +122,15 @@ odoo.define('website_slides.fullscreen', function (require) { var self = this; $(self.slide.htmlContent).appendTo('.o_wslides_fs_webpage_content'); }, + _renderQuiz: function (){ - var self = this; - var Quiz = new QuizWidget(this, self.slide, self.nextSlide); - Quiz.appendTo('.o_wslides_fs_player'); - $('.next-slide').click(function (){ - self._goToNextSlide(); - }); - $('.back-to-video').click(function (){ - self.goToQuiz = false; - self._renderPlayer(); + var quizSlideData = _.extend(this.slide, { + completed: this.slide.done, }); + var Quiz = new QuizWidget(this, quizSlideData); + return Quiz.appendTo('.o_wslides_fs_player'); }, + _renderTitle: function (){ var self = this; $('.o_wslides_fs_slide_title').empty().html(QWeb.render('website.course.fullscreen.title', { @@ -168,7 +173,7 @@ odoo.define('website_slides.fullscreen', function (require) { var totalTime = event.target.getDuration(); if (totalTime && currentTime > totalTime - 30){ clearInterval(self.tid); - if (!self.slide.has_quiz && !self.slide.done){ + if (!self.slide.hasQuiz && !self.slide.done){ self.slide.done = true; self._setSlideStateAsDone(); } @@ -194,26 +199,11 @@ odoo.define('website_slides.fullscreen', function (require) { this._getActiveSlide(); } }, + /** * @private - * @param {object} slide - * Fetch the quiz for a particular slide */ - _fetchQuiz: function (){ - var self = this; - return self._rpc({ - route:"/slide/quiz/get", - params: { - 'slide_id': self.slide.id - } - }).then(function (data){ - if (data){ - self.slide.quiz = data; - self._renderPlayer(); - } - }); - }, - _fetchHtmlContent: function (){ + _fetchHtmlContent: function(){ var self = this; return self._rpc({ route: 'slides/slide/get_html_content', @@ -223,7 +213,6 @@ odoo.define('website_slides.fullscreen', function (require) { }).then(function (data){ if (data.html_content) { self.slide.htmlContent = data.html_content; - self._renderPlayer(); } }); }, @@ -269,7 +258,7 @@ odoo.define('website_slides.fullscreen', function (require) { var self = this; clearInterval(self.tid); self.player = undefined; - self.goToQuiz = self.slide.has_quiz && !self.goToQuiz && self.slide.slide_type !== 'quiz'; + self.goToQuiz = self.slide.hasQuiz && !self.goToQuiz && self.slide.slide_type !== 'quiz'; if (self.nextSlide && !self.goToQuiz){ self.slide = self.nextSlide; self.index++; @@ -369,7 +358,7 @@ odoo.define('website_slides.fullscreen', function (require) { }, _onMiniQuizClick: function (ev){ var self = this; - self.index = parseInt($(ev.currentTarget).attr('index')); + self.index = parseInt($(ev.currentTarget).attr('index')) || 0; self.slide = self.slides[self.index]; self.goToQuiz = true; self._setPreviousAndNextSlides(); @@ -377,6 +366,9 @@ odoo.define('website_slides.fullscreen', function (require) { self._setActiveTab(); self._updateUrl(); history.pushState(null,'' ,self.url); + }, + _onQuizCompleted: function (ev) { + this._setSlideStateAsDone(); }, /** * @private diff --git a/addons/website_slides/static/src/js/slides_course_quiz.js b/addons/website_slides/static/src/js/slides_course_quiz.js index dd921d5a17f9066e7674d6d0c142cf7873838a97..319ca629bf2c4ae7db44b6a43cdeaa78d447dcd6 100644 --- a/addons/website_slides/static/src/js/slides_course_quiz.js +++ b/addons/website_slides/static/src/js/slides_course_quiz.js @@ -5,203 +5,346 @@ odoo.define('website_slides.quiz', function (require) { var Widget = require('web.Widget'); var QWeb = core.qweb; + var _t = core._t; - var Quiz= Widget.extend({ - /** + /** + * 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 or modal can be displayed. + * + * This widget can be attached to DOM rendered server-side by `website_slides.slide_type_quiz` or + * used client side (Fullscreen). + * + * Triggered events are : + * - quiz_next_slide: need to go to the next slide, when quiz is done. Event data contains the current slide id. + * - quiz_completed: when the quiz is passed and completed by the user. Event data contains completion + * percentage and current slide id. + */ + var Quiz = Widget.extend({ + template: 'slide.slide.quiz', + xmlDependencies: ['/website_slides/static/src/xml/slide_quiz.xml'], + events: { + "click .o_wslides_quiz_answer": '_onAnswerClick', + "click .o_wslides_js_lesson_quiz_submit": '_onSubmitQuiz', + "click .o_wslides_quiz_btn": '_onClickNext', + "click .o_wslides_quiz_continue": '_onClickNext' + }, + + /** * @override - * @param {Object} el - * @param {Object} data holding all the slide elements needed for the quiz - * It will either come from the fullscreen widget or the sAnimation at the end of this file + * @param {Object} parent + * @param {Object} slide_data holding all the classic slide informations + * @param {Object} quiz_data : optional quiz data to display. If not given, will be fetched. (questions and answers). */ - init: function (el, data, nextSlide){ - this.slide = data; - this.nextSlide = nextSlide; - this.answeredQuestions = []; - this.fullscreen = el; - return this._super.apply(this,arguments); - }, - start: function (){ - var self = this; - self._bindQuizEvents(); - /** - * If the quiz is rendered by the server instead of the fullscreen widget, - * questions and their answers will have to be created manually from attributes - */ - if (self.slide.quiz.questions.length === 0){ - this._setQuestions(); - } + init: function (parent, slide_data, quiz_data) { + this.slide = _.defaults(slide_data, { + id: 0, + name: '', + hasNext: false, + completed: false, + readonly: false, + }); + this.quiz = quiz_data || false; + this.readonly = slide_data.readonly || false; return this._super.apply(this, arguments); }, - _renderSuccessModal: function (data){ - var self =this; - $('.o_wslides_fs_quiz').append(QWeb.render('website.course.quiz.success', { - data: data, - nextSlide: self.nextSlide - })); - $('.submit-quiz').remove(); - $('.next-slide').css('display', 'inline-block'); - $('.next-slide').click(function (){ - self.fullscreen._goToNextSlide(); - }); - $('.o_wslides_quiz_success_modal_close').click(function (){ - $('.o_wslides_quiz_success_modal').remove(); - $('.o_wslides_quiz_modal_background').remove(); - }); - $(".o_wslides_quiz_modal_background").click(function (ev){ - $(ev.currentTarget).remove(); - $('.o_wslides_quiz_success_modal').remove(); + + /** + * @override + */ + willStart: function () { + var def = $.Deferred(); + if (this.quiz) { + def.resolve(); + } else { + def = this._fetchQuiz(); + } + return $.when(this._super.apply(this, arguments), def); + }, + + /** + * @override + */ + start: function() { + var self = this; + return this._super.apply(this, arguments).then(function () { + self._renderAnswers(); + self._renderAnswersHighlighting(); + self._renderValidationInfo(); }); }, + //-------------------------------------------------------------------------- // Private //-------------------------------------------------------------------------- - /** - * @private - * In case the quiz is rendered by the server and the data don't come from the fullscreen widget, - * questions and their answers will have to be set here by using attributes - */ - _setQuestions: function (){ - var self = this; - $('.o_wslides_quiz_question').each(function (){ - self.slide.quiz.questions.push({ - id: parseInt($(this).attr('id')), - title: $(this).attr('title'), - answers: [] - }); - }); - for (var i = 0; i < self.slide.quiz.questions.length; i++){ - self._setAnswersForQuestion(self.slide.quiz.questions[i]); + + _alertHide: function () { + this.$('.o_wslides_js_lesson_quiz_error').addClass('d-none'); + }, + + _alertShow: function (alert_code) { + var message = _t('There was an error validating this quiz.'); + if (! alert_code || alert_code === 'slide_quiz_incomplete') { + message = _t('All questions must be answered !'); + } + else if (alert_code === 'slide_quiz_done') { + message = _t('This quiz is already done. Retaking it is not possible.'); } + this.$('.o_wslides_js_lesson_quiz_error span').html(message); + this.$('.o_wslides_js_lesson_quiz_error').removeClass('d-none'); }, - _setAnswersForQuestion: function (question){ - $('.o_wslides_quiz_answer[question_id='+question.id+']').each(function (){ - question.answers.push({ - id: parseInt($(this).attr('id')), - text: $(this).attr('text_value'), - is_correct: $(this).attr('is_correct') - }); + + /* + * @private + * Fetch the quiz for a particular slide + */ + _fetchQuiz: function () { + var self = this; + return self._rpc({ + route:'/slides/slide/quiz/get', + params: { + 'slide_id': self.slide.id, + } + }).then(function (quiz_data) { + self.quiz = quiz_data; }); }, - _updateProgressbar: function (){ + + /** + * @private + * Decorate the answers according to state + */ + _renderAnswers: function () { var self = this; - var completion = self.channelCompletion <= 100 ? self.channelCompletion : 100; - $('.o_wslides_fs_sidebar_progress_gauge').css('width', completion + "%" ); - $('.o_wslides_progress_percentage').text(completion); + this.$('input[type=radio]').each(function () { + console.log($(this)); + $(this).prop('disabled', self.slide.readonly); + }); }, - _bindQuizEvents: function (){ + + /** + * @private + * Decorate the answer inputs according to the correction + */ + _renderAnswersHighlighting: function () { var self = this; - if (!self.slide.done){ - $('.o_wslides_quiz_answer').each(function (){ - $(this).click(self._onAnswerClick.bind(self)); - }); + this.$('li.o_wslides_quiz_answer').each(function () { + var $answer = $(this); + var answerId = $answer.data('answerId'); + if (_.contains(self.quiz.goodAnswers, answerId)) { + $answer.removeClass('border-danger').addClass('border border-success'); + $answer.find('i.fa').addClass('d-none'); + $answer.find('i.fa-check-circle').removeClass('d-none'); + } + else if (_.contains(self.quiz.badAnswers, answerId)) { + $answer.removeClass('border-success').addClass('border border-danger'); + $answer.find('i.fa').addClass('d-none'); + $answer.find('i.fa-times-circle').removeClass('d-none'); + $answer.find('label input').prop('checked', false); + } + else { + $answer.removeClass('border border-danger border-success'); + $answer.find('i.fa').addClass('d-none'); + $answer.find('i.fa-circle').removeClass('d-none'); + } + }); + }, + + /** + * @private + * When the quiz is done and succeed, a congratulation modal appears. + */ + _renderSuccessModal: function () { + var $modal = this.$('#slides_quiz_modal'); + if (!$modal.length) { + this.$el.append(QWeb.render('slide.slide.quiz.finish', {'widget': this})); + $modal = this.$('#slides_quiz_modal'); } - $('.submit-quiz').click(self._onSubmitQuiz.bind(self)); + $modal.modal({ + 'show': true, + }); + $modal.on('hidden.bs.modal', function () { + $modal.remove(); + }); + }, + + /* + * @private + * Update validation box (karma, buttons) according to widget state + */ + _renderValidationInfo: function () { + var $validationElem = this.$('.o_wslides_js_lesson_quiz_validation'); + $validationElem.html( + QWeb.render('slide.slide.quiz.validation', {'widget': this}) + ); + }, + + /** + * Set the slide as completed and done. Trigger up the completion. + * + * @param {Integer} completion + */ + _setCompleted: function () { + this.trigger_up('quiz_completed', { + 'slideId': this.slide.id, + }); }, - _highlightAnswers: function (answers){ + /* + * Submit the given answer, and display the result + * + * @param Array checkedAnswerIds: list of checked answers + */ + _submitQuiz: function (checkedAnswerIds) { var self = this; - self.answeredQuestions = []; - for (var i = 0; i < answers.goodAnswers.length; i++){ - $('#answer'+ answers.goodAnswers[i] +'').addClass('o_wslides_quiz_good_answer'); - $('#answer'+ answers.goodAnswers[i] +' .o_wslides_quiz_radio_box span').replaceWith($('<i class="fa fa-check-circle"></i>')); - var questionID =$('#answer'+ answers.goodAnswers[i] +' .o_wslides_quiz_radio_box input').attr('question_id'); - self.answeredQuestions.push(questionID); - $('.o_wslides_quiz_answer[question_id='+questionID+']:not(.o_wslides_quiz_good_answer)').addClass('o_wslides_quiz_ignored_answer'); - $('.o_wslides_quiz_answer[question_id='+questionID+']').each(function (){ - $(this).unbind('click'); - }); - $('input[question_id='+questionID+']').each(function (){ - $(this).prop('disabled',true); - }); - } - for (i = 0; i < answers.badAnswers.length; i++){ - $('#answer'+ answers.badAnswers[i]).removeClass('o_wslides_quiz_good_answer') - .addClass('o_wslides_quiz_bad_answer') - .unbind('click'); - $('#answer'+ answers.badAnswers[i] +' .o_wslides_quiz_radio_box span').replaceWith($('<i class="fa fa-times "></i>')); - $('#answer'+ answers.badAnswers[i] +' .o_wslides_quiz_radio_box input').prop('checked', false); - } + return this._rpc({ + route: '/slides/slide/quiz/submit', + params: { + slide_id: self.slide.id, + answer_ids: checkedAnswerIds, + } + }).then(function(data){ + if (data.error) { + self._alertShow(data.error); + } else { + self.slide.completed = data.completed; + self.quiz = _.extend(self.quiz, data); + self._renderAnswersHighlighting(); + self._renderValidationInfo(); + if (self.slide.completed) { + self._renderSuccessModal(data); + self._setCompleted(); + } + } + }); }, + //-------------------------------------------------------------------------- // Handlers //-------------------------------------------------------------------------- - _onAnswerClick: function (ev){ - var self = this; - var target = $(ev.currentTarget); - if ((self.answeredQuestions.indexOf(target.attr('question_id'))) === -1){ - var id = target.attr('id'); - var question_id = target.attr('question_id'); - $('.o_wslides_quiz_answer[question_id='+question_id+']').removeClass('o_wslides_quiz_good_answer'); - $('#'+id+' input[type=radio]').prop('checked', true); + + /** + * When clicking on an answer, this one should be marked as "checked". + * + * @private + * @param OdooEvent ev + */ + _onAnswerClick: function (ev) { + if (! this.slide.readonly) { + $(ev.currentTarget).find('input[type=radio]').prop('checked', true); } + this._alertHide(); }, - _onSubmitQuiz: function (){ - var self = this; - var inputs = $('input[type=radio]:checked'); + /** + * Triggering a event to switch to next slide + * + * @private + * @param OdooEvent ev + */ + _onClickNext: function (ev) { + if (this.slide.hasNext) { + this.trigger_up('quiz_next_slide', { + 'slideId': this.slide.id, + }); + } + }, + /** + * Submit a quiz and get the correction. It will display messages + * according to quiz result. + * + * @private + * @param OdooEvent ev + */ + _onSubmitQuiz: function (ev) { + var inputs = this.$('input[type=radio]:checked'); var values = []; for (var i = 0; i < inputs.length; i++){ values.push(parseInt($(inputs[i]).val())); } - if (values.length === self.slide.quiz.questions.length){ - $('.quiz-danger').remove(); - self._rpc({ - route: "/slide/quiz/submit", - params: { - slide_id: self.slide.id, - answer_ids: values, - quiz_id: self.slide.quiz_id - } - }).then(function (data){ - self._highlightAnswers(data); - self.slide.quiz.nb_attempts = data.attempts_count; - if (data.passed){ - self.channelCompletion = data.channel_completion; - self._updateProgressbar(); - $('#check-'+self.slide.id).replaceWith($('<i class="check-done o_wslides_slide_completed fa fa-check-circle"></i>')); - self.slide.done = true; - self._renderSuccessModal(data); - } - else { - var points = self.slide.quiz.nb_attempts < self.slide.quiz.possible_rewards.length ? self.slide.quiz.possible_rewards[self.slide.quiz.nb_attempts] : self.slide.quiz.possible_rewards[self.slide.quiz.possible_rewards.length-1]; - $('#quiz-points').text(points); - } - }); + + if (values.length === this.quiz.questions.length){ + this._alertHide(); + this._submitQuiz(values); } else { - $('#quiz_buttons').append($('<p class="quiz-danger text-danger mt-1">All questions must be answered !</p>')); + this._alertShow(); } }, -}); - - sAnimations.registry.websiteSlidesQuizNoFullscreen = Widget.extend({ - selector: '.o_w_slides_quiz_no_fullscreen', - xmlDependencies: ['/website_slides/static/src/xml/website_slides_fullscreen.xml'], - init: function (el){ - this._super.apply(this, arguments); - }, - start: function (){ - this._super.apply(this, arguments); - var slideID = parseInt(this.$el.attr('slide_id'),10); - var slideDone = this.$el.attr('slide_done'); - var nbAttempts = parseInt(this.$el.attr('nb_attempts'), 10); - var firstAttemptReward = this.$el.attr('first_reward'); - var secondAttemptReward = this.$el.attr('second_reward'); - var thirdAttemptReward = this.$el.attr('third_reward'); - var fourthAttemptReward = this.$el.attr('fourth_reward'); - var possibleRewards = [firstAttemptReward,secondAttemptReward,thirdAttemptReward,fourthAttemptReward]; - var data = { - id: slideID, - done: slideDone, - quiz: { - questions: [], - nb_attempts: nbAttempts, - possible_rewards: possibleRewards, - reward: nbAttempts < possibleRewards.length ? possibleRewards[nbAttempts] : possibleRewards[possibleRewards.length-1] - } - }; - if (!slideDone){ - var NewQuiz = new Quiz(this, data); - NewQuiz.appendTo(".o_w_slides_quiz_no_fullscreen"); - } - } + }); + + sAnimations.registry.websiteSlidesQuizNoFullscreen = sAnimations.Class.extend({ + selector: '.o_wslides_js_lesson_quiz', + custom_events: { + quiz_next_slide: '_onQuizNextSlide', + quiz_completed: '_onQuizCompleted', + }, + + //---------------------------------------------------------------------- + // Public + //---------------------------------------------------------------------- + + /** + * @override + * @param {Object} parent + */ + start: function () { + var self = this; + var defs = [this._super.apply(this, arguments)]; + $('.o_wslides_js_lesson_quiz').each(function () { + var slideData = $(this).data(); + slideData.quizData = { + questions: self._extractQuestionsAndAnswers(), + quizKarmaMax: slideData.quizKarmaMax, + quizKarmaWon: slideData.quizKarmaWon, + quizKarmaGain: slideData.quizKarmaGain, + quizAttemptsCount: slideData.quizAttemptsCount, + }; + defs.push(new Quiz(self, slideData, slideData.quizData).attachTo($(this))); + }); + return $.when.apply($, defs); + }, + + //---------------------------------------------------------------------- + // Handlers + //--------------------------------------------------------------------- + + _onQuizCompleted: function (slideId) { + console.log('set completed', slideId); + }, + + _onQuizNextSlide: function () { + var url = this.$el.data('next-slide-url'); + window.location.replace(url); + }, + + //---------------------------------------------------------------------- + // 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 /slide/quiz/get controller. + * + * @return {Array<Object>} list of questions with answers + */ + _extractQuestionsAndAnswers: function() { + var questions = []; + this.$('.o_wslides_js_lesson_quiz_question').each(function () { + var $question = $(this); + var answers = []; + $question.find('.o_wslides_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'), + answers: answers, + }); + }); + return questions; + }, }); return Quiz; diff --git a/addons/website_slides/static/src/scss/slides_slide_fullscreen.scss b/addons/website_slides/static/src/scss/slides_slide_fullscreen.scss index f6612d5d04f9988ee269b884a8edd4909b59ccae..08ef8bc299b974b82a25d7d68602dcd0b6474b3e 100644 --- a/addons/website_slides/static/src/scss/slides_slide_fullscreen.scss +++ b/addons/website_slides/static/src/scss/slides_slide_fullscreen.scss @@ -268,5 +268,44 @@ .o_wslides_fs_player_no_sidebar { max-width: 100%; width: 100%; - } + } + + // quizz + .o_wslides_fs_quiz_container { + background-color: $gray-200; + height: 100%; + overflow: scroll; + overflow-x: hidden; + width: 100%; + + .o_wslides_js_lesson_quiz_question { + li { + font-size: 1.2rem; + } + + .o_wslides_quiz_answer { + + &:hover { + cursor: pointer; + } + + label { + font-size: 1.4rem; + + i.fa-circle { + } + + input:checked + i.fa-circle { + color: $primary !important; + overflow: hidden; + } + } + } + } + } + + // Tools + .bg-brand { + background-color: $o-enterprise-color; + } } diff --git a/addons/website_slides/static/src/scss/website_slides.scss b/addons/website_slides/static/src/scss/website_slides.scss index c8d35376e17b284a0414a7a3ebe17422e8235e61..35ffa9547d42b488dd03330eafb95d0553de0f6b 100644 --- a/addons/website_slides/static/src/scss/website_slides.scss +++ b/addons/website_slides/static/src/scss/website_slides.scss @@ -42,6 +42,31 @@ $gray-50: #f4f4f4 !default; color: $link-color; } + .o_wslides_js_lesson_quiz_question { + li { + font-size: 1.2rem; + } + + .o_wslides_quiz_answer { + + &:hover { + cursor: pointer; + } + + label { + font-size: 1.4rem; + + i.fa-circle { + } + + input:checked + i.fa-circle { + color: $primary !important; + overflow: hidden; + } + } + } + } + // tools // **************************************** @@ -53,6 +78,10 @@ $gray-50: #f4f4f4 !default; opacity: 0.5; } + .bg-brand { + background-color: $o-enterprise-color; + } + .progress { overflow: visible; margin-bottom: 0em; diff --git a/addons/website_slides/static/src/scss/website_slides_quiz.scss b/addons/website_slides/static/src/scss/website_slides_quiz.scss deleted file mode 100644 index cca013c0be9ba9121edabf9774b0c33a9f680414..0000000000000000000000000000000000000000 --- a/addons/website_slides/static/src/scss/website_slides_quiz.scss +++ /dev/null @@ -1,237 +0,0 @@ - - .o_wslides_fs_quiz { - background-color: #F6F6F6; - width: 100%; - height: 100%; - overflow: scroll; - overflow-x: hidden; - padding-bottom: 50px; - - } - - .o_wslides_quiz_question { - margin-top: 50px; - width: 100%; - .o_wslides_quiz_question_title { - font-size: 1.5rem; - margin-bottom: 20px; - .o_wslides_quiz_question_title_number { - color: #8C8C8C; - font-size: 1.3rem; - font-weight: bold; - } - - .o_wslides_quiz_question_title_text { - margin-left: 2px; - color: #2F2F2F; - } - } - - .o_wslides_quiz_question_answers{ - background-color: #fff; - list-style: none; - padding: 0; - - .o_wslides_quiz_answer { - padding: 10px 20px; - border-bottom: 1px solid #B6B6B6; - font-size: 1.2rem; - display: flex; - align-items: center; - - label.o_wslides_quiz_radio_box{ - background-color: #f6f6f6; - width: 35px; - height: 35px; - display: inline-flex; - border-radius: 50%; - overflow: hidden; - justify-content: center; - align-items: center; - margin: 0; - margin-right: 20px; - font-size: 1.2rem; - outline: none; - border: none; - padding: 0; - } - - label.o_wslides_quiz_radio_box input{ - display:none; - outline: none; - border: none; - } - - - label.o_wslides_quiz_radio_box span{ - width: 18px; - height: 18px; - margin: 0; - } - - label.o_wslides_quiz_radio_box input:checked + span{ - background-color: $primary; - border-radius: 50%; - overflow: hidden; - } - - label.o_wslides_quiz_radio_box input:checked{ - background-color: #C7EAE9; - } - } - - .o_wslides_quiz_answer:not(.o_wslides_quiz_good_answer,.o_wslides_quiz_bad_answer,.o_wslides_quiz_ignored_answer):hover { - cursor: pointer; - } - - .o_wslides_quiz_good_answer{ - border: 2px solid #28A745; - label.o_wslides_quiz_radio_box input:checked + i{ - height: 100%; - width: 100%; - background-color: #fff; - color: #28A745; - display: flex; - justify-content: center; - align-items: center; - font-size: 1.4rem; - border: 10px solid #28A745; - border-radius: 50px; - } - } - - .o_wslides_quiz_bad_answer{ - border: 2px solid #A94442; - - label.o_wslides_quiz_radio_box i{ - background-color: #A94442; - color: white; - height: 100%; - width: 100%; - font-size: 1rem; - display: flex; - justify-content: center; - align-items: center; - } - } - - - .quiz-ignored-answer { - color: $text-muted; - } - } - } - - .o_wslides_quiz_modal_background{ - position: absolute; - top: 0; - left: 0; - background-color: rgba(0,0,0,.2); - width: 100%; - height: 100%; - - } - - .o_wslides_quiz_success_modal { - display: flex; - position: absolute; - width: 40%; - max-width: 600px; - height: 450px; - background-color: #fff; - top: 50%; - left: 50%; - transform: translateX(-50%) translateY(-50%); - z-index: 2000; - } - - .o_wslides_quiz_success_modal_left_panel { - width: 50%; - background-color: #855B79; - - } - - .o_wslides_quiz_success_modal_right_panel{ - width: 50%; - display: flex; - flex-direction: column; - padding: 10px 20px 10px 50px; - position: relative; - - .o_wslides_quiz_success_modal_close { - position: absolute; - top: 10px; - right: 15px; - text-transform: uppercase; - color: #787878; - font-size: 1.2rem; - } - - .o_wslides_quiz_success_modal_close:hover{ - cursor: pointer; - } - - h1 { - color: #21272B; - font-weight: bold; - font-size: 2.3rem; - padding: 20px 0; - letter-spacing: 1px; - } - - .o_wslides_quiz_success_progress_bar{ - height: 10px; - width: 100%; - background-color: rgba(0,0,0,.1); - - .o_wslides_quiz_success_progress_gauge{ - width: 75%; - height: 100%; - background-color: #01ADAB; - } - } - - .o_wslides_quiz_success_progress_bounds{ - display: flex; - justify-content: space-between; - } - - .o_wslides_quiz_success_progress_bounds{ - font-weight: bold; - span:first-child { - color: #019F9D; - } - span:last-child{ - color: #787878; - } - } - - .o_wslides_quiz_success_reward { - height: 80px; - width: 100%; - border: 1px dashed #ccc; - margin-top: 70px; - } - - .o_wslides_quiz_success_button{ - flex: 1; - display: flex; - justify-content: flex-end; - align-items: flex-end; - - a { - color: #787878; - text-transform: uppercase; - background-color: #fff; - border-radius: 3px; - border: 1px solid #C4C4C4; - font-weight: bold; - padding: 5px 15px; - text-align: center; - } - - a:hover { - cursor: pointer; - } - } - } \ No newline at end of file diff --git a/addons/website_slides/static/src/xml/slide_quiz.xml b/addons/website_slides/static/src/xml/slide_quiz.xml new file mode 100644 index 0000000000000000000000000000000000000000..6d778be9b2c34e2a2bda9965d7f19e13125decdf --- /dev/null +++ b/addons/website_slides/static/src/xml/slide_quiz.xml @@ -0,0 +1,87 @@ +<?xml version="1.0" encoding="UTF-8"?> +<templates xml:space="preserve"> + <t t-name="slide.slide.quiz"> + <div class="o_wslides_fs_quiz_container"> + <div class="mx-5"> + <div t-foreach="widget.quiz.questions" t-as="question" + class="o_wslides_js_lesson_quiz_question mt-3" t-att-data-question-id="question.id" t-att-data-title="question.question"> + <div class="h3"> + <small class="text-muted"><span t-esc="question_index+1"/>. </small> <span t-esc="question.question"/> + </div> + <ul class="bg-white list-unstyled"> + <t t-foreach="question.answers" t-as="answer"> + <li t-att-data-answer-id="answer.id" + t-att-data-text="answer.text_value" + class="o_wslides_quiz_answer pt-2 pb-2 border-bottom rounded d-flex align-items-center"> + + <label class="mb-0 d-flex align-items-center justify-content-center mr-3 ml-3"> + <input type="radio" + t-att-name="question.id" + t-att-value="answer.id" + class="d-none"/> + <i class="fa fa-circle text-muted"></i> + <i class="fa fa-times-circle text-danger d-none"></i> + <i class="fa fa-check-circle text-success d-none"></i> + </label> + <span t-esc="answer.text_value"/> + </li> + </t> + </ul> + </div> + <div class="alert alert-danger o_wslides_js_lesson_quiz_error d-none" role="alert"> + <span></span> + <button type="button" class="close" data-dismiss="alert" aria-label="Close"> + <span aria-hidden="true">&times;</span> + </button> + </div> + <div class="o_wslides_js_lesson_quiz_validation"/> + </div> + </div> + </t> + + <t t-name="slide.slide.quiz.validation"> + <t t-if="widget.readonly"> + <button class="btn btn-primary" role="button" title="Join" aria-label="Join" disabled="disabled">Join course to take quiz</button> + <span 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 py-1">+ <t t-esc="widget.quiz.quizKarmaGain"/> XP</span> + </t> + <t t-else=""> + <button t-if="! widget.slide.completed" role="button" title="Check answers" aria-label="Check answers" + class="btn btn-primary text-uppercase font-weight-bold o_wslides_js_lesson_quiz_submit">Check your answers</button> + <button t-if="widget.slide.completed" role="button" title="Quiz done" aria-label="Quiz done" disabled="disabled" + class="btn btn-primary text-uppercase font-weight-bold">Done !</button> + <span t-if="! widget.slide.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 py-1">+ <t t-esc="widget.quiz.quizKarmaGain"/> XP</span> + <span t-if="widget.slide.completed" role="button" title="Gained karma" aria-label="Gained karma" + class="badge badge-pill badge-success text-white font-weight-bold ml-3 px-2 py-1">+ <t t-esc="widget.quiz.quizKarmaWon"/> XP</span> + <button t-if="widget.slide.completed && widget.slide.hasNext" + class="btn btn-primary ml-3 o_wslides_quiz_continue">Continue</button> + </t> + </t> + + <t t-name="slide.slide.quiz.finish"> + <div class="modal o_wslides_quiz_modal" tabindex="-1" role="dialog" id="slides_quiz_modal"> + <div class="modal-dialog" role="document"> + <div class="modal-content"> + <div class="modal-body d-flex p-0"> + <div class="w-50 bg-brand"></div> + <div class="w-50 d-flex flex-column p-3"> + <h1>Amazing!</h1> + <div class="pb-3"> + <span class="pb-2">You gained <span class="badge badge-pill badge-success text-white font-weight-bold"><t t-esc="widget.quiz.quizKarmaWon"/> XP !</span> !</span> + <div class="progress o_wslide_progress_bar"> + <div class="progress-bar" role="progressbar" t-att-aria-valuenow="widget.quiz.rankProgress.progress" aria-valuemin="0" aria-valuemax="100" t-attf-style="width: #{widget.quiz.rankProgress.progress}%"/> + </div> + <small t-if="widget.quiz.rankProgress.lowerBound" class="float-left"><t t-esc="widget.quiz.rankProgress.lowerBound"/></small> + <small t-if="widget.quiz.rankProgress.upperBound" class="float-right"><t t-esc="widget.quiz.rankProgress.upperBound"/></small> + </div> + <div class="pb-3" t-raw="widget.quiz.rankProgress.motivational"/> + <button t-if="widget.slide.hasNext" type="button" class="btn btn-light o_wslides_quiz_btn align-self-end">Next <i class="fa fa-chevron-right"/></button> + </div> + </div> + </div> + </div> + </div> + </t> + +</templates> diff --git a/addons/website_slides/static/src/xml/website_slides_fullscreen.xml b/addons/website_slides/static/src/xml/website_slides_fullscreen.xml index 1817d085f71de4f7bcd92bdfb54a12c9d4a937b2..b364f486420be8f5c657390a336f405f01345e8c 100644 --- a/addons/website_slides/static/src/xml/website_slides_fullscreen.xml +++ b/addons/website_slides/static/src/xml/website_slides_fullscreen.xml @@ -6,56 +6,12 @@ <div t-if="['presentation', 'document'].indexOf(slide.slide_type) !== -1 && !showMiniQuiz" class="embed-responsive embed-responsive-4by3 embed-responsive-item "> <iframe t-att-src="embed_url" class="o_wslides_iframe_viewer" allowFullScreen="true" height="315" width="420" frameborder="0"></iframe> </div> - <div style="width:100%;height:100%;text-align:center;" t-if="slide.slide_type == 'infographic' && !showMiniQuiz"> + <div t-if="slide.slide_type == 'infographic' && !showMiniQuiz" style="width:100%;height:100%;text-align:center;"> <img t-attf-src="/web/image/slide.slide/#{slide.id}/image" class="img-fluid" style="height: 100%" alt="Slide image"/> </div> - <div t-if="(slide.slide_type == 'quiz' || showMiniQuiz) && questions " class="o_wslides_fs_quiz"> - <div> - <t t-call="website.slide.quiz"/> - <div id="quiz_buttons" class="container"> - <button t-if="!slide.done" class="btn btn-primary submit-quiz" >Check your answers</button> - <button t-if="embed_url" class="btn btn-primary back-to-video" >Watch again</button> - <button t-if="slide.done && nextSlide" class="btn btn-primary next-slide" >Continue</button> - </div> - </div> - </div> - <div t-if="slide.slide_type == 'webpage'" class="o_wslides_fs_webpage"> - <div class="o_wslides_fs_webpage_content container"/> - </div> <div class="o_wslides_fs_content"/> </t> - <t t-name="website.slide.quiz"> - <div class="container quiz-questions"> - <t t-set="i" t-value="1"/> - <t t-foreach="questions" t-as="question"> - <t t-set="j" t-value="0"/> - <div class="o_wslides_quiz_question"> - <div> - <div class="o_wslides_quiz_question_title"> - <span class="o_wslides_quiz_question_title_number"><span t-esc="i"/>. </span> - <span class="o_wslides_quiz_question_title_text" t-esc="question.title"/> - </div> - <ul class="o_wslides_quiz_question_answers"> - <t t-foreach="question.answers" t-as="answer"> - <li t-attf-id="answer#{answer.id}" t-att-question_id="question.id" t-att-class="(slide.done && answer.correct) ? 'o_wslides_quiz_answer o_wslides_quiz_good_answer': slide.done ? 'o_wslides_quiz_answer o_wslides_quiz_ignored_answer' : 'o_wslides_quiz_answer'"> - <label class="o_wslides_quiz_radio_box"> - <input t-att-answer_id="answer.id" type="radio" t-att-name="question.id" t-att-question_id="question.id" t-att-value="answer.id" t-att-checked="(slide.done && answer.correct) ? 'checked' : undefined" t-att-disabled="slide.done"/> - <span t-if="!(slide.done && answer.correct)"/> - <i t-if="(slide.done && answer.correct)" class="fa fa-check-circle"></i> - </label> - <span t-esc="answer.text"/> - </li> - <t t-set="j" t-value="j+1"/> - </t> - </ul> - </div> - </div> - <t t-set="i" t-value="i+1"/> - </t> - </div> - </t> - <t t-name="website.course.player.infos"> <div class="text-box"> <h1><t t-esc="slide.name"/></h1> @@ -67,32 +23,6 @@ </div> </t> - <t t-name="website.course.quiz.success"> - <div class="o_wslides_quiz_modal_background"> - </div> - <div class="o_wslides_quiz_success_modal"> - <div class="o_wslides_quiz_success_modal_left_panel"></div> - <div class="o_wslides_quiz_success_modal_right_panel"> - <div class="o_wslides_quiz_success_modal_close"><i class="fa fa-times"></i></div> - <h1>Amazing!</h1> - <div class="o_wslides_quiz_success_xp"> - <p>You gained <span t-esc="data.points"/> points!</p> - <div class="o_wslides_quiz_success_progress_bar"> - <div class="o_wslides_quiz_success_progress_gauge" t-att-style="'width:'+data.user.progress_bar_percentage+'%'"/> - </div> - <div class="o_wslides_quiz_success_progress_bounds"> - <span t-esc="data.user.lower_bound"/> - <span t-esc="data.user.upper_bound"/> - </div> - </div> - <div class="o_wslides_quiz_success_reward"></div> - <div t-if="nextSlide" class="o_wslides_quiz_success_button"> - <a class="next-slide">Next ></a> - </div> - </div> - </div> - </t> - <t t-name="website.course.fullscreen.title"> <t t-if="!miniQuiz"> <i t-if="slide.slide_type == 'document'" class="fa fa-file-pdf-o mr-2"></i> diff --git a/addons/website_slides/views/assets.xml b/addons/website_slides/views/assets.xml index eb000518491ed18d4efd76cb14ac162285857319..742bac7c8ea1beb98c5a74f3ab6ead8e2a5d8ad3 100644 --- a/addons/website_slides/views/assets.xml +++ b/addons/website_slides/views/assets.xml @@ -8,7 +8,6 @@ <link rel="stylesheet" type="text/scss" href="/website_slides/static/src/scss/website_slides_profile.scss"/> <link rel="stylesheet" type="text/scss" href="/website_slides/static/src/scss/slide_slide.scss" t-ignore="true"/> <link rel="stylesheet" type="text/scss" href="/website_slides/static/src/scss/slides_slide_fullscreen.scss" t-ignore="true"/> - <link rel="stylesheet" type="text/scss" href="/website_slides/static/src/scss/website_slides_quiz.scss"/> </xpath> <xpath expr="//script[last()]" position="after"> <script type="text/javascript" src="/website_slides/static/src/js/slides.js"/> diff --git a/addons/website_slides/views/website_slides_templates_lesson.xml b/addons/website_slides/views/website_slides_templates_lesson.xml index e6df3294289ce21c340097fcdc481355e7a894c3..12e9fad96114b992d33469a0e5811cc64e3a9fb0 100644 --- a/addons/website_slides/views/website_slides_templates_lesson.xml +++ b/addons/website_slides/views/website_slides_templates_lesson.xml @@ -59,12 +59,12 @@ <a class="o_wslides_slide_button" t-attf-href="/slides/slide/#{slug(previous_slide)}">Prev</a> </t> <a t-if="slide.slide_type in ('webpage', 'video', 'document', 'iconographic') and not slide.question_ids and (slide.id in user_progress and not user_progress[slide.id].completed)" - t-att-href="'/slides/slide/%s/set_completed?%s' % (slide.id, 'next_slide=%s' % next_slide.id if next_slide else '')" + t-att-href="'/slides/slide/%s/set_completed?%s' % (slide.id, 'next_slide_id=%s' % next_slide.id if next_slide else '')" class="o_wslides_slide_button_done">Set Done</a> <t t-if="next_slide"> <a class="o_wslides_slide_button" t-attf-href="/slides/slide/#{slug(next_slide)}">Next</a> </t> - <a t-if="slide.channel_id.channel_type == 'training'" t-attf-href="/slides/slide/#{slug(slide)}?fullscreen=1" class="o_wslides_slide_button_fullscreen ml-2"><i class="fa fa-desktop mr-2"></i>fullscreen</a> + <a t-if="slide.channel_id.channel_type == 'training'" t-attf-href="/slides/slide/#{slug(slide)}?fullscreen=1" class="o_wslides_slide_button_fullscreen ml-2"><i class="fa fa-desktop mr-2"></i>Fullscreen</a> </div> </div> <div class="o_wslides_slide_header_container mt16"> @@ -83,49 +83,7 @@ <div t-if="slide.slide_type == 'webpage'" class="border border-light rounded"> <div t-field="slide.html_content"/> </div> - <div t-if="slide.question_ids"> - <div class=".o_wslides_fs_quiz o_w_slides_quiz_no_fullscreen mt-2" - t-attf-slide_id="#{slide.id}" - t-attf-slide_done="#{slide.id in user_progress and user_progress[slide.id].completed}" - t-attf-nb_attempts="#{user_progress[slide.id].quiz_attempts_count if slide.id in user_progress else ''}" - t-attf-first_reward="#{slide.quiz_first_attempt_reward}" - t-attf-second_reward="#{slide.quiz_second_attempt_reward}" - t-attf-third_reward="#{slide.quiz_third_attempt_reward}" - t-attf-fourth_reward="#{slide.quiz_fourth_attempt_reward}" - t-if="slide.question_ids"> - <div> - <t t-set="i" t-value="1"/> - <t t-foreach="slide.question_ids" t-as="question"> - <t t-set="j" t-value="0"/> - <div class="o_wslides_quiz_question" t-attf-id="#{question.id}" t-attf-title="#{question.question}"> - <div> - <div class="o_wslides_quiz_question_title"> - <span class="o_wslides_quiz_question_title_number"><span t-esc="i"/>. </span> - <span class="o_wslides_quiz_question_title_text" t-esc="question.question"/> - </div> - <ul class="o_wslides_quiz_question_answers"> - <t t-foreach="question.answer_ids" t-as="answer"> - <li t-attf-id="answer#{answer.id}" t-att-question_id="question.id" t-attf-text_value="#{answer.text_value}" t-attf-class="#{'o_wslides_quiz_answer o_wslides_quiz_good_answer' if slide.id in user_progress and user_progress[slide.id].completed and answer.is_correct else 'o_wslides_quiz_answer o_wslides_quiz_ignored_answer' if slide.id in user_progress and user_progress[slide.id].completed else 'o_wslides_quiz_answer'}"> - <label class="o_wslides_quiz_radio_box"> - <input t-att-answer_id="answer.id" type="radio" t-att-name="question.id" t-att-question_id="question.id" t-att-value="answer.id" t-att-checked="'checked' if slide.id in user_progress and user_progress[slide.id].completed and answer.is_correct else False" t-att-disabled="slide.id in user_progress and user_progress[slide.id].completed"/> - <span t-if="not slide.id in user_progress or not (user_progress[slide.id].completed and answer.is_correct)"/> - <i t-if="slide.id in user_progress and user_progress[slide.id].completed and answer.is_correct" class="fa fa-check-circle"></i> - </label> - <span t-esc="answer.text_value"/> - </li> - <t t-set="j" t-value="j+1"/> - </t> - </ul> - </div> - </div> - <t t-set="i" t-value="i+1"/> - </t> - <button t-if="not slide.id in user_progress or not user_progress[slide.id].completed" class="btn btn-primary submit-quiz" >Check your answers</button> - <a t-if="next_slide" t-attf-style="#{'display: none' if not slide.id in user_progress or not user_progress[slide.id].completed else ''}" class="btn btn-primary next-slide" t-attf-href="/slides/slide/#{next_slide}">Continue</a> - </div> - - </div> - </div> + <t t-if="slide.question_ids" t-call="website_slides.lesson_content_quiz"/> <div class="row mt-3"> <div class="col-lg-6"> <div clas="row"> @@ -462,6 +420,54 @@ </ul> </template> +<!-- Slide sub-tempalte: render a quiz serverside. Should be sync with JS qweb template "slide.slide.quiz" --> +<template id="lesson_content_quiz" name="Lesson: Quiz specific content"> + <div class="o_wslides_js_lesson_quiz" + t-att-data-id="slide.id" + t-att-data-name="slide.name" + t-att-data-slide-type="slide.slide_type" + t-att-data-readonly="not slide.id in user_progress" + t-att-data-completed="slide.id in user_progress and user_progress[slide.id].completed" + t-att-data-quiz-attempts-count="quiz_attempts_count" + t-att-data-quiz-karma-max="quiz_karma_max" + t-att-data-quiz-karma-gain="quiz_karma_gain" + t-att-data-quiz-karma-won="quiz_karma_won" + t-att-data-has-next="1 if next_slide else 0" + t-att-data-next-slide-url="'/slides/slide/%s' % (slug(next_slide)) if next_slide else None"> + <div t-foreach="slide.question_ids" t-as="question" + class="o_wslides_js_lesson_quiz_question mt-3" t-att-data-question-id="question.id" t-att-data-title="question.question"> + <div class="h3"> + <small class="text-muted"><span t-esc="question_index+1"/>. </small> <span t-esc="question.question"/> + </div> + <ul class="bg-white list-unstyled"> + <t t-foreach="question.answer_ids" t-as="answer"> + <li t-att-data-answer-id="answer.id" + t-att-data-text="answer.text_value" + class="o_wslides_quiz_answer pt-2 pb-2 border-bottom rounded d-flex align-items-center"> + <label class="mb-0 d-flex align-items-center justify-content-center mr-3 ml-3"> + <input type="radio" + t-att-name="question.id" + t-att-value="answer.id" + class="d-none"/> + <i class="fa fa-circle text-muted"></i> + <i class="fa fa-times-circle text-danger d-none"></i> + <i class="fa fa-check-circle text-success d-none"></i> + </label> + <span t-esc="answer.text_value"/> + </li> + </t> + </ul> + </div> + <div class="alert alert-danger o_wslides_js_lesson_quiz_error d-none" role="alert"> + <span></span> + <button type="button" class="close" data-dismiss="alert" aria-label="Close"> + <span aria-hidden="true">&times;</span> + </button> + </div> + <div class="o_wslides_js_lesson_quiz_validation"/> + </div> +</template> + <!-- Slide sub-template: display an item in a list of related slides (Related, Most Viewed, ...) --> <template id="related_slides" name="Related Slide"> <li class="media mt-3"> diff --git a/addons/website_slides/views/website_slides_templates_lesson_fullscreen.xml b/addons/website_slides/views/website_slides_templates_lesson_fullscreen.xml index 8b7a649d107b25f32c1c7548e4db1393c62a8f98..a505f8c78e02d18fe5f58a1ac20a97dafb039dbf 100644 --- a/addons/website_slides/views/website_slides_templates_lesson_fullscreen.xml +++ b/addons/website_slides/views/website_slides_templates_lesson_fullscreen.xml @@ -82,8 +82,9 @@ t-att-data-slug="slug(course_slide)" t-att-data-done="course_slide.id in user_progress and user_progress[course_slide.id].completed" t-att-data-id="course_slide.id" + t-att-data-readonly="not course_slide.channel_id.is_member" t-att-data-name="course_slide.name" - t-attf-data-has_quiz="#{True if course_slide.question_ids else False}" + t-att-data-has-quiz="True if course_slide.question_ids else False" t-att-data-slide_type="course_slide.slide_type" t-att-data-embed_code="course_slide.embed_code" t-attf-class="o_wslides_fs_sidebar_slide_tab #{'active' if slide.id == course_slide.id else ''} d-flex justify-content-between"> diff --git a/addons/website_slides_survey/models/slide_slide.py b/addons/website_slides_survey/models/slide_slide.py index 5165589533fd9fc741214fd56d1d1c524c1dc976..731e7a37f4e55922559e0c2fbdd677bcb27ead83 100644 --- a/addons/website_slides_survey/models/slide_slide.py +++ b/addons/website_slides_survey/models/slide_slide.py @@ -32,10 +32,10 @@ class Slide(models.Model): ('check_certification_preview', "CHECK(slide_type != 'certification' OR is_preview = False)", "A slide of type certification cannot be previewed."), ] - def _action_set_viewed(self, target_partner): + def _action_set_viewed(self, target_partner, quiz_attempts_inc=False): """ If the slide viewed is a certification, we initialize the first survey.user_input for the current partner. """ - new_slide_partners = super(Slide, self)._action_set_viewed(target_partner) + new_slide_partners = super(Slide, self)._action_set_viewed(target_partner, quiz_attempts_inc=quiz_attempts_inc) certification_slides = self.search([ ('id', 'in', new_slide_partners.mapped('slide_id').ids), ('slide_type', '=', 'certification'),