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 @@