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 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/controllers/main.py b/addons/website_slides/controllers/main.py index 69de0219d71..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,26 +112,22 @@ 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] def _get_slide_quiz_data(self, slide): - slide_completed = slide.user_membership_id.sudo().completed values = { 'slide_questions': [{ 'id': question.id, @@ -129,7 +135,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] @@ -313,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, @@ -328,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): @@ -388,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, @@ -402,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 @@ -481,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'), @@ -495,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': @@ -519,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 # -------------------------------------------------- @@ -715,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 @@ -803,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'): @@ -825,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): @@ -958,7 +965,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 +999,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/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..28e85a4fada 100644 --- a/addons/website_slides/models/slide_channel.py +++ b/addons/website_slides/models/slide_channel.py @@ -34,6 +34,17 @@ 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!' + ), + ('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): 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 ed99cfd752a..93f909d91de 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') @@ -36,6 +38,17 @@ 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!' + ), + ('check_vote', + 'CHECK(vote IN (-1, 0, 1))', + 'The vote must be 1, 0 or -1.' + ), + ] + @api.model_create_multi def create(self, vals_list): res = super().create(vals_list) @@ -116,6 +129,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 +199,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 +307,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 +397,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 +411,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): @@ -813,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/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_course_publisher.js b/addons/website_slides/static/src/tests/tours/slides_course_publisher.js index e65791673cc..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 @@ -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")' @@ -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', @@ -42,24 +42,31 @@ 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', 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', @@ -77,7 +84,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..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 @@ -32,8 +32,8 @@ var addVideoToSection = function (sectionName, saveAsDraft) { trigger: 'a[data-slide-category=video]', }, { content: 'eLearning: fill video link', - trigger: 'input[name=url]', - run: 'text https://www.youtube.com/watch?v=NvS351QKFV4&list=PLtVFNIekBzqIfO4u4n78i43etfw2n1St8&index=2&t=0s', + trigger: 'input[name=video_url]', + run: 'text https://www.youtube.com/watch?v=pzmI3vAIhbE', }, { content: 'eLearning: click outside to trigger onchange', trigger: 'div.o_w_slide_upload_modal_container', @@ -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/__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 64% rename from addons/website_slides/tests/test_karma.py rename to addons/website_slides/tests/test_gamification_karma.py index a9453da8caf..712a5eaf140 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,41 @@ 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') + 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') @@ -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_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..aa936fa25a3 --- /dev/null +++ b/addons/website_slides/tests/test_slide_slide.py @@ -0,0 +1,117 @@ +# -*- 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 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': [ + '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_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/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 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/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 -
-
- - - - - - - - -
-
-
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 + + + + + + + + + + + + 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}'))