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 & previewable</field> + <field name="name">Slide: public/portal/user: restricted to published or uploaded by user, and either channel member or public channel & (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">['&', '|', - '&', ('channel_id.visibility', '=', 'public'), ('is_preview', '=', True), + '&', ('channel_id.visibility', '=', 'public'), '|', ('is_category','=', True), ('is_preview', '=', True), ('channel_id.partner_ids', '=', user.partner_id.id), '&', ('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