diff --git a/analytical/templatetags/analytical.py b/analytical/templatetags/analytical.py index 7ae606b..b424b23 100644 --- a/analytical/templatetags/analytical.py +++ b/analytical/templatetags/analytical.py @@ -25,6 +25,7 @@ TAG_MODULES = [ 'analytical.gosquared', 'analytical.heap', 'analytical.hotjar', + 'analytical.contentsquare', 'analytical.hubspot', 'analytical.intercom', 'analytical.kiss_insights', diff --git a/analytical/templatetags/contentsquare.py b/analytical/templatetags/contentsquare.py new file mode 100644 index 0000000..5caa442 --- /dev/null +++ b/analytical/templatetags/contentsquare.py @@ -0,0 +1,63 @@ +""" +Contentsquare template tags and filters. +""" + +import re + +from django.template import Library, Node, TemplateSyntaxError + +from analytical.utils import disable_html, get_required_setting, is_internal_ip + +CONTENTSQUARE_TRACKING_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 contentsquare(parser, token): + """ + Contentsquare template tag. + """ + _validate_no_args(token) + return ContentsquareNode() + + +class ContentsquareNode(Node): + + def __init__(self): + self.site_id = get_required_setting( + 'CONTENTSQUARE_SITE_ID', + re.compile(r'^\d+$'), + "must be (a string containing) a number", + ) + + def render(self, context): + html = CONTENTSQUARE_TRACKING_CODE % {'CONTENTSQUARE_SITE_ID': self.site_id} + if is_internal_ip(context, 'CONTENTSQUARE'): + return disable_html(html, 'Contentsquare') + else: + return html + + +def contribute_to_analytical(add_node): + # ensure properly configured + ContentsquareNode() + add_node('head_bottom', ContentsquareNode) diff --git a/docs/services/content_square.rst b/docs/services/content_square.rst new file mode 100644 index 0000000..ee0a4a8 --- /dev/null +++ b/docs/services/content_square.rst @@ -0,0 +1,74 @@ +===================================== +Contentsquare -- enterprise digital experience analytics +===================================== + +`Contentsquare`_ is an enterprise digital experience analytics platform that provides comprehensive insights into user behavior across web, mobile, and other digital touchpoints. + +.. _`Contentsquare`: https://contentsquare.com/ + + +.. contentsquare-installation: + +Installation +============ + +To start using the Contentsquare 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 Contentsquare 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:`contentsquare-configuration`. + +The Contentsquare 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:`contentsquare` template tag library. +Then insert the :ttag:`contentsquare` tag at the bottom of the head section:: + + {% load contentsquare %} + + + ... + {% contentsquare %} + + ... + + + +.. _contentsquare-configuration: + +Configuration +============= + +Before you can use the Contentsquare integration, you must first set your Site ID. + + +.. _contentsquare-id: + +Setting the Contentsquare Site ID +---------------------------------- + +You can find the Contentsquare Site ID in the "Sites & Organizations" section of your Contentsquare account. +Set :const:`CONTENTSQUARE_SITE_ID` in the project :file:`settings.py` file:: + + CONTENTSQUARE_SITE_ID = 'XXXXXXXXX' + +If you do not set a Contentsquare Site ID, the tracking code will not be rendered. + + +.. _contentsquare-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:`CONTENTSQUARE_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. + diff --git a/tests/unit/test_tag_content_square.py b/tests/unit/test_tag_content_square.py new file mode 100644 index 0000000..e104482 --- /dev/null +++ b/tests/unit/test_tag_content_square.py @@ -0,0 +1,81 @@ +""" +Tests for the Contentsquare template tags. +""" +import pytest +from django.http import HttpRequest +from django.template import Context, Template, TemplateSyntaxError +from django.test import override_settings +from utils import TagTestCase + +from analytical.templatetags.analytical import _load_template_nodes +from analytical.templatetags.contentsquare import ContentsquareNode +from analytical.utils import AnalyticalException + +expected_html = """\ + +""" + +@override_settings(CONTENTSQUARE_SITE_ID='123456789') +class ContentsquareTagTestCase(TagTestCase): + + maxDiff = None + + def test_tag(self): + html = self.render_tag('contentsquare', 'contentsquare') + assert expected_html == html + + def test_node(self): + html = ContentsquareNode().render(Context({})) + assert expected_html == html + + def test_tags_take_no_args(self): + with pytest.raises(TemplateSyntaxError, match="'contentsquare' takes no arguments"): + Template('{% load contentsquare %}{% contentsquare "arg" %}').render(Context({})) + + @override_settings(CONTENTSQUARE_SITE_ID=None) + def test_no_id(self): + with pytest.raises(AnalyticalException, match="CONTENTSQUARE_SITE_ID setting is not set"): + ContentsquareNode() + + @override_settings(CONTENTSQUARE_SITE_ID='invalid') + def test_invalid_id(self): + expected_pattern = ( + r"^CONTENTSQUARE_SITE_ID setting: must be \(a string containing\) a number: 'invalid'$") + with pytest.raises(AnalyticalException, match=expected_pattern): + ContentsquareNode() + + @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}) + + actual_html = ContentsquareNode().render(context) + disabled_html = '\n'.join([ + '', + ]) + assert disabled_html == actual_html + + def test_contribute_to_analytical(self): + """ + `contentsquare.contribute_to_analytical` registers the head and body nodes. + """ + template_nodes = _load_template_nodes() + assert template_nodes == { + 'head_top': [], + 'head_bottom': [ContentsquareNode], + 'body_top': [], + 'body_bottom': [], + }