From 6f5748d28854305bb33ffe54e5f94bab4a14828f Mon Sep 17 00:00:00 2001 From: qmo-odoo Date: Wed, 14 Nov 2018 13:10:48 +0000 Subject: [PATCH] [MOV] slides: Rename and split files according to their underlying models Purpose of this merge is to polish a bit this module before turning it into "courses" (see task #1902304 for more details on this) Contains * split python model into separate main files to ease models understanding; * split views into separate main files to ease views finding; * rename files according to guidelines; * merge templates in the same file to have all available at hand; * move assets in their own file in order to remove noise from templates; * Split javascript widgets into their own separate files No functional behavior change should occur with this commit. Only move has been performed. Note that we might lose part of the history as splitting files creates a new history for new files. Task: #1909281 Closes: #28679 --- addons/website_slides/__manifest__.py | 14 +- addons/website_slides/data/mail_data.xml | 64 ++++ addons/website_slides/data/slide_data.xml | 26 ++ addons/website_slides/data/website_data.xml | 15 + .../data/website_slides_data.xml | 109 ------ .../data/website_slides_ir_data.xml | 15 + addons/website_slides/models/__init__.py | 3 +- addons/website_slides/models/slide_channel.py | 207 +++++++++++ .../models/{slides.py => slide_slide.py} | 202 ----------- addons/website_slides/static/src/js/slides.js | 235 ++----------- .../static/src/js/slides_embed.js | 45 +++ .../static/src/js/slides_like.js | 63 ++++ .../static/src/js/slides_share.js | 110 ++++++ addons/website_slides/views/assets.xml | 35 ++ .../views/slide_channel_views.xml | 145 ++++++++ .../views/slide_slide_views.xml | 163 +++++++++ .../views/website_slides_backend.xml | 326 ------------------ ...xml => website_slides_embed_templates.xml} | 11 - .../views/website_slides_menu_views.xml | 25 ++ ...lides.xml => website_slides_templates.xml} | 16 - 20 files changed, 947 insertions(+), 882 deletions(-) create mode 100644 addons/website_slides/data/mail_data.xml create mode 100644 addons/website_slides/data/slide_data.xml create mode 100644 addons/website_slides/data/website_data.xml delete mode 100644 addons/website_slides/data/website_slides_data.xml create mode 100644 addons/website_slides/data/website_slides_ir_data.xml create mode 100644 addons/website_slides/models/slide_channel.py rename addons/website_slides/models/{slides.py => slide_slide.py} (65%) create mode 100644 addons/website_slides/static/src/js/slides_like.js create mode 100644 addons/website_slides/static/src/js/slides_share.js create mode 100644 addons/website_slides/views/assets.xml create mode 100644 addons/website_slides/views/slide_channel_views.xml create mode 100644 addons/website_slides/views/slide_slide_views.xml delete mode 100644 addons/website_slides/views/website_slides_backend.xml rename addons/website_slides/views/{website_slides_embed.xml => website_slides_embed_templates.xml} (93%) create mode 100644 addons/website_slides/views/website_slides_menu_views.xml rename addons/website_slides/views/{website_slides.xml => website_slides_templates.xml} (98%) 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 @@ - - - -