summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--addons/website_slides/__manifest__.py1
-rw-r--r--addons/website_slides/controllers/main.py11
-rw-r--r--addons/website_slides/data/slide_data.xml6
-rw-r--r--addons/website_slides/data/website_slides_demo.xml1
-rw-r--r--addons/website_slides/models/__init__.py1
-rw-r--r--addons/website_slides/models/slide_channel.py135
-rw-r--r--addons/website_slides/models/slide_channel_tag.py31
-rw-r--r--addons/website_slides/models/slide_slide.py59
-rw-r--r--addons/website_slides/security/ir.model.access.csv4
-rw-r--r--addons/website_slides/tests/__init__.py1
-rw-r--r--addons/website_slides/tests/common.py1
-rw-r--r--addons/website_slides/tests/test_statistics.py33
-rw-r--r--addons/website_slides/views/slide_channel_tag_views.xml96
-rw-r--r--addons/website_slides/views/slide_channel_views.xml55
-rw-r--r--addons/website_slides/views/website_slides_menu_views.xml11
-rw-r--r--addons/website_slides/views/website_slides_templates.xml85
16 files changed, 395 insertions, 136 deletions
diff --git a/addons/website_slides/__manifest__.py b/addons/website_slides/__manifest__.py
index b24100651b2..7ebfc125132 100644
--- a/addons/website_slides/__manifest__.py
+++ b/addons/website_slides/__manifest__.py
@@ -29,6 +29,7 @@ Share and Publish Videos, Presentations and Documents'
'views/website_slides_embed_templates.xml',
'views/slide_slide_views.xml',
'views/slide_channel_views.xml',
+ 'views/slide_channel_tag_views.xml',
'views/website_slides_menu_views.xml',
'data/website_slides_ir_data.xml',
'data/mail_data.xml',
diff --git a/addons/website_slides/controllers/main.py b/addons/website_slides/controllers/main.py
index 881c81984e9..3ea51a7395e 100644
--- a/addons/website_slides/controllers/main.py
+++ b/addons/website_slides/controllers/main.py
@@ -148,6 +148,7 @@ class WebsiteSlides(http.Controller):
'tag': tag,
'slide_type': slide_type,
'sorting': sorting,
+ 'search': search,
'user': user,
'pager': pager,
'is_public_user': request.website.is_public_user(),
@@ -164,9 +165,6 @@ class WebsiteSlides(http.Controller):
'last_message': html2plaintext(last_message_data.get('body', '')),
'last_rating_value': last_message_data.get('rating_value'),
})
- if search:
- values['search'] = search
- return request.render('website_slides.slides_search', values)
# Display uncategorized slides
if not slide_type and not category:
@@ -176,7 +174,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({
@@ -221,11 +219,6 @@ class WebsiteSlides(http.Controller):
return request.redirect('/web/login?redirect=/slides/slide/%s' % (slide.id))
return request.render("website.403")
- @http.route('''/slides/slide/<model("slide.slide"):slide>/promote''', type='http', auth='user', website=True)
- def slide_set_promoted(self, slide, **kwargs):
- slide.channel_id.promoted_slide_id = slide.id
- return request.redirect("/slides/%s" % slide.channel_id.id)
-
# JSONRPC
@http.route('/slides/slide/like', type='json', auth="user", website=True)
def slide_like(self, slide_id, upvote):
diff --git a/addons/website_slides/data/slide_data.xml b/addons/website_slides/data/slide_data.xml
index 164ba5957ef..cb2eda5bb3d 100644
--- a/addons/website_slides/data/slide_data.xml
+++ b/addons/website_slides/data/slide_data.xml
@@ -15,5 +15,11 @@
<field name="visibility">invite</field>
<field name="description">Used to publish slides in partner network privately.</field>
</record>
+
+ <record id="slide_channel_tag_group_data_other" model="slide.channel.tag.group">
+ <field name="name">Tags</field>
+ <field name="sequence">20</field>
+ <field name="website_published" eval="True"/>
+ </record>
</data>
</odoo>
diff --git a/addons/website_slides/data/website_slides_demo.xml b/addons/website_slides/data/website_slides_demo.xml
index 6fef184dcdd..5bc27b8198f 100644
--- a/addons/website_slides/data/website_slides_demo.xml
+++ b/addons/website_slides/data/website_slides_demo.xml
@@ -256,7 +256,6 @@ Discover Odoo Inventory: https://www.odoo.com/page/warehouse</field>
<record model="slide.channel" id="channel_public">
<field name="promote_strategy">custom</field>
- <field name="custom_slide_id" ref="slide_0"/>
</record>
</data>
diff --git a/addons/website_slides/models/__init__.py b/addons/website_slides/models/__init__.py
index c0bc050e663..3f6a3e49ddf 100644
--- a/addons/website_slides/models/__init__.py
+++ b/addons/website_slides/models/__init__.py
@@ -3,5 +3,6 @@
from . import gamification_challenge
from . import slide_slide
from . import slide_channel
+from . import slide_channel_tag
from . import res_config_settings
from . import website
diff --git a/addons/website_slides/models/slide_channel.py b/addons/website_slides/models/slide_channel.py
index 2c0d56d7b17..cad83a33b7e 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, tools, _
from odoo.addons.http_routing.models.ir_http import slug
from odoo.tools.translate import html_translate
from odoo.osv import expression
@@ -14,13 +16,39 @@ 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
- allowing to configure slide upload and access. Slides can be promoted in
- channels. """
+ """ A channel is a container of slides. """
_name = 'slide.channel'
_description = 'Slide Channel'
_inherit = ['mail.thread', 'website.seo.metadata', 'website.published.multi.mixin', 'rating.mixin']
@@ -33,10 +61,20 @@ class Channel(models.Model):
name = fields.Char('Name', translate=True, required=True)
active = fields.Boolean(default=True)
description = fields.Html('Description', translate=html_translate, sanitize_attributes=False)
+ channel_type = fields.Selection([
+ ('documentation', 'Documentation'), ('training', 'Training')],
+ string="Course type", default="documentation", required=True)
sequence = fields.Integer(default=10, help='Display order')
+ tag_ids = fields.Many2many(
+ 'slide.channel.tag', 'slide_channel_tag_rel', 'channel_id', 'tag_id',
+ string='Tags', help='Used to categorize and filter displayed channels/courses')
category_ids = fields.One2many('slide.category', 'channel_id', string="Categories")
+ image = fields.Binary("Image", attachment=True)
+ image_medium = fields.Binary("Medium image", attachment=True)
+ image_small = fields.Binary("Small image", attachment=True)
# 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'),
@@ -44,14 +82,16 @@ class Channel(models.Model):
('most_viewed', 'Most Viewed'),
('custom', 'Featured Presentation')],
string="Featuring Policy", default='most_voted', required=True)
- 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',
@@ -77,23 +117,11 @@ 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')
- @api.depends('custom_slide_id', 'promote_strategy', 'slide_ids.likes',
- 'slide_ids.total_views', "slide_ids.date_published")
- def _compute_promoted_slide_id(self):
- for record in self:
- if record.promote_strategy == 'none':
- record.promoted_slide_id = False
- elif record.promote_strategy == 'custom':
- record.promoted_slide_id = record.custom_slide_id
- elif record.promote_strategy:
- slides = self.env['slide.slide'].search(
- [('website_published', '=', True), ('channel_id', '=', record.id)],
- limit=1, order=self.env['slide.slide']._order_by_strategy[record.promote_strategy])
- record.promoted_slide_id = slides and slides[0] or False
-
@api.depends('channel_partner_ids.partner_id')
@api.model
def _compute_is_member(self):
@@ -107,22 +135,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')
@@ -167,14 +212,18 @@ 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
})]
+ if 'image' in vals:
+ tools.image_resize_images(vals)
return super(Channel, self.with_context(mail_create_nosubscribe=True)).create(vals)
@api.multi
def write(self, vals):
+ if 'image' in vals:
+ tools.image_resize_images(vals)
res = super(Channel, self).write(vals)
if 'active' in vals:
# archiving/unarchiving a channel does it on its slides, too
@@ -261,13 +310,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:
@@ -277,4 +326,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_channel_tag.py b/addons/website_slides/models/slide_channel_tag.py
new file mode 100644
index 00000000000..174ec347c7f
--- /dev/null
+++ b/addons/website_slides/models/slide_channel_tag.py
@@ -0,0 +1,31 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+from odoo import fields, models
+
+
+class SlideChannelTagGroup(models.Model):
+ _name = 'slide.channel.tag.group'
+ _description = 'Channel/Course tags'
+ _order = 'sequence asc'
+
+ name = fields.Char('Name', required=True, translate=True)
+ sequence = fields.Integer('Sequence', default=10, index=True, required=True)
+ tag_ids = fields.One2many('slide.channel.tag', 'group_id', string='Tags')
+ website_published = fields.Boolean(
+ 'Menu entry', default=True,
+ help='Makes a menu entry in main navigation of Slides, allowing to filter on its tags directly from main navigation.')
+
+
+class SlideChannelTag(models.Model):
+ _name = 'slide.channel.tag'
+ _description = 'Channel/Course tags'
+ _order = 'group_sequence asc, sequence asc'
+
+ name = fields.Char('Name', required=True, translate=True)
+ sequence = fields.Integer('Sequence', default=10, index=True, required=True)
+ group_id = fields.Many2one('slide.channel.tag.group', string='Group', index=True, required=True)
+ group_sequence = fields.Integer(
+ 'Group sequence', related='group_id.sequence',
+ index=True, readonly=True, store=True)
+ channel_ids = fields.Many2many('slide.channel.tag', 'slide_channel_tag_rel', 'tag_id', 'channel_id', string='Channels')
diff --git a/addons/website_slides/models/slide_slide.py b/addons/website_slides/models/slide_slide.py
index 38f7a49968f..908513c5da8 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)
@@ -256,9 +259,7 @@ class Slide(models.Model):
doc_data = self._parse_document_url(values['url']).get('values', dict())
for key, value in doc_data.items():
values.setdefault(key, value)
- if values.get('channel_id'):
- custom_channels = self.env['slide.channel'].search([('custom_slide_id', '=', self.id), ('id', '!=', values.get('channel_id'))])
- custom_channels.write({'custom_slide_id': False})
+
res = super(Slide, self).write(values)
if values.get('website_published'):
self.date_published = datetime.datetime.now()
@@ -360,6 +361,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/security/ir.model.access.csv b/addons/website_slides/security/ir.model.access.csv
index 046ea6582e3..6f12b3236c7 100644
--- a/addons/website_slides/security/ir.model.access.csv
+++ b/addons/website_slides/security/ir.model.access.csv
@@ -5,6 +5,10 @@ access_slide_slide_partner_all,slide.slide.partner.all,model_slide_slide_partner
access_slide_slide_partner_system,slide.slide.partner.system,model_slide_slide_partner,website.group_website_publisher,1,1,1,1
access_slide_tag_all,slide.tag.all,model_slide_tag,,1,0,0,0
access_slide_tag_publisher,slide.tag.publisher,model_slide_tag,website.group_website_publisher,1,1,1,1
+access_slide_channel_tag_all,slide.channel.tag.all,model_slide_channel_tag,,1,0,0,0
+access_slide_channel_tag_user,slide.channel.tag.user,model_slide_channel_tag,website.group_website_publisher,1,1,1,1
+access_slide_channel_tag_group_all,slide.channel.tag.group.all,model_slide_channel_tag_group,,1,0,0,0
+access_slide_channel_tag_group_user,slide.channel.tag.group.user,model_slide_channel_tag_group,website.group_website_publisher,1,1,1,1
access_slide_channel_all,slide.channel.all,model_slide_channel,,1,0,0,0
access_slide_channel_publisher,slide.channel.publisher,model_slide_channel,website.group_website_publisher,1,1,1,1
access_slide_channel_partners_all,slide.channel.users.all,model_slide_channel_partner,,0,0,0,0
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_tag_views.xml b/addons/website_slides/views/slide_channel_tag_views.xml
new file mode 100644
index 00000000000..4a7eaf48f99
--- /dev/null
+++ b/addons/website_slides/views/slide_channel_tag_views.xml
@@ -0,0 +1,96 @@
+<?xml version="1.0" encoding="utf-8"?>
+<odoo><data>
+ <!-- SLIDE.CHANNEL.TAG -->
+ <record id="slide_channel_tag_view_search" model="ir.ui.view">
+ <field name="name">slide.channel.tag.view.search</field>
+ <field name="model">slide.channel.tag</field>
+ <field name="arch" type="xml">
+ <search string="Channel Tags">
+ <field name="name"/>
+ <field name="group_id"/>
+ </search>
+ </field>
+ </record>
+
+ <record id="slide_channel_tag_view_form" model="ir.ui.view">
+ <field name="name">slide.channel.tag.view.form</field>
+ <field name="model">slide.channel.tag</field>
+ <field name="arch" type="xml">
+ <form string="Channel Tag">
+ <sheet>
+ <group>
+ <field name="name"/>
+ <field name="group_id"/>
+ </group>
+ </sheet>
+ </form>
+ </field>
+ </record>
+
+ <record id="slide_channel_tag_view_tree" model="ir.ui.view">
+ <field name="name">slide.channel.tag.view.tree</field>
+ <field name="model">slide.channel.tag</field>
+ <field name="arch" type="xml">
+ <tree string="Channel Tags">
+ <field name="sequence" widget="handle"/>
+ <field name="group_sequence" invisible="1"/>
+ <field name="name"/>
+ <field name="group_id"/>
+ </tree>
+ </field>
+ </record>
+
+ <record id="slide_channel_tag_action" model="ir.actions.act_window">
+ <field name="name">Channel Tags</field>
+ <field name="res_model">slide.channel.tag</field>
+ <field name="view_type">form</field>
+ <field name="view_mode">tree,form</field>
+ </record>
+
+ <!-- SLIDE.CHANNEL.TAG.GROUP -->
+ <record id="slide_channel_tag_group_view_search" model="ir.ui.view">
+ <field name="name">slide.channel.tag.group.view.search</field>
+ <field name="model">slide.channel.tag.group</field>
+ <field name="arch" type="xml">
+ <search string="Channel Tag Groups">
+ <field name="name"/>
+ </search>
+ </field>
+ </record>
+
+ <record id="slide_channel_tag_group_view_form" model="ir.ui.view">
+ <field name="name">slide.channel.tag.group.view.form</field>
+ <field name="model">slide.channel.tag.group</field>
+ <field name="arch" type="xml">
+ <form string="Channel Tag Group">
+ <sheet>
+ <group>
+ <field name="name"/>
+ <field name="website_published"/>
+ </group>
+ </sheet>
+ </form>
+ </field>
+ </record>
+
+ <record id="slide_channel_tag_group_view_tree" model="ir.ui.view">
+ <field name="name">slide.channel.tag.group.view.tree</field>
+ <field name="model">slide.channel.tag.group</field>
+ <field name="arch" type="xml">
+ <tree string="Channel Tag Groups">
+ <field name="sequence" widget="handle"/>
+ <field name="name"/>
+ <field name="website_published"/>
+ </tree>
+ </field>
+ </record>
+
+ <record id="slide_channel_tag_group_action" model="ir.actions.act_window">
+ <field name="name">Channel Tag Groups</field>
+ <field name="res_model">slide.channel.tag.group</field>
+ <field name="view_type">form</field>
+ <field name="view_mode">tree,form</field>
+ </record>
+
+ </data>
+</odoo>
diff --git a/addons/website_slides/views/slide_channel_views.xml b/addons/website_slides/views/slide_channel_views.xml
index ba317e74375..3200409f8e5 100644
--- a/addons/website_slides/views/slide_channel_views.xml
+++ b/addons/website_slides/views/slide_channel_views.xml
@@ -57,41 +57,56 @@
<field name="active" widget="boolean_button" options='{"terminology": "archive"}'/>
</button>
</div>
+ <field name="image" widget="image" class="oe_avatar" options="{'preview_image': 'image_medium'}"/>
<div class="oe_title">
- <div class="oe_edit_only">
- <label for="name" string="Name"/>
- </div>
- <h1>
- <field name="name" default_focus="1" placeholder="Name"/>
- </h1>
+ <label for="name" class="oe_edit_only" string="Name"/>
+ <h1><field name="name" default_focus="1" placeholder="Name"/></h1>
</div>
- <field name="category_ids" widget="many2many_tags" placeholder="Categories" context="{'default_channel_id': active_id}"
- domain="[('channel_id','=', active_id)]"/>
+ <group>
+ <group>
+ <field name="visibility" string="Enroll" widget="radio"/>
+ <field name="category_ids" widget="many2many_tags" placeholder="Categories"
+ context="{'default_channel_id': active_id}"
+ domain="[('channel_id','=', active_id)]"/>
+ <field name="tag_ids" widget="many2many_tags" placeholder="Tags"/>
+ </group>
+ </group>
<notebook colspan="4">
- <page string="General">
+ <page string="Description">
+ <field name="description" colspan="4" nolabel="1"/>
+ </page>
+ <page string="Options">
<group>
<group>
+ <field name="channel_type"/>
<field name="website_id" options="{'no_create': True}" groups="website.group_multi_website"/>
<field name="promote_strategy" widget="radio"/>
- <field name="custom_slide_id" class="oe_edit_only" attrs="{'invisible': [('promote_strategy', '!=', 'custom')]}" domain="[('channel_id','=',id), ('website_published','=',True)]"/>
- <field name="promoted_slide_id" attrs="{'invisible': [('promote_strategy', '!=', 'custom')]}"/>
</group>
<group>
+ <field name="upload_group_ids" widget="many2many_tags"/>
<field name="publish_template_id" domain="[('model','=','slide.slide')]"/>
<field name="share_template_id" domain="[('model','=','slide.slide')]"/>
</group>
</group>
- <label for="description"/>
- <field name="description" colspan="4" nolabel="1"/>
+ <group colspan="4">
+ <label for="enroll_msg"/>
+ <field name="enroll_msg" colspan="4" nolabel="1"/>
+ </group>
</page>
- <page string="Security">
+ <page string="Statistics" groups="base.group_no_one">
<group>
- <field name="upload_group_ids" widget="many2many_tags"/>
- <field name="visibility" string="Channel visibility" widget="radio"/>
- </group>
- <group colspan="4">
- <label for="enroll_msg"/>
- <field name="enroll_msg" colspan="4" nolabel="1"/>
+ <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>
diff --git a/addons/website_slides/views/website_slides_menu_views.xml b/addons/website_slides/views/website_slides_menu_views.xml
index f2bbf21c25c..bf558fd0830 100644
--- a/addons/website_slides/views/website_slides_menu_views.xml
+++ b/addons/website_slides/views/website_slides_menu_views.xml
@@ -21,5 +21,16 @@
<menuitem action="website_slides.action_slide_tag" id="menu_slide_tag" parent="menu_website_slides_root_global" sequence="10" groups="base.group_no_one"/>
+ <!-- Channel tags -->
+ <menuitem id="slide_channel_tag_menu"
+ action="website_slides.slide_channel_tag_action"
+ parent="menu_website_slides_root_global"
+ sequence="20"
+ groups="base.group_no_one"/>
+ <menuitem id="slide_channel_tag_group_menu"
+ action="website_slides.slide_channel_tag_group_action"
+ parent="menu_website_slides_root_global"
+ sequence="21"
+ groups="base.group_no_one"/>
</data>
</odoo> \ No newline at end of file
diff --git a/addons/website_slides/views/website_slides_templates.xml b/addons/website_slides/views/website_slides_templates.xml
index ac12bd467a2..97ebb4fe59d 100644
--- a/addons/website_slides/views/website_slides_templates.xml
+++ b/addons/website_slides/views/website_slides_templates.xml
@@ -39,8 +39,12 @@
<div class="card card-body">
<div>
<a t-attf-href="/slides/#{slug(channel)}">
- <img t-if="channel.promoted_slide_id" t-attf-src="/web/image/slide.slide/#{channel.promoted_slide_id.id}/image_medium" class="img-fluid oe_slides_channel_thumbnail" alt="Promoted slide"/>
- <img t-if="not channel.promoted_slide_id" src="/website_slides/static/src/img/channel-default.jpg" class="img-fluid oe_slides_opacity" alt="Channel without promoted slide"/>
+ <img t-if="channel.image" t-attf-src="/web/image/slide.channel/#{channel.id}/image_medium"
+ class="img-fluid oe_slides_channel_thumbnail"
+ t-att-alt="channel.name"/>
+ <img t-if="not channel.image" src="/website_slides/static/src/img/channel-default.jpg"
+ class="img-fluid oe_slides_opacity"
+ t-att-alt="channel.name"/>
</a>
</div>
<div>
@@ -219,7 +223,7 @@
<t t-set="header_object" t-value="category"/>
</t>
</t>
- <section t-if="not channel.promoted_slide_id and not tag and not slide_type and not search and not is_public_user and not category" class="wrap mt16 mb0" groups="website.group_website_designer">
+ <section t-if="not tag and not slide_type and not search and not is_public_user and not category" class="wrap mt16 mb0" groups="website.group_website_designer">
<div class="container">
<div class="alert alert-danger alert-dismissable mb0" role="alert">
<p>
@@ -231,26 +235,26 @@
</div>
</div>
</section>
- <section class="oe_slides_promote_box" t-if="channel.promoted_slide_id and not tag and not slide_type and not search and not category">
+ <section class="oe_slides_promote_box" t-if="channel.slide_ids and not tag and not slide_type and not search and not category">
<div class="container">
<div class="row">
<div class="col-lg-5 mt16 mb16">
- <a t-attf-href="/slides/slide/#{slug(channel.promoted_slide_id)}">
- <img t-attf-src="/web/image/slide.slide/#{channel.promoted_slide_id.id}/image_medium" class="img-fluid shadow oe_slides_promote_image" alt="Promoted slide"/>
+ <a t-attf-href="/slides/slide/#{slug(channel.slide_ids[0])}">
+ <img t-attf-src="/web/image/slide.slide/#{channel.slide_ids[0].id}/image_medium" class="img-fluid shadow oe_slides_promote_image" alt="Promoted slide"/>
</a>
</div>
<div class="col-lg-7 mt16 mb16">
<h3 class="row">
- <a t-attf-href="/slides/slide/#{slug(channel.promoted_slide_id)}">
- <t t-esc="channel.promoted_slide_id.name"/>
+ <a t-attf-href="/slides/slide/#{slug(channel.slide_ids[0])}">
+ <t t-esc="channel.slide_ids[0].name"/>
</a>
</h3>
- <div class="row mt8 mb8" t-if="channel.promoted_slide_id.tag_ids">
- <t t-foreach="channel.promoted_slide_id.tag_ids" t-as="slide_tag">
+ <div class="row mt8 mb8" t-if="channel.slide_ids[0].tag_ids">
+ <t t-foreach="channel.slide_ids[0].tag_ids" t-as="slide_tag">
<a t-attf-href="/slides/#{slug(channel)}/tag/#{slug(slide_tag)}" t-attf-class="badge badge-secondary #{tag and tag.id == slide_tag.id and 'badge-primary' ''}" t-field="slide_tag.name"/>
</t>
</div>
- <div class="row oe_no_empty" t-esc="channel.promoted_slide_id.description"/>
+ <div class="row oe_no_empty" t-esc="channel.slide_ids[0].description"/>
<p class="row mt8">
<t t-call="website_rating.rating_stars_static_popup_composer">
<t t-set="rating_avg" t-value="rating_avg"/>
@@ -267,7 +271,7 @@
<p class="row mt8">
<b>Share:</b>
<t t-call="website_slides.slides_share">
- <t t-set="slide" t-value="channel.promoted_slide_id"/>
+ <t t-set="slide" t-value="channel.slide_ids[0]"/>
</t>
</p>
</div>
@@ -291,7 +295,12 @@
<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-12" t-if="search and not slides">
+ <div class="alert alert-danger" role="alert">
+ Sorry, but nothing matches your search criteria <b><t t-esc="search"/></b>. Please try again with different keywords.
+ </div>
+ </div>
+ <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,13 +338,18 @@
<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>
</div>
- <t t-call="website_slides.slides_grid_view">
- <t t-set="slides" t-value="category['slides']"/>
+ <t t-if="not search">
+ <t t-call="website_slides.slides_grid_view">
+ <t t-set="slides" t-value="category['slides']"/>
+ </t>
+ </t>
+ <t t-else="">
+ <t t-call="website_slides.slides_grid_view"/>
</t>
</t>
</div>
@@ -346,34 +360,6 @@
</t>
</template>
-<!-- Channels: search result -->
-<template id='slides_search' name="Searching Slides">
- <t t-call="website.layout">
- <div class="wrap">
- <t t-set="main_object" t-value="channel"/>
- <t t-call="website_slides.slides_channel_header"/>
- <section>
- <div class="container mt16" t-if="not slides">
- <div class="alert alert-danger" role="alert">
- Sorry, but nothing matches your search criteria <b><t t-esc="search"/></b>. Please try again with different keywords.
- </div>
- </div>
- <div class="container mt16" t-if="slides">
- <div class="alert alert-success mb0" role="status">
- <b t-esc="len(slides)"/> results found for the given criteria <b><t t-esc="search"/></b>
- </div>
- <div class="row mt0">
- <t t-call="website_slides.slides_grid_view"/>
- </div>
- <div class="row text-center">
- <t t-call="website.pager"/>
- </div>
- </div>
- </section>
- </div>
- </t>
-</template>
-
<!-- Embedded template: display a list of slides in a grid view -->
<template id='slides_grid_view' name="Slides (Grid View)">
<div class="row">
@@ -383,17 +369,6 @@
<img t-attf-src="/web/image/slide.slide/#{slide.id}/image_thumb" class="img-fluid img-thumbnail oe_slides_grid_thumbnail" alt="slide.name"/>
</a>
<span t-if="not slide.website_published" class="badge badge-danger" style="position: absolute;right: 20px;top: 7px;">Unpublished</span>
- <a t-if="not is_public_user and slide.website_published and slide.channel_id.promote_strategy == 'custom' and slide.channel_id.promoted_slide_id.id != slide.id"
- groups="website.group_website_publisher"
- t-attf-href="/slides/slide/#{slide.id}/promote"
- style="position: absolute;right: 20px;top: 7px;"
- t-attf-title="Promote this #{slide.slide_type}"
- t-attf-aria-label="Promote this #{slide.slide_type}">
- <span class="fa-stack fa-lg">
- <i class="fa fa-square fa-stack-2x oe_slides_opacity"></i>
- <i class="fa fa-bullhorn fa-stack-1x fa-inverse"></i>
- </span>
- </a>
<div style="padding: 5px;">
<h4 class="mt4 mb8 oe_slides_ellipsis">