[REF] website_slides: revamp fullscreen widget
This commit makes the 'Fullscreen' page a complete odoo widgets. We decided to split it into some sub widget: - Fullscreen: will be the master piece, handle communication of components, displaying slide content, setting slide as completed, handling server RPC calls, .... - Sidebar: responsible for slide navigation, based on the table of content. (prev, next, keyboard naviguation, ...) - VideoPlayer: display content for Youtube video and handling player event to auto set as completed, ... The DOM of this page is still rendered serverside. DOM was cleaned in order to be less complex and less deep. CSS style was also reviewed to decrease line of code. Fullscreen is now completely using Quiz widget with events triggered. Task-1942852
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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 QuizWidget = require('website_slides.quiz');
|
||||
|
||||
tag.src = "https://www.youtube.com/iframe_api";
|
||||
var firstScriptTag = document.getElementsByTagName('script')[0];
|
||||
firstScriptTag.parentNode.insertBefore(tag, firstScriptTag);
|
||||
var Quiz = require('website_slides.quiz');
|
||||
|
||||
|
||||
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);
|
||||
/**
|
||||
* 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',
|
||||
|
||||
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);
|
||||
},
|
||||
//--------------------------------------------------------------------------
|
||||
// Private
|
||||
//--------------------------------------------------------------------------
|
||||
/**
|
||||
* @private
|
||||
* Renders the player accordingly to the current slide
|
||||
*/
|
||||
_renderPlayer: function (){
|
||||
var self = this;
|
||||
|
||||
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(this.slide.slide_type === "video"){
|
||||
this._renderYoutubeIframe();
|
||||
}
|
||||
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 {
|
||||
return $.when(this._super.apply(this, arguments), this._loadYoutubeAPI()).then(function() {
|
||||
self._setupYoutubePlayer();
|
||||
}
|
||||
catch (error) {
|
||||
onYouTubeIframeAPIReady = function (){
|
||||
var self = this;
|
||||
self._setupYoutubePlayer();
|
||||
}.bind(this);
|
||||
}
|
||||
},
|
||||
_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');
|
||||
},
|
||||
_loadYoutubeAPI: function () {
|
||||
var def = $.Deferred();
|
||||
if ($(document).find('script[src="' + this.youtubeUrl + '"]').length === 0) {
|
||||
var $youtubeElement = $('<script/>', {src: this.youtubeUrl});
|
||||
$(document.head).append($youtubeElement);
|
||||
|
||||
_renderTitle: function (){
|
||||
var self = this;
|
||||
$('.o_wslides_fs_slide_title').empty().html(QWeb.render('website.course.fullscreen.title', {
|
||||
slide: self.slide,
|
||||
miniQuiz: self.goToQuiz
|
||||
}));
|
||||
// function called when the Youtube asset is loaded
|
||||
// see https://developers.google.com/youtube/iframe_api_reference#Requirements
|
||||
onYouTubeIframeAPIReady = function () {
|
||||
def.resolve();
|
||||
};
|
||||
|
||||
} else {
|
||||
def.resolve();
|
||||
}
|
||||
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
|
||||
//--------------------------------------------------------------------------
|
||||
/**
|
||||
* Change the current slide with the next one (if there is one).
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
goNext: function () {
|
||||
var currentIndex = this._getCurrentIndex();
|
||||
if (currentIndex < this.slideEntries.length-1) {
|
||||
this.set('slideEntry', this.slideEntries[currentIndex+1]);
|
||||
}
|
||||
},
|
||||
/**
|
||||
* @private
|
||||
* Creates slides objects from every slide-list-cells attributes
|
||||
* Change the current slide with the previous 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();
|
||||
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
|
||||
//--------------------------------------------------------------------------
|
||||
/**
|
||||
* Get the index of the current slide entry (slide and/or quiz)
|
||||
*/
|
||||
_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
|
||||
* @param {*} ev
|
||||
*/
|
||||
_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'));
|
||||
},
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
_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"]';
|
||||
}
|
||||
this.$(selector).addClass('active');
|
||||
this.trigger_up('change_slide', this.get('slideEntry'));
|
||||
},
|
||||
|
||||
/**
|
||||
* Binds left and right arrow to allow the user to navigate between slides
|
||||
*
|
||||
* @param {*} ev
|
||||
* @private
|
||||
*/
|
||||
_fetchHtmlContent: function(){
|
||||
_onKeyDown: function (ev){
|
||||
switch (ev.key){
|
||||
case "ArrowLeft":
|
||||
this.goPrevious();
|
||||
break;
|
||||
case "ArrowRight":
|
||||
this.goNext();
|
||||
break;
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
/**
|
||||
* 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;
|
||||
},
|
||||
/**
|
||||
* @override
|
||||
*/
|
||||
start: function (){
|
||||
var self = this;
|
||||
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);
|
||||
},
|
||||
//--------------------------------------------------------------------------
|
||||
// Private
|
||||
//--------------------------------------------------------------------------
|
||||
/**
|
||||
* Fetches content with an rpc call for slides of type "webpage"
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
_fetchHtmlContent: function (){
|
||||
var self = this;
|
||||
var currentSlide = this.get('slide');
|
||||
return self._rpc({
|
||||
route: 'slides/slide/get_html_content',
|
||||
route:"/slides/slide/get_html_content",
|
||||
params: {
|
||||
'slide_id': self.slide.id
|
||||
'slide_id': currentSlide.id
|
||||
}
|
||||
}).then(function (data){
|
||||
if (data.html_content) {
|
||||
self.slide.htmlContent = data.html_content;
|
||||
currentSlide.htmlContent = data.html_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
|
||||
*/
|
||||
_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();
|
||||
}
|
||||
});
|
||||
},
|
||||
_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);
|
||||
* 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();
|
||||
},
|
||||
/**
|
||||
* @private
|
||||
* Creates an array of letters to be used in the quiz with a foreach
|
||||
* Extend the slide data list to add informations about rendering method, and other
|
||||
* specific values according to their slide_type.
|
||||
*/
|
||||
_generateQuizLetters: function (){
|
||||
var letters = [];
|
||||
for (var i = 65; i < 91; i++){
|
||||
letters.push(String.fromCharCode(i));
|
||||
}
|
||||
return letters;
|
||||
},
|
||||
_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);
|
||||
}
|
||||
else if (self.nextSlide){
|
||||
self._renderPlayer();
|
||||
}
|
||||
},
|
||||
_goToPreviousSlide: 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);
|
||||
}
|
||||
},
|
||||
_setPreviousAndNextSlides: 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;
|
||||
_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){
|
||||
/**
|
||||
* 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;
|
||||
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);
|
||||
}
|
||||
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();
|
||||
});
|
||||
},
|
||||
_sidebarToggle: function (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);
|
||||
},
|
||||
/**
|
||||
* 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();
|
||||
$(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 (){
|
||||
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));
|
||||
});
|
||||
$('.o_wslides_fs_toggle_sidebar').click(function (ev){
|
||||
self._sidebarToggle(ev);
|
||||
});
|
||||
},
|
||||
_onKeyDown: function (ev){
|
||||
var self = this;
|
||||
switch (ev.key){
|
||||
case "ArrowRight":
|
||||
self._goToNextSlide();
|
||||
break;
|
||||
case "ArrowLeft":
|
||||
self._goToPreviousSlide();
|
||||
break;
|
||||
}
|
||||
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;
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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',
|
||||
},
|
||||
|
||||
|
||||
@@ -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;
|
||||
background-color: $slides-bg-fullscreen;
|
||||
}
|
||||
|
||||
.o_wslides_fs_sidebar_hidden{
|
||||
transform: translateX(-100%);
|
||||
}
|
||||
|
||||
.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;
|
||||
color: $slides-text;
|
||||
.progress {
|
||||
height: 5px;
|
||||
}
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
}
|
||||
.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_webpage {
|
||||
background-color: #fff;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow-y: scroll;
|
||||
padding: 20px 0;
|
||||
.o_wslides_fs_content {
|
||||
background-color: $slides-bg-fullscreen-dark;
|
||||
}
|
||||
|
||||
.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 {
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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 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.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="slide.name"/></span>
|
||||
<span><i class="fa fa-flag-checkered"></i>Quiz: <span t-esc="widget.get('slide').name"/></span>
|
||||
</t>
|
||||
</t>
|
||||
|
||||
<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-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>
|
||||
|
||||
@@ -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>
|
||||
</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"/>
|
||||
<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>
|
||||
</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>
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user