From 60ba045b755efa390f647150e317946ef68f3579 Mon Sep 17 00:00:00 2001 From: Peter Bittner Date: Fri, 4 Jul 2014 09:11:20 +0200 Subject: [PATCH 1/3] Ignore PyCharm and Geany IDE project files --- .gitignore | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.gitignore b/.gitignore index 8472c41..152f7fd 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,5 @@ +/*.geany +/.idea /.tox /build From e49da10e0105bc977b055c9d712f18b923d39938 Mon Sep 17 00:00:00 2001 From: Peter Bittner Date: Fri, 4 Jul 2014 21:31:46 +0200 Subject: [PATCH 2/3] Added Piwik open source web analytics --- README.rst | 2 + analytical/templatetags/analytical.py | 12 +++- analytical/templatetags/piwik.py | 78 ++++++++++++++++++++ docs/install.rst | 5 ++ docs/services/piwik.rst | 100 ++++++++++++++++++++++++++ 5 files changed, 194 insertions(+), 3 deletions(-) create mode 100644 analytical/templatetags/piwik.py create mode 100644 docs/services/piwik.rst diff --git a/README.rst b/README.rst index a690b02..67833ca 100644 --- a/README.rst +++ b/README.rst @@ -34,6 +34,7 @@ Currently supported services: * `Olark`_ visitor chat * `Optimizely`_ A/B testing * `Performable`_ web analytics and landing pages +* `Piwik`_ open source web analytics * `Reinvigorate`_ visitor tracking * `SnapEngage`_ live chat * `Spring Metrics`_ conversion tracking @@ -67,6 +68,7 @@ an issue to discuss your plans. .. _`Olark`: http://www.olark.com/ .. _`Optimizely`: http://www.optimizely.com/ .. _`Performable`: http://www.performable.com/ +.. _`Piwik`: http://www.piwik.org/ .. _`Reinvigorate`: http://www.reinvigorate.net/ .. _`SnapEngage`: http://www.snapengage.com/ .. _`Spring Metrics`: http://www.springmetrics.com/ diff --git a/analytical/templatetags/analytical.py b/analytical/templatetags/analytical.py index d0cb1fc..e04ec16 100644 --- a/analytical/templatetags/analytical.py +++ b/analytical/templatetags/analytical.py @@ -30,6 +30,7 @@ TAG_MODULES = [ 'analytical.olark', 'analytical.optimizely', 'analytical.performable', + 'analytical.piwik', 'analytical.reinvigorate', 'analytical.snapengage', 'analytical.spring_metrics', @@ -37,7 +38,6 @@ TAG_MODULES = [ 'analytical.woopra', ] - logger = logging.getLogger(__name__) register = template.Library() @@ -48,8 +48,10 @@ def _location_tag(location): if len(bits) > 1: raise TemplateSyntaxError("'%s' tag takes no arguments" % bits[0]) return AnalyticalNode(location) + return analytical_tag + for loc in TAG_LOCATIONS: register.tag('analytical_%s' % loc, _location_tag(loc)) @@ -64,9 +66,11 @@ class AnalyticalNode(Node): def _load_template_nodes(): template_nodes = dict((l, dict((p, []) for p in TAG_POSITIONS)) - for l in TAG_LOCATIONS) + for l in TAG_LOCATIONS) + def add_node_cls(location, node, position=None): template_nodes[location][position].append(node) + for path in TAG_MODULES: module = _import_tag_module(path) try: @@ -75,11 +79,13 @@ def _load_template_nodes(): logger.debug("not loading tags from '%s': %s", path, e) for location in TAG_LOCATIONS: template_nodes[location] = sum((template_nodes[location][p] - for p in TAG_POSITIONS), []) + for p in TAG_POSITIONS), []) return template_nodes + def _import_tag_module(path): app_name, lib_name = path.rsplit('.', 1) return import_module("%s.templatetags.%s" % (app_name, lib_name)) + template_nodes = _load_template_nodes() diff --git a/analytical/templatetags/piwik.py b/analytical/templatetags/piwik.py new file mode 100644 index 0000000..11b5478 --- /dev/null +++ b/analytical/templatetags/piwik.py @@ -0,0 +1,78 @@ +""" +Piwik template tags and filters. +""" + +from __future__ import absolute_import + +import re + +from django.template import Library, Node, TemplateSyntaxError + +from analytical.utils import is_internal_ip, disable_html, get_required_setting + + +# domain name (characters separated by a dot), optional URI path, no slash +DOMAINPATH_RE = re.compile(r'^(([^./?#@:]+\.)+[^./?#@:]+)+(/[^/?#@:]+)*$') + +# numeric ID +SITEID_RE = re.compile(r'^\d+$') + +TRACKING_CODE = """ + + +""" # noqa + + +register = Library() + + +@register.tag +def piwik(parser, token): + """ + Piwik tracking template tag. + + Renders Javascript code to track page visits. You must supply + your Piwik domain (plus optional URI path), and tracked site ID + in the ``PIWIK_DOMAIN_PATH`` and the ``PIWIK_SITE_ID`` setting. + """ + bits = token.split_contents() + if len(bits) > 1: + raise TemplateSyntaxError("'%s' takes no arguments" % bits[0]) + return PiwikNode() + + +class PiwikNode(Node): + def __init__(self): + self.domain_path = \ + get_required_setting('PIWIK_DOMAIN_PATH', DOMAINPATH_RE, + "must be a domain name, optionally followed " + "by an URI path, no trailing slash (e.g. " + "piwik.example.com or my.piwik.server/path)") + self.site_id = \ + get_required_setting('PIWIK_SITE_ID', SITEID_RE, + "must be a (string containing a) number") + + def render(self, context): + html = TRACKING_CODE % { + 'url': self.domain_path, + 'siteid': self.site_id, + } + if is_internal_ip(context, 'PIWIK'): + html = disable_html(html, 'Piwik') + return html + + +def contribute_to_analytical(add_node): + PiwikNode() # ensure properly configured + add_node('body_bottom', PiwikNode) diff --git a/docs/install.rst b/docs/install.rst index 27716ad..b29fec3 100644 --- a/docs/install.rst +++ b/docs/install.rst @@ -157,6 +157,11 @@ settings required to enable each service are listed here: PERFORMABLE_API_KEY = '123abc' +* :doc:`Piwik `:: + + PIWIK_DOMAIN_PATH = 'your.piwik.server/optional/path' + PIWIK_SITE_ID = '123' + * :doc:`Reinvigorate `:: REINVIGORATE_TRACKING_ID = '12345-abcdefghij' diff --git a/docs/services/piwik.rst b/docs/services/piwik.rst new file mode 100644 index 0000000..41c6361 --- /dev/null +++ b/docs/services/piwik.rst @@ -0,0 +1,100 @@ +================================== +Piwik -- open source web analytics +================================== + +Piwik_ is an open analytics platform currently used by individuals, +companies and governments all over the world. With Piwik, your data +will always be yours, because you run your own analytics server. + +.. _Piwik: http://www.piwik.org/ + + +Installation +============ + +To start using the Piwik 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 Piwik 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:`piwik-configuration`. + +The Piwik tracking code is inserted into templates using a template +tag. Load the :mod:`piwik` template tag library and insert the +:ttag:`piwik` tag. Because every page that you want to track must +have the tag, it is useful to add it to your base template. Insert +the tag at the bottom of the HTML body as recommended by the +`Piwik best practice for Integration Plugins`_:: + + {% load piwik %} + ... + {% piwik %} + + + +.. _`Piwik best practice for integration`: http://piwik.org/integrate/how-to/ + + + +.. _piwik-configuration: + +Configuration +============= + +Before you can use the Piwik integration, you must first define +domain name and optional URI path to your Piwik server, as well as +the Piwik ID of the website you're tracking with your Piwik server, +in your project settings. + + +Setting the domain +------------------ + +Your Django project needs to know where your Piwik server is located. +Typically, you'll have Piwik installed on a subdomain of its own +(e.g. ``piwik.example.com``), otherwise it runs in a subdirectory of +a website of yours (e.g. ``www.example.com/piwik``). Set +:const:`PIWIK_DOMAIN_PATH` in the project :file:`settings.py` file +accordingly:: + + PIWIK_DOMAIN_PATH = 'piwik.example.com' + +If you do not set a domain the tracking code will not be rendered. + + +Setting the site ID +------------------- + +Your Piwik server can track several websites. Each website has its +site ID (this is the ``idSite`` parameter in the query string of your +browser's address bar when you visit the Piwik Dashboard). Set +:const:`PIWIK_SITE_ID` in the project :file:`settings.py` file to +the value corresponding to the website your tracking:: + + PIWIK_SITE_ID = '4' + +If you do not set the site ID the tracking code will not be rendered. + + +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:`ANALYTICAL_INTERNAL_IPS` by +default (which takes the value of :const:`INTERNAL_IPS` by default). +See :ref:`identifying-visitors` for important information about +detecting the visitor IP address. + + +---- + +Thanks go to Piwik for providing an excellent web analytics platform +entirely for free! Consider donating_ to ensure that they continue +their development efforts in the spirit of open source and freedom +for our personal data. + +.. _donating: http://piwik.org/donate/ From 6cf24c47099fdbd52403c95bf7262e228a1c5a4b Mon Sep 17 00:00:00 2001 From: Peter Bittner Date: Sat, 5 Jul 2014 00:23:03 +0200 Subject: [PATCH 3/3] Added tests for Piwik --- analytical/tests/__init__.py | 1 + analytical/tests/test_tag_piwik.py | 69 ++++++++++++++++++++++++++++++ 2 files changed, 70 insertions(+) create mode 100644 analytical/tests/test_tag_piwik.py diff --git a/analytical/tests/__init__.py b/analytical/tests/__init__.py index 13f7b5f..3c4bca8 100644 --- a/analytical/tests/__init__.py +++ b/analytical/tests/__init__.py @@ -18,6 +18,7 @@ from analytical.tests.test_tag_mixpanel import * from analytical.tests.test_tag_olark import * from analytical.tests.test_tag_optimizely import * from analytical.tests.test_tag_performable import * +from analytical.tests.test_tag_piwik import * from analytical.tests.test_tag_reinvigorate import * from analytical.tests.test_tag_snapengage import * from analytical.tests.test_tag_spring_metrics import * diff --git a/analytical/tests/test_tag_piwik.py b/analytical/tests/test_tag_piwik.py new file mode 100644 index 0000000..e06a91c --- /dev/null +++ b/analytical/tests/test_tag_piwik.py @@ -0,0 +1,69 @@ +""" +Tests for the Piwik template tags and filters. +""" + +from django.http import HttpRequest +from django.template import Context + +from analytical.templatetags.piwik import PiwikNode +from analytical.tests.utils import TagTestCase, override_settings, \ + SETTING_DELETED +from analytical.utils import AnalyticalException + + +@override_settings(PIWIK_DOMAIN_PATH='example.com', PIWIK_SITE_ID='345') +class PiwikTagTestCase(TagTestCase): + """ + Tests for the ``piwik`` template tag. + """ + + def test_tag(self): + r = self.render_tag('piwik', 'piwik') + self.assertTrue(' ? "https" : "http") + "://example.com/";' in r, r) + self.assertTrue("_paq.push(['setSiteId', 345]);" in r, r) + self.assertTrue('img src="http://example.com/piwik.php?idsite=345"' + in r, r) + + def test_node(self): + r = PiwikNode().render(Context({})) + self.assertTrue(' ? "https" : "http") + "://example.com/";' in r, r) + self.assertTrue("_paq.push(['setSiteId', 345]);" in r, r) + self.assertTrue('img src="http://example.com/piwik.php?idsite=345"' + in r, r) + + @override_settings(PIWIK_DOMAIN_PATH='example.com/piwik', + PIWIK_SITE_ID='345') + def test_domain_path_valid(self): + r = self.render_tag('piwik', 'piwik') + self.assertTrue(' ? "https" : "http") + "://example.com/piwik/";' in r, + r) + + @override_settings(PIWIK_DOMAIN_PATH=SETTING_DELETED) + def test_no_domain(self): + self.assertRaises(AnalyticalException, PiwikNode) + + @override_settings(PIWIK_SITE_ID=SETTING_DELETED) + def test_no_siteid(self): + self.assertRaises(AnalyticalException, PiwikNode) + + @override_settings(PIWIK_SITE_ID='x') + def test_siteid_not_a_number(self): + self.assertRaises(AnalyticalException, PiwikNode) + + @override_settings(PIWIK_DOMAIN_PATH='http://www.example.com') + def test_domain_protocol_invalid(self): + self.assertRaises(AnalyticalException, PiwikNode) + + @override_settings(PIWIK_DOMAIN_PATH='example.com/') + def test_domain_slash_invalid(self): + self.assertRaises(AnalyticalException, PiwikNode) + + @override_settings(ANALYTICAL_INTERNAL_IPS=['1.1.1.1']) + def test_render_internal_ip(self): + req = HttpRequest() + req.META['REMOTE_ADDR'] = '1.1.1.1' + context = Context({'request': req}) + r = PiwikNode().render(context) + self.assertTrue(r.startswith( + ''), r)