From 660e1054311d0115a107750aa30b9c282d10875f Mon Sep 17 00:00:00 2001 From: Florent Lejoly <fle@odoo.com> Date: Mon, 30 Sep 2019 11:25:38 +0000 Subject: [PATCH] [IMP] website_[sale_]slides: allow users to take the slide quiz before joining the course MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit PURPOSE When previewing a slide of type 'quiz', users are tempted to answer the questions even if they are not member of the course yet. This commit will allow users to answer the quiz without being a member of the course, and offer a smart redirection to join / buy based on the enroll type of the channel. SPECIFICATIONS We introduce a "CourseJoinWidget" that will handle the display and the behavior of the "join" button based on the user (public or not) and the enroll type (public course or based on a payment). When the user answers questions and he's not a member yet, we store the answers temporarily in the session object. When he comes back on the quiz page after having completed his membership, we fill in the quiz based on his previously entered answers and clean the session. LINKS Task 2045571 PR #41972 Co-authored-by: Florent Lejolyn <fle@odoo.com> Co-authored-by: Aurélien Warnon <awa@odoo.com> --- .../static/src/js/slides_course_join.js | 43 ++++ .../static/src/js/slides_course_quiz.js | 2 +- .../static/src/xml/slide_course_join.xml | 15 ++ .../src/xml/website_sale_slides_quiz.xml | 46 +---- addons/website_sale_slides/views/assets.xml | 1 + addons/website_slides/controllers/main.py | 35 +++- .../website_slides/data/slide_slide_demo.xml | 3 +- .../static/src/js/slides_course_join.js | 85 ++++++-- .../static/src/js/slides_course_quiz.js | 189 +++++++++++++----- .../static/src/xml/slide_course_join.xml | 14 +- .../static/src/xml/slide_quiz.xml | 56 ++++-- .../views/website_slides_templates_course.xml | 3 +- .../views/website_slides_templates_lesson.xml | 7 +- ...ite_slides_templates_lesson_fullscreen.xml | 10 +- 14 files changed, 370 insertions(+), 139 deletions(-) create mode 100644 addons/website_sale_slides/static/src/js/slides_course_join.js create mode 100644 addons/website_sale_slides/static/src/xml/slide_course_join.xml diff --git a/addons/website_sale_slides/static/src/js/slides_course_join.js b/addons/website_sale_slides/static/src/js/slides_course_join.js new file mode 100644 index 000000000000..22ebe96da3ed --- /dev/null +++ b/addons/website_sale_slides/static/src/js/slides_course_join.js @@ -0,0 +1,43 @@ +odoo.define('website_sale_slides.course.join.widget', function (require) { +"use strict"; + +var CourseJoinWidget = require('website_slides.course.join.widget').courseJoinWidget; + +CourseJoinWidget.include({ + xmlDependencies: (CourseJoinWidget.prototype.xmlDependencies || []).concat( + ["/website_sale_slides/static/src/xml/slide_course_join.xml"] + ), + init: function (parent, options) { + this._super.apply(this, arguments); + this.productId = options.channel.productId || false; + }, + + //-------------------------------------------------------------------------- + // Handlers + //-------------------------------------------------------------------------- + + /** + * When the user joins the course, if it's set as "on payment" and the user is logged in, + * we redirect to the shop page for this course. + * + * @param {MouseEvent} ev + * @override + * @private + */ + _onClickJoin: function (ev) { + ev.preventDefault(); + + if (this.channel.channelEnroll === 'payment' && !this.publicUser) { + var shopUrl = _.str.sprintf('/shop/cart/update?product_id=%s&express=1', this.productId); + this.beforeJoin().then(function () { + window.location.href = shopUrl; + }); + } else { + this._super.apply(this, arguments); + } + }, +}); + +return CourseJoinWidget; + +}); diff --git a/addons/website_sale_slides/static/src/js/slides_course_quiz.js b/addons/website_sale_slides/static/src/js/slides_course_quiz.js index 1e601acc7a70..3922a631a307 100644 --- a/addons/website_sale_slides/static/src/js/slides_course_quiz.js +++ b/addons/website_sale_slides/static/src/js/slides_course_quiz.js @@ -5,7 +5,7 @@ var sAnimations = require('website.content.snippets.animation'); var Quiz = require('website_slides.quiz').Quiz; sAnimations.registry.websiteSlidesQuizNoFullscreen.include({ - _extractChannelData: function (slideData){ + _extractChannelData: function (slideData) { return _.extend({}, this._super.apply(this, arguments), { productId: slideData.productId, enroll: slideData.enroll, diff --git a/addons/website_sale_slides/static/src/xml/slide_course_join.xml b/addons/website_sale_slides/static/src/xml/slide_course_join.xml new file mode 100644 index 000000000000..404386d7c36d --- /dev/null +++ b/addons/website_sale_slides/static/src/xml/slide_course_join.xml @@ -0,0 +1,15 @@ +<?xml version="1.0" encoding="UTF-8"?> +<templates xml:space="preserve"> + <t t-extend="slide.course.join"> + <t t-jquery=".o_wslides_js_course_join_link" t-operation="append"> + <t t-if="widget.channel.channelEnroll == 'payment'"> + <t t-if="widget.publicUser"> + Sign in + </t> + <t t-else=""> + Buy course + </t> + </t> + </t> + </t> +</templates> diff --git a/addons/website_sale_slides/static/src/xml/website_sale_slides_quiz.xml b/addons/website_sale_slides/static/src/xml/website_sale_slides_quiz.xml index 9392a8bff210..5a371469a49b 100644 --- a/addons/website_sale_slides/static/src/xml/website_sale_slides_quiz.xml +++ b/addons/website_sale_slides/static/src/xml/website_sale_slides_quiz.xml @@ -1,43 +1,15 @@ <?xml version="1.0" encoding="UTF-8"?> <templates xml:space="preserve"> <t t-extend="slide.slide.quiz.validation"> - <t t-jquery="#validation" t-operation="append"> - <div t-if="widget.readonly && widget.publicUser && widget.channel.channelEnroll == 'payment'" class="alert alert-info d-flex align-items-center justify-content-between"> - <div> - <b class="h5 mb-0">Sign in and buy the course to join it and take the quiz!</b> - <span class="my-0 h4" style="line-height: 1"> - <span 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> - </div> - <div> - <a t-att-href="'/web/login?redirect=' + widget.redirectURL" class="btn btn-primary font-weight-bold text-uppercase">Sign in</a> - <span t-if="widget.channel.signupAllowed" class="d-block mt-2">Don't have an account ? <a class="font-weight-bold" t-att-href="'/web/signup?redirect=' + widget.url">Sign Up !</a></span> - </div> - </div> - <div t-if="widget.readonly && !widget.publicUser && widget.channel.channelEnroll == 'payment'" class="card col-md-3"> - <div class="card-body d-flex flex-column align-items-center"> - <a role="button" - class="btn btn-primary btn-block o_wslides_js_course_join_link d-block mb-2" - title="Start Course" aria-label="Start Course Channel" - t-attf-href="/shop/cart/update?product_id=#{widget.channel.productId}&express=1" - t-att-data-channel-id="widget.slide.channelId"> - <span class="text-white text-uppercase font-weight-bold"> - Buy the course - </span> - </a> - <div class="text-center"> - <del t-if="widget.channel.hasDiscountedPrice" - class="text-600 text-nowrap oe_default_price d-inline" - t-esc="widget.channel.listPrice"/> - <span class="oe_price font-weight-bold text-nowrap h3 my-2" - t-esc="widget.channel.price"/> - <span t-if="widget.channel.currencySymbol" class="text-nowrap font-weight-bold h3 my-2" itemprop="priceCurrency" t-esc="widget.channel.currencySymbol"/> - <span t-else="" class="text-nowrap ml-1" itemprop="priceCurrency" t-esc="widget.channel.currencyName"/> - </div> - </div> - </div> + <t t-jquery=".o_wslides_quiz_join_course_message" t-operation="append"> + <span t-if="widget.channel.channelEnroll == 'payment'"> + <t t-if="widget.publicUser"> + Sign in and buy the course to take the quiz + </t> + <t t-else=""> + To validate your answers, buy the course! + </t> + </span> </t> </t> </templates> diff --git a/addons/website_sale_slides/views/assets.xml b/addons/website_sale_slides/views/assets.xml index 65b544695e09..0aad00a5ae18 100644 --- a/addons/website_sale_slides/views/assets.xml +++ b/addons/website_sale_slides/views/assets.xml @@ -4,6 +4,7 @@ <template id="assets_frontend" inherit_id="website.assets_frontend" name="Buy course on quiz"> <xpath expr="//script[last()]" position="after"> <script type="text/javascript" src="/website_sale_slides/static/src/js/slides_course_quiz.js"/> + <script type="text/javascript" src="/website_sale_slides/static/src/js/slides_course_join.js"/> <script type="text/javascript" src="/website_sale_slides/static/src/js/slides_course_unsubscribe.js"/> </xpath> </template> diff --git a/addons/website_slides/controllers/main.py b/addons/website_slides/controllers/main.py index d6bb401c7e27..fb2ce65a55cb 100644 --- a/addons/website_slides/controllers/main.py +++ b/addons/website_slides/controllers/main.py @@ -138,6 +138,10 @@ class WebsiteSlides(WebsiteProfile): } for answer in question.sudo().answer_ids], } for question in slide.question_ids] } + if 'slide_answer_quiz' in request.session: + slide_answer_quiz = json.loads(request.session['slide_answer_quiz']) + if str(slide.id) in slide_answer_quiz: + values['session_answers'] = slide_answer_quiz[str(slide.id)] values.update(self._get_slide_quiz_partner_info(slide)) return values @@ -239,6 +243,22 @@ class WebsiteSlides(WebsiteProfile): domain = expression.AND([domain, [('partner_ids', '=', request.env.user.partner_id.id)]]) return domain + def _channel_remove_session_answers(self, channel, slide=False): + """ Will remove the answers saved in the session for a specific channel / slide. """ + + if 'slide_answer_quiz' not in request.session: + return + + slides_domain = [('channel_id', '=', channel.id)] + if slide: + slides_domain = expression.AND([slides_domain, [('id', '=', slide.id)]]) + slides = request.env['slide.slide'].search_read(slides_domain, ['id']) + + session_slide_answer_quiz = json.loads(request.session['slide_answer_quiz']) + for slide in slides: + session_slide_answer_quiz.pop(str(slide['id']), None) + request.session['slide_answer_quiz'] = json.dumps(session_slide_answer_quiz) + # -------------------------------------------------- # SLIDE.CHANNEL MAIN / SEARCH # -------------------------------------------------- @@ -538,7 +558,9 @@ class WebsiteSlides(WebsiteProfile): @http.route(['/slides/channel/leave'], type='json', auth='user', website=True) def slide_channel_leave(self, channel_id): - request.env['slide.channel'].browse(channel_id)._remove_membership(request.env.user.partner_id.ids) + channel = request.env['slide.channel'].browse(channel_id) + channel._remove_membership(request.env.user.partner_id.ids) + self._channel_remove_session_answers(channel) return True @http.route(['/slides/channel/tag/search_read'], type='json', auth='user', methods=['POST'], website=True) @@ -782,7 +804,7 @@ class WebsiteSlides(WebsiteProfile): return result # -------------------------------------------------- - # QUIZZ SECTION + # QUIZ SECTION # -------------------------------------------------- @http.route('/slides/slide/quiz/question_add_or_update', type='json', methods=['POST'], auth='user', website=True) @@ -865,6 +887,7 @@ class WebsiteSlides(WebsiteProfile): slide = fetch_res['slide'] if slide.user_membership_id.sudo().completed: + self._channel_remove_session_answers(slide.channel_id, slide) return {'error': 'slide_quiz_done'} all_questions = request.env['slide.question'].sudo().search([('slide_id', '=', slide.id)]) @@ -889,6 +912,7 @@ class WebsiteSlides(WebsiteProfile): 'last_rank': not request.env.user._get_next_rank(), 'level_up': rank_progress['previous_rank']['lower_bound'] != rank_progress['new_rank']['lower_bound'] }) + self._channel_remove_session_answers(slide.channel_id, slide) return { 'answers': { answer.question_id.id: { @@ -904,6 +928,13 @@ class WebsiteSlides(WebsiteProfile): 'rankProgress': rank_progress, } + @http.route(['/slides/slide/quiz/save_to_session'], type='json', auth='public', website=True) + def slide_quiz_save_to_session(self, quiz_answers): + session_slide_answer_quiz = json.loads(request.session.get('slide_answer_quiz', '{}')) + slide_id = quiz_answers['slide_id'] + session_slide_answer_quiz[str(slide_id)] = quiz_answers['slide_answers'] + request.session['slide_answer_quiz'] = json.dumps(session_slide_answer_quiz) + def _get_rank_values(self, user): lower_bound = user.rank_id.karma_min or 0 next_rank = user._get_next_rank() diff --git a/addons/website_slides/data/slide_slide_demo.xml b/addons/website_slides/data/slide_slide_demo.xml index e4761ec3d1c9..fa19dff58f1c 100644 --- a/addons/website_slides/data/slide_slide_demo.xml +++ b/addons/website_slides/data/slide_slide_demo.xml @@ -75,7 +75,7 @@ <field name="channel_id" ref="website_slides.slide_channel_demo_0_gard_0"/> <field name="is_published" eval="True"/> <field name="date_published" eval="datetime.now() - timedelta(days=8)"/> - <field name="is_preview" eval="False"/> + <field name="is_preview" eval="True"/> <field name="public_views">0</field> <field name="completion_time">1</field> <field name="description">Show your newly mastered knowledge !</field> @@ -126,7 +126,6 @@ <field name="question_id" ref="slide_slide_demo_0_4_question_1"/> </record> - <!-- CHANNEL 1: Taking care of Trees --> <!-- ================================================== --> diff --git a/addons/website_slides/static/src/js/slides_course_join.js b/addons/website_slides/static/src/js/slides_course_join.js index f9d558119558..0817f372c31f 100644 --- a/addons/website_slides/static/src/js/slides_course_join.js +++ b/addons/website_slides/static/src/js/slides_course_join.js @@ -3,7 +3,6 @@ odoo.define('website_slides.course.join.widget', function (require) { var core = require('web.core'); var publicWidget = require('web.public.widget'); -require('website_slides.slides'); var _t = core._t; @@ -14,15 +13,75 @@ var CourseJoinWidget = publicWidget.Widget.extend({ 'click .o_wslides_js_course_join_link': '_onClickJoin', }, - init: function (parent, channelId){ - this.channelId = channelId; - return this._super.apply(this, arguments); + /** + * + * Overridden to add options parameters. + * + * @param {Object} parent + * @param {Object} options + * @param {Object} options.channel slide.channel information + * @param {boolean} options.isMember whether current user is member or not + * @param {boolean} options.publicUser whether current user is public or not + * @param {string} [options.joinMessage] the message to use for the simple join case + * when the course if free and the user is logged in, defaults to "Join Course". + * @param {Promise} [options.beforeJoin] a promise to execute before we redirect to + * another url within the join process (login / buy course / ...) + * @param {function} [options.afterJoin] a callback function called after the user has + * joined the course + */ + init: function (parent, options) { + this._super.apply(this, arguments); + this.channel = options.channel; + this.isMember = options.isMember; + this.publicUser = options.publicUser; + this.joinMessage = options.joinMessage || _t('Join Course'), + this.beforeJoin = options.beforeJoin || Promise.resolve(); + this.afterJoin = options.afterJoin || function () {document.location.reload();}; + }, + + //-------------------------------------------------------------------------- + // Handlers + //-------------------------------------------------------------------------- + + /** + * @private + * @param {MouseEvent} ev + */ + _onClickJoin: function (ev) { + ev.preventDefault(); + + if (this.channel.channelEnroll !== 'invite') { + if (this.publicUser) { + this.beforeJoin().then(this._redirectToLogin.bind(this)); + } else if (!this.isMember && this.channel.channelEnroll === 'public') { + this.joinChannel(this.channel.channelId); + } + } }, //-------------------------------------------------------------------------- // Private //-------------------------------------------------------------------------- + /** + * Builds a login page that then redirects to this slide page, or the channel if the course + * is not configured as public enroll type. + * + * @private + */ + _redirectToLogin: function () { + var url; + if (this.channel.channelEnroll === 'public') { + url = window.location.pathname; + if (document.location.href.indexOf("fullscreen") !== -1) { + url += '?fullscreen=1'; + } + } else { + url = `/slides/${this.channel.channelId}`; + } + document.location = _.str.sprintf('/web/login?redirect=%s', encodeURIComponent(url)); + }, + /** * @private * @param {Object} $el @@ -41,14 +100,13 @@ var CourseJoinWidget = publicWidget.Widget.extend({ }, //-------------------------------------------------------------------------- - // Handlers + // Public //-------------------------------------------------------------------------- - /** - * @private + * @public + * @param {integer} channelId */ - _onClickJoin: function (event) { - var channelId = this.channelId || $(event.currentTarget).data('channel-id'); + joinChannel: function (channelId) { var self = this; this._rpc({ route: '/slides/channel/join', @@ -56,8 +114,8 @@ var CourseJoinWidget = publicWidget.Widget.extend({ channel_id: channelId, }, }).then(function (data) { - if (! data.error) { - location.reload(); + if (!data.error) { + self.afterJoin(); } else { if (data.error === 'public_user') { var message = _t('Please <a href="/web/login?redirect=%s">login</a> to join this course'); @@ -86,14 +144,15 @@ publicWidget.registry.websiteSlidesCourseJoin = publicWidget.Widget.extend({ start: function () { var self = this; var proms = [this._super.apply(this, arguments)]; + var data = self.$el.data(); + var options = {channel: {channelEnroll: data.channelEnroll, channelId: data.channelId}}; $('.o_wslides_js_course_join').each(function () { - proms.push(new CourseJoinWidget(self).attachTo($(this))); + proms.push(new CourseJoinWidget(self, options).attachTo($(this))); }); return Promise.all(proms); }, }); - return { courseJoinWidget: CourseJoinWidget, websiteSlidesCourseJoin: publicWidget.registry.websiteSlidesCourseJoin 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 1d3942986cad..50ac2a3cef1a 100644 --- a/addons/website_slides/static/src/js/slides_course_quiz.js +++ b/addons/website_slides/static/src/js/slides_course_quiz.js @@ -24,12 +24,12 @@ odoo.define('website_slides.quiz', function (require) { * - slide_go_next: 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 current slide data. */ - var Quiz= publicWidget.Widget.extend({ + var Quiz = publicWidget.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_js_lesson_quiz_submit": '_submitQuiz', "click .o_wslides_quiz_modal_btn": '_onClickNext', "click .o_wslides_quiz_continue": '_onClickNext', "click .o_wslides_js_lesson_quiz_reset": '_onClickReset', @@ -48,27 +48,27 @@ odoo.define('website_slides.quiz', function (require) { /** * @override * @param {Object} parent - * @param {Object} slide_data holding all the classic slide informations + * @param {Object} slide_data holding all the classic slide information * @param {Object} quiz_data : optional quiz data to display. If not given, will be fetched. (questions and answers). */ init: function (parent, slide_data, channel_data, quiz_data) { + this._super.apply(this, arguments); this.slide = _.defaults(slide_data, { id: 0, name: '', hasNext: false, completed: false, - readonly: false, + isMember: false, }); this.quiz = quiz_data || false; if (this.quiz) { this.quiz.questionsCount = quiz_data.questions.length; } - this.readonly = slide_data.readonly || false; + this.isMember = slide_data.isMember || false; this.publicUser = session.is_website_user; this.userId = session.user_id; this.redirectURL = encodeURIComponent(document.URL); this.channel = channel_data; - return this._super.apply(this, arguments); }, /** @@ -83,15 +83,25 @@ odoo.define('website_slides.quiz', function (require) { }, /** + * 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() { + start: function () { var self = this; return this._super.apply(this, arguments).then(function () { self._renderValidationInfo(); self._bindSortable(); self._checkLocationHref(); - new CourseJoinWidget(self, self.channel.channelId).appendTo(self.$('.o_wslides_course_join_widget')); + if (!self.isMember) { + self._renderJoinWidget(); + } else if (self.slide.sessionAnswers) { + self._applySessionAnswers(); + self._submitQuiz(); + } }); }, @@ -99,20 +109,19 @@ odoo.define('website_slides.quiz', function (require) { // Private //-------------------------------------------------------------------------- - _alertShow: function (alert_code) { + _alertShow: function (alertCode) { var message = _t('There was an error validating this quiz.'); - if (! alert_code || alert_code === 'slide_quiz_incomplete') { + if (alertCode === 'slide_quiz_incomplete') { message = _t('All questions must be answered !'); - } - else if (alert_code === 'slide_quiz_done') { + } else if (alertCode === 'slide_quiz_done') { message = _t('This quiz is already done. Retaking it is not possible.'); - } - else if (alert_code === 'public_user') { + } else if (alertCode === 'public_user') { message = _t('You must be logged to submit the quiz.'); } + this.displayNotification({ type: 'warning', - title: _t('Something went wrong'), + title: _t('Quiz validation error'), message: message, sticky: true }); @@ -122,7 +131,7 @@ odoo.define('website_slides.quiz', function (require) { * Allows to reorder the questions * @private */ - _bindSortable: function() { + _bindSortable: function () { this.$el.sortable({ handle: '.o_wslides_js_quiz_sequence_handler', items: '.o_wslides_js_lesson_quiz_question', @@ -168,7 +177,6 @@ odoo.define('website_slides.quiz', function (require) { } }).then(this._modifyQuestionsSequence.bind(this)) }, - /* * @private * Fetch the quiz for a particular slide @@ -209,14 +217,15 @@ odoo.define('website_slides.quiz', function (require) { var self = this; this.$('.o_wslides_js_lesson_quiz_question').addClass('completed-disabled'); this.$('input[type=radio]').each(function () { - $(this).prop('disabled', self.slide.readonly || self.slide.completed); + $(this).prop('disabled', self.slide.completed); }); }, /** + * Decorate the answer inputs according to the correction and adds the answer comment if + * any. + * * @private - * Decorate the answer inputs according to the correction - * and adds the answer comment if any */ _renderAnswersHighlightingAndComments: function () { var self = this; @@ -224,22 +233,20 @@ odoo.define('website_slides.quiz', function (require) { var $question = $(this); var questionId = $question.data('questionId'); var isCorrect = self.quiz.answers[questionId].is_correct; - $question.find('a.o_wslides_quiz_answer ').each(function () { + $question.find('a.o_wslides_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').addClass('d-none'); $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').addClass('d-none'); $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').addClass('d-none'); $answer.find('i.fa-circle').removeClass('d-none'); } }); @@ -251,6 +258,30 @@ odoo.define('website_slides.quiz', function (require) { }); }, + /** + * Will check if we have answers coming from the session and re-apply them. + */ + _applySessionAnswers: function () { + if (!this.slide.sessionAnswers || this.slide.sessionAnswers.length === 0) { + return; + } + + var self = this; + this.$('.o_wslides_js_lesson_quiz_question').each(function () { + var $question = $(this); + $question.find('a.o_wslides_quiz_answer').each(function () { + var $answer = $(this); + if (!$answer.find('input[type=radio]')[0].checked && + _.contains(self.slide.sessionAnswers, $answer.data('answerId'))) { + $answer.find('input[type=radio]').prop('checked', true); + } + }); + }); + + // reset answers coming from the session + this.slide.sessionAnswers = false; + }, + /* * @private * Update validation box (karma, buttons) according to widget state @@ -261,20 +292,61 @@ odoo.define('website_slides.quiz', function (require) { QWeb.render('slide.slide.quiz.validation', {'widget': this}) ); }, - /* - * Submit the given answer, and display the result + + /** + * Renders the button to join a course. + * If the user is logged in, the course is public, and the user has previously tried to + * submit answers, we automatically attempt to join the course. + * + * @private + */ + _renderJoinWidget: function () { + var $widgetLocation = this.$(".o_wslides_join_course_widget"); + if ($widgetLocation.length !== 0) { + var courseJoinWidget = new CourseJoinWidget(this, { + isQuiz: true, + channel: this.channel, + isMember: this.isMember, + publicUser: this.publicUser, + beforeJoin: this._saveQuizAnswersToSession.bind(this), + afterJoin: this._afterJoin.bind(this), + joinMessage: _t('Join & Submit'), + }); + + courseJoinWidget.appendTo($widgetLocation); + if (!this.publicUser && courseJoinWidget.channel.channelEnroll === 'public' && this.slide.sessionAnswers) { + courseJoinWidget.joinChannel(this.channel.channelId); + } + } + }, + + /** + * 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. * - * @param Array checkedAnswerIds: list of checked answers + * @private */ - _submitQuiz: function (checkedAnswerIds) { + _submitQuiz: function () { var self = this; + return this._rpc({ route: '/slides/slide/quiz/submit', params: { slide_id: self.slide.id, - answer_ids: checkedAnswerIds, + answer_ids: this._getQuizAnswers(), } - }).then(function(data){ + }).then(function (data) { if (data.error) { self._alertShow(data.error); } else { @@ -326,7 +398,7 @@ odoo.define('website_slides.quiz', function (require) { * it goes straight to the 'Add Quiz' button and clicks on it. * @private */ - _checkLocationHref: function() { + _checkLocationHref: function () { if (window.location.href.includes('quiz_quick_create')) { this._onCreateQuizClick(); } @@ -344,10 +416,11 @@ odoo.define('website_slides.quiz', function (require) { */ _onAnswerClick: function (ev) { ev.preventDefault(); - if (! this.slide.readonly && ! this.slide.completed) { + if (!this.slide.completed) { $(ev.currentTarget).find('input[type=radio]').prop('checked', true); } }, + /** * Triggering a event to switch to next slide * @@ -359,6 +432,7 @@ odoo.define('website_slides.quiz', function (require) { this.trigger_up('slide_go_next'); } }, + /** * Resets the completion of the slide so the user can take * the quiz again @@ -371,30 +445,42 @@ odoo.define('website_slides.quiz', function (require) { params: { slide_id: this.slide.id } - }).then(function() { + }).then(function () { window.location.reload(); }); }, /** - * Submit a quiz and get the correction. It will display messages - * according to quiz result. + * Saves the answers from the user and redirect the user to the + * specified url * * @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 === this.quiz.questionsCount){ - this._submitQuiz(values); + _saveQuizAnswersToSession: function () { + var quizAnswers = this._getQuizAnswers(); + if (quizAnswers.length === this.quiz.questions.length) { + return this._rpc({ + route: '/slides/slide/quiz/save_to_session', + params: { + 'quiz_answers': {'slide_id': this.slide.id, 'slide_answers': quizAnswers}, + } + }); } else { - this._alertShow(); + this._alertShow('slide_quiz_incomplete'); + return Promise.reject('The quiz is incomplete'); } }, + /** + * After joining the course, we immediately submit the quiz and get the correction. + * This allows a smooth onboarding when the user is logged in and the course is public. + * + * @private + */ + _afterJoin: function () { + this.isMember = true; + this._renderValidationInfo(); + this._applySessionAnswers(); + this._submitQuiz(); + }, /** * When clicking on 'Add a Question' or 'Add Quiz' it @@ -510,7 +596,7 @@ odoo.define('website_slides.quiz', function (require) { * @param event * @private */ - _deleteQuestion: function(event) { + _deleteQuestion: function (event) { var questionId = event.data.questionId; this.$('.o_wslides_js_lesson_quiz_question[data-question-id=' + questionId + ']').remove(); this.quiz.questionsCount--; @@ -571,7 +657,6 @@ odoo.define('website_slides.quiz', function (require) { self.close(); }); } - }); publicWidget.registry.websiteSlidesQuizNoFullscreen = publicWidget.Widget.extend({ @@ -598,6 +683,7 @@ odoo.define('website_slides.quiz', function (require) { var channelData = self._extractChannelData(slideData); slideData.quizData = { questions: self._extractQuestionsAndAnswers(), + sessionAnswers: slideData.sessionAnswers || [], quizKarmaMax: slideData.quizKarmaMax, quizKarmaWon: slideData.quizKarmaWon, quizKarmaGain: slideData.quizKarmaGain, @@ -612,11 +698,10 @@ odoo.define('website_slides.quiz', function (require) { // Handlers //--------------------------------------------------------------------- _onQuizCompleted: function (ev) { - var self = this; var slide = ev.data.slide; var completion = ev.data.completion; this.$('#o_wslides_lesson_aside_slide_check_' + slide.id).addClass('text-success fa-check').removeClass('text-600 fa-circle-o'); - // need to use global selector as progress bar is ouside this animation widget scope + // need to use global selector as progress bar is outside this animation widget scope $('.o_wslides_lesson_header .progress-bar').css('width', completion + "%"); $('.o_wslides_lesson_header .progress span').text(_.str.sprintf("%s %%", completion)); }, @@ -629,7 +714,7 @@ odoo.define('website_slides.quiz', function (require) { // Private //--------------------------------------------------------------------- - _extractChannelData: function (slideData){ + _extractChannelData: function (slideData) { return { channelId: slideData.channelId, channelEnroll: slideData.channelEnroll, @@ -644,7 +729,7 @@ odoo.define('website_slides.quiz', function (require) { * * @return {Array<Object>} list of questions with answers */ - _extractQuestionsAndAnswers: function() { + _extractQuestionsAndAnswers: function () { var questions = []; this.$('.o_wslides_js_lesson_quiz_question').each(function () { var $question = $(this); diff --git a/addons/website_slides/static/src/xml/slide_course_join.xml b/addons/website_slides/static/src/xml/slide_course_join.xml index a616cc1baa1d..5c890cd027dc 100644 --- a/addons/website_slides/static/src/xml/slide_course_join.xml +++ b/addons/website_slides/static/src/xml/slide_course_join.xml @@ -1,11 +1,17 @@ +<?xml version="1.0" encoding="UTF-8"?> <templates> <t t-name="slide.course.join"> - <div class="p-0 col-md-2"> + <div> <a role="button" - class="btn btn-primary btn-block o_wslides_js_course_join_link text-uppercase font-weight-bold" - title="Start Course" aria-label="Start Course Channel" + class="btn btn-primary o_wslides_js_course_join_link text-uppercase font-weight-bold" + title="Join the Course" aria-label="Join the Course" href="#"> - <span class="cta-title text_small_caps">Join Course</span> + <t t-if="widget.channel.channelEnroll == 'public'"> + <t t-if="widget.publicUser"> + Sign in + </t> + <t t-else="" t-esc="widget.joinMessage" /> + </t> </a> </div> </t> diff --git a/addons/website_slides/static/src/xml/slide_quiz.xml b/addons/website_slides/static/src/xml/slide_quiz.xml index 9cbf68fc7609..e64bc96604ed 100644 --- a/addons/website_slides/static/src/xml/slide_quiz.xml +++ b/addons/website_slides/static/src/xml/slide_quiz.xml @@ -5,7 +5,7 @@ <div class="container"> <div t-foreach="widget.quiz.questions" t-as="question" - t-attf-class="o_wslides_js_lesson_quiz_question mt-3 mb-4 #{widget.readonly ? 'disabled' : ''} #{widget.slide.completed ? 'completed-disabled' : ''}" + t-attf-class="o_wslides_js_lesson_quiz_question mt-3 mb-4 #{widget.slide.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"/> @@ -14,7 +14,7 @@ <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_wslides_quiz_answer list-group-item d-flex align-items-center #{widget.readonly ? 'disabled bg-transparent' : 'list-group-item-action'} #{widget.slide.completed && answer.is_correct ? 'list-group-item-success' : '' }"> + t-attf-class="o_wslides_quiz_answer list-group-item d-flex align-items-center list-group-item-action #{widget.slide.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" @@ -44,27 +44,43 @@ <t t-name="slide.slide.quiz.validation"> <div id="validation"> - <div t-if="widget.readonly && !widget.publicUser && widget.channel.channelEnroll == 'public'" class="o_wslides_course_join_widget"> - <!-- Here comes the Join button rendered by the widget --> - </div> - <div t-if="widget.readonly && widget.publicUser && widget.channel.channelEnroll == 'public'" class="alert alert-info d-flex align-items-center justify-content-between"> - <div> - <b class="h5 mb-0">Sign in and join the course to take the quiz!</b> - <span class="my-0 h4" style="line-height: 1"> - <span 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 + <div t-if="!widget.isMember"> + <div class="o_wslides_join_course alert alert-info d-flex align-items-center justify-content-between"> + <div t-if="widget.channel.channelEnroll == 'invite'"> + <b class="h5 mb-0"> + <b>This course is private. <a href="/contactus" class="font-weight-bold">Contact the website administrator</a> to enroll.</b> + </b> + <span class="my-0 h4"> + <span 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> - </span> - </div> - <div> - <a t-att-href="'/web/login?redirect=' + widget.redirectURL" class="btn btn-primary font-weight-bold text-uppercase">Sign in</a> - <span t-if="widget.channel.signupAllowed" class="d-block mt-2">Don't have an account ? <a class="font-weight-bold" t-att-href="'/web/signup?redirect=' + widget.url">Sign Up !</a></span> + </div> + <div t-else="" class="w-100"> + <b class="h5 mb-0 o_wslides_quiz_join_course_message"> + <span t-if="widget.channel.channelEnroll == 'public'"> + <t t-if="widget.publicUser"> + Sign in and join the course to verify your answers! + </t> + <t t-else=""> + Join the course to take the quiz and verify your answers! + </t> + </span> + </b> + <span class="my-0 h4"> + <span 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> + <div class="o_wslides_join_course_widget float-right"/> + </div> </div> + <span t-if="widget.publicUser && widget.channel.signupAllowed" class="d-block mt-2"> + <span>Don't have an account ?</span> + <a class="font-weight-bold" t-att-href="'/web/signup?redirect=' + widget.url">Sign Up !</a> + </span> </div> - <div t-if="widget.readonly && widget.channel.channelEnroll == 'invite'" class="alert alert-info"> - <b>This course is private. <a href="/contactus" class="font-weight-bold">Contact the website administrator</a> to enroll.</b> - </div> - <div t-if="!widget.readonly" class="d-md-flex align-items-center justify-content-between"> + <div t-else="" class="d-md-flex align-items-center justify-content-between"> <div t-att-class="'d-flex align-items-center' + (widget.slide.completed ? ' alert alert-success my-0 py-1 px-3' : '')"> <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> diff --git a/addons/website_slides/views/website_slides_templates_course.xml b/addons/website_slides/views/website_slides_templates_course.xml index 75396abceb0f..bbdf666ea7e6 100644 --- a/addons/website_slides/views/website_slides_templates_course.xml +++ b/addons/website_slides/views/website_slides_templates_course.xml @@ -287,7 +287,8 @@ class="btn btn-primary btn-block o_wslides_js_course_join_link" title="Start Course" aria-label="Start Course Channel" t-att-href="'#'" - t-att-data-channel-id="channel.id"> + t-att-data-channel-id="channel.id" + t-att-data-channel-enroll="channel.enroll"> <span class="cta-title text_small_caps"> <t t-if="channel.channel_type == 'documentation'">Start Course</t> <t t-else="">Join Course</t> diff --git a/addons/website_slides/views/website_slides_templates_lesson.xml b/addons/website_slides/views/website_slides_templates_lesson.xml index 7ddaa1e97b3a..f731efce192d 100644 --- a/addons/website_slides/views/website_slides_templates_lesson.xml +++ b/addons/website_slides/views/website_slides_templates_lesson.xml @@ -442,7 +442,7 @@ 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.channel_id.is_member" + t-att-data-is-member="slide.channel_id.is_member" t-att-data-completed="1 if slide_completed else 0" t-att-data-quiz-attempts-count="quiz_attempts_count" t-att-data-quiz-karma-max="quiz_karma_max" @@ -453,7 +453,8 @@ t-att-data-channel-id="slide.channel_id.id" t-att-data-channel-enroll="slide.channel_id.enroll" t-att-data-channel-can-upload="slide.channel_id.can_upload" - t-att-data-signup-allowed="signup_allowed"> + t-att-data-signup-allowed="signup_allowed" + t-att-data-session-answers="session_answers"> <t t-foreach="slide_questions" t-as="question"> <t t-call="website_slides.lesson_content_quiz_question"/> </t> @@ -463,7 +464,7 @@ </template> <template id="lesson_content_quiz_question" name="Lesson: Quiz question template"> - <div t-att-class="'o_wslides_js_lesson_quiz_question mt-3 %s' % ('completed-disabled' if slide_completed else ('disabled' if not slide.channel_id.is_member else ''))" + <div t-att-class="'o_wslides_js_lesson_quiz_question mt-3 %s' % ('completed-disabled' if slide_completed else ('disabled' if not (slide.channel_id.is_member or slide.is_preview) else ''))" t-att-data-question-id="question['id']" t-att-data-title="question['question']" > <div class="row d-flex mb-2 mx-0"> <div class="h4"> 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 1eb17f650a22..4069a88f7ad0 100644 --- a/addons/website_slides/views/website_slides_templates_lesson_fullscreen.xml +++ b/addons/website_slides/views/website_slides_templates_lesson_fullscreen.xml @@ -10,7 +10,8 @@ <div class="o_wslides_fs_main d-flex flex-column font-weight-light" t-att-data-channel-id="slide.channel_id.id" t-att-data-channel-enroll="slide.channel_id.enroll" - t-att-data-signup-allowed="signup_allowed"> + t-att-data-signup-allowed="signup_allowed" + t-att-data-session-answers="session_answers"> <div class="o_wslides_slide_fs_header d-flex flex-shrink-0 text-white"> <div class="d-flex"> @@ -97,8 +98,8 @@ t-att-data-is-quiz="0" t-att-data-completed="1 if slide_completed else 0" t-att-data-embed-code="slide.embed_code if slide.slide_type in ['video', 'document', 'presentation', 'infographic'] else False" - t-att-data-readonly="not is_member" - > + t-att-data-is-member="is_member" + t-att-data-session-answers="session_answers"> <span class="ml-3"> <i t-if="slide_completed and is_member" class="o_wslides_slide_completed fa fa-check fa-fw text-success" t-att-data-slide-id="slide.id"/> <i t-if="not slide_completed and is_member" class="fa fa-circle-thin fa-fw" t-att-data-slide-id="slide.id"/> @@ -147,7 +148,8 @@ t-att-data-has-question="1 if slide.question_ids else 0" t-att-data-is-quiz="1" t-att-data-completed="1 if slide_completed else 0" - t-att-data-readonly="not is_member"> + t-att-data-is-member="is_member" + t-att-data-session-answers="session_answers"> <a t-if="can_access" class="o_wslides_fs_slide_quiz" href="#" t-att-index="i"> <i class="fa fa-flag-checkered text-warning mr-2"/>Quiz </a> -- GitLab