[IMP] website_slides: improve and add statistics computation
Purpose of this commit is to add some statistics computation on slide.channel
model. We want notably to have a count of views and votes (likes and dislikes)
on channels. Completion is also computed.
Purpose is to be able to search and order channels based on those statistics.
Having stored computed fields for some statistics help achieving that purpose.
Tests are added.
This commit is linked to task ID 1936153 and PR #30985.
This commit is contained in:
@@ -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({
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
# --------------------------------------------------
|
||||
|
||||
@@ -3,3 +3,4 @@
|
||||
|
||||
from . import common
|
||||
from . import test_security
|
||||
from . import test_statistics
|
||||
|
||||
@@ -51,6 +51,7 @@ class SlidesCase(common.SavepointCase):
|
||||
'channel_id': self.channel.id,
|
||||
'slide_type': 'presentation',
|
||||
'website_published': True,
|
||||
'completion_time': 2.0,
|
||||
})
|
||||
|
||||
@contextmanager
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -95,6 +95,22 @@
|
||||
<field name="enroll_msg" colspan="4" nolabel="1"/>
|
||||
</group>
|
||||
</page>
|
||||
<page string="Statistics" groups="base.group_no_one">
|
||||
<group>
|
||||
<group>
|
||||
<field name="nbr_presentations"/>
|
||||
<field name="nbr_documents"/>
|
||||
<field name="nbr_videos"/>
|
||||
<field name="nbr_infographics"/>
|
||||
<field name="total_slides"/>
|
||||
</group>
|
||||
<group>
|
||||
<field name="total_views"/>
|
||||
<field name="total_votes"/>
|
||||
<field name="total_time"/>
|
||||
</group>
|
||||
</group>
|
||||
</page>
|
||||
</notebook>
|
||||
</sheet>
|
||||
<div class="oe_chatter">
|
||||
|
||||
@@ -291,7 +291,7 @@
|
||||
<section>
|
||||
<div class="container mt16">
|
||||
<div class="row">
|
||||
<div class="col-xl-12 col-lg-12 col-md-12 col-12" style="padding-left: 15px;padding-right: 15px;" t-if="channel.total == 0">
|
||||
<div class="col-xl-12 col-lg-12 col-md-12 col-12" style="padding-left: 15px;padding-right: 15px;" t-if="channel.total_slides == 0">
|
||||
<div class="jumbotron">
|
||||
<h1>No presentation published yet.</h1>
|
||||
<p>
|
||||
@@ -329,7 +329,7 @@
|
||||
<span t-if="not category['id']" title="Uncategorized presentation" class="h3 mt0 mb0">
|
||||
<t t-esc="category['name']"/>
|
||||
</span>
|
||||
<a t-attf-href="/slides/#{slug(channel)}/category/#{category['id']}" class="float-right" t-if="category['total'] - 4 > 0 and category['id']">
|
||||
<a t-attf-href="/slides/#{slug(channel)}/category/#{category['id']}" class="float-right" t-if="category['total_slides'] - 4 > 0 and category['id']">
|
||||
<i class="fa fa-arrow-right"/> See all
|
||||
</a>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user