[MERGE][REF] website_slides: rework the slide.embed feature
PURPOSE Make the slide.embed feature more reliable and more visible. REASON Currently, we use the same URL to embed slide content within the e-learning module and when the end-user wants to embed that content on an external website. However, we want to know whether the slide is embedded externally or not to show different controls and update some statistics. The code previously relied on the presence of the "referer" header to achieve that detection, if the referer URL comes from a domain that is not this Odoo website -> then it's embedded externally. The issue with that strategy is that it's not very reliable, indeed some modern browsers now completely avoid passing the referer header (mostly for security and privacy reasons). SPECS - Introduce an external embed URL to ease external embed logic - Improve various sharing controls when slide is embedded externally - Globally improve the slide.embed feature and give it more visibility See underlying commits for more details. LINKS Task-2654009 closes odoo/odoo#77736 Related: odoo/upgrade#3059 Signed-off-by: Thibault Delavallee (tde) <tde@openerp.com>
This commit is contained in:
@@ -31,6 +31,7 @@ Featuring
|
||||
'views/res_config_settings_views.xml',
|
||||
'views/res_partner_views.xml',
|
||||
'views/rating_rating_views.xml',
|
||||
'views/slide_embed_views.xml',
|
||||
'views/slide_question_views.xml',
|
||||
'views/slide_slide_views.xml',
|
||||
'views/slide_channel_partner_views.xml',
|
||||
|
||||
@@ -1133,22 +1133,36 @@ class WebsiteSlides(WebsiteProfile):
|
||||
|
||||
@http.route('/slides/embed/<int:slide_id>', type='http', auth='public', website=True, sitemap=False)
|
||||
def slides_embed(self, slide_id, page="1", **kw):
|
||||
# Note : don't use the 'model' in the route (use 'slide_id'), otherwise if public cannot access the embedded
|
||||
# slide, the error will be the website.403 page instead of the one of the website_slides.embed_slide.
|
||||
# Do not forget the rendering here will be displayed in the embedded iframe
|
||||
return self._slide_embed(slide_id, page=page, is_external_embed=False, **kw)
|
||||
|
||||
@http.route('/slides/embed_external/<int:slide_id>', type='http', auth='public', website=True, sitemap=False)
|
||||
def slides_embed_external(self, slide_id, page="1", **kw):
|
||||
return self._slide_embed(slide_id, page=page, is_external_embed=True, **kw)
|
||||
|
||||
def _slide_embed(self, slide_id, page="1", is_external_embed=False, **kw):
|
||||
""" Note : don't use the 'model' in the route (use 'slide_id'), otherwise if public cannot
|
||||
access the embedded slide, the error will be the website.403 page instead of the one of the
|
||||
website_slides.embed_slide.
|
||||
|
||||
Do not forget the rendering here will be displayed in the embedded iframe
|
||||
|
||||
Try accessing slide, and display to corresponding template.
|
||||
|
||||
When the content is embedded *externally*, meaning on a third party website, we do some
|
||||
additional steps like displaying sharing controls and also updating some KPIs. """
|
||||
|
||||
# try accessing slide, and display to corresponding template
|
||||
try:
|
||||
slide = request.env['slide.slide'].browse(slide_id)
|
||||
# determine if it is embedded from external web page
|
||||
referrer_url = request.httprequest.headers.get('Referer', '')
|
||||
base_url = slide.get_base_url()
|
||||
is_embedded = referrer_url and not bool(base_url in referrer_url) or False
|
||||
if is_embedded:
|
||||
request.env['slide.embed'].sudo()._add_embed_url(slide.id, referrer_url)
|
||||
if not slide.exists():
|
||||
raise werkzeug.exceptions.NotFound()
|
||||
|
||||
referer_url = request.httprequest.headers.get('Referer', '')
|
||||
if is_external_embed:
|
||||
slide.sudo()._embed_increment(referer_url)
|
||||
|
||||
values = self._get_slide_detail(slide)
|
||||
values['page'] = page
|
||||
values['is_embedded'] = is_embedded
|
||||
values['is_external_embed'] = is_external_embed
|
||||
self._set_viewed_slide(slide)
|
||||
return request.render('website_slides.embed_slide', values)
|
||||
except AccessError: # TODO : please, make it clean one day, or find another secure way to detect
|
||||
|
||||
@@ -5,6 +5,7 @@ from . import ir_http
|
||||
from . import gamification_challenge
|
||||
from . import slide_slide
|
||||
from . import slide_question
|
||||
from . import slide_embed
|
||||
from . import slide_channel
|
||||
from . import slide_channel_tag
|
||||
from . import slide_slide_resource
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import _, api, fields, models
|
||||
|
||||
|
||||
class EmbeddedSlide(models.Model):
|
||||
""" Embedding in third party websites. Track view count, generate statistics. """
|
||||
_name = 'slide.embed'
|
||||
_description = 'Embedded Slides View Counter'
|
||||
_rec_name = 'website_name'
|
||||
|
||||
slide_id = fields.Many2one('slide.slide', string="Presentation", required=True, index=True)
|
||||
url = fields.Char('Third Party Website URL')
|
||||
website_name = fields.Char('Website', compute='_compute_website_name')
|
||||
count_views = fields.Integer('# Views', default=1)
|
||||
|
||||
@api.depends('url')
|
||||
def _compute_website_name(self):
|
||||
for slide_embed in self:
|
||||
slide_embed.website_name = slide_embed.url or _('Unknown Website')
|
||||
@@ -10,6 +10,7 @@ import PyPDF2
|
||||
import json
|
||||
|
||||
from dateutil.relativedelta import relativedelta
|
||||
from markupsafe import Markup
|
||||
from PIL import Image
|
||||
from werkzeug import urls
|
||||
|
||||
@@ -55,30 +56,6 @@ class SlidePartnerRelation(models.Model):
|
||||
('partner_id', 'in', self.partner_id.ids),
|
||||
])._recompute_completion()
|
||||
|
||||
class EmbeddedSlide(models.Model):
|
||||
""" Embedding in third party websites. Track view count, generate statistics. """
|
||||
_name = 'slide.embed'
|
||||
_description = 'Embedded Slides View Counter'
|
||||
_rec_name = 'slide_id'
|
||||
|
||||
slide_id = fields.Many2one('slide.slide', string="Presentation", required=True, index=True)
|
||||
url = fields.Char('Third Party Website URL', required=True)
|
||||
count_views = fields.Integer('# Views', default=1)
|
||||
|
||||
def _add_embed_url(self, slide_id, url):
|
||||
baseurl = urls.url_parse(url).netloc
|
||||
if not baseurl:
|
||||
return 0
|
||||
embeds = self.search([('url', '=', baseurl), ('slide_id', '=', int(slide_id))], limit=1)
|
||||
if embeds:
|
||||
embeds.count_views += 1
|
||||
else:
|
||||
embeds = self.create({
|
||||
'slide_id': slide_id,
|
||||
'url': baseurl,
|
||||
})
|
||||
return embeds.count_views
|
||||
|
||||
|
||||
class SlideTag(models.Model):
|
||||
""" Tag to search slides across channels. """
|
||||
@@ -165,8 +142,11 @@ class Slide(models.Model):
|
||||
dislikes = fields.Integer('Dislikes', compute='_compute_user_info', store=True, compute_sudo=False)
|
||||
user_vote = fields.Integer('User vote', compute='_compute_user_info', compute_sudo=False)
|
||||
embed_code = fields.Html('Embed Code', readonly=True, compute='_compute_embed_code', sanitize=False)
|
||||
embed_code_external = fields.Html('External Embed Code', readonly=True, compute='_compute_embed_code', sanitize=False,
|
||||
help="Same as 'Embed Code' but used to embed the content on an external website.")
|
||||
# views
|
||||
embedcount_ids = fields.One2many('slide.embed', 'slide_id', string="Embed Count")
|
||||
embed_ids = fields.One2many('slide.embed', 'slide_id', string="External Slide Embeds")
|
||||
embed_count = fields.Integer('# of Embeds', compute='_compute_embed_counts')
|
||||
slide_views = fields.Integer('# of Website Views', store=True, compute="_compute_slide_views")
|
||||
public_views = fields.Integer('# of Public Views', copy=False)
|
||||
total_views = fields.Integer("# Total Views", default="0", compute='_compute_total', store=True)
|
||||
@@ -272,6 +252,24 @@ class Slide(models.Model):
|
||||
for slide in self:
|
||||
slide.slide_views = mapped_data.get(slide.id, 0)
|
||||
|
||||
@api.depends('embed_ids.slide_id')
|
||||
def _compute_embed_counts(self):
|
||||
mapped_data = {}
|
||||
|
||||
if self.ids:
|
||||
read_group_res = self.env['slide.embed'].read_group(
|
||||
[('slide_id', 'in', self.ids)],
|
||||
['count_views'],
|
||||
['slide_id']
|
||||
)
|
||||
mapped_data = {
|
||||
res['slide_id'][0]: res.get('count_views', 0)
|
||||
for res in read_group_res
|
||||
}
|
||||
|
||||
for slide in self:
|
||||
slide.embed_count = mapped_data.get(slide.id, 0)
|
||||
|
||||
@api.depends('slide_ids.sequence', 'slide_ids.slide_type', 'slide_ids.is_published', 'slide_ids.is_category')
|
||||
def _compute_slides_statistics(self):
|
||||
# Do not use dict.fromkeys(self.ids, dict()) otherwise it will use the same dictionnary for all keys.
|
||||
@@ -320,26 +318,34 @@ class Slide(models.Model):
|
||||
@api.depends('document_id', 'slide_type', 'mime_type')
|
||||
def _compute_embed_code(self):
|
||||
base_url = request and request.httprequest.url_root
|
||||
|
||||
for record in self:
|
||||
embed_code_external = False
|
||||
|
||||
if not base_url:
|
||||
base_url = record.get_base_url()
|
||||
if base_url[-1] == '/':
|
||||
base_url = base_url[:-1]
|
||||
if record.datas and (not record.document_id or record.slide_type in ['document', 'presentation']):
|
||||
slide_url = base_url + url_for('/slides/embed/%s?page=1' % record.id)
|
||||
record.embed_code = '<iframe src="%s" class="o_wslides_iframe_viewer" allowFullScreen="true" height="%s" width="%s" frameborder="0"></iframe>' % (slide_url, 315, 420)
|
||||
slide_url_external = base_url + url_for('/slides/embed_external/%s?page=1' % record.id)
|
||||
base_embed_code = Markup('<iframe src="%s" class="o_wslides_iframe_viewer" allowFullScreen="true" height="%s" width="%s" frameborder="0"></iframe>')
|
||||
record.embed_code = base_embed_code % (slide_url, 315, 420)
|
||||
embed_code_external = base_embed_code % (slide_url_external, 315, 420)
|
||||
elif record.slide_type == 'video' and record.document_id:
|
||||
if not record.mime_type:
|
||||
# embed youtube video
|
||||
query = urls.url_parse(record.url).query
|
||||
query = query + '&theme=light' if query else 'theme=light'
|
||||
record.embed_code = '<iframe src="//www.youtube-nocookie.com/embed/%s?%s" allowFullScreen="true" frameborder="0"></iframe>' % (record.document_id, query)
|
||||
record.embed_code = Markup('<iframe src="//www.youtube-nocookie.com/embed/%s?%s" allowFullScreen="true" frameborder="0"></iframe>') % (record.document_id, query)
|
||||
else:
|
||||
# embed google doc video
|
||||
record.embed_code = '<iframe src="//drive.google.com/file/d/%s/preview" allowFullScreen="true" frameborder="0"></iframe>' % (record.document_id)
|
||||
record.embed_code = Markup('<iframe src="//drive.google.com/file/d/%s/preview" allowFullScreen="true" frameborder="0"></iframe>') % (record.document_id)
|
||||
else:
|
||||
record.embed_code = False
|
||||
|
||||
record.embed_code_external = embed_code_external or record.embed_code
|
||||
|
||||
@api.onchange('url')
|
||||
def _on_change_url(self):
|
||||
self.ensure_one()
|
||||
@@ -505,6 +511,32 @@ class Slide(models.Model):
|
||||
# Business Methods
|
||||
# ---------------------------------------------------------
|
||||
|
||||
def _embed_increment(self, url):
|
||||
""" Increment the view count of the record we have based on the passed url.
|
||||
If the url is empty, which typically happens if the browser does not pass the 'referer'
|
||||
header properly, then we increment the entry that has 'False' as url value. """
|
||||
|
||||
self.ensure_one()
|
||||
|
||||
url_entry = url
|
||||
if not urls.url_parse(url).netloc:
|
||||
url_entry = False
|
||||
|
||||
embed_entry = self.env['slide.embed'].search([
|
||||
('url', '=', url_entry),
|
||||
('slide_id', '=', self.id)
|
||||
], limit=1)
|
||||
|
||||
if embed_entry:
|
||||
embed_entry.count_views += 1
|
||||
else:
|
||||
embed_entry = self.env['slide.embed'].create({
|
||||
'slide_id': self.id,
|
||||
'url': url_entry,
|
||||
})
|
||||
|
||||
return embed_entry
|
||||
|
||||
def _post_publication(self):
|
||||
for slide in self.filtered(lambda slide: slide.website_published and slide.channel_id.publish_template_id):
|
||||
publish_template = slide.channel_id.publish_template_id
|
||||
@@ -669,6 +701,13 @@ class Slide(models.Model):
|
||||
|
||||
return self.env.user.sudo().add_karma(points)
|
||||
|
||||
def action_view_embeds(self):
|
||||
self.ensure_one()
|
||||
|
||||
action = self.env["ir.actions.actions"]._for_xml_id("website_slides.slide_embed_action")
|
||||
action['context'] = {'search_default_slide_id': self.id}
|
||||
return action
|
||||
|
||||
def _compute_quiz_info(self, target_partner, quiz_done=False):
|
||||
result = dict.fromkeys(self.ids, False)
|
||||
slide_partners = self.env['slide.slide.partner'].sudo().search([
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from . import common
|
||||
from . import test_embed_detection
|
||||
from . import test_karma
|
||||
from . import test_resources
|
||||
from . import test_security
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo.addons.website_slides.tests import common
|
||||
from odoo.tests import HttpCase
|
||||
|
||||
|
||||
class TestEmbedDetection(HttpCase, common.SlidesCase):
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super(TestEmbedDetection, cls).setUpClass()
|
||||
|
||||
cls.website = cls.env['website'].create({
|
||||
'name': 'Test Website',
|
||||
'domain': 'https://testwebsite.com'
|
||||
})
|
||||
|
||||
cls.channel.website_id = cls.website.id
|
||||
|
||||
def test_embed_external_no_referer(self):
|
||||
""" When hitting the external URL without a referer header, the global embed record is
|
||||
incremented. """
|
||||
self.url_open(f'/slides/embed_external/{self.slide.id}')
|
||||
embed_views = self.env['slide.embed'].search([('slide_id', '=', self.slide.id)])
|
||||
self.assertEqual(len(embed_views), 1)
|
||||
self.assertEqual(embed_views.website_name, 'Unknown Website')
|
||||
|
||||
def test_embed_external_referer(self):
|
||||
""" When hitting the external URL with a referer header, the embed record is incremented
|
||||
based on the referer URL. """
|
||||
|
||||
self.assertFalse(bool(self.env['slide.embed'].search([
|
||||
('slide_id', '=', self.slide.id)
|
||||
])))
|
||||
|
||||
self.url_open(
|
||||
f'/slides/embed_external/{self.slide.id}',
|
||||
headers={'Referer': 'https://someexternalwebsite.com'}
|
||||
)
|
||||
|
||||
embed_views = self.env['slide.embed'].search([('slide_id', '=', self.slide.id)])
|
||||
self.assertEqual(len(embed_views), 1)
|
||||
self.assertEqual(embed_views.count_views, 1)
|
||||
self.assertEqual(embed_views.website_name, 'https://someexternalwebsite.com')
|
||||
|
||||
def test_embed_not_external(self):
|
||||
""" When hitting the non-external URL, we should not add a slide_embed record. """
|
||||
self.url_open(f'/slides/embed/{self.slide.id}')
|
||||
self.assertFalse(bool(self.env['slide.embed'].search([
|
||||
('slide_id', '=', self.slide.id)
|
||||
])))
|
||||
@@ -0,0 +1,33 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<data>
|
||||
<record id="slide_embed_view_tree" model="ir.ui.view">
|
||||
<field name="name">slide.embed.view.tree</field>
|
||||
<field name="model">slide.embed</field>
|
||||
<field name="arch" type="xml">
|
||||
<tree string="Embed Views" create="0" edit="0">
|
||||
<field name="website_name" string="External Website"/>
|
||||
<field name="count_views"/>
|
||||
<field name="slide_id" string="Content" optional="hide"/>
|
||||
</tree>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="slide_embed_view_search" model="ir.ui.view">
|
||||
<field name="name">slide.embed.view.search</field>
|
||||
<field name="model">slide.embed</field>
|
||||
<field name="arch" type="xml">
|
||||
<search>
|
||||
<field name="slide_id" string="Content"/>
|
||||
</search>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="slide_embed_action" model="ir.actions.act_window">
|
||||
<field name="name">Embed Views</field>
|
||||
<field name="type">ir.actions.act_window</field>
|
||||
<field name="res_model">slide.embed</field>
|
||||
<field name="view_mode">tree,search</field>
|
||||
</record>
|
||||
</data>
|
||||
</odoo>
|
||||
@@ -87,6 +87,13 @@
|
||||
<field class="ml-1" name="comments_count" widget="statinfo" nolabel="1"/> Comments
|
||||
</span>
|
||||
</button>
|
||||
<button name="action_view_embeds" class="oe_stat_button" type="object" icon="fa-share-alt"
|
||||
attrs="{'invisible': [('embed_count', '=', 0)]}">
|
||||
<div class="o_stat_info">
|
||||
<field name="embed_count"/>
|
||||
<span class="o_stat_text">Embed Views</span>
|
||||
</div>
|
||||
</button>
|
||||
<field name="is_published" class="ml-1" widget="website_redirect_button"
|
||||
attrs="{'invisible': ['|',('is_category', '=', True), ('channel_id', '=', False)]}"/>
|
||||
</div>
|
||||
|
||||
@@ -552,9 +552,12 @@
|
||||
<div class="oe_slide_js_embed_code_widget mt-4">
|
||||
<h5 class="mt0">Embed in your website</h5>
|
||||
<div class="form-group">
|
||||
<textarea class="form-control slide_embed_code" readonly="readonly" onClick="this.select();"><t t-esc="slide.embed_code"/></textarea>
|
||||
<textarea class="form-control slide_embed_code" readonly="readonly" onClick="this.select();">
|
||||
<!-- Use the external embedding URL here, see python controller '_slide_embed' method for more details. -->
|
||||
<t t-esc="slide.embed_code_external"/>
|
||||
</textarea>
|
||||
</div>
|
||||
<div class="form-group d-flex" t-if="slide.slide_type in ('presentation', 'document')">
|
||||
<div t-if="slide.slide_type in ('presentation', 'document') and not embed_hide_starting_page" class="form-group d-flex">
|
||||
<div class="form-text p-0 col-xs-5 col-sm-5 col-md-5 col-lg-5">Select page to start with</div>
|
||||
<div class="input-group col-xs-3 col-sm-2 col-md-2 col-lg-3">
|
||||
<input type="number" value="1" class="form-control"/>
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
<body>
|
||||
<div id="PDFViewer" class="o_wslides_fs_pdf_viewer d-flex flex-column h-100">
|
||||
<!-- PDF Viewer Header : contains the name, and the share links -->
|
||||
<div t-if="is_embedded" class="oe_slides_share_bar">
|
||||
<div t-if="is_external_embed" class="oe_slides_share_bar">
|
||||
<div class="container-fluid">
|
||||
<div class="row align-items-center">
|
||||
<div class="col">
|
||||
@@ -28,9 +28,10 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="col flex-grow-0 d-flex text-nowrap small">
|
||||
<a href="#" class="oe_slide_js_embed_option_link" data-slide-option-id="#slide_share"><i class="fa fa-share-alt"/> Share</a>
|
||||
<a href="#" class="oe_slide_js_embed_option_link mx-4" data-slide-option-id="#slide_email"><i class="fa fa-envelope"/> Email</a>
|
||||
<a href="#" class="oe_slide_js_embed_option_link" data-slide-option-id="#slide_embed"><i class="fa fa-code"/> Embed</a>
|
||||
<a class="oe_slide_js_embed_option_link" href="#" data-toggle="modal" t-attf-data-target="#slideChannelShareModal_{{slide.id}}">
|
||||
<i class="fa fa-share-alt" aria-label="Share" title="Share"/>
|
||||
Share
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -41,19 +42,13 @@
|
||||
t-attf-data-slideurl="/slides/slide/#{slug(slide)}/pdf_content"
|
||||
t-attf-data-downloadable="#{False}"
|
||||
t-att-data-defaultpage="page">
|
||||
<t t-if="is_embedded">
|
||||
<div id="slide_share" class="oe_slide_embed_option" style="display: none;">
|
||||
<t t-call="website_slides.slide_share_modal">
|
||||
<t t-set="record" t-value="slide"/>
|
||||
<t t-set="website_url" t-value="slide.channel_id.website_url"/>
|
||||
</t>
|
||||
</div>
|
||||
<div id="slide_email" class="oe_slide_embed_option" style="display: none;">
|
||||
<t t-call="website_slides.slide_social_email"/>
|
||||
</div>
|
||||
<div id="slide_embed" class="oe_slide_embed_option" style="display: none;">
|
||||
<t t-call="website_slides.slide_social_embed"/>
|
||||
</div>
|
||||
<t t-if="is_external_embed">
|
||||
<t t-call="website_slides.slide_share_modal">
|
||||
<t t-set="record" t-value="slide"/>
|
||||
<t t-set="website_url" t-value="slide.channel_id.website_url"/>
|
||||
<t t-set="include_embed" t-value="True"/>
|
||||
<t t-set="embed_hide_starting_page" t-value="True"/>
|
||||
</t>
|
||||
</t>
|
||||
<div id="slide_suggest" class="oe_slide_embed_option bg-300 container-fluid overflow-auto d-none">
|
||||
<div class="row">
|
||||
|
||||
@@ -4,9 +4,18 @@
|
||||
<!-- Share on social networks -->
|
||||
<template id='slide_share_social' name="Slides Media Share">
|
||||
<div class="btn-group" role="group">
|
||||
<a t-attf-href="https://www.facebook.com/sharer/sharer.php?u=#{record.website_url}" class="btn border bg-white o_wslides_js_social_share" social-key="facebook" aria-label="Share on Facebook" title="Share on Facebook"><i class="fa fa-facebook-square fa-fw"/></a>
|
||||
<a t-attf-href="https://twitter.com/intent/tweet?text=#{record.name}&url=#{record.website_url}" class="btn border bg-white o_wslides_js_social_share" social-key="twitter" aria-label="Share on Twitter" title="Share on Twitter"><i class="fa fa-twitter fa-fw"/></a>
|
||||
<a t-attf-href="http://www.linkedin.com/sharing/share-offsite/?url=#{record.website_url}" social-key="linkedin" class="btn border bg-white o_wslides_js_social_share" aria-label="Share on LinkedIn" title="Share on LinkedIn"><i class="fa fa-linkedin fa-fw"/></a>
|
||||
<a t-attf-href="https://www.facebook.com/sharer/sharer.php?u=#{record.website_url}" class="btn border bg-white o_wslides_js_social_share"
|
||||
social-key="facebook" aria-label="Share on Facebook" title="Share on Facebook" target="_blank">
|
||||
<i class="fa fa-facebook-square fa-fw"/>
|
||||
</a>
|
||||
<a t-attf-href="https://twitter.com/intent/tweet?text=#{record.name}&url=#{record.website_url}" class="btn border bg-white o_wslides_js_social_share"
|
||||
social-key="twitter" aria-label="Share on Twitter" title="Share on Twitter" target="_blank">
|
||||
<i class="fa fa-twitter fa-fw"/>
|
||||
</a>
|
||||
<a t-attf-href="http://www.linkedin.com/sharing/share-offsite/?url=#{record.website_url}" class="btn border bg-white o_wslides_js_social_share"
|
||||
social-key="linkedin" aria-label="Share on LinkedIn" title="Share on LinkedIn" target="_blank">
|
||||
<i class="fa fa-linkedin fa-fw"/>
|
||||
</a>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -41,11 +50,12 @@
|
||||
<h5>Share on Social Networks</h5>
|
||||
<t t-call="website_slides.slide_share_social"/>
|
||||
<t t-call="website_slides.slide_share_link"/>
|
||||
<t t-if="include_embed" t-call="website_slides.slide_social_embed"/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template id="slide_share_link">
|
||||
<h5 class="mt16">Share Link</h5>
|
||||
<h5 class="mt-3">Share Link</h5>
|
||||
<div class="input-group">
|
||||
<input type="text" t-att-id="'wslides_share_link_id_%s' % record.id" class="form-control o_wslides_js_share_link" readonly="readonly" onclick="this.select();"
|
||||
t-att-value="website_url"/>
|
||||
|
||||
Reference in New Issue
Block a user