diff --git a/addons/website_sale_slides/models/slide_channel.py b/addons/website_sale_slides/models/slide_channel.py index 88920f58e55..685e0af9aa0 100644 --- a/addons/website_sale_slides/models/slide_channel.py +++ b/addons/website_sale_slides/models/slide_channel.py @@ -9,9 +9,10 @@ class Channel(models.Model): enroll = fields.Selection(selection_add=[('payment', 'On payment')]) product_id = fields.Many2one('product.product', 'Product', index=True) - product_sale_revenues = fields.Float( + product_sale_revenues = fields.Monetary( string='Total revenues', compute='_compute_product_sale_revenues', groups="sales_team.group_sale_salesman") + currency_id = fields.Many2one(related='product_id.currency_id') _sql_constraints = [ ('product_id_check', "CHECK( enroll!='payment' OR product_id IS NOT NULL )", "Product is required for on payment channels.") diff --git a/addons/website_sale_slides/views/slide_channel_views.xml b/addons/website_sale_slides/views/slide_channel_views.xml index 7df803c10d9..4099acf0ffa 100644 --- a/addons/website_sale_slides/views/slide_channel_views.xml +++ b/addons/website_sale_slides/views/slide_channel_views.xml @@ -33,7 +33,8 @@
- + +
diff --git a/addons/website_slides/controllers/main.py b/addons/website_slides/controllers/main.py index f74728ba065..028be99a530 100644 --- a/addons/website_slides/controllers/main.py +++ b/addons/website_slides/controllers/main.py @@ -831,7 +831,10 @@ class WebsiteSlides(WebsiteProfile): else: values.update({ 'category_id': post['category_id'][0], + 'sequence': request.env['slide.slide'].browse(post['category_id'][0]).sequence + 1 }) + else: + values['sequence'] = -1 # create slide itself try: diff --git a/addons/website_slides/data/mail_data.xml b/addons/website_slides/data/mail_data.xml index 55628822d44..88c8a45cd5a 100644 --- a/addons/website_slides/data/mail_data.xml +++ b/addons/website_slides/data/mail_data.xml @@ -11,11 +11,13 @@ Hello

There is something new in the course ${object.channel_id.name} you are following:

${object.name}
+ % if object.image_1024
${object.name}
+ % endif
View content diff --git a/addons/website_slides/data/slide_channel_demo.xml b/addons/website_slides/data/slide_channel_demo.xml index b9b2e022d80..c8f85fe0bea 100644 --- a/addons/website_slides/data/slide_channel_demo.xml +++ b/addons/website_slides/data/slide_channel_demo.xml @@ -3,6 +3,7 @@ Basics of Gardening + public training @@ -21,6 +22,7 @@ Taking care of Trees + public training @@ -37,6 +39,7 @@ Trees, Wood and Gardens + public documentation @@ -55,6 +58,7 @@ Choose your wood ! + public training @@ -74,6 +78,7 @@ will learn the basics of wood characteristics. Furniture Technical Specifications + invite documentation @@ -89,6 +94,7 @@ will learn the basics of wood characteristics. Basics of Furniture Creation + invite training @@ -107,6 +113,7 @@ will learn the basics of wood characteristics. DIY Furniture + invite training diff --git a/addons/website_slides/models/slide_channel.py b/addons/website_slides/models/slide_channel.py index 20e62300ff4..49289105cd3 100644 --- a/addons/website_slides/models/slide_channel.py +++ b/addons/website_slides/models/slide_channel.py @@ -106,7 +106,7 @@ class Channel(models.Model): ('latest', 'Latest Published'), ('most_voted', 'Most Voted'), ('most_viewed', 'Most Viewed')], - string="Featuring Policy", default='latest', required=True) + string="Featured Content", default='latest', required=True) access_token = fields.Char("Security Token", copy=False, default=_default_access_token) nbr_presentation = fields.Integer('Presentations', compute='_compute_slides_statistics', store=True) nbr_document = fields.Integer('Documents', compute='_compute_slides_statistics', store=True) @@ -130,11 +130,11 @@ class Channel(models.Model): help="Email template to send slide publication through email", default=lambda self: self.env['ir.model.data'].xmlid_to_res_id('website_slides.slide_template_published')) share_template_id = fields.Many2one( - 'mail.template', string='Shared Template', + 'mail.template', string='Share Template', help="Email template used when sharing a slide", default=lambda self: self.env['ir.model.data'].xmlid_to_res_id('website_slides.slide_template_shared')) enroll = fields.Selection([ - ('public', 'Public'), ('invite', 'Invite')], + ('public', 'Public'), ('invite', 'On Invitation')], default='public', string='Enroll Policy', required=True, help='Condition to enroll: everyone, on invite, on payment (sale bridge).') enroll_msg = fields.Html( @@ -142,7 +142,7 @@ class Channel(models.Model): default=False, translate=tools.html_translate, sanitize_attributes=False) enroll_group_ids = fields.Many2many('res.groups', string='Auto Enroll Groups', help="Members of those groups are automatically added as members of the channel.") visibility = fields.Selection([ - ('public', 'Public'), ('members', 'Members')], + ('public', 'Public'), ('members', 'Members Only')], default='public', string='Visibility', required=True, help='Applied directly as ACLs. Allow to hide channels and their content for non members.') partner_ids = fields.Many2many( @@ -154,7 +154,7 @@ class Channel(models.Model): channel_partner_ids = fields.One2many('slide.channel.partner', 'channel_id', string='Members Information', groups='website.group_website_publisher', depends=['partner_ids']) upload_group_ids = fields.Many2many( 'res.groups', 'rel_upload_groups', 'channel_id', 'group_id', string='Upload Groups', - help="Who can publish: responsible, members of upload_group_ids if defined or website publisher group members.") + help="Group of users allowed to publish contents on a documentation course.") # not stored access fields, depending on each user completed = fields.Boolean('Done', compute='_compute_user_statistics', compute_sudo=False) completion = fields.Integer('Completion', compute='_compute_user_statistics', compute_sudo=False) @@ -493,7 +493,7 @@ class Channel(models.Model): def action_view_slides(self): action = self.env.ref('website_slides.slide_slide_action').read()[0] action['context'] = {'default_channel_id': self.id} - action['domain'] = [('channel_id', "=", self.id)] + action['domain'] = [('channel_id', "=", self.id), ('is_category', '=', False)] return action def action_view_ratings(self): @@ -523,16 +523,7 @@ class Channel(models.Model): all_slides = self.env['slide.slide'].sudo().search(base_domain, order=order) category_data = [] - # First add uncategorized slides - uncategorized_slides = all_slides.filtered(lambda slide: not slide.category_id) - if uncategorized_slides or force_void: - category_data.append({ - 'category': False, 'id': False, - 'name': _('Uncategorized'), 'slug_name': _('Uncategorized'), - 'total_slides': len(uncategorized_slides), - 'slides': uncategorized_slides[(offset or 0):(offset + limit or len(uncategorized_slides))], - }) - # Then all categories by natural order + # First add all categories by natural order for category in all_categories: category_slides = all_slides.filtered(lambda slide: slide.category_id == category) if not category_slides and not force_void: @@ -543,6 +534,15 @@ class Channel(models.Model): 'total_slides': len(category_slides), 'slides': category_slides[(offset or 0):(limit + offset or len(category_slides))], }) + # Then add uncategorized slides + uncategorized_slides = all_slides.filtered(lambda slide: not slide.category_id) + if uncategorized_slides or force_void: + category_data.append({ + 'category': False, 'id': False, + 'name': _('Uncategorized'), 'slug_name': _('Uncategorized'), + 'total_slides': len(uncategorized_slides), + 'slides': uncategorized_slides[(offset or 0):(offset + limit or len(uncategorized_slides))], + }) return category_data def _resequence_slides(self, slide): diff --git a/addons/website_slides/models/slide_question.py b/addons/website_slides/models/slide_question.py index 98c51585dda..adc871c22e2 100644 --- a/addons/website_slides/models/slide_question.py +++ b/addons/website_slides/models/slide_question.py @@ -27,19 +27,19 @@ class SlideQuestion(models.Model): if answer.is_correct: good_answer_count += 1 if good_answer_count > 1: - raise ValidationError(_('A question can only have one good answer')) + raise ValidationError(_('Question "%s" can only have one good answer') % question.question) @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")) + raise ValidationError(_('Question "%s" must at least have one good answer') % question.question) @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")) + raise ValidationError(_('Question "%s" has no valid answer, please set one') % question.question) @api.depends('slide_id') def _compute_statistics(self): diff --git a/addons/website_slides/models/slide_slide.py b/addons/website_slides/models/slide_slide.py index 2a17ade329d..9fda9095129 100644 --- a/addons/website_slides/models/slide_slide.py +++ b/addons/website_slides/models/slide_slide.py @@ -59,7 +59,7 @@ class SlideLink(models.Model): slide_id = fields.Many2one('slide.slide', required=True, ondelete='cascade') name = fields.Char('Title', required=True) - link = fields.Char('External Link', required=True) + link = fields.Char('Link', required=True) class EmbeddedSlide(models.Model): @@ -123,11 +123,11 @@ class Slide(models.Model): description = fields.Text('Description', translate=True) channel_id = fields.Many2one('slide.channel', string="Channel", required=True) tag_ids = fields.Many2many('slide.tag', 'rel_slide_tag', 'slide_id', 'tag_id', string='Tags') - is_preview = fields.Boolean('Is Preview', default=False, help="The course is accessible by anyone : the users don't need to join the channel to access the content of the course.") - completion_time = fields.Float('Completion Time', digits=(10, 4), help="The estimated completion time for this slide") + is_preview = fields.Boolean('Allow Preview', default=False, help="The course is accessible by anyone : the users don't need to join the channel to access the content of the course.") + completion_time = fields.Float('Duration', digits=(10, 4), help="The estimated completion time for this slide") # Categories is_category = fields.Boolean('Is a category', default=False) - category_id = fields.Many2one('slide.slide', string="Category", compute="_compute_category_id", store=True) + category_id = fields.Many2one('slide.slide', string="Section", compute="_compute_category_id", store=True) slide_ids = fields.One2many('slide.slide', "category_id", string="Slides") # subscribers partner_ids = fields.Many2many('res.partner', 'slide_slide_partner', 'slide_id', 'partner_id', @@ -138,6 +138,7 @@ class Slide(models.Model): help="Subscriber information for the current logged in user") # Quiz related fields question_ids = fields.One2many("slide.question", "slide_id", string="Questions") + questions_count = fields.Integer(string="Numbers of Questions", compute='_compute_questions_count') 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,) @@ -161,7 +162,7 @@ class Slide(models.Model): html_content = fields.Html("HTML Content", help="Custom HTML content for slides of type 'Web Page'.", translate=True) # website website_id = fields.Many2one(related='channel_id.website_id', readonly=True) - date_published = fields.Datetime('Publish Date') + date_published = fields.Datetime('Publish Date', readonly=True, tracking=True) likes = fields.Integer('Likes', compute='_compute_user_info', store=True, compute_sudo=False) dislikes = fields.Integer('Dislikes', compute='_compute_user_info', store=True, compute_sudo=False) user_vote = fields.Integer('User vote', compute='_compute_user_info', compute_sudo=False) @@ -214,6 +215,11 @@ class Slide(models.Model): elif slide.category_id != current_category: slide.category_id = current_category.id + @api.depends('question_ids') + def _compute_questions_count(self): + for slide in self: + slide.questions_count = len(slide.question_ids) + @api.depends('website_message_ids.res_id', 'website_message_ids.model', 'website_message_ids.message_type') def _compute_comments_count(self): for slide in self: @@ -393,7 +399,7 @@ class Slide(models.Model): slide = super(Slide, self).create(values) - if slide.is_published: + if slide.is_published and not slide.is_category: slide._post_publication() return slide 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 index 47e5700da31..250e78e3421 100644 --- a/addons/website_slides/static/src/js/slides_course_fullscreen_player.js +++ b/addons/website_slides/static/src/js/slides_course_fullscreen_player.js @@ -257,10 +257,12 @@ odoo.define('website_slides.fullscreen', function (require) { _onClickTab: function (ev) { ev.stopPropagation(); var $elem = $(ev.currentTarget); - var isQuiz = $elem.data('isQuiz'); - var slideID = parseInt($elem.data('id')); - var slide = findSlide(this.slideEntries, {id: slideID, isQuiz: isQuiz}); - this.set('slideEntry', slide); + if ($elem.data('canAccess') === 'True') { + var isQuiz = $elem.data('isQuiz'); + var slideID = parseInt($elem.data('id')); + var slide = findSlide(this.slideEntries, {id: slideID, isQuiz: isQuiz}); + this.set('slideEntry', slide); + } }, /** * Actively changes the active tab in the sidebar so that it corresponds @@ -710,7 +712,7 @@ odoo.define('website_slides.fullscreen', function (require) { * Creates slides objects from every slide-list-cells attributes */ _getSlides: function (){ - var $slides = this.$('.o_wslides_fs_sidebar_list_item'); + var $slides = this.$('.o_wslides_fs_sidebar_list_item[data-can-access="True"]'); var slideList = []; $slides.each(function () { var slideData = $(this).data(); diff --git a/addons/website_slides/static/src/js/slides_course_quiz.js b/addons/website_slides/static/src/js/slides_course_quiz.js index 89076268c6a..1fed8ff1ac7 100644 --- a/addons/website_slides/static/src/js/slides_course_quiz.js +++ b/addons/website_slides/static/src/js/slides_course_quiz.js @@ -92,8 +92,9 @@ odoo.define('website_slides.quiz', function (require) { } else if (alert_code === 'slide_quiz_done') { message = _t('This quiz is already done. Retaking it is not possible.'); - } else if (alert_code === 'public_user') { - message = _t("You must be logged to submit the quiz."); + } + else if (alert_code === 'public_user') { + message = _t('You must be logged to submit the quiz.'); } this.$('.o_wslides_js_lesson_quiz_error span:first').html(message); this.$('.o_wslides_js_lesson_quiz_error').removeClass('d-none'); @@ -112,10 +113,10 @@ odoo.define('website_slides.quiz', function (require) { } }).then(function (quiz_data) { self.quiz = { - questions: quiz_data.slide_questions, - quizAttemptsCount: quiz_data.quiz_attempts_count, - quizKarmaGain: quiz_data.quiz_karma_gain, - quizKarmaWon: quiz_data.quiz_karma_won + questions: quiz_data.slide_questions || [], + quizAttemptsCount: quiz_data.quiz_attempts_count || 0, + quizKarmaGain: quiz_data.quiz_karma_gain || 0, + quizKarmaWon: quiz_data.quiz_karma_won || 0, }; }); }, diff --git a/addons/website_slides/static/src/js/slides_upload.js b/addons/website_slides/static/src/js/slides_upload.js index 0377abedeb1..83d24d8a447 100644 --- a/addons/website_slides/static/src/js/slides_upload.js +++ b/addons/website_slides/static/src/js/slides_upload.js @@ -74,13 +74,13 @@ var SlideUploadDialog = Dialog.extend({ this.$('.alert-warning').remove(); }, /** - * Category and tags management from select2 + * Section and tags management from select2 * * @private */ _bindSelect2Dropdown: function () { var self = this; - this.$('#category_id').select2(this._select2Wrapper(_t('Category'), false, + this.$('#category_id').select2(this._select2Wrapper(_t('Section'), false, function () { return self._rpc({ route: '/slides/category/search_read', @@ -350,6 +350,15 @@ var SlideUploadDialog = Dialog.extend({ } }; }, + /** + * Show the preview/right column and resize the modal + * @private + */ + _showPreviewColumn: function() { + this.$('#o_wslides_js_slide_upload_left_column').removeClass('col').addClass('col-md-6'); + this.$('#o_wslides_js_slide_upload_preview_column').removeClass('d-none'); + this.$modal.find('.modal-dialog').addClass('modal-lg'); + }, /** * @private */ @@ -389,6 +398,8 @@ var SlideUploadDialog = Dialog.extend({ } else { this.set_title(_t("Upload a document")); } + + this.$modal.find('.modal-dialog').removeClass('modal-lg'); }, _onChangeCanSubmitForm: function (ev) { if (this.get('can_submit_form')) { @@ -426,6 +437,7 @@ var SlideUploadDialog = Dialog.extend({ } buffer = buffer.split(',')[1]; self.file.data = buffer; + self._showPreviewColumn(); }); if (file.type === 'application/pdf') { @@ -478,6 +490,7 @@ var SlideUploadDialog = Dialog.extend({ self.set('can_submit_form', true); } loaded = true; + self._showPreviewColumn(); }); }); }); @@ -510,6 +523,7 @@ var SlideUploadDialog = Dialog.extend({ self._formSetFieldValue('description', data.description); self.isValidUrl = true; + self._showPreviewColumn(); } }); }, diff --git a/addons/website_slides/static/src/scss/rating_rating_views.scss b/addons/website_slides/static/src/scss/rating_rating_views.scss index 49886be92f5..e86a1811c10 100644 --- a/addons/website_slides/static/src/scss/rating_rating_views.scss +++ b/addons/website_slides/static/src/scss/rating_rating_views.scss @@ -7,5 +7,9 @@ $o-kanban-large-record-width: 350px; .o_slide_rating_kanban_left { min-width: 80px; + + .o_slide_rating_value { + font-size: 4rem; + } } } diff --git a/addons/website_slides/static/src/scss/slide_channel_views.scss b/addons/website_slides/static/src/scss/slide_channel_views.scss deleted file mode 100644 index 3bd8203c4eb..00000000000 --- a/addons/website_slides/static/src/scss/slide_channel_views.scss +++ /dev/null @@ -1,7 +0,0 @@ -$o-kanban-large-record-width: 400px; - -.o_kanban_view.o_slide_channel_kanban { - .o_kanban_record { - width: $o-kanban-large-record-width - } -} diff --git a/addons/website_slides/static/src/scss/category_backend.scss b/addons/website_slides/static/src/scss/slide_views.scss similarity index 54% rename from addons/website_slides/static/src/scss/category_backend.scss rename to addons/website_slides/static/src/scss/slide_views.scss index 99b017738ca..1110a3f9442 100644 --- a/addons/website_slides/static/src/scss/category_backend.scss +++ b/addons/website_slides/static/src/scss/slide_views.scss @@ -1,6 +1,14 @@ +$o-kanban-large-record-width: 400px; + +.o_kanban_view.o_slide_kanban { + .o_kanban_record { + width: $o-kanban-large-record-width + } +} + table.o_section_list_view tr.o_data_row.o_is_section { font-weight: bold; background-color: #DDD; border-top: 1px solid #BBB; border-bottom: 1px solid #BBB; -} \ 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 index 1b7ff14ae61..a6c406e5242 100644 --- a/addons/website_slides/static/src/scss/slides_slide_fullscreen.scss +++ b/addons/website_slides/static/src/scss/slides_slide_fullscreen.scss @@ -35,6 +35,7 @@ .o_wslides_fs_sidebar { background-image: linear-gradient(160deg, $o-wslides-color-dark1, $o-wslides-color-dark2); position: relative; + z-index: $zindex-fixed; .o_wslides_fs_sidebar_content { min-width: $o-wslides-fs-side-width; diff --git a/addons/website_slides/static/src/xml/website_slides_channel.xml b/addons/website_slides/static/src/xml/website_slides_channel.xml index a4528b62083..c040ef438e1 100644 --- a/addons/website_slides/static/src/xml/website_slides_channel.xml +++ b/addons/website_slides/static/src/xml/website_slides_channel.xml @@ -6,47 +6,37 @@
-
- -
- -

Please fill in this field

-
+
+ + +

Please fill in this field

-
- -
- -
+
+ +
-
- -
-
- - - Training (with progress) - -
-
- - - Documentation - -
-
+ +
+ + + Training (with progress) + +
+
+ + + Documentation +
-
- -
-