diff --git a/addons/website/views/website_navbar_templates.xml b/addons/website/views/website_navbar_templates.xml index 1e2c6fa7dfb..08ef7b54ab4 100644 --- a/addons/website/views/website_navbar_templates.xml +++ b/addons/website/views/website_navbar_templates.xml @@ -148,8 +148,8 @@
- -

New Slide Channel

+ +

New Course

diff --git a/addons/website_slides/controllers/main.py b/addons/website_slides/controllers/main.py index e8ebae00b7b..e5751893282 100644 --- a/addons/website_slides/controllers/main.py +++ b/addons/website_slides/controllers/main.py @@ -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//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/', 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 # -------------------------------------------------- diff --git a/addons/website_slides/data/website_data.xml b/addons/website_slides/data/website_data.xml index 0d2f0582deb..e2e8b1c4a78 100644 --- a/addons/website_slides/data/website_data.xml +++ b/addons/website_slides/data/website_data.xml @@ -6,7 +6,7 @@ - Presentations + Courses /slides 50 diff --git a/addons/website_slides/models/__init__.py b/addons/website_slides/models/__init__.py index 4b8210c6caa..0fcfe6373ab 100644 --- a/addons/website_slides/models/__init__.py +++ b/addons/website_slides/models/__init__.py @@ -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 diff --git a/addons/website_slides/models/slide_channel.py b/addons/website_slides/models/slide_channel.py index dd772a8ec7b..6bbd0e3b297 100644 --- a/addons/website_slides/models/slide_channel.py +++ b/addons/website_slides/models/slide_channel.py @@ -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 diff --git a/addons/website_slides/models/slide_question.py b/addons/website_slides/models/slide_question.py new file mode 100644 index 00000000000..0193558ba46 --- /dev/null +++ b/addons/website_slides/models/slide_question.py @@ -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) diff --git a/addons/website_slides/models/slide_slide.py b/addons/website_slides/models/slide_slide.py index 35b9fefc4e5..a71446b82fa 100644 --- a/addons/website_slides/models/slide_slide.py +++ b/addons/website_slides/models/slide_slide.py @@ -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).") diff --git a/addons/website_slides/security/ir.model.access.csv b/addons/website_slides/security/ir.model.access.csv index 9dbfa916ff6..ce9bcf2593d 100644 --- a/addons/website_slides/security/ir.model.access.csv +++ b/addons/website_slides/security/ir.model.access.csv @@ -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 diff --git a/addons/website_slides/static/src/js/slides_category.js b/addons/website_slides/static/src/js/slides_category.js new file mode 100644 index 00000000000..d1d7efb7747 --- /dev/null +++ b/addons/website_slides/static/src/js/slides_category.js @@ -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')); + }, + }); + }); \ No newline at end of file diff --git a/addons/website_slides/static/src/js/slides_course_fullscreen_player.js b/addons/website_slides/static/src/js/slides_course_fullscreen_player.js new file mode 100644 index 00000000000..1da173a2f46 --- /dev/null +++ b/addons/website_slides/static/src/js/slides_course_fullscreen_player.js @@ -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($('')) + 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; +}); \ No newline at end of file diff --git a/addons/website_slides/static/src/js/slides_course_progress_bar.js b/addons/website_slides/static/src/js/slides_course_progress_bar.js new file mode 100644 index 00000000000..2412368018f --- /dev/null +++ b/addons/website_slides/static/src/js/slides_course_progress_bar.js @@ -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; + }); diff --git a/addons/website_slides/static/src/js/slides_course_quiz.js b/addons/website_slides/static/src/js/slides_course_quiz.js new file mode 100644 index 00000000000..f98d5b3ed8a --- /dev/null +++ b/addons/website_slides/static/src/js/slides_course_quiz.js @@ -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($('')); + 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($('')); + $('#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($('')); + 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($('

All questions must be answered !

')); + } + }, +}); + + 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; +}); \ No newline at end of file diff --git a/addons/website_slides/static/src/js/slides_course_sidebar_list.js b/addons/website_slides/static/src/js/slides_course_sidebar_list.js new file mode 100644 index 00000000000..11b61d4e5ec --- /dev/null +++ b/addons/website_slides/static/src/js/slides_course_sidebar_list.js @@ -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"); + } + }); +}); \ No newline at end of file diff --git a/addons/website_slides/static/src/js/slides_course_slides_list.js b/addons/website_slides/static/src/js/slides_course_slides_list.js new file mode 100644 index 00000000000..bf634f542e3 --- /dev/null +++ b/addons/website_slides/static/src/js/slides_course_slides_list.js @@ -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"); + } + }); +}); \ No newline at end of file diff --git a/addons/website_slides/static/src/js/slides_delete_slide.js b/addons/website_slides/static/src/js/slides_delete_slide.js new file mode 100644 index 00000000000..e3eef38502d --- /dev/null +++ b/addons/website_slides/static/src/js/slides_delete_slide.js @@ -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; + }); diff --git a/addons/website_slides/static/src/js/slides_upload.js b/addons/website_slides/static/src/js/slides_upload.js index 855b7e52932..7b0a2964bf8 100644 --- a/addons/website_slides/static/src/js/slides_upload.js +++ b/addons/website_slides/static/src/js/slides_upload.js @@ -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+']'); + $('').appendTo(categoryElement) + self._reorderSlidesSequence(); + } window.location = data.url; } }); diff --git a/addons/website_slides/static/src/js/website_slides.editor.js b/addons/website_slides/static/src/js/website_slides.editor.js index 40513e785e2..72a18bb93ee 100644 --- a/addons/website_slides/static/src/js/website_slides.editor.js +++ b/addons/website_slides/static/src/js/website_slides.editor.js @@ -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; diff --git a/addons/website_slides/static/src/scss/slide_course.scss b/addons/website_slides/static/src/scss/slide_course.scss new file mode 100644 index 00000000000..38179498415 --- /dev/null +++ b/addons/website_slides/static/src/scss/slide_course.scss @@ -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%; +} + + + + diff --git a/addons/website_slides/static/src/scss/slide_slide.scss b/addons/website_slides/static/src/scss/slide_slide.scss new file mode 100644 index 00000000000..d99aaea101a --- /dev/null +++ b/addons/website_slides/static/src/scss/slide_slide.scss @@ -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; +} \ No newline at end of file diff --git a/addons/website_slides/static/src/scss/slides_slide_fullscreen.scss b/addons/website_slides/static/src/scss/slides_slide_fullscreen.scss new file mode 100644 index 00000000000..de5493d2f7e --- /dev/null +++ b/addons/website_slides/static/src/scss/slides_slide_fullscreen.scss @@ -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%; + } + + +} \ No newline at end of file diff --git a/addons/website_slides/static/src/scss/website_slides_quiz.scss b/addons/website_slides/static/src/scss/website_slides_quiz.scss new file mode 100644 index 00000000000..9fe4438ee57 --- /dev/null +++ b/addons/website_slides/static/src/scss/website_slides_quiz.scss @@ -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; + } + } + } \ No newline at end of file diff --git a/addons/website_slides/static/src/xml/website_slides_fullscreen.xml b/addons/website_slides/static/src/xml/website_slides_fullscreen.xml new file mode 100644 index 00000000000..a9aff0d2ec7 --- /dev/null +++ b/addons/website_slides/static/src/xml/website_slides_fullscreen.xml @@ -0,0 +1,111 @@ + + +
+ +
+
+ +
+
+ Slide image +
+
+
+ +
+ + + +
+
+
+
+
+
+
+ + + +
+ + + +
+
+
+ . + +
+
    + +
  • + + +
  • + + +
+
+
+ + +
+
+ + + +
+ + +
+
+ + +
+
+
+
+
+
+

Amazing!

+
+

You gained !

+
+
+
+
+ 600 + 1000 +
+
+
+
+ Next > +
+
+
+ + + + + + + + + + + + + + Quiz: + + + + diff --git a/addons/website_slides/static/src/xml/website_slides_upload.xml b/addons/website_slides/static/src/xml/website_slides_upload.xml index 966ff3362cd..d57923c561b 100644 --- a/addons/website_slides/static/src/xml/website_slides_upload.xml +++ b/addons/website_slides/static/src/xml/website_slides_upload.xml @@ -47,7 +47,7 @@
-
+
@@ -155,4 +155,107 @@
+ +
+
+ + + +
+
+ + + + + + + + + + + + + diff --git a/addons/website_slides/views/assets.xml b/addons/website_slides/views/assets.xml index 3602f06aab1..a43729f527d 100644 --- a/addons/website_slides/views/assets.xml +++ b/addons/website_slides/views/assets.xml @@ -6,6 +6,10 @@ + + + +