diff --git a/addons/website_slides/__manifest__.py b/addons/website_slides/__manifest__.py index a201a750463..97677eab787 100644 --- a/addons/website_slides/__manifest__.py +++ b/addons/website_slides/__manifest__.py @@ -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', diff --git a/addons/website_slides/controllers/main.py b/addons/website_slides/controllers/main.py index b1c53ed86ab..a75dae3f142 100644 --- a/addons/website_slides/controllers/main.py +++ b/addons/website_slides/controllers/main.py @@ -1133,22 +1133,36 @@ class WebsiteSlides(WebsiteProfile): @http.route('/slides/embed/', 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/', 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 diff --git a/addons/website_slides/models/__init__.py b/addons/website_slides/models/__init__.py index 7b958fbdc78..335bfd8fb08 100644 --- a/addons/website_slides/models/__init__.py +++ b/addons/website_slides/models/__init__.py @@ -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 diff --git a/addons/website_slides/models/slide_embed.py b/addons/website_slides/models/slide_embed.py new file mode 100644 index 00000000000..c1b028bdbcc --- /dev/null +++ b/addons/website_slides/models/slide_embed.py @@ -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') diff --git a/addons/website_slides/models/slide_slide.py b/addons/website_slides/models/slide_slide.py index ed11dcf1116..8a70d94a409 100644 --- a/addons/website_slides/models/slide_slide.py +++ b/addons/website_slides/models/slide_slide.py @@ -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 = '' % (slide_url, 315, 420) + slide_url_external = base_url + url_for('/slides/embed_external/%s?page=1' % record.id) + base_embed_code = Markup('') + 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 = '' % (record.document_id, query) + record.embed_code = Markup('') % (record.document_id, query) else: # embed google doc video - record.embed_code = '' % (record.document_id) + record.embed_code = Markup('') % (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([ diff --git a/addons/website_slides/tests/__init__.py b/addons/website_slides/tests/__init__.py index 9b7fc83ac65..3810dd0fad5 100644 --- a/addons/website_slides/tests/__init__.py +++ b/addons/website_slides/tests/__init__.py @@ -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 diff --git a/addons/website_slides/tests/test_embed_detection.py b/addons/website_slides/tests/test_embed_detection.py new file mode 100644 index 00000000000..5c61ffaa00d --- /dev/null +++ b/addons/website_slides/tests/test_embed_detection.py @@ -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) + ]))) diff --git a/addons/website_slides/views/slide_embed_views.xml b/addons/website_slides/views/slide_embed_views.xml new file mode 100644 index 00000000000..28190922c6f --- /dev/null +++ b/addons/website_slides/views/slide_embed_views.xml @@ -0,0 +1,33 @@ + + + + + slide.embed.view.tree + slide.embed + + + + + + + + + + + slide.embed.view.search + slide.embed + + + + + + + + + Embed Views + ir.actions.act_window + slide.embed + tree,search + + + diff --git a/addons/website_slides/views/slide_slide_views.xml b/addons/website_slides/views/slide_slide_views.xml index b35169518f5..04290773296 100644 --- a/addons/website_slides/views/slide_slide_views.xml +++ b/addons/website_slides/views/slide_slide_views.xml @@ -87,6 +87,13 @@ Comments + diff --git a/addons/website_slides/views/website_slides_templates_lesson.xml b/addons/website_slides/views/website_slides_templates_lesson.xml index 5896f2c6520..883863c311a 100644 --- a/addons/website_slides/views/website_slides_templates_lesson.xml +++ b/addons/website_slides/views/website_slides_templates_lesson.xml @@ -552,9 +552,12 @@
Embed in your website
- +
-
+
Select page to start with
diff --git a/addons/website_slides/views/website_slides_templates_lesson_embed.xml b/addons/website_slides/views/website_slides_templates_lesson_embed.xml index b5edd67fe96..da6d018c82b 100644 --- a/addons/website_slides/views/website_slides_templates_lesson_embed.xml +++ b/addons/website_slides/views/website_slides_templates_lesson_embed.xml @@ -17,7 +17,7 @@
-
+
@@ -28,9 +28,10 @@
@@ -41,19 +42,13 @@ t-attf-data-slideurl="/slides/slide/#{slug(slide)}/pdf_content" t-attf-data-downloadable="#{False}" t-att-data-defaultpage="page"> - - - - + + + + + + +
diff --git a/addons/website_slides/views/website_slides_templates_utils.xml b/addons/website_slides/views/website_slides_templates_utils.xml index 0066097e032..885f0d9c056 100644 --- a/addons/website_slides/views/website_slides_templates_utils.xml +++ b/addons/website_slides/views/website_slides_templates_utils.xml @@ -4,9 +4,18 @@ @@ -41,11 +50,12 @@
Share on Social Networks
+