diff --git a/addons/website_slides/__manifest__.py b/addons/website_slides/__manifest__.py index 28948a80ea5..0e0bbecc7f3 100644 --- a/addons/website_slides/__manifest__.py +++ b/addons/website_slides/__manifest__.py @@ -19,11 +19,17 @@ Share and Publish Videos, Presentations and Documents' """, 'depends': ['website', 'website_mail'], 'data': [ + 'views/assets.xml', 'views/res_config_settings_views.xml', - 'views/website_slides.xml', - 'views/website_slides_embed.xml', - 'views/website_slides_backend.xml', - 'data/website_slides_data.xml', + 'views/website_slides_templates.xml', + 'views/website_slides_embed_templates.xml', + 'views/slide_slide_views.xml', + 'views/slide_channel_views.xml', + 'views/website_slides_menu_views.xml', + 'data/website_slides_ir_data.xml', + 'data/mail_data.xml', + 'data/slide_data.xml', + 'data/website_data.xml', 'security/ir.model.access.csv', 'security/website_slides_security.xml' ], diff --git a/addons/website_slides/data/mail_data.xml b/addons/website_slides/data/mail_data.xml new file mode 100644 index 00000000000..db042d4f313 --- /dev/null +++ b/addons/website_slides/data/mail_data.xml @@ -0,0 +1,64 @@ + + + + + Slide Published + + New ${object.slide_type} published on ${object.channel_id.name} + +
+

+ Hello

+ A new ${object.slide_type} ${object.name} has been published on ${object.channel_id.name} at ${format_tz(object.write_date, tz=user.tz)} +

+ + ${object.name} + +
+ +

+
+
+ + +
+ + + Slide Shared + + ${user.name} shared a ${object.slide_type} with you! + ${(user.email or '')|safe} + ${ctx['email']} + +
+

+ Hello

+ ${user.name} shared the ${object.slide_type} ${object.name} with you! +

+ + ${object.name} + +
+ +

+
+
+ + +
+ + + + Presentation Published + slide.channel + + Presentation Published + +
+
diff --git a/addons/website_slides/data/slide_data.xml b/addons/website_slides/data/slide_data.xml new file mode 100644 index 00000000000..9d8315da810 --- /dev/null +++ b/addons/website_slides/data/slide_data.xml @@ -0,0 +1,26 @@ + + + + + Public Channel + most_viewed + + public + Default channel for slides, all public users can access content of this channel. + + + Partner Channel + + + + partial + Used to publish slides in partner network privately. + + + Employee Channel + + private + Used to publish internal slides of company. + + + diff --git a/addons/website_slides/data/website_data.xml b/addons/website_slides/data/website_data.xml new file mode 100644 index 00000000000..0d2f0582deb --- /dev/null +++ b/addons/website_slides/data/website_data.xml @@ -0,0 +1,15 @@ + + + + + AIzaSyDOWlmDW-7DbLmOR9ZsT5AOEXf4n6TFwQA + + + + Presentations + /slides + + 50 + + + diff --git a/addons/website_slides/data/website_slides_data.xml b/addons/website_slides/data/website_slides_data.xml deleted file mode 100644 index 7ea95265642..00000000000 --- a/addons/website_slides/data/website_slides_data.xml +++ /dev/null @@ -1,109 +0,0 @@ - - - - - - AIzaSyDOWlmDW-7DbLmOR9ZsT5AOEXf4n6TFwQA - - - - Presentations - /slides - - 50 - - - - Slide Published - - New ${object.slide_type} published on ${object.channel_id.name} - -
-

- Hello

- A new ${object.slide_type} ${object.name} has been published on ${object.channel_id.name} at ${format_tz(object.write_date, tz=user.tz)} -

- - ${object.name} - -
- -

-
-
- - -
- - - Slide Shared - - ${user.name} shared a ${object.slide_type} with you! - ${(user.email or '')|safe} - ${ctx['email']} - -
-

- Hello

- ${user.name} shared the ${object.slide_type} ${object.name} with you! -

- - ${object.name} - -
- -

-
-
- - -
- - - Public Channel - most_viewed - - public - Default channel for slides, all public users can access content of this channel. - - - Partner Channel - - - - partial - Used to publish slides in partner network privately. - - - Employee Channel - - private - Used to publish internal slides of company. - - - - - Presentation Published - slide.channel - - Presentation Published - - - - - Channels - self - - - - - open - -
-
diff --git a/addons/website_slides/data/website_slides_ir_data.xml b/addons/website_slides/data/website_slides_ir_data.xml new file mode 100644 index 00000000000..3b298c1e27d --- /dev/null +++ b/addons/website_slides/data/website_slides_ir_data.xml @@ -0,0 +1,15 @@ + + + + + + Channels + self + + + + + open + + + diff --git a/addons/website_slides/models/__init__.py b/addons/website_slides/models/__init__.py index 2ad149ccded..e64510d2710 100644 --- a/addons/website_slides/models/__init__.py +++ b/addons/website_slides/models/__init__.py @@ -1,5 +1,6 @@ # -*- coding: utf-8 -*- -from . import slides +from . import slide_slide +from . import slide_channel 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 new file mode 100644 index 00000000000..fb56ca97b25 --- /dev/null +++ b/addons/website_slides/models/slide_channel.py @@ -0,0 +1,207 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from odoo import api, fields, models, SUPERUSER_ID, _ +from odoo.addons.http_routing.models.ir_http import slug +from odoo.tools.translate import html_translate + + +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. """ + _name = 'slide.channel' + _description = 'Slide Channel' + _inherit = ['mail.thread', 'website.seo.metadata', 'website.published.multi.mixin'] + _order = 'sequence, id' + _order_by_strategy = { + 'most_viewed': 'total_views desc', + 'most_voted': 'likes desc', + 'latest': 'date_published desc', + } + + name = fields.Char('Name', translate=True, required=True) + active = fields.Boolean(default=True) + description = fields.Html('Description', translate=html_translate, sanitize_attributes=False) + sequence = fields.Integer(default=10, help='Display order') + category_ids = fields.One2many('slide.category', 'channel_id', string="Categories") + slide_ids = fields.One2many('slide.slide', 'channel_id', string="Slides") + promote_strategy = fields.Selection([ + ('none', 'No Featured Presentation'), + ('latest', 'Latest Published'), + ('most_voted', 'Most Voted'), + ('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) + + @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._order_by_strategy[record.promote_strategy]) + record.promoted_slide_id = slides and slides[0] or False + + 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) + + @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'], ['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 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 + + publish_template_id = fields.Many2one( + 'mail.template', string='Published Template', + help="Email template to send slide publication through email", + default=lambda self: self.env['ir.model.data'].xmlid_to_res_id('website_slides.slide_template_published')) + share_template_id = fields.Many2one( + 'mail.template', string='Shared Template', + help="Email template used when sharing a slide", + default=lambda self: self.env['ir.model.data'].xmlid_to_res_id('website_slides.slide_template_shared')) + visibility = fields.Selection([ + ('public', 'Public'), + ('private', 'Private'), + ('partial', 'Show channel but restrict presentations')], + default='public', required=True) + group_ids = fields.Many2many( + 'res.groups', 'rel_channel_groups', 'channel_id', 'group_id', + string='Channel Groups', help="Groups allowed to see presentations in this channel") + access_error_msg = fields.Html( + 'Error Message', help="Message to display when not accessible due to access rights", + default=lambda s: _("

This channel is private and its content is restricted to some users.

"), + translate=html_translate, sanitize_attributes=False) + upload_group_ids = fields.Many2many( + '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 + can_see = fields.Boolean('Can See', compute='_compute_access', search='_search_can_see') + can_see_full = fields.Boolean('Full Access', compute='_compute_access') + can_upload = fields.Boolean('Can Upload', compute='_compute_access') + + def _search_can_see(self, operator, value): + if operator not in ('=', '!=', '<>'): + raise ValueError('Invalid operator: %s' % (operator,)) + + if not value: + operator = operator == "=" and '!=' or '=' + + if self._uid == SUPERUSER_ID: + return [(1, '=', 1)] + + # Better perfs to split request and use inner join that left join + req = """ + SELECT id FROM slide_channel WHERE visibility='public' + UNION + SELECT c.id + FROM slide_channel c + INNER JOIN rel_channel_groups rg on c.id = rg.channel_id + INNER JOIN res_groups g on g.id = rg.group_id + INNER JOIN res_groups_users_rel u on g.id = u.gid and uid = %s + """ + op = operator == "=" and "inselect" or "not inselect" + # don't use param named because orm will add other param (test_active, ...) + return [('id', op, (req, (self._uid, )))] + + @api.one + @api.depends('visibility', 'group_ids', 'upload_group_ids') + def _compute_access(self): + self.can_see = self.visibility in ['public', 'private'] or bool(self.group_ids & self.env.user.groups_id) + self.can_see_full = self.visibility == 'public' or bool(self.group_ids & self.env.user.groups_id) + self.can_upload = self.can_see and (not self.upload_group_ids or bool(self.upload_group_ids & self.env.user.groups_id)) + + @api.multi + @api.depends('name') + def _compute_website_url(self): + super(Channel, self)._compute_website_url() + base_url = self.env['ir.config_parameter'].sudo().get_param('web.base.url') + for channel in self: + if channel.id: # avoid to perform a slug on a not yet saved record in case of an onchange. + channel.website_url = '%s/slides/%s' % (base_url, slug(channel)) + + @api.onchange('visibility') + def change_visibility(self): + if self.visibility == 'public': + self.group_ids = False + + @api.multi + def write(self, vals): + res = super(Channel, self).write(vals) + if 'active' in vals: + # archiving/unarchiving a channel does it on its slides, too + self.with_context(active_test=False).mapped('slide_ids').write({'active': vals['active']}) + return res + + @api.multi + @api.returns('mail.message', lambda value: value.id) + def message_post(self, parent_id=False, subtype=None, **kwargs): + """ Temporary workaround to avoid spam. If someone replies on a channel + through the 'Presentation Published' email, it should be considered as a + note as we don't want all channel followers to be notified of this answer. """ + self.ensure_one() + if parent_id: + parent_message = self.env['mail.message'].sudo().browse(parent_id) + if parent_message.subtype_id and parent_message.subtype_id == self.env.ref('website_slides.mt_channel_slide_published'): + if kwargs.get('subtype_id'): + kwargs['subtype_id'] = False + subtype = 'mail.mt_note' + return super(Channel, self).message_post(parent_id=parent_id, subtype=subtype, **kwargs) + + def list_all(self): + return { + 'channels': [{'id': channel.id, 'name': channel.name, 'website_url': channel.website_url} for channel in self.search([])] + } + + +class Category(models.Model): + """ Channel contain various categories to manage its slides """ + _name = 'slide.category' + _description = "Slides Category" + _order = "sequence, id" + + name = fields.Char('Name', translate=True, required=True) + channel_id = fields.Many2one('slide.channel', string="Channel", required=True, ondelete='cascade') + sequence = fields.Integer(default=10, help='Display order') + slide_ids = fields.One2many('slide.slide', 'category_id', string="Slides") + 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) + + @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), ('category_id', 'in', self.ids)], + ['category_id', 'slide_type'], ['category_id', 'slide_type'], + lazy=False) + for res_group in res: + result[res_group['category_id'][0]][res_group['slide_type']] = result[res_group['category_id'][0]].get(res_group['slide_type'], 0) + res_group['__count'] + 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 \ No newline at end of file diff --git a/addons/website_slides/models/slides.py b/addons/website_slides/models/slide_slide.py similarity index 65% rename from addons/website_slides/models/slides.py rename to addons/website_slides/models/slide_slide.py index ab8494d1212..6f1df65d99b 100644 --- a/addons/website_slides/models/slides.py +++ b/addons/website_slides/models/slide_slide.py @@ -14,213 +14,11 @@ 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.tools.translate import html_translate from odoo.exceptions import Warning from odoo.http import request from odoo.addons.http_routing.models.ir_http import url_for -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. """ - _name = 'slide.channel' - _description = 'Slide Channel' - _inherit = ['mail.thread', 'website.seo.metadata', 'website.published.multi.mixin'] - _order = 'sequence, id' - _order_by_strategy = { - 'most_viewed': 'total_views desc', - 'most_voted': 'likes desc', - 'latest': 'date_published desc', - } - - name = fields.Char('Name', translate=True, required=True) - active = fields.Boolean(default=True) - description = fields.Html('Description', translate=html_translate, sanitize_attributes=False) - sequence = fields.Integer(default=10, help='Display order') - category_ids = fields.One2many('slide.category', 'channel_id', string="Categories") - slide_ids = fields.One2many('slide.slide', 'channel_id', string="Slides") - promote_strategy = fields.Selection([ - ('none', 'No Featured Presentation'), - ('latest', 'Latest Published'), - ('most_voted', 'Most Voted'), - ('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) - - @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._order_by_strategy[record.promote_strategy]) - record.promoted_slide_id = slides and slides[0] or False - - 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) - - @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'], ['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 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 - - publish_template_id = fields.Many2one( - 'mail.template', string='Published Template', - help="Email template to send slide publication through email", - default=lambda self: self.env['ir.model.data'].xmlid_to_res_id('website_slides.slide_template_published')) - share_template_id = fields.Many2one( - 'mail.template', string='Shared Template', - help="Email template used when sharing a slide", - default=lambda self: self.env['ir.model.data'].xmlid_to_res_id('website_slides.slide_template_shared')) - visibility = fields.Selection([ - ('public', 'Public'), - ('private', 'Private'), - ('partial', 'Show channel but restrict presentations')], - default='public', required=True) - group_ids = fields.Many2many( - 'res.groups', 'rel_channel_groups', 'channel_id', 'group_id', - string='Channel Groups', help="Groups allowed to see presentations in this channel") - access_error_msg = fields.Html( - 'Error Message', help="Message to display when not accessible due to access rights", - default=lambda s: _("

This channel is private and its content is restricted to some users.

"), - translate=html_translate, sanitize_attributes=False) - upload_group_ids = fields.Many2many( - '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 - can_see = fields.Boolean('Can See', compute='_compute_access', search='_search_can_see') - can_see_full = fields.Boolean('Full Access', compute='_compute_access') - can_upload = fields.Boolean('Can Upload', compute='_compute_access') - - def _search_can_see(self, operator, value): - if operator not in ('=', '!=', '<>'): - raise ValueError('Invalid operator: %s' % (operator,)) - - if not value: - operator = operator == "=" and '!=' or '=' - - if self._uid == SUPERUSER_ID: - return [(1, '=', 1)] - - # Better perfs to split request and use inner join that left join - req = """ - SELECT id FROM slide_channel WHERE visibility='public' - UNION - SELECT c.id - FROM slide_channel c - INNER JOIN rel_channel_groups rg on c.id = rg.channel_id - INNER JOIN res_groups g on g.id = rg.group_id - INNER JOIN res_groups_users_rel u on g.id = u.gid and uid = %s - """ - op = operator == "=" and "inselect" or "not inselect" - # don't use param named because orm will add other param (test_active, ...) - return [('id', op, (req, (self._uid, )))] - - @api.one - @api.depends('visibility', 'group_ids', 'upload_group_ids') - def _compute_access(self): - self.can_see = self.visibility in ['public', 'private'] or bool(self.group_ids & self.env.user.groups_id) - self.can_see_full = self.visibility == 'public' or bool(self.group_ids & self.env.user.groups_id) - self.can_upload = self.can_see and (not self.upload_group_ids or bool(self.upload_group_ids & self.env.user.groups_id)) - - @api.multi - @api.depends('name') - def _compute_website_url(self): - super(Channel, self)._compute_website_url() - base_url = self.env['ir.config_parameter'].sudo().get_param('web.base.url') - for channel in self: - if channel.id: # avoid to perform a slug on a not yet saved record in case of an onchange. - channel.website_url = '%s/slides/%s' % (base_url, slug(channel)) - - @api.onchange('visibility') - def change_visibility(self): - if self.visibility == 'public': - self.group_ids = False - - @api.multi - def write(self, vals): - res = super(Channel, self).write(vals) - if 'active' in vals: - # archiving/unarchiving a channel does it on its slides, too - self.with_context(active_test=False).mapped('slide_ids').write({'active': vals['active']}) - return res - - @api.multi - @api.returns('mail.message', lambda value: value.id) - def message_post(self, parent_id=False, subtype=None, **kwargs): - """ Temporary workaround to avoid spam. If someone replies on a channel - through the 'Presentation Published' email, it should be considered as a - note as we don't want all channel followers to be notified of this answer. """ - self.ensure_one() - if parent_id: - parent_message = self.env['mail.message'].sudo().browse(parent_id) - if parent_message.subtype_id and parent_message.subtype_id == self.env.ref('website_slides.mt_channel_slide_published'): - if kwargs.get('subtype_id'): - kwargs['subtype_id'] = False - subtype = 'mail.mt_note' - return super(Channel, self).message_post(parent_id=parent_id, subtype=subtype, **kwargs) - - def list_all(self): - return { - 'channels': [{'id': channel.id, 'name': channel.name, 'website_url': channel.website_url} for channel in self.search([])] - } - - -class Category(models.Model): - """ Channel contain various categories to manage its slides """ - _name = 'slide.category' - _description = "Slides Category" - _order = "sequence, id" - - name = fields.Char('Name', translate=True, required=True) - channel_id = fields.Many2one('slide.channel', string="Channel", required=True, ondelete='cascade') - sequence = fields.Integer(default=10, help='Display order') - slide_ids = fields.One2many('slide.slide', 'category_id', string="Slides") - 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) - - @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), ('category_id', 'in', self.ids)], - ['category_id', 'slide_type'], ['category_id', 'slide_type'], - lazy=False) - for res_group in res: - result[res_group['category_id'][0]][res_group['slide_type']] = result[res_group['category_id'][0]].get(res_group['slide_type'], 0) + res_group['__count'] - 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 - - class EmbeddedSlide(models.Model): """ Embedding in third party websites. Track view count, generate statistics. """ _name = 'slide.embed' diff --git a/addons/website_slides/static/src/js/slides.js b/addons/website_slides/static/src/js/slides.js index e869f607d6e..0377394c452 100644 --- a/addons/website_slides/static/src/js/slides.js +++ b/addons/website_slides/static/src/js/slides.js @@ -2,225 +2,34 @@ odoo.define('website_slides.slides', function (require) { "use strict"; -var ajax = require('web.ajax'); -var core = require('web.core'); -var time = require('web.time'); -var Widget = require('web.Widget'); -var local_storage = require('web.local_storage'); -require('root.widget'); + var time = require('web.time'); + require('root.widget'); -var _t = core._t; -var page_widgets = {}; - -(function () { - var widget_parent = $('body'); - - - $("timeago.timeago").each(function (index, el) { - var datetime = $(el).attr('datetime'), - datetime_obj = time.str_to_datetime(datetime), - // if presentation 7 days, 24 hours, 60 min, 60 second, 1000 millis old(one week) - // then return fix formate string else timeago - display_str = ""; - if (datetime_obj && new Date().getTime() - datetime_obj.getTime() > 7 * 24 * 60 * 60 * 1000) { - display_str = moment(datetime_obj).format('ll'); - } else { - display_str = moment(datetime_obj).fromNow(); - } - $(el).text(display_str); - }); - - // To prevent showing channel settings alert box once user closed it. - $('.o_slides_hide_channel_settings').on('click', function(ev) { - var channel_id = $(this).data("channelId"); - ev.preventDefault(); - document.cookie = "slides_channel_" + channel_id + " = closed"; - return true; - }); - - /* - * Like/Dislike Buttons Widget - */ - var LikeButton = Widget.extend({ - setElement: function($el){ - this._super.apply(this, arguments); - this.$el.on('click', this, _.bind(this.apply_action, this)); - }, - apply_action: function(ev){ - var button = $(ev.currentTarget); - var slide_id = button.data('slide-id'); - var user_id = button.data('user-id'); - var is_public = button.data('public-user'); - var href = button.data('href'); - if(is_public){ - this.popover_alert(button, _.str.sprintf(_t('Please login to vote this slide'), (document.URL))); - }else{ - var target = button.find('.fa'); - if (local_storage.getItem('slide_vote_' + slide_id) !== user_id.toString()) { - ajax.jsonRpc(href, 'call', {slide_id: slide_id}).then(function (data) { - target.text(data); - local_storage.setItem('slide_vote_' + slide_id, user_id); - }); - } else { - this.popover_alert(button, _t('You have already voted for this slide')); - } - } - }, - popover_alert: function($el, message){ - $el.popover({ - trigger: 'focus', - placement: 'bottom', - container: 'body', - html: true, - content: function(){ - return message; - } - }).popover('show'); - }, - }); - - page_widgets['likeButton'] = new LikeButton(widget_parent).setElement($('.oe_slide_js_like')); - page_widgets['dislikeButton'] = new LikeButton(widget_parent).setElement($('.oe_slide_js_unlike')); - - /* - * Embedded Code Widget - */ - var SlideSocialEmbed = Widget.extend({ - events: { - 'change input' : 'change_page', - }, - init: function(parent, max_page){ - this._super(parent); - this.max_page = max_page || false; - }, - change_page: function(ev){ - ev.preventDefault(); - var input = this.$('input'); - var page = parseInt(input.val()); - if (this.max_page && !(page > 0 && page <= this.max_page)) { - page = 1; - } - this.update_embedded_code(page); - }, - update_embedded_code: function(page){ - var embed_input = this.$('.slide_embed_code'); - var new_code = embed_input.val().replace(/(page=).*?([^\d]+)/, '$1' + page + '$2'); - embed_input.val(new_code); - }, - }); - - $('iframe.o_wslides_iframe_viewer').ready(function() { - // TODO : make it work. For now, once the iframe is loaded, the value of #page_count is - // still now set (the pdf is still loading) - var $iframe = $(this); - var max_page = $iframe.contents().find('#page_count').val(); - new SlideSocialEmbed($iframe, max_page).setElement($('.oe_slide_js_embed_code_widget')); - }); - - - /* - * Send by email Widget - */ - var ShareMail = Widget.extend({ - events: { - 'click button' : 'send_mail', - }, - send_mail: function(){ - var self = this; - var input = this.$('input'); - var slide_id = this.$('button').data('slide-id'); - if(input.val() && input[0].checkValidity()){ - this.$el.removeClass('o_has_error').find('.form-control, .custom-select').removeClass('is-invalid'); - ajax.jsonRpc('/slides/slide/send_share_email', 'call', { - slide_id: slide_id, - email: input.val(), - }).then(function () { - self.$el.html($('')); - }); - }else{ - this.$el.addClass('o_has_error').find('.form-control, .custom-select').addClass('is-invalid'); - input.focus(); - } - }, - }); - - page_widgets['share_mail'] = new ShareMail(widget_parent).setElement($('.oe_slide_js_share_email')); - - /* - * Social Sharing Statistics Widget - */ - if ($('div#statistic').length) { - var slide_url = $("div#statistic").attr('slide-url'); - var social_urls = { - 'linkedin': 'https://www.linkedin.com/countserv/count/share?url=', - 'twitter': 'https://cdn.api.twitter.com/1/urls/count.json?url=', - 'facebook': 'https://graph.facebook.com/?id=', - 'gplus': 'https://clients6.google.com/rpc' - } - - var update_statistics = function(social_site, slide_url) { - if (social_site == 'gplus') { - $.ajax({ - url: social_urls['gplus'], - type: "POST", - dataType: 'json', - contentType: 'application/json', - data: JSON.stringify([{ - "method": "pos.plusones.get", - "id": "p", - "params": { - "nolog": true, - "id": slide_url, - "source": "widget", - "userId": "@viewer", - "groupId": "@self" - }, - // TDE NOTE: should there be a key here ? - "jsonrpc": "2.0", - "apiVersion": "v1" - }]), - success: function(data) { - $('#google-badge').text(data[0].result.metadata.globalCounts.count || 0); - $('#total-share').text(parseInt($('#total-share').text()) + parseInt($('#google-badge').text())); - }, - }); + (function () { + $("timeago.timeago").each(function (index, el) { + var datetime = $(el).attr('datetime'), + datetime_obj = time.str_to_datetime(datetime), + // if presentation 7 days, 24 hours, 60 min, 60 second, 1000 millis old(one week) + // then return fix formate string else timeago + display_str = ""; + if (datetime_obj && new Date().getTime() - datetime_obj.getTime() > 7 * 24 * 60 * 60 * 1000) { + display_str = moment(datetime_obj).format('ll'); } else { - $.ajax({ - url: social_urls[social_site] + slide_url, - dataType: 'jsonp', - success: function(data) { - var shareCount = (social_site === 'facebook' ? data.shares : data.count) || 0; - $('#' + social_site + '-badge').text(shareCount); - $('#total-share').text(parseInt($('#total-share').text()) + parseInt($('#' + social_site+ '-badge').text())); - }, - }); + display_str = moment(datetime_obj).fromNow(); } - }; - - $.each(social_urls, function(key, value) { - update_statistics(key, slide_url); + $(el).text(display_str); }); - $("a.o_slides_social_share").on('click', function(ev) { + // To prevent showing channel settings alert box once user closed it. + $('.o_slides_hide_channel_settings').on('click', function(ev) { + var channel_id = $(this).data("channelId"); ev.preventDefault(); - var key = $(ev.currentTarget).attr('social-key'); - var popUpURL = $(ev.currentTarget).attr('href'); - var popUp = window.open( - popUpURL, - 'Share Dialog', - 'width=626,height=436'); - $(window).on('focus', function() { - if (popUp.closed) { - update_statistics(key, slide_url); - $(window).off('focus'); - } - }); + document.cookie = "slides_channel_" + channel_id + " = closed"; + return true; }); + })(); + + return { + page_widgets: {}, } -})(); - -return { - page_widgets: page_widgets, -}; - }); diff --git a/addons/website_slides/static/src/js/slides_embed.js b/addons/website_slides/static/src/js/slides_embed.js index 617b0b900e8..2ba46a1990d 100644 --- a/addons/website_slides/static/src/js/slides_embed.js +++ b/addons/website_slides/static/src/js/slides_embed.js @@ -1,5 +1,50 @@ /*global $, _, PDFSlidesViewer*/ +odoo.define('website_slides.slides_embed', function (require) { + "use strict"; + + var time = require('web.time'); + var Widget = require('web.Widget'); + require('website_slides.slides'); + + (function () { + /* + * Embedded Code Widget + */ + var SlideSocialEmbed = Widget.extend({ + events: { + 'change input' : 'change_page', + }, + init: function(parent, max_page){ + this._super(parent); + this.max_page = max_page || false; + }, + change_page: function(ev){ + ev.preventDefault(); + var input = this.$('input'); + var page = parseInt(input.val()); + if (this.max_page && !(page > 0 && page <= this.max_page)) { + page = 1; + } + this.update_embedded_code(page); + }, + update_embedded_code: function(page){ + var embed_input = this.$('.slide_embed_code'); + var new_code = embed_input.val().replace(/(page=).*?([^\d]+)/, '$1' + page + '$2'); + embed_input.val(new_code); + }, + }); + $('iframe.o_wslides_iframe_viewer').ready(function() { + // TODO : make it work. For now, once the iframe is loaded, the value of #page_count is + // still now set (the pdf is still loading) + var $iframe = $(this); + var max_page = $iframe.contents().find('#page_count').val(); + slides.page_widgets['social_embed'] = new SlideSocialEmbed($iframe, max_page).setElement($('.oe_slide_js_embed_code_widget')); + }); + + })(); + +}); /** This file is a minimal version of the PDFViewer widget. It is NOT use in the website_slides module, but it is diff --git a/addons/website_slides/static/src/js/slides_like.js b/addons/website_slides/static/src/js/slides_like.js new file mode 100644 index 00000000000..d9c0da2afa2 --- /dev/null +++ b/addons/website_slides/static/src/js/slides_like.js @@ -0,0 +1,63 @@ +/*global $, _, PDFJS */ +odoo.define('website_slides.slides_like', function (require) { + "use strict"; + + var ajax = require('web.ajax'); + var core = require('web.core'); + var Widget = require('web.Widget'); + var local_storage = require('web.local_storage'); + var slides = require('website_slides.slides'); + + var _t = core._t; + + (function () { + var widget_parent = $('body'); + + /* + * Like/Dislike Buttons Widget + */ + var LikeButton = Widget.extend({ + setElement: function($el){ + this._super.apply(this, arguments); + this.$el.on('click', this, _.bind(this.apply_action, this)); + }, + apply_action: function(ev){ + var button = $(ev.currentTarget); + var slide_id = button.data('slide-id'); + var user_id = button.data('user-id'); + var is_public = button.data('public-user'); + var href = button.data('href'); + if(is_public){ + this.popover_alert(button, _.str.sprintf(_t('Please login to vote this slide'), (document.URL))); + }else{ + var target = button.find('.fa'); + if (local_storage.getItem('slide_vote_' + slide_id) !== user_id.toString()) { + ajax.jsonRpc(href, 'call', {slide_id: slide_id}).then(function (data) { + target.text(data); + local_storage.setItem('slide_vote_' + slide_id, user_id); + }); + } else { + this.popover_alert(button, _t('You have already voted for this slide')); + } + } + }, + popover_alert: function($el, message){ + $el.popover({ + trigger: 'focus', + placement: 'bottom', + container: 'body', + html: true, + content: function(){ + return message; + } + }).popover('show'); + }, + }); + + slides.page_widgets['likeButton'] = new LikeButton(widget_parent).setElement($('.oe_slide_js_like')); + slides.page_widgets['dislikeButton'] = new LikeButton(widget_parent).setElement($('.oe_slide_js_unlike')); + + })(); + +}); + \ No newline at end of file diff --git a/addons/website_slides/static/src/js/slides_share.js b/addons/website_slides/static/src/js/slides_share.js new file mode 100644 index 00000000000..72e810f16f6 --- /dev/null +++ b/addons/website_slides/static/src/js/slides_share.js @@ -0,0 +1,110 @@ +odoo.define('website_slides.slides_share', function (require) { + "use strict"; + var ajax = require('web.ajax'); + var Widget = require('web.Widget'); + var slides = require('website_slides.slides'); + + (function () { + var widget_parent = $('body'); + + /* + * Send by email Widget + */ + var ShareMail = Widget.extend({ + events: { + 'click button' : 'send_mail', + }, + send_mail: function(){ + var self = this; + var input = this.$('input'); + var slide_id = this.$('button').data('slide-id'); + if(input.val() && input[0].checkValidity()){ + this.$el.removeClass('o_has_error').find('.form-control, .custom-select').removeClass('is-invalid'); + ajax.jsonRpc('/slides/slide/send_share_email', 'call', { + slide_id: slide_id, + email: input.val(), + }).then(function () { + self.$el.html($('')); + }); + }else{ + this.$el.addClass('o_has_error').find('.form-control, .custom-select').addClass('is-invalid'); + input.focus(); + } + }, + }); + + slides.page_widgets['share_mail'] = new ShareMail(widget_parent).setElement($('.oe_slide_js_share_email')); + + /* + * Social Sharing Statistics Widget + */ + if ($('div#statistic').length) { + var slide_url = $("div#statistic").attr('slide-url'); + var social_urls = { + 'linkedin': 'https://www.linkedin.com/countserv/count/share?url=', + 'twitter': 'https://cdn.api.twitter.com/1/urls/count.json?url=', + 'facebook': 'https://graph.facebook.com/?id=', + 'gplus': 'https://clients6.google.com/rpc' + } + + var update_statistics = function(social_site, slide_url) { + if (social_site == 'gplus') { + $.ajax({ + url: social_urls['gplus'], + type: "POST", + dataType: 'json', + contentType: 'application/json', + data: JSON.stringify([{ + "method": "pos.plusones.get", + "id": "p", + "params": { + "nolog": true, + "id": slide_url, + "source": "widget", + "userId": "@viewer", + "groupId": "@self" + }, + // TDE NOTE: should there be a key here ? + "jsonrpc": "2.0", + "apiVersion": "v1" + }]), + success: function(data) { + $('#google-badge').text(data[0].result.metadata.globalCounts.count || 0); + $('#total-share').text(parseInt($('#total-share').text()) + parseInt($('#google-badge').text())); + }, + }); + } else { + $.ajax({ + url: social_urls[social_site] + slide_url, + dataType: 'jsonp', + success: function(data) { + var shareCount = (social_site === 'facebook' ? data.shares : data.count) || 0; + $('#' + social_site + '-badge').text(shareCount); + $('#total-share').text(parseInt($('#total-share').text()) + parseInt($('#' + social_site+ '-badge').text())); + }, + }); + } + }; + + $.each(social_urls, function(key, value) { + update_statistics(key, slide_url); + }); + + $("a.o_slides_social_share").on('click', function(ev) { + ev.preventDefault(); + var key = $(ev.currentTarget).attr('social-key'); + var popUpURL = $(ev.currentTarget).attr('href'); + var popUp = window.open( + popUpURL, + 'Share Dialog', + 'width=626,height=436'); + $(window).on('focus', function() { + if (popUp.closed) { + update_statistics(key, slide_url); + $(window).off('focus'); + } + }); + }); + } + })(); +}) \ No newline at end of file diff --git a/addons/website_slides/views/assets.xml b/addons/website_slides/views/assets.xml new file mode 100644 index 00000000000..db8bed5a7dc --- /dev/null +++ b/addons/website_slides/views/assets.xml @@ -0,0 +1,35 @@ + + + + + + + + + \ No newline at end of file diff --git a/addons/website_slides/views/slide_channel_views.xml b/addons/website_slides/views/slide_channel_views.xml new file mode 100644 index 00000000000..308610c0e90 --- /dev/null +++ b/addons/website_slides/views/slide_channel_views.xml @@ -0,0 +1,145 @@ + + + + + + slide.category.form + slide.category + +
+ + + + + + +
+
+
+ + + slide.category.tree + slide.category + + + + + + + + + + + Categories + slide.category + form + tree,form + +

+ Create a new category +

+
+
+ + + + slide.channel.form + slide.channel + +
+ +
+ + +
+
+
+
+

+ +

+
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ + +
+
+
+
+ + + slide.channel.tree + slide.channel + + + + + + + + + + + + + + slide.channel.search + slide.channel + + + + + + + + + Channels + slide.channel + form + tree,form + +

+ Create a channel +

+
+
+
+
\ No newline at end of file diff --git a/addons/website_slides/views/slide_slide_views.xml b/addons/website_slides/views/slide_slide_views.xml new file mode 100644 index 00000000000..a64b55630f0 --- /dev/null +++ b/addons/website_slides/views/slide_slide_views.xml @@ -0,0 +1,163 @@ + + + + + + slide.tag.form + slide.tag + +
+ + +
+
+ + + slide.tag.tree + slide.tag + + + + + + + + + Tags + ir.actions.act_window + slide.tag + form + tree,form + + + + + slide.slide.form + slide.slide + +
+ +
+ + +
+ +
+
+
+

+ +

+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ + +
+
+
+
+ + + slide.slide.tree + slide.slide + + + + + + + + + + + + + + + slide.slide.filter + slide.slide + + + + + + + + + + + + + + + + + + slide.slide.graph + slide.slide + + + + + + + + + + Slides + slide.slide + form + tree,form,graph + +

+ Add a new slide +

+
+
+
+
\ No newline at end of file diff --git a/addons/website_slides/views/website_slides_backend.xml b/addons/website_slides/views/website_slides_backend.xml deleted file mode 100644 index cf496a3d24b..00000000000 --- a/addons/website_slides/views/website_slides_backend.xml +++ /dev/null @@ -1,326 +0,0 @@ - - - - - - - slide.tag.form - slide.tag - -
- - -
-
- - - slide.tag.tree - slide.tag - - - - - - - - - Tags - ir.actions.act_window - slide.tag - form - tree,form - - - - - slide.slide.form - slide.slide - -
- -
- - -
- -
-
-
-

- -

- -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
-
- - -
-
-
-
- - - slide.slide.tree - slide.slide - - - - - - - - - - - - - - - slide.slide.filter - slide.slide - - - - - - - - - - - - - - - - - - slide.slide.graph - slide.slide - - - - - - - - - - Slides - slide.slide - form - tree,form,graph - -

- Add a new slide -

-
-
- - - - slide.category.form - slide.category - -
- - - - - - -
-
-
- - - slide.category.tree - slide.category - - - - - - - - - - - Categories - slide.category - form - tree,form - -

- Create a new category -

-
-
- - - - slide.channel.form - slide.channel - -
- -
- - -
-
-
-
-

- -

-
- - - - - - - - - - - - - - - - - - - - - - - - - - -
-
- - -
-
-
-
- - - slide.channel.tree - slide.channel - - - - - - - - - - - - - - slide.channel.search - slide.channel - - - - - - - - - Channels - slide.channel - form - tree,form - -

- Create a channel -

-
-
- - - - - - - - - - - -
-
diff --git a/addons/website_slides/views/website_slides_embed.xml b/addons/website_slides/views/website_slides_embed_templates.xml similarity index 93% rename from addons/website_slides/views/website_slides_embed.xml rename to addons/website_slides/views/website_slides_embed_templates.xml index 35731f1f4cc..55f63c1d3e1 100644 --- a/addons/website_slides/views/website_slides_embed.xml +++ b/addons/website_slides/views/website_slides_embed_templates.xml @@ -1,17 +1,6 @@ - - - -