diff --git a/addons/website_slides/controllers/main.py b/addons/website_slides/controllers/main.py index 881c81984e9..e99e5f035f4 100644 --- a/addons/website_slides/controllers/main.py +++ b/addons/website_slides/controllers/main.py @@ -176,7 +176,7 @@ class WebsiteSlides(http.Controller): category_datas.append({ 'id': category_id, 'name': name, - 'total': category['category_id_count'], + 'total_slides': category['category_id_count'], 'slides': Slide.search(category['__domain'], limit=4, offset=0, order=order) }) values.update({ diff --git a/addons/website_slides/models/slide_channel.py b/addons/website_slides/models/slide_channel.py index ec68cc3f46e..11ee28cc3f9 100644 --- a/addons/website_slides/models/slide_channel.py +++ b/addons/website_slides/models/slide_channel.py @@ -1,8 +1,10 @@ # -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. + +import math import uuid -from odoo import api, fields, models, SUPERUSER_ID, _ +from odoo import api, fields, models, _ from odoo.addons.http_routing.models.ir_http import slug from odoo.tools.translate import html_translate from odoo.osv import expression @@ -14,8 +16,36 @@ class ChannelUsersRelation(models.Model): _table = 'slide_channel_partner' channel_id = fields.Many2one('slide.channel', index=True, required=True) + completed = fields.Boolean('Is Completed', help='Channel validated, even if slides / lessons are added once done.') + completion = fields.Integer('Completion', compute='_compute_completion', store=True) partner_id = fields.Many2one('res.partner', index=True, required=True) + @api.depends('channel_id.slide_partner_ids.partner_id', 'channel_id.slide_partner_ids.completed', 'channel_id.total_slides', 'partner_id') + def _compute_completion(self): + read_group_res = self.env['slide.slide.partner'].sudo().read_group( + ['&', '&', ('channel_id', 'in', self.mapped('channel_id').ids), + ('partner_id', 'in', self.mapped('partner_id').ids), + ('completed', '=', True)], + ['channel_id', 'partner_id'], + groupby=['channel_id', 'partner_id'], lazy=False) + mapped_data = dict() + for item in read_group_res: + mapped_data.setdefault(item['channel_id'][0], dict()) + mapped_data[item['channel_id'][0]][item['partner_id'][0]] = item['__count'] + + channel_data = {} + channel_ids = mapped_data.keys() + if channel_ids: + channel_read_res = self.env['slide.channel'].sudo().browse(channel_ids).read(['total_slides']) + channel_data = dict((channel['id'], channel['total_slides']) for channel in channel_read_res) + + for record in self: + slide_done = mapped_data.get(record.channel_id.id, dict()).get(record.partner_id.id, 0) + slide_total = channel_data.get(record.channel_id.id) or 1 + record.completion = math.ceil(100.0 * slide_done / slide_total) + if record.completion >= 100: + record.update({'completed': True}) + class Channel(models.Model): """ A channel is a container of slides. It has group-based access configuration @@ -40,6 +70,7 @@ class Channel(models.Model): category_ids = fields.One2many('slide.category', 'channel_id', string="Categories") # slides: promote, statistics slide_ids = fields.One2many('slide.slide', 'channel_id', string="Slides") + slide_partner_ids = fields.One2many('slide.slide.partner', 'channel_id', string="Slide User Data", groups='website.group_website_publisher') promote_strategy = fields.Selection([ ('none', 'No Featured Presentation'), ('latest', 'Latest Published'), @@ -50,11 +81,14 @@ class Channel(models.Model): custom_slide_id = fields.Many2one('slide.slide', string='Slide to Promote') promoted_slide_id = fields.Many2one('slide.slide', string='Featured Slide', compute='_compute_promoted_slide_id', store=True) access_token = fields.Char("Security Token", copy=False, default=_default_access_token) - nbr_presentations = fields.Integer('Number of Presentations', compute='_count_presentations', store=True) - nbr_documents = fields.Integer('Number of Documents', compute='_count_presentations', store=True) - nbr_videos = fields.Integer('Number of Videos', compute='_count_presentations', store=True) - nbr_infographics = fields.Integer('Number of Infographics', compute='_count_presentations', store=True) - total = fields.Integer(compute='_count_presentations', store=True) + nbr_presentations = fields.Integer('Number of Presentations', compute='_compute_slides_statistics', store=True) + nbr_documents = fields.Integer('Number of Documents', compute='_compute_slides_statistics', store=True) + nbr_videos = fields.Integer('Number of Videos', compute='_compute_slides_statistics', store=True) + nbr_infographics = fields.Integer('Number of Infographics', compute='_compute_slides_statistics', store=True) + total_slides = fields.Integer('# Slides', compute='_compute_slides_statistics', store=True, oldname='total') + total_views = fields.Integer('# Views', compute='_compute_slides_statistics', store=True) + total_votes = fields.Integer('# Votes', compute='_compute_slides_statistics', store=True) + total_time = fields.Float('# Hours', compute='_compute_slides_statistics', digits=(10, 4), store=True) # configuration publish_template_id = fields.Many2one( 'mail.template', string='Published Template', @@ -80,6 +114,8 @@ class Channel(models.Model): 'res.groups', 'rel_upload_groups', 'channel_id', 'group_id', string='Upload Groups', help="Groups allowed to upload presentations in this channel. If void, every user can upload.") # not stored access fields, depending on each user + completed = fields.Boolean('Done', compute='_compute_user_statistics') + completion = fields.Integer('Completion', compute='_compute_user_statistics') can_upload = fields.Boolean('Can Upload', compute='_compute_access') can_publish = fields.Boolean('Can Publish', compute='_compute_access') @@ -110,22 +146,39 @@ class Channel(models.Model): channel.valid_channel_partner_ids = result.get(channel.id, False) channel.is_member = self.env.user.partner_id.id in channel.valid_channel_partner_ids if channel.valid_channel_partner_ids else False - @api.depends('slide_ids.slide_type', 'slide_ids.website_published') - def _count_presentations(self): - result = dict.fromkeys(self.ids, dict()) - res = self.env['slide.slide'].read_group( - [('website_published', '=', True), ('channel_id', 'in', self.ids)], - ['channel_id', 'slide_type'], + @api.depends('slide_ids.slide_type', 'slide_ids.is_published', + 'slide_ids.likes', 'slide_ids.dislikes', 'slide_ids.total_views') + def _compute_slides_statistics(self): + result = dict.fromkeys(self.ids, dict( + nbr_presentations=0, nbr_documents=0, nbr_videos=0, nbr_infographics=0, + total_slides=0, total_views=0, total_votes=0, total_time=0)) + read_group_res = self.env['slide.slide'].read_group( + [('is_published', '=', True), ('channel_id', 'in', self.ids)], + ['channel_id', 'slide_type', 'likes', 'dislikes', 'total_views', 'completion_time'], groupby=['channel_id', 'slide_type'], lazy=False) - for res_group in res: - result[res_group['channel_id'][0]][res_group['slide_type']] = result[res_group['channel_id'][0]].get(res_group['slide_type'], 0) + res_group['__count'] + for res_group in read_group_res: + cid = res_group['channel_id'][0] + result[cid]['nbr_presentations'] += res_group.get('slide_type', '') == 'presentation' and res_group['__count'] or 0 + result[cid]['nbr_documents'] += res_group.get('slide_type', '') == 'document' and res_group['__count'] or 0 + result[cid]['nbr_videos'] += res_group.get('slide_type', '') == 'video' and res_group['__count'] or 0 + result[cid]['nbr_infographics'] += res_group.get('slide_type', '') == 'infographic' and res_group['__count'] or 0 + result[cid]['total_slides'] += res_group['__count'] + result[cid]['total_views'] += res_group.get('total_views', 0) + result[cid]['total_votes'] += res_group.get('likes', 0) + result[cid]['total_votes'] -= res_group.get('dislikes', 0) + result[cid]['total_time'] += res_group.get('completion_time', 0) for record in self: - record.nbr_presentations = result[record.id].get('presentation', 0) - record.nbr_documents = result[record.id].get('document', 0) - record.nbr_videos = result[record.id].get('video', 0) - record.nbr_infographics = result[record.id].get('infographic', 0) - record.total = record.nbr_presentations + record.nbr_documents + record.nbr_videos + record.nbr_infographics + record.update(result[record.id]) + + @api.depends('slide_partner_ids') + def _compute_user_statistics(self): + current_user_info = self.env['slide.channel.partner'].sudo().search( + [('channel_id', 'in', self.ids), ('partner_id', '=', self.env.user.partner_id.id)] + ) + mapped_data = dict((info.channel_id.id, (info.completed, info.completion)) for info in current_user_info) + for record in self: + record.completed, record.completion = mapped_data.get(record.id, (False, 0)) @api.one @api.depends('visibility', 'partner_ids', 'upload_group_ids') @@ -170,7 +223,7 @@ class Channel(models.Model): @api.model def create(self, vals): # Ensure creator is member of its channel it is easier for him to manage it - if vals.get('visibility') == 'invite' and not vals.get('channel_partner_ids'): + if not vals.get('channel_partner_ids'): vals['channel_partner_ids'] = [(0, 0, { 'partner_id': self.env.user.partner_id.id })] @@ -264,13 +317,13 @@ class Category(models.Model): nbr_documents = fields.Integer("Number of Documents", compute='_count_presentations', store=True) nbr_videos = fields.Integer("Number of Videos", compute='_count_presentations', store=True) nbr_infographics = fields.Integer("Number of Infographics", compute='_count_presentations', store=True) - total = fields.Integer(compute='_count_presentations', store=True) + total_slides = fields.Integer(compute='_count_presentations', store=True, oldname='total') - @api.depends('slide_ids.slide_type', 'slide_ids.website_published') + @api.depends('slide_ids.slide_type', 'slide_ids.is_published') def _count_presentations(self): result = dict.fromkeys(self.ids, dict()) res = self.env['slide.slide'].read_group( - [('website_published', '=', True), ('category_id', 'in', self.ids)], + [('is_published', '=', True), ('category_id', 'in', self.ids)], ['category_id', 'slide_type'], ['category_id', 'slide_type'], lazy=False) for res_group in res: @@ -280,4 +333,4 @@ class Category(models.Model): record.nbr_documents = result[record.id].get('document', 0) record.nbr_videos = result[record.id].get('video', 0) record.nbr_infographics = result[record.id].get('infographic', 0) - record.total = record.nbr_presentations + record.nbr_documents + record.nbr_videos + record.nbr_infographics + record.total_slides = record.nbr_presentations + record.nbr_documents + record.nbr_videos + record.nbr_infographics diff --git a/addons/website_slides/models/slide_slide.py b/addons/website_slides/models/slide_slide.py index 38f7a49968f..c49042d8547 100644 --- a/addons/website_slides/models/slide_slide.py +++ b/addons/website_slides/models/slide_slide.py @@ -14,7 +14,7 @@ from werkzeug import urls from odoo import api, fields, models, SUPERUSER_ID, _ from odoo.addons.http_routing.models.ir_http import slug from odoo.tools import image -from odoo.exceptions import Warning +from odoo.exceptions import Warning, UserError from odoo.http import request from odoo.addons.http_routing.models.ir_http import url_for @@ -25,8 +25,10 @@ class SlidePartnerRelation(models.Model): _table = 'slide_slide_partner' slide_id = fields.Many2one('slide.slide', index=True, required=True) + channel_id = fields.Many2one('slide.channel', string="Channel", related="slide_id.channel_id", store=True, index=True) partner_id = fields.Many2one('res.partner', index=True, required=True) vote = fields.Integer('Vote', default=0) + completed = fields.Boolean('Completed') class EmbeddedSlide(models.Model): @@ -106,7 +108,8 @@ class Slide(models.Model): [('none', 'No One'), ('user', 'Authenticated Users Only'), ('public', 'Everyone')], string='Download Security', required=True, default='user') - is_preview = fields.Boolean('Previewable', default=False) # TDE FIXME: clean name + help + is_preview = fields.Boolean('Always visible', default=False) + completion_time = fields.Float('# Hours', default=1, digits=(10, 4)) image = fields.Binary('Image', attachment=True) image_medium = fields.Binary('Medium', compute="_get_image", store=True, attachment=True) image_thumb = fields.Binary('Thumbnail', compute="_get_image", store=True, attachment=True) @@ -360,6 +363,54 @@ class Slide(models.Model): 'slide_partner_ids': [(0, 0, {'vote': new_vote, 'partner_id': self.env.user.partner_id.id})] }) + def action_set_viewed(self): + if not all(slide.channel_id.is_member for slide in self): + raise UserError(_('You cannot mark a slide as viewed if you are not among its members.')) + + return self._action_set_viewed(self.env.user.partner_id) + + def _action_set_viewed(self, target_partner): + self_sudo = self.sudo() + SlidePartnerSudo = self.env['slide.slide.partner'].sudo() + existing_sudo = SlidePartnerSudo.search([ + ('slide_id', 'in', self.ids), + ('partner_id', '=', target_partner.id) + ]) + + new_slides = self_sudo - existing_sudo.mapped('slide_id') + SlidePartnerSudo.create([{ + 'slide_id': new_slide.id, + 'channel_id': new_slide.channel_id.id, + 'partner_id': target_partner.id, + 'vote': 0} for new_slide in new_slides]) + + return True + + def action_set_completed(self): + if not all(slide.channel_id.is_member for slide in self): + raise UserError(_('You cannot mark a slide as completed if you are not among its members.')) + + return self._action_set_completed(self.env.user.partner_id) + + def _action_set_completed(self, target_partner): + self_sudo = self.sudo() + SlidePartnerSudo = self.env['slide.slide.partner'].sudo() + existing_sudo = SlidePartnerSudo.search([ + ('slide_id', 'in', self.ids), + ('partner_id', '=', target_partner.id) + ]) + existing_sudo.write({'completed': True}) + + new_slides = self_sudo - existing_sudo.mapped('slide_id') + SlidePartnerSudo.create([{ + 'slide_id': new_slide.id, + 'channel_id': new_slide.channel_id.id, + 'partner_id': target_partner.id, + 'vote': 0, + 'completed': True} for new_slide in new_slides]) + + return True + # -------------------------------------------------- # Parsing methods # -------------------------------------------------- diff --git a/addons/website_slides/tests/__init__.py b/addons/website_slides/tests/__init__.py index bb7921266e3..c9a6c617426 100644 --- a/addons/website_slides/tests/__init__.py +++ b/addons/website_slides/tests/__init__.py @@ -3,3 +3,4 @@ from . import common from . import test_security +from . import test_statistics diff --git a/addons/website_slides/tests/common.py b/addons/website_slides/tests/common.py index 5bf179cbdc8..6745da6614a 100644 --- a/addons/website_slides/tests/common.py +++ b/addons/website_slides/tests/common.py @@ -51,6 +51,7 @@ class SlidesCase(common.SavepointCase): 'channel_id': self.channel.id, 'slide_type': 'presentation', 'website_published': True, + 'completion_time': 2.0, }) @contextmanager diff --git a/addons/website_slides/tests/test_statistics.py b/addons/website_slides/tests/test_statistics.py index 011044ef99f..3c68535a171 100644 --- a/addons/website_slides/tests/test_statistics.py +++ b/addons/website_slides/tests/test_statistics.py @@ -51,8 +51,35 @@ class TestStatistics(common.SlidesCase): 'visibility': 'invite', }) channel_publisher._action_add_member(self.user_emp.partner_id) + channel_emp = self.channel.sudo(self.user_emp) slides_emp = (self.slide | self.slide_2).sudo(self.user_emp) - slides_emp.action_view() - channel_emp = self.channel.sudo(self.user_emp) - self.assertEqual(channel_emp.completion, math.ceil(100.0 * len(slides_emp) / len(channel_publisher.slide_ids))) + slides_emp.action_set_viewed() + self.assertEqual(channel_emp.completion, 0) + + slides_emp.action_set_completed() + channel_emp.invalidate_cache() + self.assertEqual( + channel_emp.completion, + math.ceil(100.0 * len(slides_emp) / len(channel_publisher.slide_ids))) + self.assertFalse(channel_emp.completed) + + self.slide_3.sudo(self.user_emp).action_set_completed() + self.assertEqual(channel_emp.completion, 100) + self.assertTrue(channel_emp.completed) + + @mute_logger('odoo.models') + def test_channel_user_statistics_complete_check_member(self): + (self.slide | self.slide_2).write({'is_preview': True}) + slides_emp = (self.slide | self.slide_2).sudo(self.user_emp) + slides_emp.read(['name']) + with self.assertRaises(UserError): + slides_emp.action_set_completed() + + @mute_logger('odoo.models') + def test_channel_user_statistics_view_check_member(self): + (self.slide | self.slide_2).write({'is_preview': True}) + slides_emp = (self.slide | self.slide_2).sudo(self.user_emp) + slides_emp.read(['name']) + with self.assertRaises(UserError): + slides_emp.action_set_viewed() diff --git a/addons/website_slides/views/slide_channel_views.xml b/addons/website_slides/views/slide_channel_views.xml index bb044e82723..70fc407e590 100644 --- a/addons/website_slides/views/slide_channel_views.xml +++ b/addons/website_slides/views/slide_channel_views.xml @@ -95,6 +95,22 @@ + + + + + + + + + + + + + + + +
diff --git a/addons/website_slides/views/website_slides_templates.xml b/addons/website_slides/views/website_slides_templates.xml index ac12bd467a2..a4aec0f8358 100644 --- a/addons/website_slides/views/website_slides_templates.xml +++ b/addons/website_slides/views/website_slides_templates.xml @@ -291,7 +291,7 @@
-
+

No presentation published yet.

@@ -329,7 +329,7 @@ - + See all