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 @@
+
+
+ Hello
+ Hello
- Hello
- 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)}
+
+
+
+ ${user.name} shared the ${object.slide_type} ${object.name} with you!
+
+
+
- A new ${object.slide_type} ${object.name} has been published on ${object.channel_id.name} at ${format_tz(object.write_date, tz=user.tz)}
-
-
-
- ${user.name} shared the ${object.slide_type} ${object.name} with you!
-
-
-
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($('+ Create a new category +
++ Create a channel +
++ Add a new slide +
+- Add a new slide -
-- Create a new category -
-- Create a channel -
-