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 e4141799000..a75dae3f142 100644 --- a/addons/website_slides/controllers/main.py +++ b/addons/website_slides/controllers/main.py @@ -1153,12 +1153,12 @@ class WebsiteSlides(WebsiteProfile): try: slide = request.env['slide.slide'].browse(slide_id) + if not slide.exists(): + raise werkzeug.exceptions.NotFound() + referer_url = request.httprequest.headers.get('Referer', '') - if is_external_embed and referer_url: - request.env['slide.embed'].sudo()._add_embed_url( - slide.id, - referer_url - ) + if is_external_embed: + slide.sudo()._embed_increment(referer_url) values = self._get_slide_detail(slide) values['page'] = page diff --git a/addons/website_slides/models/slide_embed.py b/addons/website_slides/models/slide_embed.py index 7cbe5fbc56d..c1b028bdbcc 100644 --- a/addons/website_slides/models/slide_embed.py +++ b/addons/website_slides/models/slide_embed.py @@ -1,31 +1,21 @@ # -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. -from werkzeug import urls - -from odoo import fields, models +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 = 'slide_id' + _rec_name = 'website_name' slide_id = fields.Many2one('slide.slide', string="Presentation", required=True, index=True) - url = fields.Char('Third Party Website URL', required=True) + url = fields.Char('Third Party Website URL') + website_name = fields.Char('Website', compute='_compute_website_name') 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 + @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 dd4e697aefd..8a70d94a409 100644 --- a/addons/website_slides/models/slide_slide.py +++ b/addons/website_slides/models/slide_slide.py @@ -145,7 +145,8 @@ class Slide(models.Model): 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) @@ -251,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. @@ -492,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 @@ -656,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 +