From dc76c99a1e6508f24ea059f08fa48975cbfcaff5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thibault=20Delavall=C3=A9e?= Date: Thu, 28 Feb 2019 15:54:15 +0000 Subject: [PATCH] [FIX] website_slides: fix display of lessons in training course Currently slides are displayed in a reorderable list, using current user access rights. This means external customers see only published and free lessons. However purpose of course page is to display all lessons, and display only available ones as clickable. Other one should be displayed as muted to tease people. Note that a specific lesson page does check for access rights. Here we just display all lessons names. A label for free preview is also added. Fetch of data when displaying a channel is therefore a bit modified as there are less differences between training and documentation channels. User progress is also updated to be easier to use in templates. Commit linked to task ID 1941250 and PR #31453 --- addons/website_slides/controllers/main.py | 125 +++++++++----- addons/website_slides/models/slide_slide.py | 1 + .../src/js/slides_course_slides_list.js | 14 +- .../static/src/scss/website_slides.scss | 44 ++--- .../views/website_slides_templates_course.xml | 157 +++++++++--------- 5 files changed, 180 insertions(+), 161 deletions(-) diff --git a/addons/website_slides/controllers/main.py b/addons/website_slides/controllers/main.py index 8d3c696c540..dc87a24dafd 100644 --- a/addons/website_slides/controllers/main.py +++ b/addons/website_slides/controllers/main.py @@ -19,8 +19,9 @@ _logger = logging.getLogger(__name__) class WebsiteSlides(WebsiteProfile): - _slides_per_page = 12 - _slides_per_list = 20 + SLIDES_PER_PAGE = 12 + SLIDES_PER_ASIDE = 20 + SLIDES_PER_CATEGORY = 4 _channel_order_by_criterion = { 'vote': 'total_votes desc', 'view': 'total_views desc', @@ -64,8 +65,8 @@ class WebsiteSlides(WebsiteProfile): return True 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) + most_viewed_slides = slide._get_most_viewed_slides(self.SLIDES_PER_ASIDE) + related_slides = slide._get_related_slides(self.SLIDES_PER_ASIDE) values = { 'slide': slide, 'most_viewed_slides': most_viewed_slides, @@ -90,12 +91,41 @@ class WebsiteSlides(WebsiteProfile): possible_points = [slide.quiz_first_attempt_reward,slide.quiz_second_attempt_reward,slide.quiz_third_attempt_reward, slide.quiz_fourth_attempt_reward] return possible_points[attempt_count] if attempt_count < len(possible_points) else possible_points[-1] + # CHANNEL UTILITIES + # -------------------------------------------------- + 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)])} + 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 _get_channel_progress(self, channel): + """ Replacement to user_progress. Both may exist in some transient state. """ + slides = request.env['slide.slide'].sudo().search([('channel_id', '=', channel.id)]) + channel_progress = dict((sid, dict()) for sid in slides.ids) + if not request.env.user._is_public() and channel.is_member: + slide_partners = request.env['slide.slide.partner'].sudo().search([ + ('channel_id', '=', channel.id), + ('partner_id', '=', request.env.user.partner_id.id) + ]) + for slide_partner in slide_partners: + channel_progress[slide_partner.slide_id.id].update(slide_partner.read()[0]) + if slide_partner.slide_id.question_ids: + gains = [slide_partner.slide_id.quiz_first_attempt_reward, + slide_partner.slide_id.quiz_second_attempt_reward, + slide_partner.slide_id.quiz_third_attempt_reward, + slide_partner.slide_id.quiz_fourth_attempt_reward] + channel_progress[slide_partner.slide_id.id]['quiz_gain'] = gains[slide_partner.quiz_attempts_count] if slide_partner.quiz_attempts_count < len(gains) else gains[-1] + + return channel_progress + def _extract_channel_tag_search(self, **post): tags = request.env['slide.channel.tag'] for key in (_key for _key in post if _key.startswith('channel_tag_group_id_')): @@ -133,9 +163,6 @@ class WebsiteSlides(WebsiteProfile): domain = expression.AND([domain, [('partner_ids', '=', request.env.user.partner_id.id)]]) return domain - def _prepare_channel_values(self, **kwargs): - return dict(**kwargs) - # -------------------------------------------------- # MAIN / SEARCH # -------------------------------------------------- @@ -245,11 +272,6 @@ class WebsiteSlides(WebsiteProfile): raise werkzeug.exceptions.NotFound() domain = [('channel_id', '=', channel.id)] - if not channel.can_publish: - domain = expression.AND([ - domain, - ['&', ('website_published', '=', True), ('channel_id.website_published', '=', True)] - ]) pager_url = "/slides/%s" % (channel.id) pager_args = {} @@ -278,13 +300,16 @@ class WebsiteSlides(WebsiteProfile): pager_url += "?slide_type=%s" % slide_type # sorting criterion - actual_sorting = sorting if sorting and sorting in request.env['slide.slide']._order_by_strategy else channel.promote_strategy + if channel.channel_type == 'documentation': + actual_sorting = sorting if sorting and sorting in request.env['slide.slide']._order_by_strategy else channel.promote_strategy + else: + actual_sorting = 'sequence' order = request.env['slide.slide']._order_by_strategy[actual_sorting] pager_args['sorting'] = actual_sorting pager_count = request.env['slide.slide'].sudo().search_count(domain) pager = request.website.pager(url=pager_url, total=pager_count, page=page, - step=self._slides_per_page, scope=self._slides_per_page, + step=self.SLIDES_PER_PAGE, scope=self.SLIDES_PER_PAGE, url_args=pager_args) values = { @@ -323,24 +348,47 @@ class WebsiteSlides(WebsiteProfile): 'last_rating_value': last_message_data.get('rating_value'), }) - # Display uncategorized slides - # fetch slides; done as sudo because we want to display all of them but - # unreachable ones won't be clickable (+ slide controller will crash anyway) + # fetch slides and handle uncategorized slides; done as sudo because we want to display all + # of them but unreachable ones won't be clickable (+ slide controller will crash anyway) + # documentation mode may display less slides than content by category but overhead of + # computation is reasonable if not category and not uncategorized: category_data = [] - for category in request.env['slide.slide'].sudo().read_group(domain, ['category_id'], ['category_id']): - category_id, name = category.get('category_id') or (False, _('Uncategorized')) - slides_sudo = request.env['slide.slide'].sudo().search(category['__domain'], limit=4, offset=0, order=order) + all_categories = request.env['slide.category'].search([('channel_id', '=', channel.id)]) + all_slides = request.env['slide.slide'].sudo().search(domain, order=order) + + uncategorized_slides = all_slides.filtered(lambda slide: not slide.category_id) + displayed_slides = uncategorized_slides + if channel.channel_type == 'documentation': + displayed_slides = uncategorized_slides[:self.SLIDES_PER_CATEGORY] + if channel.channel_type == 'training' or displayed_slides: category_data.append({ - 'id': category_id, - 'name': name, - 'slug_name': slug((category_id, name)) if category_id else name, - 'total_slides': category['category_id_count'], - 'slides': slides_sudo, + 'category': False, 'id': False, + 'name': _('Uncategorized'), 'slug_name': _('Uncategorized'), + 'total_slides': len(uncategorized_slides), + 'slides': displayed_slides, + }) + for category in all_categories: + category_slides = all_slides.filtered(lambda slide: slide.category_id == category) + displayed_slides = category_slides + if channel.channel_type == 'documentation': + displayed_slides = category_slides[:self.SLIDES_PER_CATEGORY] + if not displayed_slides: + continue + category_data.append({ + 'id': category.id, + 'name': category.name, + 'slug_name': slug(category), + 'total_slides': len(category_slides), + 'slides': displayed_slides, }) elif uncategorized: - slides_sudo = request.env['slide.slide'].sudo().search(domain, limit=self._slides_per_page, offset=pager['offset'], order=order) + if channel.channel_type == 'documentation': + slides_sudo = request.env['slide.slide'].sudo().search(domain, limit=self.SLIDES_PER_PAGE, offset=pager['offset'], order=order) + else: + slides_sudo = request.env['slide.slide'].sudo().search(domain, order=order) category_data = [{ + 'category': False, 'id': False, 'name': _('Uncategorized'), 'slug_name': _('Uncategorized'), @@ -348,35 +396,24 @@ class WebsiteSlides(WebsiteProfile): 'slides': slides_sudo, }] else: - slides_sudo = request.env['slide.slide'].sudo().search(domain, limit=self._slides_per_page, offset=pager['offset'], order=order) + if channel.channel_type == 'documentation': + slides_sudo = request.env['slide.slide'].sudo().search(domain, limit=self.SLIDES_PER_PAGE, offset=pager['offset'], order=order) + else: + slides_sudo = request.env['slide.slide'].sudo().search(domain, order=order) category_data = [{ + 'category': category, 'id': category.id, 'name': category.name, 'slug_name': slug(category), 'total_slides': len(slides_sudo), 'slides': slides_sudo, }] - - # post slide-fetch computation: promoted, user completion (separated because sudo-ed) - if not request.website.is_public_user() and channel.is_member: - displayed_slide_ids = list(set(sid for item in category_data for sid in item['slides'].ids)) - done_slide_ids = request.env['slide.slide.partner'].sudo().search([ - ('slide_id', 'in', displayed_slide_ids), - ('partner_id', '=', request.env.user.partner_id.id), - ('completed', '=', True) - ]).mapped('slide_id').ids - else: - done_slide_ids = [] - values['done_slide_ids'] = done_slide_ids values['slide_promoted'] = request.env['slide.slide'].sudo().search(domain, limit=1, order=order) values['category_data'] = category_data + values['channel_progress'] = self._get_channel_progress(channel) values = self._prepare_additional_channel_values(values, **kw) - if channel.channel_type == "training": - values.update(self._get_user_progress(channel)) - values['uncategorized_slides'] = channel.slide_ids.filtered(lambda slide: not slide.category_id) - return request.render('website_slides.course_main', values) @http.route(['/slides/channel/add'], type='http', auth='user', methods=['POST'], website=True) diff --git a/addons/website_slides/models/slide_slide.py b/addons/website_slides/models/slide_slide.py index 2b2b1e43fd8..6f2bb6feac5 100644 --- a/addons/website_slides/models/slide_slide.py +++ b/addons/website_slides/models/slide_slide.py @@ -88,6 +88,7 @@ class Slide(models.Model): _description = 'Slides' _mail_post_access = 'read' _order_by_strategy = { + 'sequence': 'category_sequence asc, sequence asc', 'most_viewed': 'total_views desc', 'most_voted': 'likes desc', 'latest': 'date_published desc', 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 index 756026b5a0c..93376a08d31 100644 --- a/addons/website_slides/static/src/js/slides_course_slides_list.js +++ b/addons/website_slides/static/src/js/slides_course_slides_list.js @@ -28,17 +28,17 @@ sAnimations.registry.websiteSlidesCourseSlidesList = sAnimations.Class.extend({ * @private */ _bindSortable: function () { - this.$('ul.o_wslides_slides_list_container').sortable({ + this.$('ul.o_wslides_js_slides_list_container').sortable({ handle: '.fa-arrows', stop: this._reorderCategories.bind(this), items: '.o_wslides_slide_list_category' }); - this.$('.o_wslides_slides_list_container ul').sortable({ + this.$('.o_wslides_js_slides_list_container ul').sortable({ handle: '.fa-arrows', - connectWith: '.o_wslides_slides_list_container ul', + connectWith: '.o_wslides_js_slides_list_container ul', stop: this._reorderSlides.bind(this), - items: '.o_wslides_slides_list_slide:not(.o_wslides_empty_category)' + items: '.o_wslides_slides_list_slide:not(.o_wslides_js_slides_list_empty)' }); }, @@ -50,8 +50,8 @@ sAnimations.registry.websiteSlidesCourseSlidesList = sAnimations.Class.extend({ * @private */ _checkForEmptySections: function (){ - this.$('.o_wslides_slides_list_container ul').each(function (){ - var $emptyCategory = $(this).find('.o_wslides_empty_category'); + this.$('.o_wslides_js_slides_list_container ul').each(function (){ + var $emptyCategory = $(this).find('.o_wslides_js_slides_list_empty'); if ($(this).find('li.o_wslides_slides_list_slide[data-slide-id]').length === 0) { $emptyCategory.removeClass('d-none').addClass('d-flex'); } else { @@ -130,7 +130,7 @@ sAnimations.registry.websiteSlidesCourseSlidesList = sAnimations.Class.extend({ * @private */ _updateHref: function () { - this.$(".o_wslides_slides_list_slide_link").each(function (){ + this.$(".o_wslides_js_slides_list_slide_link").each(function (){ var href = $(this).attr('href'); var operator = href.indexOf('?') !== -1 ? '&' : '?'; $(this).attr('href', href + operator + "fullscreen=1"); diff --git a/addons/website_slides/static/src/scss/website_slides.scss b/addons/website_slides/static/src/scss/website_slides.scss index f1fdfd9b2c5..c8d35376e17 100644 --- a/addons/website_slides/static/src/scss/website_slides.scss +++ b/addons/website_slides/static/src/scss/website_slides.scss @@ -2,7 +2,7 @@ $nav-tabs-border-color: #dddddd !default; $body-bg: #FFFFFF !default; $MAX-Z-INDEX : 2147483647 !default; - +$gray-50: #f4f4f4 !default; // Common to new slides pages // ************************************************** @@ -204,49 +204,33 @@ $MAX-Z-INDEX : 2147483647 !default; } .o_wslides_course_main { - .o_wslides_course_content_aside { - max-width: 286px; - .bg-white { - max-width: 256px; - background-color: white; - padding-bottom: 8px; - > div.row { - padding-left: 15px; - padding-right: 15px; - } - } + .o_wslides_course_content_aside { + max-width: 286px; + .bg-white { + max-width: 256px; + background-color: white; + padding-bottom: 8px; + > div.row { + padding-left: 15px; + padding-right: 15px; + } } + } } // Slides list reordering widget .o_wslides_slides_list { - padding: 0; - - ul { - list-style: none; - padding: 0; - } - - .o_wslides_slide_list_category_container { - margin: 0; - background-color: #ddd; - display: flex; - list-style: none; - font-size: 1.05rem; - border-bottom: 1px solid #ccc; - } - .o_wslides_slides_list_slide { &.o_not_editable { height: 0px; } &:nth-child(odd) { - background-color: #f6f6f6; + background-color: $gray-50; } &:nth-child(even) { - background-color: #f9f9f9; + background-color: $gray-100; } .o_wslides_slides_list_slide_controls { diff --git a/addons/website_slides/views/website_slides_templates_course.xml b/addons/website_slides/views/website_slides_templates_course.xml index c4261638b1a..d9e7b8c48af 100644 --- a/addons/website_slides/views/website_slides_templates_course.xml +++ b/addons/website_slides/views/website_slides_templates_course.xml @@ -264,39 +264,44 @@
-
    +
      -
    • -
      - -
      -
        -
      • -
        - Empty category + + +
      • +
        +
        + + + + + + Uncategorized +
        -
      • -
      • - - - - -
      -
    • - -
    • - -
        -
      • -
        +
          +
        • +
          Empty category
        • - + @@ -305,15 +310,16 @@
        - Add Content - Add Section + t-att-data-can-publish="channel.can_publish"> Add Content + Add Section
        @@ -321,58 +327,51 @@ - -