diff options
11 files changed, 634 insertions, 685 deletions
diff --git a/addons/website_slides/controllers/main.py b/addons/website_slides/controllers/main.py index f8ebb91a70b..70137487c0c 100644 --- a/addons/website_slides/controllers/main.py +++ b/addons/website_slides/controllers/main.py @@ -622,8 +622,10 @@ class WebsiteSlides(WebsiteProfile): 'quizKarmaWon': quiz_info['quiz_karma_won'], } - @http.route('/slides/slide/quiz/submit', type="json", auth="user", website=True) + @http.route('/slides/slide/quiz/submit', type="json", auth="public", website=True) def slide_quiz_submit(self, slide_id, answer_ids): + if request.website.is_public_user(): + return {'error': 'public_user'} fetch_res = self._fetch_slide(slide_id) if fetch_res.get('error'): return fetch_res diff --git a/addons/website_slides/models/slide_slide.py b/addons/website_slides/models/slide_slide.py index 53378df947c..c98d0b9a69f 100644 --- a/addons/website_slides/models/slide_slide.py +++ b/addons/website_slides/models/slide_slide.py @@ -512,7 +512,7 @@ class Slide(models.Model): slide_partner = slide_partners_map.get(slide.id) if slide.question_ids and slide_partner: if slide_partner.quiz_attempts_count: - result[slide.id]['quiz_karma_gain'] = gains[slide_partner.quiz_attempts_count] if slide_partner.quiz_attempts_count <= len(gains) else gains[-1] + result[slide.id]['quiz_karma_gain'] = gains[slide_partner.quiz_attempts_count] if slide_partner.quiz_attempts_count < len(gains) else gains[-1] result[slide.id]['quiz_attempts_count'] = slide_partner.quiz_attempts_count if quiz_done or slide_partner.completed: result[slide.id]['quiz_karma_won'] = gains[slide_partner.quiz_attempts_count-1] if slide_partner.quiz_attempts_count < len(gains) else gains[-1] 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 b7165e92d4d..f8c34ee222f 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 @@ -7,414 +7,561 @@ odoo.define('website_slides.fullscreen', function (require) { var core = require('web.core'); var QWeb = core.qweb; - var tag = document.createElement('script'); + var Quiz = require('website_slides.quiz'); - var QuizWidget = require('website_slides.quiz'); - tag.src = "https://www.youtube.com/iframe_api"; - var firstScriptTag = document.getElementsByTagName('script')[0]; - firstScriptTag.parentNode.insertBefore(tag, firstScriptTag); + /** + * Helper: Get the slide dict matching the given criteria + * + * @private + * @param {Array<Object>} slideList List of dict reprensenting a slide + * @param {Object} matcher (see https://underscorejs.org/#matcher) + */ + var findSlide = function (slideList, matcher) { + var slideMatch = _.matcher(matcher); + return _.find(slideList, slideMatch); + }; + /** + * This widget is responsible of display Youtube Player + * + * The widget will trigger an event `change_slide` when the video is at + * its end, and `slide_completed` when the player is at 30 sec before the + * end of the video (30 sec before is considered as completed). + */ + var VideoPlayer = Widget.extend({ + template: 'website.slides.fullscreen.video', + youtubeUrl: 'https://www.youtube.com/iframe_api', - var Fullscreen = Widget.extend({ - custom_events: { - quiz_next_slide: '_goToNextSlide', - quiz_completed: '_onQuizCompleted' - }, - /** - * @override - * @param {Object} el - * @param {Object} data holding channelId and optionally upload and publish control parameters - */ - init: function (el, courseId, slideId, userId) { - this.courseID = parseInt(courseId, 10); - this.slideID = parseInt(slideId, 10); - this.userID = parseInt(userId, 10); - this.course = undefined; - this.slide = undefined; - this.slides = []; - this.nextSlide = undefined; - this.previousSlide = undefined; - this.url = undefined; - this.urlToSmallScreen = undefined; - this.activetab = undefined; - this.player = undefined; - this.goToQuiz = false; - this.slideTitle = undefined; - return this._super.apply(this,arguments); + init: function (parent, slide) { + this.slide = slide; + return this._super.apply(this, arguments); }, start: function (){ var self = this; - this.url = window.location.pathname; - this.urlToSmallScreen = this.url.replace('/fullscreen',''); - this._getSlides(); - this._renderPlayer(); - this._bindListEvents(); - $(document).keydown(this._onKeyDown.bind(this)); - return this._super.apply(this, arguments); + return $.when(this._super.apply(this, arguments), this._loadYoutubeAPI()).then(function() { + self._setupYoutubePlayer(); + }); }, - //-------------------------------------------------------------------------- - // Private - //-------------------------------------------------------------------------- - /** - * @private - * Renders the player accordingly to the current slide - */ - _renderPlayer: function (){ - var self = this; - + _loadYoutubeAPI: function () { 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: '', - embed_url: embed_url, - question_count: self.slide.quiz ? self.slide.quiz.questions.length : '', - letters: self.slide.quiz ? self.letters : '', - showMiniQuiz: self.goToQuiz - })); + if ($(document).find('script[src="' + this.youtubeUrl + '"]').length === 0) { + var $youtubeElement = $('<script/>', {src: this.youtubeUrl}); + $(document.head).append($youtubeElement); - if(this.slide.slide_type === "video"){ - this._renderYoutubeIframe(); - } + // function called when the Youtube asset is loaded + // see https://developers.google.com/youtube/iframe_api_reference#Requirements + onYouTubeIframeAPIReady = function () { def.resolve(); - } - } - - 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 { - self._setupYoutubePlayer(); - } - catch (error) { - onYouTubeIframeAPIReady = function (){ - var self = this; - self._setupYoutubePlayer(); - }.bind(this); + } else { + def.resolve(); } - }, - _renderWebpage: function (){ - var self = this; - $(self.slide.htmlContent).appendTo('.o_wslides_fs_webpage_content'); - }, - - _renderQuiz: function (){ - 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', { - slide: self.slide, - miniQuiz: self.goToQuiz - })); + return def; }, /** - * @private * Links the youtube api to the iframe present in the template + * + * @private */ _setupYoutubePlayer: function (){ - var self = this; - self.player = new YT.Player('youtube-player', { + this.player = new YT.Player('youtube-player' + this.slide.id, { host: 'https://www.youtube.com', - playerVars: {'autoplay': 1, 'origin': window.location.origin}, - autoplay: 1, + playerVars: { + 'autoplay': 1, + 'origin': window.location.origin + }, events: { - 'onReady': self._onPlayerReady, - 'onStateChange': this._onPlayerStateChange.bind(self) + 'onStateChange': this._onPlayerStateChange.bind(this) } }); }, /** - * @param {*} event * Specific method of the youtube api. * Whenever the player starts playing, a setinterval is created. * This setinterval is used to check te user's progress in the video. - * Once the user reaches a particular time in the video, the slide will be considered as completed if the video doesn't have a mini-quiz. - * This method also allows to automatically go to the next slide (or the quiz associated to the current video) once the video is over + * Once the user reaches a particular time in the video, the slide will be considered as completed + * if the video doesn't have a mini-quiz. + * This method also allows to automatically go to the next slide (or the quiz associated to the current + * video) once the video is over + * + * @private + * @param {*} event */ _onPlayerStateChange: function (event){ var self = this; - var tid; clearInterval(self.tid); - if (event.data === YT.PlayerState.PLAYING && !self.slide.done) { + if (event.data === YT.PlayerState.PLAYING && !self.slide.completed) { self.tid = setInterval(function (){ if (event.target.getCurrentTime){ var currentTime = event.target.getCurrentTime(); var totalTime = event.target.getDuration(); if (totalTime && currentTime > totalTime - 30){ clearInterval(self.tid); - if (!self.slide.hasQuiz && !self.slide.done){ - self.slide.done = true; - self._setSlideStateAsDone(); + if (!self.slide.hasQuestion && !self.slide.completed){ + self.trigger_up('slide_completed', self.slide); } } } }, 1000); } if (event.data === YT.PlayerState.ENDED){ - self.player = undefined; - self._goToNextSlide(); + this.player = undefined; + if (this.slide.hasNext) { + this.trigger_up('slide_go_next'); + } } }, + }); + + + /** + * This widget is responsible of navigation for one slide to another: + * - by clicking on any slide list entry + * - by mouse click (next / prev) + * - by recieving the order to go to prev/next slide (`goPrevious` and `goNext` public methods) + * + * The widget will trigger an event `change_slide` with + * the `slideId` and `isMiniQuiz` as data. + */ + var Sidebar = Widget.extend({ + events: { + "click .o_wslides_fs_sidebar_list_item": '_onClickTab', + }, + init: function (parent, slideList, defaultSlide) { + var result = this._super.apply(this, arguments); + this.slideEntries = slideList; + this.set('slideEntry', defaultSlide); + return result; + }, + start: function (){ + var self = this; + this.on('change:slideEntry', this, this._onChangeCurrentSlide); + return this._super.apply(this, arguments).then(function (){ + $(document).keydown(self._onKeyDown.bind(self)); + }); + }, + destroy: function () { + $(document).unbind('keydown', this._onKeyDown.bind(this)); + return this._super.apply(this, arguments); + }, + //-------------------------------------------------------------------------- + // Public + //-------------------------------------------------------------------------- /** - * @private - * Creates slides objects from every slide-list-cells attributes + * Change the current slide with the next one (if there is one). + * + * @public */ - _getSlides: function (){ - var self = this; - var slides = $('.o_wslides_fs_sidebar_slide_tab'); - for (var i = 0; i < slides.length;i++){ - var slide = $(slides[i]); - self.slides.push(slide.data()); - this._getActiveSlide(); + goNext: function () { + var currentIndex = this._getCurrentIndex(); + if (currentIndex < this.slideEntries.length-1) { + this.set('slideEntry', this.slideEntries[currentIndex+1]); + } + }, + /** + * Change the current slide with the previous one (if there is one). + * + * @public + */ + goPrevious: function () { + var currentIndex = this._getCurrentIndex(); + if (currentIndex >= 1) { + this.set('slideEntry', this.slideEntries[currentIndex-1]); } }, + /** + * Greens up the bullet when the slide is completed + * + * @public + * @param {*} slide_id + */ + setSlideCompleted: function (slideId) { + var $elem = this.$('.fa-circle-thin[data-slide-id="'+slideId+'"]'); + $elem.removeClass('fa-circle-thin').addClass('fa-check-circle text-success'); + }, + /** + * Updates the progressbar whenever a lesson is completed + * + * @public + * @param {*} channelCompletion + */ + updateProgressbar: function (channelCompletion) { + var completion = Math.min(100, channelCompletion); + this.$('.progress-bar').css('width', completion + "%" ); + this.$('.o_wslides_progress_percentage').text(completion); + }, + //-------------------------------------------------------------------------- + // Private + //-------------------------------------------------------------------------- /** - * @private + * Get the index of the current slide entry (slide and/or quiz) */ - _fetchHtmlContent: function(){ - var self = this; - return self._rpc({ - route: 'slides/slide/get_html_content', - params: { - 'slide_id': self.slide.id - } - }).then(function (data){ - if (data.html_content) { - self.slide.htmlContent = data.html_content; - } + _getCurrentIndex: function () { + var slide = this.get('slideEntry'); + var currentIndex = _.findIndex(this.slideEntries, function (entry) { + return entry.id === slide.id && entry.isQuiz === slide.isQuiz; }); + return currentIndex; }, + //-------------------------------------------------------------------------- + // Handler + //-------------------------------------------------------------------------- /** + * Handler called whenever the user clicks on a sub-quiz which is linked to a slide. + * This does NOT handle the case of a slide of type "quiz". + * By going through this handler, the widget will be able to determine that it has to render + * the associated quiz and not the main content. + * * @private - * Once the completion conditions are filled, - * sends a json request to the backend to set the relation between the slide and the user as being completed + * @param {*} ev */ - _setSlideStateAsDone: function (){ - var self = this; - return self._rpc({ - route: '/slides/slide/set_completed', - params: { - slide_id: self.slide.id, - } - }).then(function (data) { - if (! data.error) { - $('#check-'+self.slide.id).replaceWith($('<i class="check-done o_wslides_slide_completed fa fa-check-circle"></i>')); - self.slide.done = true; - clearInterval(self.tid); - self.channelCompletion = data.channel_completion; - self._updateProgressbar(); - } + _onClickMiniQuiz: function (ev){ + var slideID = parseInt($(ev.currentTarget).data().slide_id); + this.set('slideEntry',{ + slideID: slideID, + isMiniQuiz: true }); + this.trigger_up('change_slide', this.get('slideEntry')); }, - _updateProgressbar: function () { - var completion = _.min([this.channelCompletion, 100]); - this.$('.o_wslides_fs_sidebar_progressbar .progress-bar').css('width', completion + "%" ); - this.$('.o_wslides_progress_percentage').text(completion); + /** + * Handler called when the user clicks on a normal slide tab + * + * @private + * @param {*} ev + */ + _onClickTab: function (ev) { + var $elem = $(ev.currentTarget); + var isQuiz = $elem.data('isQuiz'); + var slideID = parseInt($elem.data('id')); + var slide = findSlide(this.slideEntries, {id: slideID, isQuiz: isQuiz}); + this.set('slideEntry', slide); }, /** + * Actively changes the active tab in the sidebar so that it corresponds + * the slide currently displayed + * * @private - * Creates an array of letters to be used in the quiz with a foreach */ - _generateQuizLetters: function (){ - var letters = []; - for (var i = 65; i < 91; i++){ - letters.push(String.fromCharCode(i)); + _onChangeCurrentSlide: function () { + var slide = this.get('slideEntry'); + this.$('.o_wslides_fs_sidebar_list_item.active').removeClass('active'); + var selector = '.o_wslides_fs_sidebar_list_item[data-id='+slide.id+']'; + if (slide.isQuiz) { + selector += '[data-is-quiz="1"]'; + } else { + selector += '[data-is-quiz!="1"]'; } - return letters; + this.$(selector).addClass('active'); + this.trigger_up('change_slide', this.get('slideEntry')); }, - _goToNextSlide: function (){ - var self = this; - clearInterval(self.tid); - self.player = undefined; - self.goToQuiz = self.slide.hasQuiz && !self.goToQuiz && self.slide.slide_type !== 'quiz'; - if (self.nextSlide && !self.goToQuiz){ - self.slide = self.nextSlide; - self.index++; - self._setActiveTab(); - self._renderPlayer(); - self._setPreviousAndNextSlides(); - self._updateUrl(); - history.pushState(null,'',self.url); + + /** + * Binds left and right arrow to allow the user to navigate between slides + * + * @param {*} ev + * @private + */ + _onKeyDown: function (ev){ + switch (ev.key){ + case "ArrowLeft": + this.goPrevious(); + break; + case "ArrowRight": + this.goNext(); + break; } - else if (self.nextSlide){ - self._renderPlayer(); + }, + }); + + + /** + * This widget's purpose is to show content of a course, naviguating through contents + * and correclty display it. It also handle slide completion, course progress, ... + * + * This widget is rendered sever side, and attached to the existing DOM. + */ + var Fullscreen = Widget.extend({ + events: { + "click .o_wslides_fs_toggle_sidebar": '_onClickToggleSidebar', + }, + custom_events: { + 'change_slide': '_onChangeSlideRequest', + 'toggle_sidebar': '_onToggleSidebar', + 'slide_completed': '_onSlideCompleted', + 'quiz_completed': '_onSlideCompleted', + 'slide_go_next': '_onSlideGoToNext', + }, + /** + * @override + * @param {Object} el + * @param {Object} slides Contains the list of all slides of the course + * @param {integer} defaultSlideId Contains the ID of the slide requested by the user + */ + init: function (el, slides, defaultSlideId){ + var result = this._super.apply(this,arguments); + this.initialSlideID = defaultSlideId; + this.slides = this._preprocessSlideData(slides); + + var slide; + if (defaultSlideId) { + slide = findSlide(this.slides, {id: defaultSlideId}); + } else { + slide = this.slides[0]; } + + this.set('slide', slide); + + this.sidebar = new Sidebar(this, this.slides, slide); + return result; }, - _goToPreviousSlide: function (){ + /** + * @override + */ + start: function (){ var self = this; - clearInterval(self.tid); - self.goToQuiz = false; - self.player = undefined; - if (self.previousSlide){ - self.slide = self.previousSlide; - self.index--; - self._setActiveTab(); - self._renderPlayer(); - self._setPreviousAndNextSlides(); - self._updateUrl(); - history.pushState(null,'',self.url); - } + this.on('change:slide', this, this._onChangeSlide); + return this._super.apply(this, arguments).then(function () { + self._onChangeSlide(); // trigger manually once DOM ready, since slide content is not rendered server side + }); + }, + /** + * Extended to attach sub widget to sub DOM. This might be experimental but + * seems working fine. + * + * @override + */ + attachTo: function (){ + var defs = [this._super.apply(this, arguments)]; + defs.push(this.sidebar.attachTo(this.$('.o_wslides_fs_sidebar'))); + return $.when.apply($, defs); }, - _setPreviousAndNextSlides: function (){ + //-------------------------------------------------------------------------- + // Private + //-------------------------------------------------------------------------- + /** + * Fetches content with an rpc call for slides of type "webpage" + * + * @private + */ + _fetchHtmlContent: function (){ var self = this; - self.previousSlide = self.index > 0 ? self.slides[self.index-1] : undefined; - self.nextSlide = self.index < (self.slides.length - 1) ? self.slides[self.index+1] : undefined; + var currentSlide = this.get('slide'); + return self._rpc({ + route:"/slides/slide/get_html_content", + params: { + 'slide_id': currentSlide.id + } + }).then(function (data){ + if (data.html_content) { + currentSlide.htmlContent = data.html_content; + } + }); + }, + /** + * Fetches slide content depending on its type. + * If the slide doesn't need to fetch any content, return a resolved deferred + * + * @private + */ + _fetchSlideContent: function (){ + var slide = this.get('slide'); + if (slide.type === 'webpage') { + return this._fetchHtmlContent(); + } + return $.when(); + }, + /** + * Extend the slide data list to add informations about rendering method, and other + * specific values according to their slide_type. + */ + _preprocessSlideData: function (slidesDataList) { + slidesDataList.forEach(function (slideData, index) { + // compute hasNext slide + slideData.hasNext = index < slidesDataList.length-1; + // compute embed url + if (slideData.type === 'video') { + slideData.embedCode = $(slideData.embedCode).attr('src'); // embedCode containts an iframe tag, where src attribute is the url (youtube or embed document from odoo) + slideData.embedUrl = "https://" + slideData.embedCode + "&rel=0&autoplay=1&enablejsapi=1&origin=" + window.location.origin; + } else if (slideData.type === 'infographic') { + slideData.embedUrl = _.str.sprintf('/web/image/slide.slide/%s/image', slideData.id); + } else if (_.contains(['document', 'presentation'], slideData.type)) { + slideData.embedUrl = $(slideData.embedCode).attr('src'); + } + // fill empty property to allow searching on it with _.filter(list, matcher) + slideData.isQuiz = !!slideData.isQuiz; + slideData.hasQuestion = !!slideData.hasQuestion; + // technical settings for the Fullscreen to work + slideData._autoSetDone = _.contains(['infographic', 'presentation', 'document', 'webpage'], slideData.type) && !slideData.hasQuestion; + }); + return slidesDataList; }, /** * Changes the url whenever the user changes slides. - * This allows the user to refresh the page and stay at the right video + * This allows the user to refresh the page and stay on the right slide + * + * @private */ - _updateUrl: function (){ - var self = this; - var url = window.location.pathname.split('/'); - url[url.length-1] = self.slide.slug; - url = url.join('/'); - self.url = url; - self.urlToSmallScreen = self.url; - self.url += "?fullscreen=1"; - $('.o_wslides_small_screen').attr('href', self.urlToSmallScreen); + _pushUrlState: function (){ + var urlParts = window.location.pathname.split('/'); + urlParts[urlParts.length-1] = this.get('slide').slug; + var url = _.str.sprintf('%s?fullscreen=1', urlParts.join('/')); + history.pushState(null, '', url); }, /** - * Whenever the user changes slide, change the active tab + * Render the current slide content using specific mecanism according to slide type: + * - simply append content (for webpage) + * - template rendering (for image, document, ....) + * - using a sub widget (quiz and video) + * + * @private + * @returns Deferred */ - _setActiveTab: function (){ - var self = this; - self.activeTab.removeClass('active'); - $('li.active').removeClass('active'); - $('li[data-slide-id='+self.slide.id+']').addClass('active'); - self.activeTab = $('.o_wslides_fs_sidebar_slide_tab[index="'+self.index+'"]'); - self.activeTab.addClass('active'); + _renderSlide: function () { + var slide = this.get('slide'); + var $content = this.$('.o_wslides_fs_content'); + $content.empty(); + $content.removeClass('bg-white'); // webpage case + + // display quiz slide, or quiz attached to a slide + if (slide.type === 'quiz' || slide.isQuiz) { + var QuizWidget = new Quiz(this, slide); + return QuizWidget.appendTo($content); + } + + // render slide content + if (_.contains(['document', 'presentation', 'infographic'], slide.type)) { + $content.html(QWeb.render('website.slides.fullscreen.content', {widget: this})); + } else if (slide.type === 'video') { + this.videoPlayer = new VideoPlayer(this, slide); + return this.videoPlayer.appendTo($content); + } else if (slide.type === 'webpage'){ + $(slide.htmlContent).appendTo($content); + $content.addClass('bg-white'); + } + return $.when(); }, /** - * The first time the user gets on the player, - * get the slide that is represented by the active tab in the sidebar + * Once the completion conditions are filled, + * rpc call to set the the relation between the slide and the user as "completed" + * + * @private + * @param {Integer} slideId: the id of slide to set as completed */ - _getActiveSlide: function (){ + _setCompleted: function (slideId){ var self = this; - self.activeTab = $('.o_wslides_fs_sidebar_slide_tab.active'); - self.index = parseInt(self.activeTab.attr('index'), 10); - self.slide = self.slides[self.index]; - self._setPreviousAndNextSlides(); + var slide = findSlide(this.slides, {id: slideId}); + if (!slide.completed) { // no useless RPC call + return this._rpc({ + route: '/slides/slide/set_completed', + params: { + slide_id: slide.id, + } + }).then(function (data){ + slide.completed = true; + self.sidebar.setSlideCompleted(slide.id); + self.sidebar.updateProgressbar(data.channel_completion); + }); + } + return $.when(); }, //-------------------------------------------------------------------------- // Handlers //-------------------------------------------------------------------------- - _onListCellClick: function (ev){ - var self = this; - clearInterval(self.tid); - self.player = undefined; - var target = $(ev.currentTarget); - self.goToQuiz = false; - if (target[0] !== self.activeTab[0]){ - self.activeTab.removeClass('active'); - target.addClass('active'); - self.index = parseInt(target.attr('index')); - self._getActiveSlide(); - self._renderPlayer(); - $('li.active').removeClass('active'); - $('li[data-slide-id='+self.slide.id+']').addClass('active'); - self._setPreviousAndNextSlides(); - self._updateUrl(); - history.pushState(null,'',self.url); - } - }, - _sidebarToggle: function (ev){ - ev.preventDefault(); - $(ev.currentTarget).toggleClass('active'); - $('.o_wslides_fs_sidebar').toggleClass('o_wslides_fs_sidebar_hidden'); - $('.o_wslides_fs_player').toggleClass('o_wslides_fs_player_no_sidebar'); - }, - _onMiniQuizClick: function (ev){ - var self = this; - self.index = parseInt($(ev.currentTarget).attr('index')) || 0; - self.slide = self.slides[self.index]; - self.goToQuiz = true; - self._setPreviousAndNextSlides(); - self._renderPlayer(); - self._setActiveTab(); - self._updateUrl(); - history.pushState(null,'' ,self.url); - }, - _onQuizCompleted: function (ev) { - this._setSlideStateAsDone(); - }, - /** - * @private - * Binds events related to the list - */ - _bindListEvents: function (){ + /** + * Method called whenever the user changes slides. + * When the current slide is changed, it will autoimatically update the widget + * and allow it to fetch the content if needs be, render it, update the url and set the + * slide as "completed" if its type makes it necessary + * + * @private + */ + _onChangeSlide: function (){ var self = this; - $('.o_wslides_fs_sidebar_slide_tab').each(function () { - $(this).click(self._onListCellClick.bind(self)); - }); - $('.o_wslides_fs_slide_quiz ').each(function (){ - $(this).click(self._onMiniQuizClick.bind(self)); + var slide = this.get('slide'); + return this._fetchSlideContent().then(function() { // update title and render content + self.$('.o_wslides_fs_slide_title').html(QWeb.render('website.slides.fullscreen.title', {widget: self})); + return self._renderSlide(); + }).then(function() { + if (slide._autoSetDone) { + self._setCompleted(slide.id); + } + self._pushUrlState(); }); - $('.o_wslides_fs_toggle_sidebar').click(function (ev){ - self._sidebarToggle(ev); + }, + /** + * Changes current slide when receiving custom event `change_slide` with + * its id and if it's its quizz or not we need to display. + * + * @private + */ + _onChangeSlideRequest: function (ev){ + var slideData = ev.data; + var newSlide = findSlide(this.slides, { + id: slideData.id, + isQuiz: slideData.isQuiz || false, }); + this.set('slide', newSlide); }, - _onKeyDown: function (ev){ - var self = this; - switch (ev.key){ - case "ArrowRight": - self._goToNextSlide(); - break; - case "ArrowLeft": - self._goToPreviousSlide(); - break; - } + /** + * Triggered when sub widget business is done and that slide + * can now be marked as done. + * + * @private + */ + _onSlideCompleted: function (ev) { + var slideId = ev.data.id; + this._setCompleted(slideId); + }, + /** + * Go the next slide + * + * @private + */ + _onSlideGoToNext: function (ev) { + this.sidebar.goNext(); + }, + /** + * Show or Hide the sidebar + * + * @private + */ + _onClickToggleSidebar: function (ev){ + ev.preventDefault(); + this.$('.o_wslides_fs_sidebar').toggleClass('d-none'); + this.$('.o_wslides_fs_content').toggleClass('col-10').toggleClass('col-12'); + this.$('.o_wslides_fs_toggle_sidebar').toggleClass('active'); }, }); sAnimations.registry.websiteSlidesFullscreenPlayer = Widget.extend({ selector: '.o_wslides_fs_main', xmlDependencies: ['/website_slides/static/src/xml/website_slides_fullscreen.xml'], - init: function (el){ - this._super.apply(this, arguments); - }, start: function (){ var defs = [this._super.apply(this, arguments)]; - var userId = this.$el.data('userId'); - var courseId = this.$el.data('courseId'); - var slideId = this.$el.data('slideId'); - var fullscreen = new Fullscreen(this, courseId, slideId, userId); - defs.push(fullscreen.attachTo(this.$el)); + var fullscreen = new Fullscreen(this, this._getSlides(), this._getCurrentSlideID()); + defs.push(fullscreen.attachTo(".o_wslides_fs_main")); return $.when.apply($, defs); - } + }, + _getCurrentSlideID: function (){ + return parseInt(this.$('.o_wslides_fs_sidebar_list_item.active').data('id')); + }, + /** + * @private + * Creates slides objects from every slide-list-cells attributes + */ + _getSlides: function (){ + var $slides = this.$('.o_wslides_fs_sidebar_list_item'); + var slideList = []; + $slides.each(function () { + var slideData = $(this).data(); + slideList.push(slideData); + }); + return slideList; + }, }); return Fullscreen; -});
\ No newline at end of file +}); 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 319ca629bf2..c969e38623b 100644 --- a/addons/website_slides/static/src/js/slides_course_quiz.js +++ b/addons/website_slides/static/src/js/slides_course_quiz.js @@ -15,9 +15,8 @@ odoo.define('website_slides.quiz', function (require) { * 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. + * - 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 = Widget.extend({ template: 'slide.slide.quiz', @@ -88,6 +87,8 @@ odoo.define('website_slides.quiz', function (require) { } else if (alert_code === 'slide_quiz_done') { message = _t('This quiz is already done. Retaking it is not possible.'); + } else if (alert_code === 'public_user') { + message = _t("You must be logged to submit the quiz."); } this.$('.o_wslides_js_lesson_quiz_error span').html(message); this.$('.o_wslides_js_lesson_quiz_error').removeClass('d-none'); @@ -116,7 +117,6 @@ odoo.define('website_slides.quiz', function (require) { _renderAnswers: function () { var self = this; this.$('input[type=radio]').each(function () { - console.log($(this)); $(this).prop('disabled', self.slide.readonly); }); }, @@ -184,9 +184,7 @@ odoo.define('website_slides.quiz', function (require) { * @param {Integer} completion */ _setCompleted: function () { - this.trigger_up('quiz_completed', { - 'slideId': this.slide.id, - }); + this.trigger_up('quiz_completed', this.slide); }, /* * Submit the given answer, and display the result @@ -241,9 +239,7 @@ odoo.define('website_slides.quiz', function (require) { */ _onClickNext: function (ev) { if (this.slide.hasNext) { - this.trigger_up('quiz_next_slide', { - 'slideId': this.slide.id, - }); + this.trigger_up('slide_go_next'); } }, /** @@ -272,7 +268,7 @@ odoo.define('website_slides.quiz', function (require) { sAnimations.registry.websiteSlidesQuizNoFullscreen = sAnimations.Class.extend({ selector: '.o_wslides_js_lesson_quiz', custom_events: { - quiz_next_slide: '_onQuizNextSlide', + slide_go_next: '_onQuizNextSlide', quiz_completed: '_onQuizCompleted', }, 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 08ef8bc299b..3868ad873cb 100644 --- a/addons/website_slides/static/src/scss/slides_slide_fullscreen.scss +++ b/addons/website_slides/static/src/scss/slides_slide_fullscreen.scss @@ -1,3 +1,9 @@ +$slides-bg-fullscreen: #313A44; +$slides-bg-fullscreen-dark: darken($slides-bg-fullscreen,5%); +$slides-bg-fullscreen-darker: darken($slides-bg-fullscreen,10%); + +$slides-text: #8A8F93; + .o_wslides_fs_main { position: absolute; top: 0; @@ -5,267 +11,60 @@ width: 100%; height: 100%; z-index: 2000; + color: $slides-text; + .o_wslides_slide_fs_header { - align-items: center; - background-color: #313A44; - color: #AEB0B2; - display: flex; + background-color: $slides-bg-fullscreen; height: 50px; - justify-content: space-between; - width: 100%; - - .o_wslides_fs_slide_title { - height: 100%; - display: flex; - align-items: center; - padding: 0 20px; - color: #D5D7D9; - font-weight: bold; - - i.fa-flag-checkered{ - color: #D39B4B; - margin-right: 10px; - } - } - - div { - display: flex; - height: 100%; - - a { - align-items: center; - background-color: #1F242C; - color: inherit; - display: flex; - font-size: 0.8rem; - font-weight: bold; - height: 100%; - justify-content: center; - padding: 0 20px; - text-decoration: none !important; - text-transform: uppercase; - - i { - align-items: center; - display: flex; - height: 100%; - margin: 0; - margin-right: 5px; - } - } - - a:hover, - a.active{ - background-color: #313A44; - color: #fff; - } - } } .o_wslides_fs_container { - display: flex; - align-items: stretch; height: calc(100% - 50px); - position: relative; - background-color: #313A44; - } - - .o_wslides_fs_sidebar_hidden{ - transform: translateX(-100%); + background-color: $slides-bg-fullscreen; } - .o_wslides_fs_sidebar{ - width: 400px; - min-width: 400px; - background-color: #313A44; - color: #fff; + .o_wslides_fs_sidebar { transition: all .2s ease-in; overflow: auto; - a { - text-decoration: none !important; - color: inherit; - } - - ul { - margin: 0; - padding: 0; - list-style: none; - } - - li { - padding-left: 20px; - } - .o_wslides_fs_sidebar_header { - height: 100px; - padding: 20px 0; - display: flex; - flex-direction: column; - - .o_wslides_slide_fs_progress_box{ - display: flex; - align-items: center; - font-size: .9rem; - .o_wslides_fs_sidebar_progressbar{ - height: 5px; - border-radius: 10px; - width: 70%; - background-color: rgba(0,0,0,.3); - margin-right: 40px; - overflow: hidden; - } - .progress-bar { - background-color: $primary; - height: 100%; - width: 0; - transition: all 1.5s ease-in; - } - span { - color: #BABDC0; - } - } - a { - font-size: 1.5rem; - font-weight: bold; + color: $slides-text; + .progress { + height: 5px; } } + .o_wslides_fs_sidebar_section { - margin-bottom: 20px; - background-color: #283038; - padding: 10px 0; - > a > li { - color: #656B72; - font-size: .8rem; - text-transform: uppercase; - font-weight: bold; - padding: 5px 0 5px 15px; + background-color: $slides-bg-fullscreen-dark; + border: none; + border-radius: 0; + + .card-body { + color: $slides-text; } } - .o_wslides_fs_sidebar_section_slides{ - padding: 0; - margin-bottom: 15px; - li { - margin: 0; - padding: 0; - border-left: 2px solid transparent; - display: flex; - a { - flex: 1; - } - > span{ - width: 15px; - padding: 0; - margin: 0; - margin-left: 10px; - display: flex; - flex-direction: column; - align-items: center; - i { - z-index: 100; - padding: 0; - margin: 0; - } - i.o_wslides_slide_completed{ - color: #00A04A; - border-radius: 50%; - overflow: hidden; - background-color: #fff; - height: 12px; - width: 12px; - display: flex; - justify-content: center; - align-items: center; - border: 1px solid #00A04A; - } - .o_wslides_fs_full_line{ - width: 1px; - height: 100%; - background-color: #787D82; - } - } - } - .o_wslides_top_line, - .o_wslides_bottom_line{ - width: 1px; - display: flex; - flex-grow: 1; - background-color: #787D82; - } - li:first-child .o_wslides_top_line{ - background-color: transparent; - } - li:last-child .o_wslides_bottom_line{ - background-color: transparent; - } + + .o_wslides_fs_sidebar_list{ li:hover, li.active { - border-left: 2px solid $primary; cursor: pointer; - background-color: #1D2228; + background-color: $slides-bg-fullscreen-darker; } - .o_wslides_fs_sidebar_slide_tab { - padding:5px 0 5px 15px; - font-size: .8rem; - color: #8A8F93; - font-weight: bold; - flex: 1; + li { + color: $slides-text; } - .o_wslides_fs_sidebar_slide_tab.active{ + li.active{ + border-left: 2px solid $primary; color: #fff; } - .o_wslides_fs_slide_link { - color: #098586; - font-size: .8rem; - margin-left: 15px; - flex: 1; - i { - margin-right: 10px; - } - } - .o_wslides_fs_slide_quiz { - font-size: .8rem; - margin-left: 15px; - color: #8A8F93; - flex: 1; - i { - color: #D39B4B; - margin-right: 10px; - } - } } - - } - .o_wslides_fs_player { - display: flex; - align-items: stretch; - max-width: calc(100% - 400px); - height: 100%; - background-color: #283038; - transition: all .2s ease-in; - position: absolute; - top: 0; - right: 0; - height: 100%; - width: 80%; - - iframe { - height: 100%; - width: 100%; - } + .o_wslides_fs_content { + background-color: $slides-bg-fullscreen-dark; } - .o_wslides_fs_webpage { - background-color: #fff; - width: 100%; - height: 100%; - overflow-y: scroll; - padding: 20px 0; - } - - .o_wslides_fs_player_no_sidebar { + .o_wslides_fs_content_no_sidebar { max-width: 100%; width: 100%; } @@ -273,10 +72,8 @@ // 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 { diff --git a/addons/website_slides/static/src/xml/slide_quiz.xml b/addons/website_slides/static/src/xml/slide_quiz.xml index f512985a3af..0af500914c5 100644 --- a/addons/website_slides/static/src/xml/slide_quiz.xml +++ b/addons/website_slides/static/src/xml/slide_quiz.xml @@ -1,7 +1,7 @@ <?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="o_wslides_fs_quiz_container h-100 w-100"> <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"> 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 b364f486420..683dc45752d 100644 --- a/addons/website_slides/static/src/xml/website_slides_fullscreen.xml +++ b/addons/website_slides/static/src/xml/website_slides_fullscreen.xml @@ -1,41 +1,36 @@ <templates id="template" xml:space="preserve"> - <t t-name="website.slides.fullscreen"> - <div t-if="slide.slide_type == 'video' && !showMiniQuiz" class="player embed-responsive embed-responsive-16by9 embed-responsive-item"> - <iframe id="youtube-player" t-att-src="embed_url" allowFullScreen="true" frameborder="0" enablejsapi="1" autoplay="1" allow="autoplay"></iframe> - </div> - <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 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 class="o_wslides_fs_content"/> - </t> - <t t-name="website.course.player.infos"> - <div class="text-box"> - <h1><t t-esc="slide.name"/></h1> - <a t-att-href="url" class="oe_js_show_comments">Show comments</a> - </div> - <div> - <button class="oe_js_slide_full_screen btn btn-secondary">Full Screen</button> - <button t-att-current_slide_id="slide.id" class="btn btn-primary oe_js_slide_completed">Mark as completed<i class="fa fa-check-circle ml-1"></i></button> - </div> + <t t-name="website.slides.fullscreen.title"> + <t t-if="!widget.get('slide').isQuiz"> + <i t-if="widget.get('slide').type == 'document'" class="fa fa-file-pdf-o mr-2"></i> + <i t-if="widget.get('slide').type == 'infographic'" class="fa fa-file-picture-o mr-2"></i> + <i t-if="widget.get('slide').type == 'video'" class="fa fa-play-circle mr-2"></i> + <i t-if="widget.get('slide').type == 'webpage'" class="fa fa-file-text mr-2"></i> + <i t-if="widget.get('slide').type == 'quiz'" class="fa fa-question-circle mr-2 text"></i> + <span class="o_wslides_fs_slide_title_span" t-esc="widget.get('slide').name"/> + </t> + <t t-else=""> + <span><i class="fa fa-flag-checkered"></i>Quiz: <span t-esc="widget.get('slide').name"/></span> + </t> </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> - <i t-if="slide.slide_type == 'infographic'" class="fa fa-file-picture-o mr-2"></i> - <i t-if="slide.slide_type == 'video'" class="fa fa-play-circle mr-2"></i> - <i t-if="slide.slide_type == 'link'" class="fa fa-file-code-o mr-2"></i> - <i t-if="slide.slide_type == 'webpage'" class="fa fa-file-text mr-2"></i> - <i t-if="slide.slide_type == 'quiz'" class="fa fa-question-circle mr-2 text"></i> - <span class="o_wslides_fs_slide_title_span" t-esc="slide.name"/> + <t t-name="website.slides.fullscreen.content"> + <t t-if="_.contains(['document', 'presentation'], widget.get('slide').type)"> + <div class="embed-responsive embed-responsive-4by3 embed-responsive-item h-100"> + <iframe t-att-src="widget.get('slide').embedUrl" class="o_wslides_iframe_viewer h-100 w-100" allowFullScreen="true" frameborder="0"></iframe> + </div> </t> - <t t-else=""> - <span><i class="fa fa-flag-checkered"></i>Quiz: <span t-esc="slide.name"/></span> + <t t-if="widget.get('slide').type === 'infographic'"> + <div class="w-100 h-100 text-center"> + <img t-att-src="'/web/image/slide.slide/'+ widget.get('slide').id +'/image'" class="img-fluid" style="height: 100%" alt="Slide image"/> + </div> </t> </t> + <t t-name="website.slides.fullscreen.video"> + <div class="player embed-responsive embed-responsive-16by9 embed-responsive-item h-100"> + <iframe t-att-id="'youtube-player' + widget.slide.id" t-att-src="widget.slide.embedUrl" allowFullScreen="true" frameborder="0" enablejsapi="1" autoplay="1" allow="autoplay"></iframe> + </div> + </t> + </templates> 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 1361559c23f..8285a76bacd 100644 --- a/addons/website_slides/views/website_slides_templates_lesson_fullscreen.xml +++ b/addons/website_slides/views/website_slides_templates_lesson_fullscreen.xml @@ -4,115 +4,125 @@ <!-- Slide template for the fullscreen mode --> <template id="slide_fullscreen" name="Fullscreen"> <t t-call="website.layout"> - <div class="o_wslides_fs_main" + <div class="o_wslides_fs_main d-flex flex-column" t-att-data-user-id="user.id" t-att-data-course-name="slide.channel_id.name" t-att-data-course-id="slide.channel_id.id" t-att-data-course-slug="slug(slide.channel_id)" t-att-data-slide-id="slide.id"> - <div class="o_wslides_slide_fs_header"> - <div> - <a id="fullscreen_sidebar_button" href="#" class="active o_wslides_fs_toggle_sidebar"><i class="fa fa-bars"></i>Lessons</a> - <a class="o_wslides_small_screen" t-attf-href="/slides/slide/#{slug(slide)}" target="new"><i class="fa fa-pencil"></i>write a review</a> - <div class="o_wslides_fs_slide_title"/> + <div class="o_wslides_slide_fs_header d-flex w-100"> + <a class="p-3 h-100 d-inline-block o_wslides_fs_toggle_sidebar active text-uppercase" href="#"> + <i class="fa fa-bars"/> Lessons + </a> + <a class="p-3 h-100 d-inline-block text-uppercase" t-attf-href="/slides/slide/#{slug(slide)}" target="new"> + <i class="fa fa-pencil"/> Write a review + </a> + <div class="p-3 h-100 d-inline-block o_wslides_fs_slide_title font-weight-bold"> + <t> + <t t-call="website_slides.slide_icon"> + <t t-set="icon_class" t-value="'mr-2 text'"/> + </t> + <span class="o_wslides_fs_slide_title_span" t-esc="slide.name"/> + </t> </div> - <div> - <a t-attf-href="/slides/">back to courses</a> + <div class="ml-auto p-3 text-uppercase"> + <a href="/slides/">Back to courses</a> </div> </div> - <div class="o_wslides_fs_container"> - <div class="o_wslides_fs_sidebar"> - <ul> - <li> - <div class="o_wslides_fs_sidebar_header"> - <a t-attf-href="/slides/#{slug(slide.channel_id)}"> - <span t-field="slide.channel_id.name"/> - </a> - <div class="o_wslides_slide_fs_progress_box"> - <div class="progress o_wslides_fs_sidebar_progressbar"> - <div class="progress-bar" role="progressbar" - t-att-aria-valuenow="slide.channel_id.completion" aria-valuemin="0" aria-valuemax="100" - t-att-style="'width: %s' % (slide.channel_id.completion)"/> - </div> - <div><span class="o_wslides_progress_percentage" t-esc="slide.channel_id.completion"/>%</div> - </div> + <div class="o_wslides_fs_container d-flex flex-wrap"> + <div class="col-2 o_wslides_fs_sidebar p-0"> + <div class="o_wslides_fs_sidebar_header mt-2 p-2"> + <a t-attf-href="/slides/#{slug(slide.channel_id)}"> + <span class="h3" t-field="slide.channel_id.name"/> + </a> + <div class="mt-2"> + <div class="progress"> + <div class="progress-bar" role="progressbar" t-attf-style="width: #{slide.channel_id.completion}%" t-att-aria-valuenow="slide.channel_id.completion" aria-valuemin="0" aria-valuemax="100"></div> </div> - </li> + <span class="o_wslides_progress_percentage" t-esc="slide.channel_id.completion"/> % + </div> + </div> + <div class="o_wslides_fs_category mt-4"> <t t-if="uncategorized_slides" t-call="website_slides.slide_fullscreen_sidebar_category"> - <t t-set="category_slide_ids" t-value="uncategorized_slides" /> + <t t-set="slides" t-value="uncategorized_slides"/> + <t t-set="current_slide" t-value="slide"/> </t> <t t-foreach="slide.channel_id.category_ids" t-as="category"> - <t t-call="website_slides.slide_fullscreen_sidebar_category"> - <t t-set="category_slide_ids" t-value="category.slide_ids" /> + <t t-if="category.slide_ids"> + <t t-call="website_slides.slide_fullscreen_sidebar_category"> + <t t-set="slides" t-value="category.slide_ids"/> + <t t-set="current_slide" t-value="slide"/> + </t> </t> </t> - </ul> + </div> </div> - <div class="o_wslides_fs_player"/> + <div class="col-10 o_wslides_fs_content p-0"></div> </div> </div> </t> </template> <template id="slide_fullscreen_sidebar_category" name="Slides category template for fullscreen view side bar"> - <div class="o_wslides_fs_sidebar_section"> - <a data-toggle="collapse" t-att-href="('#collapse-%s') % (category.id if category else 0)" role="button" aria-expanded="true" t-att-aria-controls="('collapse-%s') % (category.id if category else 0)"> - <li> - <t t-if="category"> - <span t-field="category.name"/> - </t> - <t t-else=""> - <span>Uncategorized</span> - </t> - </li> - </a> - <ul class="o_wslides_fs_sidebar_section_slides collapse show p-0 m-0" t-att-id="('collapse-%s') % (category.id if category else 0)" > - <t t-foreach="category_slide_ids" t-as="course_slide"> - <!-- AWA FIXME: remove that index crap but right now fullscreen JS uses it too much --> - <t t-set="i" t-value="list(slide.channel_id.slide_ids).index(course_slide)"/> - <li t-att-index="i" t-att-data-slide-id="course_slide.id" t-attf-class="#{'active' if slide.id == course_slide.id else ''}"> - <span> - <span class="o_wslides_top_line"></span> - <i t-attf-id="check-#{course_slide.id}" t-if="not channel_progress[course_slide.id].get('completed')" class="check-done fa fa-circle-thin"></i> - <i t-attf-id="check-#{course_slide.id}" t-if="channel_progress[course_slide.id].get('completed')" class="check-done o_wslides_slide_completed fa fa-check-circle"></i> - <span class="o_wslides_bottom_line"></span> - </span> - <a t-att-index="i" t-att-data-slide-id="course_slide.id"> - <div t-attf-index="#{i}" - t-att-data-slug="slug(course_slide)" - t-att-data-done="channel_progress[course_slide.id].get('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-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"> - <div> - <t t-call="website_slides.slide_icon"> - <t t-set="slide" t-value="course_slide" /> - </t> - <t t-esc="course_slide.name"/> - </div> + <div class="card o_wslides_fs_sidebar_section mb-3" t-attf-id="category-collapse-#{category.id if category else 0}"> + <div class="card-header" t-attf-id="#category-title-#{category.id if category else 0}" data-toggle="collapse" t-attf-data-target="#category-#{category.id if category else 0}" aria-expanded="true" t-attf-aria-controls="category-#{category.id if category else 0}"> + <t t-if="category"> + <span class="h6 text-uppercase" t-field="category.name"/> + </t> + <t t-else=""> + <span class="h6 text-uppercase">Uncategorized</span> + </t> + </div> + <div t-attf-id="category-#{category.id if category else 0}" class="collapse show" t-attf-aria-labelledby="#category-title-#{category.id if category else 0}" t-attf-data-parent="#category-collapse-#{category.id if category else 0}"> + <div class="card-body p-0" style="background-color: transparent !important;"><!-- inline style as boostrap used !important property to force color on .card-body --> + <ul class="o_wslides_fs_sidebar_list list-group list-unstyled collapse show p-0 m-0" t-att-id="('collapse-%s') % (category.id if category else 0)" > + <t t-foreach="slides" t-as="slide"> + <t t-set="slide_completed" t-value="channel_progress[slide.id].get('completed')"/> + <li t-att-class="'o_wslides_fs_sidebar_list_item d-flex p-1 font-weight-bold %s' % ('active' if slide.id == current_slide.id else '')" + t-att-data-id="slide.id" + t-att-data-name="slide.name" + t-att-data-type="slide.slide_type" + t-att-data-slug="slug(slide)" + t-att-data-has-question="1 if slide.question_ids else 0" + 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 channel_progress[slide.id]" + > + <div class="mr-2 ml-2 flex-grow"> + <i t-if="not slide_completed" class="fa fa-circle-thin" t-att-data-slide-id="slide.id"/> + <i t-if="slide_completed" class="fa fa-check-circle text-success" t-att-data-slide-id="slide.id"/> + </div> + <div class=""> + <t t-call="website_slides.slide_icon"> + <t t-set="icon_class" t-value="'mr-2'"/> + <t t-set="slide" t-value="slide"/> + </t> + <span t-field="slide.name"/> + + <ul t-if="slide.link_ids" class="list-group list-unstyled"> + <li t-foreach="slide.link_ids" t-as="link"> + <a class="btn btn-link" t-att-href="link.link" target="new"><i class="fa fa-link"/> <span t-esc="link.name"/></a> + </li> + </ul> </div> - </a> - </li> - <t t-if="course_slide.link_ids" t-foreach="course_slide.link_ids" t-as="link"> - <li> - <span> - <span class="o_wslides_fs_full_line"/> + </li> + <li t-if="slide.question_ids and slide.slide_type != 'quiz'" class="o_wslides_fs_sidebar_list_item" + t-att-data-id="slide.id" + t-att-data-name="slide.name" + t-att-data-type="slide.slide_type" + t-att-data-slug="slug(slide)" + t-att-data-has-question="1 if slide.question_ids else 0" + t-att-data-is-quiz="1"> + <span class="ml-4"> + <i class="fa fa-flag-checkered"/> Quiz: <span t-field="slide.name"/> </span> - <a class="o_wslides_fs_slide_link" t-att-href="link.link" target="new"><i class="fa fa-link"></i><span t-esc="link.name"/></a> </li> </t> - <li t-att-index="i" t-if="course_slide.question_ids and not course_slide.slide_type =='quiz'"> - <span> - <span class="o_wslides_fs_full_line"/> - </span> - <span class="o_wslides_fs_slide_quiz" t-att-index="i"><i class="fa fa-flag-checkered"></i>Quiz: <span t-esc="course_slide.name"/></span> - </li> - </t> - </ul> + </ul> + + </div> + </div> </div> </template> diff --git a/addons/website_slides_forum/views/website_slides_templates.xml b/addons/website_slides_forum/views/website_slides_templates.xml index 554d5b18978..dd9717a8bd7 100644 --- a/addons/website_slides_forum/views/website_slides_templates.xml +++ b/addons/website_slides_forum/views/website_slides_templates.xml @@ -12,8 +12,10 @@ <template id="slide_fullscreen" inherit_id="website_slides.slide_fullscreen"> <!-- Fullscreen template: add link to forum --> - <xpath expr="//a[@id='fullscreen_sidebar_button']" position="after"> - <a t-if="slide.channel_id.forum_id" id="fullscreen_forum_button" t-attf-href="/forum/#{slug(slide.channel_id.forum_id)}" target="new"><i class="fa fa-comments"></i>Ask a question</a> + <xpath expr="//div[hasclass('o_wslides_fs_slide_title')]" position="before"> + <a t-if="slide.channel_id.forum_id" class="p-3 h-100 d-inline-block text-uppercase" t-att-href="'/forum/%s' % (slug(slide.channel_id.forum_id))" target="new"> + <i class="fa fa-comments"></i>Ask a question + </a> </xpath> </template> diff --git a/addons/website_slides_survey/static/src/xml/website_slides_fullscreen.xml b/addons/website_slides_survey/static/src/xml/website_slides_fullscreen.xml index c5d22bfc46f..6daa08ffe65 100644 --- a/addons/website_slides_survey/static/src/xml/website_slides_fullscreen.xml +++ b/addons/website_slides_survey/static/src/xml/website_slides_fullscreen.xml @@ -1,21 +1,21 @@ <?xml version="1.0" encoding="UTF-8"?> <templates> <t t-extend="website.slides.fullscreen"> - <t t-jquery=".o_wslides_fs_content" t-operation="append"> - <div class="o_wslides_fs_certification" t-if="slide.slide_type == 'certification' && !slide.certificationDone"> - <a class="btn btn-primary" t-att-href="'/slides/slide/' + slide.slug" target="new"><i class="fa fa-trophy"/> Pass Certification</a> + <t t-jquery="t:last" t-operation="after"> + <div t-if="widget.get('slide').type === 'certification' && !widget.get('slide').certificationDone" class="o_wslides_fs_certification"> + <a class="btn btn-primary" t-att-href="'/slides/slide/' + widget.get('slide').id" target="new"><i class="fa fa-trophy"/> Pass Certification</a> </div> - <div class="o_wslides_fs_certification" t-if="slide.slide_type == 'certification' && slide.certificationDone"> - <a role="button" t-att-data-survey_id="slide.certificationId" class="btn btn-primary o_wslides_survey_download_certification" href="#"> + <div class="o_wslides_fs_certification" t-if="widget.get('slide').type === 'certification' && widget.get('slide').certificationDone"> + <a role="button" t-att-data-survey_id="widget.get('slide').certificationId" class="btn btn-primary o_wslides_survey_download_certification" href="#"> <i class="fa fa-fw fa-trophy" role="img" aria-label="Download certification" title="Download certification"/> Download certification </a> </div> </t> </t> - <t t-extend="website.course.fullscreen.title"> + <t t-extend="website.slides.fullscreen.title"> <t t-jquery=".o_wslides_fs_slide_title_span" t-operation="before"> - <i t-if="slide.slide_type == 'certification'" class="fa fa-trophy mr-2 text"></i> + <i t-if="widget.get('slide').type === 'certification'" class="fa fa-trophy mr-2 text"></i> </t> </t> </templates> diff --git a/addons/website_slides_survey/views/website_slides_templates_lesson_fullscreen.xml b/addons/website_slides_survey/views/website_slides_templates_lesson_fullscreen.xml index d42674336ac..03baa27ef70 100644 --- a/addons/website_slides_survey/views/website_slides_templates_lesson_fullscreen.xml +++ b/addons/website_slides_survey/views/website_slides_templates_lesson_fullscreen.xml @@ -2,9 +2,9 @@ <odoo><data> <template id="slide_fullscreen_sidebar_category" inherit_id="website_slides.slide_fullscreen_sidebar_category"> - <xpath expr="//div[@t-attf-index='#{i}']" position="attributes"> - <attribute name="t-att-data-certification-done">channel_progress[course_slide.id].get('survey_quizz_passed')</attribute> - <attribute name="t-att-data-certification-id">course_slide.survey_id.id</attribute> + <xpath expr="//li[hasclass('o_wslides_fs_sidebar_list_item')]" position="attributes"> + <attribute name="t-att-data-certification-done">channel_progress[slide.id].get('survey_quizz_passed')</attribute> + <attribute name="t-att-data-certification-id">slide.survey_id.id</attribute> </xpath> </template> |
