From b180c66a80ae3ae34716f8524a990c9e9d2c1aa7 Mon Sep 17 00:00:00 2001
From: qmo-odoo <qmo@odoo.com>
Date: Mon, 13 May 2019 11:09:31 +0000
Subject: [PATCH] [REF] website_slides: replace slide.category by slide with
 is_category flag

PURPOSE

Like already done for sale order, invoice of survey, purpose of this commit
is to remove category model and replace by a flagged line (slide). It allows
to easily reorder slides in an embedded list view.

SPECIFICATIONS

Instead of having a fully fledged slide.category model, slide.slide will serve
that purpose with a is_category flag. This will allow to drag and drop slides
and sections in the channel form view.

This change had an impact on the way slides were added/sorted on the front-end.
In fact, whenever a slide is added from the front-end, a resequencing of all
the slides in the course has to be triggered.

Category of a slide is now a computed field based on the sequence. Order
of slides is based on sequence, with categories splitting the slide list based
on is_category flag.

In this commit tests are added. Some cleaning in tests is also performed to
speedup a bit tests (savepointcase) and some cleaning / renaming to ease
their understanding.

Future commit will add JS necessary to manage slides in the section list view.

LINKS

TaskID: 1978731
PR: #33255
---
 addons/website_slides/controllers/main.py     |  84 +++++++-------
 .../data/slide_channel_demo.xml               |  45 --------
 .../website_slides/data/slide_slide_demo.xml  | 106 ++++++++++++++----
 addons/website_slides/models/slide_channel.py |  89 ++++++---------
 addons/website_slides/models/slide_slide.py   |  84 +++++++++++++-
 .../security/ir.model.access.csv              |   2 -
 .../security/website_slides_security.xml      |   4 +-
 .../src/js/slides_course_slides_list.js       |  55 ++-------
 addons/website_slides/tests/__init__.py       |   5 +-
 addons/website_slides/tests/common.py         |  64 ++++++-----
 addons/website_slides/tests/test_from_url.py  |  30 -----
 addons/website_slides/tests/test_karma.py     |   4 +-
 addons/website_slides/tests/test_security.py  |  13 +--
 .../website_slides/tests/test_slide_utils.py  |  99 ++++++++++++++++
 .../website_slides/tests/test_statistics.py   |  50 ++++-----
 .../views/slide_channel_views.xml             |  55 ---------
 .../views/slide_slide_views.xml               |  11 +-
 .../views/website_slides_templates_course.xml |   4 +-
 .../views/website_slides_templates_lesson.xml |   8 +-
 ...ite_slides_templates_lesson_fullscreen.xml |  11 +-
 .../data/slide_slide_demo.xml                 |   2 +-
 .../models/slide_channel.py                   |   4 -
 .../models/slide_slide.py                     |   1 +
 23 files changed, 435 insertions(+), 395 deletions(-)
 delete mode 100644 addons/website_slides/tests/test_from_url.py
 create mode 100644 addons/website_slides/tests/test_slide_utils.py

diff --git a/addons/website_slides/controllers/main.py b/addons/website_slides/controllers/main.py
index ca0a8d83c724..7d7eee4fb8e8 100644
--- a/addons/website_slides/controllers/main.py
+++ b/addons/website_slides/controllers/main.py
@@ -137,19 +137,29 @@ class WebsiteSlides(WebsiteProfile):
         values.update(self._get_slide_quiz_partner_info(slide))
         return values
 
+    def _get_new_slide_category_values(self, channel, name):
+        return {
+            'name': name,
+            'channel_id': channel.id,
+            'is_category': True,
+            'is_published': True,
+            'sequence': channel.slide_ids[-1]['sequence'] + 1 if channel.slide_id else 1,
+        }
+
     # CHANNEL UTILITIES
     # --------------------------------------------------
 
     def _get_channel_slides_base_domain(self, channel):
         """ base domain when fetching slide list data related to a given channel
 
-         * website related domain, and restricted to the channel;
+         * website related domain, and restricted to the channel and is not a
+           category slide (behavior is different from classic slide);
          * if publisher: everything is ok;
          * if not publisher but has user: either slide is published, either
            current user is the one that uploaded it;
          * if not publisher and public: published;
         """
-        base_domain = expression.AND([request.website.website_domain(), [('channel_id', '=', channel.id)]])
+        base_domain = expression.AND([request.website.website_domain(), ['&', ('channel_id', '=', channel.id), ('is_category', '=', False)]])
         if not channel.can_publish:
             if request.website.is_public_user():
                 base_domain = expression.AND([base_domain, [('website_published', '=', True)]])
@@ -331,8 +341,8 @@ class WebsiteSlides(WebsiteProfile):
         '/slides/<model("slide.channel"):channel>/page/<int:page>',
         '/slides/<model("slide.channel"):channel>/tag/<model("slide.tag"):tag>',
         '/slides/<model("slide.channel"):channel>/tag/<model("slide.tag"):tag>/page/<int:page>',
-        '/slides/<model("slide.channel"):channel>/category/<model("slide.category"):category>',
-        '/slides/<model("slide.channel"):channel>/category/<model("slide.category"):category>/page/<int:page>'
+        '/slides/<model("slide.channel"):channel>/category/<model("slide.slide"):category>',
+        '/slides/<model("slide.channel"):channel>/category/<model("slide.slide"):category>/page/<int:page>'
     ], type='http', auth="public", website=True, sitemap=sitemap_slide)
     def channel(self, channel, category=None, tag=None, page=1, slide_type=None, uncategorized=False, sorting=None, search=None, **kw):
         if not channel.can_access_from_current_website():
@@ -486,24 +496,6 @@ class WebsiteSlides(WebsiteProfile):
             return {'error': 'join_done'}
         return success
 
-    @http.route('/slides/channel/resequence', type="json", website=True, auth="user")
-    def resequence_slides(self, channel_id, slides_data):
-        """" Reorder the slides within the channel by reassigning their 'sequence' field.
-        This method also handles slides that are put in a new category (or uncategorized). """
-        channel = request.env['slide.channel'].browse(int(channel_id))
-        if not channel.can_publish:
-            return {'error': 'Only the publishers of the channel can edit it'}
-
-        slides = request.env['slide.slide'].search([
-            ('id', 'in', [int(key) for key in slides_data.keys()]),
-            ('channel_id', '=', channel.id)
-        ])
-
-        for slide in slides:
-            slide_key = str(slide.id)
-            slide.sequence = slides_data[slide_key]['sequence']
-            slide.category_id = slides_data[slide_key]['category_id'] if 'category_id' in slides_data[slide_key] else False
-
     @http.route(['/slides/channel/tag/search_read'], type='json', auth='user', methods=['POST'], website=True)
     def slide_channel_tag_search_read(self, fields, domain):
         can_create = request.env['slide.channel.tag'].check_access_rights('create', raise_exception=False)
@@ -738,30 +730,23 @@ class WebsiteSlides(WebsiteProfile):
 
     @http.route(['/slides/category/search_read'], type='json', auth='user', methods=['POST'], website=True)
     def slide_category_search_read(self, fields, domain):
-        can_create = request.env['slide.category'].check_access_rights('create', raise_exception=False)
+        category_slide_domain = domain if domain else []
+        category_slide_domain = expression.AND([category_slide_domain, [('is_category', '=', True)]])
+        can_create = request.env['slide.slide'].check_access_rights('create', raise_exception=False)
         return {
-            'read_results': request.env['slide.category'].search_read(domain, fields),
+            'read_results': request.env['slide.slide'].search_read(category_slide_domain, fields),
             'can_create': can_create,
         }
 
     @http.route('/slides/category/add', type="http", website=True, auth="user")
     def slide_category_add(self, channel_id, name):
-        """ Adds a category to the specified channel. If categories already exist
-        within this channel, it will be added at the bottom (sequence+1) """
+        """ Adds a category to the specified channel. Slide is added at the end
+        of slide list based on sequence. """
         channel = request.env['slide.channel'].browse(int(channel_id))
+        if not channel.can_upload or not channel.can_publish:
+            raise werkzeug.exceptions.NotFound()
 
-        values = {
-            'name': name,
-            'channel_id': channel.id
-        }
-
-        latest_category = request.env['slide.category'].search_read([
-            ('channel_id', '=', channel.id)
-        ], ["sequence"], order="sequence desc", limit=1)
-        if latest_category:
-            values['sequence'] = latest_category[0]['sequence'] + 1
-
-        request.env['slide.category'].create(values)
+        request.env['slide.slide'].create(self._get_new_slide_category_values(channel, name))
 
         return werkzeug.utils.redirect("/slides/%s" % (slug(channel)))
 
@@ -797,14 +782,6 @@ class WebsiteSlides(WebsiteProfile):
 
         values = dict((fname, post[fname]) for fname in self._get_valid_slide_post_values() if post.get(fname))
 
-        if post.get('category_id'):
-            if post['category_id'][0] == 0:
-                values['category_id'] = request.env['slide.category'].create({
-                    'name': post['category_id'][1]['name'],
-                    'channel_id': values.get('channel_id')}).id
-            else:
-                values['category_id'] = post['category_id'][0]
-
         # handle exception during creation of slide and sent error notification to the client
         # otherwise client slide create dialog box continue processing even server fail to create a slide
         try:
@@ -818,6 +795,18 @@ class WebsiteSlides(WebsiteProfile):
             if not can_upload:
                 return {'error': _('You cannot upload on this channel.')}
 
+        # handle creation of new categories on the fly
+        if post.get('category_id'):
+            if post['category_id'][0] == 0:
+                category = request.env['slide.slide'].create(self._get_new_slide_category_values(channel, post['category_id'][1]['name']))
+                values['category_id'] = category.id
+                values['sequence'] = category.sequence + 1
+            else:
+                values.update({
+                    'category_id': post['category_id'][0],
+                })
+
+        # create slide itself
         try:
             values['user_id'] = request.env.uid
             values['is_published'] = values.get('is_published', False) and can_publish
@@ -829,6 +818,9 @@ class WebsiteSlides(WebsiteProfile):
             _logger.error(e)
             return {'error': _('Internal server error, please try again later or contact administrator.\nHere is the error message: %s') % e}
 
+        # ensure correct ordering by re sequencing slides in front-end (backend should be ok thanks to list view)
+        channel._resequence_slides(slide)
+
         redirect_url = "/slides/slide/%s" % (slide.id)
         if channel.channel_type == "training" and not slide.slide_type == "webpage":
             redirect_url = "/slides/%s" % (slug(channel))
diff --git a/addons/website_slides/data/slide_channel_demo.xml b/addons/website_slides/data/slide_channel_demo.xml
index 2b2cc2eeb182..f0b3e8626b27 100644
--- a/addons/website_slides/data/slide_channel_demo.xml
+++ b/addons/website_slides/data/slide_channel_demo.xml
@@ -32,16 +32,6 @@
         <field name="description">Learn how to take care of your favorite trees. Learn when to plant, how to manage potted trees, ...</field>
         <field name="create_date" eval="DateTime.now() - relativedelta(days=7)"/>
     </record>
-    <record id="slide_category_demo_1_0" model="slide.category">
-        <field name="name">Interesting Facts</field>
-        <field name="channel_id" ref="website_slides.slide_channel_demo_1_gard1"/>
-        <field name="sequence">10</field>
-    </record>
-    <record id="slide_category_demo_1_1" model="slide.category">
-        <field name="name">Methods</field>
-        <field name="channel_id" ref="website_slides.slide_channel_demo_1_gard1"/>
-        <field name="sequence">20</field>
-    </record>
 
     <record id="slide_channel_demo_2_gard2" model="slide.channel">
         <field name="name">Trees, Wood and Gardens</field>
@@ -60,16 +50,6 @@
         <field name="description">A lot of nice documentation: trees, wood, gardens. A gold mine for references.</field>
         <field name="create_date" eval="DateTime.now() - relativedelta(days=6)"/>
     </record>
-    <record id="slide_category_demo_2_0" model="slide.category">
-        <field name="name">Trees</field>
-        <field name="channel_id" ref="website_slides.slide_channel_demo_2_gard2"/>
-        <field name="sequence">10</field>
-    </record>
-    <record id="slide_category_demo_2_1" model="slide.category">
-        <field name="name">Wood</field>
-        <field name="channel_id" ref="website_slides.slide_channel_demo_2_gard2"/>
-        <field name="sequence">20</field>
-    </record>
 
     <record id="slide_channel_demo_3_furn0" model="slide.channel">
         <field name="name">Choose your wood !</field>
@@ -89,11 +69,6 @@
 will learn the basics of wood characteristics.</field>
         <field name="create_date" eval="DateTime.now() - relativedelta(days=5)"/>
     </record>
-    <record id="slide_category_demo_3_0" model="slide.category">
-        <field name="name">Introduction</field>
-        <field name="channel_id" ref="website_slides.slide_channel_demo_3_furn0"/>
-        <field name="sequence">10</field>
-    </record>
 
     <record id="slide_channel_demo_4_furn1" model="slide.channel">
         <field name="name">Furniture Technical Specifications</field>
@@ -109,11 +84,6 @@ will learn the basics of wood characteristics.</field>
         <field name="description">If you are looking for technical specifications, have a look at this documentation.</field>
         <field name="create_date" eval="DateTime.now() - relativedelta(days=4)"/>
     </record>
-    <record id="slide_category_demo_4_0" model="slide.category">
-        <field name="name">Introduction</field>
-        <field name="channel_id" ref="website_slides.slide_channel_demo_4_furn1"/>
-        <field name="sequence">10</field>
-    </record>
 
     <record id="slide_channel_demo_5_furn2" model="slide.channel">
         <field name="name">Basics of Furniture Creation</field>
@@ -131,21 +101,6 @@ will learn the basics of wood characteristics.</field>
         <field name="description">All you need to know about furniture creation.</field>
         <field name="create_date" eval="DateTime.now() - relativedelta(days=3)"/>
     </record>
-    <record id="slide_category_demo_5_0" model="slide.category">
-        <field name="name">Tools and Methods</field>
-        <field name="channel_id" ref="website_slides.slide_channel_demo_5_furn2"/>
-        <field name="sequence">10</field>
-    </record>
-    <record id="slide_category_demo_5_1" model="slide.category">
-        <field name="name">Hand on !</field>
-        <field name="channel_id" ref="website_slides.slide_channel_demo_5_furn2"/>
-        <field name="sequence">20</field>
-    </record>
-    <record id="slide_category_demo_5_2" model="slide.category">
-        <field name="name">Test Yourself</field>
-        <field name="channel_id" ref="website_slides.slide_channel_demo_5_furn2"/>
-        <field name="sequence">30</field>
-    </record>
 
     <!-- This channel will be set on payment and receive certifications capabilities -->
     <record id="slide_channel_demo_6_furn3" model="slide.channel">
diff --git a/addons/website_slides/data/slide_slide_demo.xml b/addons/website_slides/data/slide_slide_demo.xml
index e097b4de33b6..b35f2defc5f7 100644
--- a/addons/website_slides/data/slide_slide_demo.xml
+++ b/addons/website_slides/data/slide_slide_demo.xml
@@ -10,7 +10,6 @@
         <field name="image_1920" type="base64" file="website_slides/static/src/img/channel-training-default.jpg"/>
         <field name="slide_type">presentation</field>
         <field name="channel_id" ref="website_slides.slide_channel_demo_0_gard_0"/>
-        <field name="category_id" eval="False"/>
         <field name="is_published" eval="True"/>
         <field name="is_preview" eval="True"/>
         <field name="public_views">10</field>
@@ -24,7 +23,6 @@
         <field name="image_1920" type="base64" file="website_slides/static/src/img/slide_demo_gardening_1.jpg"/>
         <field name="slide_type">infographic</field>
         <field name="channel_id" ref="website_slides.slide_channel_demo_0_gard_0"/>
-        <field name="category_id" eval="False"/>
         <field name="is_published" eval="True"/>
         <field name="is_preview" eval="False"/>
         <field name="public_views">5</field>
@@ -35,13 +33,28 @@
 
     <!-- CHANNEL 1: Taking care of Trees -->
     <!-- ================================================== -->
+
+    <!--                    Categories                      -->
+    <record id="slide_category_demo_1_0" model="slide.slide">
+        <field name="name">Interesting Facts</field>
+        <field name="is_category" eval="True"/>
+        <field name="channel_id" ref="website_slides.slide_channel_demo_1_gard1"/>
+        <field name="sequence">0</field>
+    </record>
+    <record id="slide_category_demo_1_1" model="slide.slide">
+        <field name="name">Methods</field>
+        <field name="is_category" eval="True"/>
+        <field name="channel_id" ref="website_slides.slide_channel_demo_1_gard1"/>
+        <field name="sequence">4</field>
+    </record>
+
+    <!--                    Slides                      -->
     <record id="slide_slide_demo_1_0" model="slide.slide">
         <field name="name">Tree Infographic</field>
         <field name="sequence">1</field>
         <field name="image_1920" type="base64" file="website_slides/static/src/img/slide_demo_tree_infographic_1.jpg"/>
         <field name="slide_type">infographic</field>
         <field name="channel_id" ref="website_slides.slide_channel_demo_1_gard1"/>
-        <field name="category_id" ref="website_slides.slide_category_demo_1_0"/>
         <field name="is_published" eval="True"/>
         <field name="is_preview" eval="True"/>
         <field name="public_views">5</field>
@@ -55,7 +68,6 @@
         <field name="image_1920" type="base64" file="website_slides/static/src/img/slide_demo_tree_infographic_2.jpg"/>
         <field name="slide_type">infographic</field>
         <field name="channel_id" ref="website_slides.slide_channel_demo_1_gard1"/>
-        <field name="category_id" ref="website_slides.slide_category_demo_1_0"/>
         <field name="is_published" eval="True"/>
         <field name="is_preview" eval="False"/>
         <field name="public_views">5</field>
@@ -69,7 +81,6 @@
         <field name="image_1920" type="base64" file="website_slides/static/src/img/slide_demo_tree_infographic_3.jpg"/>
         <field name="slide_type">infographic</field>
         <field name="channel_id" ref="website_slides.slide_channel_demo_1_gard1"/>
-        <field name="category_id" ref="website_slides.slide_category_demo_1_0"/>
         <field name="is_published" eval="True"/>
         <field name="is_preview" eval="False"/>
         <field name="public_views">10</field>
@@ -89,13 +100,12 @@
         </record>
     <record id="slide_slide_demo_1_3" model="slide.slide">
         <field name="name">How to plant a potted tree</field>
-        <field name="sequence">4</field>
+        <field name="sequence">5</field>
         <field name="url">https://www.youtube.com/watch?v=QYmgrw0PgLU</field>
         <field name="image_1920" type="base64" file="website_slides/static/src/img/slide_demo_thumb_QYmgrw0PgLU.jpg"/>
         <field name="document_id">QYmgrw0PgLU</field>
         <field name="slide_type">video</field>
         <field name="channel_id" ref="website_slides.slide_channel_demo_1_gard1"/>
-        <field name="category_id" ref="website_slides.slide_category_demo_1_1"/>
         <field name="is_published" eval="True"/>
         <field name="is_preview" eval="False"/>
         <field name="public_views">0</field>
@@ -105,11 +115,10 @@
     </record>
     <record id="slide_slide_demo_1_4" model="slide.slide">
         <field name="name">A little chat with Harry Potted</field>
-        <field name="sequence">5</field>
+        <field name="sequence">6</field>
         <field name="image_1920" type="base64" file="website_slides/static/src/img/slide_demo_tree_img_1.jpg"/>
         <field name="slide_type">webpage</field>
         <field name="channel_id" ref="website_slides.slide_channel_demo_1_gard1"/>
-        <field name="category_id" ref="website_slides.slide_category_demo_1_1"/>
         <field name="html_content" type="html">
 <section class="s_cover parallax bg-black-50 pt16 pb16" data-scroll-background-ratio="0" style="background-image: none;">
     <span class="s_parallax_bg oe_img_bg oe_custom_bg" style="background-image: url('/website_slides/static/src/img/slide_demo_tree_img_1.jpg'); background-position: 50% 0;"></span>
@@ -202,7 +211,6 @@
         <field name="image_1920" type="base64" file="website_slides/static/src/img/channel-training-default.jpg"/>
         <field name="slide_type">presentation</field>
         <field name="channel_id" ref="website_slides.slide_channel_demo_1_gard1"/>
-        <field name="category_id" ref="website_slides.slide_category_demo_1_1"/>
         <field name="is_published" eval="True"/>
         <field name="is_preview" eval="False"/>
         <field name="public_views">10</field>
@@ -213,6 +221,22 @@
 
     <!-- CHANNEL 2: Trees, Wood and Garden -->
     <!-- ================================================== -->
+
+    <!--                    Categories                      -->
+    <record id="slide_category_demo_2_0" model="slide.slide">
+        <field name="name">Trees</field>
+        <field name="is_category" eval="True"/>
+        <field name="channel_id" ref="website_slides.slide_channel_demo_2_gard2"/>
+        <field name="sequence">0</field>
+    </record>
+    <record id="slide_category_demo_2_1" model="slide.slide">
+        <field name="name">Wood</field>
+        <field name="is_category" eval="True"/>
+        <field name="channel_id" ref="website_slides.slide_channel_demo_2_gard2"/>
+        <field name="sequence">4</field>
+    </record>
+
+    <!--                    Slides                      -->
     <record id="slide_slide_demo_2_0" model="slide.slide">
         <field name="name">Main Trees Categories</field>
         <field name="sequence">1</field>
@@ -220,7 +244,6 @@
         <field name="image_1920" type="base64" file="website_slides/static/src/img/slide_demo_tree_img_2.jpg"/>
         <field name="slide_type">presentation</field>
         <field name="channel_id" ref="website_slides.slide_channel_demo_2_gard2"/>
-        <field name="category_id" ref="website_slides.slide_category_demo_2_0"/>
         <field name="is_published" eval="True"/>
         <field name="is_preview" eval="True"/>
         <field name="public_views">0</field>
@@ -281,7 +304,6 @@
         <field name="image_1920" type="base64" file="website_slides/static/src/img/slide_demo_tree_img_3.jpg"/>
         <field name="slide_type">webpage</field>
         <field name="channel_id" ref="website_slides.slide_channel_demo_2_gard2"/>
-        <field name="category_id" ref="website_slides.slide_category_demo_2_0"/>
         <field name="html_content" type="html">
 <section class="s_cover parallax bg-black-50 pt16 pb16" data-scroll-background-ratio="0" style="background-image: none;">
     <span class="s_parallax_bg oe_img_bg oe_custom_bg" style="background-image: url('/website_slides/static/src/img/slide_demo_tree_img_3.jpg'); background-position: 50% 0;"></span>
@@ -326,12 +348,12 @@
     </record>
     <record id="slide_slide_demo_2_2" model="slide.slide">
         <field name="name">Tree planting in hanging bottles on wall</field>
+        <field name="sequence">3</field>
         <field name="url">https://www.youtube.com/watch?v=ebBez6bcSEc</field>
         <field name="image_1920" type="base64" file="website_slides/static/src/img/slide_demo_thumb_ebBez6bcSEc.jpg"/>
         <field name="document_id">ebBez6bcSEc</field>
         <field name="slide_type">video</field>
         <field name="channel_id" ref="website_slides.slide_channel_demo_2_gard2"/>
-        <field name="category_id" ref="website_slides.slide_category_demo_2_0"/>
         <field name="is_published" eval="True"/>
         <field name="is_preview" eval="False"/>
         <field name="public_views">0</field>
@@ -340,8 +362,54 @@
         <field name="description">How to wall decorating by tree planting in hanging plastic bottles.</field>
     </record>
 
+    <!-- CHANNEL 3: Choose your wood !           -->
+    <!-- ======================================= -->
+
+    <!--                 Categories              -->
+    <record id="slide_category_demo_3_0" model="slide.slide">
+        <field name="name">Introduction</field>
+        <field name="is_category" eval="True"/>
+        <field name="channel_id" ref="website_slides.slide_channel_demo_3_furn0"/>
+        <field name="sequence">0</field>
+    </record>
+
+    <!-- CHANNEL 4: Furniture Technical Specifications -->
+    <!-- ======================================= -->
+
+    <!--                 Categories              -->
+    <record id="slide_category_demo_4_0" model="slide.slide">
+        <field name="name">Introduction</field>
+        <field name="is_category" eval="True"/>
+        <field name="channel_id" ref="website_slides.slide_channel_demo_4_furn1"/>
+        <field name="sequence">0</field>
+    </record>
+
     <!-- CHANNEL 5: Basics of Furniture Creation -->
     <!-- ======================================= -->
+
+    <!--                 Categories              -->
+    <record id="slide_category_demo_5_0" model="slide.slide">
+        <field name="name">Tools and Methods</field>
+        <field name="is_category" eval="True"/>
+        <field name="channel_id" ref="website_slides.slide_channel_demo_5_furn2"/>
+        <field name="sequence">0</field>
+    </record>
+
+    <record id="slide_category_demo_5_1" model="slide.slide">
+        <field name="name">Hand on !</field>
+        <field name="is_category" eval="True"/>
+        <field name="channel_id" ref="website_slides.slide_channel_demo_5_furn2"/>
+        <field name="sequence">3</field>
+    </record>
+
+    <record id="slide_category_demo_5_2" model="slide.slide">
+        <field name="name">Test Yourself</field>
+        <field name="is_category" eval="True"/>
+        <field name="channel_id" ref="website_slides.slide_channel_demo_5_furn2"/>
+        <field name="sequence">5</field>
+    </record>
+
+    <!--                    Slides                     -->
     <record id="slide_slide_demo_5_0" model="slide.slide">
         <field name="name">Unforgettable Tools</field>
         <field name="sequence">1</field>
@@ -349,7 +417,6 @@
         <field name="image_1920" type="base64" file="website_slides/static/src/img/channel-training-default.jpg"/>
         <field name="slide_type">presentation</field>
         <field name="channel_id" ref="website_slides.slide_channel_demo_5_furn2"/>
-        <field name="category_id" ref="website_slides.slide_category_demo_5_0"/>
         <field name="is_published" eval="True"/>
         <field name="is_preview" eval="True"/>
         <field name="public_views">10</field>
@@ -375,8 +442,6 @@
         <field name="document_id">5WMqwTnZ-qs</field>
         <field name="slide_type">video</field>
         <field name="channel_id" ref="website_slides.slide_channel_demo_5_furn2"/>
-        <field name="category_id" ref="website_slides.slide_category_demo_5_1"/>
-        <field name="category_id" eval="False"/>
         <field name="is_published" eval="True"/>
         <field name="is_preview" eval="False"/>
         <field name="public_views">0</field>
@@ -385,14 +450,12 @@
     </record>
      <record id="slide_slide_demo_5_2" model="slide.slide">
         <field name="name">How to create your own piece of furniture</field>
-        <field name="sequence">3</field>
+        <field name="sequence">4</field>
         <field name="image_1920" type="base64" file="website_slides/static/src/img/slide_demo_thumb_ptjeDDoURL8.jpg"/>
         <field name="url">https://www.youtube.com/watch?v=ptjeDDoURL8</field>
         <field name="document_id">ptjeDDoURL8</field>
         <field name="slide_type">video</field>
         <field name="channel_id" ref="website_slides.slide_channel_demo_5_furn2"/>
-        <field name="category_id" ref="website_slides.slide_category_demo_5_1"/>
-        <field name="category_id" eval="False"/>
         <field name="is_published" eval="True"/>
         <field name="is_preview" eval="True"/>
         <field name="public_views">5</field>
@@ -401,11 +464,10 @@
     </record>
     <record id="slide_slide_demo_5_3" model="slide.slide">
         <field name="name">Test your knowledge !</field>
-        <field name="sequence">4</field>
+        <field name="sequence">6</field>
         <field name="image_1920" type="base64" file="website_slides/static/src/img/slide_demo_owl.jpg"/>
         <field name="slide_type">quiz</field>
         <field name="channel_id" ref="website_slides.slide_channel_demo_5_furn2"/>
-        <field name="category_id" ref="website_slides.slide_category_demo_5_0"/>
         <field name="is_published" eval="True"/>
         <field name="is_preview" eval="False"/>
         <field name="public_views">0</field>
@@ -430,4 +492,4 @@
     <!-- CHANNEL 6: DIY Furniture -->
     <!-- ======================================= -->
 
-</data></odoo>
+</data></odoo>
\ No newline at end of file
diff --git a/addons/website_slides/models/slide_channel.py b/addons/website_slides/models/slide_channel.py
index 4c1730b5600d..1c8f7b434850 100644
--- a/addons/website_slides/models/slide_channel.py
+++ b/addons/website_slides/models/slide_channel.py
@@ -101,9 +101,10 @@ class Channel(models.Model):
     tag_ids = fields.Many2many(
         'slide.channel.tag', 'slide_channel_tag_rel', 'channel_id', 'tag_id',
         string='Tags', help='Used to categorize and filter displayed channels/courses')
-    category_ids = fields.One2many('slide.category', 'channel_id', string="Categories")
     # slides: promote, statistics
-    slide_ids = fields.One2many('slide.slide', 'channel_id', string="Slides")
+    slide_ids = fields.One2many('slide.slide', 'channel_id', string="Slides and categories")
+    slide_content_ids = fields.One2many('slide.slide', string='Slides', compute="_compute_category_and_slide_ids")
+    slide_category_ids = fields.One2many('slide.slide', string='Categories', compute="_compute_category_and_slide_ids")
     slide_last_update = fields.Date('Last Update', compute='_compute_slide_last_update', store=True)
     slide_partner_ids = fields.One2many('slide.slide.partner', 'channel_id', string="Slide User Data", groups='website.group_website_publisher')
     promote_strategy = fields.Selection([
@@ -206,12 +207,18 @@ class Channel(models.Model):
         for channel in self:
             channel.is_member = channel.is_member = self.env.user.partner_id.id in result.get(channel.id, [])
 
+    @api.depends('slide_ids.is_category')
+    def _compute_category_and_slide_ids(self):
+        for channel in self:
+            channel.slide_category_ids = channel.slide_ids.filtered(lambda slide: slide.is_category)
+            channel.slide_content_ids = channel.slide_ids - channel.slide_category_ids
+
     @api.depends('slide_ids.slide_type', 'slide_ids.is_published', 'slide_ids.completion_time',
-                 'slide_ids.likes', 'slide_ids.dislikes', 'slide_ids.total_views')
+                 'slide_ids.likes', 'slide_ids.dislikes', 'slide_ids.total_views', 'slide_ids.is_category')
     def _compute_slides_statistics(self):
         result = dict((cid, dict(total_views=0, total_votes=0, total_time=0)) for cid in self.ids)
         read_group_res = self.env['slide.slide'].read_group(
-            [('website_published', '=', True), ('channel_id', 'in', self.ids)],
+            [('is_published', '=', True), ('channel_id', 'in', self.ids), ('is_category', '=', False)],
             ['channel_id', 'slide_type', 'likes', 'dislikes', 'total_views', 'completion_time'],
             groupby=['channel_id', 'slide_type'],
             lazy=False)
@@ -233,13 +240,13 @@ class Channel(models.Model):
         """ Compute statistics based on all existing slide types """
         slide_types = self.env['slide.slide']._fields['slide_type'].get_values(self.env)
         keys = ['nbr_%s' % slide_type for slide_type in slide_types]
-        keys.append('total_slides')
         result = dict((cid, dict((key, 0) for key in keys)) for cid in self.ids)
         for res_group in read_group_res:
             cid = res_group['channel_id'][0]
+            result[cid]['total_slides'] = 0
             for slide_type in slide_types:
                 result[cid]['nbr_%s' % slide_type] += res_group.get('slide_type', '') == slide_type and res_group['__count'] or 0
-                result[cid]['total_slides'] += res_group.get('slide_type', '') == slide_type and res_group['__count'] or 0
+                result[cid]['total_slides'] += result[cid]['nbr_%s' % slide_type]
         return result
 
     def _compute_rating_stats(self):
@@ -535,7 +542,7 @@ class Channel(models.Model):
         """ Return an ordered structure of slides by categories within a given
         base_domain that must fulfill slides. """
         self.ensure_one()
-        all_categories = self.env['slide.category'].search([('channel_id', '=', self.id)])
+        all_categories = self.env['slide.slide'].sudo().search([('channel_id', '=', self.id), ('is_category', '=', True)])
         all_slides = self.env['slide.slide'].sudo().search(base_domain, order=order)
         category_data = []
 
@@ -561,49 +568,25 @@ class Channel(models.Model):
             })
         return category_data
 
-
-class Category(models.Model):
-    """ Channel contain various categories to manage its slides """
-    _name = 'slide.category'
-    _description = "Slides Category"
-    _order = "sequence, id"
-
-    name = fields.Char('Name', translate=True, required=True)
-    channel_id = fields.Many2one('slide.channel', string="Channel", required=True, ondelete='cascade')
-    sequence = fields.Integer(default=10, help='Display order')
-    slide_ids = fields.One2many('slide.slide', 'category_id', string="Slides")
-    nbr_presentation = fields.Integer("Number of Presentations", compute='_count_presentations', store=True)
-    nbr_document = fields.Integer("Number of Documents", compute='_count_presentations', store=True)
-    nbr_video = fields.Integer("Number of Videos", compute='_count_presentations', store=True)
-    nbr_infographic = fields.Integer("Number of Infographics", compute='_count_presentations', store=True)
-    nbr_webpage = fields.Integer("Number of Webpages", compute='_count_presentations', store=True)
-    nbr_quiz = fields.Integer("Number of Quizs", compute="_count_presentations", store=True)
-    total_slides = fields.Integer(compute='_count_presentations', store=True)
-
-    @api.depends('slide_ids.slide_type', 'slide_ids.is_published')
-    def _count_presentations(self):
-        result = dict.fromkeys(self.ids, dict())
-        res = self.env['slide.slide'].read_group(
-            [('website_published', '=', True), ('category_id', 'in', self.ids)],
-            ['category_id', 'slide_type'], ['category_id', 'slide_type'],
-            lazy=False)
-
-        type_stats = self._compute_slides_statistics_type(res)
-        for cid, cdata in type_stats.items():
-            result[cid].update(cdata)
-
-        for record in self:
-            record.update(result[record.id])
-
-    def _compute_slides_statistics_type(self, read_group_res):
-        """ Compute statistics based on all existing slide types """
-        slide_types = self.env['slide.slide']._fields['slide_type'].get_values(self.env)
-        keys = ['nbr_%s' % slide_type for slide_type in slide_types]
-        keys.append('total_slides')
-        result = dict((cid, dict((key, 0) for key in keys)) for cid in self.ids)
-        for res_group in read_group_res:
-            cid = res_group['category_id'][0]
-            for slide_type in slide_types:
-                result[cid]['nbr_%s' % slide_type] += res_group.get('slide_type', '') == slide_type and res_group['__count'] or 0
-                result[cid]['total_slides'] += res_group.get('slide_type', '') == slide_type and res_group['__count'] or 0
-        return result
+    def _resequence_slides(self, slide):
+        ids_to_resequence = self.slide_ids.ids
+        index_of_added_slide = ids_to_resequence.index(slide.id)
+        category_id = slide.category_id.id
+        next_category_id = None
+        if self.slide_category_ids:
+            index_of_category = self.slide_category_ids.ids.index(category_id) if category_id else None
+            if index_of_category is None:
+                next_category_id = self.slide_category_ids.ids[0]
+            elif index_of_category < len(self.slide_category_ids.ids) - 1:
+                next_category_id = self.slide_category_ids.ids[index_of_category + 1]
+
+        if next_category_id:
+            added_slide_id = ids_to_resequence.pop(index_of_added_slide)
+            index_of_next_category = ids_to_resequence.index(next_category_id)
+            ids_to_resequence.insert(index_of_next_category, added_slide_id)
+            for i, record in enumerate(self.env['slide.slide'].browse(ids_to_resequence)):
+                record.write({'sequence': i})
+        else:
+            slide.write({
+                'sequence': self.env['slide.slide'].browse(ids_to_resequence[-1]).sequence + 1
+            })
diff --git a/addons/website_slides/models/slide_slide.py b/addons/website_slides/models/slide_slide.py
index 493b908dde46..e9ebe0398863 100644
--- a/addons/website_slides/models/slide_slide.py
+++ b/addons/website_slides/models/slide_slide.py
@@ -89,25 +89,27 @@ class Slide(models.Model):
     _description = 'Slides'
     _mail_post_access = 'read'
     _order_by_strategy = {
-        'sequence': 'category_sequence asc, sequence asc',
+        'sequence': 'sequence asc',
         'most_viewed': 'total_views desc',
         'most_voted': 'likes desc',
         'latest': 'date_published desc',
     }
-    _order = 'category_sequence asc, sequence asc'
+    _order = 'sequence asc, is_category asc'
 
     # 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)
+    sequence = fields.Integer('Sequence', default=0)
     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)
-    category_id = fields.Many2one('slide.category', string="Category", domain="[('channel_id', '=', channel_id)]")
     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('# Hours', default=1, digits=(10, 4))
+    # Categories
+    is_category = fields.Boolean('Is a category', default=False)
+    category_id = fields.Many2one('slide.slide', string="Category", 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',
                                    string='Subscribers', groups='website.group_website_publisher')
@@ -156,11 +158,41 @@ class Slide(models.Model):
     # channel
     channel_type = fields.Selection(related="channel_id.channel_type", string="Channel type")
     channel_allow_comment = fields.Boolean(related="channel_id.allow_comment", string="Allows comment")
+    # Statistics in case the slide is a category
+    nbr_presentation = fields.Integer("Number of Presentations", compute='_compute_slides_statistics', store=True)
+    nbr_document = fields.Integer("Number of Documents", compute='_compute_slides_statistics', store=True)
+    nbr_video = fields.Integer("Number of Videos", compute='_compute_slides_statistics', store=True)
+    nbr_infographic = fields.Integer("Number of Infographics", compute='_compute_slides_statistics', store=True)
+    nbr_webpage = fields.Integer("Number of Webpages", compute='_compute_slides_statistics', store=True)
+    nbr_quiz = fields.Integer("Number of Quizs", compute="_compute_slides_statistics", store=True)
+    total_slides = fields.Integer(compute='_compute_slides_statistics', store=True)
 
     _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.")
     ]
 
+    @api.depends('channel_id.slide_ids.is_category', 'channel_id.slide_ids.sequence')
+    def _compute_category_id(self):
+        """ Will take all the slides of the channel for which the index is higher
+        than the index of this category and lower than the index of the next category.
+
+        Lists are manually sorted because when adding a new browse record order
+        will not be correct as the added slide would actually end up at the
+        first place no matter its sequence."""
+        channel_slides = dict.fromkeys(self.mapped('channel_id').ids, self.env['slide.slide'])
+        for slide in self:
+            channel_slides[slide.channel_id.id] += slide
+
+        for cid, slides in channel_slides.items():
+            current_category = self.env['slide.slide']
+            slide_list = list(slides)
+            slide_list.sort(key=lambda s: (s.sequence, not s.is_category))
+            for slide in slide_list:
+                if slide.is_category:
+                    current_category = slide
+                elif slide.category_id != current_category:
+                    slide.category_id = current_category.id
+
     @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:
@@ -201,6 +233,34 @@ class Slide(models.Model):
         for slide in self:
             slide.slide_views = mapped_data.get(slide.id, 0)
 
+    @api.depends('slide_ids.slide_type', 'slide_ids.is_published', 'slide_ids.is_category')
+    def _compute_slides_statistics(self):
+        result = dict.fromkeys(self.ids, dict())
+        res = self.env['slide.slide'].read_group(
+            [('is_published', '=', True), ('category_id', 'in', self.ids), ('is_category', '=', False)],
+            ['category_id', 'slide_type'], ['category_id', 'slide_type'],
+            lazy=False)
+
+        type_stats = self._compute_slides_statistics_type(res)
+        for cid, cdata in type_stats.items():
+            result[cid].update(cdata)
+
+        for record in self:
+            record.update(result[record.id])
+
+    def _compute_slides_statistics_type(self, read_group_res):
+        """ Compute statistics based on all existing slide types """
+        slide_types = self.env['slide.slide']._fields['slide_type'].get_values(self.env)
+        keys = ['nbr_%s' % slide_type for slide_type in slide_types]
+        result = dict((cid, dict((key, 0) for key in keys)) for cid in self.ids)
+        for res_group in read_group_res:
+            cid = res_group['category_id'][0]
+            result[cid]['total_slides'] = 0
+            for slide_type in slide_types:
+                result[cid]['nbr_%s' % slide_type] += res_group.get('slide_type', '') == slide_type and res_group['__count'] or 0
+                result[cid]['total_slides'] += result[cid]['nbr_%s' % slide_type]
+        return result
+
     @api.depends('slide_partner_ids.partner_id')
     def _compute_user_membership_id(self):
         slide_partners = self.env['slide.slide.partner'].sudo().search([
@@ -288,6 +348,9 @@ class Slide(models.Model):
             values['index_content'] = values.get('description')
         if values.get('slide_type') == 'infographic' and not values.get('image_1920'):
             values['image_1920'] = values['datas']
+        if values.get('is_category'):
+            values['is_preview'] = True
+            values['is_published'] = True
         if values.get('is_published') and not values.get('date_published'):
             values['date_published'] = datetime.datetime.now()
         if values.get('url') and not values.get('document_id'):
@@ -306,6 +369,9 @@ class Slide(models.Model):
             doc_data = self._parse_document_url(values['url']).get('values', dict())
             for key, value in doc_data.items():
                 values.setdefault(key, value)
+        if values.get('is_category'):
+            values['is_preview'] = True
+            values['is_published'] = True
 
         res = super(Slide, self).write(values)
         if values.get('is_published'):
@@ -313,6 +379,14 @@ class Slide(models.Model):
             self._post_publication()
         return res
 
+    @api.returns('self', lambda value: value.id)
+    def copy(self, default=None):
+        """Sets the sequence to zero so that it always lands at the beginning
+        of the newly selected course as an uncategorized slide"""
+        rec = super(Slide, self).copy(default)
+        rec.sequence = 0
+        return rec
+
     # ---------------------------------------------------------
     # Mail/Rating
     # ---------------------------------------------------------
diff --git a/addons/website_slides/security/ir.model.access.csv b/addons/website_slides/security/ir.model.access.csv
index 82fbf8312ea0..2efb0211b169 100644
--- a/addons/website_slides/security/ir.model.access.csv
+++ b/addons/website_slides/security/ir.model.access.csv
@@ -17,8 +17,6 @@ access_slide_channel_all,slide.channel.all,model_slide_channel,,1,0,0,0
 access_slide_channel_publisher,slide.channel.publisher,model_slide_channel,website.group_website_publisher,1,1,1,1
 access_slide_channel_partners_all,slide.channel.users.all,model_slide_channel_partner,,0,0,0,0
 access_slide_channel_partners_system,slide.channel.users.system,model_slide_channel_partner,website.group_website_publisher,1,1,1,1
-access_slide_category_all,slide.category.all,model_slide_category,,1,0,0,0
-access_slide_category_publisher,slide.category.publisher,model_slide_category,website.group_website_publisher,1,1,1,1
 access_slide_embed_all,slide.embed.all,model_slide_embed,,1,0,0,0
 access_slide_embed_user,slide.embed.user,model_slide_embed,base.group_user,1,1,1,1
 access_slide_slide_link_all,slide.slide.link.all,model_slide_slide_link,,1,0,0,0
diff --git a/addons/website_slides/security/website_slides_security.xml b/addons/website_slides/security/website_slides_security.xml
index e5ab59748d00..aeb16d43c1de 100644
--- a/addons/website_slides/security/website_slides_security.xml
+++ b/addons/website_slides/security/website_slides_security.xml
@@ -32,12 +32,12 @@
         </record>
 
         <record id="rule_slide_slide_not_website" model="ir.rule">
-            <field name="name">Slide: public/portal/user: restricted to published or uploaded by user, and either channel member or public channel &amp; previewable</field>
+            <field name="name">Slide: public/portal/user: restricted to published or uploaded by user, and either channel member or public channel &amp; (category or previewable)</field>
             <field name="model_id" ref="model_slide_slide"/>
             <field name="groups" eval="[(4, ref('base.group_public')), (4, ref('base.group_portal')), (4, ref('base.group_user'))]"/>
             <field name="domain_force">['&amp;',
     '|',
-        '&amp;', ('channel_id.visibility', '=', 'public'), ('is_preview', '=', True),
+        '&amp;', ('channel_id.visibility', '=', 'public'), '|', ('is_category','=', True), ('is_preview', '=', True),
         ('channel_id.partner_ids', '=', user.partner_id.id),
     '&amp;', ('channel_id.website_published', '=', True), '|', ('user_id', '=', user.id), ('website_published', '=', True)]</field>
         </record>
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 a43844a6b33f..0c35bcd65fb2 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
@@ -30,7 +30,7 @@ publicWidget.registry.websiteSlidesCourseSlidesList = publicWidget.Widget.extend
     _bindSortable: function () {
         this.$('ul.o_wslides_js_slides_list_container').sortable({
             handle: '.o_wslides_slides_list_drag',
-            stop: this._reorderCategories.bind(this),
+            stop: this._reorderSlides.bind(this),
             items: '.o_wslides_slide_list_category',
             placeholder: 'o_wslides_slides_list_slide_hilight position-relative mb-1'
         });
@@ -62,61 +62,20 @@ publicWidget.registry.websiteSlidesCourseSlidesList = publicWidget.Widget.extend
         });
     },
 
-    _getCategories: function (){
+    _getSlides: function (){
         var categories = [];
-        this.$('.o_wslides_js_category').each(function (){
-            categories.push(parseInt($(this).data('categoryId')));
+        this.$('.o_wslides_js_list_item').each(function (){
+            categories.push(parseInt($(this).data('slideId')));
         });
         return categories;
     },
-
-    /**
-     * Returns a slides dict in the form:
-     * {slide_id: {'sequence': slide_sequence, 'category_id': slide.category_id.id}}
-     *
-     *
-     * (Uncategorized slides don't have the category_id key)
-     *
-     * @private
-     */
-    _getSlides: function (){
-        var slides = {};
-        this.$('li.o_wslides_slides_list_slide[data-slide-id]').each(function (index){
-            var $slide = $(this);
-            var values = {
-                sequence: index
-            };
-
-            var categoryId = $slide.closest('.o_wslides_slide_list_category').data('categoryId');
-            if (typeof categoryId !== typeof undefined && categoryId !== false) {
-                values.category_id = categoryId;
-            }
-
-            slides[$slide.data('slideId')] = values;
-        });
-
-        return slides;
-    },
-
-    _reorderCategories: function (){
+    _reorderSlides: function (){
         var self = this;
         self._rpc({
             route: '/web/dataset/resequence',
             params: {
-                model: "slide.category",
-                ids: self._getCategories()
-            }
-        });
-    },
-
-    _reorderSlides: function (){
-        this._checkForEmptySections();
-
-        this._rpc({
-            route: "/slides/channel/resequence",
-            params: {
-                channel_id: this.channelId,
-                slides_data: this._getSlides()
+                model: "slide.slide",
+                ids: self._getSlides()
             }
         });
     },
diff --git a/addons/website_slides/tests/__init__.py b/addons/website_slides/tests/__init__.py
index 466b2b82674e..a984e03ac619 100644
--- a/addons/website_slides/tests/__init__.py
+++ b/addons/website_slides/tests/__init__.py
@@ -2,8 +2,7 @@
 # Part of Odoo. See LICENSE file for full copyright and licensing details.
 
 from . import common
+from . import test_slide_utils
+from . import test_karma
 from . import test_security
 from . import test_statistics
-from . import test_karma
-
-from . import test_from_url
\ No newline at end of file
diff --git a/addons/website_slides/tests/common.py b/addons/website_slides/tests/common.py
index 29d0405dec1d..e979fade22ef 100644
--- a/addons/website_slides/tests/common.py
+++ b/addons/website_slides/tests/common.py
@@ -11,35 +11,36 @@ slides_new_test_user = partial(new_test_user, context={'mail_create_nolog': True
 
 class SlidesCase(common.SavepointCase):
 
-    def setUp(self):
-        super(SlidesCase, self).setUp()
+    @classmethod
+    def setUpClass(cls):
+        super(SlidesCase, cls).setUpClass()
 
-        self.user_publisher = slides_new_test_user(
-            self.env, name='Paul Publisher', login='user_publisher', email='publisher@example.com',
+        cls.user_publisher = slides_new_test_user(
+            cls.env, name='Paul Publisher', login='user_publisher', email='publisher@example.com',
             groups='base.group_user,website.group_website_publisher'
         )
 
-        self.user_emp = slides_new_test_user(
-            self.env, name='Eglantine Employee', login='user_emp', email='employee@example.com',
+        cls.user_emp = slides_new_test_user(
+            cls.env, name='Eglantine Employee', login='user_emp', email='employee@example.com',
             groups='base.group_user'
         )
 
-        self.user_portal = slides_new_test_user(
-            self.env, name='Patrick Portal', login='user_portal', email='portal@example.com',
+        cls.user_portal = slides_new_test_user(
+            cls.env, name='Patrick Portal', login='user_portal', email='portal@example.com',
             groups='base.group_portal'
         )
 
-        self.user_public = slides_new_test_user(
-            self.env, name='Pauline Public', login='user_public', email='public@example.com',
+        cls.user_public = slides_new_test_user(
+            cls.env, name='Pauline Public', login='user_public', email='public@example.com',
             groups='base.group_public'
         )
 
-        self.customer = self.env['res.partner'].create({
+        cls.customer = cls.env['res.partner'].create({
             'name': 'Caroline Customer',
             'email': 'customer@example.com',
         })
 
-        self.channel = self.env['slide.channel'].with_user(self.user_publisher).create({
+        cls.channel = cls.env['slide.channel'].with_user(cls.user_publisher).create({
             'name': 'Test Channel',
             'channel_type': 'documentation',
             'promote_strategy': 'most_voted',
@@ -50,21 +51,34 @@ class SlidesCase(common.SavepointCase):
             'karma_gen_slide_vote': 5,
             'karma_gen_channel_rank': 10,
         })
-        self.slide = self.env['slide.slide'].with_user(self.user_publisher).create({
+        cls.slide = cls.env['slide.slide'].with_user(cls.user_publisher).create({
             'name': 'How To Cook Humans',
-            'channel_id': self.channel.id,
+            'channel_id': cls.channel.id,
             'slide_type': 'presentation',
             'is_published': True,
             'completion_time': 2.0,
+            'sequence': 1,
+        })
+        cls.category = cls.env['slide.slide'].with_user(cls.user_publisher).create({
+            'name': 'Cooking Tips for Humans',
+            'channel_id': cls.channel.id,
+            'is_category': True,
+            'is_published': True,
+            'sequence': 2,
+        })
+        cls.slide_2 = cls.env['slide.slide'].with_user(cls.user_publisher).create({
+            'name': 'How To Cook For Humans',
+            'channel_id': cls.channel.id,
+            'slide_type': 'presentation',
+            'is_published': True,
+            'completion_time': 3.0,
+            'sequence': 3,
+        })
+        cls.slide_3 = cls.env['slide.slide'].with_user(cls.user_publisher).create({
+            'name': 'How To Cook Humans For Humans',
+            'channel_id': cls.channel.id,
+            'slide_type': 'document',
+            'is_published': True,
+            'completion_time': 1.5,
+            'sequence': 4,
         })
-
-    @contextmanager
-    def with_user(self, user):
-        """ Quick with_user environment """
-        old_uid = self.uid
-        try:
-            self.uid = user.id
-            yield
-        finally:
-            # back
-            self.uid = old_uid
diff --git a/addons/website_slides/tests/test_from_url.py b/addons/website_slides/tests/test_from_url.py
deleted file mode 100644
index dc9a1d6768e1..000000000000
--- a/addons/website_slides/tests/test_from_url.py
+++ /dev/null
@@ -1,30 +0,0 @@
-import odoo.tests
-
-
-class TestFromURL(odoo.tests.TransactionCase):
-    def test_youtube_urls(self):
-        urls = {
-            'W0JQcpGLSFw': [
-                'https://youtu.be/W0JQcpGLSFw',
-                'https://www.youtube.com/watch?v=W0JQcpGLSFw',
-                'https://www.youtube.com/watch?v=W0JQcpGLSFw&list=PL1-aSABtP6ACZuppkBqXFgzpNb2nVctZx',
-            ],
-            'vmhB-pt7EfA': [  # id starts with v, it is important
-                'https://youtu.be/vmhB-pt7EfA',
-                'https://www.youtube.com/watch?feature=youtu.be&v=vmhB-pt7EfA',
-                'https://www.youtube.com/watch?v=vmhB-pt7EfA&list=PL1-aSABtP6ACZuppkBqXFgzpNb2nVctZx&index=7',
-            ],
-            'hlhLv0GN1hA': [
-                'https://www.youtube.com/v/hlhLv0GN1hA',
-                'https://www.youtube.com/embed/hlhLv0GN1hA',
-                'https://m.youtube.com/watch?v=hlhLv0GN1hA'
-            ],
-        }
-
-        model = self.env['slide.slide']
-        for id, urls in urls.items():
-            for url in urls:
-                with self.subTest(url=url, id=id):
-                    document = model._find_document_data_from_url(url)
-                    self.assertEqual(document[0], 'youtube')
-                    self.assertEqual(document[1], id)
diff --git a/addons/website_slides/tests/test_karma.py b/addons/website_slides/tests/test_karma.py
index 05cbaa9c9817..3861d1064933 100644
--- a/addons/website_slides/tests/test_karma.py
+++ b/addons/website_slides/tests/test_karma.py
@@ -53,6 +53,8 @@ class TestKarmaGain(common.SlidesCase):
 
         # Finish the Course
         self.slide.with_user(user).action_set_completed()
+        self.assertFalse(self.channel.with_user(user).completed)
+        (self.slide_2 | self.slide_3).with_user(user).action_set_completed()
         self.assertTrue(self.channel.with_user(user).completed)
         computed_karma += self.channel.karma_gen_channel_finish
         self.assertEqual(user.karma, computed_karma)
@@ -93,6 +95,6 @@ class TestKarmaGain(common.SlidesCase):
         # Finish two course at the same time (should not ever happen but hey, we never know)
         (self.channel | self.channel_2)._action_add_members(user.partner_id)
 
-        (self.slide | self.slide_2_0 | self.slide_2_1).with_user(user).action_set_completed()
+        (self.slide | self.slide_2 | self.slide_3 | self.slide_2_0 | self.slide_2_1).with_user(user).action_set_completed()
         computed_karma += self.channel.karma_gen_channel_finish + self.channel_2.karma_gen_channel_finish
         self.assertEqual(user.karma, computed_karma)
diff --git a/addons/website_slides/tests/test_security.py b/addons/website_slides/tests/test_security.py
index 6cc498afaf4c..b1e6a979a9de 100644
--- a/addons/website_slides/tests/test_security.py
+++ b/addons/website_slides/tests/test_security.py
@@ -130,13 +130,12 @@ class TestAccessFeatures(common.SlidesCase):
     def test_channel_auto_subscription(self):
         user_employees = self.env['res.users'].search([('groups_id', 'in', self.ref('base.group_user'))])
 
-        with self.with_user(self.user_publisher):
-            channel = self.env['slide.channel'].create({
-                'name': 'Test',
-                'enroll': 'invite',
-                'is_published': True,
-                'enroll_group_ids': [(4, self.ref('base.group_user'))]
-            })
+        channel = self.env['slide.channel'].with_user(self.user_publisher).create({
+            'name': 'Test',
+            'enroll': 'invite',
+            'is_published': True,
+            'enroll_group_ids': [(4, self.ref('base.group_user'))]
+        })
 
         self.assertEqual(channel.partner_ids, user_employees.mapped('partner_id'))
 
diff --git a/addons/website_slides/tests/test_slide_utils.py b/addons/website_slides/tests/test_slide_utils.py
new file mode 100644
index 000000000000..d934d2d5b13a
--- /dev/null
+++ b/addons/website_slides/tests/test_slide_utils.py
@@ -0,0 +1,99 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+from odoo.addons.website_slides.tests import common as slides_common
+from odoo.tests.common import users
+
+
+class TestSequencing(slides_common.SlidesCase):
+
+    @users('user_publisher')
+    def test_category_update(self):
+        self.assertEqual(self.channel.slide_category_ids, self.category)
+        self.assertEqual(self.channel.slide_content_ids, self.slide | self.slide_2 | self.slide_3)
+        self.assertEqual(self.slide.category_id, self.env['slide.slide'])
+        self.assertEqual(self.slide_2.category_id, self.category)
+        self.assertEqual(self.slide_3.category_id, self.category)
+        self.assertEqual([s.id for s in self.channel.slide_ids], [self.slide.id, self.category.id, self.slide_2.id, self.slide_3.id])
+
+        self.slide.write({'sequence': 0})
+        self.assertEqual([s.id for s in self.channel.slide_ids], [self.slide.id, self.category.id, self.slide_2.id, self.slide_3.id])
+        self.assertEqual(self.slide_2.category_id, self.category)
+        self.slide_2.write({'sequence': 1})
+        self.channel.invalidate_cache()
+        self.assertEqual([s.id for s in self.channel.slide_ids], [self.slide.id, self.slide_2.id, self.category.id, self.slide_3.id])
+        self.assertEqual(self.slide_2.category_id, self.env['slide.slide'])
+
+        channel_2 = self.env['slide.channel'].create({
+            'name': 'Test2'
+        })
+        new_category = self.env['slide.slide'].create({
+            'name': 'NewCategorySlide',
+            'channel_id': channel_2.id,
+            'is_category': True,
+            'sequence': 1,
+        })
+        new_category_2 = self.env['slide.slide'].create({
+            'name': 'NewCategorySlide2',
+            'channel_id': channel_2.id,
+            'is_category': True,
+            'sequence': 2,
+        })
+        new_slide = self.env['slide.slide'].create({
+            'name': 'NewTestSlide',
+            'channel_id': channel_2.id,
+            'sequence': 2,
+        })
+        self.assertEqual(new_slide.category_id, new_category_2)
+        (new_slide | self.slide_3).write({'sequence': 1})
+        self.assertEqual(new_slide.category_id, new_category)
+        self.assertEqual(self.slide_3.category_id, self.env['slide.slide'])
+
+        (new_slide | self.slide_3).write({'sequence': 0})
+        self.assertEqual(new_slide.category_id, self.env['slide.slide'])
+        self.assertEqual(self.slide_3.category_id, self.env['slide.slide'])
+
+    @users('user_publisher')
+    def test_resequence(self):
+        self.category.write({'sequence': 4})
+        self.slide_2.write({'sequence': 8})
+        self.slide_3.write({'sequence': 3})
+
+        self.channel.invalidate_cache()
+        self.assertEqual([s.id for s in self.channel.slide_ids], [self.slide.id, self.slide_3.id, self.category.id, self.slide_2.id])
+
+        self.assertEqual(self.slide.sequence, 1)
+        # self.channel._resequence_slides(self.slide)
+        # self.channel.invalidate_cache()
+        # # self.assertEqual([s.id for s in self.channel.slide_ids], [self.slide.id, self.slide_3.id, self.category.id, self.slide_2.id])
+        # self.assertEqual(self.slide_3.sequence, 2)
+        # self.assertEqual(self.category.sequence, 3)
+        # self.assertEqual(self.slide_2.sequence, 4)
+
+
+class TestFromURL(slides_common.SlidesCase):
+    def test_youtube_urls(self):
+        urls = {
+            'W0JQcpGLSFw': [
+                'https://youtu.be/W0JQcpGLSFw',
+                'https://www.youtube.com/watch?v=W0JQcpGLSFw',
+                'https://www.youtube.com/watch?v=W0JQcpGLSFw&list=PL1-aSABtP6ACZuppkBqXFgzpNb2nVctZx',
+            ],
+            'vmhB-pt7EfA': [  # id starts with v, it is important
+                'https://youtu.be/vmhB-pt7EfA',
+                'https://www.youtube.com/watch?feature=youtu.be&v=vmhB-pt7EfA',
+                'https://www.youtube.com/watch?v=vmhB-pt7EfA&list=PL1-aSABtP6ACZuppkBqXFgzpNb2nVctZx&index=7',
+            ],
+            'hlhLv0GN1hA': [
+                'https://www.youtube.com/v/hlhLv0GN1hA',
+                'https://www.youtube.com/embed/hlhLv0GN1hA',
+                'https://m.youtube.com/watch?v=hlhLv0GN1hA'
+            ],
+        }
+
+        for id, urls in urls.items():
+            for url in urls:
+                with self.subTest(url=url, id=id):
+                    document = self.env['slide.slide']._find_document_data_from_url(url)
+                    self.assertEqual(document[0], 'youtube')
+                    self.assertEqual(document[1], id)
diff --git a/addons/website_slides/tests/test_statistics.py b/addons/website_slides/tests/test_statistics.py
index b602679e864b..5fb755496db0 100644
--- a/addons/website_slides/tests/test_statistics.py
+++ b/addons/website_slides/tests/test_statistics.py
@@ -12,37 +12,19 @@ from odoo.tools import mute_logger, float_compare
 
 
 @tagged('functional')
-class TestStatistics(common.SlidesCase):
-
-    def setUp(self):
-        super(TestStatistics, self).setUp()
-
-        self.slide_2 = self.env['slide.slide'].with_user(self.user_publisher).create({
-            'name': 'How To Cook For Humans',
-            'channel_id': self.channel.id,
-            'slide_type': 'presentation',
-            'is_published': True,
-            'completion_time': 3.0,
-        })
-        self.slide_3 = self.env['slide.slide'].with_user(self.user_publisher).create({
-            'name': 'How To Cook Humans For Humans',
-            'channel_id': self.channel.id,
-            'slide_type': 'document',
-            'is_published': True,
-            'completion_time': 1.5,
-        })
+class TestChannelStatistics(common.SlidesCase):
 
     @mute_logger('odoo.models')
     def test_channel_statistics(self):
         channel_publisher = self.channel.with_user(self.user_publisher)
         # slide type computation
-        self.assertEqual(channel_publisher.total_slides, len(channel_publisher.slide_ids))
-        self.assertEqual(channel_publisher.nbr_infographic, len(channel_publisher.slide_ids.filtered(lambda s: s.slide_type == 'infographic')))
-        self.assertEqual(channel_publisher.nbr_presentation, len(channel_publisher.slide_ids.filtered(lambda s: s.slide_type == 'presentation')))
-        self.assertEqual(channel_publisher.nbr_document, len(channel_publisher.slide_ids.filtered(lambda s: s.slide_type == 'document')))
-        self.assertEqual(channel_publisher.nbr_video, len(channel_publisher.slide_ids.filtered(lambda s: s.slide_type == 'video')))
+        self.assertEqual(channel_publisher.total_slides, len(channel_publisher.slide_content_ids))
+        self.assertEqual(channel_publisher.nbr_infographic, len(channel_publisher.slide_content_ids.filtered(lambda s: s.slide_type == 'infographic')))
+        self.assertEqual(channel_publisher.nbr_presentation, len(channel_publisher.slide_content_ids.filtered(lambda s: s.slide_type == 'presentation')))
+        self.assertEqual(channel_publisher.nbr_document, len(channel_publisher.slide_content_ids.filtered(lambda s: s.slide_type == 'document')))
+        self.assertEqual(channel_publisher.nbr_video, len(channel_publisher.slide_content_ids.filtered(lambda s: s.slide_type == 'video')))
         # slide statistics computation
-        self.assertEqual(float_compare(channel_publisher.total_time, sum(s.completion_time for s in channel_publisher.slide_ids), 3), 0)
+        self.assertEqual(float_compare(channel_publisher.total_time, sum(s.completion_time for s in channel_publisher.slide_content_ids), 3), 0)
         # members computation
         self.assertEqual(channel_publisher.members_count, 1)
         channel_publisher.action_add_member()
@@ -68,7 +50,7 @@ class TestStatistics(common.SlidesCase):
         channel_emp.invalidate_cache()
         self.assertEqual(
             channel_emp.completion,
-            math.ceil(100.0 * len(slides_emp) / len(channel_publisher.slide_ids)))
+            math.ceil(100.0 * len(slides_emp) / len(channel_publisher.slide_content_ids)))
         self.assertFalse(channel_emp.completed)
 
         self.slide_3.with_user(self.user_emp).action_set_completed()
@@ -100,6 +82,10 @@ class TestStatistics(common.SlidesCase):
         with self.assertRaises(UserError):
             slides_emp.action_set_viewed()
 
+
+@tagged('functional')
+class TestSlideStatistics(common.SlidesCase):
+
     def test_slide_user_statistics(self):
         channel_publisher = self.channel.with_user(self.user_publisher)
         channel_publisher._action_add_members(self.user_emp.partner_id)
@@ -121,7 +107,7 @@ class TestStatistics(common.SlidesCase):
         self.assertEqual(slide_emp.dislikes, 1)
         self.assertEqual(slide_emp.user_vote, -1)
 
-    def test_slide_statistics(self):
+    def test_slide_statistics_views(self):
         channel_publisher = self.channel.with_user(self.user_publisher)
         channel_publisher._action_add_members(self.user_emp.partner_id)
 
@@ -140,3 +126,13 @@ class TestStatistics(common.SlidesCase):
         self.assertEqual(slide_emp.slide_views, 1)
         self.assertEqual(slide_emp.public_views, 4)
         self.assertEqual(slide_emp.total_views, 5)
+
+    @users('user_publisher')
+    def test_slide_statistics_types(self):
+        category = self.category.with_user(self.env.user)
+        self.assertEqual(
+            category.nbr_presentation,
+            len(category.channel_id.slide_ids.filtered(lambda s: s.category_id == category and s.slide_type == 'presentation')))
+        self.assertEqual(
+            category.nbr_document,
+            len(category.channel_id.slide_ids.filtered(lambda s: s.category_id == category and s.slide_type == 'document')))
diff --git a/addons/website_slides/views/slide_channel_views.xml b/addons/website_slides/views/slide_channel_views.xml
index 7c8a80b513e1..8aac4c745265 100644
--- a/addons/website_slides/views/slide_channel_views.xml
+++ b/addons/website_slides/views/slide_channel_views.xml
@@ -1,58 +1,6 @@
 <?xml version="1.0" encoding="utf-8"?>
 <odoo>
     <data>
-        <!-- SLIDE.CATEGORY -->
-        <record model="ir.ui.view" id="view_slides_category_form">
-            <field name="name">slide.category.form</field>
-            <field name="model">slide.category</field>
-            <field name="arch" type="xml">
-                <form string="Category">
-                    <sheet>
-                        <group>
-                            <field name="name"/>
-                            <field name="channel_id"/>
-                        </group>
-                    </sheet>
-                </form>
-            </field>
-        </record>
-
-        <record model="ir.ui.view" id="slide_category_view_from_fchannel">
-            <field name="name">slide.category.form</field>
-            <field name="model">slide.category</field>
-            <field name="arch" type="xml">
-                <form string="Category">
-                    <sheet>
-                        <group>
-                            <field name="name"/>
-                        </group>
-                    </sheet>
-                </form>
-            </field>
-        </record>
-
-        <record id="view_slides_category_tree" model="ir.ui.view">
-            <field name="name">slide.category.tree</field>
-            <field name="model">slide.category</field>
-            <field name="arch" type="xml">
-                <tree string="Category">
-                    <field name="sequence" widget="handle"/>
-                    <field name="name"/>
-                    <field name="channel_id"/>
-                </tree>
-            </field>
-        </record>
-
-        <record id="action_ir_slide_category" model="ir.actions.act_window">
-            <field name="name">Categories</field>
-            <field name="res_model">slide.category</field>
-            <field name="view_mode">tree,form</field>
-            <field name="help" type="html">
-                <p class="o_view_nocontent_smiling_face">
-                    Create a new category
-                </p>
-            </field>
-        </record>
 
         <!-- SLIDE.CHANNEL VIEWS -->
         <record model="ir.ui.view" id="view_slide_channel_form">
@@ -87,9 +35,6 @@
                             <group>
                                 <field name="active" invisible="1"/>
                                 <field name="enroll" widget="radio" options="{'horizontal': true}"/>
-                                <field name="category_ids" widget="many2many_tags" placeholder="Categories"
-                                    context="{'default_channel_id': active_id, 'form_view_ref': 'website_slides.slide_category_view_from_fchannel'}"
-                                    domain="[('channel_id','=', active_id)]"/>
                                 <field name="tag_ids" widget="many2many_tags" placeholder="Tags"/>
                                 <field name="user_id"/>
                             </group>
diff --git a/addons/website_slides/views/slide_slide_views.xml b/addons/website_slides/views/slide_slide_views.xml
index 20a14d92041b..00a52993060d 100644
--- a/addons/website_slides/views/slide_slide_views.xml
+++ b/addons/website_slides/views/slide_slide_views.xml
@@ -43,12 +43,12 @@
                         <widget name="web_ribbon" text="Archived" bg_color="bg-danger" attrs="{'invisible': [('active', '=', True)]}"/>
                         <div class="oe_button_box" name="button_box">
                             <button class="oe_stat_button" name="website_publish_button"
-                                    type="object" icon="fa-globe">
+                                    type="object" icon="fa-globe" attrs="{'invisible': [('is_category', '=', True)]}">
                                 <field name="is_published" widget="website_button"/>
                             </button>
                         </div>
                         <field name="image_1920" widget="image" class="oe_avatar" options='{"preview_image": "image_256"}'
-                            attrs="{'readonly': [('slide_type', 'in', ('document', 'presentation', 'video'))]}"
+                            attrs="{'readonly': [('slide_type', 'in', ('document', 'presentation', 'video'))], 'invisible': [('is_category', '=', True)]}"
                         />
                         <div class="oe_title">
                             <div class="oe_edit_only">
@@ -56,10 +56,11 @@
                             </div>
                             <h1>
                                 <field name="name" default_focus="1" placeholder="Title"/>
+                                <field name="is_category" invisible="1"/>
                             </h1>
-                            <field name="tag_ids" widget="many2many_tags" placeholder="Tags..."/>
+                            <field name="tag_ids" attrs="{'invisible': [('is_category', '=', True)]}" widget="many2many_tags" placeholder="Tags..."/>
                         </div>
-                        <notebook>
+                        <notebook attrs="{'invisible': [('is_category', '=', True)]}">
                             <page string="Document">
                                 <group>
                                     <group>
@@ -86,7 +87,7 @@
                             <page string="Description">
                                 <field name="description"/>
                             </page>
-                            <page name="external_links" string="External Links">
+                            <page string="External Links" name="external_links" >
                                 <group>
                                     <field name="link_ids" widget="one2many" nolabel="1">
                                          <tree editable="top">
diff --git a/addons/website_slides/views/website_slides_templates_course.xml b/addons/website_slides/views/website_slides_templates_course.xml
index 6af0b555ce2d..554b8486239b 100644
--- a/addons/website_slides/views/website_slides_templates_course.xml
+++ b/addons/website_slides/views/website_slides_templates_course.xml
@@ -343,7 +343,7 @@
             <t t-foreach="category_data" t-as="category">
                 <t t-set="category_id" t-value="category['id'] if category['id'] else None"/>
 
-                <li t-if="category['total_slides'] or channel.can_publish" t-att-class="'o_wslides_slide_list_category mb-2' if category_id else 'mb-3'" t-att-data-category-id="category_id">
+                <li t-if="category['total_slides'] or channel.can_publish" t-att-class="'o_wslides_slide_list_category o_wslides_js_list_item mb-2' if category_id else 'mb-3'" t-att-data-slide-id="category_id" t-att-data-category-id="category_id">
                     <div t-att-data-category-id="category_id"
                          t-att-class="'o_wslides_slide_list_category_header position-relative d-flex justify-content-between align-items-center %s' % ('o_wslides_js_category bg-white shadow-sm border-bottom-0' if category_id and channel.can_upload  else 'border-0 py-1')">
                         <div t-att-class="'d-flex align-items-center pl-3 %s' % ('o_wslides_slides_list_drag' if channel.can_publish else '')">
@@ -396,7 +396,7 @@
 </template>
 
 <template id="course_slides_list_slide" name="Slide template for a training channel">
-    <li t-att-index="j" t-att-data-slide-id="slide.id" t-att-data-category-id="category_id" t-attf-class="o_wslides_slides_list_slide bg-white-50 border-top-0 d-flex align-items-center pl-2 #{'py-1 pr-2' if not channel.can_upload else ''}">
+    <li t-att-index="j" t-att-data-slide-id="slide.id" t-att-data-category-id="category_id" t-attf-class="o_wslides_slides_list_slide o_wslides_js_list_item bg-white-50 border-top-0 d-flex align-items-center pl-2 #{'py-1 pr-2' if not channel.can_upload else ''}">
         <div t-if="channel.can_publish" class=" o_wslides_slides_list_drag border-right p-2">
             <i class="fa fa-bars mr-2"></i>
         </div>
diff --git a/addons/website_slides/views/website_slides_templates_lesson.xml b/addons/website_slides/views/website_slides_templates_lesson.xml
index 560bc59fa193..d049a333f3fa 100644
--- a/addons/website_slides/views/website_slides_templates_lesson.xml
+++ b/addons/website_slides/views/website_slides_templates_lesson.xml
@@ -113,12 +113,9 @@
         </div>
         <ul id="collapse_slide_aside" class="list-unstyled my-0 pb-3 collapse d-lg-block">
             <t t-set="i" t-value="0"/>
-            <t t-if="uncategorized_slides" t-call="website_slides.slide_aside_training_category">
-                <t t-set="category_slide_ids" t-value="uncategorized_slides"/>
-            </t>
-            <t t-foreach="slide.channel_id.category_ids" t-as="category">
+            <t t-if="category.get('slides')" t-foreach="category_data" t-as="category">
                 <t t-call="website_slides.slide_aside_training_category">
-                    <t t-set="category_slide_ids" t-value="category.slide_ids"/>
+                    <t t-set="category_slide_ids" t-value="category['slides']"/>
                 </t>
             </t>
         </ul>
@@ -126,6 +123,7 @@
 </template>
 
 <template id="slide_aside_training_category" name="Category item for the slide detailed view list">
+    <t t-if="category" t-set="category" t-value="category.get('category')"/>
     <li class="o_wslides_fs_sidebar_section mt-2">
         <a t-att-href="('#collapse-%s') % (category.id if category else 0)" data-toggle="collapse" role="button" aria-expanded="true"
             class="o_wslides_lesson_aside_list_link pl-2 text-600 text-uppercase text-decoration-none py-1 small d-block"
diff --git a/addons/website_slides/views/website_slides_templates_lesson_fullscreen.xml b/addons/website_slides/views/website_slides_templates_lesson_fullscreen.xml
index 593fb2bfab12..cbaacc47cbce 100644
--- a/addons/website_slides/views/website_slides_templates_lesson_fullscreen.xml
+++ b/addons/website_slides/views/website_slides_templates_lesson_fullscreen.xml
@@ -54,14 +54,10 @@
                             </div>
                         </div>
                         <ul class="mx-n3 list-unstyled my-0 pb-2 overflow-auto">
-                            <t t-if="uncategorized_slides" t-call="website_slides.slide_fullscreen_sidebar_category">
-                                <t t-set="slides" t-value="uncategorized_slides"/>
-                                <t t-set="current_slide" t-value="slide"/>
-                            </t>
-                            <t t-foreach="slide.channel_id.category_ids" t-as="category">
-                                <t t-if="category.slide_ids">
+                            <t t-foreach="category_data" t-as="category">
+                                <t t-if="category.get('slides')">
                                     <t t-call="website_slides.slide_fullscreen_sidebar_category">
-                                        <t t-set="slides" t-value="category.slide_ids"/>
+                                        <t t-set="slides" t-value="category['slides']"/>
                                         <t t-set="current_slide" t-value="slide"/>
                                     </t>
                                 </t>
@@ -77,6 +73,7 @@
 
 
 <template id="slide_fullscreen_sidebar_category" name="Slides category template for fullscreen view side bar">
+    <t t-if="category" t-set="category" t-value="category.get('category')"/>
     <li class="o_wslides_fs_sidebar_section py-2 px-3">
         <a t-if="category" class="text-uppercase text-500 py-1 small d-block" t-attf-id="category-collapse-#{category.id if category else 0}" data-toggle="collapse" role="button" aria-expanded="true" t-att-href="('#collapse-%s') % (category.id if category else 0)" t-attf-aria-controls="collapse-#{category.id if category else 0}">
             <b t-field="category.name"/>
diff --git a/addons/website_slides_survey/data/slide_slide_demo.xml b/addons/website_slides_survey/data/slide_slide_demo.xml
index b3ece42ffca1..d65158cc7875 100644
--- a/addons/website_slides_survey/data/slide_slide_demo.xml
+++ b/addons/website_slides_survey/data/slide_slide_demo.xml
@@ -5,7 +5,7 @@
     <!-- ======================================= -->
     <record id="slide_slide_demo_5_4" model="slide.slide">
         <field name="name">Furniture Creation Certification</field>
-        <field name="sequence">5</field>
+        <field name="sequence">7</field>
         <field name="image_1920" type="base64" file="website_slides/static/src/img/channel_demo_furniture.jpg"/>
         <field name="slide_type">certification</field>
         <field name="channel_id" ref="website_slides.slide_channel_demo_5_furn2"/>
diff --git a/addons/website_slides_survey/models/slide_channel.py b/addons/website_slides_survey/models/slide_channel.py
index 75d5d6ad88fe..2e09ea05cd81 100644
--- a/addons/website_slides_survey/models/slide_channel.py
+++ b/addons/website_slides_survey/models/slide_channel.py
@@ -10,7 +10,3 @@ class Channel(models.Model):
     nbr_certification = fields.Integer("Number of Certifications", compute='_compute_slides_statistics', store=True)
 
 
-class Category(models.Model):
-    _inherit = 'slide.category'
-
-    nbr_certification = fields.Integer("Number of Certifications", compute='_count_presentations', store=True)
diff --git a/addons/website_slides_survey/models/slide_slide.py b/addons/website_slides_survey/models/slide_slide.py
index 49caaa29a938..8181812e28e7 100644
--- a/addons/website_slides_survey/models/slide_slide.py
+++ b/addons/website_slides_survey/models/slide_slide.py
@@ -38,6 +38,7 @@ class Slide(models.Model):
 
     slide_type = fields.Selection(selection_add=[('certification', 'Certification')])
     survey_id = fields.Many2one('survey.survey', 'Certification')
+    nbr_certification = fields.Integer("Number of Certifications", compute='_compute_slides_statistics', store=True)
 
     _sql_constraints = [
         ('check_survey_id', "CHECK(slide_type != 'certification' OR survey_id IS NOT NULL)", "A slide of type 'certification' requires a certification."),
-- 
GitLab