[IMP] website_slides: Add elearning feature to slides

Purpose of this commit is to introduce a new way of displaying and managing
slides in eLearning module. Its purpose is to give a better experience to
users when going through a course. Notably a fullscreen mode allows to take
lessons one by one without going out of the elearning display. Integration
of all slide types eases taking the course step by step.

Quiz are introduced in this commit. Those are an addition to slides with
some question / answers (multiple choice). It allows customer to gain karma
and improves gamification. Certifications are still done using the survey
application. Quiz targets only small tests at the end of a given slide.

A new widget is added to display and control slide display in fullscreen
mode. Old display is still available for documentation channels or when
going out of fullscreen mode, to see details on comments / review / statistics
and have access to share options.

This commit is linked to task ID 1902304 and PR #29876. It closes the main
work on refactoring website slides into eLearning, with other tasks already
merged in current community [1][2][3][4][5].

[1] Task ID 1940360 landed at b2149bc7a2: certification inclusion
[2] Task ID 1936153 landed at 6c6179e57c: homepage inclusion
[3] Task ID 1937160 landed at 8b7605c9e5: selling courses
[4] Task ID 1922159 and 1940516 landed at 5fa651cb01 and 6a64c3ce82:
    new user profile and gamification of slides / forum
[5] Task ID 1938643 landed at 77b5673a0d: upload channel/slide
This commit is contained in:
qmo-odoo
2019-02-19 13:11:15 +00:00
committed by Thibault Delavallée
parent 7a80ebe35e
commit c564ae6c87
33 changed files with 3399 additions and 55 deletions
@@ -148,8 +148,8 @@
</div>
<div groups="base.group_system" name="module_website_slides" t-att-data-module-id="env.ref('base.module_website_slides').id" t-att-data-module-shortdesc="env.ref('base.module_website_slides').shortdesc" class="col-md-4 mb8 o_new_content_element">
<a href="#" data-action="new_slide_channel">
<i class="fa fa-youtube-play"/>
<p>New Slide Channel</p>
<i class="fa fa-graduation-cap"></i>
<p>New Course</p>
</a>
</div>
<div groups="base.group_system" name="module_website_livechat" t-att-data-module-id="env.ref('base.module_website_livechat').id" t-att-data-module-shortdesc="env.ref('base.module_website_livechat').shortdesc" class="col-md-4 mb8 o_new_content_element">
+164 -4
View File
@@ -50,13 +50,34 @@ class WebsiteSlides(WebsiteProfile):
def _get_slide_detail(self, slide):
most_viewed_slides = slide.get_most_viewed_slides(self._slides_per_list)
related_slides = slide.get_related_slides(self._slides_per_list)
return {
values = {
'slide': slide,
'most_viewed_slides': most_viewed_slides,
'related_slides': related_slides,
'user': request.env.user,
'is_public_user': request.website.is_public_user(),
'comments': slide.website_message_ids or [],
'user_progress': {}
}
if slide.channel_id.channel_type == "training":
channel_slides = slide.channel_id.slide_ids.ids
slide_index = channel_slides.index(slide.id)
previous_slide = None
next_slide = None
if slide_index > 0:
previous_slide = slide.channel_id.slide_ids[slide_index-1]
if slide_index < len(channel_slides) - 1:
next_slide = slide.channel_id.slide_ids[slide_index+1]
values.update({
'previous_slide': slug(previous_slide) if previous_slide else "",
'next_slide': slug(next_slide) if next_slide else ""
})
return values
def _get_user_progress(self, channel):
user_progress = { slide_partner.slide_id.id: slide_partner for slide_partner in request.env['slide.slide.partner'].sudo().search([('channel_id', '=', channel.id),('partner_id', '=', request.env.user.partner_id.id)])}
return {
'user_progress': user_progress
}
def _extract_channel_tag_search(self, **post):
@@ -198,7 +219,6 @@ class WebsiteSlides(WebsiteProfile):
def channel(self, channel, category=None, tag=None, page=1, slide_type=None, sorting=None, search=None, **kw):
if not channel.can_access_from_current_website():
raise werkzeug.exceptions.NotFound()
domain = [('channel_id', '=', channel.id)]
if not request.env.user.has_group('website.group_website_publisher'):
domain = expression.AND([
@@ -299,6 +319,9 @@ class WebsiteSlides(WebsiteProfile):
values = self._prepare_additional_channel_values(values, **kw)
if channel.channel_type == "training":
values.update(self._get_user_progress(channel))
return request.render('website_slides.course_main', values)
@http.route(['/slides/channel/add'], type='http', auth='user', methods=['POST'], website=True)
@@ -339,7 +362,13 @@ class WebsiteSlides(WebsiteProfile):
'message_post_hash': slide._generate_signed_token(request.env.user.partner_id.id),
'message_post_pid': request.env.user.partner_id.id,
})
return request.render('website_slides.slide_detail_view', values)
self._set_viewed_slide(slide)
if slide.channel_id.channel_type == "training":
values.update(self._get_user_progress(slide.channel_id))
if 'fullscreen' in kwargs:
return request.render("website_slides.slide_fullscreen", values)
return request.render("website_slides.slide_detail_view", values)
@http.route('''/slides/slide/<model("slide.slide"):slide>/pdf_content''',
type='http', auth="public", website=True, sitemap=False)
@@ -394,6 +423,94 @@ class WebsiteSlides(WebsiteProfile):
response.status_code = status
return response
@http.route('/slide/html_content/get', type="json", auth="public", website=True)
def get_html_content(self, slide_id):
slide = request.env['slide.slide'].browse(slide_id)
return {
'html_content': slide.html_content
}
#SLIDE QUIZ CONTROLLERS
@http.route('/slide/quiz/get', type="json", auth="public", website=True)
def get_quiz(self, **kw):
if 'slide_id' in kw:
slide = request.env['slide.slide'].browse(kw['slide_id'])
slide_partner = request.env['slide.slide.partner'].search([('slide_id', '=', slide.id), ('partner_id', '=', request.env.user.partner_id.id)])
possible_points = [slide.quiz_first_attempt_reward,slide.quiz_second_attempt_reward,slide.quiz_third_attempt_reward, slide.quiz_fourth_attempt_reward]
points = 0
if slide_partner.quiz_attempts_count < len(possible_points):
points = possible_points[slide_partner.quiz_attempts_count]
else:
points = possible_points[len(possible_points)-1]
res = {
'questions':[
{'title': question.question,
'id': question.id,
'answers': [{'text': answer.text_value, 'correct':answer.is_correct,'id': answer.id} for answer in question.answer_ids]
} for question in slide.question_ids
],
'nb_attempts': slide_partner.quiz_attempts_count if slide_partner else 0,
'possible_rewards': possible_points,
'reward': points
}
return res
@http.route('/slide/quiz/submit', type="json", auth="user", website=True)
def submit_quiz(self, slide_id, answer_ids,**kw):
slide = request.env['slide.slide'].browse(slide_id)
good_answers = request.env['slide.answer'].search([('id', 'in', answer_ids), ('is_correct', '=', True)])
bad_answers = request.env['slide.answer'].browse(answer_ids) - good_answers
slide_partner = request.env['slide.slide.partner'].search([('slide_id', '=', slide_id), ('partner_id', '=', request.env.user.partner_id.id)])
possible_points = [slide.quiz_first_attempt_reward, slide.quiz_second_attempt_reward, slide.quiz_third_attempt_reward, slide.quiz_fourth_attempt_reward]
points = 0
if not slide_partner:
slide_partner = request.env['slide.slide.partner'].sudo().create({
"slide_id": slide.id,
"partner_id": request.env.user.partner_id.id,
"channel_id": slide.channel_id.id,
"completed": False
})
if not slide_partner.completed:
if slide_partner.quiz_attempts_count < len(possible_points):
points = possible_points[slide_partner.quiz_attempts_count]
else:
points = possible_points[-1]
slide_partner.sudo().write({
'quiz_attempts_count': slide_partner.quiz_attempts_count if not bad_answers else slide_partner.quiz_attempts_count + 1,
'points_won': points if not bad_answers else 0,
'completed': not bad_answers
})
return {
'goodAnswers': [good_answer.id for good_answer in good_answers],
'badAnswers': [bad_answer.id for bad_answer in bad_answers],
'passed': not bad_answers,
'points': points if not bad_answers else 0,
'attempts_count': slide_partner.quiz_attempts_count if slide_partner else 0,
'channel_completion': slide.channel_id.completion
}
return {
'error': "You already passed this quiz"
}
#SLIDE STATE CONTROLLERS
@http.route('/slide/completed/<int:slide_id>', website=True, type="http", auth="user")
def mark_as_completed(self, slide_id, next_slide=None, **kw):
slide = request.env['slide.slide'].browse(slide_id)
slide.action_set_completed()
return werkzeug.utils.redirect("/slides/slide/%s" %(next_slide))
@http.route('/slides/set_completed', website=True, type="json", auth="user")
def set_status_as_done(self, slide_id, **kw):
slide = request.env['slide.slide'].browse(slide_id)
slide.channel_id.invalidate_cache()
slide.action_set_completed()
return {
'channel_completion': slide.channel_id.completion
}
# JSONRPC
@http.route('/slides/slide/like', type='json', auth="user", website=True)
def slide_like(self, slide_id, upvote):
@@ -423,6 +540,13 @@ class WebsiteSlides(WebsiteProfile):
# TOOLS
# --------------------------------------------------
@http.route(['/slides/channel/enroll'], type='http', auth='public', website=True)
def slide_channel_join_http(self, channel_id):
if not request.website.is_public_user():
channel = request.env['slide.channel'].browse(int(channel_id))
channel.action_add_member()
return werkzeug.utils.redirect("/slides/%s" % (slug(channel)))
@http.route(['/slides/channel/join'], type='json', auth='public', website=True)
def slide_channel_join(self, channel_id):
if request.website.is_public_user():
@@ -495,9 +619,19 @@ class WebsiteSlides(WebsiteProfile):
return {'error': _('Internal server error, please try again later or contact administrator.\nHere is the error message: %s') % e}
redirect_url = "/slides/slide/%s" % (slide.id)
if channel.channel_type == "training" and not slide.slide_type == "webpage":
redirect_url = "/slides/%s" % (slug(channel))
if slide.slide_type == 'webpage':
redirect_url += "?enable_editor=1"
return {'url': redirect_url}
if slide.slide_type == "quiz":
action_id = request.env.ref('website_slides.action_slides_slides').id
redirect_url = '/web#id=%s&action=%s&model=slide.slide&view_type=form' %(slide.id,action_id)
return {
'url': redirect_url,
'channel_type': channel.channel_type,
'slide_id': slide.id,
'category_id': slide.category_id
}
def _get_valid_slide_post_values(self):
return ['name', 'url', 'tag_ids', 'slide_type', 'channel_id',
@@ -527,6 +661,32 @@ class WebsiteSlides(WebsiteProfile):
'can_create': can_create,
}
@http.route('/slides/add_category', type="json", website=True, auth="user")
def add_category(self, channel_id, name, **kw):
channel = request.env['slide.channel'].browse(channel_id)
request.env['slide.category'].create({
'name': name,
'channel_id': channel.id
})
return {'url': "/slides/%s" %(slug(channel))}
#Not using the /web/dataset/resequence route as a slide can be dragged into another category at the same time
@http.route('/slides/resequence_slides', type="json", website=True, auth="user")
def resequence_slides(self, slides_data=None, **kw):
channel_user = None
for slide in slides_data:
s = request.env['slide.slide'].browse(slide['id'])
if not channel_user:
channel_user = s.channel_id.user_id
if request.env.user != channel_user:
return {'error': 'Only the responsible of the channel can edit it'}
s.write({
'sequence': slide['sequence'],
'category_id': slide['category_id']
})
# --------------------------------------------------
# EMBED IN THIRD PARTY WEBSITES
# --------------------------------------------------
+1 -1
View File
@@ -6,7 +6,7 @@
</record>
<record id="website_menu_slides" model="website.menu">
<field name="name">Presentations</field>
<field name="name">Courses</field>
<field name="url">/slides</field>
<field name="parent_id" ref="website.main_menu"/>
<field name="sequence" type="int">50</field>
+1
View File
@@ -2,6 +2,7 @@
from . import gamification_challenge
from . import slide_slide
from . import slide_question
from . import slide_channel
from . import slide_channel_tag
from . import slide_channel_invite
@@ -101,6 +101,7 @@ class Channel(models.Model):
nbr_videos = fields.Integer('Number of Videos', compute='_compute_slides_statistics', store=True)
nbr_infographics = fields.Integer('Number of Infographics', compute='_compute_slides_statistics', store=True)
nbr_webpages = fields.Integer("Number of Webpages", compute='_compute_slides_statistics', store=True)
nbr_quizs = fields.Integer("Number of quizzes", compute="_compute_slides_statistics", store=True)
total_slides = fields.Integer('# Slides', compute='_compute_slides_statistics', store=True, oldname='total')
total_views = fields.Integer('# Views', compute='_compute_slides_statistics', store=True)
total_votes = fields.Integer('# Votes', compute='_compute_slides_statistics', store=True)
@@ -174,7 +175,7 @@ class Channel(models.Model):
def _compute_slides_statistics(self):
result = dict((cid, dict(total_slides=0, total_views=0, total_votes=0, total_time=0)) for cid in self.ids)
read_group_res = self.env['slide.slide'].read_group(
[('is_published', '=', True), ('channel_id', 'in', self.ids)],
[('is_published', '=', True),('channel_id', 'in', self.ids)],
['channel_id', 'slide_type', 'likes', 'dislikes', 'total_views', 'completion_time'],
groupby=['channel_id', 'slide_type'],
lazy=False)
@@ -195,7 +196,7 @@ class Channel(models.Model):
def _compute_slides_statistics_type(self, read_group_res):
""" Can be overridden to compute stats on added slide_types """
result = dict((cid, dict(nbr_presentations=0, nbr_documents=0, nbr_videos=0, nbr_infographics=0, nbr_webpages=0)) for cid in self.ids)
result = dict((cid, dict(nbr_presentations=0, nbr_documents=0, nbr_videos=0, nbr_infographics=0, nbr_webpages=0, nbr_quizs=0)) for cid in self.ids)
for res_group in read_group_res:
cid = res_group['channel_id'][0]
result[cid]['nbr_presentations'] += res_group.get('slide_type', '') == 'presentation' and res_group['__count'] or 0
@@ -203,6 +204,7 @@ class Channel(models.Model):
result[cid]['nbr_videos'] += res_group.get('slide_type', '') == 'video' and res_group['__count'] or 0
result[cid]['nbr_infographics'] += res_group.get('slide_type', '') == 'infographic' and res_group['__count'] or 0
result[cid]['nbr_webpages'] += res_group.get('slide_type', '') == 'webpage' and res_group['__count'] or 0
result[cid]['nbr_quizs'] += res_group.get('slide_type', '') == 'quiz' and res_group['__count'] or 0
return result
@api.depends('slide_partner_ids')
@@ -409,6 +411,7 @@ class Category(models.Model):
nbr_videos = fields.Integer("Number of Videos", compute='_count_presentations', store=True)
nbr_infographics = fields.Integer("Number of Infographics", compute='_count_presentations', store=True)
nbr_webpages = fields.Integer("Number of Webpages", compute='_count_presentations', store=True)
nbr_quizs = fields.Integer("Number of quizzes", compute="_count_presentations", store=True)
total_slides = fields.Integer(compute='_count_presentations', store=True, oldname='total')
@api.depends('slide_ids.slide_type', 'slide_ids.is_published')
@@ -432,6 +435,7 @@ class Category(models.Model):
'nbr_videos': result[record_id].get('video', 0),
'nbr_infographics': result[record_id].get('infographic', 0),
'nbr_webpages': result[record_id].get('webpage', 0),
'nbr_quizs': result[record_id].get('quiz', 0),
'total_slides': 0
}
@@ -440,5 +444,6 @@ class Category(models.Model):
statistics['total_slides'] += statistics['nbr_videos']
statistics['total_slides'] += statistics['nbr_infographics']
statistics['total_slides'] += statistics['nbr_webpages']
statistics['total_slides'] += statistics['nbr_quizs']
return statistics
@@ -0,0 +1,51 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import api, fields, models, _
from odoo.exceptions import ValidationError
class SlideQuestion(models.Model):
_name = "slide.question"
_rec_name = "question"
_description = """
Question for a slide. A slide can have multiple questions and each question
must have at least 2 possible answers and only one good answer.
"""
sequence = fields.Integer("Sequence", default=10)
question = fields.Char("Question Name", required=True)
slide_id = fields.Many2one('slide.slide', string="Slide")
answer_ids = fields.One2many('slide.answer', 'question_id', string="Answer")
@api.constrains('answer_ids')
def _check_only_one_good_answer(self):
for question in self:
good_answer_count = 0
for answer in question.answer_ids:
if answer.is_correct:
good_answer_count += 1
if good_answer_count > 1:
raise ValidationError(_('A question can only have one good answer'))
@api.constrains('answer_ids')
def _check_correct_answer(self):
for question in self:
if not any([answer.is_correct for answer in question.answer_ids]):
raise ValidationError(_("A question must at least have one good answer"))
@api.constrains('answer_ids')
def _check_at_least_2_answers(self):
for question in self:
if len(question.answer_ids) < 2:
raise ValidationError(_("A question must at least have two possible answers"))
class SlideAnswer(models.Model):
_name = "slide.answer"
_rec_name = "text_value"
_description = """Answer for a slide question"""
question_id = fields.Many2one('slide.question', string="Question")
text_value = fields.Char("Answer", required=True,)
is_correct = fields.Boolean("Is correct answer", default=False)
+18 -4
View File
@@ -23,12 +23,14 @@ class SlidePartnerRelation(models.Model):
_description = 'Slide / Partner decorated m2m'
_table = 'slide_slide_partner'
slide_id = fields.Many2one('slide.slide', index=True, required=True)
slide_id = fields.Many2one('slide.slide', ondelete="cascade", index=True, required=True)
channel_id = fields.Many2one('slide.channel', string="Channel", related="slide_id.channel_id", store=True, index=True)
partner_id = fields.Many2one('res.partner', index=True, required=True)
vote = fields.Integer('Vote', default=0)
completed = fields.Boolean('Completed')
quiz_attempts_count = fields.Integer(string="Number of times this user attempted the quiz")
class SlideLink(models.Model):
_name = 'slide.slide.link'
@@ -95,6 +97,7 @@ class Slide(models.Model):
'most_voted': 'likes desc',
'latest': 'date_published desc',
}
_order = 'category_sequence asc, sequence asc'
_sql_constraints = [
('exclusion_html_content_and_url', "CHECK(html_content IS NULL OR url IS NULL)", "A slide is either filled with a document url or HTML content. Not both.")
@@ -106,6 +109,8 @@ class Slide(models.Model):
# description
name = fields.Char('Title', required=True, translate=True)
active = fields.Boolean(default=True)
sequence = fields.Integer('Sequence', default=10)
category_sequence = fields.Integer('Category sequence', related="category_id.sequence", store=True)
user_id = fields.Many2one('res.users', string='Uploaded by', default=lambda self: self.env.uid)
description = fields.Text('Description', translate=True)
channel_id = fields.Many2one('slide.channel', string="Channel", required=True)
@@ -126,15 +131,24 @@ class Slide(models.Model):
partner_ids = fields.Many2many('res.partner', 'slide_slide_partner', 'slide_id', 'partner_id',
string='Subscribers', groups='base.group_website_publisher')
slide_partner_ids = fields.One2many('slide.slide.partner', 'slide_id', string='Subscribers information', groups='base.group_website_publisher')
user_membership_id = fields.Many2one('slide.slide.partner', string="Subscriber information", compute='_compute_user_membership_id',
user_membership_id = fields.Many2one(
'slide.slide.partner', string="Subscriber information", compute='_compute_user_membership_id',
help="Subscriber information for the current logged in user")
# Quiz related fields
question_ids = fields.One2many("slide.question","slide_id", string="Questions")
quiz_first_attempt_reward = fields.Integer("First attempt reward", default=10)
quiz_second_attempt_reward = fields.Integer("Second attempt reward", default=7)
quiz_third_attempt_reward = fields.Integer("Third attempt reward", default=5,)
quiz_fourth_attempt_reward = fields.Integer("Reward for every attempt after the third try", default=2)
# content
slide_type = fields.Selection([
('infographic', 'Infographic'),
('webpage', 'Web Page'),
('presentation', 'Presentation'),
('document', 'Document'),
('webpage', 'Web Page'),
('video', 'Video')],
('video', 'Video'),
('quiz', "Quiz")],
string='Type', required=True,
default='document', readonly=True,
help="The document type will be set automatically based on the document URL and properties (e.g. height and width for presentation and document).")
@@ -3,6 +3,10 @@ access_slide_slide_all,slide.slide.all,model_slide_slide,,1,0,0,0
access_slide_slide_publisher,slide.slide.publisher,model_slide_slide,website.group_website_publisher,1,1,1,1
access_slide_slide_partner_all,slide.slide.partner.all,model_slide_slide_partner,,0,0,0,0
access_slide_slide_partner_system,slide.slide.partner.system,model_slide_slide_partner,website.group_website_publisher,1,1,1,1
access_slide_question_all,slide.question.all,model_slide_question,,1,0,0,0
access_slide_question_publisher,slide.question.publisher,model_slide_question,website.group_website_publisher,1,1,1,1
access_slide_answer_all,slide.answer.all,model_slide_answer,,1,0,0,0
access_slide_answer_publisher,slide.answer.publisher,model_slide_answer,website.group_website_publisher,1,1,1,1
access_slide_tag_all,slide.tag.all,model_slide_tag,,1,0,0,0
access_slide_tag_publisher,slide.tag.publisher,model_slide_tag,website.group_website_publisher,1,1,1,1
access_slide_channel_tag_all,slide.channel.tag.all,model_slide_channel_tag,,1,0,0,0
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
3 access_slide_slide_publisher slide.slide.publisher model_slide_slide website.group_website_publisher 1 1 1 1
4 access_slide_slide_partner_all slide.slide.partner.all model_slide_slide_partner 0 0 0 0
5 access_slide_slide_partner_system slide.slide.partner.system model_slide_slide_partner website.group_website_publisher 1 1 1 1
6 access_slide_question_all slide.question.all model_slide_question 1 0 0 0
7 access_slide_question_publisher slide.question.publisher model_slide_question website.group_website_publisher 1 1 1 1
8 access_slide_answer_all slide.answer.all model_slide_answer 1 0 0 0
9 access_slide_answer_publisher slide.answer.publisher model_slide_answer website.group_website_publisher 1 1 1 1
10 access_slide_tag_all slide.tag.all model_slide_tag 1 0 0 0
11 access_slide_tag_publisher slide.tag.publisher model_slide_tag website.group_website_publisher 1 1 1 1
12 access_slide_channel_tag_all slide.channel.tag.all model_slide_channel_tag 1 0 0 0
@@ -0,0 +1,148 @@
odoo.define('website_slides.add.section', function (require) {
'use strict';
var sAnimations = require('website.content.snippets.animation');
var core = require('web.core');
var Widget = require('web.Widget');
var _t = core._t;
var SectionDialog = Widget.extend({
template: 'website.slide.add.section',
events: {
'hidden.bs.modal': 'destroy',
'click button.save': '_save',
'click button[data-dismiss="modal"]': '_cancel',
'change input#upload': '_slideUpload',
'change input#url': '_slideUrl',
'click .list-group-item': function (ev) {
this.$('.list-group-item').removeClass('active');
$(ev.target).closest('li').addClass('active');
}
},
/**
* @override
* @param {Object} el
* @param {number} channel_id
*/
init: function (el, channelID) {
this._super(el, channelID);
this.channel_id = parseInt(channelID, 10);
this.index_content = '';
},
/**
* @override
*/
start: function () {
this.$el.modal({
backdrop: 'static'
});
return this._super.apply(this, arguments);
},
_getValue: function () {
var canvas = this.$('#data_canvas')[0],
values = {
'channel_id': this.channel_id || '',
'url': this.$('#url').val(),
'name': this.$('#section_name').val()
};
return values;
},
/**
* @private
*/
_validate: function () {
this.$('.form-group').removeClass('o_has_error').find('.form-control, .custom-select').removeClass('is-invalid');
if (!this.$('#name').val()) {
this.$('#name').closest('.form-group').addClass('o_has_error').find('.form-control, .custom-select').addClass('is-invalid');
return false;
}
var url = this.$('#url').val() ? this.is_valid_url : false;
if (!(this.file.name || url)) {
this.$('#url').closest('.form-group').addClass('o_has_error').find('.form-control, .custom-select').addClass('is-invalid');
return false;
}
return true;
},
//--------------------------------------------------------------------------
// Handlers
//--------------------------------------------------------------------------
/**
* @override
* @param {Object} ev
*/
_save: function (ev) {
var self = this;
var values = this._getValue();
if ($(ev.target).data('published')) {
values.website_published = true;
}
this.$('.oe_slides_upload_loading').show();
this.$('.modal-footer, .modal-body').hide();
this._rpc({
route: '/slides/add_category',
params: values,
}).then(function (data) {
if (data.error) {
self._displayAlert(data.error);
self.$('.oe_slides_upload_loading').hide();
self.$('.modal-footer, .modal-body').show();
} else {
window.location = data.url;
}
});
},
/**
* @override
*/
_cancel: function () {
this.trigger('cancel');
},
});
sAnimations.registry.websiteSlidesSection = sAnimations.Class.extend({
selector: '.oe_slide_js_add_section',
xmlDependencies: ['/website_slides/static/src/xml/website_slides_upload.xml'],
read_events: {
'click': '_onAddSectionClick',
},
/**
* @override
*/
start: function () {
// Automatically open the upload dialog if requested from query string
if ($.deparam.querystring().enable_slide_upload !== undefined) {
this._openDialog(this.$el.attr('channel_id'));
}
return this._super.apply(this, arguments);
},
//--------------------------------------------------------------------------
// Private
//--------------------------------------------------------------------------
_openDialog: function (channelID) {
new SectionDialog(this, channelID).appendTo(document.body);
},
//--------------------------------------------------------------------------
// Handlers
//--------------------------------------------------------------------------
/**
* @private
* @param {Event} ev
*/
_onAddSectionClick: function (ev) {
console.log('test');
this._openDialog($(ev.currentTarget).attr('channel_id'));
},
});
});
@@ -0,0 +1,427 @@
var onYouTubeIframeAPIReady;
odoo.define('website_slides.fullscreen', function (require) {
'use strict';
var sAnimations = require('website.content.snippets.animation');
var Widget = require('web.Widget');
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 Fullscreen = Widget.extend({
/**
* @override
* @param {Object} el
* @param {Object} data holding channelId and optionally upload and publish control parameters
*/
init: function(el, course_id,slide_id, user_id){
this._super.apply(this,arguments);
this.courseID = parseInt(course_id, 10);
this.slideID = parseInt(slide_id, 10);
this.userID = parseInt(user_id, 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.answeredQuestions = [];
this.slideTitle = undefined;
},
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 embed_url;
if(self.slide.slide_type !== 'webpage' || self.slide.htmlContent){
if((self.slide.slide_type == "quiz" || self.slide.hasQuiz) && !self.slide.quiz){
self._fetchQuiz();
} else {
embed_url = $(this.slide.embed_code).attr('src');
if (self.slide.slide_type === "video"){
embed_url = "https://" + embed_url + "&rel=0&autoplay=1&enablejsapi=1&origin=" + window.location.origin;
}
$('.o_wslides_fullscreen_player').html(QWeb.render('website.slides.fullscreen', {
slide: self.slide,
nextSlide: self.nextSlide,
questions: self.slide.quiz ? self.slide.quiz.questions: '',
reward: self.slide.quiz ? self.slide.quiz.nb_attempts < 3 ? self.slide.quiz.possible_rewards[self.slide.quiz.nb_attempts] : self.slide.quiz.possible_rewards[3]: self.slide.maxPoints,
embed_url: embed_url,
question_count: self.slide.quiz ? self.slide.quiz.questions.length : '',
letters: self.slide.quiz ? self.letters : '',
showMiniQuiz: self.goToQuiz
}));
if(self.slide.slide_type === "video"){
self._renderYoutubeIframe();
}
if(self.slide.slide_type == 'webpage'){
self._renderWebpage();
}
if((self.slide.slide_type == "presentation" || self.slide.slide_type == "document" || self.slide.slide_type == "infographic" || self.slide.slide_type == "webpage") && !self.slide.quiz){
self._setSlideStateAsDone();
}
if ((self.slide.quiz && self.slide.slide_type === "quiz") || self.goToQuiz){
self._renderQuiz();
}
}
} else {
self._fetchHtmlContent();
}
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 {
onYouTubeIframeAPIReady = function(){
var self = this;
self._setupYoutubePlayer();
}.bind(this)
}
},
_renderWebpage: function(){
var self = this;
$(self.slide.htmlContent).appendTo('.o_wslides_fullscreen_webpage_content');
},
_renderQuiz: function(){
var self = this;
var Quiz = new QuizWidget(this, self.slide);
Quiz.appendTo('.o_wslides_fullscreen_player');
$('.next-slide').click(function(){
self._goToNextSlide();
})
$('.back-to-video').click(function(){
self.goToQuiz = false;
self._renderPlayer();
});
},
_renderTitle: function(){
var self = this;
$('.o_wslides_fullscreen_slide_title').empty().html(QWeb.render('website.course.fullscreen.title', {
slide: self.slide,
miniQuiz: self.goToQuiz
}));
},
/**
* @private
* Links the youtube api to the iframe present in the template
*/
_setupYoutubePlayer: function(){
var self = this;
self.player = new YT.Player('youtube-player', {
host: 'https://www.youtube.com',
playerVars: {'autoplay': 1, 'origin': window.location.origin},
autoplay: 1,
events: {
'onReady': self._onPlayerReady,
'onStateChange': this._onPlayerStateChange.bind(self)
}
});
},
/**
* @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
*/
_onPlayerStateChange: function(event){
var self = this;
var tid;
clearInterval(self.tid);
if (event.data == YT.PlayerState.PLAYING && !self.slide.done) {
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();
}
}
}
}, 1000);
}
if(event.data == YT.PlayerState.ENDED){
self.player = undefined;
self._goToNextSlide();
}
},
/**
* @private
* Creates slides objects from every slide-list-cells attributes
*/
_getSlides: function(){
var self = this;
var slides = $('.o_wslides_fullscreen_sidebar_slide_tab');
for(var i = 0; i < slides.length;i++){
var slide = $(slides[i]);
self.slides.push({
id: parseInt(slide.attr('slide_id'), 10),
name: slide.attr('slide_name'),
embed_code: slide.attr('slide_embed_code'),
slide_type: slide.attr('slide_type'),
done: slide.attr('done'),
hasQuiz: slide.attr('quiz'),
slug: slide.attr('slide_slug'),
htmlContent: undefined
});
this._getActiveSlide();
}
},
/**
* @private
* @param {object} slide
* Fetch the quiz for a particular slide
*/
_fetchQuiz: function(){
var self = this;
self._rpc({
route:"/slide/quiz/get",
params: {
'slide_id': self.slide.id
}
}).then(function(data){
if(data){
self.slide.quiz = data;
self._renderPlayer();
}
})
},
_fetchHtmlContent: function(){
var self = this;
self._rpc({
route:"/slide/html_content/get",
params: {
'slide_id': self.slide.id
}
}).then(function(data){
self.slide.htmlContent = data.html_content;
self._renderPlayer();
})
},
/**
* @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;
self._rpc({
route: '/slides/set_completed',
params: {
slide_id: self.slide.id,
}
}).then(function(data){
$('#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 self = this;
var completion = self.channelCompletion <= 100 ? self.channelCompletion : 100;
$('.o_wslides_fullscreen_sidebar_progress_gauge').css('width', completion + "%" );
$('.o_wslides_progress_percentage').text(completion);
},
/**
* @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));
}
return letters;
},
_goToNextSlide: function(){
var self = this;
clearInterval(self.tid);
self.player = undefined;
self.goToQuiz = self.slide.hasQuiz && !self.goToQuiz;
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;
},
/**
* Changes the url whenever the user changes slides.
* This allows the user to refresh the page and stay at the right video
*/
_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);
},
/**
* Whenever the user changes slide, change the active tab
*/
_setActiveTab: function(){
var self = this;
self.activeTab.removeClass('active');
$('li.active').removeClass('active');
$('li[slide_id='+self.slide.id+']').addClass('active');
self.activeTab = $('.o_wslides_fullscreen_sidebar_slide_tab[index="'+self.index+'"]')
self.activeTab.addClass('active');
},
/**
* The first time the user gets on the player,
* get the slide that is represented by the active tab in the sidebar
*/
_getActiveSlide: function(){
var self = this;
self.activeTab = $('.o_wslides_fullscreen_sidebar_slide_tab.active');
self.index = parseInt(self.activeTab.attr('index'), 10);
self.slide = self.slides[self.index];
self._setPreviousAndNextSlides();
},
//--------------------------------------------------------------------------
// 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[slide_id='+self.slide.id+']').addClass('active');
self._setPreviousAndNextSlides();
self._updateUrl();
history.pushState(null,'',self.url);
}
},
_onMiniQuizClick: function(ev){
var self = this;
self.index = parseInt($(ev.currentTarget).attr('index'));
self.slide = self.slides[self.index];
self.goToQuiz = true;
self._setPreviousAndNextSlides();
self._renderPlayer();
self._setActiveTab();
self._updateUrl();
history.pushState(null,'' ,self.url);
},
/**
* @private
* Binds events related to the list
*/
_bindListEvents: function(){
var self = this;
$('.o_wslides_fullscreen_sidebar_slide_tab').each(function () {
$(this).click(self._onListCellClick.bind(self));
});
$('.o_wslides_slide_quiz ').each(function(){
$(this).click(self._onMiniQuizClick.bind(self));
})
},
_onKeyDown: function(ev){
var self = this;
switch(ev.key){
case "ArrowRight":
self._goToNextSlide();
break;
case "ArrowLeft":
self._goToPreviousSlide();
break;
}
},
})
sAnimations.registry.websiteSlidesFullscreenPlayer = Widget.extend({
selector: '.oe_js_course_slide',
xmlDependencies: ['/website_slides/static/src/xml/website_slides_fullscreen.xml'],
init: function(el){
this._super.apply(this, arguments);
},
start: function(){
this._super.apply(this, arguments);
var user_id = this.$el.attr('user_id');
var course_id = this.$el.attr('course_id');
var slide_id = this.$el.attr('slide_id');
var fullscreen = new Fullscreen(this, course_id, slide_id, user_id);
fullscreen.appendTo(".oe_js_course_slide");
}
});
return Fullscreen;
});
@@ -0,0 +1,47 @@
odoo.define('website_slides.progress.bar', function (require) {
var sAnimations = require('website.content.snippets.animation');
var Widget = require('web.Widget');
var ProgressBar = Widget.extend({
/**
* @override
* @param {Object} el
* @param {number} channel_id
*/
init: function (el, completion) {
this._super(el);
this.completion = completion;
},
/**
* @override
*/
start: function () {
this._renderProgressBar();
return this._super.apply(this, arguments);
},
//--------------------------------------------------------------------------
// Handlers
//--------------------------------------------------------------------------
_renderProgressBar: function (ev) {
var self = this;
$('.oe_slide_js_progress_bar').css('width', (self.completion <= 100 ? self.completion : 100) + '%');
},
});
sAnimations.registry.websiteSlidesProgressBar = sAnimations.Class.extend({
selector: '.oe_slide_js_progress_bar',
/**
* @override
*/
start: function () {
var completion = parseInt($('.oe_slide_js_progress_bar').attr('channel_completion'));
var progressBar = new ProgressBar(this, completion);
progressBar.appendTo(".oe_slide_js_progress_bar");
return this._super.apply(this, arguments);
},
});
return ProgressBar;
});
@@ -0,0 +1,206 @@
odoo.define('website_slides.quiz', function (require) {
'use strict';
var sAnimations = require('website.content.snippets.animation');
var core = require('web.core');
var Widget = require('web.Widget');
var QWeb = core.qweb;
var Quiz= Widget.extend({
/**
* @override
* @param {Object} el
* @param {Object} data holding all the slide elements needed for the quiz
* It will either come from the fullscreen widget or the sAnimation at the end of this file
*/
init: function(el, data){
this.slide = data;
this.answeredQuestions = [];
this.fullscreen = el;
return this._super.apply(this,arguments);
},
start: function(){
var self = this;
self._bindQuizEvents();
/**
* If the quiz is rendered by the server instead of the fullscreen widget,
* questions and their answers will have to be created manually from attributes
*/
if(self.slide.quiz.questions.length === 0){
this._setQuestions();
}
return this._super.apply(this, arguments);
},
_renderSuccessModal: function(){
var self =this;
$('.o_wslides_fullscreen_quiz').append(QWeb.render('website.course.quiz.success', {
data: data
}));
$('.submit-quiz').remove();
$('.next-slide').css('display', 'inline-block');
$('.next-slide').click(function(){
self.fullscreen._goToNextSlide();
});
$('.o_wslides_quiz_success_modal_close').click(function(){
$('.o_wslides_quiz_success_modal').remove();
$('.o_wslides_quiz_modal_background').remove();
});
$(".o_wslides_quiz_modal_background").click(function(ev){
$(ev.currentTarget).remove();
$('.o_wslides_quiz_success_modal').remove();
});
},
//--------------------------------------------------------------------------
// Private
//--------------------------------------------------------------------------
/**
* @private
* In case the quiz is rendered by the server and the data don't come from the fullscreen widget,
* questions and their answers will have to be set here by using attributes
*/
_setQuestions: function(){
var self = this;
$('.o_wslides_quiz_question').each(function(){
self.slide.quiz.questions.push({
id: parseInt($(this).attr('id')),
title: $(this).attr('title'),
answers: []
});
})
for(var i = 0; i < self.slide.quiz.questions.length; i++){
self._setAnswersForQuestion(self.slide.quiz.questions[i]);
}
},
_setAnswersForQuestion: function (question){
var self = this;
$('.o_wslides_quiz_answer[question_id='+question.id+']').each(function (){
question.answers.push({
id: parseInt($(this).attr('id')),
text: $(this).attr('text_value'),
is_correct: $(this).attr('is_correct')
});
});
},
_updateProgressbar: function (){
var self = this;
var completion = self.channelCompletion <= 100 ? self.channelCompletion : 100;
$('.o_wslides_fullscreen_sidebar_progress_gauge').css('width', completion + "%" );
$('.o_wslides_progress_percentage').text(completion);
},
_bindQuizEvents: function (){
var self = this;
if (!self.slide.done){
$('.o_wslides_quiz_answer').each(function(){
$(this).click(self._onAnswerClick.bind(self));
});
}
$('.submit-quiz').click(self._onSubmitQuiz.bind(self));
},
_highlightAnswers: function (answers){
var self = this;
self.answeredQuestions = [];
for (var i = 0; i < answers.goodAnswers.length; i++){
$('#answer'+ answers.goodAnswers[i] +'').addClass('o_wslides_quiz_good_answer');
$('#answer'+ answers.goodAnswers[i] +' .o_wslides_quiz_radio_box span').replaceWith($('<i class="fa fa-check-circle"></i>'));
var questionID =$('#answer'+ answers.goodAnswers[i] +' .o_wslides_quiz_radio_box input').attr('question_id');
self.answeredQuestions.push(questionID);
$('.o_wslides_quiz_answer[question_id='+questionID+']:not(.o_wslides_quiz_good_answer)').addClass('o_wslides_quiz_ignored_answer');
$('.o_wslides_quiz_answer[question_id='+questionID+']').each(function(){
$(this).unbind('click')
});
$('input[question_id='+questionID+']').each(function(){
$(this).prop('disabled',true);
});
}
for (i = 0; i < answers.badAnswers.length; i++){
$('#answer'+ answers.badAnswers[i]).removeClass('o_wslides_quiz_good_answer')
.addClass('o_wslides_quiz_bad_answer')
.unbind('click');
$('#answer'+ answers.badAnswers[i] +' .o_wslides_quiz_radio_box span').replaceWith($('<i class="fa fa-times "></i>'));
$('#answer'+ answers.badAnswers[i] +' .o_wslides_quiz_radio_box input').prop('checked', false);
}
},
//--------------------------------------------------------------------------
// Handlers
//--------------------------------------------------------------------------
_onAnswerClick: function (ev){
var self = this;
var target = $(ev.currentTarget);
if ((self.answeredQuestions.indexOf(target.attr('question_id'))) === -1){
var id = target.attr('id');
var question_id = target.attr('question_id');
$('.o_wslides_quiz_answer[question_id='+question_id+']').removeClass('o_wslides_quiz_good_answer');
$('#'+id+' input[type=radio]').prop('checked', true);
}
},
_onSubmitQuiz: function (){
var self = this;
var inputs = $('input[type=radio]:checked');
var values = [];
for (var i = 0; i < inputs.length; i++){
values.push(parseInt($(inputs[i]).val()));
}
if (values.length === self.slide.quiz.questions.length){
$('.quiz-danger').remove();
self._rpc({
route: "/slide/quiz/submit",
params: {
slide_id: self.slide.id,
answer_ids: values,
quiz_id: self.slide.quiz_id
}
}).then(function(data){
self._highlightAnswers(data);
self.slide.quiz.nb_attempts = data.attempts_count;
if (data.passed){
self.channelCompletion = data.channel_completion;
self._updateProgressbar();
$('#check-'+self.slide.id).replaceWith($('<i class="check-done o_wslides_slide_completed fa fa-check-circle"></i>'));
self.slide.done = true;
self._renderSuccessModal();
}
else {
$('#quiz-points').text(self.slide.quiz.nb_attempts < 3 ? self.slide.quiz.possible_rewards[self.slide.quiz.nb_attempts] : self.slide.quiz.possible_rewards[self.slide.quiz.possible_rewards.length-1]);
}
});
} else {
$('.o_wslides_fullscreen_player').append($('<p class="quiz-danger text-danger mt-1">All questions must be answered !</p>'));
}
},
});
sAnimations.registry.websiteSlidesQuizNoFullscreen = Widget.extend({
selector: '.o_w_slides_quiz_no_fullscreen',
xmlDependencies: ['/website_slides/static/src/xml/website_slides_fullscreen.xml'],
init: function (el){
this._super.apply(this, arguments);
},
start: function (){
this._super.apply(this, arguments);
var slideID = parseInt(this.$el.attr('slide_id'),10);
var slideDone = this.$el.attr('slide_done');
var nbAttempts = parseInt(this.$el.attr('nb_attempts'), 10);
var firstAttemptReward = this.$el.attr('first_reward');
var secondAttemptReward = this.$el.attr('second_reward');
var thirdAttemptReward = this.$el.attr('third_reward');
var fourthAttemptReward = this.$el.attr('fourth_reward');
var possibleRewards = [firstAttemptReward,secondAttemptReward,thirdAttemptReward,fourthAttemptReward]
var data = {
id: slideID,
done: slideDone,
quiz: {
questions: [],
nb_attempts: nbAttempts,
possible_rewards: possibleRewards,
reward: nbAttempts < possibleRewards.length ? possibleRewards[nbAttempts] : possibleRewards[possibleRewards.length-1]
}
}
if(!slideDone){
var NewQuiz = new Quiz(this, data);
NewQuiz.appendTo(".o_w_slides_quiz_no_fullscreen");
}
}
});
return Quiz;
});
@@ -0,0 +1,34 @@
odoo.define('website_slides.sidebar', function (require) {
'use strict';
var sAnimations = require('website.content.snippets.animation');
var core = require('web.core');
var Widget = require('web.Widget');
var SideBar = Widget.extend({
init: function (el){
this._super.apply(this,arguments);
},
start: function (){
$('.o_wslides_fullscreen_toggle_sidebar').click(function (ev){
ev.preventDefault();
$(ev.currentTarget).toggleClass('active');
$('.o_wslides_fullscreen_sidebar').toggleClass('o_wslides_fullscreen_sidebar_hidden');
$('.o_wslides_fullscreen_player').toggleClass('o_wslides_fullscreen_player_no_sidebar')
})
return this._super.apply(this, arguments);
},
});
sAnimations.registry.websiteSlidesSidebarList = Widget.extend({
selector: '.o_wslides_fullscreen_toggle_sidebar',
// xmlDependencies: ['/website_slides/static/src/xml/website_slides.xml'],
init: function (el){
this._super.apply(this, arguments);
},
start: function (){
this._super.apply(this, arguments);
var sideBar = new SideBar(this);
sideBar.appendTo(".oe_js_side_bar_list");
}
});
});
@@ -0,0 +1,251 @@
odoo.define('website_slides.slideslist', function (require) {
'use strict';
var sAnimations = require('website.content.snippets.animation');
var Widget = require('web.Widget');
var SlideUpload = require('website_slides.upload_modal');
var List = Widget.extend({
init: function (el){
this._super.apply(this,arguments);
this.draggedElement = undefined;
this.dropTarget = undefined;
this.slideCount = undefined;
this.slides = [];
this.categories = [];
},
start: function (){
this._super.apply(this,arguments);
this.slideCount = $('li.content-slide').length;
//Change links HREF to fullscreen mode for SEO
var links = $(".link-to-slide");
for (var i = 0; i < links.length; i++){
$(links[i]).attr('href', $(links[i]).attr('href') + "?fullscreen=1");
}
this._bindEvents();
},
_bindEvents: function (){
var self = this;
$('.slide-draggable').each(function (){
self._addSlideDragAndDropHandlers($(this));
});
$('.section-draggable').each(function (){
self._addSectionDragAndDropHandlers($(this));
});
$('.course-section').each(function (){
self._addDropSlideOnSectionHandler($(this));
});
},
_unbind: function (className){
$("."+className).each(function (){
$(this).unbind();
});
},
_unbindAll: function (){
this._unbind('slide-draggable');
this._unbind('section-draggable');
this._unbind('course-section');
},
_getSlides: function (){
var self = this;
var slides = $('li.content-slide');
for(var i = 0; i < slides.length;i++){
var slide = $(slides[i]);
self.slides.push({
id: parseInt(slide.attr('slide_id')),
category_id: parseInt(slide.attr('category_id')),
sequence: i
});
}
},
_getCategories: function (){
var self = this;
self.categories = [];
var categories = $('.course-section');
for (var i = 0; i < categories.length;i++){
var category = $(categories[i]);
self.categories.push(parseInt(category.attr('category_id')));
}
},
_addDropSlideOnSectionHandler: function (target){
var self = this;
target.on('drop', function (ev){
if (ev.preventDefault){
ev.preventDefault();
}
self.dropTarget = $(ev.currentTarget);
self.draggedElement[0].parentNode.removeChild(self.draggedElement[0]);
$('ul[category_id='+target.attr('category_id')+']').append(self.draggedElement)
self._addSlideDragAndDropHandlers(self.draggedElement);
self._reorderSlides();
});
target.on('dragover', function (ev){
if(ev.preventDefault){
ev.preventDefault();
}
});
},
_addSlideDragAndDropHandlers: function (target){
var self = this;
target.on('dragstart', function (ev){
$('.section-draggable').removeClass('hold')
self._unbind('section-draggable');
ev.originalEvent.dataTransfer.effectAllowed = 'move';
ev.originalEvent.dataTransfer.setData('text/html', this.outerHTML);
self.draggedElement = target;
self.draggedElement.addClass('hold');
});
target.on('dragover', function (ev){
if ($(ev.currentTarget) !== self.draggedElement){
if (ev.preventDefault){
ev.preventDefault();
}
target.addClass('slide-hovered');
}
});
target.on('dragleave', function (ev){
if (ev.preventDefault){
ev.preventDefault();
}
target.removeClass('slide-hovered');
});
target.on('drop', function (ev){
if (self.draggedElement.hasClass('slide-draggable') && target.hasClass('slide-draggable')){
if (ev.preventDefault){
ev.preventDefault();
}
target.removeClass('slide-hovered');
target.removeClass('hold');
if (target !== self.draggedElement){
self.dropTarget = $(ev.currentTarget);
self.draggedElement[0].parentNode.removeChild(self.draggedElement[0]);
var dropHTML = ev.originalEvent.dataTransfer.getData('text/html');
target[0].insertAdjacentHTML('beforebegin',dropHTML);
self.draggedElement = $(target[0].previousSibling);
self._reorderSlides();
}
self._unbindAll();
self._bindEvents();
}
});
target.on('dragend', function (ev){
if (ev.preventDefault){
ev.preventDefault();
}
target.removeClass('slide-hovered');
target.removeClass('hold');
});
},
_addSectionDragAndDropHandlers: function(target){
var self = this;
target.on('dragstart', function (ev){
self._unbind('slide-draggable');
self._unbind('course-section');
ev.originalEvent.dataTransfer.effectAllowed = 'move';
ev.originalEvent.dataTransfer.setData('text/html', this.outerHTML);
self.draggedElement = target;
self.draggedElement.addClass('hold');
});
target.on('dragover', function (ev){
if (target.hasClass('section-draggable') && self.draggedElement.hasClass('section-draggable')){
if (ev.preventDefault){
ev.preventDefault();
}
target.addClass('slide-hovered');
}
});
target.on('dragleave', function (ev){
if (ev.preventDefault){
ev.preventDefault();
}
target.removeClass('slide-hovered');
});
target.on('drop', function (ev){
if(ev.preventDefault){
ev.preventDefault();
}
if(self.draggedElement.hasClass('section-draggable') && target.hasClass('section-draggable')){
target.removeClass('slide-hovered');
target.removeClass('hold');
self.dropTarget = $(ev.currentTarget);
if(target !== self.draggedElement && $(ev.currentTarget).hasClass('section-draggable')){
self.draggedElement[0].parentNode.removeChild(self.draggedElement[0]);
var dropHTML = ev.originalEvent.dataTransfer.getData('text/html');
target[0].insertAdjacentHTML('beforebegin',dropHTML);
self.draggedElement = $(target[0].previousSibling);
self._reorderCategories();
self._reorderSlides();
self._rebindUploadButton(self.draggedElement.attr('category_id'));
}
self._unbindAll();
self._bindEvents();
}
});
target.on('dragend', function (ev){
if (ev.preventDefault){
ev.preventDefault();
}
target.removeClass('slide-hovered');
target.removeClass('hold');
});
},
_reorderCategories: function (){
var self = this;
self._getCategories();
self._rpc({
route: '/web/dataset/resequence',
params: {
model: "slide.category",
ids: self.categories
}
}).then(function (){
self._resetCategoriesIndex();
});
},
_resetCategoriesIndex: function (){
var categoriesIndexes = $('.section-index')
for (var i = 0; i < categoriesIndexes.length; i++){
$(categoriesIndexes[i]).text(i+1)
}
},
_reorderSlides: function(){
var self = this;
// In case the slide was transfered to another section
if (self.draggedElement.hasClass('slide-draggable')){
self.draggedElement.attr('category_id', parseInt(self.dropTarget.attr('category_id')))
}
self.slides = [];
self._getSlides();
self._rpc({
route: "/slides/resequence_slides",
params: {
slides_data: self.slides
}
}).then(function(){
});
},
_rebindUploadButton: function(categoryID){
var self = this;
this.$('.oe_slide_js_upload[data-category-id='+categoryID+']').click(function(ev){
ev.preventDefault();
var data = $(ev.currentTarget).data();
var dialog = new SlideUpload.SlideUploadDialog(self, data);
dialog.appendTo(document.body);
dialog.open();
})
}
})
sAnimations.registry.websiteSlidesCourseSlidesList = Widget.extend({
selector: '.oe_js_course_slides_list',
xmlDependencies: ['/website_slides/static/src/xml/website_slides_upload.xml'],
init: function (el){
this._super.apply(this, arguments);
},
start: function (){
this._super.apply(this, arguments);
var list = new List(this);
list.appendTo(".oe_js_course_slides_list");
}
});
});
@@ -0,0 +1,88 @@
odoo.define('website_slides.delete.slide', function (require) {
var sAnimations = require('website.content.snippets.animation');
var core = require('web.core');
var Widget = require('web.Widget');
var _t = core._t;
var QWeb = core.qweb;
var DeleteSlideDialog = Widget.extend({
template: 'website.slide.delete.slide',
events: {
'hidden.bs.modal': 'destroy',
'click button[data-dismiss="modal"]': '_cancel',
'click button.delete': '_delete'
},
/**
* @override
* @param {Object} el
* @param {number} channel_id
*/
init: function (el, slideID) {
this._super(el, slideID);
this.slide_id = parseInt(slideID, 10);
},
/**
* @override
*/
start: function () {
this.$el.modal({
backdrop: 'static'
});
return this._super.apply(this, arguments);
},
//--------------------------------------------------------------------------
// Handlers
//--------------------------------------------------------------------------
_delete: function (ev) {
var self = this;
// TO FIX: CallBack is not executed
$('[slide_id='+this.slide_id+']').remove();
this._rpc({
model: 'slide.slide',
method: 'unlink',
args: [[self.slide_id]],
}).then(function () {
$('[slide='+this.slide_id+']').remove();
});
},
/**
* @override
*/
_cancel: function () {
this.trigger('cancel');
}
});
sAnimations.registry.websiteSlidesDeleteSlide = sAnimations.Class.extend({
selector: '.oe_slide_js_delete_slide',
xmlDependencies: ['/website_slides/static/src/xml/website_slides_upload.xml'],
read_events: {
'click': '_onDeleteSlideClick',
},
//--------------------------------------------------------------------------
// Private
//--------------------------------------------------------------------------
_openDialog: function (slideID) {
new DeleteSlideDialog(this, slideID).appendTo(document.body);
},
//--------------------------------------------------------------------------
// Handlers
//--------------------------------------------------------------------------
/**
* @private
* @param {Event} ev
*/
_onDeleteSlideClick: function (ev) {
var target = $(ev.currentTarget);
this._openDialog(target.attr('slide_id'));
},
});
return DeleteSlideDialog;
});
@@ -30,6 +30,7 @@ var SlideUploadDialog = Dialog.extend({
this._setup();
this.channelID = parseInt(options.channelId, 10);
this.defaultCategoryID = parseInt(options.categoryId,10)
this.canUpload = options.canUpload === 'True';
this.canPublish = options.canPublish === 'True';
@@ -198,6 +199,7 @@ var SlideUploadDialog = Dialog.extend({
*/
_getSelect2DropdownValues: function (){
var result = {};
var self = this;
// tags
var tagValues = [];
_.each(this.$('#tag_ids').select2('data'), function (val) {
@@ -211,11 +213,17 @@ var SlideUploadDialog = Dialog.extend({
result['tag_ids'] = tagValues;
}
// category
var categoryValue = this.$('#category_id').select2('data');
if (categoryValue && categoryValue.create) {
result['category_id'] = [0, {'name': categoryValue.text}];
} else if (categoryValue) {
result['category_id'] = [categoryValue.id];
if(!self.defaultCategoryID){
var categoryValue = this.$('#category_id').select2('data');
if (categoryValue && categoryValue.create) {
result['category_id'] = [0, {'name': categoryValue.text}];
} else if (categoryValue) {
result['category_id'] = [categoryValue.id];
this.categoryID = categoryValue.id;
}
} else {
result['category_id'] = [self.defaultCategoryID];
this.categoryID = self.defaultCategoryID;
}
return result;
},
@@ -303,6 +311,25 @@ var SlideUploadDialog = Dialog.extend({
return values;
},
_reorderSlidesSequence: function(){
var self = this;
var slidesElement = $('li.content-slide');
var slides = [];
for(var i = 0; i < slidesElement.length;i++){
slides.push({
id: parseInt($(slidesElement[i]).attr('slide_id')),
category_id: parseInt($(slidesElement[i]).attr('category_id')),
sequence: i
})
}
self._rpc({
route: '/slides/resequence_slides',
params: {
slides_data: slides
}
}).then(function(){
})
},
/**
* Init the data relative to the support slide type to upload
*
@@ -325,6 +352,11 @@ var SlideUploadDialog = Dialog.extend({
label: _t('Video'),
template: 'website.slide.upload.modal.video',
},
quiz: {
icon: 'fa-question-circle',
label: _t('Quiz'),
template: 'website.slide.upload.quiz'
}
};
},
/**
@@ -472,12 +504,11 @@ var SlideUploadDialog = Dialog.extend({
_onChangeSlideUrl: function (ev) {
var self = this;
var url = $(ev.target).val();
this._alertRemove();
this.isValidUrl = false;
this.set('can_submit_form', false);
this._fetchUrlPreview(url).then(function (data) {
this.set('can_submit_form', true);
self.set('can_submit_form', true);
if (data.error) {
self._alertDisplay(data.error);
} else {
@@ -508,6 +539,12 @@ var SlideUploadDialog = Dialog.extend({
self.set('state', oldType);
self._alertDisplay(data.error);
} else {
//Quick and really dirty fix for reordering issues
if(data.channel_type == 'training' && self.categoryID){
var categoryElement = $('ul[category_id='+self.categoryID+']');
$('<li hidden class="content-slide" slide_id="'+data.slide_id+'" category_id="'+self.categoryID+'">temp</li>').appendTo(categoryElement)
self._reorderSlidesSequence();
}
window.location = data.url;
}
});
@@ -5,6 +5,7 @@ var core = require('web.core');
var Dialog = require('web.Dialog');
var QWeb = core.qweb;
var WebsiteNewMenu = require('website.newMenu');
var wUtils = require('website.utils');
var _t = core._t;
@@ -0,0 +1,536 @@
.banner{
position: absolute;
top: 0;
left: 0;
height: 20%;
background-color: $primary;
width: 256px;
opacity: 0;
color: #fff;
transition: all .3s ease-out;
z-index: 2;
visibility: hidden;
display: flex;
justify-content: space-around;
align-items: center;
margin-left: 15px;
}
.banner a {
font-size: 1.5rem;
text-align: center;
padding: 20px;
}
.banner a:hover{
cursor: pointer;
}
.fill {
display: flex;
justify-content: center;
align-items: center;
overflow: hidden
}
.fill img {
flex-shrink: 0;
min-width: 100%;
min-height: 100%
}
.main_image{
position: relative
}
.main_image:hover{
.banner{
opacity: 1;
visibility: visible;
}
}
.course-content {
padding: 0;
ul {
list-style: none;
padding: 0
}
}
.course-section {
margin: 0;
background-color: #ddd;
display: flex;
list-style: none;
font-size: 1.05rem;
border-bottom: 1px solid #ccc
}
.content-type {
width: 30%;
}
.content-type:hover{
cursor: pointer;
}
.course-progress-bar{
height: 10px;
width: 80%;
box-sizing: border-box;
border-radius: 5px;
background-color: rgba(0,0,0,.2);
overflow: hidden;
.progress-gauge{
height: 100%;
width: 0;
background-color: $primary;
transition: all .8s ease-out;
}
}
.course-rating{
color:$primary;
}
.course-banner{
border-bottom: 1px solid $primary;
}
.content-tab{
.active {
border: none;
border-bottom: 1px solid $secondary;
}
}
.content-tab a:hover{
border:none;
border-bottom: 1px solid $secondary;
}
.content-container{
border:none;
}
.content-slide:nth-child(odd){
background-color: #f6f6f6;
}
.content-slide:nth-child(even){
background-color: #f9f9f9;
}
.content-slide-controls {
i:hover{
color: $primary !important;
cursor: pointer;
}
}
.viewer-container {
display: flex;
align-items: stretch;
height: 500px;
width: 100%;
background-color: #ccc;
justify-content: flex-start;
}
.slide-infos {
margin-top: 1rem;
display: flex;
justify-content: space-between;
align-items: start;
}
.oe_js_player {
}
.slide-deactivated {
pointer-events: none;
}
.slide-draggable {
user-select: none;
-khtml-user-drag: element;
-webkit-user-drag: element;
}
.slide-hovered{
margin-top: 10px;
}
.section-draggable {
user-select: none;
-khtml-user-drag: element;
-webkit-user-drag: element;
}
.hold{
border: solid #ccc 4px;
}
.fullscreen {
position: absolute;
top: 0;
right: 0;
width: 100%;
z-index: 1000;
height: 100%;
margin: 0;
padding: 0;
}
.sidebar-list{
position: fixed;
top: 46px;
bottom: 0;
left: 0;
width: 300px;
z-index: 2000;
}
.sidebar-list-visible {
left: 50px;
}
.show-list {
position: absolute;
top: 15%;
left: 50px;
font-size: 2rem;
z-index: 10000;
color: #a0a0a0;
border: 2px solid #ccc;
border-radius: 10px;
padding: 5px 10px;
}
.hover-background {
opacity: 0;
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
z-index: 3000;
}
.hover-background:hover {
opacity: 1;
}
.show-list:hover {
cursor: pointer;
}
.change-small-screen {
font-size: 1.5rem;
z-index: 10000;
color: #a0a0a0;
}
.change-small-screen:hover {
cursor: pointer;
border-color: $primary;
color: $primary;
}
.sidebar-header {
padding: 10px 15px;
display: flex;
justify-content: space-between;
align-items: center;
font-size: 1.5rem;
border-bottom: 1px solid #ccc;
height: 80px;
a {
color: black;
}
a:hover {
color: $primary;
}
i:hover{
color: $primary;
}
}
.close-list:hover {
cursor: pointer;
}
.fullscreen-btn{
font-size: 2rem;
display: inline-block;
a {
text-decoration: none;
color: $text-muted;
}
}
.fullscreen-btn:hover{
cursor: pointer;
a {
color: $primary;
}
}
.sidebar-fullscreen {
position: relative;
top: 0;
left: 0;
bottom: 0;
height: auto;
transform: translateX(-100%);
width: 0;
}
.fullscreen-question-container{
background-color: #fff;
width: 100%;
padding-top: 20px;
overflow: auto;
}
.fullscreen-question{
border-radius: 10px;
padding: 10px 20px;
box-shadow: 0px 1px 4px rgba(0,0,0,.5);
margin-top: 50px;
}
.question-title {
font-size: 1.5rem;
}
.buttons-container{
width: 80%;
display: flex;
justify-content: space-between;
flex-direction: row-reverse;
margin-top: 40px;
}
// .slide-resources {
// border-bottom: 1px solid #ccc;
// }
.slide-resource {
padding: 5px 50px;
width: 100%;
i{
margin-right: 10px;
}
}
.list-cell {
}
.quiz-header{
border-bottom: 2px solid #CBCBCB;
margin-bottom: 50px;
}
.quiz-header-container{
display: flex;
justify-content: space-between;
align-items: center;
h1 {
width: 80%;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
}
.quiz-good-answer{
background-color: #C7EAE9;
}
.quiz-bad-answer{
background-color: #FEBEBE;
}
.quiz-bad-answer label.radio-box,
.quiz-good-answer label.radio-box{
border: none;
}
.quiz-bad-answer label.radio-box i{
background-color: #FD0606;
color: white;
height: 100%;
width: 100%;
font-size: 1.5rem;
display: flex;
justify-content: center;
align-items: center;
padding: 50px;
}
.quiz-ignored-answer {
color: $text-muted;
}
.quiz-good-answer label.radio-box input:checked + i{
height: 100%;
width: 100%;
background-color: #fff;
display: flex;
justify-content: center;
align-items: center;
font-size: 2.4rem;
border: 10px solid $primary;
border-radius: 50px;
}
.quiz-answer {
border-radius: 10px;
padding: 5px 10px;
transition: all .2s ease-in-out;
display: flex;
align-items: center;
margin-bottom: 5px;
}
.quiz-answer:not(.quiz-good-answer,.quiz-bad-answer,.quiz-ignored-answer):hover{
background-color: #E5E5E5;
cursor: pointer;
}
label.radio-box{
border: 2px solid #CBCBCB;
background-color: #fff;
width: 50px;
height: 50px;
display: inline-flex;
border-radius: 50%;
overflow: hidden;
justify-content: center;
align-items: center;
margin: 0;
margin-right: 20px;
font-size: 1.2rem;
outline: none;
}
.quiz-answer:not(.quiz-good-answer,.quiz-bad-answer,.quiz-ignored-answer) label.radio-box:hover{
cursor: pointer;
}
label.radio-box input{
display:none;
outline: none;
border: none;
}
label.radio-box span{
color: inherit;
padding: 20px;
}
label.radio-box input:checked + span{
background-color: $primary;
color: #fff;
}
label.radio-box input:checked{
background-color: #C7EAE9;
}
.quiz-question {
border-bottom: 2px solid #CBCBCB;
padding-bottom: 20px;
margin-bottom: 20px;
}
.quiz-question-answers {
list-style: none;
margin: 0;
margin-top: 20px;
padding: 0;
font-size: 1rem;
}
.submit-quiz {
margin-top: 20px;
}
.quiz-modal-background{
position: absolute;
top: 0;
left: 0;
background-color: rgba(0,0,0,.5);
width: 100%;
height: 100%;
}
.quiz-modal-container{
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
}
.quiz-modal{
position: absolute;
width: 30%;
height: 500px;
background-color: #fff;
top: 50%;
left: 50%;
transform: translateX(-50%) translateY(-50%);
border-radius: 10px;
z-index: 2000;
}
.quiz-points-container{
font-size: 1.2rem;
}
.quiz-points {
background-color: #C4C4C4;
padding: 0 4px ;
border-radius: 10px;
color: #fff;
}
.quiz-points-won{
background-color: $primary;
}
.webpage-container{
background-color: #fff;
width: 100%;
}
.webpage-content {
background-color: #fff;
height: 100%;
}
@@ -0,0 +1,156 @@
.slide-header {
background-color: #62495B;
height: 200px;
}
.slide-header-container {
width: 1000px;
margin: 0 auto;
height: 100%;
}
.slide-header-box{
display: flex;
height: 100%;
width: 50%;
flex-direction: column;
justify-content: flex-end;
padding-bottom: 50px;
a {
color: #fff;
font-size: 2.5rem;
text-decoration: none !important;
}
}
.slide-content-list {
position: absolute;
top: 140px;
left: 80px;
background-color: #fff;
height: 500px;
z-index: 200;
}
.slide-list{
background-color: #fff;
list-style: none;
height: 100%;
padding: 0;
overflow: auto;
border: 1px solid #ccc;
border-left: none;
box-sizing: border-box;
flex: 1;
a {
text-decoration: none;
color: $text-muted;
}
a .active{
background-color: #eee !important;
}
}
.slide-content-list-section{
border-bottom: 1px solid #fff;
color: #9B9B9B;
text-transform: uppercase;
padding: 10px 15px;
}
.slide-content-list-slide{
color: #666666;
padding: 10px 15px;
}
.slide-list-cell{
word-wrap: none;
padding: 0.5rem 1rem;;
height: 40px;
box-sizing: border-box;
display: flex;
align-items: center;
text-decoration: none !important;
}
.slide-list-cell:hover{
background-color: #eee;
cursor: pointer;
}
.slide-list-cell:active{
background-color: #eee;
cursor: pointer;
}
.slide-content-list-header {
padding: 10px 15px;
font-size: 1.2rem;
background-color: #F7F7F7;
color: #6F6F6F;
}
.slide-button {
color: #666;
text-decoration: none;
border:1px solid #B9B9B9;
padding: 5px 10px;
width: 50px;
text-align: center;
text-transform: uppercase;
margin: 0;
border-radius: 3px;
}
.slide-button-fullscreen {
color: #666;
text-decoration: none;
border:1px solid #B9B9B9;
padding: 5px 10px;
text-align: center;
text-transform: uppercase;
margin: 0;
border-radius: 3px;
}
.slide-button-done {
color: #fff;
text-transform: uppercase;
background-color: $primary;
text-decoration: none;
padding: 10px 15px;
width: 50px;
text-align: center;
border-radius: 3px;
margin: 0;
}
.slide-title {
width: 60%;
display: flex;
align-items: center;
h1 {
font-size: 1.5rem;
}
.slide-points {
margin-left: 20px;
padding: 5px 10px;
background-color: #DC9D45;
border-radius: 10px;
color: #fff;
font-weight: bold;
}
}
.slide-documentation-sidebar {
position: absolute;
top: 300px;
left: 30px;
}
@@ -0,0 +1,303 @@
.o_wslides {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
z-index: 2000;
.o_wslides_slide_fullscreen_header {
align-items: center;
background-color: #313A44;
color: #AEB0B2;
display: flex;
height: 50px;
justify-content: space-between;
width: 100%;
.o_wslides_fullscreen_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_fullscreen_container {
display: flex;
align-items: stretch;
height: 100%;
position: relative;
background-color: #313A44;
}
.o_wslides_fullscreen_sidebar_hidden{
transform: translateX(-100%);
}
.o_wslides_fullscreen_sidebar{
width: 400px;
min-width: 400px;
background-color: #313A44;
color: #fff;
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_fullscreen_sidebar_header{
height: 100px;
padding: 20px 0;
display: flex;
flex-direction: column;
.o_wslides_slide_fullscreen_progress_box{
display: flex;
align-items: center;
font-size: .9rem;
.o_wslides_fullscreen_sidebar_progressbar{
height: 5px;
border-radius: 10px;
width: 70%;
background-color: rgba(0,0,0,.3);
margin-right: 40px;
overflow: hidden;
}
.o_wslides_fullscreen_sidebar_progress_gauge {
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_fullscreen_sidebar_section {
margin-bottom: 20px;
background-color: #283038;
padding: 10px 0;
}
.o_wslides_fullscreen_sidebar_section_tab {
color: #656B72;
font-size: .8rem;
text-transform: uppercase;
font-weight: bold;
padding: 5px 0 5px 15px;
}
.o_wslides_fullscreen_sidebar_section_slides{
padding: 0;
margin-bottom: 15px;
li {
margin: 0;
padding: 0;
border-left: 2px solid transparent;
display: flex;
a {
flex: 1;
}
}
.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;
}
li:hover,
li.active {
border-left: 2px solid $primary;
cursor: pointer;
background-color: #1D2228;
}
.o_wslides_fullscreen_sidebar_slide_tab {
padding:5px 0 5px 15px;
font-size: .8rem;
color: #8A8F93;
font-weight: bold;
flex: 1;
}
.o_wslides_fullscreen_sidebar_slide_tab.active{
color: #fff;
}
.o_wslides_fullscreen_slide_tab_line{
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_full_line{
width: 1px;
height: 100%;
background-color: #787D82;
}
}
.o_wslides_slide_link {
color: #098586;
font-size: .8rem;
margin-left: 15px;
flex: 1;
i {
margin-right: 10px;
}
}
.o_wslides_slide_quiz {
font-size: .8rem;
margin-left: 15px;
color: #8A8F93;
flex: 1;
i {
color: #D39B4B;
margin-right: 10px;
}
}
}
}
.o_wslides_fullscreen_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_fullscreen_webpage {
background-color: #fff;
width: 100%;
height: 100%;
overflow-y: scroll;
padding: 20px 0;
}
.o_wslides_fullscreen_player_no_sidebar {
max-width: 100%;
width: 100%;
}
}
@@ -0,0 +1,237 @@
.o_wslides_fullscreen_quiz {
background-color: #F6F6F6;
width: 100%;
height: 100%;
overflow: scroll;
overflow-x: hidden;
padding-bottom: 50px;
}
.o_wslides_quiz_question {
margin-top: 50px;
width: 100%;
.o_wslides_quiz_question_title {
font-size: 1.5rem;
margin-bottom: 20px;
.o_wslides_quiz_question_title_number {
color: #8C8C8C;
font-size: 1.3rem;
font-weight: bold;
}
.o_wslides_quiz_question_title_text {
margin-left: 2px;
color: #2F2F2F;
}
}
.o_wslides_quiz_question_answers{
background-color: #fff;
list-style: none;
padding: 0;
.o_wslides_quiz_answer {
padding: 10px 20px;
border-bottom: 1px solid #B6B6B6;
font-size: 1.2rem;
display: flex;
align-items: center;
label.o_wslides_quiz_radio_box{
background-color: #f6f6f6;
width: 35px;
height: 35px;
display: inline-flex;
border-radius: 50%;
overflow: hidden;
justify-content: center;
align-items: center;
margin: 0;
margin-right: 20px;
font-size: 1.2rem;
outline: none;
border: none;
padding: 0;
}
label.o_wslides_quiz_radio_box input{
display:none;
outline: none;
border: none;
}
label.o_wslides_quiz_radio_box span{
width: 18px;
height: 18px;
margin: 0;
}
label.o_wslides_quiz_radio_box input:checked + span{
background-color: $primary;
border-radius: 50%;
overflow: hidden;
}
label.o_wslides_quiz_radio_box input:checked{
background-color: #C7EAE9;
}
}
.o_wslides_quiz_answer:not(.o_wslides_quiz_good_answer,.o_wslides_quiz_bad_answer,.o_wslides_quiz_ignored_answer):hover {
cursor: pointer;
}
.o_wslides_quiz_good_answer{
border: 2px solid #28A745;
label.o_wslides_quiz_radio_box input:checked + i{
height: 100%;
width: 100%;
background-color: #fff;
color: #28A745;
display: flex;
justify-content: center;
align-items: center;
font-size: 1.4rem;
border: 10px solid #28A745;
border-radius: 50px;
}
}
.o_wslides_quiz_bad_answer{
border: 2px solid #A94442;
label.o_wslides_quiz_radio_box i{
background-color: #A94442;
color: white;
height: 100%;
width: 100%;
font-size: 1rem;
display: flex;
justify-content: center;
align-items: center;
}
}
.quiz-ignored-answer {
color: $text-muted;
}
}
}
.o_wslides_quiz_modal_background{
position: absolute;
top: 0;
left: 0;
background-color: rgba(0,0,0,.2);
width: 100%;
height: 100%;
}
.o_wslides_quiz_success_modal {
display: flex;
position: absolute;
width: 40%;
max-width: 600px;
height: 450px;
background-color: #fff;
top: 50%;
left: 50%;
transform: translateX(-50%) translateY(-50%);
z-index: 2000;
}
.o_wslides_quiz_success_modal_left_panel {
width: 50%;
background-color: #855B79;
}
.o_wslides_quiz_success_modal_right_panel{
width: 50%;
display: flex;
flex-direction: column;
padding: 10px 20px 10px 50px;
position: relative;
.o_wslides_quiz_success_modal_close {
position: absolute;
top: 10px;
right: 15px;
text-transform: uppercase;
color: #787878;
font-size: 1.2rem;
}
.o_wslides_quiz_success_modal_close:hover{
cursor: pointer;
}
h1 {
color: #21272B;
font-weight: bold;
font-size: 2.3rem;
padding: 20px 0;
letter-spacing: 1px;
}
.o_wslides_quiz_success_progress_bar{
height: 10px;
width: 100%;
background-color: rgba(0,0,0,.1);
.o_wslides_quiz_success_progress_gauge{
width: 75%;
height: 100%;
background-color: #01ADAB;
}
}
.o_wslides_quiz_success_progress_bounds{
display: flex;
justify-content: space-between;
}
.o_wslides_quiz_success_progress_bounds{
font-weight: bold;
span:first-child {
color: #019F9D;
}
span:last-child{
color: #787878;
}
}
.o_wslides_quiz_success_reward {
height: 80px;
width: 100%;
border: 1px dashed #ccc;
margin-top: 70px;
}
.o_wslides_quiz_success_button{
flex: 1;
display: flex;
justify-content: flex-end;
align-items: flex-end;
a {
color: #787878;
text-transform: uppercase;
background-color: #fff;
border-radius: 3px;
border: 1px solid #C4C4C4;
font-weight: bold;
padding: 5px 15px;
text-align: center;
}
a:hover {
cursor: pointer;
}
}
}
@@ -0,0 +1,111 @@
<templates id="template" xml:space="preserve">
<t t-name="website.slides.fullscreen">
<div t-if="slide.slide_type == 'video' &amp;&amp; !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="slide.slide_type in ['presentation', 'document'] &amp;&amp; !showMiniQuiz" class="embed-responsive embed-responsive-4by3 embed-responsive-item ">
<iframe t-att-src="embed_url" class="o_wslides_iframe_viewer" allowFullScreen="true" height="315" width="420" frameborder="0"></iframe>
</div>
<div style="width:100%;height:100%;" t-if="slide.slide_type == 'infographic' &amp;&amp; !showMiniQuiz">
<img t-attf-src="/web/image/slide.slide/#{slide.id}/datas" class="img-fluid" style="width: 100%" alt="Slide image"/>
</div>
<div t-if="(slide.slide_type == 'quiz' || showMiniQuiz) &amp;&amp; questions " class="o_wslides_fullscreen_quiz">
<div>
<t t-call="website.slide.quiz"/>
<div class="container">
<button t-if="!slide.done" class="btn btn-primary submit-quiz" >Check your answers</button>
<button t-if="embed_url" class="btn btn-primary back-to-video" >Watch again</button>
<button t-if="slide.done &amp;&amp; nextSlide" class="btn btn-primary next-slide" >Continue</button>
</div>
</div>
</div>
<div t-if="slide.slide_type == 'webpage'" class="o_wslides_fullscreen_webpage">
<div class="o_wslides_fullscreen_webpage_content container"/>
</div>
<div class="o_wslides_fullscreen_content"/>
</t>
<t t-name="website.slide.quiz">
<div class="container quiz-questions">
<t t-set="i" t-value="1"/>
<t t-foreach="questions" t-as="question">
<t t-set="j" t-value="0"/>
<div class="o_wslides_quiz_question">
<div>
<div class="o_wslides_quiz_question_title">
<span class="o_wslides_quiz_question_title_number"><span t-esc="i"/>. </span>
<span class="o_wslides_quiz_question_title_text" t-esc="question.title"/>
</div>
<ul class="o_wslides_quiz_question_answers">
<t t-foreach="question.answers" t-as="answer">
<li t-attf-id="answer#{answer.id}" t-att-question_id="question.id" t-att-class="(slide.done &amp;&amp; answer.correct) ? 'o_wslides_quiz_answer o_wslides_quiz_good_answer': slide.done ? 'o_wslides_quiz_answer o_wslides_quiz_ignored_answer' : 'o_wslides_quiz_answer'">
<label class="o_wslides_quiz_radio_box">
<input t-att-answer_id="answer.id" type="radio" t-att-name="question.id" t-att-question_id="question.id" t-att-value="answer.id" t-att-checked="(slide.done &amp;&amp; answer.correct) ? 'checked' : undefined" t-att-disabled="slide.done"/>
<span t-if="!(slide.done &amp;&amp; answer.correct)"/>
<i t-if="(slide.done &amp;&amp; answer.correct)" class="fa fa-check-circle"></i>
</label>
<span t-esc="answer.text"/>
</li>
<t t-set="j" t-value="j+1"/>
</t>
</ul>
</div>
</div>
<t t-set="i" t-value="i+1"/>
</t>
</div>
</t>
<t t-name="website.course.player.infos">
<div class="text-box">
<h1><t t-esc="slide.name"/></h1>
<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.quiz.success">
<div class="o_wslides_quiz_modal_background">
</div>
<div class="o_wslides_quiz_success_modal">
<div class="o_wslides_quiz_success_modal_left_panel"></div>
<div class="o_wslides_quiz_success_modal_right_panel">
<div class="o_wslides_quiz_success_modal_close"><i class="fa fa-times"></i></div>
<h1>Amazing!</h1>
<div class="o_wslides_quiz_success_xp">
<p>You gained <span t-esc="data.points"/> !</p>
<div class="o_wslides_quiz_success_progress_bar">
<div class="o_wslides_quiz_success_progress_gauge"/>
</div>
<div class="o_wslides_quiz_success_progress_bounds">
<span>600</span>
<span>1000</span>
</div>
</div>
<div class="o_wslides_quiz_success_reward"></div>
<div class="o_wslides_quiz_success_button">
<a class="next-slide">Next &gt;</a>
</div>
</div>
</div>
</t>
<t t-name="website.course.fullscreen.title">
<t t-if="!miniQuiz">
<i t-if="slide.slide_type == 'document'" class="fa fa-file-pdf-o mr-2"></i>
<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_fullscreen_slide_title_span" t-esc="slide.name"/>
</t>
<t t-else="">
<span><i class="fa fa-flag-checkered"></i>Quiz: <span t-esc="slide.name"/></span>
</t>
</t>
</templates>
@@ -47,7 +47,7 @@
<input id="name" name="name" placeholder="Title" class="form-control" required="required"/>
</div>
</div>
<div class="form-group row">
<div t-if="!widget.defaultCategoryID" class="form-group row">
<label for="category_id" class="col-form-label col-md-3">Category</label>
<div class="controls col-md-9">
<input class="form-control" id="category_id"/>
@@ -155,4 +155,107 @@
</div>
</t>
<t t-name="website.slide.upload.quiz">
<div>
<form class="clearfix">
<canvas id="data_canvas" class="d-none"></canvas>
<t t-call="website.slide.upload.modal.common"/>
</form>
</div>
</t>
<t t-name="website.slide.image.upload">
<div role="dialog" class="modal" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content">
<header class="modal-header">
<h3 class="modal-title">Upload Image</h3>
<button type="button" class="close" data-dismiss="modal" aria-label="Close">×</button>
</header>
<div class="oe_slides_upload_loading text-center" style="display:none" aria-hidden="true" role="status">
<h4><i class='fa fa-spinner fa-spin'></i> Uploading Image... </h4>
</div>
<main class="modal-body">
<form class="clearfix">
<div class="form-group row">
<div class="col-md-4">
<div class="img-thumbnail">
<div class="o_slide_preview">
<img src="/website_slides/static/src/img/document.png" id="slide-image" title="Content Preview" alt="Content Preview" class="img-fluid"/>
</div>
</div>
</div>
<div class="col-md-8">
<ul class="list-group">
<li class="list-group-item">
<h5 class="list-group-item-heading">
<label for="upload" class="col-form-label">Image File</label>
</h5>
<input id="upload" name="file" class="form-control" accept="image/*,application/pdf" type="file"/>
</li>
</ul>
</div>
</div>
<canvas id="data_canvas" style="display: none;"></canvas>
</form>
</main>
<footer class="modal-footer">
<button type="button" data-loading-text="Loading..." data-published="true" class="btn btn-primary save">Upload</button>
<button type="button" class="btn btn-secondary" data-dismiss="modal">Discard</button>
</footer>
</div>
</div>
</div>
</t>
<t t-name="website.slide.delete.slide">
<div role="dialog" class="modal" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content">
<header class="modal-header">
<h3 class="modal-title">Delete Slide</h3>
<button type="button" class="close" data-dismiss="modal" aria-label="Close">×</button>
</header>
<main class="modal-body">
<div>
<p>Are you sure you want to remove this slide ? </p>
</div>
</main>
<footer class="modal-footer">
<button type="button" class="btn btn-primary delete" data-dismiss="modal">Delete</button>
<button type="button" class="btn btn-secondary" data-dismiss="modal">Cancel</button>
</footer>
</div>
</div>
</div>
</t>
<t t-name="website.slide.add.section">
<div role="dialog" class="modal" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content">
<header class="modal-header">
<h3 class="modal-title">Add a section</h3>
<button type="button" class="close" data-dismiss="modal" aria-label="Close">×</button>
</header>
<main class="modal-body modal-content">
<form class="clearfix">
<div class="form-group row">
<label for="section_name" class="col-form-label col-md-3">Section name</label>
<div class="col-md-9">
<input type="text" class="form-control" id="section_name"/>
</div>
</div>
<canvas id="data_canvas" style="display: none;"></canvas>
</form>
</main>
<footer class="modal-footer">
<button type="button" data-loading-text="Loading..." data-published="true" class="btn btn-primary save">Add</button>
<button type="button" class="btn btn-secondary" data-dismiss="modal">Discard</button>
</footer>
</div>
</div>
</div>
</t>
</templates>
+11
View File
@@ -6,6 +6,10 @@
<xpath expr="//link[last()]" position="after">
<link rel="stylesheet" type="text/scss" href="/website_slides/static/src/scss/website_slides.scss" t-ignore="true"/>
<link rel="stylesheet" type="text/scss" href="/website_slides/static/src/scss/website_slides_profile.scss"/>
<link rel="stylesheet" type="text/scss" href="/website_slides/static/src/scss/slide_course.scss" t-ignore="true"/>
<link rel="stylesheet" type="text/scss" href="/website_slides/static/src/scss/slide_slide.scss" t-ignore="true"/>
<link rel="stylesheet" type="text/scss" href="/website_slides/static/src/scss/slides_slide_fullscreen.scss" t-ignore="true"/>
<link rel="stylesheet" type="text/scss" href="/website_slides/static/src/scss/website_slides_quiz.scss"/>
</xpath>
<xpath expr="//script[last()]" position="after">
<script type="text/javascript" src="/website_slides/static/src/js/slides.js"/>
@@ -13,6 +17,13 @@
<script type="text/javascript" src="/website_slides/static/src/js/slides_like.js"/>
<script type="text/javascript" src="/website_slides/static/src/js/slides_share.js"/>
<script type="text/javascript" src="/website_slides/static/src/js/slides_upload.js"/>
<script type="text/javascript" src="/website_slides/static/src/js/slides_category.js"/>
<script type="text/javascript" src="/website_slides/static/src/js/slides_delete_slide.js"/>
<script type="text/javascript" src="/website_slides/static/src/js/slides_course_slides_list.js"/>
<script type="text/javascript" src="/website_slides/static/src/js/slides_course_progress_bar.js"/>
<script type="text/javascript" src="/website_slides/static/src/js/slides_course_sidebar_list.js"/>
<script type="text/javascript" src="/website_slides/static/src/js/slides_course_fullscreen_player.js"/>
<script type="text/javascript" src="/website_slides/static/src/js/slides_course_quiz.js"/>
</xpath>
</template>
@@ -135,7 +135,7 @@
</field>
</record>
<record id="view_slide_channel_tree" model="ir.ui.view">
<record id="slide_channel_view_tree" model="ir.ui.view">
<field name="name">slide.channel.tree</field>
<field name="model">slide.channel</field>
<field name="arch" type="xml">
@@ -154,7 +154,8 @@
<field name="name">slide.channel.search</field>
<field name="model">slide.channel</field>
<field name="arch" type="xml">
<search string="Channel">
<search string="Courses">
<field name="name" string="Course"/>
<filter string="Archived" name="inactive" domain="[('active','=',False)]"/>
</search>
</field>
@@ -165,6 +166,7 @@
<field name="res_model">slide.channel</field>
<field name="view_type">form</field>
<field name="view_mode">tree,form</field>
<field name="search_view_id" ref="website_slides.slide_channel_view_search"/>
<field name="help" type="html">
<p class="o_view_nocontent_smiling_face">
Create a channel
@@ -72,7 +72,7 @@
<group>
<field name="slide_type" readonly="1"/>
<field name="url"
attrs="{'required': [('slide_type', 'in', ('document', 'video'))], 'readonly': [('slide_type', 'in', ('document', 'video'))], 'invisible': [('slide_type', 'not in', ('document', 'presentation', 'video'))]}" />
attrs="{'required': [('slide_type', 'in', ('video'))], 'readonly': [('slide_type', 'in', ('document', 'video'))], 'invisible': [('slide_type', 'not in', ('document', 'presentation', 'video'))]}" />
<field name="document_id" readonly="1" attrs="{'invisible': [('document_id', '=', False)]}"/>
<field name="mime_type" readonly="1"
attrs="{'invisible': [('slide_type', 'not in', ('document', 'presentation', 'infographic', 'webpage'))]}"/>
@@ -108,6 +108,26 @@
</field>
</group>
</page>
<page string="Quiz">
<group name="container_row_2">
<group string="Earnings">
<group>
<field name="quiz_first_attempt_reward"/>
<field name="quiz_second_attempt_reward"/>
<field name="quiz_third_attempt_reward"/>
<field name="quiz_fourth_attempt_reward"/>
</group>
</group>
<group string="Questions">
<field name="question_ids" nolabel="1">
<tree>
<field name="sequence" widget="handle"/>
<field name="question"/>
</tree>
</field>
</group>
</group>
</page>
</notebook>
</sheet>
<div class="oe_chatter">
@@ -118,6 +138,29 @@
</field>
</record>
<record id="view_slide_question_form" model="ir.ui.view">
<field name="name">slide.question.form</field>
<field name="model">slide.question</field>
<field name="arch" type="xml">
<form string="Slide">
<sheet>
<div class="oe_edit_only">
<label for="question" string="Question Name"/>
</div>
<h1>
<field name="question" default_focus="1" placeholder="Name"/>
</h1>
<field name="answer_ids">
<tree editable="bottom" create="true" delete="true">
<field name="text_value"/>
<field name="is_correct"/>
</tree>
</field>
</sheet>
</form>
</field>
</record>
<record id="view_slide_slide_tree" model="ir.ui.view">
<field name="name">slide.slide.tree</field>
<field name="model">slide.slide</field>
@@ -169,6 +212,7 @@
<field name="res_model">slide.slide</field>
<field name="view_type">form</field>
<field name="view_mode">tree,form,graph</field>
<field name="context">{'search_default_channel': 1}</field>
<field name="help" type="html">
<p class="o_view_nocontent_smiling_face">
Add a new slide
@@ -108,8 +108,8 @@
<div class="row o_wslides_course_header">
<div class="col-lg-12 o_wslides_container o_wslides_course_header_container">
<div class="row align-items-end">
<div class="col o_wslides_course_header_aside">
<img class="mr-3" t-att-src="'/web/image/slide.channel/%s/image' % channel.id"/>
<div class="col o_wslides_course_header_aside main_image">
<div t-field="channel.image" widget="image" t-options="{'widget': 'image'}"/>
</div>
<div class="col-8 o_wslides_course_header_content">
<h3 class="row"><span class="font-weight-bold col" t-field="channel.name"/></h3>
@@ -254,8 +254,74 @@
</a>
</div>
</div>
<!-- training mode ==============================================-->
<div t-if="channel.channel_type == 'training'" style="width:70%;" class="d-flex align-items-center ml-4 justify-content-center">
<div class="course-content oe_js_course_slides_list" style="width: 100%">
<ul>
<t t-set="i" t-value="1"/>
<t t-set="j" t-value="0"/>
<t t-foreach="channel.category_ids" t-as="category">
<li t-attf-class="#{'section-draggable' if user == user_id else ''}" t-attf-category_id="#{category.id}">
<div t-attf-category_id="#{category.id}" class="course-section text-muted font-weight-bold pt-0 pb-0 pl-2 pr-2 d-flex justify-content-between align-items-center">
<div style="width:50%;" class="d-flex align-items-center">
<i t-if="channel.user_id == user" class="fa fa-arrows mr-3 text-muted"></i>
<div class="mr-2">Section <span class="section-index" t-esc="i"/>:</div>
<span t-field="category.name"/>
</div>
<!-- <a t-if="channel.user_id == user" title="Add content to this section" class="mr-2 p-0 oe_slide_js_add_slide" href="#" t-attf-channel_id="#{channel.id}" t-attf-category_id="#{category.id}">+</a> -->
<a class="mr-2 oe_slide_js_upload"
role="button"
aria-label="Upload Presentation"
href="#"
style="font-size: 1.5rem;text-decoration: none;"
t-att-data-channel-id="channel.id"
t-att-data-category-id="category.id"
t-att-data-can-upload="channel.can_upload"
t-att-data-can-publish="channel.can_publish">+</a>
<t t-set="i" t-value="i+1"/>
</div>
<ul t-attf-category_id="#{category.id}" >
<t t-foreach="category.slide_ids" t-as="slide">
<li t-att-index="j" t-attf-slide_id="#{slide.id}" t-attf-category_id="#{category.id}" t-attf-class="#{'content-slide slide-draggable d-flex justify-content-between align-items-center p-2' if channel.user_id == user else 'content-slide d-flex justify-content-between align-items-center p-2'}">
<div class="content-slide-infos ml-2">
<i t-if="channel.user_id == user" class="fa fa-arrows mr-2 text-muted"></i>
<i t-if="slide.slide_type == 'document'" class="fa fa-file-pdf-o mr-2 text-muted"></i>
<i t-if="slide.slide_type == 'infographic'" class="fa fa-file-picture-o mr-2 text-muted"></i>
<i t-if="slide.slide_type == 'video'" class="fa fa-play-circle mr-2 text-muted"></i>
<i t-if="slide.slide_type == 'link'" class="fa fa-file-code-o mr-2 text-muted"></i>
<i t-if="slide.slide_type == 'webpage'" class="fa fa-file-text mr-2 text-muted"></i>
<i t-if="slide.slide_type == 'quiz'" class="fa fa-question-circle mr-2 text-muted"></i>
<i t-if="slide.slide_type == 'certification'" class="fa fa-trophy mr-2 text-muted"></i>
<a class="link-to-slide" t-attf-href="/slides/slide/#{slug(slide)}"><span t-field="slide.name"/></a>
</div>
<div class="content-slide-controls mr-2">
<i t-if="not slide.id in user_progress or not user_progress[slide.id].completed" class="check-done fa fa-check-circle text-muted mr-1"></i>
<i t-if="slide.id in user_progress and user_progress[slide.id].completed" class="check-done text-primary fa fa-check-circle mr-1"></i>
<a t-if="channel.user_id == user and not slide.slide_type == 'webpage'" t-attf-href="/web#id=#{slide.id}&amp;action=#{slide_action}&amp;model=slide.slide&amp;view_type=form"><i class="fa fa-pencil text-muted mr-1"></i></a>
<a t-if="channel.user_id == user and slide.slide_type == 'webpage'" t-attf-href="/slides/slide/#{slug(slide)}?enable_editor=1"><i class="fa fa-pencil text-muted mr-1"></i></a>
<i t-if="channel.user_id == user" t-attf-slide_id="#{slide.id}" class="fa fa-trash text-muted oe_slide_js_delete_slide"></i>
</div>
</li>
<t t-set="j" t-value="j+1"/>
</t>
</ul>
</li>
</t>
</ul>
<div t-if="channel.user_id == user" class="content-actions">
<a class="mr-2 oe_slide_js_upload"
role="button"
aria-label="Upload Presentation"
href="#"
t-att-data-channel-id="channel.id"
t-att-data-can-upload="channel.can_upload"
t-att-data-can-publish="channel.can_publish">Add Content</a>
<a class="oe_slide_js_add_section" t-attf-channel_id="#{channel.id}" href="#">Add Section</a>
</div>
</div>
</div>
<!-- Channel content, aka slides (lessons in documentation mode) -->
<div class="col-8 mt24 ml32 o_wslides_channel_content_promoted">
<div t-if="channel.channel_type == 'documentation'" class="col-8 mt24 ml32 o_wslides_channel_content_promoted">
<div class="row mt16 align-items-center">
<div class="col">
<h3><i class="fa fa-plus-square"/> Featured lesson</h3>
@@ -284,7 +350,7 @@
</div>
</div>
<!-- Channel content, aka slides (lessons in documentation mode) -->
<div class="col-12">
<div t-if="channel.channel_type == 'documentation'" class="col-12">
<div class="tab-content mt16" id="slideChannelContent">
<div class="tab-pane active" role="tabpanel" aria-labelledby="profile-tab" id="slideChannelContentAbout">
<t t-foreach="category_data" t-as="category">
@@ -363,14 +429,14 @@
<i class="fa fa-thumbs-down fa-1x" role="img" aria-label="Dislikes" title="Dislikes"></i>
<span t-esc="slide.dislikes"/>
</span>
</div>
</div>
<small class="text-muted">
<t t-call="website_slides.slides_misc_float_time">
<t t-set="value" t-value="slide.completion_time"/>
</t>
<!-- <t t-esc="slide.total_views"/> Views
<timeago class="timeago" t-att-datetime="slide.create_date"></timeago> -->
</small>
</small>
</div>
</div>
</div>
@@ -386,14 +452,100 @@
<template id="slide_detail_view" name="Slide Detailed View">
<t t-call="website.layout">
<t t-set="main_object" t-value="slide"/>
<div class="container mt16">
<t t-call="website_slides.course_nav">
<t t-set="channel" t-value="slide.channel_id"/>
</t>
<div class="slide-header">
<div class="slide-header-container">
<div class="slide-header-box">
<a t-attf-href="/slides/#{slug(slide.channel_id)}" t-field="slide.channel_id.name"/>
<div t-if="slide.channel_id.channel_type == 'training'" class="d-flex align-items-end">
<div class="course-progress-bar">
<div class="progress-gauge m-0 oe_slide_js_progress_bar" t-attf-channel_completion="#{slide.channel_id.completion}"/>
</div>
<i class="fa fa-trophy ml-2 mb-0 p-0" style="font-size:1.8rem; color: #cdcdcd;"></i>
</div>
</div>
</div>
</div>
<div t-if="slide.channel_id.channel_type == 'training'" class="sidebar-list oe_js_side_bar_list slide-content-list">
<ul class="slide-list">
<li>
<div class="slide-content-list-header">
Course content
</div>
</li>
<t t-set="i" t-value="0"/>
<t t-foreach="slide.channel_id.category_ids" t-as="category">
<a data-toggle="collapse" t-attf-href="#collapse-#{category.id}" role="button" aria-expanded="false" t-attf-aria-controls="collapse-#{category.id}">
<li class="slide-content-list-section">
<span t-field="category.name"/>
</li>
</a>
<ul class="collapse p-0 m-0" t-attf-id="collapse-#{category.id}" >
<t t-foreach="category.slide_ids" t-as="course_slide">
<li class="slide-content-list-slide">
<a t-attf-href="/slides/slide/#{slug(course_slide)}" t-att-index="i" t-att-slide_id="course_slide.id">
<div t-attf-index="#{i}" t-attf-slide_id="#{course_slide.id}" t-attf-class="#{'active' if slide.id == course_slide.id else ''} d-flex justify-content-between">
<div>
<i t-if="course_slide.slide_type == 'presentation'" class="fa fa-file-pdf-o mr-2 text-muted"></i>
<i t-if="course_slide.slide_type == 'infographic'" class="fa fa-file-picture-o mr-2 text-muted"></i>
<i t-if="course_slide.slide_type == 'video'" class="fa fa-play-circle mr-2 text-muted"></i>
<i t-if="course_slide.slide_type == 'link'" class="fa fa-file-code-o mr-2 text-muted"></i>
<i t-if="course_slide.slide_type == 'webpage'" class="fa fa-file-text mr-2 text-muted"></i>
<i t-if="course_slide.slide_type == 'quiz'" class="fa fa-question-circle mr-2 text-muted"></i>
<t t-esc="course_slide.name"/>
</div>
<div class="content-slide-controls mr-2">
<i t-attf-id="check-#{course_slide.id}" t-if="course_slide.id in user_progress and not user_progress[course_slide.id].completed" class="check-done fa fa-check-circle text-muted mr-1"></i>
<i t-attf-id="check-#{course_slide.id}" t-if="course_slide.id in user_progress and user_progress[course_slide.id].completed" class="check-done text-primary fa fa-check-circle mr-1"></i>
</div>
</div>
</a>
<ul t-if="course_slide.link_ids" class="list-group slide-resources">
<t t-foreach="course_slide.link_ids" t-as="resource">
<a t-attf-href="#{resource.link}" target="new">
<li><div class="slide-resource"><i class="fa fa-link"></i><span t-field="resource.link"/></div></li>
</a>
</t>
</ul>
</li>
<t t-set="i" t-value="i+1"/>
</t>
</ul>
</t>
</ul>
</div>
<div class="slide-header-container d-flex justify-content-between mt-4 mb-3">
<div class="slide-title">
<h1 t-field="slide.name"/>
<span t-if="slide.question_ids" t-attf-class="slide-points ml-2 #{'quiz-points-won' if slide.id in user_progress and user_progress[slide.id].completed else ''}">
<span t-if="slide.id in user_progress and user_progress[slide.id].quiz_attempts_count == 0" id="quiz-points" t-esc="slide.quiz_first_attempt_reward"/>
<span t-if="slide.id in user_progress and user_progress[slide.id].quiz_attempts_count == 1" id="quiz-points" t-esc="slide.quiz_second_attempt_reward"/>
<span t-if="slide.id in user_progress and user_progress[slide.id].quiz_attempts_count == 2" id="quiz-points" t-esc="slide.quiz_third_attempt_reward"/>
<span t-if="slide.id in user_progress and user_progress[slide.id].quiz_attempts_count > 2" id="quiz-points" t-esc="slide.quiz_fourth_attempt_reward"/>
points
</span>
</div>
<div class="course-nav-links">
<t t-if="previous_slide">
<a class="slide-button" t-attf-href="/slides/slide/#{previous_slide}">Prev</a>
</t>
<a t-if="slide.slide_type in ('webpage', 'video', 'document', 'iconographic') and not slide.question_ids and (slide.id in user_progress and not user_progress[slide.id].completed)" t-attf-href="/slide/completed/#{slide.id}?next_slide=#{next_slide}&amp;course=#{slug(slide.channel_id)}" class="slide-button-done">Set Done</a>
<t t-if="next_slide">
<a class="slide-button" t-attf-href="/slides/slide/#{next_slide}">Next</a>
</t>
<a t-if="slide.channel_id.channel_type == 'training'" t-attf-href="/slides/slide/#{slug(slide)}?fullscreen=1" class="slide-button-fullscreen ml-2"><i class="fa fa-desktop mr-2"></i>fullscreen</a>
</div>
</div>
<div class="slide-header-container mt16">
<div class="row">
<div class="col-xl-8 col-lg-8 col-md-12 col-12">
<div class="o_w_slides_slide_detail_container">
<div style="width:100%;">
<div class="o_w_slides_slide_detail_container" style="width: 100%;">
<t t-if="slide.datas and slide.slide_type == 'infographic'">
<img t-attf-src="/web/image/slide.slide/#{slide.id}/datas" class="img-fluid" style="width:100%" alt="Slide image"/>
</t>
<div t-if="slide.slide_type in ('presentation', 'document')" class="embed-responsive embed-responsive-4by3 embed-responsive-item mb8">
<div style="height: 600px;" t-if="slide.slide_type in ('presentation', 'document')" class="embed-responsive embed-responsive-4by3 embed-responsive-item mb8">
<t t-raw="slide.embed_code"/>
</div>
<div t-if="slide.slide_type == 'video' and slide.document_id" class="embed-responsive embed-responsive-16by9 embed-responsive-item mb8">
@@ -402,8 +554,50 @@
<div t-if="slide.slide_type == 'webpage'" class="border border-light rounded">
<div t-field="slide.html_content"/>
</div>
<div t-if="slide.question_ids">
<div class=".o_wslides_fullscreen_quiz o_w_slides_quiz_no_fullscreen mt-2"
t-attf-slide_id="#{slide.id}"
t-attf-slide_done="#{slide.id in user_progress and user_progress[slide.id].completed}"
t-attf-nb_attempts="#{user_progress[slide.id].quiz_attempts_count if slide.id in user_progress else ''}"
t-attf-first_reward="#{slide.quiz_first_attempt_reward}"
t-attf-second_reward="#{slide.quiz_second_attempt_reward}"
t-attf-third_reward="#{slide.quiz_third_attempt_reward}"
t-attf-fourth_reward="#{slide.quiz_fourth_attempt_reward}"
t-if="slide.question_ids">
<div>
<t t-set="i" t-value="1"/>
<t t-foreach="slide.question_ids" t-as="question">
<t t-set="j" t-value="0"/>
<div class="o_wslides_quiz_question" t-attf-id="#{question.id}" t-attf-title="#{question.question}">
<div>
<div class="o_wslides_quiz_question_title">
<span class="o_wslides_quiz_question_title_number"><span t-esc="i"/>. </span>
<span class="o_wslides_quiz_question_title_text" t-esc="question.question"/>
</div>
<ul class="o_wslides_quiz_question_answers">
<t t-foreach="question.answer_ids" t-as="answer">
<li t-attf-id="answer#{answer.id}" t-att-question_id="question.id" t-attf-text_value="#{answer.text_value}" t-attf-class="#{'o_wslides_quiz_answer o_wslides_quiz_good_answer' if slide.id in user_progress and user_progress[slide.id].completed and answer.is_correct else 'o_wslides_quiz_answer o_wslides_quiz_ignored_answer' if slide.id in user_progress and user_progress[slide.id].completed else 'o_wslides_quiz_answer'}">
<label class="o_wslides_quiz_radio_box">
<input t-att-answer_id="answer.id" type="radio" t-att-name="question.id" t-att-question_id="question.id" t-att-value="answer.id" t-att-checked="'checked' if slide.id in user_progress and user_progress[slide.id].completed and answer.is_correct else False" t-att-disabled="slide.id in user_progress and user_progress[slide.id].completed"/>
<span t-if="not slide.id in user_progress or not (user_progress[slide.id].completed and answer.is_correct)"/>
<i t-if="slide.id in user_progress and user_progress[slide.id].completed and answer.is_correct" class="fa fa-check-circle"></i>
</label>
<span t-esc="answer.text_value"/>
</li>
<t t-set="j" t-value="j+1"/>
</t>
</ul>
</div>
</div>
<t t-set="i" t-value="i+1"/>
</t>
<button t-if="not slide.id in user_progress or not user_progress[slide.id].completed" class="btn btn-primary submit-quiz" >Check your answers</button>
<a t-if="next_slide" t-attf-style="#{'display: none' if not slide.id in user_progress or not user_progress[slide.id].completed else ''}" class="btn btn-primary next-slide" t-attf-href="/slides/slide/#{next_slide}">Continue</a>
</div>
</div>
</div>
<div class="row mt-3">
<h4 t-field="slide.name" class="col-lg-6"/>
<div class="col-lg-6">
<div clas="row">
<div class="col-lg-8" t-if="slide.tag_ids">
@@ -454,6 +648,21 @@
<span class="sr-only"><t t-esc="slide.likes"/> Likes</span>
</div>
</div>
<div class="mt8">
<t t-call="website_rating.rating_stars_static_popup_composer">
<t t-set="rating_avg" t-value="rating_avg"/>
<t t-set="rating_total" t-value="rating_count"/>
<t t-set="object" t-value="slide"/>
<t t-set="hash" t-value="message_post_hash"/>
<t t-set="pid" t-value="message_post_pid"/>
<t t-set="default_message_id" t-value="last_message_id"/>
<t t-set="default_message" t-value="last_message"/>
<t t-set="default_rating_value" t-value="last_rating_value"/>
<t t-set="force_submit_url" t-value="'/slides/mail/update_comment' if last_message_id else False"/>
</t>
</div>
<!-- LikeButton widget -->
<div t-if="slide.channel_id.channel_type == 'documentation' and slide.channel_id.allow_comment" class="text-muted mt4">
<div class="float-right mb16 text-right o_wslides_like">
@@ -494,6 +703,11 @@
<i class="fa fa-comments-o"></i> Comments
</a>
</li>
<li t-if="not slide.channel_id.allow_comment" class="nav-item">
<a href="#discuss" aria-controls="discuss" t-attf-class="nav-link#{comments and ' active' or ''}" role="tab" data-toggle="tab">
<i class="fa fa-star"></i> Ratings
</a>
</li>
<li class="nav-item">
<a href="#transcript" aria-controls="transcript" class="nav-link" role="tab" data-toggle="tab">
<i class="fa fa-align-justify"></i> Transcript
@@ -532,7 +746,7 @@
The social sharing module will be unlocked when a moderator will allow your publication.
</h4>
</div>
<div t-if="slide.channel_id.allow_comment" role="tabpanel" t-att-class="comments and 'tab-pane fade in show active' or 'tab-pane fade'" id="discuss">
<div role="tabpanel" t-att-class="comments and 'tab-pane fade in show active' or 'tab-pane fade'" id="discuss">
<t t-call="portal.message_thread">
<t t-set="object" t-value="slide"/>
<t t-set="hash" t-value="message_post_hash"/>
@@ -638,7 +852,7 @@
</div>
</div>
</div>
<div class="col-xl-4 col-lg-4 col-md-12 col-12">
<div t-if="slide.channel_id.channel_type == 'documentation'" class="col-xl-4 col-lg-4 col-md-12 col-12">
<ul class="nav nav-tabs" role="tablist">
<li class="nav-item"><a aria-controls="related" href="#related" class="nav-link active" data-toggle="tab">Related</a></li>
<li class="nav-item"><a aria-controls="most_viewed" href="#most_viewed" class="nav-link" data-toggle="tab">Most Viewed</a></li>
@@ -651,7 +865,7 @@
<t t-if="not related_slides_list">
No presentation available.
</t>
<t t-foreach="related_slides_list" t-as="slide">
<t t-else="" t-foreach="related_slides_list" t-as="related_slide">
<t t-call="website_slides.related_slides"/>
</t>
</ul>
@@ -664,7 +878,7 @@
<t t-if="not list(most_viewed_slides_list)">
No presentation available.
</t>
<t t-foreach="most_viewed_slides_list" t-as="slide">
<t t-else="" t-foreach="most_viewed_slides_list" t-as="related_slide">
<t t-call="website_slides.related_slides"/>
</t>
</ul>
@@ -677,16 +891,114 @@
</t>
</template>
<!-- Slide template for the fullscreen mode -->
<template id="slide_fullscreen" name="Fullscreen">
<t t-call="website.layout">
<div class="o_wslides">
<div class="o_wslides_slide_fullscreen_header">
<div>
<a id="fullscreen_sidebar_button" href="#" class="active o_wslides_fullscreen_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_fullscreen_slide_title"/>
</div>
<div>
<a t-attf-href="/slides/">back to courses</a>
</div>
</div>
<div class="o_wslides_fullscreen_container">
<div class="oe_js_side_bar_list o_wslides_fullscreen_sidebar">
<ul>
<li>
<div class="o_wslides_fullscreen_sidebar_header">
<a t-attf-href="/slides/#{slug(slide.channel_id)}">
<span t-field="slide.channel_id.name"/>
</a>
<div class="o_wslides_slide_fullscreen_progress_box">
<div class="o_wslides_fullscreen_sidebar_progressbar">
<div class="o_wslides_fullscreen_sidebar_progress_gauge oe_slide_js_progress_bar" t-attf-channel_completion="#{slide.channel_id.completion}"/>
</div>
<span><span class="o_wslides_progress_percentage" t-esc="slide.channel_id.completion"/>%</span>
</div>
</div>
</li>
<t t-set="i" t-value="0"/>
<t t-foreach="slide.channel_id.category_ids" t-as="category">
<div class="o_wslides_fullscreen_sidebar_section">
<a data-toggle="collapse" t-attf-href="#collapse-#{category.id}" role="button" aria-expanded="true" t-attf-aria-controls="collapse-#{category.id}">
<li class="o_wslides_fullscreen_sidebar_section_tab">
<span t-field="category.name"/>
</li>
</a>
<ul class="o_wslides_fullscreen_sidebar_section_slides collapsed p-0 m-0" t-attf-id="collapse-#{category.id}" >
<t t-foreach="category.slide_ids" t-as="course_slide">
<li t-att-index="i" t-att-slide_id="course_slide.id" t-attf-class="#{'active' if slide.id == course_slide.id else ''}">
<span class="o_wslides_fullscreen_slide_tab_line">
<span class="o_wslides_top_line"></span>
<i t-attf-id="check-#{course_slide.id}" t-if="not course_slide.id in user_progress or not user_progress[course_slide.id].completed" class="check-done fa fa-circle-thin"></i>
<i t-attf-id="check-#{course_slide.id}" t-if="course_slide.id in user_progress and user_progress[course_slide.id].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-slide_id="course_slide.id">
<div t-attf-index="#{i}"
t-attf-slide_slug="#{slug(course_slide)}"
t-attf-done="#{course_slide.id in user_progress and user_progress[course_slide.id].completed}"
t-attf-slide_id="#{course_slide.id}"
t-attf-slide_name="#{course_slide.name}"
t-attf-quiz="#{True if course_slide.question_ids else False}"
t-attf-slide_type="#{course_slide.slide_type}"
t-attf-slide_embed_code="#{course_slide.embed_code}"
t-attf-class="o_wslides_fullscreen_sidebar_slide_tab #{'active' if slide.id == course_slide.id else ''} d-flex justify-content-between">
<div>
<i t-if="course_slide.slide_type == 'document'" class="fa fa-file-pdf-o mr-2 text-muted"></i>
<i t-if="course_slide.slide_type == 'infographic'" class="fa fa-file-picture-o mr-2 text-muted"></i>
<i t-if="course_slide.slide_type == 'video'" class="fa fa-play-circle mr-2 text-muted"></i>
<i t-if="course_slide.slide_type == 'link'" class="fa fa-file-code-o mr-2 text-muted"></i>
<i t-if="course_slide.slide_type == 'webpage'" class="fa fa-file-text mr-2 text-muted"></i>
<i t-if="course_slide.slide_type == 'quiz'" class="fa fa-question-circle mr-2 text-muted"></i>
<i t-if="course_slide.slide_type == 'certification'" class="fa fa-trophy mr-2 text-muted"></i>
<t t-esc="course_slide.name"/>
</div>
</div>
</a>
</li>
<t t-if="course_slide.link_ids" t-foreach="course_slide.link_ids" t-as="link">
<li>
<span class="o_wslides_fullscreen_slide_tab_line">
<span class="o_wslides_full_line"/>
</span>
<a class="o_wslides_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 class="o_wslides_fullscreen_slide_tab_line">
<span class="o_wslides_full_line"/>
</span>
<span class="o_wslides_slide_quiz" t-att-index="i"><i class="fa fa-flag-checkered"></i>Quiz: <span t-esc="course_slide.name"/></span>
</li>
<t t-set="i" t-value="i+1"/>
</t>
</ul>
</div>
</t>
</ul>
</div>
<div class="oe_js_course_slide" t-attf-user_id="#{user.id}" t-attf-course_name="#{slide.channel_id.name}" t-attf-course_id="#{slide.channel_id.id}" t-attf-course_slug="#{slug(slide.channel_id)}" t-attf-slide_id="#{slide.id}"/>
<div class="o_wslides_fullscreen_player"/>
</div>
</div>
</t>
</template>
<!-- Slide sub-template: display an item in a list of related slides (Related, Most Viewed, ...) -->
<template id="related_slides" name="Related Slide">
<li class="media mt-3">
<a t-attf-href="/slides/slide/#{slug(slide)}" class="mr-3">
<img class="oe_slides_apart_small" t-attf-src="/web/image/slide.slide/#{slide.id}/image_small" alt="slide.name"/>
<a t-attf-href="/slides/slide/#{slug(related_slide)}" class="mr-3">
<img class="oe_slides_apart_small" t-attf-src="/web/image/slide.slide/#{related_slide.id}/image_small" alt="related_slide.name"/>
</a>
<div class="media-body">
<a t-attf-href="/slides/slide/#{slug(slide)}"><h6 t-esc="slide.name" class="mb-1"/></a>
<a t-attf-href="/slides/slide/#{slug(related_slide)}"><h6 t-esc="related_slide.name" class="mb-1"/></a>
<small class="text-muted">
<t t-esc="slide.total_views"/> Views . <timeago class="timeago" t-att-datetime="slide.create_date"></timeago>
<t t-esc="related_slide.total_views"/> Views . <timeago class="timeago" t-att-datetime="related_slide.create_date"></timeago>
</small>
</div>
</li>
@@ -9,14 +9,15 @@ class WebsiteSlidesForum(WebsiteSlides):
def _slide_channel_prepare_values(self, **kwargs):
communication_type = kwargs.get('communication_type')
channel = super(WebsiteSlidesForum, self)._slide_channel_prepare_values(**kwargs)
if communication_type:
if communication_type == 'forum':
forum = request.env['forum.forum'].create({
'name': kwargs.get('name')
})
kwargs['forum_id'] = forum.id
kwargs['allow_comment'] = communication_type == 'comment'
return super(WebsiteSlidesForum, self)._slide_channel_prepare_values(**kwargs)
channel['forum_id'] = forum.id
channel['allow_comment'] = communication_type == 'comment'
return channel
# Profile
# ---------------------------------------------------
@@ -1,14 +1,21 @@
<?xml version="1.0" ?>
<odoo><data>
<template id='course_main' inherit_id="website_slides.course_main">
<!-- Channel main template: add link to forum -->
<xpath expr="//li[hasclass('o_wslides_nav_tabs_item_home')]" position="after">
<li class="nav-item" t-if="channel.forum_id">
<a t-att-href="'/forum/%s' % (slug(channel.forum_id))"
t-att-class="'nav-link o_wslides_navlink'" target="new">Forum</a>
</li>
</xpath>
</template>
<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>
</template>
<template id='course_main' inherit_id="website_slides.course_main">
<!-- Channel main template: add link to forum -->
<xpath expr="//li[hasclass('o_wslides_nav_tabs_item_home')]" position="after">
<li class="nav-item" t-if="channel.forum_id">
<a t-att-href="'/forum/%s' % (slug(channel))"
t-att-class="'nav-link o_wslides_navlink'">Forum</a>
</li>
</xpath>
</template>
</data></odoo>
@@ -0,0 +1,15 @@
odoo.define('website_slides_survey.fullscreen', function (require) {
"use strict";
var core = require('web.core');
var _t = core._t;
var Fullscreen = require('website_slides.fullscreen');
Fullscreen.include({
xmlDependencies: (Fullscreen.prototype.xmlDependencies || []).concat(
["/website_slides_survey/static/src/xml/website_slides_fullscreen.xml"]
),
});
});
@@ -0,0 +1,12 @@
.o_wslides_fullscreen_certification {
background-color: #f6f6f6;
display: flex;
justify-content: center;
align-items: center;
width: 400px;
height: 300px;
position: absolute;
top: 50%;
left: 50%;
transform: translateX(-50%) translateY(-50%);
}
@@ -0,0 +1,16 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates>
<t t-extend="website.slides.fullscreen">
<t t-jquery=".o_wslides_fullscreen_content" t-operation="append">
<div class="o_wslides_fullscreen_certification" t-if="slide.slide_type == 'certification'">
<a t-att-href="'/slides/slide/' + slide.slug" target="new">Pass Certification</a>
</div>
</t>
</t>
<t t-extend="website.course.fullscreen.title">
<t t-jquery=".o_wslides_fullscreen_slide_title_span" t-operation="before">
<i t-if="slide.slide_type == 'certification'" class="fa fa-trophy mr-2 text"></i>
</t>
</t>
</templates>
@@ -2,10 +2,14 @@
<odoo>
<data>
<template id="assets_frontend" inherit_id="website.assets_frontend" name="Slides Certification">
<xpath expr="//link[last()]" position="after">
<link rel="stylesheet" type="text/scss" href="/website_slides_survey/static/src/scss/website_slides.scss" t-ignore="true"/>
</xpath>
<xpath expr="//script[last()]" position="after">
<script type="text/javascript" src="/website_slides_survey/static/src/js/slides_upload.js"/>
<script type="text/javascript" src="/website_slides_survey/static/src/js/slides_certification_download.js"/>
<link rel="stylesheet" type="text/scss" href="/website_slides_survey/static/src/scss/website_profile.scss"/>
<script type="text/javascript" src="/website_slides_survey/static/src/js/slides_course_fullscreen_player.js"/>
</xpath>
</template>
</data>