diff --git a/addons/website/views/website_navbar_templates.xml b/addons/website/views/website_navbar_templates.xml index 1e2c6fa7dfbc5f3ee7b75412241ff9349133646a..08ef7b54ab46ba5636953b7d98ad6501f9a413eb 100644 --- a/addons/website/views/website_navbar_templates.xml +++ b/addons/website/views/website_navbar_templates.xml @@ -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"> diff --git a/addons/website_slides/controllers/main.py b/addons/website_slides/controllers/main.py index e8ebae00b7be9580487ec71d8ad031da3ccbc62d..e5751893282895e189b863544cc67c1818d93931 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/<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 # -------------------------------------------------- diff --git a/addons/website_slides/data/website_data.xml b/addons/website_slides/data/website_data.xml index 0d2f0582debcf124828d0797fc2858406c39043e..e2e8b1c4a78c2c799b6b259d8d9d4eb10fe2d4be 100644 --- a/addons/website_slides/data/website_data.xml +++ b/addons/website_slides/data/website_data.xml @@ -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> diff --git a/addons/website_slides/models/__init__.py b/addons/website_slides/models/__init__.py index 4b8210c6caaf94964c6391acc6422403d48f4092..0fcfe6373abf302d0d6cbd7bfe9f5c0b7bd9d0bb 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 dd772a8ec7b8b15220cfae7e4abef3dc0b7e30f0..6bbd0e3b297790fd6c51362f709cad42fc7928b2 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 0000000000000000000000000000000000000000..0193558ba463e197474e8429d9b4d1178f25a8ac --- /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 35b9fefc4e57a6dd93e03279f05cea83dcdf1d08..a71446b82faee68da39ab2a6b2617d3cafa36a47 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 9dbfa916ff6dbe8143e768d1b6923561aba4a955..ce9bcf2593d3b331fc61965362a092c682f4f479 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 0000000000000000000000000000000000000000..d1d7efb77477d3d278c70607da50b97c3edef081 --- /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 0000000000000000000000000000000000000000..1da173a2f4655e63e24e255d43333c85bb378493 --- /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($('<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; +}); \ 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 0000000000000000000000000000000000000000..2412368018f3d740d1b56f12c43cd4d5e0d86e69 --- /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 0000000000000000000000000000000000000000..f98d5b3ed8abda5853d99c3e69b8fe1520d1038a --- /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($('<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; +}); \ 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 0000000000000000000000000000000000000000..11b61d4e5ec6ea0855d34c6c712b5adf2ec698f0 --- /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 0000000000000000000000000000000000000000..bf634f542e3bd3cbffd58a935de3425d2cf0b727 --- /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 0000000000000000000000000000000000000000..e3eef38502dd7d9178345f2d6ea94b90bce76991 --- /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 855b7e5293207b9961d05aef06e4c3561d59d1f4..7b0a2964bf8ad7ec8caacba61e941ff598c27fc2 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+']'); + $('<li hidden class="content-slide" slide_id="'+data.slide_id+'" category_id="'+self.categoryID+'">temp</li>').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 40513e785e2e85aa38bfa7ed9da790f1e8bccda7..72a18bb93ee9204b488fd553ae905345e313201b 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 0000000000000000000000000000000000000000..38179498415a662fc984a55a58c188436509c0b3 --- /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 0000000000000000000000000000000000000000..d99aaea101aaf87e33882afa465c0bcf5e64dc05 --- /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 0000000000000000000000000000000000000000..de5493d2f7e418e68f8632616a5db8334733800a --- /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 0000000000000000000000000000000000000000..9fe4438ee57673d2a6a18a7f96a82f9e731aaf6d --- /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 0000000000000000000000000000000000000000..a9aff0d2ec7b73bfeb98e6a962f9dfff4d03ed54 --- /dev/null +++ b/addons/website_slides/static/src/xml/website_slides_fullscreen.xml @@ -0,0 +1,111 @@ +<templates id="template" xml:space="preserve"> + <t t-name="website.slides.fullscreen"> + <div t-if="slide.slide_type == 'video' && !showMiniQuiz" class="player embed-responsive embed-responsive-16by9 embed-responsive-item"> + <iframe id="youtube-player" t-att-src="embed_url" allowFullScreen="true" frameborder="0" enablejsapi="1" autoplay="1" allow="autoplay"></iframe> + </div> + <div t-if="slide.slide_type in ['presentation', 'document'] && !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' && !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) && 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 && 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 && 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 && answer.correct) ? 'checked' : undefined" t-att-disabled="slide.done"/> + <span t-if="!(slide.done && answer.correct)"/> + <i t-if="(slide.done && 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 ></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> 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 966ff3362cd59b2cddff4e269f70f5ad3c6a7ab6..d57923c561b96b51fa02bca1854509c617c78195 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 @@ <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> diff --git a/addons/website_slides/views/assets.xml b/addons/website_slides/views/assets.xml index 3602f06aab18cacb2d957ddc412a527bf2776d71..a43729f527dbae69fbed0e228225a12aaf452e74 100644 --- a/addons/website_slides/views/assets.xml +++ b/addons/website_slides/views/assets.xml @@ -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> diff --git a/addons/website_slides/views/slide_channel_views.xml b/addons/website_slides/views/slide_channel_views.xml index 91607fbace5b011660b96d951b673c5d3630e2df..4e083dc26505588e6b43f07865b40632d3bccbc2 100644 --- a/addons/website_slides/views/slide_channel_views.xml +++ b/addons/website_slides/views/slide_channel_views.xml @@ -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 diff --git a/addons/website_slides/views/slide_slide_views.xml b/addons/website_slides/views/slide_slide_views.xml index 7e7fcdf92e4f871085a5c8e219f5ec423933bd6e..444207483c8db9476c00d0615e64bad754f348ae 100644 --- a/addons/website_slides/views/slide_slide_views.xml +++ b/addons/website_slides/views/slide_slide_views.xml @@ -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 diff --git a/addons/website_slides/views/website_slides_templates.xml b/addons/website_slides/views/website_slides_templates.xml index e06e75d0b669f61a7c0c6d4c3996ca0a01146783..7bcf5bd77b7e536fc7a47bd8b310ddaed0ae728b 100644 --- a/addons/website_slides/views/website_slides_templates.xml +++ b/addons/website_slides/views/website_slides_templates.xml @@ -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}&action=#{slide_action}&model=slide.slide&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}&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> diff --git a/addons/website_slides_forum/controllers/main.py b/addons/website_slides_forum/controllers/main.py index 6f898b90fc44390a51d9166f9d421d3bf157358e..052df5f4de2c66dc5aa1e61accec02606b1fa820 100644 --- a/addons/website_slides_forum/controllers/main.py +++ b/addons/website_slides_forum/controllers/main.py @@ -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 # --------------------------------------------------- diff --git a/addons/website_slides_forum/views/website_slides_templates.xml b/addons/website_slides_forum/views/website_slides_templates.xml index 339f4d526d794d8096c3826895888f475d74cfe9..0acb40bc24145f571cefd547cd83f0faaea34059 100644 --- a/addons/website_slides_forum/views/website_slides_templates.xml +++ b/addons/website_slides_forum/views/website_slides_templates.xml @@ -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> diff --git a/addons/website_slides_survey/static/src/js/slides_course_fullscreen_player.js b/addons/website_slides_survey/static/src/js/slides_course_fullscreen_player.js new file mode 100644 index 0000000000000000000000000000000000000000..9fc8ffe3407d8e173130d466f68d8525f591ddb8 --- /dev/null +++ b/addons/website_slides_survey/static/src/js/slides_course_fullscreen_player.js @@ -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"] + ), + }); + + }); + \ No newline at end of file diff --git a/addons/website_slides_survey/static/src/scss/website_slides.scss b/addons/website_slides_survey/static/src/scss/website_slides.scss new file mode 100644 index 0000000000000000000000000000000000000000..fd1506e345b2c4a2d9412fb62feb4bc80a6dbfad --- /dev/null +++ b/addons/website_slides_survey/static/src/scss/website_slides.scss @@ -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%); +} \ No newline at end of file diff --git a/addons/website_slides_survey/static/src/xml/website_slides_fullscreen.xml b/addons/website_slides_survey/static/src/xml/website_slides_fullscreen.xml new file mode 100644 index 0000000000000000000000000000000000000000..f6171db9a1388f04d1726ebc3c74157e2e25a9b1 --- /dev/null +++ b/addons/website_slides_survey/static/src/xml/website_slides_fullscreen.xml @@ -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> diff --git a/addons/website_slides_survey/views/assets.xml b/addons/website_slides_survey/views/assets.xml index 2ad6ba7f43aa4c44f112ea0988eae880a5c4593d..7f239d7a1c97534b98656acc3ead85c2d967c055 100644 --- a/addons/website_slides_survey/views/assets.xml +++ b/addons/website_slides_survey/views/assets.xml @@ -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>