diff options
| -rw-r--r-- | addons/website_slides/__manifest__.py | 1 | ||||
| -rw-r--r-- | addons/website_slides/controllers/main.py | 11 | ||||
| -rw-r--r-- | addons/website_slides/data/slide_data.xml | 6 | ||||
| -rw-r--r-- | addons/website_slides/data/website_slides_demo.xml | 1 | ||||
| -rw-r--r-- | addons/website_slides/models/__init__.py | 1 | ||||
| -rw-r--r-- | addons/website_slides/models/slide_channel.py | 135 | ||||
| -rw-r--r-- | addons/website_slides/models/slide_channel_tag.py | 31 | ||||
| -rw-r--r-- | addons/website_slides/models/slide_slide.py | 59 | ||||
| -rw-r--r-- | addons/website_slides/security/ir.model.access.csv | 4 | ||||
| -rw-r--r-- | addons/website_slides/tests/__init__.py | 1 | ||||
| -rw-r--r-- | addons/website_slides/tests/common.py | 1 | ||||
| -rw-r--r-- | addons/website_slides/tests/test_statistics.py | 33 | ||||
| -rw-r--r-- | addons/website_slides/views/slide_channel_tag_views.xml | 96 | ||||
| -rw-r--r-- | addons/website_slides/views/slide_channel_views.xml | 55 | ||||
| -rw-r--r-- | addons/website_slides/views/website_slides_menu_views.xml | 11 | ||||
| -rw-r--r-- | addons/website_slides/views/website_slides_templates.xml | 85 |
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"> |
