[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
This commit is contained in:
qmo-odoo
2018-11-19 08:28:34 +00:00
parent 2b896bcf82
commit 6f5748d288
20 changed files with 947 additions and 882 deletions
+10 -4
View File
@@ -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'
],
+64
View File
@@ -0,0 +1,64 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<data noupdate="1">
<record id="slide_template_published" model="mail.template">
<field name="name">Slide Published</field>
<field name="model_id" ref="model_slide_slide"/>
<field name="subject">New ${object.slide_type} published on ${object.channel_id.name}</field>
<field name="body_html" type="html">
<div style="margin: 0px; padding: 0px;">
<p style="margin: 0px; padding: 0px; font-size: 13px;">
Hello<br/><br/>
A new ${object.slide_type} <strong>${object.name}</strong> has been published on ${object.channel_id.name} at ${format_tz(object.write_date, tz=user.tz)}
<div style="margin: 16px 8px 16px 8px; text-align: center;">
<a href="${object.website_url}">
<img alt="${object.name}" src="${ctx['base_url']}/web/image/slide.slide/${object.id}/image" style="height:auto; width:150px; margin: 16px;"/>
</a>
</div>
<div style="margin: 16px 8px 16px 8px; text-align: center;">
<a href="${object.website_url}"
style="background-color: #875a7b; padding: 8px 16px 8px 16px; text-decoration: none; color: #fff; border-radius: 5px;">View <strong>${object.name}</strong></a>
</div>
</p>
</div>
</field>
<field name="auto_delete" eval="True"/>
<field name="user_signature" eval="True"/>
</record>
<record id="slide_template_shared" model="mail.template">
<field name="name">Slide Shared</field>
<field name="model_id" ref="model_slide_slide"/>
<field name="subject">${user.name} shared a ${object.slide_type} with you!</field>
<field name="email_from">${(user.email or '')|safe}</field>
<field name="email_to">${ctx['email']}</field>
<field name="body_html" type="html">
<div style="margin: 0px; padding: 0px;">
<p style="margin: 0px; padding: 0px; font-size: 13px;">
Hello<br/><br/>
${user.name} shared the ${object.slide_type} <strong>${object.name}</strong> with you!
<div style="margin: 16px 8px 16px 8px; text-align: center;">
<a href="${object.website_url}">
<img alt="${object.name}" src="${ctx['base_url']}/web/image/slide.slide/${object.id}/image" style="height:auto; width:150px; margin: 16px;"/>
</a>
</div>
<div style="margin: 16px 8px 16px 8px; text-align: center;">
<a href="${object.website_url}"
style="background-color: #875a7b; padding: 8px 16px 8px 16px; text-decoration: none; color: #fff; border-radius: 5px;">View <strong>${object.name}</strong></a>
</div>
</p>
</div>
</field>
<field name="auto_delete" eval="True"/>
<field name="user_signature" eval="True"/>
</record>
<!-- Channel subtypes -->
<record id="mt_channel_slide_published" model="mail.message.subtype">
<field name="name">Presentation Published</field>
<field name="res_model">slide.channel</field>
<field name="default" eval="True"/>
<field name="description">Presentation Published</field>
</record>
</data>
</odoo>
+26
View File
@@ -0,0 +1,26 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<data noupdate="1">
<record model="slide.channel" id="channel_public">
<field name="name">Public Channel</field>
<field name="promote_strategy">most_viewed</field>
<field name="website_published" eval="True"/>
<field name="visibility">public</field>
<field name="description">Default channel for slides, all public users can access content of this channel.</field>
</record>
<record model="slide.channel" id="channel_partial">
<field name="name">Partner Channel</field>
<field name="website_published" eval="True"/>
<field name="group_ids" eval="[(4, ref('base.group_portal')), (4, ref('base.group_user'))]"/>
<field name="upload_group_ids" eval="[(4, ref('base.group_user'))]"/>
<field name="visibility">partial</field>
<field name="description">Used to publish slides in partner network privately.</field>
</record>
<record model="slide.channel" id="channel_private">
<field name="name">Employee Channel</field>
<field name="group_ids" eval="[(4, ref('base.group_user'))]"/>
<field name="visibility">private</field>
<field name="description">Used to publish internal slides of company.</field>
</record>
</data>
</odoo>
@@ -0,0 +1,15 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<data noupdate="1">
<record id="website.default_website" model="website">
<field name="website_slide_google_app_key">AIzaSyDOWlmDW-7DbLmOR9ZsT5AOEXf4n6TFwQA</field>
</record>
<record id="website_menu_slides" model="website.menu">
<field name="name">Presentations</field>
<field name="url">/slides</field>
<field name="parent_id" ref="website.main_menu"/>
<field name="sequence" type="int">50</field>
</record>
</data>
</odoo>
@@ -1,109 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<data noupdate="1">
<record id="website.default_website" model="website">
<field name="website_slide_google_app_key">AIzaSyDOWlmDW-7DbLmOR9ZsT5AOEXf4n6TFwQA</field>
</record>
<record id="website_menu_slides" model="website.menu">
<field name="name">Presentations</field>
<field name="url">/slides</field>
<field name="parent_id" ref="website.main_menu"/>
<field name="sequence" type="int">50</field>
</record>
<record id="slide_template_published" model="mail.template">
<field name="name">Slide Published</field>
<field name="model_id" ref="model_slide_slide"/>
<field name="subject">New ${object.slide_type} published on ${object.channel_id.name}</field>
<field name="body_html" type="html">
<div style="margin: 0px; padding: 0px;">
<p style="margin: 0px; padding: 0px; font-size: 13px;">
Hello<br/><br/>
A new ${object.slide_type} <strong>${object.name}</strong> has been published on ${object.channel_id.name} at ${format_tz(object.write_date, tz=user.tz)}
<div style="margin: 16px 8px 16px 8px; text-align: center;">
<a href="${object.website_url}">
<img alt="${object.name}" src="${ctx['base_url']}/web/image/slide.slide/${object.id}/image" style="height:auto; width:150px; margin: 16px;"/>
</a>
</div>
<div style="margin: 16px 8px 16px 8px; text-align: center;">
<a href="${object.website_url}"
style="background-color: #875a7b; padding: 8px 16px 8px 16px; text-decoration: none; color: #fff; border-radius: 5px;">View <strong>${object.name}</strong></a>
</div>
</p>
</div>
</field>
<field name="auto_delete" eval="True"/>
<field name="user_signature" eval="True"/>
</record>
<record id="slide_template_shared" model="mail.template">
<field name="name">Slide Shared</field>
<field name="model_id" ref="model_slide_slide"/>
<field name="subject">${user.name} shared a ${object.slide_type} with you!</field>
<field name="email_from">${(user.email or '')|safe}</field>
<field name="email_to">${ctx['email']}</field>
<field name="body_html" type="html">
<div style="margin: 0px; padding: 0px;">
<p style="margin: 0px; padding: 0px; font-size: 13px;">
Hello<br/><br/>
${user.name} shared the ${object.slide_type} <strong>${object.name}</strong> with you!
<div style="margin: 16px 8px 16px 8px; text-align: center;">
<a href="${object.website_url}">
<img alt="${object.name}" src="${ctx['base_url']}/web/image/slide.slide/${object.id}/image" style="height:auto; width:150px; margin: 16px;"/>
</a>
</div>
<div style="margin: 16px 8px 16px 8px; text-align: center;">
<a href="${object.website_url}"
style="background-color: #875a7b; padding: 8px 16px 8px 16px; text-decoration: none; color: #fff; border-radius: 5px;">View <strong>${object.name}</strong></a>
</div>
</p>
</div>
</field>
<field name="auto_delete" eval="True"/>
<field name="user_signature" eval="True"/>
</record>
<record model="slide.channel" id="channel_public">
<field name="name">Public Channel</field>
<field name="promote_strategy">most_viewed</field>
<field name="website_published" eval="True"/>
<field name="visibility">public</field>
<field name="description">Default channel for slides, all public users can access content of this channel.</field>
</record>
<record model="slide.channel" id="channel_partial">
<field name="name">Partner Channel</field>
<field name="website_published" eval="True"/>
<field name="group_ids" eval="[(4, ref('base.group_portal')), (4, ref('base.group_user'))]"/>
<field name="upload_group_ids" eval="[(4, ref('base.group_user'))]"/>
<field name="visibility">partial</field>
<field name="description">Used to publish slides in partner network privately.</field>
</record>
<record model="slide.channel" id="channel_private">
<field name="name">Employee Channel</field>
<field name="group_ids" eval="[(4, ref('base.group_user'))]"/>
<field name="visibility">private</field>
<field name="description">Used to publish internal slides of company.</field>
</record>
<!-- Channel subtypes -->
<record id="mt_channel_slide_published" model="mail.message.subtype">
<field name="name">Presentation Published</field>
<field name="res_model">slide.channel</field>
<field name="default" eval="True"/>
<field name="description">Presentation Published</field>
</record>
<!-- jump to Channel on installation complete -->
<record id="action_open_channels" model="ir.actions.act_url">
<field name="name">Channels</field>
<field name="target">self</field>
<field name="url" eval="'/slides'"/>
</record>
<record id="base.open_menu" model="ir.actions.todo">
<field name="action_id" ref="action_open_channels"/>
<field name="state">open</field>
</record>
</data>
</odoo>
@@ -0,0 +1,15 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<data noupdate="1">
<!-- jump to Channel on installation complete -->
<record id="action_open_channels" model="ir.actions.act_url">
<field name="name">Channels</field>
<field name="target">self</field>
<field name="url" eval="'/slides'"/>
</record>
<record id="base.open_menu" model="ir.actions.todo">
<field name="action_id" ref="action_open_channels"/>
<field name="state">open</field>
</record>
</data>
</odoo>
+2 -1
View File
@@ -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
@@ -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: _("<p>This channel is private and its content is restricted to some users.</p>"),
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
@@ -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: _("<p>This channel is private and its content is restricted to some users.</p>"),
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'
+22 -213
View File
@@ -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 <a href="/web?redirect=%s">login</a> 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($('<div class="alert alert-info" role="alert"><strong>Thank you!</strong> Mail has been sent.</div>'));
});
}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,
};
});
@@ -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
@@ -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 <a href="/web?redirect=%s">login</a> 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'));
})();
});
@@ -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($('<div class="alert alert-info" role="alert"><strong>Thank you!</strong> Mail has been sent.</div>'));
});
}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');
}
});
});
}
})();
})
+35
View File
@@ -0,0 +1,35 @@
<?xml version="1.0" ?>
<odoo>
<data>
<template id="assets_frontend" inherit_id="website.assets_frontend" name="Slide">
<xpath expr="." position="inside">
<link rel="stylesheet" type="text/scss" href="/website_slides/static/src/scss/website_slides.scss" t-ignore="true"/>
<!-- Js widgets, ... order loading files is IMPORTANT -->
<script type="text/javascript" src="/website_slides/static/src/js/slides.js"/>
<script type="text/javascript" src="/website_slides/static/src/js/slides_like.js"/>
<script type="text/javascript" src="/website_slides/static/src/js/slides_share.js"/>
<script type="text/javascript" src="/website_slides/static/src/js/slides_upload.js"/>
</xpath>
</template>
<template id="assets_editor_inherit_website_slides" inherit_id="website.assets_editor" name="website_slides Assets Editor">
<xpath expr="." position="inside">
<script type="text/javascript" src="/website_slides/static/src/js/website_slides.editor.js"/>
</xpath>
</template>
<!-- Bundle (minimal) for embedded slide iframe -->
<template id="website_slides.slide_embed_assets" name="Website slides embed assets">
<t t-call="web._assets_helpers"><t t-set="no_bs_vars" t-value="True"/></t>
<t t-call="web._assets_bootstrap"/>
<link rel="stylesheet" type="text/scss" href="/website_slides/static/src/scss/website_slides.scss" t-ignore="true"/>
<t t-call="web.pdf_js_lib"></t>
<script type="text/javascript" src="/website_slides/static/lib/pdfslidesviewer/PDFSlidesViewer.js"></script>
<script type="text/javascript" src="/website_slides/static/src/js/slides_embed.js"></script>
</template>
</data>
</odoo>
@@ -0,0 +1,145 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<data>
<!-- SLIDE.CATEGORY -->
<record model="ir.ui.view" id="view_slides_category_form">
<field name="name">slide.category.form</field>
<field name="model">slide.category</field>
<field name="arch" type="xml">
<form string="Category">
<sheet>
<group>
<field name="name"/>
<field name="channel_id"/>
</group>
</sheet>
</form>
</field>
</record>
<record id="view_slides_category_tree" model="ir.ui.view">
<field name="name">slide.category.tree</field>
<field name="model">slide.category</field>
<field name="arch" type="xml">
<tree string="Category">
<field name="sequence" widget="handle"/>
<field name="name"/>
<field name="channel_id"/>
</tree>
</field>
</record>
<record id="action_ir_slide_category" model="ir.actions.act_window">
<field name="name">Categories</field>
<field name="res_model">slide.category</field>
<field name="view_type">form</field>
<field name="view_mode">tree,form</field>
<field name="help" type="html">
<p class="o_view_nocontent_smiling_face">
Create a new category
</p>
</field>
</record>
<!-- SLIDE.CHANNEL VIEWS -->
<record model="ir.ui.view" id="view_slide_channel_form">
<field name="name">slide.channel.form</field>
<field name="model">slide.channel</field>
<field name="arch" type="xml">
<form string="Channels">
<sheet>
<div class="oe_button_box" name="button_box">
<button class="oe_stat_button" name="website_publish_button"
type="object" icon="fa-globe">
<field name="website_published" widget="website_button"/>
</button>
<button name="toggle_active" type="object" confirm="(Un)archiving a channel automatically (un)archives its slides. Do you want to proceed?" class="oe_stat_button" icon="fa-archive">
<field name="active" widget="boolean_button" options='{"terminology": "archive"}'/>
</button>
</div>
<div class="oe_title">
<div class="oe_edit_only">
<label for="name" string="Name"/>
</div>
<h1>
<field name="name" default_focus="1" placeholder="Name"/>
</h1>
</div>
<field name="category_ids" widget="many2many_tags" placeholder="Categories" context="{'default_channel_id': active_id}"
domain="[('channel_id','=', active_id)]"/>
<notebook colspan="4">
<page string="General">
<group>
<group>
<field name="website_id" options="{'no_create': True}" groups="website.group_multi_website"/>
<field name="promote_strategy" widget="radio"/>
<field name="custom_slide_id" class="oe_edit_only" attrs="{'invisible': [('promote_strategy', '!=', 'custom')]}" domain="[('channel_id','=',id), ('website_published','=',True)]"/>
<field name="promoted_slide_id" attrs="{'invisible': [('promote_strategy', '!=', 'custom')]}"/>
</group>
<group>
<field name="publish_template_id" domain="[('model','=','slide.slide')]"/>
<field name="share_template_id" domain="[('model','=','slide.slide')]"/>
</group>
</group>
<label for="description"/>
<field name="description" colspan="4" nolabel="1"/>
</page>
<page string="Security">
<group>
<field name="upload_group_ids" widget="many2many_tags"/>
<field name="visibility" string="Channel visibility" widget="radio"/>
<field name="group_ids" widget="many2many_tags" attrs="{'invisible':[('visibility', '=', 'public')], 'required':[('visibility', 'in',('private', 'partial'))]}"/>
</group>
<group attrs="{'invisible':[('visibility','in',('public'))]}" colspan="4">
<label for="access_error_msg" string="Error Message"/>
<field name="access_error_msg" colspan="4" nolabel="1"/>
</group>
</page>
</notebook>
</sheet>
<div class="oe_chatter">
<field name="message_follower_ids" widget="mail_followers"/>
<field name="message_ids" widget="mail_thread"/>
</div>
</form>
</field>
</record>
<record id="view_slide_channel_tree" model="ir.ui.view">
<field name="name">slide.channel.tree</field>
<field name="model">slide.channel</field>
<field name="arch" type="xml">
<tree string="Channel">
<field name="sequence" widget="handle"/>
<field name="name"/>
<field name="website_id" groups="website.group_multi_website"/>
<field name="active" invisible="1"/>
<field name="visibility"/>
<field name="promote_strategy"/>
</tree>
</field>
</record>
<record id="slide_channel_view_search" model="ir.ui.view">
<field name="name">slide.channel.search</field>
<field name="model">slide.channel</field>
<field name="arch" type="xml">
<search string="Channel">
<filter string="Archived" name="inactive" domain="[('active','=',False)]"/>
</search>
</field>
</record>
<record id="action_slide_channels" model="ir.actions.act_window">
<field name="name">Channels</field>
<field name="res_model">slide.channel</field>
<field name="view_type">form</field>
<field name="view_mode">tree,form</field>
<field name="help" type="html">
<p class="o_view_nocontent_smiling_face">
Create a channel
</p>
</field>
</record>
</data>
</odoo>
@@ -0,0 +1,163 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<data>
<!-- SLIDE.TAG -->
<record id="view_slide_tag_form" model="ir.ui.view">
<field name="name">slide.tag.form</field>
<field name="model">slide.tag</field>
<field name="arch" type="xml">
<form string="Tag">
<field name="name"/>
</form>
</field>
</record>
<record id="view_slide_tag_tree" model="ir.ui.view">
<field name="name">slide.tag.tree</field>
<field name="model">slide.tag</field>
<field name="arch" type="xml">
<tree string="Tags">
<field name="name"/>
</tree>
</field>
</record>
<record id="action_slide_tag" model="ir.actions.act_window">
<field name="name">Tags</field>
<field name="type">ir.actions.act_window</field>
<field name="res_model">slide.tag</field>
<field name="view_type">form</field>
<field name="view_mode">tree,form</field>
</record>
<!-- SLIDE.SLIDE -->
<record id="view_slide_slide_form" model="ir.ui.view">
<field name="name">slide.slide.form</field>
<field name="model">slide.slide</field>
<field name="arch" type="xml">
<form string="Slide">
<sheet>
<div class="oe_button_box" name="button_box">
<button class="oe_stat_button" name="website_publish_button"
type="object" icon="fa-globe">
<field name="website_published" widget="website_button"/>
</button>
<button name="toggle_active" type="object" class="oe_stat_button" icon="fa-archive">
<field name="active" widget="boolean_button" options='{"terminology": "archive"}'/>
</button>
</div>
<field name="image" widget="image" class="oe_avatar" options='{"preview_image": "image_thumb"}' readonly="1"/>
<div class="oe_title">
<div class="oe_edit_only">
<label for="name" string="Name"/>
</div>
<h1>
<field name="name" default_focus="1" placeholder="Name"/>
</h1>
<field name="tag_ids" widget="many2many_tags" placeholder="Tags..."/>
</div>
<group>
<field name="channel_id"/>
<field name="category_id" context="{'default_channel_id': channel_id}"/>
<field name="website_id" groups="website.group_multi_website"/>
<field name="website_url"/>
</group>
<notebook>
<page string="Description">
<field name="description"/>
</page>
<page string="Document">
<group>
<field name="url" attrs="{'required': [('image', '=', False)]}"/>
<field name="slide_type" readonly="1"/>
<field name="document_id" readonly="1"/>
<field name="mime_type" readonly="1"/>
<field name="datas" readonly="1"/>
</group>
</page>
<page string="Website">
<group>
<group>
<field name="download_security"/>
<field name="date_published"/>
<field name="likes"/>
<field name="dislikes"/>
</group>
<group>
<field name="slide_views"/>
<field name="embed_views"/>
<field name="total_views"/>
</group>
</group>
<label for="index_content"/>
<field name="index_content"/>
</page>
</notebook>
</sheet>
<div class="oe_chatter">
<field name="message_follower_ids" widget="mail_followers"/>
<field name="message_ids" widget="mail_thread"/>
</div>
</form>
</field>
</record>
<record id="view_slide_slide_tree" model="ir.ui.view">
<field name="name">slide.slide.tree</field>
<field name="model">slide.slide</field>
<field name="arch" type="xml">
<tree string="Slides">
<field name="name"/>
<field name="website_id" groups="website.group_multi_website"/>
<field name="active" invisible="1"/>
<field name="slide_type"/>
<field name="channel_id"/>
<field name="category_id"/>
<field name="date_published"/>
</tree>
</field>
</record>
<record id="view_slide_slide_search" model="ir.ui.view">
<field name="name">slide.slide.filter</field>
<field name="model">slide.slide</field>
<field name="arch" type="xml">
<search string="Search Slides">
<field name="name"/>
<filter name="published" string="Published" domain="[('website_published', '=', True)]"/>
<filter name="not_published" string="Waiting for validation" domain="[('website_published', '=', False)]"/>
<separator/>
<filter string="Archived" name="inactive" domain="[('active','=',False)]"/>
<group expand="0" string="Group By">
<filter string="Channel" name="channel" domain="[]" context="{'group_by': 'channel_id'}"/>
<filter string="Category" name="category" domain="[]" context="{'group_by': 'category_id'}"/>
<filter string="Type" name="type" domain="[]" context="{'group_by': 'slide_type'}"/>
</group>
</search>
</field>
</record>
<record id="view_slide_slide_graph" model="ir.ui.view">
<field name="name">slide.slide.graph</field>
<field name="model">slide.slide</field>
<field name="arch" type="xml">
<graph string="Graph of Slides">
<field name="date_published" interval="month" type="col"/>
<field name="channel_id" type="row"/>
</graph>
</field>
</record>
<record id="action_slides_slides" model="ir.actions.act_window">
<field name="name">Slides</field>
<field name="res_model">slide.slide</field>
<field name="view_type">form</field>
<field name="view_mode">tree,form,graph</field>
<field name="help" type="html">
<p class="o_view_nocontent_smiling_face">
Add a new slide
</p>
</field>
</record>
</data>
</odoo>
@@ -1,326 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<data>
<!-- SLIDE.TAG -->
<record id="view_slide_tag_form" model="ir.ui.view">
<field name="name">slide.tag.form</field>
<field name="model">slide.tag</field>
<field name="arch" type="xml">
<form string="Tag">
<field name="name"/>
</form>
</field>
</record>
<record id="view_slide_tag_tree" model="ir.ui.view">
<field name="name">slide.tag.tree</field>
<field name="model">slide.tag</field>
<field name="arch" type="xml">
<tree string="Tags">
<field name="name"/>
</tree>
</field>
</record>
<record id="action_slide_tag" model="ir.actions.act_window">
<field name="name">Tags</field>
<field name="type">ir.actions.act_window</field>
<field name="res_model">slide.tag</field>
<field name="view_type">form</field>
<field name="view_mode">tree,form</field>
</record>
<!-- SLIDE.SLIDE -->
<record id="view_slide_slide_form" model="ir.ui.view">
<field name="name">slide.slide.form</field>
<field name="model">slide.slide</field>
<field name="arch" type="xml">
<form string="Slide">
<sheet>
<div class="oe_button_box" name="button_box">
<button class="oe_stat_button" name="website_publish_button"
type="object" icon="fa-globe">
<field name="website_published" widget="website_button"/>
</button>
<button name="toggle_active" type="object" class="oe_stat_button" icon="fa-archive">
<field name="active" widget="boolean_button" options='{"terminology": "archive"}'/>
</button>
</div>
<field name="image" widget="image" class="oe_avatar" options='{"preview_image": "image_thumb"}' readonly="1"/>
<div class="oe_title">
<div class="oe_edit_only">
<label for="name" string="Name"/>
</div>
<h1>
<field name="name" default_focus="1" placeholder="Name"/>
</h1>
<field name="tag_ids" widget="many2many_tags" placeholder="Tags..."/>
</div>
<group>
<field name="channel_id"/>
<field name="category_id" context="{'default_channel_id': channel_id}"/>
<field name="website_id" groups="website.group_multi_website"/>
<field name="website_url"/>
</group>
<notebook>
<page string="Description">
<field name="description"/>
</page>
<page string="Document">
<group>
<field name="url" attrs="{'required': [('image', '=', False)]}"/>
<field name="slide_type" readonly="1"/>
<field name="document_id" readonly="1"/>
<field name="mime_type" readonly="1"/>
<field name="datas" readonly="1"/>
</group>
</page>
<page string="Website">
<group>
<group>
<field name="download_security"/>
<field name="date_published"/>
<field name="likes"/>
<field name="dislikes"/>
</group>
<group>
<field name="slide_views"/>
<field name="embed_views"/>
<field name="total_views"/>
</group>
</group>
<label for="index_content"/>
<field name="index_content"/>
</page>
</notebook>
</sheet>
<div class="oe_chatter">
<field name="message_follower_ids" widget="mail_followers"/>
<field name="message_ids" widget="mail_thread"/>
</div>
</form>
</field>
</record>
<record id="view_slide_slide_tree" model="ir.ui.view">
<field name="name">slide.slide.tree</field>
<field name="model">slide.slide</field>
<field name="arch" type="xml">
<tree string="Slides">
<field name="name"/>
<field name="website_id" groups="website.group_multi_website"/>
<field name="active" invisible="1"/>
<field name="slide_type"/>
<field name="channel_id"/>
<field name="category_id"/>
<field name="date_published"/>
</tree>
</field>
</record>
<record id="view_slide_slide_search" model="ir.ui.view">
<field name="name">slide.slide.filter</field>
<field name="model">slide.slide</field>
<field name="arch" type="xml">
<search string="Search Slides">
<field name="name"/>
<filter name="published" string="Published" domain="[('website_published', '=', True)]"/>
<filter name="not_published" string="Waiting for validation" domain="[('website_published', '=', False)]"/>
<separator/>
<filter string="Archived" name="inactive" domain="[('active','=',False)]"/>
<group expand="0" string="Group By">
<filter string="Channel" name="channel" domain="[]" context="{'group_by': 'channel_id'}"/>
<filter string="Category" name="category" domain="[]" context="{'group_by': 'category_id'}"/>
<filter string="Type" name="type" domain="[]" context="{'group_by': 'slide_type'}"/>
</group>
</search>
</field>
</record>
<record id="view_slide_slide_graph" model="ir.ui.view">
<field name="name">slide.slide.graph</field>
<field name="model">slide.slide</field>
<field name="arch" type="xml">
<graph string="Graph of Slides">
<field name="date_published" interval="month" type="col"/>
<field name="channel_id" type="row"/>
</graph>
</field>
</record>
<record id="action_slides_slides" model="ir.actions.act_window">
<field name="name">Slides</field>
<field name="res_model">slide.slide</field>
<field name="view_type">form</field>
<field name="view_mode">tree,form,graph</field>
<field name="help" type="html">
<p class="o_view_nocontent_smiling_face">
Add a new slide
</p>
</field>
</record>
<!-- SLIDE.CATEGORY -->
<record model="ir.ui.view" id="view_slides_category_form">
<field name="name">slide.category.form</field>
<field name="model">slide.category</field>
<field name="arch" type="xml">
<form string="Category">
<sheet>
<group>
<field name="name"/>
<field name="channel_id"/>
</group>
</sheet>
</form>
</field>
</record>
<record id="view_slides_category_tree" model="ir.ui.view">
<field name="name">slide.category.tree</field>
<field name="model">slide.category</field>
<field name="arch" type="xml">
<tree string="Category">
<field name="sequence" widget="handle"/>
<field name="name"/>
<field name="channel_id"/>
</tree>
</field>
</record>
<record id="action_ir_slide_category" model="ir.actions.act_window">
<field name="name">Categories</field>
<field name="res_model">slide.category</field>
<field name="view_type">form</field>
<field name="view_mode">tree,form</field>
<field name="help" type="html">
<p class="o_view_nocontent_smiling_face">
Create a new category
</p>
</field>
</record>
<!-- SLIDE.CHANNEL VIEWS -->
<record model="ir.ui.view" id="view_slide_channel_form">
<field name="name">slide.channel.form</field>
<field name="model">slide.channel</field>
<field name="arch" type="xml">
<form string="Channels">
<sheet>
<div class="oe_button_box" name="button_box">
<button class="oe_stat_button" name="website_publish_button"
type="object" icon="fa-globe">
<field name="website_published" widget="website_button"/>
</button>
<button name="toggle_active" type="object" confirm="(Un)archiving a channel automatically (un)archives its slides. Do you want to proceed?" class="oe_stat_button" icon="fa-archive">
<field name="active" widget="boolean_button" options='{"terminology": "archive"}'/>
</button>
</div>
<div class="oe_title">
<div class="oe_edit_only">
<label for="name" string="Name"/>
</div>
<h1>
<field name="name" default_focus="1" placeholder="Name"/>
</h1>
</div>
<field name="category_ids" widget="many2many_tags" placeholder="Categories" context="{'default_channel_id': active_id}"
domain="[('channel_id','=', active_id)]"/>
<notebook colspan="4">
<page string="General">
<group>
<group>
<field name="website_id" options="{'no_create': True}" groups="website.group_multi_website"/>
<field name="promote_strategy" widget="radio"/>
<field name="custom_slide_id" class="oe_edit_only" attrs="{'invisible': [('promote_strategy', '!=', 'custom')]}" domain="[('channel_id','=',id), ('website_published','=',True)]"/>
<field name="promoted_slide_id" attrs="{'invisible': [('promote_strategy', '!=', 'custom')]}"/>
</group>
<group>
<field name="publish_template_id" domain="[('model','=','slide.slide')]"/>
<field name="share_template_id" domain="[('model','=','slide.slide')]"/>
</group>
</group>
<label for="description"/>
<field name="description" colspan="4" nolabel="1"/>
</page>
<page string="Security">
<group>
<field name="upload_group_ids" widget="many2many_tags"/>
<field name="visibility" string="Channel visibility" widget="radio"/>
<field name="group_ids" widget="many2many_tags" attrs="{'invisible':[('visibility', '=', 'public')], 'required':[('visibility', 'in',('private', 'partial'))]}"/>
</group>
<group attrs="{'invisible':[('visibility','in',('public'))]}" colspan="4">
<label for="access_error_msg" string="Error Message"/>
<field name="access_error_msg" colspan="4" nolabel="1"/>
</group>
</page>
</notebook>
</sheet>
<div class="oe_chatter">
<field name="message_follower_ids" widget="mail_followers"/>
<field name="message_ids" widget="mail_thread"/>
</div>
</form>
</field>
</record>
<record id="view_slide_channel_tree" model="ir.ui.view">
<field name="name">slide.channel.tree</field>
<field name="model">slide.channel</field>
<field name="arch" type="xml">
<tree string="Channel">
<field name="sequence" widget="handle"/>
<field name="name"/>
<field name="website_id" groups="website.group_multi_website"/>
<field name="active" invisible="1"/>
<field name="visibility"/>
<field name="promote_strategy"/>
</tree>
</field>
</record>
<record id="slide_channel_view_search" model="ir.ui.view">
<field name="name">slide.channel.search</field>
<field name="model">slide.channel</field>
<field name="arch" type="xml">
<search string="Channel">
<filter string="Archived" name="inactive" domain="[('active','=',False)]"/>
</search>
</field>
</record>
<record id="action_slide_channels" model="ir.actions.act_window">
<field name="name">Channels</field>
<field name="res_model">slide.channel</field>
<field name="view_type">form</field>
<field name="view_mode">tree,form</field>
<field name="help" type="html">
<p class="o_view_nocontent_smiling_face">
Create a channel
</p>
</field>
</record>
<menuitem name="Slides"
id="menu_website_slides_root"
sequence="32"
parent="website.menu_website_configuration"
groups="website.group_website_designer"
action="action_slides_slides"/>
<menuitem name="Slides"
id="menu_website_slides_root_global"
sequence="140"
parent="website.menu_website_global_configuration"
groups="website.group_website_designer"/>
<menuitem action="action_slide_channels" id="menu_action_slide_channels_global"
parent="menu_website_slides_root_global" sequence="1"/>
<menuitem action="action_ir_slide_category" id="menu_action_ir_slide_category_global" parent="menu_website_slides_root_global" sequence="2"/>
<menuitem action="action_slide_tag" id="menu_slide_tag" parent="menu_website_slides_root_global" sequence="10" groups="base.group_no_one"/>
</data>
</odoo>
@@ -1,17 +1,6 @@
<?xml version="1.0" ?>
<odoo>
<data>
<!-- Bundle (minimal) for embedded slide iframe -->
<template id="website_slides.slide_embed_assets" name="Website slides embed assets">
<t t-call="web._assets_helpers"><t t-set="no_bs_vars" t-value="True"/></t>
<t t-call="web._assets_bootstrap"/>
<link rel="stylesheet" type="text/scss" href="/website_slides/static/src/scss/website_slides.scss" t-ignore="true"/>
<t t-call="web.pdf_js_lib"></t>
<script type="text/javascript" src="/website_slides/static/lib/pdfslidesviewer/PDFSlidesViewer.js"></script>
<script type="text/javascript" src="/website_slides/static/src/js/slides_embed.js"></script>
</template>
<!--
This template is the PDF Viewer. It will mostly rendered throught an iFrame.
The js file to bind event is slides_embed.js
@@ -0,0 +1,25 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<data>
<menuitem name="Slides"
id="menu_website_slides_root"
sequence="32"
parent="website.menu_website_configuration"
groups="website.group_website_designer"
action="action_slides_slides"/>
<menuitem name="Slides"
id="menu_website_slides_root_global"
sequence="140"
parent="website.menu_website_global_configuration"
groups="website.group_website_designer"/>
<menuitem action="website_slides.action_slide_channels" id="menu_action_slide_channels_global"
parent="menu_website_slides_root_global" sequence="1"/>
<menuitem action="website_slides.action_ir_slide_category" id="menu_action_ir_slide_category_global" parent="menu_website_slides_root_global" sequence="2"/>
<menuitem action="website_slides.action_slide_tag" id="menu_slide_tag" parent="menu_website_slides_root_global" sequence="10" groups="base.group_no_one"/>
</data>
</odoo>
@@ -2,22 +2,6 @@
<odoo>
<data>
<!-- assets -->
<template id="assets_frontend" inherit_id="website.assets_frontend" name="Slide">
<xpath expr="." position="inside">
<link rel="stylesheet" type="text/scss" href="/website_slides/static/src/scss/website_slides.scss" t-ignore="true"/>
<!-- Js widgets, ... order loading files is IMPORTANT -->
<script type="text/javascript" src="/website_slides/static/src/js/slides.js"/>
<script type="text/javascript" src="/website_slides/static/src/js/slides_upload.js"/>
</xpath>
</template>
<template id="assets_editor_inherit_website_slides" inherit_id="website.assets_editor" name="website_slides Assets Editor">
<xpath expr="." position="inside">
<script type="text/javascript" src="/website_slides/static/src/js/website_slides.editor.js"/>
</xpath>
</template>
<!-- Tools template: share on social networkds -->
<template id='slides_share' name="Slides Media Share">
<div>