From c564ae6c87a79832d94de99d650d437fcf68b1c1 Mon Sep 17 00:00:00 2001 From: qmo-odoo Date: Wed, 21 Nov 2018 09:06:40 +0000 Subject: [PATCH] [IMP] website_slides: Add elearning feature to slides Purpose of this commit is to introduce a new way of displaying and managing slides in eLearning module. Its purpose is to give a better experience to users when going through a course. Notably a fullscreen mode allows to take lessons one by one without going out of the elearning display. Integration of all slide types eases taking the course step by step. Quiz are introduced in this commit. Those are an addition to slides with some question / answers (multiple choice). It allows customer to gain karma and improves gamification. Certifications are still done using the survey application. Quiz targets only small tests at the end of a given slide. A new widget is added to display and control slide display in fullscreen mode. Old display is still available for documentation channels or when going out of fullscreen mode, to see details on comments / review / statistics and have access to share options. This commit is linked to task ID 1902304 and PR #29876. It closes the main work on refactoring website slides into eLearning, with other tasks already merged in current community [1][2][3][4][5]. [1] Task ID 1940360 landed at b2149bc7a274217a0b96e587abd52093bd98e5e8: certification inclusion [2] Task ID 1936153 landed at 6c6179e57cede3d9889c33873740a7b1ddecda2e: homepage inclusion [3] Task ID 1937160 landed at 8b7605c9e5df4a700b0139d1781db24ae5d31626: selling courses [4] Task ID 1922159 and 1940516 landed at 5fa651cb013ac67d7ba592498c4cf2770dec2483 and 6a64c3ce821f080969e49e5e760e52ae9bfc26cc: new user profile and gamification of slides / forum [5] Task ID 1938643 landed at 77b5673a0dd220b3c28e88f8acbb5274b486c5bd: upload channel/slide --- .../views/website_navbar_templates.xml | 4 +- addons/website_slides/controllers/main.py | 168 +++++- addons/website_slides/data/website_data.xml | 2 +- addons/website_slides/models/__init__.py | 1 + addons/website_slides/models/slide_channel.py | 9 +- .../website_slides/models/slide_question.py | 51 ++ addons/website_slides/models/slide_slide.py | 22 +- .../security/ir.model.access.csv | 4 + .../static/src/js/slides_category.js | 148 +++++ .../src/js/slides_course_fullscreen_player.js | 427 ++++++++++++++ .../src/js/slides_course_progress_bar.js | 47 ++ .../static/src/js/slides_course_quiz.js | 206 +++++++ .../src/js/slides_course_sidebar_list.js | 34 ++ .../src/js/slides_course_slides_list.js | 251 ++++++++ .../static/src/js/slides_delete_slide.js | 88 +++ .../static/src/js/slides_upload.js | 51 +- .../static/src/js/website_slides.editor.js | 1 + .../static/src/scss/slide_course.scss | 536 ++++++++++++++++++ .../static/src/scss/slide_slide.scss | 156 +++++ .../src/scss/slides_slide_fullscreen.scss | 303 ++++++++++ .../static/src/scss/website_slides_quiz.scss | 237 ++++++++ .../src/xml/website_slides_fullscreen.xml | 111 ++++ .../static/src/xml/website_slides_upload.xml | 105 +++- addons/website_slides/views/assets.xml | 11 + .../views/slide_channel_views.xml | 6 +- .../views/slide_slide_views.xml | 46 +- .../views/website_slides_templates.xml | 350 +++++++++++- .../website_slides_forum/controllers/main.py | 7 +- .../views/website_slides_templates.xml | 25 +- .../src/js/slides_course_fullscreen_player.js | 15 + .../static/src/scss/website_slides.scss | 12 + .../src/xml/website_slides_fullscreen.xml | 16 + addons/website_slides_survey/views/assets.xml | 4 + 33 files changed, 3399 insertions(+), 55 deletions(-) create mode 100644 addons/website_slides/models/slide_question.py create mode 100644 addons/website_slides/static/src/js/slides_category.js create mode 100644 addons/website_slides/static/src/js/slides_course_fullscreen_player.js create mode 100644 addons/website_slides/static/src/js/slides_course_progress_bar.js create mode 100644 addons/website_slides/static/src/js/slides_course_quiz.js create mode 100644 addons/website_slides/static/src/js/slides_course_sidebar_list.js create mode 100644 addons/website_slides/static/src/js/slides_course_slides_list.js create mode 100644 addons/website_slides/static/src/js/slides_delete_slide.js create mode 100644 addons/website_slides/static/src/scss/slide_course.scss create mode 100644 addons/website_slides/static/src/scss/slide_slide.scss create mode 100644 addons/website_slides/static/src/scss/slides_slide_fullscreen.scss create mode 100644 addons/website_slides/static/src/scss/website_slides_quiz.scss create mode 100644 addons/website_slides/static/src/xml/website_slides_fullscreen.xml create mode 100644 addons/website_slides_survey/static/src/js/slides_course_fullscreen_player.js create mode 100644 addons/website_slides_survey/static/src/scss/website_slides.scss create mode 100644 addons/website_slides_survey/static/src/xml/website_slides_fullscreen.xml diff --git a/addons/website/views/website_navbar_templates.xml b/addons/website/views/website_navbar_templates.xml index 1e2c6fa7dfb..08ef7b54ab4 100644 --- a/addons/website/views/website_navbar_templates.xml +++ b/addons/website/views/website_navbar_templates.xml @@ -148,8 +148,8 @@
- -

New Slide Channel

+ +

New Course

diff --git a/addons/website_slides/controllers/main.py b/addons/website_slides/controllers/main.py index e8ebae00b7b..e5751893282 100644 --- a/addons/website_slides/controllers/main.py +++ b/addons/website_slides/controllers/main.py @@ -50,13 +50,34 @@ class WebsiteSlides(WebsiteProfile): def _get_slide_detail(self, slide): most_viewed_slides = slide.get_most_viewed_slides(self._slides_per_list) related_slides = slide.get_related_slides(self._slides_per_list) - return { + values = { 'slide': slide, 'most_viewed_slides': most_viewed_slides, 'related_slides': related_slides, 'user': request.env.user, 'is_public_user': request.website.is_public_user(), 'comments': slide.website_message_ids or [], + 'user_progress': {} + } + if slide.channel_id.channel_type == "training": + channel_slides = slide.channel_id.slide_ids.ids + slide_index = channel_slides.index(slide.id) + previous_slide = None + next_slide = None + if slide_index > 0: + previous_slide = slide.channel_id.slide_ids[slide_index-1] + if slide_index < len(channel_slides) - 1: + next_slide = slide.channel_id.slide_ids[slide_index+1] + values.update({ + 'previous_slide': slug(previous_slide) if previous_slide else "", + 'next_slide': slug(next_slide) if next_slide else "" + }) + return values + + def _get_user_progress(self, channel): + user_progress = { slide_partner.slide_id.id: slide_partner for slide_partner in request.env['slide.slide.partner'].sudo().search([('channel_id', '=', channel.id),('partner_id', '=', request.env.user.partner_id.id)])} + return { + 'user_progress': user_progress } def _extract_channel_tag_search(self, **post): @@ -198,7 +219,6 @@ class WebsiteSlides(WebsiteProfile): def channel(self, channel, category=None, tag=None, page=1, slide_type=None, sorting=None, search=None, **kw): if not channel.can_access_from_current_website(): raise werkzeug.exceptions.NotFound() - domain = [('channel_id', '=', channel.id)] if not request.env.user.has_group('website.group_website_publisher'): domain = expression.AND([ @@ -299,6 +319,9 @@ class WebsiteSlides(WebsiteProfile): values = self._prepare_additional_channel_values(values, **kw) + if channel.channel_type == "training": + values.update(self._get_user_progress(channel)) + return request.render('website_slides.course_main', values) @http.route(['/slides/channel/add'], type='http', auth='user', methods=['POST'], website=True) @@ -339,7 +362,13 @@ class WebsiteSlides(WebsiteProfile): 'message_post_hash': slide._generate_signed_token(request.env.user.partner_id.id), 'message_post_pid': request.env.user.partner_id.id, }) - return request.render('website_slides.slide_detail_view', values) + self._set_viewed_slide(slide) + if slide.channel_id.channel_type == "training": + values.update(self._get_user_progress(slide.channel_id)) + if 'fullscreen' in kwargs: + return request.render("website_slides.slide_fullscreen", values) + return request.render("website_slides.slide_detail_view", values) + @http.route('''/slides/slide//pdf_content''', type='http', auth="public", website=True, sitemap=False) @@ -394,6 +423,94 @@ class WebsiteSlides(WebsiteProfile): response.status_code = status return response + @http.route('/slide/html_content/get', type="json", auth="public", website=True) + def get_html_content(self, slide_id): + slide = request.env['slide.slide'].browse(slide_id) + return { + 'html_content': slide.html_content + } + + #SLIDE QUIZ CONTROLLERS + + @http.route('/slide/quiz/get', type="json", auth="public", website=True) + def get_quiz(self, **kw): + if 'slide_id' in kw: + slide = request.env['slide.slide'].browse(kw['slide_id']) + slide_partner = request.env['slide.slide.partner'].search([('slide_id', '=', slide.id), ('partner_id', '=', request.env.user.partner_id.id)]) + possible_points = [slide.quiz_first_attempt_reward,slide.quiz_second_attempt_reward,slide.quiz_third_attempt_reward, slide.quiz_fourth_attempt_reward] + points = 0 + if slide_partner.quiz_attempts_count < len(possible_points): + points = possible_points[slide_partner.quiz_attempts_count] + else: + points = possible_points[len(possible_points)-1] + res = { + 'questions':[ + {'title': question.question, + 'id': question.id, + 'answers': [{'text': answer.text_value, 'correct':answer.is_correct,'id': answer.id} for answer in question.answer_ids] + } for question in slide.question_ids + ], + 'nb_attempts': slide_partner.quiz_attempts_count if slide_partner else 0, + 'possible_rewards': possible_points, + 'reward': points + } + return res + + @http.route('/slide/quiz/submit', type="json", auth="user", website=True) + def submit_quiz(self, slide_id, answer_ids,**kw): + slide = request.env['slide.slide'].browse(slide_id) + good_answers = request.env['slide.answer'].search([('id', 'in', answer_ids), ('is_correct', '=', True)]) + bad_answers = request.env['slide.answer'].browse(answer_ids) - good_answers + slide_partner = request.env['slide.slide.partner'].search([('slide_id', '=', slide_id), ('partner_id', '=', request.env.user.partner_id.id)]) + possible_points = [slide.quiz_first_attempt_reward, slide.quiz_second_attempt_reward, slide.quiz_third_attempt_reward, slide.quiz_fourth_attempt_reward] + points = 0 + if not slide_partner: + slide_partner = request.env['slide.slide.partner'].sudo().create({ + "slide_id": slide.id, + "partner_id": request.env.user.partner_id.id, + "channel_id": slide.channel_id.id, + "completed": False + }) + if not slide_partner.completed: + if slide_partner.quiz_attempts_count < len(possible_points): + points = possible_points[slide_partner.quiz_attempts_count] + else: + points = possible_points[-1] + slide_partner.sudo().write({ + 'quiz_attempts_count': slide_partner.quiz_attempts_count if not bad_answers else slide_partner.quiz_attempts_count + 1, + 'points_won': points if not bad_answers else 0, + 'completed': not bad_answers + }) + return { + 'goodAnswers': [good_answer.id for good_answer in good_answers], + 'badAnswers': [bad_answer.id for bad_answer in bad_answers], + 'passed': not bad_answers, + 'points': points if not bad_answers else 0, + 'attempts_count': slide_partner.quiz_attempts_count if slide_partner else 0, + 'channel_completion': slide.channel_id.completion + } + return { + 'error': "You already passed this quiz" + } + + #SLIDE STATE CONTROLLERS + + @http.route('/slide/completed/', website=True, type="http", auth="user") + def mark_as_completed(self, slide_id, next_slide=None, **kw): + slide = request.env['slide.slide'].browse(slide_id) + slide.action_set_completed() + return werkzeug.utils.redirect("/slides/slide/%s" %(next_slide)) + + + @http.route('/slides/set_completed', website=True, type="json", auth="user") + def set_status_as_done(self, slide_id, **kw): + slide = request.env['slide.slide'].browse(slide_id) + slide.channel_id.invalidate_cache() + slide.action_set_completed() + return { + 'channel_completion': slide.channel_id.completion + } + # JSONRPC @http.route('/slides/slide/like', type='json', auth="user", website=True) def slide_like(self, slide_id, upvote): @@ -423,6 +540,13 @@ class WebsiteSlides(WebsiteProfile): # TOOLS # -------------------------------------------------- + @http.route(['/slides/channel/enroll'], type='http', auth='public', website=True) + def slide_channel_join_http(self, channel_id): + if not request.website.is_public_user(): + channel = request.env['slide.channel'].browse(int(channel_id)) + channel.action_add_member() + return werkzeug.utils.redirect("/slides/%s" % (slug(channel))) + @http.route(['/slides/channel/join'], type='json', auth='public', website=True) def slide_channel_join(self, channel_id): if request.website.is_public_user(): @@ -495,9 +619,19 @@ class WebsiteSlides(WebsiteProfile): return {'error': _('Internal server error, please try again later or contact administrator.\nHere is the error message: %s') % e} redirect_url = "/slides/slide/%s" % (slide.id) + if channel.channel_type == "training" and not slide.slide_type == "webpage": + redirect_url = "/slides/%s" % (slug(channel)) if slide.slide_type == 'webpage': redirect_url += "?enable_editor=1" - return {'url': redirect_url} + if slide.slide_type == "quiz": + action_id = request.env.ref('website_slides.action_slides_slides').id + redirect_url = '/web#id=%s&action=%s&model=slide.slide&view_type=form' %(slide.id,action_id) + return { + 'url': redirect_url, + 'channel_type': channel.channel_type, + 'slide_id': slide.id, + 'category_id': slide.category_id + } def _get_valid_slide_post_values(self): return ['name', 'url', 'tag_ids', 'slide_type', 'channel_id', @@ -527,6 +661,32 @@ class WebsiteSlides(WebsiteProfile): 'can_create': can_create, } + @http.route('/slides/add_category', type="json", website=True, auth="user") + def add_category(self, channel_id, name, **kw): + channel = request.env['slide.channel'].browse(channel_id) + request.env['slide.category'].create({ + 'name': name, + 'channel_id': channel.id + }) + + return {'url': "/slides/%s" %(slug(channel))} + + #Not using the /web/dataset/resequence route as a slide can be dragged into another category at the same time + @http.route('/slides/resequence_slides', type="json", website=True, auth="user") + def resequence_slides(self, slides_data=None, **kw): + channel_user = None + for slide in slides_data: + s = request.env['slide.slide'].browse(slide['id']) + if not channel_user: + channel_user = s.channel_id.user_id + if request.env.user != channel_user: + return {'error': 'Only the responsible of the channel can edit it'} + s.write({ + 'sequence': slide['sequence'], + 'category_id': slide['category_id'] + }) + + # -------------------------------------------------- # EMBED IN THIRD PARTY WEBSITES # -------------------------------------------------- diff --git a/addons/website_slides/data/website_data.xml b/addons/website_slides/data/website_data.xml index 0d2f0582deb..e2e8b1c4a78 100644 --- a/addons/website_slides/data/website_data.xml +++ b/addons/website_slides/data/website_data.xml @@ -6,7 +6,7 @@ - Presentations + Courses /slides 50 diff --git a/addons/website_slides/models/__init__.py b/addons/website_slides/models/__init__.py index 4b8210c6caa..0fcfe6373ab 100644 --- a/addons/website_slides/models/__init__.py +++ b/addons/website_slides/models/__init__.py @@ -2,6 +2,7 @@ from . import gamification_challenge from . import slide_slide +from . import slide_question from . import slide_channel from . import slide_channel_tag from . import slide_channel_invite diff --git a/addons/website_slides/models/slide_channel.py b/addons/website_slides/models/slide_channel.py index dd772a8ec7b..6bbd0e3b297 100644 --- a/addons/website_slides/models/slide_channel.py +++ b/addons/website_slides/models/slide_channel.py @@ -101,6 +101,7 @@ class Channel(models.Model): nbr_videos = fields.Integer('Number of Videos', compute='_compute_slides_statistics', store=True) nbr_infographics = fields.Integer('Number of Infographics', compute='_compute_slides_statistics', store=True) nbr_webpages = fields.Integer("Number of Webpages", compute='_compute_slides_statistics', store=True) + nbr_quizs = fields.Integer("Number of quizzes", compute="_compute_slides_statistics", store=True) total_slides = fields.Integer('# Slides', compute='_compute_slides_statistics', store=True, oldname='total') total_views = fields.Integer('# Views', compute='_compute_slides_statistics', store=True) total_votes = fields.Integer('# Votes', compute='_compute_slides_statistics', store=True) @@ -174,7 +175,7 @@ class Channel(models.Model): def _compute_slides_statistics(self): result = dict((cid, dict(total_slides=0, total_views=0, total_votes=0, total_time=0)) for cid in self.ids) read_group_res = self.env['slide.slide'].read_group( - [('is_published', '=', True), ('channel_id', 'in', self.ids)], + [('is_published', '=', True),('channel_id', 'in', self.ids)], ['channel_id', 'slide_type', 'likes', 'dislikes', 'total_views', 'completion_time'], groupby=['channel_id', 'slide_type'], lazy=False) @@ -195,7 +196,7 @@ class Channel(models.Model): def _compute_slides_statistics_type(self, read_group_res): """ Can be overridden to compute stats on added slide_types """ - result = dict((cid, dict(nbr_presentations=0, nbr_documents=0, nbr_videos=0, nbr_infographics=0, nbr_webpages=0)) for cid in self.ids) + result = dict((cid, dict(nbr_presentations=0, nbr_documents=0, nbr_videos=0, nbr_infographics=0, nbr_webpages=0, nbr_quizs=0)) for cid in self.ids) for res_group in read_group_res: cid = res_group['channel_id'][0] result[cid]['nbr_presentations'] += res_group.get('slide_type', '') == 'presentation' and res_group['__count'] or 0 @@ -203,6 +204,7 @@ class Channel(models.Model): result[cid]['nbr_videos'] += res_group.get('slide_type', '') == 'video' and res_group['__count'] or 0 result[cid]['nbr_infographics'] += res_group.get('slide_type', '') == 'infographic' and res_group['__count'] or 0 result[cid]['nbr_webpages'] += res_group.get('slide_type', '') == 'webpage' and res_group['__count'] or 0 + result[cid]['nbr_quizs'] += res_group.get('slide_type', '') == 'quiz' and res_group['__count'] or 0 return result @api.depends('slide_partner_ids') @@ -409,6 +411,7 @@ class Category(models.Model): nbr_videos = fields.Integer("Number of Videos", compute='_count_presentations', store=True) nbr_infographics = fields.Integer("Number of Infographics", compute='_count_presentations', store=True) nbr_webpages = fields.Integer("Number of Webpages", compute='_count_presentations', store=True) + nbr_quizs = fields.Integer("Number of quizzes", compute="_count_presentations", store=True) total_slides = fields.Integer(compute='_count_presentations', store=True, oldname='total') @api.depends('slide_ids.slide_type', 'slide_ids.is_published') @@ -432,6 +435,7 @@ class Category(models.Model): 'nbr_videos': result[record_id].get('video', 0), 'nbr_infographics': result[record_id].get('infographic', 0), 'nbr_webpages': result[record_id].get('webpage', 0), + 'nbr_quizs': result[record_id].get('quiz', 0), 'total_slides': 0 } @@ -440,5 +444,6 @@ class Category(models.Model): statistics['total_slides'] += statistics['nbr_videos'] statistics['total_slides'] += statistics['nbr_infographics'] statistics['total_slides'] += statistics['nbr_webpages'] + statistics['total_slides'] += statistics['nbr_quizs'] return statistics diff --git a/addons/website_slides/models/slide_question.py b/addons/website_slides/models/slide_question.py new file mode 100644 index 00000000000..0193558ba46 --- /dev/null +++ b/addons/website_slides/models/slide_question.py @@ -0,0 +1,51 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from odoo import api, fields, models, _ +from odoo.exceptions import ValidationError + + +class SlideQuestion(models.Model): + _name = "slide.question" + _rec_name = "question" + _description = """ + Question for a slide. A slide can have multiple questions and each question + must have at least 2 possible answers and only one good answer. + """ + + sequence = fields.Integer("Sequence", default=10) + question = fields.Char("Question Name", required=True) + slide_id = fields.Many2one('slide.slide', string="Slide") + answer_ids = fields.One2many('slide.answer', 'question_id', string="Answer") + + @api.constrains('answer_ids') + def _check_only_one_good_answer(self): + for question in self: + good_answer_count = 0 + for answer in question.answer_ids: + if answer.is_correct: + good_answer_count += 1 + if good_answer_count > 1: + raise ValidationError(_('A question can only have one good answer')) + + @api.constrains('answer_ids') + def _check_correct_answer(self): + for question in self: + if not any([answer.is_correct for answer in question.answer_ids]): + raise ValidationError(_("A question must at least have one good answer")) + + @api.constrains('answer_ids') + def _check_at_least_2_answers(self): + for question in self: + if len(question.answer_ids) < 2: + raise ValidationError(_("A question must at least have two possible answers")) + + +class SlideAnswer(models.Model): + _name = "slide.answer" + _rec_name = "text_value" + _description = """Answer for a slide question""" + + question_id = fields.Many2one('slide.question', string="Question") + text_value = fields.Char("Answer", required=True,) + is_correct = fields.Boolean("Is correct answer", default=False) diff --git a/addons/website_slides/models/slide_slide.py b/addons/website_slides/models/slide_slide.py index 35b9fefc4e5..a71446b82fa 100644 --- a/addons/website_slides/models/slide_slide.py +++ b/addons/website_slides/models/slide_slide.py @@ -23,12 +23,14 @@ class SlidePartnerRelation(models.Model): _description = 'Slide / Partner decorated m2m' _table = 'slide_slide_partner' - slide_id = fields.Many2one('slide.slide', index=True, required=True) + slide_id = fields.Many2one('slide.slide', ondelete="cascade", index=True, required=True) channel_id = fields.Many2one('slide.channel', string="Channel", related="slide_id.channel_id", store=True, index=True) partner_id = fields.Many2one('res.partner', index=True, required=True) vote = fields.Integer('Vote', default=0) completed = fields.Boolean('Completed') + quiz_attempts_count = fields.Integer(string="Number of times this user attempted the quiz") + class SlideLink(models.Model): _name = 'slide.slide.link' @@ -95,6 +97,7 @@ class Slide(models.Model): 'most_voted': 'likes desc', 'latest': 'date_published desc', } + _order = 'category_sequence asc, sequence asc' _sql_constraints = [ ('exclusion_html_content_and_url', "CHECK(html_content IS NULL OR url IS NULL)", "A slide is either filled with a document url or HTML content. Not both.") @@ -106,6 +109,8 @@ class Slide(models.Model): # description name = fields.Char('Title', required=True, translate=True) active = fields.Boolean(default=True) + sequence = fields.Integer('Sequence', default=10) + category_sequence = fields.Integer('Category sequence', related="category_id.sequence", store=True) user_id = fields.Many2one('res.users', string='Uploaded by', default=lambda self: self.env.uid) description = fields.Text('Description', translate=True) channel_id = fields.Many2one('slide.channel', string="Channel", required=True) @@ -126,15 +131,24 @@ class Slide(models.Model): partner_ids = fields.Many2many('res.partner', 'slide_slide_partner', 'slide_id', 'partner_id', string='Subscribers', groups='base.group_website_publisher') slide_partner_ids = fields.One2many('slide.slide.partner', 'slide_id', string='Subscribers information', groups='base.group_website_publisher') - user_membership_id = fields.Many2one('slide.slide.partner', string="Subscriber information", compute='_compute_user_membership_id', + user_membership_id = fields.Many2one( + 'slide.slide.partner', string="Subscriber information", compute='_compute_user_membership_id', help="Subscriber information for the current logged in user") + # Quiz related fields + question_ids = fields.One2many("slide.question","slide_id", string="Questions") + quiz_first_attempt_reward = fields.Integer("First attempt reward", default=10) + quiz_second_attempt_reward = fields.Integer("Second attempt reward", default=7) + quiz_third_attempt_reward = fields.Integer("Third attempt reward", default=5,) + quiz_fourth_attempt_reward = fields.Integer("Reward for every attempt after the third try", default=2) + # content slide_type = fields.Selection([ ('infographic', 'Infographic'), + ('webpage', 'Web Page'), ('presentation', 'Presentation'), ('document', 'Document'), - ('webpage', 'Web Page'), - ('video', 'Video')], + ('video', 'Video'), + ('quiz', "Quiz")], string='Type', required=True, default='document', readonly=True, help="The document type will be set automatically based on the document URL and properties (e.g. height and width for presentation and document).") diff --git a/addons/website_slides/security/ir.model.access.csv b/addons/website_slides/security/ir.model.access.csv index 9dbfa916ff6..ce9bcf2593d 100644 --- a/addons/website_slides/security/ir.model.access.csv +++ b/addons/website_slides/security/ir.model.access.csv @@ -3,6 +3,10 @@ access_slide_slide_all,slide.slide.all,model_slide_slide,,1,0,0,0 access_slide_slide_publisher,slide.slide.publisher,model_slide_slide,website.group_website_publisher,1,1,1,1 access_slide_slide_partner_all,slide.slide.partner.all,model_slide_slide_partner,,0,0,0,0 access_slide_slide_partner_system,slide.slide.partner.system,model_slide_slide_partner,website.group_website_publisher,1,1,1,1 +access_slide_question_all,slide.question.all,model_slide_question,,1,0,0,0 +access_slide_question_publisher,slide.question.publisher,model_slide_question,website.group_website_publisher,1,1,1,1 +access_slide_answer_all,slide.answer.all,model_slide_answer,,1,0,0,0 +access_slide_answer_publisher,slide.answer.publisher,model_slide_answer,website.group_website_publisher,1,1,1,1 access_slide_tag_all,slide.tag.all,model_slide_tag,,1,0,0,0 access_slide_tag_publisher,slide.tag.publisher,model_slide_tag,website.group_website_publisher,1,1,1,1 access_slide_channel_tag_all,slide.channel.tag.all,model_slide_channel_tag,,1,0,0,0 diff --git a/addons/website_slides/static/src/js/slides_category.js b/addons/website_slides/static/src/js/slides_category.js new file mode 100644 index 00000000000..d1d7efb7747 --- /dev/null +++ b/addons/website_slides/static/src/js/slides_category.js @@ -0,0 +1,148 @@ +odoo.define('website_slides.add.section', function (require) { + 'use strict'; + + var sAnimations = require('website.content.snippets.animation'); + var core = require('web.core'); + var Widget = require('web.Widget'); + + var _t = core._t; + + var SectionDialog = Widget.extend({ + template: 'website.slide.add.section', + events: { + 'hidden.bs.modal': 'destroy', + 'click button.save': '_save', + 'click button[data-dismiss="modal"]': '_cancel', + 'change input#upload': '_slideUpload', + 'change input#url': '_slideUrl', + 'click .list-group-item': function (ev) { + this.$('.list-group-item').removeClass('active'); + $(ev.target).closest('li').addClass('active'); + } + }, + + /** + * @override + * @param {Object} el + * @param {number} channel_id + */ + init: function (el, channelID) { + this._super(el, channelID); + this.channel_id = parseInt(channelID, 10); + this.index_content = ''; + }, + /** + * @override + */ + start: function () { + this.$el.modal({ + backdrop: 'static' + }); + + return this._super.apply(this, arguments); + }, + _getValue: function () { + var canvas = this.$('#data_canvas')[0], + values = { + 'channel_id': this.channel_id || '', + 'url': this.$('#url').val(), + 'name': this.$('#section_name').val() + }; + return values; + }, + /** + * @private + */ + _validate: function () { + this.$('.form-group').removeClass('o_has_error').find('.form-control, .custom-select').removeClass('is-invalid'); + if (!this.$('#name').val()) { + this.$('#name').closest('.form-group').addClass('o_has_error').find('.form-control, .custom-select').addClass('is-invalid'); + return false; + } + var url = this.$('#url').val() ? this.is_valid_url : false; + if (!(this.file.name || url)) { + this.$('#url').closest('.form-group').addClass('o_has_error').find('.form-control, .custom-select').addClass('is-invalid'); + return false; + } + return true; + }, + //-------------------------------------------------------------------------- + // Handlers + //-------------------------------------------------------------------------- + + /** + * @override + * @param {Object} ev + */ + _save: function (ev) { + var self = this; + + var values = this._getValue(); + if ($(ev.target).data('published')) { + values.website_published = true; + } + this.$('.oe_slides_upload_loading').show(); + this.$('.modal-footer, .modal-body').hide(); + this._rpc({ + route: '/slides/add_category', + params: values, + }).then(function (data) { + if (data.error) { + self._displayAlert(data.error); + self.$('.oe_slides_upload_loading').hide(); + self.$('.modal-footer, .modal-body').show(); + + } else { + window.location = data.url; + } + }); + }, + /** + * @override + */ + _cancel: function () { + this.trigger('cancel'); + }, + + }); + + sAnimations.registry.websiteSlidesSection = sAnimations.Class.extend({ + selector: '.oe_slide_js_add_section', + xmlDependencies: ['/website_slides/static/src/xml/website_slides_upload.xml'], + read_events: { + 'click': '_onAddSectionClick', + }, + + /** + * @override + */ + start: function () { + // Automatically open the upload dialog if requested from query string + if ($.deparam.querystring().enable_slide_upload !== undefined) { + this._openDialog(this.$el.attr('channel_id')); + } + return this._super.apply(this, arguments); + }, + + //-------------------------------------------------------------------------- + // Private + //-------------------------------------------------------------------------- + + _openDialog: function (channelID) { + new SectionDialog(this, channelID).appendTo(document.body); + }, + + //-------------------------------------------------------------------------- + // Handlers + //-------------------------------------------------------------------------- + + /** + * @private + * @param {Event} ev + */ + _onAddSectionClick: function (ev) { + console.log('test'); + this._openDialog($(ev.currentTarget).attr('channel_id')); + }, + }); + }); \ No newline at end of file diff --git a/addons/website_slides/static/src/js/slides_course_fullscreen_player.js b/addons/website_slides/static/src/js/slides_course_fullscreen_player.js new file mode 100644 index 00000000000..1da173a2f46 --- /dev/null +++ b/addons/website_slides/static/src/js/slides_course_fullscreen_player.js @@ -0,0 +1,427 @@ +var onYouTubeIframeAPIReady; + +odoo.define('website_slides.fullscreen', function (require) { + 'use strict'; + var sAnimations = require('website.content.snippets.animation'); + var Widget = require('web.Widget'); + var core = require('web.core'); + var QWeb = core.qweb; + + var tag = document.createElement('script'); + + var QuizWidget = require('website_slides.quiz'); + + tag.src = "https://www.youtube.com/iframe_api"; + var firstScriptTag = document.getElementsByTagName('script')[0]; + firstScriptTag.parentNode.insertBefore(tag, firstScriptTag); + + + var Fullscreen = Widget.extend({ + /** + * @override + * @param {Object} el + * @param {Object} data holding channelId and optionally upload and publish control parameters + */ + init: function(el, course_id,slide_id, user_id){ + this._super.apply(this,arguments); + this.courseID = parseInt(course_id, 10); + this.slideID = parseInt(slide_id, 10); + this.userID = parseInt(user_id, 10); + this.course = undefined; + this.slide = undefined; + this.slides = []; + this.nextSlide = undefined; + this.previousSlide = undefined; + this.url = undefined; + this.urlToSmallScreen = undefined; + this.activetab = undefined; + this.player = undefined; + this.goToQuiz = false; + this.answeredQuestions = []; + this.slideTitle = undefined; + }, + start: function(){ + var self = this; + this.url = window.location.pathname; + this.urlToSmallScreen = this.url.replace('/fullscreen',''); + this._getSlides(); + this._renderPlayer(); + this._bindListEvents(); + + $(document).keydown(this._onKeyDown.bind(this)); + return this._super.apply(this, arguments); + }, + //-------------------------------------------------------------------------- + // Private + //-------------------------------------------------------------------------- + /** + * @private + * Renders the player accordingly to the current slide + */ + _renderPlayer: function(){ + var self = this; + var embed_url; + if(self.slide.slide_type !== 'webpage' || self.slide.htmlContent){ + if((self.slide.slide_type == "quiz" || self.slide.hasQuiz) && !self.slide.quiz){ + self._fetchQuiz(); + } else { + embed_url = $(this.slide.embed_code).attr('src'); + if (self.slide.slide_type === "video"){ + embed_url = "https://" + embed_url + "&rel=0&autoplay=1&enablejsapi=1&origin=" + window.location.origin; + } + $('.o_wslides_fullscreen_player').html(QWeb.render('website.slides.fullscreen', { + slide: self.slide, + nextSlide: self.nextSlide, + questions: self.slide.quiz ? self.slide.quiz.questions: '', + reward: self.slide.quiz ? self.slide.quiz.nb_attempts < 3 ? self.slide.quiz.possible_rewards[self.slide.quiz.nb_attempts] : self.slide.quiz.possible_rewards[3]: self.slide.maxPoints, + embed_url: embed_url, + question_count: self.slide.quiz ? self.slide.quiz.questions.length : '', + letters: self.slide.quiz ? self.letters : '', + showMiniQuiz: self.goToQuiz + })); + if(self.slide.slide_type === "video"){ + self._renderYoutubeIframe(); + } + if(self.slide.slide_type == 'webpage'){ + self._renderWebpage(); + } + if((self.slide.slide_type == "presentation" || self.slide.slide_type == "document" || self.slide.slide_type == "infographic" || self.slide.slide_type == "webpage") && !self.slide.quiz){ + self._setSlideStateAsDone(); + } + if ((self.slide.quiz && self.slide.slide_type === "quiz") || self.goToQuiz){ + self._renderQuiz(); + } + } + } else { + self._fetchHtmlContent(); + } + self._renderTitle(); + }, + _renderYoutubeIframe: function(){ + var self = this; + /** + * Due to issues of synchronization between the youtube api script and the widget's instanciation. + */ + try{ + self._setupYoutubePlayer(); + } + catch { + onYouTubeIframeAPIReady = function(){ + var self = this; + self._setupYoutubePlayer(); + }.bind(this) + } + }, + _renderWebpage: function(){ + var self = this; + $(self.slide.htmlContent).appendTo('.o_wslides_fullscreen_webpage_content'); + }, + _renderQuiz: function(){ + var self = this; + var Quiz = new QuizWidget(this, self.slide); + Quiz.appendTo('.o_wslides_fullscreen_player'); + $('.next-slide').click(function(){ + self._goToNextSlide(); + }) + $('.back-to-video').click(function(){ + self.goToQuiz = false; + self._renderPlayer(); + }); + }, + _renderTitle: function(){ + var self = this; + $('.o_wslides_fullscreen_slide_title').empty().html(QWeb.render('website.course.fullscreen.title', { + slide: self.slide, + miniQuiz: self.goToQuiz + })); + }, + /** + * @private + * Links the youtube api to the iframe present in the template + */ + _setupYoutubePlayer: function(){ + var self = this; + self.player = new YT.Player('youtube-player', { + host: 'https://www.youtube.com', + playerVars: {'autoplay': 1, 'origin': window.location.origin}, + autoplay: 1, + events: { + 'onReady': self._onPlayerReady, + 'onStateChange': this._onPlayerStateChange.bind(self) + } + }); + }, + /** + * @param {*} event + * Specific method of the youtube api. + * Whenever the player starts playing, a setinterval is created. + * This setinterval is used to check te user's progress in the video. + * Once the user reaches a particular time in the video, the slide will be considered as completed if the video doesn't have a mini-quiz. + * This method also allows to automatically go to the next slide (or the quiz associated to the current video) once the video is over + */ + _onPlayerStateChange: function(event){ + var self = this; + var tid; + clearInterval(self.tid); + if (event.data == YT.PlayerState.PLAYING && !self.slide.done) { + self.tid = setInterval(function(){ + if(event.target.getCurrentTime){ + var currentTime = event.target.getCurrentTime(); + var totalTime = event.target.getDuration(); + if(totalTime && currentTime > totalTime - 30){ + clearInterval(self.tid); + if(!self.slide.hasQuiz && !self.slide.done){ + self.slide.done = true; + self._setSlideStateAsDone(); + } + } + } + }, 1000); + } + if(event.data == YT.PlayerState.ENDED){ + self.player = undefined; + self._goToNextSlide(); + } + }, + /** + * @private + * Creates slides objects from every slide-list-cells attributes + */ + _getSlides: function(){ + var self = this; + var slides = $('.o_wslides_fullscreen_sidebar_slide_tab'); + for(var i = 0; i < slides.length;i++){ + var slide = $(slides[i]); + self.slides.push({ + id: parseInt(slide.attr('slide_id'), 10), + name: slide.attr('slide_name'), + embed_code: slide.attr('slide_embed_code'), + slide_type: slide.attr('slide_type'), + done: slide.attr('done'), + hasQuiz: slide.attr('quiz'), + slug: slide.attr('slide_slug'), + htmlContent: undefined + }); + this._getActiveSlide(); + } + }, + /** + * @private + * @param {object} slide + * Fetch the quiz for a particular slide + */ + _fetchQuiz: function(){ + var self = this; + self._rpc({ + route:"/slide/quiz/get", + params: { + 'slide_id': self.slide.id + } + }).then(function(data){ + if(data){ + self.slide.quiz = data; + self._renderPlayer(); + } + }) + }, + _fetchHtmlContent: function(){ + var self = this; + self._rpc({ + route:"/slide/html_content/get", + params: { + 'slide_id': self.slide.id + } + }).then(function(data){ + self.slide.htmlContent = data.html_content; + self._renderPlayer(); + }) + }, + /** + * @private + * Once the completion conditions are filled, + * sends a json request to the backend to set the relation between the slide and the user as being completed + */ + _setSlideStateAsDone: function(){ + var self = this; + self._rpc({ + route: '/slides/set_completed', + params: { + slide_id: self.slide.id, + } + }).then(function(data){ + $('#check-'+self.slide.id).replaceWith($('')) + self.slide.done = true; + clearInterval(self.tid); + self.channelCompletion = data.channel_completion; + self._updateProgressbar(); + }); + }, + _updateProgressbar: function(){ + var self = this; + var completion = self.channelCompletion <= 100 ? self.channelCompletion : 100; + $('.o_wslides_fullscreen_sidebar_progress_gauge').css('width', completion + "%" ); + $('.o_wslides_progress_percentage').text(completion); + }, + /** + * @private + * Creates an array of letters to be used in the quiz with a foreach + */ + _generateQuizLetters: function(){ + var letters = []; + for(var i = 65; i < 91; i++){ + letters.push(String.fromCharCode(i)); + } + return letters; + }, + _goToNextSlide: function(){ + var self = this; + clearInterval(self.tid); + self.player = undefined; + self.goToQuiz = self.slide.hasQuiz && !self.goToQuiz; + if(self.nextSlide && !self.goToQuiz){ + self.slide = self.nextSlide; + self.index++; + self._setActiveTab(); + self._renderPlayer(); + self._setPreviousAndNextSlides(); + self._updateUrl(); + history.pushState(null,'',self.url); + } + else if(self.nextSlide){ + self._renderPlayer(); + } + }, + _goToPreviousSlide: function(){ + var self = this; + clearInterval(self.tid); + self.goToQuiz = false; + self.player = undefined; + if(self.previousSlide){ + self.slide = self.previousSlide; + self.index--; + self._setActiveTab(); + self._renderPlayer(); + self._setPreviousAndNextSlides(); + self._updateUrl(); + history.pushState(null,'',self.url); + } + }, + _setPreviousAndNextSlides: function(){ + var self = this; + self.previousSlide = self.index > 0 ? self.slides[self.index-1] : undefined; + self.nextSlide = self.index < (self.slides.length - 1) ? self.slides[self.index+1] : undefined; + }, + /** + * Changes the url whenever the user changes slides. + * This allows the user to refresh the page and stay at the right video + */ + _updateUrl: function(){ + var self = this; + var url = window.location.pathname.split('/'); + url[url.length-1] = self.slide.slug; + url = url.join('/'); + self.url = url; + self.urlToSmallScreen = self.url; + self.url += "?fullscreen=1"; + $('.o_wslides_small_screen').attr('href', self.urlToSmallScreen); + }, + /** + * Whenever the user changes slide, change the active tab + */ + _setActiveTab: function(){ + var self = this; + self.activeTab.removeClass('active'); + $('li.active').removeClass('active'); + $('li[slide_id='+self.slide.id+']').addClass('active'); + self.activeTab = $('.o_wslides_fullscreen_sidebar_slide_tab[index="'+self.index+'"]') + self.activeTab.addClass('active'); + }, + /** + * The first time the user gets on the player, + * get the slide that is represented by the active tab in the sidebar + */ + _getActiveSlide: function(){ + var self = this; + self.activeTab = $('.o_wslides_fullscreen_sidebar_slide_tab.active'); + self.index = parseInt(self.activeTab.attr('index'), 10); + self.slide = self.slides[self.index]; + self._setPreviousAndNextSlides(); + }, + //-------------------------------------------------------------------------- + // Handlers + //-------------------------------------------------------------------------- + _onListCellClick: function(ev){ + var self = this; + clearInterval(self.tid); + self.player = undefined; + var target = $(ev.currentTarget); + self.goToQuiz = false; + if(target[0] !== self.activeTab[0]){ + self.activeTab.removeClass('active'); + target.addClass('active'); + self.index = parseInt(target.attr('index')); + self._getActiveSlide(); + self._renderPlayer(); + $('li.active').removeClass('active'); + $('li[slide_id='+self.slide.id+']').addClass('active'); + self._setPreviousAndNextSlides(); + self._updateUrl(); + history.pushState(null,'',self.url); + } + }, + _onMiniQuizClick: function(ev){ + var self = this; + self.index = parseInt($(ev.currentTarget).attr('index')); + self.slide = self.slides[self.index]; + self.goToQuiz = true; + self._setPreviousAndNextSlides(); + self._renderPlayer(); + self._setActiveTab(); + self._updateUrl(); + history.pushState(null,'' ,self.url); + }, + /** + * @private + * Binds events related to the list + */ + _bindListEvents: function(){ + var self = this; + $('.o_wslides_fullscreen_sidebar_slide_tab').each(function () { + $(this).click(self._onListCellClick.bind(self)); + }); + + $('.o_wslides_slide_quiz ').each(function(){ + $(this).click(self._onMiniQuizClick.bind(self)); + }) + }, + _onKeyDown: function(ev){ + var self = this; + switch(ev.key){ + case "ArrowRight": + self._goToNextSlide(); + break; + case "ArrowLeft": + self._goToPreviousSlide(); + break; + } + }, + }) + + sAnimations.registry.websiteSlidesFullscreenPlayer = Widget.extend({ + selector: '.oe_js_course_slide', + xmlDependencies: ['/website_slides/static/src/xml/website_slides_fullscreen.xml'], + init: function(el){ + this._super.apply(this, arguments); + }, + start: function(){ + this._super.apply(this, arguments); + var user_id = this.$el.attr('user_id'); + var course_id = this.$el.attr('course_id'); + var slide_id = this.$el.attr('slide_id'); + var fullscreen = new Fullscreen(this, course_id, slide_id, user_id); + fullscreen.appendTo(".oe_js_course_slide"); + } + }); + + return Fullscreen; +}); \ No newline at end of file diff --git a/addons/website_slides/static/src/js/slides_course_progress_bar.js b/addons/website_slides/static/src/js/slides_course_progress_bar.js new file mode 100644 index 00000000000..2412368018f --- /dev/null +++ b/addons/website_slides/static/src/js/slides_course_progress_bar.js @@ -0,0 +1,47 @@ +odoo.define('website_slides.progress.bar', function (require) { + + var sAnimations = require('website.content.snippets.animation'); + var Widget = require('web.Widget'); + + var ProgressBar = Widget.extend({ + + /** + * @override + * @param {Object} el + * @param {number} channel_id + */ + init: function (el, completion) { + this._super(el); + this.completion = completion; + }, + /** + * @override + */ + start: function () { + this._renderProgressBar(); + return this._super.apply(this, arguments); + }, + //-------------------------------------------------------------------------- + // Handlers + //-------------------------------------------------------------------------- + _renderProgressBar: function (ev) { + var self = this; + $('.oe_slide_js_progress_bar').css('width', (self.completion <= 100 ? self.completion : 100) + '%'); + }, + }); + + sAnimations.registry.websiteSlidesProgressBar = sAnimations.Class.extend({ + selector: '.oe_slide_js_progress_bar', + /** + * @override + */ + start: function () { + var completion = parseInt($('.oe_slide_js_progress_bar').attr('channel_completion')); + var progressBar = new ProgressBar(this, completion); + progressBar.appendTo(".oe_slide_js_progress_bar"); + return this._super.apply(this, arguments); + }, + }); + + return ProgressBar; + }); diff --git a/addons/website_slides/static/src/js/slides_course_quiz.js b/addons/website_slides/static/src/js/slides_course_quiz.js new file mode 100644 index 00000000000..f98d5b3ed8a --- /dev/null +++ b/addons/website_slides/static/src/js/slides_course_quiz.js @@ -0,0 +1,206 @@ +odoo.define('website_slides.quiz', function (require) { + 'use strict'; + var sAnimations = require('website.content.snippets.animation'); + var core = require('web.core'); + var Widget = require('web.Widget'); + + var QWeb = core.qweb; + + var Quiz= Widget.extend({ + /** + * @override + * @param {Object} el + * @param {Object} data holding all the slide elements needed for the quiz + * It will either come from the fullscreen widget or the sAnimation at the end of this file + */ + init: function(el, data){ + this.slide = data; + this.answeredQuestions = []; + this.fullscreen = el; + return this._super.apply(this,arguments); + }, + start: function(){ + var self = this; + self._bindQuizEvents(); + /** + * If the quiz is rendered by the server instead of the fullscreen widget, + * questions and their answers will have to be created manually from attributes + */ + if(self.slide.quiz.questions.length === 0){ + this._setQuestions(); + } + return this._super.apply(this, arguments); + }, + _renderSuccessModal: function(){ + var self =this; + $('.o_wslides_fullscreen_quiz').append(QWeb.render('website.course.quiz.success', { + data: data + })); + $('.submit-quiz').remove(); + $('.next-slide').css('display', 'inline-block'); + $('.next-slide').click(function(){ + self.fullscreen._goToNextSlide(); + }); + $('.o_wslides_quiz_success_modal_close').click(function(){ + $('.o_wslides_quiz_success_modal').remove(); + $('.o_wslides_quiz_modal_background').remove(); + }); + $(".o_wslides_quiz_modal_background").click(function(ev){ + $(ev.currentTarget).remove(); + $('.o_wslides_quiz_success_modal').remove(); + }); + }, + //-------------------------------------------------------------------------- + // Private + //-------------------------------------------------------------------------- + /** + * @private + * In case the quiz is rendered by the server and the data don't come from the fullscreen widget, + * questions and their answers will have to be set here by using attributes + */ + _setQuestions: function(){ + var self = this; + $('.o_wslides_quiz_question').each(function(){ + self.slide.quiz.questions.push({ + id: parseInt($(this).attr('id')), + title: $(this).attr('title'), + answers: [] + }); + }) + for(var i = 0; i < self.slide.quiz.questions.length; i++){ + self._setAnswersForQuestion(self.slide.quiz.questions[i]); + } + }, + _setAnswersForQuestion: function (question){ + var self = this; + $('.o_wslides_quiz_answer[question_id='+question.id+']').each(function (){ + question.answers.push({ + id: parseInt($(this).attr('id')), + text: $(this).attr('text_value'), + is_correct: $(this).attr('is_correct') + }); + }); + }, + _updateProgressbar: function (){ + var self = this; + var completion = self.channelCompletion <= 100 ? self.channelCompletion : 100; + $('.o_wslides_fullscreen_sidebar_progress_gauge').css('width', completion + "%" ); + $('.o_wslides_progress_percentage').text(completion); + }, + _bindQuizEvents: function (){ + var self = this; + if (!self.slide.done){ + $('.o_wslides_quiz_answer').each(function(){ + $(this).click(self._onAnswerClick.bind(self)); + }); + } + $('.submit-quiz').click(self._onSubmitQuiz.bind(self)); + }, + _highlightAnswers: function (answers){ + var self = this; + self.answeredQuestions = []; + for (var i = 0; i < answers.goodAnswers.length; i++){ + $('#answer'+ answers.goodAnswers[i] +'').addClass('o_wslides_quiz_good_answer'); + $('#answer'+ answers.goodAnswers[i] +' .o_wslides_quiz_radio_box span').replaceWith($('')); + var questionID =$('#answer'+ answers.goodAnswers[i] +' .o_wslides_quiz_radio_box input').attr('question_id'); + self.answeredQuestions.push(questionID); + $('.o_wslides_quiz_answer[question_id='+questionID+']:not(.o_wslides_quiz_good_answer)').addClass('o_wslides_quiz_ignored_answer'); + $('.o_wslides_quiz_answer[question_id='+questionID+']').each(function(){ + $(this).unbind('click') + }); + $('input[question_id='+questionID+']').each(function(){ + $(this).prop('disabled',true); + }); + } + for (i = 0; i < answers.badAnswers.length; i++){ + $('#answer'+ answers.badAnswers[i]).removeClass('o_wslides_quiz_good_answer') + .addClass('o_wslides_quiz_bad_answer') + .unbind('click'); + $('#answer'+ answers.badAnswers[i] +' .o_wslides_quiz_radio_box span').replaceWith($('')); + $('#answer'+ answers.badAnswers[i] +' .o_wslides_quiz_radio_box input').prop('checked', false); + } + }, + //-------------------------------------------------------------------------- + // Handlers + //-------------------------------------------------------------------------- + _onAnswerClick: function (ev){ + var self = this; + var target = $(ev.currentTarget); + if ((self.answeredQuestions.indexOf(target.attr('question_id'))) === -1){ + var id = target.attr('id'); + var question_id = target.attr('question_id'); + $('.o_wslides_quiz_answer[question_id='+question_id+']').removeClass('o_wslides_quiz_good_answer'); + $('#'+id+' input[type=radio]').prop('checked', true); + } + }, + _onSubmitQuiz: function (){ + var self = this; + var inputs = $('input[type=radio]:checked'); + var values = []; + for (var i = 0; i < inputs.length; i++){ + values.push(parseInt($(inputs[i]).val())); + } + if (values.length === self.slide.quiz.questions.length){ + $('.quiz-danger').remove(); + self._rpc({ + route: "/slide/quiz/submit", + params: { + slide_id: self.slide.id, + answer_ids: values, + quiz_id: self.slide.quiz_id + } + }).then(function(data){ + self._highlightAnswers(data); + self.slide.quiz.nb_attempts = data.attempts_count; + if (data.passed){ + self.channelCompletion = data.channel_completion; + self._updateProgressbar(); + $('#check-'+self.slide.id).replaceWith($('')); + self.slide.done = true; + self._renderSuccessModal(); + } + else { + $('#quiz-points').text(self.slide.quiz.nb_attempts < 3 ? self.slide.quiz.possible_rewards[self.slide.quiz.nb_attempts] : self.slide.quiz.possible_rewards[self.slide.quiz.possible_rewards.length-1]); + } + }); + } else { + $('.o_wslides_fullscreen_player').append($('

All questions must be answered !

')); + } + }, +}); + + sAnimations.registry.websiteSlidesQuizNoFullscreen = Widget.extend({ + selector: '.o_w_slides_quiz_no_fullscreen', + xmlDependencies: ['/website_slides/static/src/xml/website_slides_fullscreen.xml'], + init: function (el){ + this._super.apply(this, arguments); + }, + start: function (){ + this._super.apply(this, arguments); + var slideID = parseInt(this.$el.attr('slide_id'),10); + var slideDone = this.$el.attr('slide_done'); + var nbAttempts = parseInt(this.$el.attr('nb_attempts'), 10); + var firstAttemptReward = this.$el.attr('first_reward'); + var secondAttemptReward = this.$el.attr('second_reward'); + var thirdAttemptReward = this.$el.attr('third_reward'); + var fourthAttemptReward = this.$el.attr('fourth_reward'); + var possibleRewards = [firstAttemptReward,secondAttemptReward,thirdAttemptReward,fourthAttemptReward] + var data = { + id: slideID, + done: slideDone, + quiz: { + questions: [], + nb_attempts: nbAttempts, + possible_rewards: possibleRewards, + reward: nbAttempts < possibleRewards.length ? possibleRewards[nbAttempts] : possibleRewards[possibleRewards.length-1] + } + } + if(!slideDone){ + var NewQuiz = new Quiz(this, data); + NewQuiz.appendTo(".o_w_slides_quiz_no_fullscreen"); + } + } + }); + + return Quiz; +}); \ No newline at end of file diff --git a/addons/website_slides/static/src/js/slides_course_sidebar_list.js b/addons/website_slides/static/src/js/slides_course_sidebar_list.js new file mode 100644 index 00000000000..11b61d4e5ec --- /dev/null +++ b/addons/website_slides/static/src/js/slides_course_sidebar_list.js @@ -0,0 +1,34 @@ +odoo.define('website_slides.sidebar', function (require) { + 'use strict'; + var sAnimations = require('website.content.snippets.animation'); + var core = require('web.core'); + var Widget = require('web.Widget'); + + var SideBar = Widget.extend({ + init: function (el){ + this._super.apply(this,arguments); + }, + start: function (){ + $('.o_wslides_fullscreen_toggle_sidebar').click(function (ev){ + ev.preventDefault(); + $(ev.currentTarget).toggleClass('active'); + $('.o_wslides_fullscreen_sidebar').toggleClass('o_wslides_fullscreen_sidebar_hidden'); + $('.o_wslides_fullscreen_player').toggleClass('o_wslides_fullscreen_player_no_sidebar') + }) + return this._super.apply(this, arguments); + }, + }); + + sAnimations.registry.websiteSlidesSidebarList = Widget.extend({ + selector: '.o_wslides_fullscreen_toggle_sidebar', + // xmlDependencies: ['/website_slides/static/src/xml/website_slides.xml'], + init: function (el){ + this._super.apply(this, arguments); + }, + start: function (){ + this._super.apply(this, arguments); + var sideBar = new SideBar(this); + sideBar.appendTo(".oe_js_side_bar_list"); + } + }); +}); \ No newline at end of file diff --git a/addons/website_slides/static/src/js/slides_course_slides_list.js b/addons/website_slides/static/src/js/slides_course_slides_list.js new file mode 100644 index 00000000000..bf634f542e3 --- /dev/null +++ b/addons/website_slides/static/src/js/slides_course_slides_list.js @@ -0,0 +1,251 @@ +odoo.define('website_slides.slideslist', function (require) { + 'use strict'; + var sAnimations = require('website.content.snippets.animation'); + var Widget = require('web.Widget'); + + var SlideUpload = require('website_slides.upload_modal'); + + var List = Widget.extend({ + init: function (el){ + this._super.apply(this,arguments); + this.draggedElement = undefined; + this.dropTarget = undefined; + this.slideCount = undefined; + this.slides = []; + this.categories = []; + }, + start: function (){ + this._super.apply(this,arguments); + this.slideCount = $('li.content-slide').length; + //Change links HREF to fullscreen mode for SEO + var links = $(".link-to-slide"); + for (var i = 0; i < links.length; i++){ + $(links[i]).attr('href', $(links[i]).attr('href') + "?fullscreen=1"); + } + this._bindEvents(); + }, + _bindEvents: function (){ + var self = this; + $('.slide-draggable').each(function (){ + self._addSlideDragAndDropHandlers($(this)); + }); + $('.section-draggable').each(function (){ + self._addSectionDragAndDropHandlers($(this)); + }); + $('.course-section').each(function (){ + self._addDropSlideOnSectionHandler($(this)); + }); + }, + _unbind: function (className){ + $("."+className).each(function (){ + $(this).unbind(); + }); + }, + _unbindAll: function (){ + this._unbind('slide-draggable'); + this._unbind('section-draggable'); + this._unbind('course-section'); + }, + _getSlides: function (){ + var self = this; + var slides = $('li.content-slide'); + for(var i = 0; i < slides.length;i++){ + var slide = $(slides[i]); + self.slides.push({ + id: parseInt(slide.attr('slide_id')), + category_id: parseInt(slide.attr('category_id')), + sequence: i + }); + } + }, + _getCategories: function (){ + var self = this; + self.categories = []; + var categories = $('.course-section'); + for (var i = 0; i < categories.length;i++){ + var category = $(categories[i]); + self.categories.push(parseInt(category.attr('category_id'))); + } + }, + _addDropSlideOnSectionHandler: function (target){ + var self = this; + target.on('drop', function (ev){ + if (ev.preventDefault){ + ev.preventDefault(); + } + self.dropTarget = $(ev.currentTarget); + self.draggedElement[0].parentNode.removeChild(self.draggedElement[0]); + $('ul[category_id='+target.attr('category_id')+']').append(self.draggedElement) + self._addSlideDragAndDropHandlers(self.draggedElement); + self._reorderSlides(); + }); + target.on('dragover', function (ev){ + if(ev.preventDefault){ + ev.preventDefault(); + } + }); + }, + _addSlideDragAndDropHandlers: function (target){ + var self = this; + target.on('dragstart', function (ev){ + $('.section-draggable').removeClass('hold') + self._unbind('section-draggable'); + ev.originalEvent.dataTransfer.effectAllowed = 'move'; + ev.originalEvent.dataTransfer.setData('text/html', this.outerHTML); + self.draggedElement = target; + self.draggedElement.addClass('hold'); + }); + target.on('dragover', function (ev){ + if ($(ev.currentTarget) !== self.draggedElement){ + if (ev.preventDefault){ + ev.preventDefault(); + } + target.addClass('slide-hovered'); + } + }); + target.on('dragleave', function (ev){ + if (ev.preventDefault){ + ev.preventDefault(); + } + target.removeClass('slide-hovered'); + }); + target.on('drop', function (ev){ + if (self.draggedElement.hasClass('slide-draggable') && target.hasClass('slide-draggable')){ + if (ev.preventDefault){ + ev.preventDefault(); + } + target.removeClass('slide-hovered'); + target.removeClass('hold'); + if (target !== self.draggedElement){ + self.dropTarget = $(ev.currentTarget); + self.draggedElement[0].parentNode.removeChild(self.draggedElement[0]); + var dropHTML = ev.originalEvent.dataTransfer.getData('text/html'); + target[0].insertAdjacentHTML('beforebegin',dropHTML); + self.draggedElement = $(target[0].previousSibling); + self._reorderSlides(); + } + self._unbindAll(); + self._bindEvents(); + } + }); + target.on('dragend', function (ev){ + if (ev.preventDefault){ + ev.preventDefault(); + } + target.removeClass('slide-hovered'); + target.removeClass('hold'); + }); + }, + _addSectionDragAndDropHandlers: function(target){ + var self = this; + target.on('dragstart', function (ev){ + self._unbind('slide-draggable'); + self._unbind('course-section'); + ev.originalEvent.dataTransfer.effectAllowed = 'move'; + ev.originalEvent.dataTransfer.setData('text/html', this.outerHTML); + self.draggedElement = target; + self.draggedElement.addClass('hold'); + }); + target.on('dragover', function (ev){ + if (target.hasClass('section-draggable') && self.draggedElement.hasClass('section-draggable')){ + if (ev.preventDefault){ + ev.preventDefault(); + } + target.addClass('slide-hovered'); + } + }); + target.on('dragleave', function (ev){ + if (ev.preventDefault){ + ev.preventDefault(); + } + target.removeClass('slide-hovered'); + }); + target.on('drop', function (ev){ + if(ev.preventDefault){ + ev.preventDefault(); + } + if(self.draggedElement.hasClass('section-draggable') && target.hasClass('section-draggable')){ + target.removeClass('slide-hovered'); + target.removeClass('hold'); + self.dropTarget = $(ev.currentTarget); + if(target !== self.draggedElement && $(ev.currentTarget).hasClass('section-draggable')){ + self.draggedElement[0].parentNode.removeChild(self.draggedElement[0]); + var dropHTML = ev.originalEvent.dataTransfer.getData('text/html'); + target[0].insertAdjacentHTML('beforebegin',dropHTML); + self.draggedElement = $(target[0].previousSibling); + self._reorderCategories(); + self._reorderSlides(); + self._rebindUploadButton(self.draggedElement.attr('category_id')); + } + self._unbindAll(); + self._bindEvents(); + } + }); + target.on('dragend', function (ev){ + if (ev.preventDefault){ + ev.preventDefault(); + } + target.removeClass('slide-hovered'); + target.removeClass('hold'); + }); + }, + _reorderCategories: function (){ + var self = this; + self._getCategories(); + self._rpc({ + route: '/web/dataset/resequence', + params: { + model: "slide.category", + ids: self.categories + } + }).then(function (){ + self._resetCategoriesIndex(); + }); + }, + _resetCategoriesIndex: function (){ + var categoriesIndexes = $('.section-index') + for (var i = 0; i < categoriesIndexes.length; i++){ + $(categoriesIndexes[i]).text(i+1) + } + }, + _reorderSlides: function(){ + var self = this; + // In case the slide was transfered to another section + if (self.draggedElement.hasClass('slide-draggable')){ + self.draggedElement.attr('category_id', parseInt(self.dropTarget.attr('category_id'))) + } + self.slides = []; + self._getSlides(); + self._rpc({ + route: "/slides/resequence_slides", + params: { + slides_data: self.slides + } + }).then(function(){ + }); + }, + _rebindUploadButton: function(categoryID){ + var self = this; + this.$('.oe_slide_js_upload[data-category-id='+categoryID+']').click(function(ev){ + ev.preventDefault(); + var data = $(ev.currentTarget).data(); + var dialog = new SlideUpload.SlideUploadDialog(self, data); + dialog.appendTo(document.body); + dialog.open(); + }) + } + }) + + sAnimations.registry.websiteSlidesCourseSlidesList = Widget.extend({ + selector: '.oe_js_course_slides_list', + xmlDependencies: ['/website_slides/static/src/xml/website_slides_upload.xml'], + init: function (el){ + this._super.apply(this, arguments); + }, + start: function (){ + this._super.apply(this, arguments); + var list = new List(this); + list.appendTo(".oe_js_course_slides_list"); + } + }); +}); \ No newline at end of file diff --git a/addons/website_slides/static/src/js/slides_delete_slide.js b/addons/website_slides/static/src/js/slides_delete_slide.js new file mode 100644 index 00000000000..e3eef38502d --- /dev/null +++ b/addons/website_slides/static/src/js/slides_delete_slide.js @@ -0,0 +1,88 @@ +odoo.define('website_slides.delete.slide', function (require) { + + var sAnimations = require('website.content.snippets.animation'); + var core = require('web.core'); + var Widget = require('web.Widget'); + + var _t = core._t; + var QWeb = core.qweb; + + var DeleteSlideDialog = Widget.extend({ + template: 'website.slide.delete.slide', + events: { + 'hidden.bs.modal': 'destroy', + 'click button[data-dismiss="modal"]': '_cancel', + 'click button.delete': '_delete' + }, + + /** + * @override + * @param {Object} el + * @param {number} channel_id + */ + init: function (el, slideID) { + this._super(el, slideID); + this.slide_id = parseInt(slideID, 10); + }, + /** + * @override + */ + start: function () { + this.$el.modal({ + backdrop: 'static' + }); + return this._super.apply(this, arguments); + }, + //-------------------------------------------------------------------------- + // Handlers + //-------------------------------------------------------------------------- + _delete: function (ev) { + var self = this; + // TO FIX: CallBack is not executed + $('[slide_id='+this.slide_id+']').remove(); + this._rpc({ + model: 'slide.slide', + method: 'unlink', + args: [[self.slide_id]], + }).then(function () { + $('[slide='+this.slide_id+']').remove(); + }); + }, + /** + * @override + */ + _cancel: function () { + this.trigger('cancel'); + } + }); + + sAnimations.registry.websiteSlidesDeleteSlide = sAnimations.Class.extend({ + selector: '.oe_slide_js_delete_slide', + xmlDependencies: ['/website_slides/static/src/xml/website_slides_upload.xml'], + read_events: { + 'click': '_onDeleteSlideClick', + }, + + //-------------------------------------------------------------------------- + // Private + //-------------------------------------------------------------------------- + + _openDialog: function (slideID) { + new DeleteSlideDialog(this, slideID).appendTo(document.body); + }, + + //-------------------------------------------------------------------------- + // Handlers + //-------------------------------------------------------------------------- + + /** + * @private + * @param {Event} ev + */ + _onDeleteSlideClick: function (ev) { + var target = $(ev.currentTarget); + this._openDialog(target.attr('slide_id')); + }, + }); + return DeleteSlideDialog; + }); diff --git a/addons/website_slides/static/src/js/slides_upload.js b/addons/website_slides/static/src/js/slides_upload.js index 855b7e52932..7b0a2964bf8 100644 --- a/addons/website_slides/static/src/js/slides_upload.js +++ b/addons/website_slides/static/src/js/slides_upload.js @@ -30,6 +30,7 @@ var SlideUploadDialog = Dialog.extend({ this._setup(); this.channelID = parseInt(options.channelId, 10); + this.defaultCategoryID = parseInt(options.categoryId,10) this.canUpload = options.canUpload === 'True'; this.canPublish = options.canPublish === 'True'; @@ -198,6 +199,7 @@ var SlideUploadDialog = Dialog.extend({ */ _getSelect2DropdownValues: function (){ var result = {}; + var self = this; // tags var tagValues = []; _.each(this.$('#tag_ids').select2('data'), function (val) { @@ -211,11 +213,17 @@ var SlideUploadDialog = Dialog.extend({ result['tag_ids'] = tagValues; } // category - var categoryValue = this.$('#category_id').select2('data'); - if (categoryValue && categoryValue.create) { - result['category_id'] = [0, {'name': categoryValue.text}]; - } else if (categoryValue) { - result['category_id'] = [categoryValue.id]; + if(!self.defaultCategoryID){ + var categoryValue = this.$('#category_id').select2('data'); + if (categoryValue && categoryValue.create) { + result['category_id'] = [0, {'name': categoryValue.text}]; + } else if (categoryValue) { + result['category_id'] = [categoryValue.id]; + this.categoryID = categoryValue.id; + } + } else { + result['category_id'] = [self.defaultCategoryID]; + this.categoryID = self.defaultCategoryID; } return result; }, @@ -303,6 +311,25 @@ var SlideUploadDialog = Dialog.extend({ return values; }, + _reorderSlidesSequence: function(){ + var self = this; + var slidesElement = $('li.content-slide'); + var slides = []; + for(var i = 0; i < slidesElement.length;i++){ + slides.push({ + id: parseInt($(slidesElement[i]).attr('slide_id')), + category_id: parseInt($(slidesElement[i]).attr('category_id')), + sequence: i + }) + } + self._rpc({ + route: '/slides/resequence_slides', + params: { + slides_data: slides + } + }).then(function(){ + }) + }, /** * Init the data relative to the support slide type to upload * @@ -325,6 +352,11 @@ var SlideUploadDialog = Dialog.extend({ label: _t('Video'), template: 'website.slide.upload.modal.video', }, + quiz: { + icon: 'fa-question-circle', + label: _t('Quiz'), + template: 'website.slide.upload.quiz' + } }; }, /** @@ -472,12 +504,11 @@ var SlideUploadDialog = Dialog.extend({ _onChangeSlideUrl: function (ev) { var self = this; var url = $(ev.target).val(); - this._alertRemove(); this.isValidUrl = false; this.set('can_submit_form', false); this._fetchUrlPreview(url).then(function (data) { - this.set('can_submit_form', true); + self.set('can_submit_form', true); if (data.error) { self._alertDisplay(data.error); } else { @@ -508,6 +539,12 @@ var SlideUploadDialog = Dialog.extend({ self.set('state', oldType); self._alertDisplay(data.error); } else { + //Quick and really dirty fix for reordering issues + if(data.channel_type == 'training' && self.categoryID){ + var categoryElement = $('ul[category_id='+self.categoryID+']'); + $('').appendTo(categoryElement) + self._reorderSlidesSequence(); + } window.location = data.url; } }); diff --git a/addons/website_slides/static/src/js/website_slides.editor.js b/addons/website_slides/static/src/js/website_slides.editor.js index 40513e785e2..72a18bb93ee 100644 --- a/addons/website_slides/static/src/js/website_slides.editor.js +++ b/addons/website_slides/static/src/js/website_slides.editor.js @@ -5,6 +5,7 @@ var core = require('web.core'); var Dialog = require('web.Dialog'); var QWeb = core.qweb; var WebsiteNewMenu = require('website.newMenu'); +var wUtils = require('website.utils'); var _t = core._t; diff --git a/addons/website_slides/static/src/scss/slide_course.scss b/addons/website_slides/static/src/scss/slide_course.scss new file mode 100644 index 00000000000..38179498415 --- /dev/null +++ b/addons/website_slides/static/src/scss/slide_course.scss @@ -0,0 +1,536 @@ + +.banner{ + position: absolute; + top: 0; + left: 0; + height: 20%; + background-color: $primary; + width: 256px; + opacity: 0; + color: #fff; + transition: all .3s ease-out; + z-index: 2; + visibility: hidden; + display: flex; + justify-content: space-around; + align-items: center; + margin-left: 15px; +} + +.banner a { + font-size: 1.5rem; + text-align: center; + padding: 20px; +} + +.banner a:hover{ + cursor: pointer; +} + +.fill { + display: flex; + justify-content: center; + align-items: center; + overflow: hidden +} +.fill img { + flex-shrink: 0; + min-width: 100%; + min-height: 100% +} + +.main_image{ + position: relative +} +.main_image:hover{ + .banner{ + opacity: 1; + visibility: visible; + } + +} + +.course-content { + padding: 0; + + ul { + list-style: none; + padding: 0 + } +} + +.course-section { + margin: 0; + background-color: #ddd; + display: flex; + list-style: none; + font-size: 1.05rem; + border-bottom: 1px solid #ccc +} + +.content-type { + width: 30%; +} + +.content-type:hover{ + cursor: pointer; +} + +.course-progress-bar{ + height: 10px; + width: 80%; + box-sizing: border-box; + border-radius: 5px; + background-color: rgba(0,0,0,.2); + overflow: hidden; + + .progress-gauge{ + height: 100%; + width: 0; + background-color: $primary; + + transition: all .8s ease-out; + } +} + +.course-rating{ + color:$primary; +} +.course-banner{ + border-bottom: 1px solid $primary; +} + +.content-tab{ + .active { + + border: none; + border-bottom: 1px solid $secondary; + } +} + +.content-tab a:hover{ + border:none; + border-bottom: 1px solid $secondary; +} + +.content-container{ + border:none; +} + +.content-slide:nth-child(odd){ + background-color: #f6f6f6; +} + +.content-slide:nth-child(even){ + background-color: #f9f9f9; +} + +.content-slide-controls { + i:hover{ + color: $primary !important; + cursor: pointer; + } +} + +.viewer-container { + display: flex; + align-items: stretch; + height: 500px; + width: 100%; + background-color: #ccc; + justify-content: flex-start; +} + + + +.slide-infos { + margin-top: 1rem; + display: flex; + justify-content: space-between; + align-items: start; +} + +.oe_js_player { + +} + +.slide-deactivated { + pointer-events: none; +} + +.slide-draggable { + user-select: none; + -khtml-user-drag: element; + -webkit-user-drag: element; +} + +.slide-hovered{ + margin-top: 10px; +} + +.section-draggable { + user-select: none; + -khtml-user-drag: element; + -webkit-user-drag: element; +} + +.hold{ + border: solid #ccc 4px; +} + + +.fullscreen { + position: absolute; + top: 0; + right: 0; + width: 100%; + z-index: 1000; + height: 100%; + margin: 0; + padding: 0; +} + + +.sidebar-list{ + position: fixed; + top: 46px; + bottom: 0; + left: 0; + width: 300px; + z-index: 2000; +} + + + +.sidebar-list-visible { + left: 50px; +} + +.show-list { + position: absolute; + top: 15%; + left: 50px; + font-size: 2rem; + z-index: 10000; + color: #a0a0a0; + border: 2px solid #ccc; + border-radius: 10px; + padding: 5px 10px; +} + +.hover-background { + opacity: 0; + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + z-index: 3000; +} + +.hover-background:hover { + opacity: 1; +} + + +.show-list:hover { + cursor: pointer; +} + +.change-small-screen { + font-size: 1.5rem; + z-index: 10000; + color: #a0a0a0; +} + +.change-small-screen:hover { + cursor: pointer; + border-color: $primary; + color: $primary; +} + +.sidebar-header { + padding: 10px 15px; + display: flex; + justify-content: space-between; + align-items: center; + font-size: 1.5rem; + border-bottom: 1px solid #ccc; + height: 80px; + + a { + color: black; + } + + a:hover { + color: $primary; + } + + i:hover{ + color: $primary; + } +} + +.close-list:hover { + cursor: pointer; +} + +.fullscreen-btn{ + font-size: 2rem; + display: inline-block; + a { + text-decoration: none; + color: $text-muted; + } +} + +.fullscreen-btn:hover{ + cursor: pointer; + + a { + color: $primary; + } +} + +.sidebar-fullscreen { + position: relative; + top: 0; + left: 0; + bottom: 0; + height: auto; + transform: translateX(-100%); + width: 0; +} + +.fullscreen-question-container{ + background-color: #fff; + width: 100%; + padding-top: 20px; + overflow: auto; +} + +.fullscreen-question{ + border-radius: 10px; + padding: 10px 20px; + box-shadow: 0px 1px 4px rgba(0,0,0,.5); + margin-top: 50px; +} + +.question-title { + font-size: 1.5rem; +} + +.buttons-container{ + width: 80%; + display: flex; + justify-content: space-between; + flex-direction: row-reverse; + margin-top: 40px; +} + +// .slide-resources { +// border-bottom: 1px solid #ccc; +// } + +.slide-resource { + padding: 5px 50px; + width: 100%; + i{ + margin-right: 10px; + } +} + +.list-cell { +} + + +.quiz-header{ + border-bottom: 2px solid #CBCBCB; + margin-bottom: 50px; +} + +.quiz-header-container{ + display: flex; + justify-content: space-between; + align-items: center; + + h1 { + width: 80%; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + } +} + +.quiz-good-answer{ + background-color: #C7EAE9; +} + +.quiz-bad-answer{ + background-color: #FEBEBE; +} + +.quiz-bad-answer label.radio-box, +.quiz-good-answer label.radio-box{ + border: none; +} + +.quiz-bad-answer label.radio-box i{ + background-color: #FD0606; + color: white; + height: 100%; + width: 100%; + font-size: 1.5rem; + display: flex; + justify-content: center; + align-items: center; + padding: 50px; +} + +.quiz-ignored-answer { + color: $text-muted; +} + + +.quiz-good-answer label.radio-box input:checked + i{ + height: 100%; + width: 100%; + background-color: #fff; + display: flex; + justify-content: center; + align-items: center; + font-size: 2.4rem; + border: 10px solid $primary; + border-radius: 50px; +} + +.quiz-answer { + border-radius: 10px; + padding: 5px 10px; + transition: all .2s ease-in-out; + display: flex; + align-items: center; + margin-bottom: 5px; +} + +.quiz-answer:not(.quiz-good-answer,.quiz-bad-answer,.quiz-ignored-answer):hover{ + background-color: #E5E5E5; + cursor: pointer; +} + +label.radio-box{ + border: 2px solid #CBCBCB; + background-color: #fff; + width: 50px; + height: 50px; + display: inline-flex; + border-radius: 50%; + overflow: hidden; + justify-content: center; + align-items: center; + margin: 0; + margin-right: 20px; + font-size: 1.2rem; + outline: none; +} + +.quiz-answer:not(.quiz-good-answer,.quiz-bad-answer,.quiz-ignored-answer) label.radio-box:hover{ + cursor: pointer; +} + +label.radio-box input{ + display:none; + outline: none; + border: none; +} + + +label.radio-box span{ + color: inherit; + padding: 20px; +} + +label.radio-box input:checked + span{ + background-color: $primary; + color: #fff; +} + +label.radio-box input:checked{ + background-color: #C7EAE9; +} + +.quiz-question { + border-bottom: 2px solid #CBCBCB; + padding-bottom: 20px; + margin-bottom: 20px; +} + +.quiz-question-answers { + list-style: none; + margin: 0; + margin-top: 20px; + padding: 0; + font-size: 1rem; +} + +.submit-quiz { + margin-top: 20px; +} + + +.quiz-modal-background{ + position: absolute; + top: 0; + left: 0; + background-color: rgba(0,0,0,.5); + width: 100%; + height: 100%; +} + +.quiz-modal-container{ + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; +} + +.quiz-modal{ + position: absolute; + width: 30%; + height: 500px; + background-color: #fff; + top: 50%; + left: 50%; + transform: translateX(-50%) translateY(-50%); + border-radius: 10px; + z-index: 2000; +} +.quiz-points-container{ + font-size: 1.2rem; +} + +.quiz-points { + background-color: #C4C4C4; + padding: 0 4px ; + border-radius: 10px; + color: #fff; +} + + +.quiz-points-won{ + background-color: $primary; +} + +.webpage-container{ + background-color: #fff; + width: 100%; +} + +.webpage-content { + background-color: #fff; + height: 100%; +} + + + + diff --git a/addons/website_slides/static/src/scss/slide_slide.scss b/addons/website_slides/static/src/scss/slide_slide.scss new file mode 100644 index 00000000000..d99aaea101a --- /dev/null +++ b/addons/website_slides/static/src/scss/slide_slide.scss @@ -0,0 +1,156 @@ +.slide-header { + background-color: #62495B; + height: 200px; +} + +.slide-header-container { + width: 1000px; + margin: 0 auto; + height: 100%; +} + +.slide-header-box{ + display: flex; + height: 100%; + width: 50%; + flex-direction: column; + justify-content: flex-end; + padding-bottom: 50px; + + a { + color: #fff; + font-size: 2.5rem; + text-decoration: none !important; + } +} + + +.slide-content-list { + position: absolute; + top: 140px; + left: 80px; + background-color: #fff; + height: 500px; + z-index: 200; +} + +.slide-list{ + background-color: #fff; + list-style: none; + height: 100%; + padding: 0; + overflow: auto; + border: 1px solid #ccc; + border-left: none; + box-sizing: border-box; + flex: 1; + + a { + text-decoration: none; + color: $text-muted; + } + + a .active{ + background-color: #eee !important; + } +} + +.slide-content-list-section{ + border-bottom: 1px solid #fff; + color: #9B9B9B; + text-transform: uppercase; + padding: 10px 15px; +} + +.slide-content-list-slide{ + color: #666666; + padding: 10px 15px; +} + +.slide-list-cell{ + word-wrap: none; + padding: 0.5rem 1rem;; + height: 40px; + box-sizing: border-box; + display: flex; + align-items: center; + text-decoration: none !important; +} + +.slide-list-cell:hover{ + background-color: #eee; + cursor: pointer; +} + +.slide-list-cell:active{ + background-color: #eee; + cursor: pointer; +} + + +.slide-content-list-header { + padding: 10px 15px; + font-size: 1.2rem; + background-color: #F7F7F7; + color: #6F6F6F; +} + +.slide-button { + color: #666; + text-decoration: none; + border:1px solid #B9B9B9; + padding: 5px 10px; + width: 50px; + text-align: center; + text-transform: uppercase; + margin: 0; + border-radius: 3px; +} + +.slide-button-fullscreen { + color: #666; + text-decoration: none; + border:1px solid #B9B9B9; + padding: 5px 10px; + text-align: center; + text-transform: uppercase; + margin: 0; + border-radius: 3px; +} + +.slide-button-done { + color: #fff; + text-transform: uppercase; + background-color: $primary; + text-decoration: none; + padding: 10px 15px; + width: 50px; + text-align: center; + border-radius: 3px; + margin: 0; +} + +.slide-title { + width: 60%; + display: flex; + align-items: center; + + h1 { + font-size: 1.5rem; + } + + .slide-points { + margin-left: 20px; + padding: 5px 10px; + background-color: #DC9D45; + border-radius: 10px; + color: #fff; + font-weight: bold; + } +} + +.slide-documentation-sidebar { + position: absolute; + top: 300px; + left: 30px; +} \ No newline at end of file diff --git a/addons/website_slides/static/src/scss/slides_slide_fullscreen.scss b/addons/website_slides/static/src/scss/slides_slide_fullscreen.scss new file mode 100644 index 00000000000..de5493d2f7e --- /dev/null +++ b/addons/website_slides/static/src/scss/slides_slide_fullscreen.scss @@ -0,0 +1,303 @@ +.o_wslides { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + z-index: 2000; + + .o_wslides_slide_fullscreen_header { + align-items: center; + background-color: #313A44; + color: #AEB0B2; + display: flex; + height: 50px; + justify-content: space-between; + width: 100%; + + .o_wslides_fullscreen_slide_title { + height: 100%; + display: flex; + align-items: center; + padding: 0 20px; + color: #D5D7D9; + font-weight: bold; + + i.fa-flag-checkered{ + color: #D39B4B; + margin-right: 10px; + } + } + + div { + display: flex; + height: 100%; + + a { + align-items: center; + background-color: #1F242C; + color: inherit; + display: flex; + font-size: 0.8rem; + font-weight: bold; + height: 100%; + justify-content: center; + padding: 0 20px; + text-decoration: none !important; + text-transform: uppercase; + + i { + align-items: center; + display: flex; + height: 100%; + margin: 0; + margin-right: 5px; + } + } + + a:hover, + a.active{ + background-color: #313A44; + color: #fff; + } + } + } + + .o_wslides_fullscreen_container { + display: flex; + align-items: stretch; + height: 100%; + position: relative; + background-color: #313A44; + } + + .o_wslides_fullscreen_sidebar_hidden{ + transform: translateX(-100%); + } + + .o_wslides_fullscreen_sidebar{ + width: 400px; + min-width: 400px; + background-color: #313A44; + color: #fff; + transition: all .2s ease-in; + overflow: auto; + + a { + text-decoration: none !important; + color: inherit; + } + + ul { + margin: 0; + padding: 0; + list-style: none; + } + + li { + padding-left: 20px; + } + + .o_wslides_fullscreen_sidebar_header{ + height: 100px; + padding: 20px 0; + display: flex; + flex-direction: column; + + .o_wslides_slide_fullscreen_progress_box{ + display: flex; + align-items: center; + font-size: .9rem; + .o_wslides_fullscreen_sidebar_progressbar{ + height: 5px; + border-radius: 10px; + width: 70%; + background-color: rgba(0,0,0,.3); + margin-right: 40px; + overflow: hidden; + + } + + .o_wslides_fullscreen_sidebar_progress_gauge { + background-color: $primary; + height: 100%; + width: 0; + transition: all 1.5s ease-in; + } + + span { + color: #BABDC0; + } + + } + + a { + font-size: 1.5rem; + font-weight: bold; + } + + } + + .o_wslides_fullscreen_sidebar_section { + margin-bottom: 20px; + background-color: #283038; + padding: 10px 0; + + } + + .o_wslides_fullscreen_sidebar_section_tab { + color: #656B72; + font-size: .8rem; + text-transform: uppercase; + font-weight: bold; + padding: 5px 0 5px 15px; + } + + .o_wslides_fullscreen_sidebar_section_slides{ + padding: 0; + margin-bottom: 15px; + + + + li { + margin: 0; + padding: 0; + border-left: 2px solid transparent; + display: flex; + + a { + flex: 1; + } + } + + .o_wslides_top_line, + .o_wslides_bottom_line{ + width: 1px; + display: flex; + flex-grow: 1; + background-color: #787D82; + } + + li:first-child .o_wslides_top_line{ + background-color: transparent; + } + + li:last-child .o_wslides_bottom_line{ + background-color: transparent; + } + + li:hover, + li.active { + border-left: 2px solid $primary; + cursor: pointer; + background-color: #1D2228; + } + + .o_wslides_fullscreen_sidebar_slide_tab { + padding:5px 0 5px 15px; + font-size: .8rem; + color: #8A8F93; + font-weight: bold; + flex: 1; + + } + + .o_wslides_fullscreen_sidebar_slide_tab.active{ + color: #fff; + } + + .o_wslides_fullscreen_slide_tab_line{ + width: 15px; + padding: 0; + margin: 0; + margin-left: 10px; + display: flex; + flex-direction: column; + align-items: center; + + i { + z-index: 100; + padding: 0; + margin: 0; + } + + i.o_wslides_slide_completed{ + color: #00A04A; + border-radius: 50%; + overflow: hidden; + background-color: #fff; + height: 12px; + width: 12px; + display: flex; + justify-content: center; + align-items: center; + border: 1px solid #00A04A; + } + + .o_wslides_full_line{ + width: 1px; + height: 100%; + background-color: #787D82; + } + } + + .o_wslides_slide_link { + color: #098586; + font-size: .8rem; + margin-left: 15px; + flex: 1; + + i { + margin-right: 10px; + } + } + + .o_wslides_slide_quiz { + font-size: .8rem; + margin-left: 15px; + color: #8A8F93; + flex: 1; + i { + color: #D39B4B; + margin-right: 10px; + } + } + } + + + } + + .o_wslides_fullscreen_player { + display: flex; + align-items: stretch; + max-width: calc(100% - 400px); + height: 100%; + background-color: #283038; + transition: all .2s ease-in; + position: absolute; + top: 0; + right: 0; + height: 100%; + width: 80%; + + iframe { + height: 100%; + width: 100%; + } + } + + .o_wslides_fullscreen_webpage { + background-color: #fff; + width: 100%; + height: 100%; + overflow-y: scroll; + padding: 20px 0; + } + + .o_wslides_fullscreen_player_no_sidebar { + max-width: 100%; + width: 100%; + } + + +} \ No newline at end of file diff --git a/addons/website_slides/static/src/scss/website_slides_quiz.scss b/addons/website_slides/static/src/scss/website_slides_quiz.scss new file mode 100644 index 00000000000..9fe4438ee57 --- /dev/null +++ b/addons/website_slides/static/src/scss/website_slides_quiz.scss @@ -0,0 +1,237 @@ + + .o_wslides_fullscreen_quiz { + background-color: #F6F6F6; + width: 100%; + height: 100%; + overflow: scroll; + overflow-x: hidden; + padding-bottom: 50px; + + } + + .o_wslides_quiz_question { + margin-top: 50px; + width: 100%; + .o_wslides_quiz_question_title { + font-size: 1.5rem; + margin-bottom: 20px; + .o_wslides_quiz_question_title_number { + color: #8C8C8C; + font-size: 1.3rem; + font-weight: bold; + } + + .o_wslides_quiz_question_title_text { + margin-left: 2px; + color: #2F2F2F; + } + } + + .o_wslides_quiz_question_answers{ + background-color: #fff; + list-style: none; + padding: 0; + + .o_wslides_quiz_answer { + padding: 10px 20px; + border-bottom: 1px solid #B6B6B6; + font-size: 1.2rem; + display: flex; + align-items: center; + + label.o_wslides_quiz_radio_box{ + background-color: #f6f6f6; + width: 35px; + height: 35px; + display: inline-flex; + border-radius: 50%; + overflow: hidden; + justify-content: center; + align-items: center; + margin: 0; + margin-right: 20px; + font-size: 1.2rem; + outline: none; + border: none; + padding: 0; + } + + label.o_wslides_quiz_radio_box input{ + display:none; + outline: none; + border: none; + } + + + label.o_wslides_quiz_radio_box span{ + width: 18px; + height: 18px; + margin: 0; + } + + label.o_wslides_quiz_radio_box input:checked + span{ + background-color: $primary; + border-radius: 50%; + overflow: hidden; + } + + label.o_wslides_quiz_radio_box input:checked{ + background-color: #C7EAE9; + } + } + + .o_wslides_quiz_answer:not(.o_wslides_quiz_good_answer,.o_wslides_quiz_bad_answer,.o_wslides_quiz_ignored_answer):hover { + cursor: pointer; + } + + .o_wslides_quiz_good_answer{ + border: 2px solid #28A745; + label.o_wslides_quiz_radio_box input:checked + i{ + height: 100%; + width: 100%; + background-color: #fff; + color: #28A745; + display: flex; + justify-content: center; + align-items: center; + font-size: 1.4rem; + border: 10px solid #28A745; + border-radius: 50px; + } + } + + .o_wslides_quiz_bad_answer{ + border: 2px solid #A94442; + + label.o_wslides_quiz_radio_box i{ + background-color: #A94442; + color: white; + height: 100%; + width: 100%; + font-size: 1rem; + display: flex; + justify-content: center; + align-items: center; + } + } + + + .quiz-ignored-answer { + color: $text-muted; + } + } + } + + .o_wslides_quiz_modal_background{ + position: absolute; + top: 0; + left: 0; + background-color: rgba(0,0,0,.2); + width: 100%; + height: 100%; + + } + + .o_wslides_quiz_success_modal { + display: flex; + position: absolute; + width: 40%; + max-width: 600px; + height: 450px; + background-color: #fff; + top: 50%; + left: 50%; + transform: translateX(-50%) translateY(-50%); + z-index: 2000; + } + + .o_wslides_quiz_success_modal_left_panel { + width: 50%; + background-color: #855B79; + + } + + .o_wslides_quiz_success_modal_right_panel{ + width: 50%; + display: flex; + flex-direction: column; + padding: 10px 20px 10px 50px; + position: relative; + + .o_wslides_quiz_success_modal_close { + position: absolute; + top: 10px; + right: 15px; + text-transform: uppercase; + color: #787878; + font-size: 1.2rem; + } + + .o_wslides_quiz_success_modal_close:hover{ + cursor: pointer; + } + + h1 { + color: #21272B; + font-weight: bold; + font-size: 2.3rem; + padding: 20px 0; + letter-spacing: 1px; + } + + .o_wslides_quiz_success_progress_bar{ + height: 10px; + width: 100%; + background-color: rgba(0,0,0,.1); + + .o_wslides_quiz_success_progress_gauge{ + width: 75%; + height: 100%; + background-color: #01ADAB; + } + } + + .o_wslides_quiz_success_progress_bounds{ + display: flex; + justify-content: space-between; + } + + .o_wslides_quiz_success_progress_bounds{ + font-weight: bold; + span:first-child { + color: #019F9D; + } + span:last-child{ + color: #787878; + } + } + + .o_wslides_quiz_success_reward { + height: 80px; + width: 100%; + border: 1px dashed #ccc; + margin-top: 70px; + } + + .o_wslides_quiz_success_button{ + flex: 1; + display: flex; + justify-content: flex-end; + align-items: flex-end; + + a { + color: #787878; + text-transform: uppercase; + background-color: #fff; + border-radius: 3px; + border: 1px solid #C4C4C4; + font-weight: bold; + padding: 5px 15px; + text-align: center; + } + + a:hover { + cursor: pointer; + } + } + } \ No newline at end of file diff --git a/addons/website_slides/static/src/xml/website_slides_fullscreen.xml b/addons/website_slides/static/src/xml/website_slides_fullscreen.xml new file mode 100644 index 00000000000..a9aff0d2ec7 --- /dev/null +++ b/addons/website_slides/static/src/xml/website_slides_fullscreen.xml @@ -0,0 +1,111 @@ + + +
+ +
+
+ +
+
+ Slide image +
+
+
+ +
+ + + +
+
+
+
+
+
+
+ + + +
+ + + +
+
+
+ . + +
+
    + +
  • + + +
  • + + +
+
+
+ + +
+
+ + + +
+ + +
+
+ + +
+
+
+
+
+
+

Amazing!

+
+

You gained !

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