diff --git a/.gitignore b/.gitignore
index 8472c41..152f7fd 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,3 +1,5 @@
+/*.geany
+/.idea
/.tox
/build
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/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)
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 %}
+