From cd9394467e0149a5b0e1be2e1b68019cb0105f53 Mon Sep 17 00:00:00 2001 From: Pi Delport Date: Tue, 17 Oct 2017 16:00:54 +0200 Subject: [PATCH 1/5] Add initial Facebook Pixel support --- analytical/templatetags/analytical.py | 1 + analytical/templatetags/facebook_pixel.py | 97 +++++++++++++++++++++ analytical/tests/test_tag_facebook_pixel.py | 87 ++++++++++++++++++ 3 files changed, 185 insertions(+) create mode 100644 analytical/templatetags/facebook_pixel.py create mode 100644 analytical/tests/test_tag_facebook_pixel.py diff --git a/analytical/templatetags/analytical.py b/analytical/templatetags/analytical.py index ad4fba7..7851268 100644 --- a/analytical/templatetags/analytical.py +++ b/analytical/templatetags/analytical.py @@ -20,6 +20,7 @@ TAG_MODULES = [ 'analytical.clickmap', 'analytical.clicky', 'analytical.crazy_egg', + 'analytical.facebook_pixel', 'analytical.gauges', 'analytical.google_analytics', 'analytical.gosquared', diff --git a/analytical/templatetags/facebook_pixel.py b/analytical/templatetags/facebook_pixel.py new file mode 100644 index 0000000..c855784 --- /dev/null +++ b/analytical/templatetags/facebook_pixel.py @@ -0,0 +1,97 @@ +""" +Facebook Pixel template tags and filters. +""" +from __future__ import absolute_import + +import re + +from django.template import Library, Node, TemplateSyntaxError + +from analytical.utils import get_required_setting, is_internal_ip, disable_html + + +FACEBOOK_PIXEL_HEAD_CODE = """\ + +""" + +FACEBOOK_PIXEL_BODY_CODE = """\ + +""" + +register = Library() + + +def _validate_no_args(token): + bits = token.split_contents() + if len(bits) > 1: + raise TemplateSyntaxError("'%s' takes no arguments" % bits[0]) + + +@register.tag +def facebook_pixel_head(parser, token): + """ + Facebook Pixel head template tag. + """ + _validate_no_args(token) + return FacebookPixelHeadNode() + + +@register.tag +def facebook_pixel_body(parser, token): + """ + Facebook Pixel body template tag. + """ + _validate_no_args(token) + return FacebookPixelBodyNode() + + +class _FacebookPixelNode(Node): + """ + Base class: override and provide code_template. + """ + def __init__(self): + self.pixel_id = get_required_setting( + 'FACEBOOK_PIXEL_ID', + re.compile(r'^\d+$'), + "must be (a string containing) a number", + ) + + def render(self, context): + html = self.code_template % {'FACEBOOK_PIXEL_ID': self.pixel_id} + if is_internal_ip(context, 'FACEBOOK_PIXEL'): + return disable_html(html, 'Facebook Pixel') + else: + return html + + @property + def code_template(self): + raise NotImplementedError + + +class FacebookPixelHeadNode(_FacebookPixelNode): + code_template = FACEBOOK_PIXEL_HEAD_CODE + + +class FacebookPixelBodyNode(_FacebookPixelNode): + code_template = FACEBOOK_PIXEL_BODY_CODE + + +def contribute_to_analytical(add_node): + # ensure properly configured + FacebookPixelHeadNode() + FacebookPixelBodyNode() + add_node('head_bottom', FacebookPixelHeadNode) + add_node('body_bottom', FacebookPixelBodyNode) diff --git a/analytical/tests/test_tag_facebook_pixel.py b/analytical/tests/test_tag_facebook_pixel.py new file mode 100644 index 0000000..373c603 --- /dev/null +++ b/analytical/tests/test_tag_facebook_pixel.py @@ -0,0 +1,87 @@ +""" +Tests for the Facebook Pixel template tags. +""" +from django.http import HttpRequest +from django.template import Context +from django.test import override_settings + +from analytical.templatetags.facebook_pixel import FacebookPixelHeadNode, FacebookPixelBodyNode +from analytical.tests.utils import TagTestCase +from analytical.utils import AnalyticalException + + +expected_head_html = """\ + +""" + + +expected_body_html = """\ + +""" + + +@override_settings(FACEBOOK_PIXEL_ID='1234567890') +class FacebookPixelTagTestCase(TagTestCase): + + maxDiff = None + + def test_head_tag(self): + html = self.render_tag('facebook_pixel', 'facebook_pixel_head') + self.assertEqual(expected_head_html, html) + + def test_head_node(self): + html = FacebookPixelHeadNode().render(Context({})) + self.assertEqual(expected_head_html, html) + + def test_body_tag(self): + html = self.render_tag('facebook_pixel', 'facebook_pixel_body') + self.assertEqual(expected_body_html, html) + + def test_body_node(self): + html = FacebookPixelBodyNode().render(Context({})) + self.assertEqual(expected_body_html, html) + + @override_settings(FACEBOOK_PIXEL_ID=None) + def test_no_id(self): + expected_pattern = r'^FACEBOOK_PIXEL_ID setting is not set$' + self.assertRaisesRegexp(AnalyticalException, expected_pattern, FacebookPixelHeadNode) + self.assertRaisesRegexp(AnalyticalException, expected_pattern, FacebookPixelBodyNode) + + @override_settings(FACEBOOK_PIXEL_ID='invalid') + def test_invalid_id(self): + expected_pattern = ( + r"^FACEBOOK_PIXEL_ID setting: must be \(a string containing\) a number: 'invalid'$") + self.assertRaisesRegexp(AnalyticalException, expected_pattern, FacebookPixelHeadNode) + self.assertRaisesRegexp(AnalyticalException, expected_pattern, FacebookPixelBodyNode) + + @override_settings(ANALYTICAL_INTERNAL_IPS=['1.1.1.1']) + def test_render_internal_ip(self): + request = HttpRequest() + request.META['REMOTE_ADDR'] = '1.1.1.1' + context = Context({'request': request}) + + def _disabled(html): + return '\n'.join([ + '', + ]) + + head_html = FacebookPixelHeadNode().render(context) + self.assertEqual(_disabled(expected_head_html), head_html) + + body_html = FacebookPixelBodyNode().render(context) + self.assertEqual(_disabled(expected_body_html), body_html) From 70b2ff90817b01f8519261299e9e3f0463d6b864 Mon Sep 17 00:00:00 2001 From: Pi Delport Date: Tue, 17 Oct 2017 16:07:40 +0200 Subject: [PATCH 2/5] README: Add Facebook Pixel --- README.rst | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.rst b/README.rst index 3fd5d4b..174d97a 100644 --- a/README.rst +++ b/README.rst @@ -53,6 +53,7 @@ Currently Supported Services * `Clickmap`_ visual click tracking * `Clicky`_ traffic analysis * `Crazy Egg`_ visual click tracking +* `Facebook Pixel`_ advertising analytics * `Gaug.es`_ real time web analytics * `Google Analytics`_ traffic analysis * `GoSquared`_ traffic monitoring @@ -76,6 +77,7 @@ Currently Supported Services .. _`Clickmap`: http://getclickmap.com/ .. _`Clicky`: http://getclicky.com/ .. _`Crazy Egg`: http://www.crazyegg.com/ +.. _`Facebook Pixel`: https://developers.facebook.com/docs/facebook-pixel/ .. _`Gaug.es`: http://get.gaug.es/ .. _`Google Analytics`: http://www.google.com/analytics/ .. _`GoSquared`: http://www.gosquared.com/ From a7246fbe4494e2a15d139a67e15f4abf5d977e1a Mon Sep 17 00:00:00 2001 From: Pi Delport Date: Tue, 17 Oct 2017 16:46:14 +0200 Subject: [PATCH 3/5] Add initial Facebook Pixel docs --- docs/install.rst | 4 ++ docs/services/facebook_pixel.rst | 84 ++++++++++++++++++++++++++++++++ 2 files changed, 88 insertions(+) create mode 100644 docs/services/facebook_pixel.rst diff --git a/docs/install.rst b/docs/install.rst index defae2b..a223025 100644 --- a/docs/install.rst +++ b/docs/install.rst @@ -125,6 +125,10 @@ settings required to enable each service are listed here: CRAZY_EGG_ACCOUNT_NUMBER = '12345678' +* :doc:`Facebook Pixel `:: + + FACEBOOK_PIXEL_ID = '1234567890' + * :doc:`Gaug.es `:: GAUGES_SITE_ID = '0123456789abcdef0123456789abcdef' diff --git a/docs/services/facebook_pixel.rst b/docs/services/facebook_pixel.rst new file mode 100644 index 0000000..cfe4d21 --- /dev/null +++ b/docs/services/facebook_pixel.rst @@ -0,0 +1,84 @@ +======================================= +Facebook Pixel -- advertising analytics +======================================= + +`Facebook Pixel`_ is Facebook's tool for conversion tracking, optimisation and remarketing. + +.. _`Facebook Pixel`: https://developers.facebook.com/docs/facebook-pixel/ + + +.. facebook-pixel-installation: + +Installation +============ + +To start using the Facebook Pixel integration, you must have installed the +django-analytical package and have added the ``analytical`` application +to :const:`INSTALLED_APPS` in your project :file:`settings.py` file. +See :doc:`../install` for details. + +Next you need to add the Facebook Pixel template tag to your templates. +This step is only needed if you are not using the generic +:ttag:`analytical.*` tags. If you are, skip to +:ref:`facebook-pixel-configuration`. + +The Facebook Pixel code is inserted into templates using template tags. +Because every page that you want to track must have the tag, +it is useful to add it to your base template. +At the top of the template, load the :mod:`facebook_pixel` template tag library. +Then insert the :ttag:`facebook_pixel_head` tag at the bottom of the head section, +and optionally insert the :ttag:`facebook_pixel_body` tag at the bottom of the body section:: + + {% load facebook_pixel %} + + + ... + {% facebook_pixel_head %} + + + ... + {% facebook_pixel_body %} + + + +.. note:: + The :ttag:`facebook_pixel_body` tag code will only be used for browsers with JavaScript disabled. + It can be omitted if you don't need to support them. + + +.. _facebook-pixel-configuration: + +Configuration +============= + +Before you can use the Facebook Pixel integration, +you must first set your Pixel ID. + + +.. _facebook-pixel-id: + +Setting the Pixel ID +-------------------- + +Each Facebook Adverts account you have can have a Pixel ID, +and the :mod:`facebook_pixel` tags will include it in the rendered page. +You can find the Pixel ID on the "Pixels" section of your Facebook Adverts account. +Set :const:`FACEBOOK_PIXEL_ID` in the project :file:`settings.py` file:: + + FACEBOOK_PIXEL_ID = 'XXXXXXXXXX' + +If you do not set a Pixel ID, the code will not be rendered. + + +.. _facebook-pixel-internal-ips: + +Internal IP addresses +--------------------- + +Usually you do not want to track clicks from your development or +internal IP addresses. By default, if the tags detect that the client +comes from any address in the :const:`FACEBOOK_PIXEL_INTERNAL_IPS` +setting, the tracking code is commented out. It takes the value of +:const:`ANALYTICAL_INTERNAL_IPS` by default (which in turn is +:const:`INTERNAL_IPS` by default). See :ref:`identifying-visitors` for +important information about detecting the visitor IP address. From be564dd2b8a7bc333b1d3f315bc98a975dd37b6d Mon Sep 17 00:00:00 2001 From: Pi Delport Date: Wed, 18 Oct 2017 12:34:22 +0200 Subject: [PATCH 4/5] (Ignore coverage for intentionally unimplemented property) --- analytical/templatetags/facebook_pixel.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/analytical/templatetags/facebook_pixel.py b/analytical/templatetags/facebook_pixel.py index c855784..33dac0a 100644 --- a/analytical/templatetags/facebook_pixel.py +++ b/analytical/templatetags/facebook_pixel.py @@ -78,7 +78,7 @@ class _FacebookPixelNode(Node): @property def code_template(self): - raise NotImplementedError + raise NotImplementedError # pragma: no cover class FacebookPixelHeadNode(_FacebookPixelNode): From 2ea2f824e8c198b412c088801d453198a29e13a8 Mon Sep 17 00:00:00 2001 From: Pi Delport Date: Wed, 18 Oct 2017 12:36:18 +0200 Subject: [PATCH 5/5] Add some remaining test coverage This should bring the facebook_pixel module up to 100%. --- analytical/tests/test_tag_facebook_pixel.py | 29 ++++++++++++++++++++- 1 file changed, 28 insertions(+), 1 deletion(-) diff --git a/analytical/tests/test_tag_facebook_pixel.py b/analytical/tests/test_tag_facebook_pixel.py index 373c603..75d52aa 100644 --- a/analytical/tests/test_tag_facebook_pixel.py +++ b/analytical/tests/test_tag_facebook_pixel.py @@ -2,9 +2,10 @@ Tests for the Facebook Pixel template tags. """ from django.http import HttpRequest -from django.template import Context +from django.template import Context, Template, TemplateSyntaxError from django.test import override_settings +from analytical.templatetags.analytical import _load_template_nodes from analytical.templatetags.facebook_pixel import FacebookPixelHeadNode, FacebookPixelBodyNode from analytical.tests.utils import TagTestCase from analytical.utils import AnalyticalException @@ -54,6 +55,20 @@ class FacebookPixelTagTestCase(TagTestCase): html = FacebookPixelBodyNode().render(Context({})) self.assertEqual(expected_body_html, html) + def test_tags_take_no_args(self): + self.assertRaisesRegexp( + TemplateSyntaxError, + r"^'facebook_pixel_head' takes no arguments$", + lambda: (Template('{% load facebook_pixel %}{% facebook_pixel_head "arg" %}') + .render(Context({}))), + ) + self.assertRaisesRegexp( + TemplateSyntaxError, + r"^'facebook_pixel_body' takes no arguments$", + lambda: (Template('{% load facebook_pixel %}{% facebook_pixel_body "arg" %}') + .render(Context({}))), + ) + @override_settings(FACEBOOK_PIXEL_ID=None) def test_no_id(self): expected_pattern = r'^FACEBOOK_PIXEL_ID setting is not set$' @@ -85,3 +100,15 @@ class FacebookPixelTagTestCase(TagTestCase): body_html = FacebookPixelBodyNode().render(context) self.assertEqual(_disabled(expected_body_html), body_html) + + def test_contribute_to_analytical(self): + """ + `facebook_pixel.contribute_to_analytical` registers the head and body nodes. + """ + template_nodes = _load_template_nodes() + self.assertEqual({ + 'head_top': [], + 'head_bottom': [FacebookPixelHeadNode], + 'body_top': [], + 'body_bottom': [FacebookPixelBodyNode], + }, template_nodes)