diff --git a/mx_elearning_plus/__init__.py b/mx_elearning_plus/__init__.py new file mode 100644 index 0000000..3ea0f0c --- /dev/null +++ b/mx_elearning_plus/__init__.py @@ -0,0 +1,5 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from . import controllers +from . import models \ No newline at end of file diff --git a/mx_elearning_plus/__manifest__.py b/mx_elearning_plus/__manifest__.py new file mode 100644 index 0000000..90d961e --- /dev/null +++ b/mx_elearning_plus/__manifest__.py @@ -0,0 +1,38 @@ +# -*- coding: utf-8 -*- +{ + 'name': 'eLearning Plus', + 'version': '18.0.0.2', + 'sequence': 10, + 'summary': 'Added extra features to enhance learning.', + 'website': 'https://www.manprax.com', + 'author': 'ManpraX Software LLP', + 'category': 'Website/eLearning', + 'description': """ +Extended feature of elearning. +""", + 'depends': [ + 'website_slides', + ], + 'data': [ + 'views/slide_view.xml', + 'views/website_slides_templates_course.xml', + ], + 'assets': { + 'web.assets_backend': [ + ], + 'web.assets_frontend': [ + 'mx_elearning_plus/static/src/js/slides_course.js', + 'mx_elearning_plus/static/src/js/slides_course_extend.js', + 'mx_elearning_plus/static/src/js/slides_course_rating_fullscreen.js', + 'mx_elearning_plus/static/src/js/slide_comment_composer_fullscreen.js', + 'mx_elearning_plus/static/src/xml/comment_composer.xml', + 'mx_elearning_plus/static/src/xml/comment_button_composer_extend.xml', + ], + }, + 'demo': [], + 'qweb': [], + 'images': ["static/description/images/app_banner_plus.png"], + 'installable': True, + 'application': True, + 'license': 'AGPL-3', +} \ No newline at end of file diff --git a/mx_elearning_plus/controllers/__init__.py b/mx_elearning_plus/controllers/__init__.py new file mode 100644 index 0000000..7fc0cd7 --- /dev/null +++ b/mx_elearning_plus/controllers/__init__.py @@ -0,0 +1,3 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. +from . import main diff --git a/mx_elearning_plus/controllers/main.py b/mx_elearning_plus/controllers/main.py new file mode 100644 index 0000000..610e256 --- /dev/null +++ b/mx_elearning_plus/controllers/main.py @@ -0,0 +1,73 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. +from odoo import http, tools, _ +from odoo.http import request +from odoo.tools import plaintext2html +from odoo.addons.website_slides.controllers.main import WebsiteSlides + + +class SlideController(http.Controller): + + @http.route(['/website/publish/slide'], type='json', auth="user", website=True) + def publish(self, id): + slide_id = request.env['slide.slide'].browse(id) + return bool(slide_id.website_published) + + @http.route(['/slides/slide/mx/like'], type='json', auth="public", website=True) + def slide_like_dislike(self, slide_id): + slide = request.env['slide.slide'].browse(slide_id) + return { + 'user_vote': slide.user_vote, + 'likes': tools.misc.format_decimalized_number(slide.likes), + 'dislikes': tools.misc.format_decimalized_number(slide.dislikes), + } + + def _portal_post_has_content(self, res_model, res_id, message, attachment_ids=None, **kw): + """ Tells if we can effectively post on the model based on content. """ + return bool(message) or bool(attachment_ids) + + @http.route(['/mail/slide/comment'], type='json', methods=['POST'], auth='public', website=True) + def portal_chatter_post(self, res_model, res_id, message, attachment_ids=None, attachment_tokens=None, **kw): + """Create a new `mail.message` with the given `message` and/or `attachment_ids` and return new message values.""" + if not self._portal_post_has_content(res_model, res_id, message, + attachment_ids=attachment_ids, attachment_tokens=attachment_tokens, + **kw): + return + res_id = int(res_id) + result = {'default_message': message} + # message is received in plaintext and saved in html + if message: + message = plaintext2html(message) + vals = ({ + 'email_from': request.env.user.email_formatted, + 'author_id': request.env.user.partner_id.id, + 'message_type':'comment', + 'body':message if message else '', + 'subtype_id': 1, + 'model':res_model, + 'res_id': res_id, + # 'attachment_ids': False, + 'record_name': (request.env[str(res_model)].browse(res_id)).name + }) + message=request.env['mail.message'].sudo().create(vals) + result.update({'default_message_id': message.id}) + + if attachment_ids: + # sudo write the attachment to bypass the read access + # verification in mail message + record = request.env[res_model].browse(res_id) + message_values = {'res_id': res_id, 'model': res_model} + attachments = record._message_post_process_attachments([], attachment_ids, message_values) + + if attachments.get('attachment_ids'): + message.sudo().write(attachments) + + result.update({'default_attachment_ids': message.attachment_ids.sudo().read(['id', 'name', 'mimetype', 'file_size', 'access_token'])}) + return result + +class WebsiteSlideController(WebsiteSlides): + + @http.route('/slides/slide/like', type='json', auth="public", website=True) + def slide_like(self, slide_id, upvote): + res = super(WebsiteSlideController, self).slide_like(slide_id, upvote) + return res \ No newline at end of file diff --git a/mx_elearning_plus/models/__init__.py b/mx_elearning_plus/models/__init__.py new file mode 100644 index 0000000..98006fb --- /dev/null +++ b/mx_elearning_plus/models/__init__.py @@ -0,0 +1,4 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from . import slide_slide \ No newline at end of file diff --git a/mx_elearning_plus/models/slide_slide.py b/mx_elearning_plus/models/slide_slide.py new file mode 100644 index 0000000..d61d23a --- /dev/null +++ b/mx_elearning_plus/models/slide_slide.py @@ -0,0 +1,161 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. +import logging +import re +import requests +from werkzeug import urls +from markupsafe import Markup +from odoo import models, fields, api, _ + +_logger = logging.getLogger(__name__) + + +class Slide(models.Model): + _inherit = 'slide.slide' + + # is_hide = fields.Boolean(string="Allow Hide") + + def action_publish(self): + if not self.id: + return { + 'type': 'ir.actions.client', + 'tag': 'display_notification', + 'params': { + 'type': 'warning', + 'message': _("Please save the course to publish it.") + } + } + else: + self.is_published = True + + def action_unpublish(self): + self.is_published = False + + class Channel(models.Model): + _inherit = 'slide.channel' + + description_short = fields.Html('Short Description', help="The description that is displayed on the course card") + description = fields.Html('Description', help="The description that is displayed on top of the course page, just below the title") + YOUTUBE_VIDEO_ID_REGEX_PRO = r'^.*(youtu.be\/|v\/|u\/\w\/|embed\/|watch\?v=|&v=)([^#&?]*).*' + GOOGLE_DRIVE_DOCUMENT_ID_REGEX_PRO = r'(^https:\/\/docs.google.com|^https:\/\/drive.google.com).*\/d\/([^\/]*)' + VIMEO_VIDEO_ID_REGEX_PRO = r'\/\/(player.)?vimeo.com\/(?:[a-z]*\/)*([0-9]{6,11})\/?([0-9a-z]{6,11})?[?]?.*' + intro_video_type = fields.Selection([ + ('youtube_video', 'YouTube Video'), + ('google_drive_video', 'Google Drive Video'), + ('vimeo_video', 'Vimeo Video')], + string="Slide Type", compute='_compute_intro_video_type', store=True, readonly=False, + help="Subtype of the video category, allows more precision on the actual file type / source type.") + intro_url = fields.Char('External URL', help="URL of the Google Drive file or URL of the YouTube video") + intro_video_url = fields.Char('Introduction Video Link', related='intro_url', readonly=False, + help="Link of the video (we support YouTube, Google Drive and Vimeo as sources)") + intro_video_source_type = fields.Selection([ + ('youtube', 'YouTube'), + ('google_drive', 'Google Drive'), + ('vimeo', 'Vimeo')], + string='Video Source', compute="_compute_intro_video_source_type") + video_image_1920 = fields.Image(store=True, readonly=False) + intro_youtube_id = fields.Char('Video YouTube ID', compute='_compute_intro_youtube_id') + intro_vimeo_id = fields.Char('Video Vimeo ID', compute='_compute_intro_vimeo_id') + intro_google_drive_id = fields.Char('Google Drive ID of the external URL', compute='_compute_intro_google_drive_id') + video_embed_code = fields.Html('Embed Code', readonly=True, compute='_compute_video_embed_code', sanitize=False) + + @api.depends('intro_video_url', 'intro_google_drive_id', 'intro_video_source_type', 'intro_youtube_id') + def _compute_video_embed_code(self): + for course in self: + video_embed_code = False + if course.intro_video_url : + if course.intro_video_source_type == 'youtube': + query_params = urls.url_parse(course.intro_video_url).query + query_params = query_params + '&theme=light' if query_params else 'theme=light' + video_embed_code = Markup('') % (course.intro_youtube_id, query_params) + elif course.intro_video_source_type == 'google_drive': + video_embed_code = Markup('') % (course.intro_google_drive_id) + elif course.intro_video_source_type == 'vimeo': + if '/' in course.intro_vimeo_id: + # in case of privacy 'with URL only', vimeo adds a token after the video ID + # the embed url needs to receive that token as a "h" parameter + [vimeo_id, vimeo_token] = course.intro_vimeo_id.split('/') + video_embed_code = Markup(""" + """) % ( + vimeo_id, vimeo_token) + else: + video_embed_code = Markup(""" + """) % (course.intro_vimeo_id) + course.video_embed_code = video_embed_code + + + @api.depends('intro_video_type', 'intro_video_source_type') + def _compute_intro_video_type(self): + """ For 'local content' or specific slide categories, the slide type is directly derived + from the slide category. + + For external content, the slide type is determined from the metadata and the mime_type. + (See #_fetch_google_drive_metadata() for more details).""" + for course in self: + if course.intro_video_url and course.intro_video_source_type == 'youtube': + course.intro_video_type = 'youtube_video' + elif course.intro_video_url and course.intro_video_source_type == 'google_drive': + course.intro_video_type = 'google_drive_video' + elif course.intro_video_url and course.intro_video_source_type == 'vimeo': + course.intro_video_type = 'vimeo_video' + else: + course.intro_video_type = False + + @api.depends('intro_video_url') + def _compute_intro_video_source_type(self): + for course in self: + intro_video_source_type = False + youtube_match = re.match(self.YOUTUBE_VIDEO_ID_REGEX_PRO, course.intro_video_url) if course.intro_video_url else False + if youtube_match and len(youtube_match.groups()) == 2 and len(youtube_match.group(2)) == 11: + intro_video_source_type = 'youtube' + if course.intro_video_url and not intro_video_source_type and re.match(self.GOOGLE_DRIVE_DOCUMENT_ID_REGEX_PRO, course.intro_video_url): + intro_video_source_type = 'google_drive' + vimeo_match = re.search(self.VIMEO_VIDEO_ID_REGEX_PRO, course.intro_video_url) if course.intro_video_url else False + if not intro_video_source_type and vimeo_match and len(vimeo_match.groups()) == 3: + intro_video_source_type = 'vimeo' + + course.intro_video_source_type = intro_video_source_type + + @api.depends('intro_video_url', 'intro_video_source_type') + def _compute_intro_youtube_id(self): + for course in self: + if course.intro_video_url and course.intro_video_source_type == 'youtube': + match = re.match(self.YOUTUBE_VIDEO_ID_REGEX_PRO, course.intro_video_url) + if match and len(match.groups()) == 2 and len(match.group(2)) == 11: + course.intro_youtube_id = match.group(2) + else: + course.intro_youtube_id = False + else: + course.intro_youtube_id = False + + @api.depends('intro_video_url', 'intro_video_source_type') + def _compute_intro_vimeo_id(self): + for course in self: + if course.intro_video_url and course.intro_video_source_type == 'vimeo': + match = re.search(self.VIMEO_VIDEO_ID_REGEX_PRO, course.intro_video_url) + if match and len(match.groups()) == 3: + if match.group(3): + # in case of privacy 'with URL only', vimeo adds a token after the video ID + # the share url is then 'vimeo_id/token' + # the token will be captured in the third group of the regex (if any) + course.intro_vimeo_id = '%s/%s' % (match.group(2), match.group(3)) + else: + # regular video, we just capture the vimeo_id + course.intro_vimeo_id = match.group(2) + else: + course.intro_vimeo_id = False + + @api.depends('intro_video_url') + def _compute_intro_google_drive_id(self): + """ Extracts the Google Drive ID from the url based on the slide category. """ + for course in self: + url = course.intro_video_url + intro_google_drive_id = False + if url: + match = re.match(self.GOOGLE_DRIVE_DOCUMENT_ID_REGEX_PRO, url) + if match and len(match.groups()) == 2: + intro_google_drive_id = match.group(2) + + course.intro_google_drive_id = intro_google_drive_id \ No newline at end of file diff --git a/mx_elearning_plus/static/description/icon.png b/mx_elearning_plus/static/description/icon.png new file mode 100644 index 0000000..66107c4 Binary files /dev/null and b/mx_elearning_plus/static/description/icon.png differ diff --git a/mx_elearning_plus/static/description/images/app_banner_plus.png b/mx_elearning_plus/static/description/images/app_banner_plus.png new file mode 100644 index 0000000..20d775f Binary files /dev/null and b/mx_elearning_plus/static/description/images/app_banner_plus.png differ diff --git a/mx_elearning_plus/static/description/images/manprax.png b/mx_elearning_plus/static/description/images/manprax.png new file mode 100644 index 0000000..be16ef9 Binary files /dev/null and b/mx_elearning_plus/static/description/images/manprax.png differ diff --git a/mx_elearning_plus/static/description/images/screen1.png b/mx_elearning_plus/static/description/images/screen1.png new file mode 100644 index 0000000..0897e9b Binary files /dev/null and b/mx_elearning_plus/static/description/images/screen1.png differ diff --git a/mx_elearning_plus/static/description/images/screen10.png b/mx_elearning_plus/static/description/images/screen10.png new file mode 100644 index 0000000..1b3f8e3 Binary files /dev/null and b/mx_elearning_plus/static/description/images/screen10.png differ diff --git a/mx_elearning_plus/static/description/images/screen11.png b/mx_elearning_plus/static/description/images/screen11.png new file mode 100644 index 0000000..87ec3b4 Binary files /dev/null and b/mx_elearning_plus/static/description/images/screen11.png differ diff --git a/mx_elearning_plus/static/description/images/screen2.png b/mx_elearning_plus/static/description/images/screen2.png new file mode 100644 index 0000000..701ad50 Binary files /dev/null and b/mx_elearning_plus/static/description/images/screen2.png differ diff --git a/mx_elearning_plus/static/description/images/screen3.png b/mx_elearning_plus/static/description/images/screen3.png new file mode 100644 index 0000000..00e38a5 Binary files /dev/null and b/mx_elearning_plus/static/description/images/screen3.png differ diff --git a/mx_elearning_plus/static/description/images/screen4.png b/mx_elearning_plus/static/description/images/screen4.png new file mode 100644 index 0000000..0fc36d1 Binary files /dev/null and b/mx_elearning_plus/static/description/images/screen4.png differ diff --git a/mx_elearning_plus/static/description/images/screen5.png b/mx_elearning_plus/static/description/images/screen5.png new file mode 100644 index 0000000..a22a337 Binary files /dev/null and b/mx_elearning_plus/static/description/images/screen5.png differ diff --git a/mx_elearning_plus/static/description/images/screen6.png b/mx_elearning_plus/static/description/images/screen6.png new file mode 100644 index 0000000..fc7b015 Binary files /dev/null and b/mx_elearning_plus/static/description/images/screen6.png differ diff --git a/mx_elearning_plus/static/description/images/screen7.png b/mx_elearning_plus/static/description/images/screen7.png new file mode 100644 index 0000000..81ee085 Binary files /dev/null and b/mx_elearning_plus/static/description/images/screen7.png differ diff --git a/mx_elearning_plus/static/description/images/screen8.png b/mx_elearning_plus/static/description/images/screen8.png new file mode 100644 index 0000000..3ea0f03 Binary files /dev/null and b/mx_elearning_plus/static/description/images/screen8.png differ diff --git a/mx_elearning_plus/static/description/images/screen9.png b/mx_elearning_plus/static/description/images/screen9.png new file mode 100644 index 0000000..d62460e Binary files /dev/null and b/mx_elearning_plus/static/description/images/screen9.png differ diff --git a/mx_elearning_plus/static/description/index.html b/mx_elearning_plus/static/description/index.html new file mode 100644 index 0000000..89b60ea --- /dev/null +++ b/mx_elearning_plus/static/description/index.html @@ -0,0 +1,211 @@ +
+ manprax-logo +
+
+
+
+

Elearning Plus

+ +
+

Key Highlights

+ +
+
+
+

Screenshots

+
+
+
+
+
+

+ + Collapse/Expand Feature in course content list. +

+
+
+
+
+
+

+ + Fix Bug of Publish/Unpublish Button on course Fullscreen. +
+

+
+
+
+
+
+

+ + Publish slides directly from backend. +
+

+
+
+
+
+
+

+ + Add Description for course in HTML Editor. +
+

+
+
+
+
+
+
+

+ + Add Youtube Video URL or Google Drive Video URL. +
+

+
+
+
+
+
+
+
+

+ + Like/Dislike option for content in fullscreen. +
+

+
+
+
+
+
+

+ + Comment option for content in fullscreen. +
+

+
+
+
+
+
+
+
+
+
+
+
+

Need Help?

+
+
+
+ + +
+
+
Visit us
+ ManpraX Software LLP +
+
+
Bite Sized Conversation
+ WhatsApp +
+
+
+
+
+
+

Enquires

+
+
+
+ + +
+
+
Business Enquiry
+ Click Here +
+
+
General Enquiry
+ Click Here +
+
+
Join the MX Team
+ Click Here +
+
+
diff --git a/mx_elearning_plus/static/src/js/slide_comment_composer_fullscreen.js b/mx_elearning_plus/static/src/js/slide_comment_composer_fullscreen.js new file mode 100644 index 0000000..951157b --- /dev/null +++ b/mx_elearning_plus/static/src/js/slide_comment_composer_fullscreen.js @@ -0,0 +1,144 @@ +/** @odoo-module **/ + +import publicWidget from '@web/legacy/js/public/public_widget'; +import portalComposer from "@portal/js/portal_composer"; +import { renderToElement } from "@web/core/utils/render"; +import { session } from "@web/session"; + +const PortalComposer = portalComposer.PortalComposer; + + +PortalComposer.include({ + /** + * @private + * @param {Event} ev + */ + async _onSubmitButtonClick(ev) { + await this._super(...arguments).then((result) => { + const $modal = this.$el.closest('#commentpopupcomposer'); + $modal.on('hidden.bs.modal', () => { + this.trigger_up('reload_comment_popup_composer', result); + }); + $modal.modal('hide'); + }); + }, + + /** + * Prepare message data for submission. + * + * @private + * @returns {Object} The data object to be sent to the backend. + */ + _prepareMessageData() { + const messageBody = this.$('textarea[name="message"]').val(); + const attachmentIds = this.attachments.map((a) => a.id); + const attachmentTokens = this.attachments.map((a) => a.access_token); + + if (this.options.is_comment_fullscreen) { + const resId = $('.o_wslides_fs_sidebar_list_item.active').data('id'); + return { + thread_model: this.options.res_model, + thread_id: resId, + post_data: { + body: messageBody, + attachment_ids: attachmentIds, + message_type: "comment", + subtype_xmlid: "mail.mt_comment", + }, + attachment_tokens: attachmentTokens, + }; + } else { + return { + thread_model: this.options.res_model, + thread_id: this.options.res_id, + post_data: { + body: messageBody, + attachment_ids: attachmentIds, + message_type: "comment", + subtype_xmlid: "mail.mt_comment", + }, + attachment_tokens: attachmentTokens, + message_id: this.options.default_message_id, + }; + } + }, + +}); +/** + * CommentPopupComposer + * + * Open a popup with the portal composer when clicking on it. + **/ +const CommentPopupComposer = publicWidget.Widget.extend({ + selector: '.o_comment_popup_composer', + custom_events: { + reload_comment_popup_composer: '_onReloadCommentPopupComposer', + }, + + willStart: function (parent) { + const def = this._super.apply(this, arguments); + const options = this.$el.data(); + this.options = Object.assign({ + 'token': false, + 'res_model': false, + 'res_id': false, + 'pid': 0, + 'csrf_token': odoo.csrf_token, + 'user_id': session.user_id, + }, options, {}); + return def; + }, + + /** + * @override + */ + start: function () { + return Promise.all([ + this._super.apply(this, arguments), + this._reloadCommentPopupComposer(), + ]); + }, + + /** + * Destroy existing commentPopup and insert new commentPopup widget + * + * @private + * @param {Object} data + */ + _reloadCommentPopupComposer: function () { + // Append the modal + const modal = renderToElement( + 'mx_elearning_plus.PopupCommentComposer', { + inline_mode: true, + widget: this, + }); + this.$('.o_comment_popup_composer_modal').html(modal); + + if (this._composer) { + this._composer.destroy(); + } + + // Instantiate the "Portal Composer" widget and insert it into the modal + this._composer = new PortalComposer(this, this.options); + return this._composer.appendTo(this.$('.o_comment_popup_composer_modal .o_portal_chatter_composer')) + }, + + //-------------------------------------------------------------------------- + // Handlers + //-------------------------------------------------------------------------- + + /** + * @private + * @param {OdooEvent} event + */ + _onReloadCommentPopupComposer: function (event) { + const data = event.data; + this.options = Object.assign(this.options, data); + + this._reloadCommentPopupComposer(); + } +}); + +publicWidget.registry.CommentPopupComposer = CommentPopupComposer; + +export default CommentPopupComposer; diff --git a/mx_elearning_plus/static/src/js/slides_course.js b/mx_elearning_plus/static/src/js/slides_course.js new file mode 100644 index 0000000..411d1b6 --- /dev/null +++ b/mx_elearning_plus/static/src/js/slides_course.js @@ -0,0 +1,101 @@ +/** @odoo-module **/ + +import Fullscreen from "@website_slides/js/slides_course_fullscreen_player"; +import { _t } from "@web/core/l10n/translation"; +import { rpc } from "@web/core/network/rpc"; + +Fullscreen.include({ + events: Object.assign({}, Fullscreen.prototype.events, { + 'click .o_btn_comment_unable': '_onClickCommentUnable', + 'click .o_btn_comment_public': '_onClickCommentPublic', + 'click .o_btn_no_comment_karma': '_onClickNoCommentKarma', + 'click .o_btn_comment_karma': '_onClickCommentKarma', + }), + + _onClickCommentUnable: function () { + var message = ('Commenting is not enabled on this course.'); + this.displayNotification({ + type: 'warning', + message: message, + sticky: true + }); + }, + + _onClickCommentPublic: function () { + var message = ('There are no comments for now. Join Course to be the first to leave a comment.'); + this.displayNotification({ + type: 'warning', + message: message, + sticky: true + }); + }, + + _onClickNoCommentKarma: function () { + var message = ('There are no comments for now. Earn more Karma to be the first to leave a comment.'); + this.displayNotification({ + type: 'warning', + message: message, + sticky: true + }); + }, + + _onClickCommentKarma: function () { + var message = ('Earn more Karma to leave a comment.'); + this.displayNotification({ + type: 'warning', + message: message, + sticky: true + }); + }, + + _renderSlide: function () { + this._super.apply(this, arguments); + const slide = this._slideValue; + rpc('/slides/slide/mx/like', { + slide_id: slide.id, + }).then(function (data) { + if (! data.error) { + const $likesBtn = self.$('span.o_wslides_js_slide_like_up_mx'); + const $likesIcon = $likesBtn.find('i.fa'); + const $dislikesBtn = self.$('span.o_wslides_js_slide_like_down_mx'); + const $dislikesIcon = $dislikesBtn.find('i.fa'); + + // update 'thumbs-up' button with latest state + $likesBtn.data('user-vote', data.user_vote); + $likesBtn.find('span').text(data.likes); + $likesIcon.toggleClass("fa-thumbs-up", data.user_vote === 1); + $likesIcon.toggleClass("fa-thumbs-o-up", data.user_vote !== 1); + // update 'thumbs-down' button with latest state + $dislikesBtn.data('user-vote', data.user_vote); + $dislikesBtn.find('span').text(data.dislikes); + $dislikesIcon.toggleClass("fa-thumbs-down", data.user_vote === -1); + $dislikesIcon.toggleClass("fa-thumbs-o-down", data.user_vote !== -1); + $('.o_wslides_js_slide_like_up_mx').data('slide-id', slide.id); + $('.o_wslides_js_slide_like_down_mx').data('slide-id', slide.id); + $('.o_wslides_js_slide_like_up_mx span').text(data.likes); + $('.o_wslides_js_slide_like_down_mx span').text(data.dislikes); + } + }); + }, + + _onChangeSlideRequest: function (ev){ + this._super.apply(this, arguments); + var slideData = ev.data; + var $data = this.$el.prevObject.find('.js_publish_btn:visible').parents(".js_publish_management:first"); + rpc('/website/publish/slide', { + id: slideData.id, + }).then(function (result) { + if (result){ + $data.removeClass("css_unpublished"); + $data.addClass("css_published"); + $data.find('input').prop("checked", result); + $data.parents("[data-publish]").attr("data-publish", +result ? 'on' : 'off'); + }else{ + $data.removeClass("css_published"); + $data.addClass("css_unpublished"); + $data.find('input').prop("checked", result); + $data.parents("[data-publish]").attr("data-publish", +result ? 'on' : 'off'); + } + }) + } +}); diff --git a/mx_elearning_plus/static/src/js/slides_course_extend.js b/mx_elearning_plus/static/src/js/slides_course_extend.js new file mode 100644 index 0000000..b17c234 --- /dev/null +++ b/mx_elearning_plus/static/src/js/slides_course_extend.js @@ -0,0 +1,26 @@ +/** @odoo-module **/ + +import publicWidget from '@web/legacy/js/public/public_widget'; + + +publicWidget.registry.websiteCourseExtended = publicWidget.Widget.extend({ + selector: '.o_course_extended', + + /** + * @override + */ + start: function () { + $('.o_course_extended').on('click','#collapse_div',function() { + var id = this.dataset.target.split('-')[1] + if($('#slide-'+id).is(":visible")) { + $('#slide-'+id).hide() + $(this).children().first().children().removeClass('fa-minus'); + $(this).children().first().children().addClass('fa-plus'); + }else { + $('#slide-'+id).show() + $(this).children().first().children().removeClass('fa-plus'); + $(this).children().first().children().addClass('fa-minus'); + } + }); + } +}) diff --git a/mx_elearning_plus/static/src/js/slides_course_rating_fullscreen.js b/mx_elearning_plus/static/src/js/slides_course_rating_fullscreen.js new file mode 100644 index 0000000..96a99f1 --- /dev/null +++ b/mx_elearning_plus/static/src/js/slides_course_rating_fullscreen.js @@ -0,0 +1,118 @@ +/** @odoo-module **/ + +import publicWidget from '@web/legacy/js/public/public_widget'; +import { rpc } from "@web/core/network/rpc"; +import { sprintf } from '@web/core/utils/strings'; +import { _t } from "@web/core/l10n/translation"; +import '@website_slides/js/slides'; + +var FullscreenSlideLikeWidget = publicWidget.Widget.extend({ + events: { + 'click .o_wslides_js_slide_like_up_mx': '_onClickUpMx', + 'click .o_wslides_js_slide_like_down_mx': '_onClickDownMx', + }, + + //-------------------------------------------------------------------------- + // Private + //-------------------------------------------------------------------------- + + /** + * @private + * @param {Object} $el + * @param {String} message + */ + _popoverAlert: function ($el, message) { + $el.popover({ + trigger: 'focus', + delay: {'hide': 300}, + placement: 'bottom', + container: 'body', + html: true, + content: function () { + return message; + } + }).popover('show'); + }, + + //-------------------------------------------------------------------------- + // Handlers + //-------------------------------------------------------------------------- + + /** + * @private + */ + _onClickMx: function (slideId, voteType) { + var self = this; + rpc('/slides/slide/like', { + slide_id: slideId, + upvote: voteType === 'like', + }).then(function (data) { + if (! data.error) { + const $likesBtn = self.$('span.o_wslides_js_slide_like_up_mx'); + const $likesIcon = $likesBtn.find('i.fa'); + const $dislikesBtn = self.$('span.o_wslides_js_slide_like_down_mx'); + const $dislikesIcon = $dislikesBtn.find('i.fa'); + + // update 'thumbs-up' button with latest state + $likesBtn.data('user-vote', data.user_vote); + $likesBtn.find('span').text(data.likes); + $likesIcon.toggleClass("fa-thumbs-up", data.user_vote === 1); + $likesIcon.toggleClass("fa-thumbs-o-up", data.user_vote !== 1); + // update 'thumbs-down' button with latest state + $dislikesBtn.data('user-vote', data.user_vote); + $dislikesBtn.find('span').text(data.dislikes); + $dislikesIcon.toggleClass("fa-thumbs-down", data.user_vote === -1); + $dislikesIcon.toggleClass("fa-thumbs-o-down", data.user_vote !== -1); + } else { + if (data.error === 'public_user') { + const message = data.error_signup_allowed ? + _t('Please login or create an account to vote for this lesson') : + _t('Please login to vote for this lesson'); + self._popoverAlert(self.$el, sprintf(message, document.URL, document.URL)); + } else if (data.error === 'slide_access') { + self._popoverAlert(self.$el, _t('You don\'t have access to this lesson')); + } else if (data.error === 'channel_membership_required') { + self._popoverAlert(self.$el, _t('You must be member of this course to vote')); + } else if (data.error === 'channel_comment_disabled') { + self._popoverAlert(self.$el, _t('Votes and comments are disabled for this course')); + } else if (data.error === 'channel_karma_required') { + self._popoverAlert(self.$el, _t('You don\'t have enough karma to vote')); + } else { + self._popoverAlert(self.$el, _t('Unknown error')); + } + } + }); + }, + + _onClickUpMx: function (ev) { + var slideId = $(ev.currentTarget).data('slide-id'); + return this._onClickMx(slideId, 'like'); + }, + + _onClickDownMx: function (ev) { + var slideId = $(ev.currentTarget).data('slide-id'); + return this._onClickMx(slideId, 'dislike'); + }, +}); + +publicWidget.registry.FullscreenWebsiteSlidesSlideLike = publicWidget.Widget.extend({ + selector: '#wrapwrap', + + /** + * @override + * @param {Object} parent + */ + start: function () { + var self = this; + var defs = [this._super.apply(this, arguments)]; + $('.o_wslides_js_slide_like_mx').each(function () { + defs.push(new FullscreenSlideLikeWidget(self).attachTo($(this))); + }); + return Promise.all(defs); + }, +}); + +export default { + FullscreenSlideLikeWidget: FullscreenSlideLikeWidget, + FullscreenWebsiteSlidesSlideLike: publicWidget.registry.FullscreenWebsiteSlidesSlideLike +}; diff --git a/mx_elearning_plus/static/src/xml/comment_button_composer_extend.xml b/mx_elearning_plus/static/src/xml/comment_button_composer_extend.xml new file mode 100644 index 0000000..20bfd7a --- /dev/null +++ b/mx_elearning_plus/static/src/xml/comment_button_composer_extend.xml @@ -0,0 +1,13 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/mx_elearning_plus/static/src/xml/comment_composer.xml b/mx_elearning_plus/static/src/xml/comment_composer.xml new file mode 100644 index 0000000..96189fe --- /dev/null +++ b/mx_elearning_plus/static/src/xml/comment_composer.xml @@ -0,0 +1,23 @@ + + + + + + + +
+
+
+ + + +
+
+
+
+
+ + + + + + + + + + + + + + + + + + + + +
+
+
+ + diff --git a/website_ora_elearning/views/slide_assessment_view.xml b/website_ora_elearning/views/slide_assessment_view.xml new file mode 100644 index 0000000..87f7c81 --- /dev/null +++ b/website_ora_elearning/views/slide_assessment_view.xml @@ -0,0 +1,249 @@ + + + + + slide.slide.form + slide.slide + + +
+ +
+ + + + + + + + + + + + +
+
+ + + + + + +
+ + + + +
+
+ + + + + + +
+
+
+
+
+ + ora.response.view.form + ora.response + +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+
+

+ +

+

Response

+ +

+ +

+
+ +

+ +

+
+
+
+
+
+
+
+
+
+ + + + + + + + + + +
+

+ +

+
+ + + + + + + + + + + + + + + + + + + + + + +
+
+ + + + + + +
+
+ + +
+
+ + ora.response.view.list + ora.response + + + + + + + + + + + + + ora.response.search + ora.response + + + + + + + + + + + + + + + + + + + ORA Responses + ora.response + list,form + + + {'search_default_group_by_user': True} + +

+ Nobody has replied to your prompts yet +

+
+
+ + ORA Responses + ora.response + list + + + + + +
+
\ No newline at end of file diff --git a/website_ora_elearning/views/slide_fullscreen_view.xml b/website_ora_elearning/views/slide_fullscreen_view.xml new file mode 100644 index 0000000..50a3864 --- /dev/null +++ b/website_ora_elearning/views/slide_fullscreen_view.xml @@ -0,0 +1,33 @@ + + + + + + \ No newline at end of file diff --git a/website_ora_elearning/views/templates.xml b/website_ora_elearning/views/templates.xml new file mode 100644 index 0000000..84101d9 --- /dev/null +++ b/website_ora_elearning/views/templates.xml @@ -0,0 +1,711 @@ + + + + + + + + + + + + + + + + +