From c0d56da9b0e1fc4912975ff87d9b61e6b55ad45c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thibault=20Delavall=C3=A9e?= Date: Wed, 23 Feb 2022 08:20:40 +0000 Subject: [PATCH 01/14] [IMP] website_slides: quickly lint tests Purpose is to make a quick reordering / linting of tests while passing by. Task-2607416 Part-of: odoo/odoo#74171 --- addons/website_slides/tests/__init__.py | 7 +- addons/website_slides/tests/common.py | 49 ++++++--- .../tests/test_embed_detection.py | 1 + ...st_karma.py => test_gamification_karma.py} | 30 +++--- addons/website_slides/tests/test_security.py | 3 +- ...t_slide_utils.py => test_slide_channel.py} | 95 ------------------ ...st_resources.py => test_slide_resource.py} | 6 +- .../website_slides/tests/test_slide_slide.py | 99 +++++++++++++++++++ 8 files changed, 159 insertions(+), 131 deletions(-) rename addons/website_slides/tests/{test_karma.py => test_gamification_karma.py} (86%) rename addons/website_slides/tests/{test_slide_utils.py => test_slide_channel.py} (65%) rename addons/website_slides/tests/{test_resources.py => test_slide_resource.py} (90%) create mode 100644 addons/website_slides/tests/test_slide_slide.py diff --git a/addons/website_slides/tests/__init__.py b/addons/website_slides/tests/__init__.py index 3810dd0fad5..a0f9220e40c 100644 --- a/addons/website_slides/tests/__init__.py +++ b/addons/website_slides/tests/__init__.py @@ -3,9 +3,10 @@ from . import common from . import test_embed_detection -from . import test_karma -from . import test_resources +from . import test_gamification_karma from . import test_security -from . import test_slide_utils +from . import test_slide_channel +from . import test_slide_resource +from . import test_slide_slide from . import test_statistics from . import test_ui_wslides diff --git a/addons/website_slides/tests/common.py b/addons/website_slides/tests/common.py index 73111d1eda1..d3288dc5669 100644 --- a/addons/website_slides/tests/common.py +++ b/addons/website_slides/tests/common.py @@ -1,44 +1,65 @@ # -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. -from odoo.tests import common -from odoo.addons.mail.tests.common import mail_new_test_user +from odoo.addons.mail.tests.common import mail_new_test_user, MailCommon -class SlidesCase(common.TransactionCase): +class SlidesCase(MailCommon): @classmethod def setUpClass(cls): super(SlidesCase, cls).setUpClass() cls.user_officer = mail_new_test_user( - cls.env, name='Ophélie Officer', login='user_officer', email='officer@example.com', - groups='base.group_user,website_slides.group_website_slides_officer' + cls.env, + email='officer@example.com', + groups='base.group_user,website_slides.group_website_slides_officer', + login='user_officer', + name='Ophélie Officer', + notification_type='email', ) cls.user_manager = mail_new_test_user( - cls.env, name='Manuel Manager', login='user_manager', email='manager@example.com', - groups='base.group_user,website_slides.group_website_slides_manager' + cls.env, + email='manager@example.com', + login='user_manager', + groups='base.group_user,website_slides.group_website_slides_manager', + name='Manuel Manager', + notification_type='email', ) cls.user_emp = mail_new_test_user( - cls.env, name='Eglantine Employee', login='user_emp', email='employee@example.com', - groups='base.group_user' + cls.env, + email='employee@example.com', + groups='base.group_user', + login='user_emp', + name='Eglantine Employee', + notification_type='email', ) cls.user_portal = mail_new_test_user( - cls.env, name='Patrick Portal', login='user_portal', email='portal@example.com', - groups='base.group_portal' + cls.env, + email='portal@example.com', + groups='base.group_portal', + login='user_portal', + name='Patrick Portal', + notification_type='email', ) cls.user_public = mail_new_test_user( - cls.env, name='Pauline Public', login='user_public', email='public@example.com', - groups='base.group_public' + cls.env, + email='public@example.com', + groups='base.group_public', + login='user_public', + name='Pauline Public', + notification_type='email', ) cls.customer = cls.env['res.partner'].create({ + 'country_id': cls.env.ref('base.be').id, + 'email': 'customer@customer.example.com', + 'mobile': '0456001122', 'name': 'Caroline Customer', - 'email': 'customer@example.com', }) cls.channel = cls.env['slide.channel'].with_user(cls.user_officer).create({ diff --git a/addons/website_slides/tests/test_embed_detection.py b/addons/website_slides/tests/test_embed_detection.py index 5c61ffaa00d..14340313cc0 100644 --- a/addons/website_slides/tests/test_embed_detection.py +++ b/addons/website_slides/tests/test_embed_detection.py @@ -6,6 +6,7 @@ from odoo.tests import HttpCase class TestEmbedDetection(HttpCase, common.SlidesCase): + @classmethod def setUpClass(cls): super(TestEmbedDetection, cls).setUpClass() diff --git a/addons/website_slides/tests/test_karma.py b/addons/website_slides/tests/test_gamification_karma.py similarity index 86% rename from addons/website_slides/tests/test_karma.py rename to addons/website_slides/tests/test_gamification_karma.py index a9453da8caf..d7e0643e681 100644 --- a/addons/website_slides/tests/test_karma.py +++ b/addons/website_slides/tests/test_gamification_karma.py @@ -7,7 +7,7 @@ from odoo.tests.common import users from odoo.tools import mute_logger -@tagged('functional') +@tagged('gamification') class TestKarmaGain(common.SlidesCase): def setUp(self): @@ -25,20 +25,20 @@ class TestKarmaGain(common.SlidesCase): 'karma_gen_channel_rank': 10, }) - self.slide_2_0 = self.env['slide.slide'].with_user(self.user_officer).create({ - 'name': 'How to travel through space and time', - 'channel_id': self.channel_2.id, - 'slide_category': 'document', - 'is_published': True, - 'completion_time': 2.0, - }) - self.slide_2_1 = self.env['slide.slide'].with_user(self.user_officer).create({ - 'name': 'How to duplicate yourself', - 'channel_id': self.channel_2.id, - 'slide_category': 'document', - 'is_published': True, - 'completion_time': 2.0, - }) + self.slide_2_0, self.slide_2_1 = self.env['slide.slide'].with_user(self.user_officer).create([ + {'name': 'How to travel through space and time', + 'channel_id': self.channel_2.id, + 'slide_category': 'document', + 'is_published': True, + 'completion_time': 2.0, + }, + {'name': 'How to duplicate yourself', + 'channel_id': self.channel_2.id, + 'slide_category': 'document', + 'is_published': True, + 'completion_time': 2.0, + } + ]) @mute_logger('odoo.models') @users('user_emp', 'user_portal', 'user_officer') diff --git a/addons/website_slides/tests/test_security.py b/addons/website_slides/tests/test_security.py index 1f5eecc3143..9dddaa7e4e3 100644 --- a/addons/website_slides/tests/test_security.py +++ b/addons/website_slides/tests/test_security.py @@ -2,9 +2,8 @@ # Part of Odoo. See LICENSE file for full copyright and licensing details. from odoo.addons.website_slides.tests import common -from odoo.exceptions import AccessError, UserError +from odoo.exceptions import AccessError from odoo.tests import tagged -from odoo.tests.common import users from odoo.tools import mute_logger diff --git a/addons/website_slides/tests/test_slide_utils.py b/addons/website_slides/tests/test_slide_channel.py similarity index 65% rename from addons/website_slides/tests/test_slide_utils.py rename to addons/website_slides/tests/test_slide_channel.py index e46130a61e1..21085e4fe4b 100644 --- a/addons/website_slides/tests/test_slide_utils.py +++ b/addons/website_slides/tests/test_slide_channel.py @@ -165,98 +165,3 @@ class TestSequencing(slides_common.SlidesCase): self.assertEqual(self.category.sequence, 4) self.assertEqual(self.slide_2.sequence, 5) self.assertEqual([s.id for s in self.channel.slide_ids], [self.slide.id, new_category.id, self.slide_3.id, self.category.id, self.slide_2.id]) - - -class TestFromURL(slides_common.SlidesCase): - def test_video_youtube(self): - youtube_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://www.youtube-nocookie.com/embed/hlhLv0GN1hA', - 'https://m.youtube.com/watch?v=hlhLv0GN1hA', - ], - } - - Slide = self.env['slide.slide'].with_context(website_slides_skip_fetch_metadata=True) - - # test various YouTube URL formats - for youtube_id, urls in youtube_urls.items(): - for url in urls: - with self.subTest(url=url, id=youtube_id): - slide = Slide.create({ - 'name': 'dummy', - 'channel_id': self.channel.id, - 'url': url, - 'slide_category': 'video' - }) - self.assertEqual('youtube', slide.video_source_type) - self.assertEqual(youtube_id, slide.youtube_id) - - def test_video_google_drive(self): - google_drive_urls = { - '1qU5nHVNbz_r84P_IS5kDzoCuC1h5ZAZR': [ - 'https://drive.google.com/file/d/1qU5nHVNbz_r84P_IS5kDzoCuC1h5ZAZR/view?usp=sharing', - 'https://drive.google.com/file/d/1qU5nHVNbz_r84P_IS5kDzoCuC1h5ZAZR', - ], - } - - Slide = self.env['slide.slide'].with_context(website_slides_skip_fetch_metadata=True) - - # test various Google Drive URL formats - for google_drive_id, urls in google_drive_urls.items(): - for url in urls: - with self.subTest(url=url, id=google_drive_id): - slide = Slide.create({ - 'name': 'dummy', - 'channel_id': self.channel.id, - 'url': url, - 'slide_category': 'video' - }) - self.assertEqual('google_drive', slide.video_source_type) - self.assertEqual(google_drive_id, slide.google_drive_id) - - def test_video_vimeo(self): - vimeo_urls = { - # regular URL from Vimeo - '545859999': [ - 'https://vimeo.com/545859999', - 'https://vimeo.com/545859999?autoplay=1', - ], - # test channel URL from Vimeo - '551979139': [ - 'https://vimeo.com/channels/staffpicks/551979139', - 'https://vimeo.com/channels/staffpicks/551979139?autoplay=1', - ], - # test URL from Vimeo with setting 'with URL only' - # we need to store both the ID and the token, see '_compute_embed_code' method for details - '545859999/94dd03ddb0': [ - 'https://vimeo.com/545859999/94dd03ddb0', - 'https://vimeo.com/545859999/94dd03ddb0?autoplay=1', - ], - } - - Slide = self.env['slide.slide'].with_context(website_slides_skip_fetch_metadata=True) - - # test various Vimeo URL formats - for vimeo_id, urls in vimeo_urls.items(): - for url in urls: - with self.subTest(url=url, id=vimeo_id): - slide = Slide.create({ - 'name': 'dummy', - 'channel_id': self.channel.id, - 'url': url, - 'slide_category': 'video' - }) - self.assertEqual('vimeo', slide.video_source_type) - self.assertEqual(vimeo_id, slide.vimeo_id) diff --git a/addons/website_slides/tests/test_resources.py b/addons/website_slides/tests/test_slide_resource.py similarity index 90% rename from addons/website_slides/tests/test_resources.py rename to addons/website_slides/tests/test_slide_resource.py index 561a3a6d3aa..a10298dd737 100644 --- a/addons/website_slides/tests/test_resources.py +++ b/addons/website_slides/tests/test_slide_resource.py @@ -14,19 +14,21 @@ class TestResources(common.SlidesCase): @users('user_officer') @mute_logger('odoo') def test_constraints(self): - self.link = self.env["slide.slide.resource"].create({ + link = self.env["slide.slide.resource"].create({ 'name': 'Test Link', 'resource_type': 'url', 'link': 'https://www.example.com', 'slide_id': self.slide.id, }) + self.assertFalse(link.data) - self.resource = self.env["slide.slide.resource"].create({ + resource = self.env["slide.slide.resource"].create({ 'name': 'Test Resource', 'resource_type': 'file', 'data': '1111', 'slide_id': self.slide.id, }) + self.assertFalse(resource.link) self.assertEqual(len(self.slide.slide_resource_ids), 2) with self.assertRaises(ValidationError, msg="Cannot have a type link with a file"): diff --git a/addons/website_slides/tests/test_slide_slide.py b/addons/website_slides/tests/test_slide_slide.py new file mode 100644 index 00000000000..da2c9483094 --- /dev/null +++ b/addons/website_slides/tests/test_slide_slide.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 + + +class TestFromURL(slides_common.SlidesCase): + def test_video_youtube(self): + youtube_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://www.youtube-nocookie.com/embed/hlhLv0GN1hA', + 'https://m.youtube.com/watch?v=hlhLv0GN1hA', + ], + } + + Slide = self.env['slide.slide'].with_context(website_slides_skip_fetch_metadata=True) + + # test various YouTube URL formats + for youtube_id, urls in youtube_urls.items(): + for url in urls: + with self.subTest(url=url, id=youtube_id): + slide = Slide.create({ + 'name': 'dummy', + 'channel_id': self.channel.id, + 'url': url, + 'slide_category': 'video' + }) + self.assertEqual('youtube', slide.video_source_type) + self.assertEqual(youtube_id, slide.youtube_id) + + def test_video_google_drive(self): + google_drive_urls = { + '1qU5nHVNbz_r84P_IS5kDzoCuC1h5ZAZR': [ + 'https://drive.google.com/file/d/1qU5nHVNbz_r84P_IS5kDzoCuC1h5ZAZR/view?usp=sharing', + 'https://drive.google.com/file/d/1qU5nHVNbz_r84P_IS5kDzoCuC1h5ZAZR', + ], + } + + Slide = self.env['slide.slide'].with_context(website_slides_skip_fetch_metadata=True) + + # test various Google Drive URL formats + for google_drive_id, urls in google_drive_urls.items(): + for url in urls: + with self.subTest(url=url, id=google_drive_id): + slide = Slide.create({ + 'name': 'dummy', + 'channel_id': self.channel.id, + 'url': url, + 'slide_category': 'video' + }) + self.assertEqual('google_drive', slide.video_source_type) + self.assertEqual(google_drive_id, slide.google_drive_id) + + def test_video_vimeo(self): + vimeo_urls = { + # regular URL from Vimeo + '545859999': [ + 'https://vimeo.com/545859999', + 'https://vimeo.com/545859999?autoplay=1', + ], + # test channel URL from Vimeo + '551979139': [ + 'https://vimeo.com/channels/staffpicks/551979139', + 'https://vimeo.com/channels/staffpicks/551979139?autoplay=1', + ], + # test URL from Vimeo with setting 'with URL only' + # we need to store both the ID and the token, see '_compute_embed_code' method for details + '545859999/94dd03ddb0': [ + 'https://vimeo.com/545859999/94dd03ddb0', + 'https://vimeo.com/545859999/94dd03ddb0?autoplay=1', + ], + } + + Slide = self.env['slide.slide'].with_context(website_slides_skip_fetch_metadata=True) + + # test various Vimeo URL formats + for vimeo_id, urls in vimeo_urls.items(): + for url in urls: + with self.subTest(url=url, id=vimeo_id): + slide = Slide.create({ + 'name': 'dummy', + 'channel_id': self.channel.id, + 'url': url, + 'slide_category': 'video' + }) + self.assertEqual('vimeo', slide.video_source_type) + self.assertEqual(vimeo_id, slide.vimeo_id) From 1bcd34ea6bed6158c8ffb63d0345d0b2927cbd15 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thibault=20Delavall=C3=A9e?= Date: Thu, 24 Feb 2022 08:58:29 +0000 Subject: [PATCH 02/14] [REF] website_slides: cleanup membership fields Purpose of this commit is to finalize the cleanup of current user's membership on a given slide. As a followup of odoo/odoo@8b2f887c29f4377d8ebf5a3b8e2fcd9f7660add2 we remove now dead ``_compute_user_info`` method. We also introduce a shortcut to ``user_membership_id.sudo().completed`` by adding its computation directly while computing user_membership_id as well as user_vote. It allows to remove some sudo on user_membership_id. Task-2607416 Part-of: odoo/odoo#74171 --- addons/website_slides/controllers/main.py | 7 ++-- addons/website_slides/models/slide_slide.py | 37 +++------------------ 2 files changed, 8 insertions(+), 36 deletions(-) diff --git a/addons/website_slides/controllers/main.py b/addons/website_slides/controllers/main.py index 69de0219d71..4d37870eccd 100644 --- a/addons/website_slides/controllers/main.py +++ b/addons/website_slides/controllers/main.py @@ -121,7 +121,6 @@ class WebsiteSlides(WebsiteProfile): return slide._compute_quiz_info(request.env.user.partner_id, quiz_done=quiz_done)[slide.id] def _get_slide_quiz_data(self, slide): - slide_completed = slide.user_membership_id.sudo().completed values = { 'slide_questions': [{ 'id': question.id, @@ -129,7 +128,7 @@ class WebsiteSlides(WebsiteProfile): 'answer_ids': [{ 'id': answer.id, 'text_value': answer.text_value, - 'is_correct': answer.is_correct if slide_completed or request.website.is_publisher() else None, + 'is_correct': answer.is_correct if slide.user_has_completed or request.website.is_publisher() else None, 'comment': answer.comment if request.website.is_publisher else None } for answer in question.sudo().answer_ids], } for question in slide.question_ids] @@ -958,7 +957,7 @@ class WebsiteSlides(WebsiteProfile): return fetch_res slide = fetch_res['slide'] - if slide.user_membership_id.sudo().completed: + if slide.user_has_completed: self._channel_remove_session_answers(slide.channel_id, slide) return {'error': 'slide_quiz_done'} @@ -992,7 +991,7 @@ class WebsiteSlides(WebsiteProfile): 'comment': answer.comment } for answer in user_answers }, - 'completed': slide.user_membership_id.sudo().completed, + 'completed': slide.user_has_completed, 'channel_completion': slide.channel_id.completion, 'quizKarmaWon': quiz_info['quiz_karma_won'], 'quizKarmaGain': quiz_info['quiz_karma_gain'], diff --git a/addons/website_slides/models/slide_slide.py b/addons/website_slides/models/slide_slide.py index ed99cfd752a..74ada83cee0 100644 --- a/addons/website_slides/models/slide_slide.py +++ b/addons/website_slides/models/slide_slide.py @@ -116,6 +116,9 @@ class Slide(models.Model): 'slide.slide.partner', string="Subscriber information", compute='_compute_user_membership_id', compute_sudo=False, help="Subscriber information for the current logged in user") + # current user membership + user_vote = fields.Integer('User vote', compute='_compute_user_membership_id', compute_sudo=False) + user_has_completed = fields.Boolean('Is Member', compute='_compute_user_membership_id', compute_sudo=False) # Quiz related fields question_ids = fields.One2many("slide.question", "slide_id", string="Questions") questions_count = fields.Integer(string="Numbers of Questions", compute='_compute_questions_count') @@ -183,7 +186,6 @@ class Slide(models.Model): date_published = fields.Datetime('Publish Date', readonly=True, tracking=False) likes = fields.Integer('Likes', compute='_compute_like_info', store=True, compute_sudo=False) dislikes = fields.Integer('Dislikes', compute='_compute_like_info', store=True, compute_sudo=False) - user_vote = fields.Integer('User vote', compute='_compute_user_membership_id', compute_sudo=False) embed_code = fields.Html('Embed Code', readonly=True, compute='_compute_embed_code', sanitize=False) embed_code_external = fields.Html('External Embed Code', readonly=True, compute='_compute_embed_code', sanitize=False, help="Same as 'Embed Code' but used to embed the content on an external website.") @@ -292,36 +294,6 @@ class Slide(models.Model): slide.likes = mapped_data_like.get(slide.id, 0) slide.dislikes = mapped_data_dislike.get(slide.id, 0) - @api.depends('slide_partner_ids.vote') - @api.depends_context('uid') - def _compute_user_info(self): - """ Deprecated. Now computed directly by _compute_user_membership_id - for user_vote and _compute_like_info for likes / dislikes. Remove me in - master. """ - default_stats = {'likes': 0, 'dislikes': 0, 'user_vote': False} - - if not self.ids: - self.update(default_stats) - return - - slide_data = dict.fromkeys(self.ids, default_stats) - slide_partners = self.env['slide.slide.partner'].sudo().search([ - ('slide_id', 'in', self.ids) - ]) - - for slide_partner in slide_partners: - if slide_partner.vote == 1: - slide_data[slide_partner.slide_id.id]['likes'] += 1 - if slide_partner.partner_id == self.env.user.partner_id: - slide_data[slide_partner.slide_id.id]['user_vote'] = 1 - elif slide_partner.vote == -1: - slide_data[slide_partner.slide_id.id]['dislikes'] += 1 - if slide_partner.partner_id == self.env.user.partner_id: - slide_data[slide_partner.slide_id.id]['user_vote'] = -1 - - for slide in self: - slide.update(slide_data[slide.id]) - @api.depends('slide_partner_ids.slide_id') def _compute_slide_views(self): # TODO awa: tried compute_sudo, for some reason it doesn't work in here... @@ -412,7 +384,7 @@ class Slide(models.Model): else: slide.slide_type = False - @api.depends('slide_partner_ids.partner_id', 'slide_partner_ids.vote') + @api.depends('slide_partner_ids.partner_id', 'slide_partner_ids.vote', 'slide_partner_ids.completed') @api.depends('uid') def _compute_user_membership_id(self): slide_partners = self.env['slide.slide.partner'].sudo().search([ @@ -426,6 +398,7 @@ class Slide(models.Model): self.env['slide.slide.partner'] ) record.user_vote = record.user_membership_id.vote + record.user_has_completed = record.user_membership_id.completed @api.depends('slide_category', 'google_drive_id', 'video_source_type', 'youtube_id') def _compute_embed_code(self): From 57be5167a933e2b0604a024b8eaae4a2f5e80d14 Mon Sep 17 00:00:00 2001 From: Noe Antoine Date: Mon, 14 Jun 2021 10:25:59 +0000 Subject: [PATCH 03/14] [IMP] website_slides: add constraints to make membership unique Before, a partner could be present several times in the list of attendees (slide_channel_partner model). To avoid noise and confusion, a sql constraint is added to the model to make the couple (partner, channel) unique in db. If several exist, we only keep the most advanced one in terms of completion (see UPG script). Same move is done for slide activity tracking (slide_slide_partner model). Certification or completion is considered as more advanced. Demo data is updated as we try to create a membership that already exists due to auto subscription of channel responsible. We also make some channel memberships explicit as some demo data about slide completion exists. --- Links --- Task-2777216 Prepares Task-2508019 (Slides membership refactoring) Also soo UPG PR - odoo/upgrade#2854 Part-of: odoo/odoo#74171 --- .../website_slides/data/slide_user_demo.xml | 27 ++++++++++++++++--- addons/website_slides/models/slide_channel.py | 7 +++++ addons/website_slides/models/slide_slide.py | 7 +++++ 3 files changed, 37 insertions(+), 4 deletions(-) diff --git a/addons/website_slides/data/slide_user_demo.xml b/addons/website_slides/data/slide_user_demo.xml index 80d87842323..cc8b45a6219 100644 --- a/addons/website_slides/data/slide_user_demo.xml +++ b/addons/website_slides/data/slide_user_demo.xml @@ -2,6 +2,15 @@ + + + + + + + + + @@ -78,6 +87,15 @@ + + + + + + + + + @@ -187,6 +205,10 @@ + + + + @@ -199,10 +221,7 @@ 1 - - - - + diff --git a/addons/website_slides/models/slide_channel.py b/addons/website_slides/models/slide_channel.py index fdda9b34b2d..402222f0e8b 100644 --- a/addons/website_slides/models/slide_channel.py +++ b/addons/website_slides/models/slide_channel.py @@ -34,6 +34,13 @@ class ChannelUsersRelation(models.Model): channel_enroll = fields.Selection(related='channel_id.enroll') channel_website_id = fields.Many2one('website', string='Website', related='channel_id.website_id') + _sql_constraints = [ + ('channel_partner_uniq', + 'unique(channel_id, partner_id)', + 'A partner membership to a channel must be unique!' + ), + ] + def _recompute_completion(self): read_group_res = self.env['slide.slide.partner'].sudo().read_group( ['&', '&', ('channel_id', 'in', self.mapped('channel_id').ids), diff --git a/addons/website_slides/models/slide_slide.py b/addons/website_slides/models/slide_slide.py index 74ada83cee0..7386cfc597b 100644 --- a/addons/website_slides/models/slide_slide.py +++ b/addons/website_slides/models/slide_slide.py @@ -36,6 +36,13 @@ class SlidePartnerRelation(models.Model): completed = fields.Boolean('Completed') quiz_attempts_count = fields.Integer('Quiz attempts count', default=0) + _sql_constraints = [ + ('slide_partner_uniq', + 'unique(slide_id, partner_id)', + 'A partner membership to a slide must be unique!' + ) + ] + @api.model_create_multi def create(self, vals_list): res = super().create(vals_list) From ffa2ed0e752c0b48c407792e6b251920f546eb0c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?No=C3=A9=20Antoine?= Date: Wed, 23 Feb 2022 08:35:20 +0000 Subject: [PATCH 04/14] [IMP] website_slides: add constraint completion to be a percentage Before completion could hold about any value, even if it is generally correctly computed as a percentage. To be sure we don't have incoherent values in db a constraint is added to ensure completion is contained between 0 and 100. --- Links --- Task-2777216 Prepares Task-2508019 (Slides membership refactoring) Also see UPG PR - odoo/upgrade#2854 Part-of: odoo/odoo#74171 --- addons/website_slides/models/slide_channel.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/addons/website_slides/models/slide_channel.py b/addons/website_slides/models/slide_channel.py index 402222f0e8b..28e85a4fada 100644 --- a/addons/website_slides/models/slide_channel.py +++ b/addons/website_slides/models/slide_channel.py @@ -39,6 +39,10 @@ class ChannelUsersRelation(models.Model): 'unique(channel_id, partner_id)', 'A partner membership to a channel must be unique!' ), + ('check_completion', + 'check(completion >= 0 and completion <= 100)', + 'The completion of a channel is a percentage and should be between 0% and 100.' + ) ] def _recompute_completion(self): From 924ca810efa669192d96b8b62bf5bd4fca80e470 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thibault=20Delavall=C3=A9e?= Date: Wed, 23 Feb 2022 09:14:51 +0000 Subject: [PATCH 05/14] [IMP] website_slides: add views and access for slide progress Purpose of this commit is to have access to slide.slide.partner model that holds member progress on specific slide. It is used notably to build completion on slide.channel.partner or to store vote data. It is interesting to have access to it to have some detailed informations. It is now reachable through the "attendees" smart button that replaces the 'views' button. Public / other views are moved in slide details as those buttons do not add much to the slide usability. Task-2607416 Part-of: odoo/odoo#74171 --- addons/website_slides/__manifest__.py | 1 + addons/website_slides/models/slide_slide.py | 4 +- .../views/slide_slide_partner_views.xml | 79 +++++++++++++++++++ .../views/slide_slide_views.xml | 23 ++---- addons/website_slides_survey/__manifest__.py | 1 + .../views/slide_slide_partner_views.xml | 42 ++++++++++ 6 files changed, 133 insertions(+), 17 deletions(-) create mode 100644 addons/website_slides/views/slide_slide_partner_views.xml create mode 100644 addons/website_slides_survey/views/slide_slide_partner_views.xml diff --git a/addons/website_slides/__manifest__.py b/addons/website_slides/__manifest__.py index a0a58046f3f..f7b64a54797 100644 --- a/addons/website_slides/__manifest__.py +++ b/addons/website_slides/__manifest__.py @@ -33,6 +33,7 @@ Featuring 'views/rating_rating_views.xml', 'views/slide_embed_views.xml', 'views/slide_question_views.xml', + 'views/slide_slide_partner_views.xml', 'views/slide_slide_views.xml', 'views/slide_channel_partner_views.xml', 'views/slide_channel_views.xml', diff --git a/addons/website_slides/models/slide_slide.py b/addons/website_slides/models/slide_slide.py index 7386cfc597b..1a722cb2714 100644 --- a/addons/website_slides/models/slide_slide.py +++ b/addons/website_slides/models/slide_slide.py @@ -26,8 +26,10 @@ class SlidePartnerRelation(models.Model): _name = 'slide.slide.partner' _description = 'Slide / Partner decorated m2m' _table = 'slide_slide_partner' + _rec_name = 'partner_id' - slide_id = fields.Many2one('slide.slide', ondelete="cascade", index=True, required=True) + slide_id = fields.Many2one('slide.slide', string="Content", ondelete="cascade", index=True, required=True) + slide_category = fields.Selection(related='slide_id.slide_category') channel_id = fields.Many2one( 'slide.channel', string="Channel", related="slide_id.channel_id", store=True, index=True, ondelete='cascade') diff --git a/addons/website_slides/views/slide_slide_partner_views.xml b/addons/website_slides/views/slide_slide_partner_views.xml new file mode 100644 index 00000000000..eb3e9927ace --- /dev/null +++ b/addons/website_slides/views/slide_slide_partner_views.xml @@ -0,0 +1,79 @@ + + + + slide.slide.partner.view.search + slide.slide.partner + + + + + + + + + + + + + + + + slide.slide.partner.view.tree + slide.slide.partner + + + + + + + + + + + + + + + slide.slide.partner.view.form + slide.slide.partner + +
+ + + + + + + + + + + + + + + +
+
+
+ + + Attendees + slide.slide.partner + tree,form,kanban + + {'default_slide_id': active_id} + [('slide_id', '=', active_id)] + +

+ No Attendee Yet! +

+

+ From here you'll be able to monitor attendees and to track their progress. +

+
+
+ +
diff --git a/addons/website_slides/views/slide_slide_views.xml b/addons/website_slides/views/slide_slide_views.xml index 3f8affcb44d..1018ce0a381 100644 --- a/addons/website_slides/views/slide_slide_views.xml +++ b/addons/website_slides/views/slide_slide_views.xml @@ -51,22 +51,11 @@
- - -
- + + + diff --git a/addons/website_slides_survey/__manifest__.py b/addons/website_slides_survey/__manifest__.py index 346961eb7de..7d2f6734d48 100644 --- a/addons/website_slides_survey/__manifest__.py +++ b/addons/website_slides_survey/__manifest__.py @@ -12,6 +12,7 @@ 'data': [ 'security/ir.model.access.csv', 'views/slide_channel_views.xml', + 'views/slide_slide_partner_views.xml', 'views/slide_slide_views.xml', 'views/survey_survey_views.xml', 'views/website_slides_menu_views.xml', diff --git a/addons/website_slides_survey/views/slide_slide_partner_views.xml b/addons/website_slides_survey/views/slide_slide_partner_views.xml new file mode 100644 index 00000000000..33cd07c32ba --- /dev/null +++ b/addons/website_slides_survey/views/slide_slide_partner_views.xml @@ -0,0 +1,42 @@ + + + + + slide.slide.partner.view.search.inherit.survey + slide.slide.partner + + + + + + + + + + slide.slide.partner.view.tree.inherit.survey + slide.slide.partner + + + + + + + + + + slide.slide.partner.view.form.inherit.survey + slide.slide.partner + + + + + + + + + + + + From 2a3d8dd1c2b2a9767c34ed9cbf6eaaca2bc1abdd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thibault=20Delavall=C3=A9e?= Date: Wed, 23 Feb 2022 12:54:41 +0000 Subject: [PATCH 06/14] [MOV] various: move ir.qweb.fields code into right files When working on qweb fields it is difficult to find some custom override as they are never in the right file. Finding them is always a bit of random pick. Task-2607416 Part-of: odoo/odoo#74171 --- addons/web/models/__init__.py | 2 +- addons/web/models/{ir_qweb.py => ir_qweb_fields.py} | 0 addons/web_editor/models/__init__.py | 2 +- addons/web_editor/models/{ir_qweb.py => ir_qweb_fields.py} | 0 addons/web_editor/tests/test_converter.py | 2 +- addons/web_unsplash/models/__init__.py | 2 +- addons/web_unsplash/models/{ir_qweb.py => ir_qweb_fields.py} | 0 7 files changed, 4 insertions(+), 4 deletions(-) rename addons/web/models/{ir_qweb.py => ir_qweb_fields.py} (100%) rename addons/web_editor/models/{ir_qweb.py => ir_qweb_fields.py} (100%) rename addons/web_unsplash/models/{ir_qweb.py => ir_qweb_fields.py} (100%) diff --git a/addons/web/models/__init__.py b/addons/web/models/__init__.py index 7c9ec692afc..3dbae07edb1 100644 --- a/addons/web/models/__init__.py +++ b/addons/web/models/__init__.py @@ -1,7 +1,7 @@ # -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. -from . import ir_qweb +from . import ir_qweb_fields from . import ir_http from . import ir_ui_menu from . import models diff --git a/addons/web/models/ir_qweb.py b/addons/web/models/ir_qweb_fields.py similarity index 100% rename from addons/web/models/ir_qweb.py rename to addons/web/models/ir_qweb_fields.py diff --git a/addons/web_editor/models/__init__.py b/addons/web_editor/models/__init__.py index e9a790f7e61..c0c42b8e1f9 100644 --- a/addons/web_editor/models/__init__.py +++ b/addons/web_editor/models/__init__.py @@ -2,7 +2,7 @@ # Part of Odoo. See LICENSE file for full copyright and licensing details. from . import ir_attachment -from . import ir_qweb +from . import ir_qweb_fields from . import ir_ui_view from . import ir_http from . import ir_translation diff --git a/addons/web_editor/models/ir_qweb.py b/addons/web_editor/models/ir_qweb_fields.py similarity index 100% rename from addons/web_editor/models/ir_qweb.py rename to addons/web_editor/models/ir_qweb_fields.py diff --git a/addons/web_editor/tests/test_converter.py b/addons/web_editor/tests/test_converter.py index 3d9259ba5b5..245ef631e0d 100644 --- a/addons/web_editor/tests/test_converter.py +++ b/addons/web_editor/tests/test_converter.py @@ -8,7 +8,7 @@ from lxml.builder import E from odoo.tests import common from odoo.tests.common import BaseCase -from odoo.addons.web_editor.models.ir_qweb import html_to_text +from odoo.addons.web_editor.models.ir_qweb_fields import html_to_text class TestHTMLToText(BaseCase): diff --git a/addons/web_unsplash/models/__init__.py b/addons/web_unsplash/models/__init__.py index ffce82298b0..06c3c1ac4cd 100644 --- a/addons/web_unsplash/models/__init__.py +++ b/addons/web_unsplash/models/__init__.py @@ -3,4 +3,4 @@ from . import res_config_settings from . import res_users -from . import ir_qweb +from . import ir_qweb_fields diff --git a/addons/web_unsplash/models/ir_qweb.py b/addons/web_unsplash/models/ir_qweb_fields.py similarity index 100% rename from addons/web_unsplash/models/ir_qweb.py rename to addons/web_unsplash/models/ir_qweb_fields.py From e50bc749f69a955af99fb85c635ddc5e06723b89 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thibault=20Delavall=C3=A9e?= Date: Wed, 23 Feb 2022 13:03:47 +0000 Subject: [PATCH 07/14] [IMP] base: support decimalized in integer qweb formatting Purpose of this commit is to allow decimalized formatting of numbers when rendering integer fields in qweb. This is done through an option and will be used notably in elearning. Task-2607416 Part-of: odoo/odoo#74171 --- odoo/addons/base/models/ir_qweb_fields.py | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/odoo/addons/base/models/ir_qweb_fields.py b/odoo/addons/base/models/ir_qweb_fields.py index 7d82c53777b..6ee26f0c085 100644 --- a/odoo/addons/base/models/ir_qweb_fields.py +++ b/odoo/addons/base/models/ir_qweb_fields.py @@ -10,7 +10,7 @@ from markupsafe import Markup as M, escape from PIL import Image from lxml import etree, html -from odoo import api, fields, models, _, _lt +from odoo import api, fields, models, _, _lt, tools from odoo.tools import posix_to_ldml, float_utils, format_date, format_duration, pycompat from odoo.tools.mail import safe_attrs from odoo.tools.misc import get_lang, babel_locale_parse @@ -136,8 +136,18 @@ class IntegerConverter(models.AbstractModel): _description = 'Qweb Field Integer' _inherit = 'ir.qweb.field' + @api.model + def get_available_options(self): + options = super(IntegerConverter, self).get_available_options() + options.update( + format_decimalized_number=dict(type='boolean', string=_('Decimalized number')), + ) + return options + @api.model def value_to_html(self, value, options): + if options.get('format_decimalized_number'): + return tools.format_decimalized_number(value) return pycompat.to_text(self.user_lang().format('%d', value, grouping=True).replace(r'-', '-\N{ZERO WIDTH NO-BREAK SPACE}')) From fa7451544d017e3f8b50c4b1c9091d60db822dad Mon Sep 17 00:00:00 2001 From: Munaf Khan Date: Fri, 23 Jul 2021 13:17:29 +0000 Subject: [PATCH 08/14] [IMP] website_slides: improve lesson design in documentation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Currently, in the documentation type of courses, card layout for the lessons is broken because there isn't enough space to display everything. To fix this, this commit moves the 'Completed' badge (which is in the card footer) and other tags at the end of the card-body. It formats likes and dislikes using a decimalized formatting (ex, 10000 will be displayed as 10k). But that improvement leads to another trouble. Say there are '1k' likes and user upvotes the lesson. There is no indication whether or not the vote was recorded. To fix this, we will now use different icons based on the user vote - The thumbs-up icon will be filled if user has liked the lesson and it will be hollow otherwise; - Likewise, the thumbs-down icon will be filled if user has disliked the lesson, and it will be hollow otherwise; Clicking on either of the button, if the vote is recorded successfully, icon and user vote will be toggled for better indication. Note that before this commit, if user was performing the same action (for example upvoting again) a warning popover used to appear, which isn't the case anymore because now the vote will simply be toggled. The mechanism is somewhat similar to youtube (you can also directly dislike the already liked content and visa-versa). The same mechanism is also applied for likes/dislikes in the footer of each lesson's page. And, on the same page under 'Statistics' tab, they are simply formatted like everywhere else. Apart from that, this commit makes other small UI changes listed as below: - We now display three slides per category instead of four, giving the cards a bit more space; - On the lessons' page, we move the likes/dislikes right after description which previously was at bottom right of the page; - Likes / Rating is now always displayed the same in lessen view whatever the channel type (displaying ratings / comments / likes); Also see odoo/upgrade#2854 Task-2607416 Part-of: odoo/odoo#74171 Co-authored-by: Munaf Bahelim Co-authored-by: Thibault Delavallée --- addons/website_slides/controllers/main.py | 76 ++++++++++--------- addons/website_slides/models/slide_slide.py | 20 +++-- .../static/src/js/slides_slide_like.js | 19 ++++- .../static/src/scss/website_slides.scss | 5 ++ .../tests/test_gamification_karma.py | 45 +++++++++-- .../website_slides/tests/test_slide_slide.py | 20 ++++- .../website_slides/tests/test_statistics.py | 8 +- .../views/website_slides_templates_course.xml | 28 +++---- .../views/website_slides_templates_lesson.xml | 56 +++++--------- 9 files changed, 169 insertions(+), 108 deletions(-) diff --git a/addons/website_slides/controllers/main.py b/addons/website_slides/controllers/main.py index 4d37870eccd..16d3271f6af 100644 --- a/addons/website_slides/controllers/main.py +++ b/addons/website_slides/controllers/main.py @@ -24,7 +24,7 @@ _logger = logging.getLogger(__name__) class WebsiteSlides(WebsiteProfile): _slides_per_page = 12 _slides_per_aside = 20 - _slides_per_category = 4 + _slides_per_category = 3 _channel_order_by_criterion = { 'vote': 'total_votes desc', 'view': 'total_views desc', @@ -40,6 +40,15 @@ class WebsiteSlides(WebsiteProfile): if not qs or qs.lower() in loc: yield {'loc': loc} + def _slide_render_context_base(self): + return { + # current user info + 'user': request.env.user, + 'is_public_user': request.website.is_public_user(), + # tools + '_slugify_tags': self._slugify_tags, + } + # SLIDE UTILITIES # -------------------------------------------------- @@ -93,7 +102,8 @@ class WebsiteSlides(WebsiteProfile): previous_slide = slide.channel_id.slide_content_ids[slide_index-1] if slide_index > 0 else None next_slide = slide.channel_id.slide_content_ids[slide_index+1] if slide_index < len(channel_slides_ids) - 1 else None - values = { + render_values = self._slide_render_context_base() + render_values.update({ # slide 'slide': slide, 'main_object': slide, @@ -102,20 +112,17 @@ class WebsiteSlides(WebsiteProfile): 'previous_slide': previous_slide, 'next_slide': next_slide, 'category_data': category_data, - # user - 'user': request.env.user, - 'is_public_user': request.website.is_public_user(), # rating and comments 'comments': slide.website_message_ids or [], - } + }) # allow rating and comments if slide.channel_id.allow_comment: - values.update({ + render_values.update({ 'message_post_pid': request.env.user.partner_id.id, }) - return values + return render_values def _get_slide_quiz_partner_info(self, slide, quiz_done=False): return slide._compute_quiz_info(request.env.user.partner_id, quiz_done=quiz_done)[slide.id] @@ -312,8 +319,9 @@ class WebsiteSlides(WebsiteProfile): ('karma', '>', 0), ('website_published', '=', True)], limit=5, order='karma desc') - values = self._prepare_user_values(**post) - values.update({ + render_values = self._slide_render_context_base() + render_values.update(self._prepare_user_values(**post)) + render_values.update({ 'channels_my': channels_my, 'channels_popular': channels_popular, 'channels_newest': channels_newest, @@ -327,7 +335,7 @@ class WebsiteSlides(WebsiteProfile): 'slugify_tags': self._slugify_tags, }) - return request.render('website_slides.courses_home', values) + return request.render('website_slides.courses_home', render_values) @http.route(['/slides/all', '/slides/all/tag/'], type='http', auth="public", website=True, sitemap=True) def slides_channel_all(self, slide_category=None, slug_tags=None, my=False, **post): @@ -387,8 +395,9 @@ class WebsiteSlides(WebsiteProfile): else: search_tags = request.env['slide.channel.tag'] - values = self._prepare_user_values(**post) - values.update({ + render_values = self._slide_render_context_base() + render_values.update(self._prepare_user_values(**post)) + render_values.update({ 'channels': channels, 'tag_groups': tag_groups, 'search_term': fuzzy_search_term or search, @@ -401,7 +410,7 @@ class WebsiteSlides(WebsiteProfile): 'slide_query_url': QueryURL('/slides/all', ['tag']), }) - return request.render('website_slides.courses_all', values) + return request.render('website_slides.courses_all', render_values) def _prepare_additional_channel_values(self, values, **kwargs): return values @@ -480,7 +489,8 @@ class WebsiteSlides(WebsiteProfile): elif uncategorized: query_string = "?search_uncategorized=1" - values = { + render_values = self._slide_render_context_base() + render_values.update({ 'channel': channel, 'main_object': channel, 'active_tab': kw.get('active_tab', 'home'), @@ -494,22 +504,20 @@ class WebsiteSlides(WebsiteProfile): 'sorting': actual_sorting, 'search': search, # display data - 'user': request.env.user, 'pager': pager, - 'is_public_user': request.website.is_public_user(), # display upload modal 'enable_slide_upload': 'enable_slide_upload' in kw, ** self._slide_channel_prepare_review_values(channel), - } + }) # fetch slides and handle uncategorized slides; done as sudo because we want to display all # of them but unreachable ones won't be clickable (+ slide controller will crash anyway) # documentation mode may display less slides than content by category but overhead of # computation is reasonable if channel.promote_strategy == 'specific': - values['slide_promoted'] = channel.sudo().promoted_slide_id + render_values['slide_promoted'] = channel.sudo().promoted_slide_id else: - values['slide_promoted'] = request.env['slide.slide'].sudo().search(domain, limit=1, order=order) + render_values['slide_promoted'] = request.env['slide.slide'].sudo().search(domain, limit=1, order=order) limit_category_data = False if channel.channel_type == 'documentation': @@ -518,26 +526,26 @@ class WebsiteSlides(WebsiteProfile): else: limit_category_data = self._slides_per_category - values['category_data'] = channel._get_categorized_slides( + render_values['category_data'] = channel._get_categorized_slides( domain, order, force_void=not category, limit=limit_category_data, offset=pager['offset']) - values['channel_progress'] = self._get_channel_progress(channel, include_quiz=True) + render_values['channel_progress'] = self._get_channel_progress(channel, include_quiz=True) # for sys admins: prepare data to install directly modules from eLearning when # uploading slides. Currently supporting only survey, because why not. if request.env.user.has_group('base.group_system'): module = request.env.ref('base.module_survey') if module.state != 'installed': - values['modules_to_install'] = [{ + render_values['modules_to_install'] = [{ 'id': module.id, 'name': module.shortdesc, 'motivational': _('Evaluate and certify your students.'), }] - values = self._prepare_additional_channel_values(values, **kw) - return request.render('website_slides.course_main', values) + render_values = self._prepare_additional_channel_values(render_values, **kw) + return request.render('website_slides.course_main', render_values) # SLIDE.CHANNEL UTILS # -------------------------------------------------- @@ -714,7 +722,7 @@ class WebsiteSlides(WebsiteProfile): 'search_tag': request.env['slide.tag'].browse(int(kwargs.get('search_tag'))) if kwargs.get('search_tag') else None, 'slide_categories': dict(request.env['slide.slide']._fields['slide_category']._description_selection(request.env)) if kwargs.get('search_slide_category') else None, 'search_slide_category': kwargs.get('search_slide_category'), - 'search_uncategorized': kwargs.get('search_uncategorized') + 'search_uncategorized': kwargs.get('search_uncategorized'), }) values['channel'] = slide.channel_id @@ -802,12 +810,6 @@ class WebsiteSlides(WebsiteProfile): def slide_like(self, slide_id, upvote): if request.website.is_public_user(): return {'error': 'public_user', 'error_signup_allowed': request.env['res.users'].sudo()._get_signup_invitation_scope() == 'b2c'} - slide_partners = request.env['slide.slide.partner'].sudo().search([ - ('slide_id', '=', slide_id), - ('partner_id', '=', request.env.user.partner_id.id) - ]) - if (upvote and slide_partners.vote == 1) or (not upvote and slide_partners.vote == -1): - return {'error': 'vote_done'} # check slide access fetch_res = self._fetch_slide(slide_id) if fetch_res.get('error'): @@ -824,8 +826,14 @@ class WebsiteSlides(WebsiteProfile): slide.action_like() else: slide.action_dislike() - slide.invalidate_cache() - return slide.read(['likes', 'dislikes', 'user_vote'])[0] + # for large number of likes/dislikes, format them so they don't break the UI + # first display is done using a widget but this route updated the UI directly + # hence calling format_decimalized_number + return { + 'user_vote': slide.user_vote, + 'likes': tools.format_decimalized_number(slide.likes), + 'dislikes': tools.format_decimalized_number(slide.dislikes), + } @http.route('/slides/slide/archive', type='json', auth='user', website=True) def slide_archive(self, slide_id): diff --git a/addons/website_slides/models/slide_slide.py b/addons/website_slides/models/slide_slide.py index 1a722cb2714..93f909d91de 100644 --- a/addons/website_slides/models/slide_slide.py +++ b/addons/website_slides/models/slide_slide.py @@ -42,7 +42,11 @@ class SlidePartnerRelation(models.Model): ('slide_partner_uniq', 'unique(slide_id, partner_id)', 'A partner membership to a slide must be unique!' - ) + ), + ('check_vote', + 'CHECK(vote IN (-1, 0, 1))', + 'The vote must be 1, 0 or -1.' + ), ] @api.model_create_multi @@ -795,13 +799,15 @@ class Slide(models.Model): for slide_partner in slide_partners: if upvote: - new_vote = 0 if slide_partner.vote == -1 else 1 - if slide_partner.vote != 1: - karma_to_add += channel.karma_gen_slide_vote + new_vote = 0 if slide_partner.vote == 1 else 1 else: - new_vote = 0 if slide_partner.vote == 1 else -1 - if slide_partner.vote != -1: - karma_to_add -= channel.karma_gen_slide_vote + new_vote = 0 if slide_partner.vote == -1 else -1 + # 2 if the disliked slide was liked by the user + # 1 if the slide was liked OR if the dislike was removed + # -1 if the slide was disliked OR if the like was removed + # -2 if the liked slide was disliked by the user + vote_diff = new_vote - slide_partner.vote + karma_to_add += channel.karma_gen_slide_vote * vote_diff slide_partner.vote = new_vote for new_slide in new_slides: diff --git a/addons/website_slides/static/src/js/slides_slide_like.js b/addons/website_slides/static/src/js/slides_slide_like.js index 99ad4d4e937..f7751d30938 100644 --- a/addons/website_slides/static/src/js/slides_slide_like.js +++ b/addons/website_slides/static/src/js/slides_slide_like.js @@ -48,8 +48,21 @@ var SlideLikeWidget = publicWidget.Widget.extend({ }, }).then(function (data) { if (! data.error) { - self.$el.find('span.o_wslides_js_slide_like_up span').text(data.likes); - self.$el.find('span.o_wslides_js_slide_like_down span').text(data.dislikes); + const $likesBtn = self.$('span.o_wslides_js_slide_like_up'); + const $likesIcon = $likesBtn.find('i.fa'); + const $dislikesBtn = self.$('span.o_wslides_js_slide_like_down'); + const $dislikesIcon = $dislikesBtn.find('i.fa'); + + // update 'thumbs-up' button with latest state + $likesBtn.data('user-vote', data.user_vote); + $likesBtn.find('span').text(data.likes); + $likesIcon.toggleClass("fa-thumbs-up", data.user_vote === 1); + $likesIcon.toggleClass("fa-thumbs-o-up", data.user_vote !== 1); + // update 'thumbs-down' button with latest state + $dislikesBtn.data('user-vote', data.user_vote); + $dislikesBtn.find('span').text(data.dislikes); + $dislikesIcon.toggleClass("fa-thumbs-down", data.user_vote === -1); + $dislikesIcon.toggleClass("fa-thumbs-o-down", data.user_vote !== -1); } else { if (data.error === 'public_user') { var message = _t('Please login to vote this lesson'); @@ -58,8 +71,6 @@ var SlideLikeWidget = publicWidget.Widget.extend({ message = _t('Please create an account to vote this lesson'); } self._popoverAlert(self.$el, _.str.sprintf(message, (document.URL))); - } else if (data.error === 'vote_done') { - self._popoverAlert(self.$el, _t('You have already voted for this lesson')); } else if (data.error === 'slide_access') { self._popoverAlert(self.$el, _t('You don\'t have access to this lesson')); } else if (data.error === 'channel_membership_required') { diff --git a/addons/website_slides/static/src/scss/website_slides.scss b/addons/website_slides/static/src/scss/website_slides.scss index 2f9febde3f6..3133b1bda8c 100644 --- a/addons/website_slides/static/src/scss/website_slides.scss +++ b/addons/website_slides/static/src/scss/website_slides.scss @@ -122,6 +122,11 @@ $truncate-limits: 2, 3, 10; .o_wslides_js_slide_like_up, .o_wslides_js_slide_like_down { + user-select: none; + min-width: 2rem; + > i.fa { + min-width: 1rem; + } &:not(.disabled) { cursor: pointer; color: $link-color; diff --git a/addons/website_slides/tests/test_gamification_karma.py b/addons/website_slides/tests/test_gamification_karma.py index d7e0643e681..712a5eaf140 100644 --- a/addons/website_slides/tests/test_gamification_karma.py +++ b/addons/website_slides/tests/test_gamification_karma.py @@ -40,6 +40,27 @@ class TestKarmaGain(common.SlidesCase): } ]) + @mute_logger('odoo.models') + @users('user_emp') + def test_karma_change_vote(self): + """ Test like / dislike only karma changes """ + channel = self.channel_2.with_user(self.env.user) + channel.action_add_member() + + start_karma = self.env.user.karma + + # join slide through like + slide_2_0 = self.slide_2_0.with_user(self.env.user) + self.assertFalse(slide_2_0.user_membership_id) + slide_2_0.action_like() + self.assertTrue(slide_2_0.user_membership_id) + self.assertEqual(self.env.user.karma, start_karma + channel.karma_gen_slide_vote) + + # dislike: remove gained karma, then remove it again due to dislike + slide_2_0.action_dislike() + self.assertTrue(slide_2_0.user_membership_id) + self.assertEqual(self.env.user.karma, start_karma - channel.karma_gen_slide_vote) + @mute_logger('odoo.models') @users('user_emp', 'user_portal', 'user_officer') def test_karma_gain(self): @@ -78,18 +99,30 @@ class TestKarmaGain(common.SlidesCase): # Vote for a slide slide_user = self.slide.with_user(user) + # up-voting for the first time should add karma slide_user.action_like() computed_karma += self.channel.karma_gen_slide_vote self.assertEqual(user.karma, computed_karma) - slide_user.action_like() # re-like something already liked should not add karma again - self.assertEqual(user.karma, computed_karma) - slide_user.action_dislike() + + # toggling the up-vote should reduce karma + slide_user.action_like() computed_karma -= self.channel.karma_gen_slide_vote self.assertEqual(user.karma, computed_karma) - slide_user.action_dislike() - computed_karma -= self.channel.karma_gen_slide_vote + + slide_user.action_like() + computed_karma += self.channel.karma_gen_slide_vote self.assertEqual(user.karma, computed_karma) - slide_user.action_dislike() # dislike again something already disliked should not remove karma again + + # down-voting the content that was already up-voted + # should reduce the karma two times + slide_user.action_dislike() + computed_karma -= self.channel.karma_gen_slide_vote * 2 + self.assertEqual(user.karma, computed_karma) + + # up-voting the content that was already down-voted + # should add the karma two times + slide_user.action_like() + computed_karma += self.channel.karma_gen_slide_vote * 2 self.assertEqual(user.karma, computed_karma) # Leave the finished course diff --git a/addons/website_slides/tests/test_slide_slide.py b/addons/website_slides/tests/test_slide_slide.py index da2c9483094..aa936fa25a3 100644 --- a/addons/website_slides/tests/test_slide_slide.py +++ b/addons/website_slides/tests/test_slide_slide.py @@ -1,10 +1,28 @@ # -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. +import psycopg2 + from odoo.addons.website_slides.tests import common as slides_common +from odoo.tests.common import users +from odoo.tools import mute_logger -class TestFromURL(slides_common.SlidesCase): +class TestSlideInternals(slides_common.SlidesCase): + + @mute_logger('odoo.sql_db') + @users('user_manager') + def test_slide_create_vote_constraint(self): + # test vote value must be 1, 0 and -1. + with self.assertRaises(psycopg2.errors.CheckViolation), self.cr.savepoint(): + self.env['slide.slide.partner'].create({ + 'slide_id': self.slide.id, + 'channel_id': self.channel.id, + 'partner_id': self.user_manager.partner_id.id, + 'vote': 2, + }) + +class TestVideoFromURL(slides_common.SlidesCase): def test_video_youtube(self): youtube_urls = { 'W0JQcpGLSFw': [ diff --git a/addons/website_slides/tests/test_statistics.py b/addons/website_slides/tests/test_statistics.py index a570530ee84..a418e14765a 100644 --- a/addons/website_slides/tests/test_statistics.py +++ b/addons/website_slides/tests/test_statistics.py @@ -127,12 +127,12 @@ class TestSlideStatistics(common.SlidesCase): self.assertEqual(slide_emp.user_vote, 1) slide_emp.action_dislike() self.assertEqual(slide_emp.likes, 0) - self.assertEqual(slide_emp.dislikes, 0) - self.assertEqual(slide_emp.user_vote, 0) - slide_emp.action_dislike() - self.assertEqual(slide_emp.likes, 0) self.assertEqual(slide_emp.dislikes, 1) self.assertEqual(slide_emp.user_vote, -1) + slide_emp.action_dislike() + self.assertEqual(slide_emp.likes, 0) + self.assertEqual(slide_emp.dislikes, 0) + self.assertEqual(slide_emp.user_vote, 0) def test_slide_statistics_views(self): channel_publisher = self.channel.with_user(self.user_officer) diff --git a/addons/website_slides/views/website_slides_templates_course.xml b/addons/website_slides/views/website_slides_templates_course.xml index ee2c570be92..93d254e0898 100644 --- a/addons/website_slides/views/website_slides_templates_course.xml +++ b/addons/website_slides/views/website_slides_templates_course.xml @@ -731,7 +731,7 @@
-
+
@@ -768,7 +768,7 @@
-
+
@@ -781,29 +781,31 @@
diff --git a/addons/website_slides/views/website_slides_templates_lesson.xml b/addons/website_slides/views/website_slides_templates_lesson.xml index 23855baa091..61d08c2f283 100644 --- a/addons/website_slides/views/website_slides_templates_lesson.xml +++ b/addons/website_slides/views/website_slides_templates_lesson.xml @@ -305,17 +305,32 @@
+
+ Rating +
+
+ + + + + + + + +
+
+
- +
-
+
@@ -336,27 +351,6 @@
-
- - - - - - - - - - - - - - - - - - -
Actions
Likes
Dislikes
Comments
-
@@ -394,22 +388,6 @@
-
- Rating -
-
- - - - - - - - -
-
-
From c1ab7ae88a60b7242b0cc4a7c6e2c32276a29846 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thibault=20Delavall=C3=A9e?= Date: Mon, 28 Feb 2022 09:32:42 +0000 Subject: [PATCH 09/14] [FIX] website_slides: fix youtube video usage Linked video is not reachable anymore on youtube. Let us replace it by another great video. Task-2780920 (eLearning: fix publisher tour) Part-of: odoo/odoo#74171 --- .../static/src/tests/tours/slides_course_publisher.js | 2 +- .../website_slides/static/src/tests/tours/slides_tour_tools.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/addons/website_slides/static/src/tests/tours/slides_course_publisher.js b/addons/website_slides/static/src/tests/tours/slides_course_publisher.js index e65791673cc..adb2c9cc39f 100644 --- a/addons/website_slides/static/src/tests/tours/slides_course_publisher.js +++ b/addons/website_slides/static/src/tests/tours/slides_course_publisher.js @@ -77,7 +77,7 @@ tour.register('course_publisher', { slidesTourTools.addVideoToSection('Introduction'), [{ content: 'eLearning: publish newly added course', - trigger: 'span:contains("Dschinghis Khan - Moskau 1979")', // wait for slide to appear + trigger: 'span:contains("Dschinghis Khan - Dschinghis Khan (1979)")', // wait for slide to appear // trigger: 'span.o_wslides_js_slide_toggle_is_preview:first', run: function () { $('span.o_wslides_js_slide_toggle_is_preview:first').click(); diff --git a/addons/website_slides/static/src/tests/tours/slides_tour_tools.js b/addons/website_slides/static/src/tests/tours/slides_tour_tools.js index bc579154516..bb336619838 100644 --- a/addons/website_slides/static/src/tests/tours/slides_tour_tools.js +++ b/addons/website_slides/static/src/tests/tours/slides_tour_tools.js @@ -33,7 +33,7 @@ var addVideoToSection = function (sectionName, saveAsDraft) { }, { content: 'eLearning: fill video link', trigger: 'input[name=url]', - run: 'text https://www.youtube.com/watch?v=NvS351QKFV4&list=PLtVFNIekBzqIfO4u4n78i43etfw2n1St8&index=2&t=0s', + run: 'text https://www.youtube.com/watch?v=pzmI3vAIhbE', }, { content: 'eLearning: click outside to trigger onchange', trigger: 'div.o_w_slide_upload_modal_container', From 7cc7800e7815782f16391c096a585c380d6fb534 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thibault=20Delavall=C3=A9e?= Date: Mon, 28 Feb 2022 09:33:32 +0000 Subject: [PATCH 10/14] [FIX] website_slides: fix new content used in tour Followup of odoo/odoo@60fadfeaf631590e7c0ceb357e380b52f5f91939 where navbar was updated to be responsive. However that broke eLearning tours. Youpie. Task-2780920 (eLearning: fix publisher tour) Part-of: odoo/odoo#74171 --- .../static/src/tests/tours/slides_course_publisher.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/addons/website_slides/static/src/tests/tours/slides_course_publisher.js b/addons/website_slides/static/src/tests/tours/slides_course_publisher.js index adb2c9cc39f..c1c70f19177 100644 --- a/addons/website_slides/static/src/tests/tours/slides_course_publisher.js +++ b/addons/website_slides/static/src/tests/tours/slides_course_publisher.js @@ -15,7 +15,7 @@ tour.register('course_publisher', { test: true }, [{ content: 'eLearning: click on New (top-menu)', - trigger: 'li.o_new_content_menu a' + trigger: 'div.o_new_content_menu a' }, { content: 'eLearning: click on New Course', trigger: 'a:contains("Course")' @@ -42,7 +42,7 @@ tour.register('course_publisher', { trigger: 'button:contains("Create")', }, { content: 'eLearning: launch course edition', - trigger: 'li[id="edit-page-menu"] a', + trigger: 'div[id="edit-page-menu"] a', }, { content: 'eLearning: double click image to edit it', trigger: 'img.o_wslides_course_pict', From fb83d30290cf0193997f957d9985a13d57994173 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thibault=20Delavall=C3=A9e?= Date: Mon, 28 Feb 2022 08:30:18 +0000 Subject: [PATCH 11/14] [FIX] website_slides: fix tour with video URLs Followup of odoo/odoo@7b5427f6d6026bbb33cbf6bce1f6a7e309d60eef where a frontend form was updated but not the tour using it. Task-2780920 (eLearning: fix publisher tour) Part-of: odoo/odoo#74171 --- .../website_slides/static/src/tests/tours/slides_tour_tools.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/addons/website_slides/static/src/tests/tours/slides_tour_tools.js b/addons/website_slides/static/src/tests/tours/slides_tour_tools.js index bb336619838..9a3a5059d6d 100644 --- a/addons/website_slides/static/src/tests/tours/slides_tour_tools.js +++ b/addons/website_slides/static/src/tests/tours/slides_tour_tools.js @@ -32,7 +32,7 @@ var addVideoToSection = function (sectionName, saveAsDraft) { trigger: 'a[data-slide-category=video]', }, { content: 'eLearning: fill video link', - trigger: 'input[name=url]', + trigger: 'input[name=video_url]', run: 'text https://www.youtube.com/watch?v=pzmI3vAIhbE', }, { content: 'eLearning: click outside to trigger onchange', From 3ccd903b5faa8efd2099e223d78365289cc7dea3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thibault=20Delavall=C3=A9e?= Date: Mon, 28 Feb 2022 09:14:08 +0000 Subject: [PATCH 12/14] [FIX] website_slides: fix tour using image selector Since "I don't know" image selection tour is broken as the current image selector is quite different from the time this tour was run. Update it to use an external URL as it seems we don't have any default image anymore. Task-2780920 (eLearning: fix publisher tour) Part-of: odoo/odoo#74171 --- .../src/tests/tours/slides_course_publisher.js | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/addons/website_slides/static/src/tests/tours/slides_course_publisher.js b/addons/website_slides/static/src/tests/tours/slides_course_publisher.js index c1c70f19177..93a89d5340d 100644 --- a/addons/website_slides/static/src/tests/tours/slides_course_publisher.js +++ b/addons/website_slides/static/src/tests/tours/slides_course_publisher.js @@ -48,18 +48,25 @@ tour.register('course_publisher', { trigger: 'img.o_wslides_course_pict', run: 'dblclick', }, { - content: 'eLearning: click pâtissière', - trigger: 'img[title="s_company_team_image_4.png"]', + content: 'eLearning: click "Add URL" to trigger URL box', + trigger: '.o_upload_media_url_button', }, { - content: 'eLearning: is the pâtissière set ?', + content: 'eLearning: add a bioutifoul URL', + trigger: 'input.o_we_url_input', + run: 'text https://upload.wikimedia.org/wikipedia/commons/thumb/1/12/ThreeTimeAKCGoldWinnerPembrookeWelshCorgi.jpg/800px-ThreeTimeAKCGoldWinnerPembrookeWelshCorgi.jpg' +}, { + content: 'eLearning: click "Add URL" really adding image', + trigger: '.o_upload_media_url_button', +}, { + content: 'eLearning: is the Corgi set ?', trigger: 'img.o_wslides_course_pict', run: function () { - if ($('img.o_wslides_course_pict').attr('src').endsWith('s_team_member_4.png')) { + if ($('img.o_wslides_course_pict').attr('src').endsWith('GoldWinnerPembrookeWelshCorgi.jpg')) { $('img.o_wslides_course_pict').addClass('o_wslides_tour_success'); } }, }, { - content: 'eLearning: the pâtissière is set !', + content: 'eLearning: the Corgi is set !', trigger: 'img.o_wslides_course_pict.o_wslides_tour_success', }, { content: 'eLearning: save course edition', From 7a5e81f9e13431404c33786d0b2bbbf87c06c5aa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thibault=20Delavall=C3=A9e?= Date: Mon, 28 Feb 2022 09:14:13 +0000 Subject: [PATCH 13/14] [FIX] website_slides: fix publisher tour Not sure how it was working before anyway. Task-2780920 (eLearning: fix publisher tour) Part-of: odoo/odoo#74171 --- .../static/src/tests/tours/slides_course_publisher.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/addons/website_slides/static/src/tests/tours/slides_course_publisher.js b/addons/website_slides/static/src/tests/tours/slides_course_publisher.js index 93a89d5340d..c2c15e24ff8 100644 --- a/addons/website_slides/static/src/tests/tours/slides_course_publisher.js +++ b/addons/website_slides/static/src/tests/tours/slides_course_publisher.js @@ -32,7 +32,7 @@ tour.register('course_publisher', { in_modal: false, }, { content: 'eLearning: set description', - trigger: 'input[name="description"]', + trigger: 'textarea[name="description"]', run: 'text Déboulonnate is very common at Fleurus', }, { content: 'eLearning: we want reviews', From 1af6dfe47baf1f42865203d0f3edafc6b50bbae8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thibault=20Delavall=C3=A9e?= Date: Mon, 28 Feb 2022 10:05:45 +0000 Subject: [PATCH 14/14] [IMP] website_slides: add a standard publisher tour Purpose of this commit is to add a publisher tour in standard that is not only run using external tags. Indeed external tours are not monitored and previous commits show that they can be broken since a long time. In this commit we somehow copy the publisher tour with an article-based slide instead of a video slide. That way it can be launched with other standard tours. Task-2780920 (eLearning: fix publisher tour) Part-of: odoo/odoo#74171 --- .../tours/slide_course_publisher_standard.js | 104 ++++++++++++++++++ .../src/tests/tours/slides_tour_tools.js | 3 + .../website_slides/tests/test_ui_wslides.py | 23 +++- 3 files changed, 129 insertions(+), 1 deletion(-) create mode 100644 addons/website_slides/static/src/tests/tours/slide_course_publisher_standard.js diff --git a/addons/website_slides/static/src/tests/tours/slide_course_publisher_standard.js b/addons/website_slides/static/src/tests/tours/slide_course_publisher_standard.js new file mode 100644 index 00000000000..dc8ea89c266 --- /dev/null +++ b/addons/website_slides/static/src/tests/tours/slide_course_publisher_standard.js @@ -0,0 +1,104 @@ +/** @odoo-module **/ + +import tour from 'web_tour.tour'; +import slidesTourTools from '@website_slides/tests/tours/slides_tour_tools'; + +/** + * Global use case: + * a user (website publisher) creates a course; + * he updates it; + * he creates some lessons in it; + * he publishes it; + */ +tour.register('course_publisher_standard', { + url: '/slides', + test: true +}, [{ + content: 'eLearning: click on New (top-menu)', + trigger: 'div.o_new_content_menu a' +}, { + content: 'eLearning: click on New Course', + trigger: 'a:contains("Course")' +}, { + content: 'eLearning: set name', + trigger: 'input[name="name"]', + run: 'text How to Déboulonnate', +}, { + content: 'eLearning: click on tags', + trigger: 'ul.select2-choices:first', +}, { + content: 'eLearning: select gardener tag', + trigger: 'div.select2-result-label:contains("Gardener")', + in_modal: false, +}, { + content: 'eLearning: set description', + trigger: 'textarea[name="description"]', + run: 'text Déboulonnate is very common at Fleurus', +}, { + content: 'eLearning: we want reviews', + trigger: 'input[name="allow_comment"]', +}, { + content: 'eLearning: seems cool, create it', + trigger: 'button:contains("Create")', +}, { + content: 'eLearning: launch course edition', + trigger: 'div[id="edit-page-menu"] a', +}, { + content: 'eLearning: double click image to edit it', + trigger: 'img.o_wslides_course_pict', + run: 'dblclick', +}, { + content: 'eLearning: click "Add URL" to trigger URL box', + trigger: '.o_upload_media_url_button', +}, { + content: 'eLearning: add a bioutifoul URL', + trigger: 'input.o_we_url_input', + run: 'text https://upload.wikimedia.org/wikipedia/commons/thumb/1/12/ThreeTimeAKCGoldWinnerPembrookeWelshCorgi.jpg/800px-ThreeTimeAKCGoldWinnerPembrookeWelshCorgi.jpg' +}, { + content: 'eLearning: click "Add URL" really adding image', + trigger: '.o_upload_media_url_button', +}, { + content: 'eLearning: is the Corgi set ?', + trigger: 'img.o_wslides_course_pict', + run: function () { + if ($('img.o_wslides_course_pict').attr('src').endsWith('GoldWinnerPembrookeWelshCorgi.jpg')) { + $('img.o_wslides_course_pict').addClass('o_wslides_tour_success'); + } + }, +}, { + content: 'eLearning: the Corgi is set !', + trigger: 'img.o_wslides_course_pict.o_wslides_tour_success', +}, { + content: 'eLearning: save course edition', + trigger: 'button[data-action="save"]', +}, { + content: 'eLearning: course create with current member', + extra_trigger: 'body:not(.editor_enable)', // wait for editor to close + trigger: '.o_wslides_js_course_join:contains("You\'re enrolled")', + run: function () {} // check membership +} +].concat( + slidesTourTools.addExistingCourseTag(), + slidesTourTools.addNewCourseTag('The Most Awesome Course'), + slidesTourTools.addSection('Introduction'), + slidesTourTools.addArticleToSection('Introduction', 'MyArticle'), + [{ + content: "eLearning: check editor is loaded for article", + trigger: 'body.editor_enable', + timeout: 30000, + run: () => null, // it's a check +}, { + content: "eLearning: save article", + trigger: '.o_we_website_top_actions button.btn-primary:contains("Save")', +}, { + content: "eLearning: use breadcrumb to go back to channel", + trigger: '.o_wslides_course_nav a:contains("Déboulonnate")', +}] +// [ +// { +// content: 'eLearning: move new course inside introduction', +// trigger: 'div.o_wslides_slides_list_drag', +// // run: 'drag_and_drop div.o_wslides_slides_list_drag ul.ui-sortable:first', +// run: 'drag_and_drop div.o_wslides_slides_list_drag a.o_wslides_js_slide_section_add', +// }] +)); diff --git a/addons/website_slides/static/src/tests/tours/slides_tour_tools.js b/addons/website_slides/static/src/tests/tours/slides_tour_tools.js index 9a3a5059d6d..6c8ae37ade4 100644 --- a/addons/website_slides/static/src/tests/tours/slides_tour_tools.js +++ b/addons/website_slides/static/src/tests/tours/slides_tour_tools.js @@ -79,6 +79,9 @@ var addArticleToSection = function (sectionName, pageName) { content: 'eLearning: fill article completion time', trigger: 'input[name=duration]', run: 'text 4', +}, { + content: 'eLearning: create and publish slide', + trigger: 'footer.modal-footer button:contains("Publish")', }]; }; diff --git a/addons/website_slides/tests/test_ui_wslides.py b/addons/website_slides/tests/test_ui_wslides.py index bc5fa4bf21f..2062a788ac1 100644 --- a/addons/website_slides/tests/test_ui_wslides.py +++ b/addons/website_slides/tests/test_ui_wslides.py @@ -152,8 +152,29 @@ class TestUi(TestUICommon): login=user_demo.login) +@tests.common.tagged('post_install', '-at_install') +class TestUiPublisher(HttpCaseWithUserDemo): + + def test_course_publisher_elearning_manager(self): + user_demo = self.user_demo + user_demo.flush() + user_demo.write({ + 'groups_id': [ + (5, 0), + (4, self.env.ref('base.group_user').id), + (4, self.env.ref('website_slides.group_website_slides_manager').id) + ], + }) + + self.browser_js( + '/slides', + 'odoo.__DEBUG__.services["web_tour.tour"].run("course_publisher_standard")', + 'odoo.__DEBUG__.services["web_tour.tour"].tours.course_publisher_standard.ready', + login=user_demo.login) + + @tests.common.tagged('external', 'post_install', '-standard', '-at_install') -class TestUiYoutube(HttpCaseWithUserDemo): +class TestUiPublisherYoutube(HttpCaseWithUserDemo): def test_course_member_yt_employee(self): # remove membership because we need to be able to join the course during the tour