diff --git a/CHANGELOG.rst b/CHANGELOG.rst
index 3110e74..b970b9d 100644
--- a/CHANGELOG.rst
+++ b/CHANGELOG.rst
@@ -1,9 +1,10 @@
Version 0.7.0
-------------
-* Fixed Crazy Egg template ``
+"""
+
+
+register = Library()
+
+
+@register.tag
+def woopra(parser, token):
+ """
+ Woopra tracking template tag.
+
+ Renders Javascript code to track page visits. You must supply
+ your Woopra domain in the ``WOOPRA_DOMAIN`` setting.
+ """
+ bits = token.split_contents()
+ if len(bits) > 1:
+ raise TemplateSyntaxError("'%s' takes no arguments" % bits[0])
+ return WoopraNode()
+
+class WoopraNode(Node):
+ def __init__(self):
+ self.domain = get_required_setting('WOOPRA_DOMAIN', DOMAIN_RE,
+ "must be a domain name")
+
+ def render(self, context):
+ settings = self._get_settings(context)
+ visitor = self._get_visitor(context)
+
+ html = TRACKING_CODE % {
+ 'settings': simplejson.dumps(settings),
+ 'visitor': simplejson.dumps(visitor),
+ }
+ if is_internal_ip(context, 'WOOPRA'):
+ html = disable_html(html, 'Woopra')
+ return html
+
+ def _get_settings(self, context):
+ vars = {'domain': self.domain}
+ try:
+ vars['idle_timeout'] = str(settings.WOOPRA_IDLE_TIMEOUT)
+ except AttributeError:
+ pass
+ return vars
+
+ def _get_visitor(self, context):
+ vars = {}
+ for dict_ in context:
+ for var, val in dict_.items():
+ if var.startswith('woopra_'):
+ vars[var[7:]] = val
+ if 'name' not in vars and 'email' not in vars:
+ user = get_user_from_context(context)
+ if user is not None:
+ vars['name'] = get_identity(context, 'woopra',
+ self._identify, user)
+ if user.email:
+ vars['email'] = user.email
+ return vars
+
+ def _identify(self, user):
+ name = user.get_full_name()
+ if not name:
+ name = user.username
+ return name
+
+
+def contribute_to_analytical(add_node):
+ WoopraNode() # ensure properly configured
+ add_node('head_bottom', WoopraNode)
diff --git a/analytical/tests/__init__.py b/analytical/tests/__init__.py
index e20af34..41375e0 100644
--- a/analytical/tests/__init__.py
+++ b/analytical/tests/__init__.py
@@ -15,4 +15,5 @@ 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_reinvigorate import *
+from analytical.tests.test_tag_woopra import *
from analytical.tests.test_utils import *
diff --git a/analytical/tests/test_tag_woopra.py b/analytical/tests/test_tag_woopra.py
new file mode 100644
index 0000000..d222cb8
--- /dev/null
+++ b/analytical/tests/test_tag_woopra.py
@@ -0,0 +1,84 @@
+"""
+Tests for the Woopra template tags and filters.
+"""
+
+from django.contrib.auth.models import User
+from django.http import HttpRequest
+from django.template import Context
+
+from analytical.templatetags.woopra import WoopraNode
+from analytical.tests.utils import TagTestCase
+from analytical.utils import AnalyticalException
+
+
+class WoopraTagTestCase(TagTestCase):
+ """
+ Tests for the ``woopra`` template tag.
+ """
+
+ def setUp(self):
+ super(WoopraTagTestCase, self).setUp()
+ self.settings_manager.set(WOOPRA_DOMAIN='example.com')
+
+ def test_tag(self):
+ r = self.render_tag('woopra', 'woopra')
+ self.assertTrue('var woo_settings = {"domain": "example.com"};' in r, r)
+
+ def test_node(self):
+ r = WoopraNode().render(Context({}))
+ self.assertTrue('var woo_settings = {"domain": "example.com"};' in r, r)
+
+ def test_no_domain(self):
+ self.settings_manager.set(WOOPRA_DOMAIN='this is not a domain')
+ self.assertRaises(AnalyticalException, WoopraNode)
+
+ def test_wrong_domain(self):
+ self.settings_manager.delete('WOOPRA_DOMAIN')
+ self.assertRaises(AnalyticalException, WoopraNode)
+
+ def test_idle_timeout(self):
+ self.settings_manager.set(WOOPRA_IDLE_TIMEOUT=1234)
+ r = WoopraNode().render(Context({}))
+ self.assertTrue('var woo_settings = {"domain": "example.com", '
+ '"idle_timeout": "1234"};' in r, r)
+
+ def test_custom(self):
+ r = WoopraNode().render(Context({'woopra_var1': 'val1',
+ 'woopra_var2': 'val2'}))
+ self.assertTrue('var woo_visitor = {"var1": "val1", "var2": "val2"};'
+ in r, r)
+
+ def test_identify_name_and_email(self):
+ self.settings_manager.set(ANALYTICAL_AUTO_IDENTIFY=True)
+ r = WoopraNode().render(Context({'user': User(username='test',
+ first_name='Firstname', last_name='Lastname',
+ email="test@example.com")}))
+ self.assertTrue('var woo_visitor = {"name": "Firstname Lastname", '
+ '"email": "test@example.com"};' in r, r)
+
+ def test_identify_username_no_email(self):
+ self.settings_manager.set(ANALYTICAL_AUTO_IDENTIFY=True)
+ r = WoopraNode().render(Context({'user': User(username='test')}))
+ self.assertTrue('var woo_visitor = {"name": "test"};' in r, r)
+
+ def test_no_identify_when_explicit_name(self):
+ self.settings_manager.set(ANALYTICAL_AUTO_IDENTIFY=True)
+ r = WoopraNode().render(Context({'woopra_name': 'explicit',
+ 'user': User(username='implicit')}))
+ self.assertTrue('var woo_visitor = {"name": "explicit"};' in r, r)
+
+ def test_no_identify_when_explicit_email(self):
+ self.settings_manager.set(ANALYTICAL_AUTO_IDENTIFY=True)
+ r = WoopraNode().render(Context({'woopra_email': 'explicit',
+ 'user': User(username='implicit')}))
+ self.assertTrue('var woo_visitor = {"email": "explicit"};' in r, r)
+
+ def test_render_internal_ip(self):
+ self.settings_manager.set(ANALYTICAL_INTERNAL_IPS=['1.1.1.1'])
+ req = HttpRequest()
+ req.META['REMOTE_ADDR'] = '1.1.1.1'
+ context = Context({'request': req})
+ r = WoopraNode().render(context)
+ self.assertTrue(r.startswith(
+ ''), r)
diff --git a/analytical/utils.py b/analytical/utils.py
index 71ed067..202b699 100644
--- a/analytical/utils.py
+++ b/analytical/utils.py
@@ -27,7 +27,26 @@ def get_required_setting(setting, value_re, invalid_msg):
return value
-def get_identity(context, prefix=None, identity_func=None):
+def get_user_from_context(context):
+ """
+ Get the user instance from the template context, if possible.
+
+ If the context does not contain a `request` or `user` attribute,
+ `None` is returned.
+ """
+ try:
+ return context['user']
+ except KeyError:
+ pass
+ try:
+ request = context['request']
+ return request.user
+ except (KeyError, AttributeError):
+ pass
+ return None
+
+
+def get_identity(context, prefix=None, identity_func=None, user=None):
"""
Get the identity of a logged in user from a template context.
@@ -47,11 +66,8 @@ def get_identity(context, prefix=None, identity_func=None):
pass
if getattr(settings, 'ANALYTICAL_AUTO_IDENTIFY', True):
try:
- try:
- user = context['user']
- except KeyError:
- request = context['request']
- user = request.user
+ if user is None:
+ user = get_user_from_context(context)
if user.is_authenticated():
if identity_func is not None:
return identity_func(user)
diff --git a/docs/install.rst b/docs/install.rst
index dda5aa1..cdb2ea6 100644
--- a/docs/install.rst
+++ b/docs/install.rst
@@ -149,6 +149,10 @@ settings required to enable each service are listed here:
REINVIGORATE_TRACKING_ID = '12345-abcdefghij'
+* :doc:`Woopra `::
+
+ WOOPRA_DOMAIN = 'abcde.com'
+
----
diff --git a/docs/services/woopra.rst b/docs/services/woopra.rst
new file mode 100644
index 0000000..631109c
--- /dev/null
+++ b/docs/services/woopra.rst
@@ -0,0 +1,172 @@
+===========================
+Woopra -- website analytics
+===========================
+
+Woopra_ generates live detailed statistics about the visitors to your
+website. You can watch your visitors navigate live and interact with
+them via chat. The service features notifications, campaigns, funnels
+and more.
+
+.. _Woopra: http://www.woopra.com/
+
+
+Installation
+============
+
+To start using the Woopra 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 Woopra 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:`woopra-configuration`.
+
+The Woopra tracking code is inserted into templates using a template
+tag. Load the :mod:`woopra` template tag library and insert the
+:ttag:`woopra` 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 head::
+
+ {% load woopra %}
+
+
+ ...
+ {% woopra %}
+
+ ...
+
+Because Javascript code is asynchronous, putting the tag in the head
+section increases the chances that a page view is going to be tracked
+before the visitor leaves the page. See for details the `Asynchronous
+JavaScript Developer’s Guide`_ on the Woopra website.
+
+.. _`Asynchronous JavaScript Developer’s Guide`: http://www.woopra.com/docs/async/
+
+
+
+.. _woopra-configuration:
+
+Configuration
+=============
+
+Before you can use the Woopra integration, you must first set the
+website domain. You can also customize the data that Woopra tracks and
+identify authenticated users.
+
+
+Setting the domain
+------------------
+
+A Woopra account is tied to a website domain. Set
+:const:`WOOPRA_DOMAIN` in the project :file:`settings.py` file::
+
+ WOOPRA_DOMAIN = 'XXXXXXXX.XXX'
+
+If you do not set a domain, the tracking code will not be rendered.
+
+(In theory, the django-analytical application could get the website
+domain from the current ``Site`` or the ``request`` object, but this
+setting also works as a sign that the Woopra integration should be
+enabled for the :ttag:`analytical.*` template tags.)
+
+
+Visitor timeout
+---------------
+
+The default Woopra visitor timeout -- the time after which Woopra
+ignores inactive visitors on a website -- is set at 4 minutes. This
+means that if a user opens your Web page and then leaves it open in
+another browser window, Woopra will report that the visitor has gone
+away after 4 minutes of inactivity on that page (no page scrolling,
+clicking or other action).
+
+If you would like to increase or decrease the idle timeout setting you
+can set :const:`WOOPRA_IDLE_TIMEOUT` to a time in milliseconds. For
+example, to set the default timout to 10 minutes::
+
+ WOOPRA_IDLE_TIMEOUT = 10 * 60 * 1000
+
+Keep in mind that increasing this number will not only show you more
+visitors on your site at a time, but will also skew your average time on
+a page reporting. So it’s important to keep the number reasonable in
+order to accurately make predictions.
+
+
+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:`WOOPRA_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.
+
+
+Custom data
+-----------
+
+As described in the Woopra documentation on `custom visitor data`_,
+the data that is tracked by Woopra can be customized. Using template
+context variables, you can let the :ttag:`woopra` tag pass custom data
+to Woopra automatically. You can set the context variables in your view
+when your render a template containing the tracking code::
+
+ context = RequestContext({'woopra_cart_value': cart.total_price})
+ return some_template.render(context)
+
+For some data, it is annoying to do this for every view, so you may want
+to set variables in a context processor that you add to the
+:data:`TEMPLATE_CONTEXT_PROCESSORS` list in :file:`settings.py`::
+
+ from django.utils.hashcompat import md5_constructor as md5
+
+ GRAVATAR_URL = 'http://www.gravatar.com/avatar/'
+
+ def woopra_custom_data(request):
+ try:
+ email = request.user.email
+ except AttributeError:
+ return {}
+ email_hash = md5(email).hexdigest()
+ avatar_url = "%s%s" % (GRAVATAR_URL, email_hash)
+ return {'woopra_avatar': avatar_url}
+
+Just remember that if you set the same context variable in the
+:class:`~django.template.context.RequestContext` constructor and in a
+context processor, the latter clobbers the former.
+
+Standard variables that will be displayed in the Woopra live visitor
+data are listed in the table below, but you can define any ``woopra_*``
+variable you like and have that detail passed from within the visitor
+live stream data when viewing Woopra.
+
+==================== ===================================
+Context variable Description
+==================== ===================================
+``woopra_name`` The visitor's full name.
+-------------------- -----------------------------------
+``woopra_email`` The visitor's email address.
+-------------------- -----------------------------------
+``woopra_avatar`` A URL link to a visitor avatar.
+==================== ===================================
+
+
+.. _`custom visitor data`: http://www.woopra.com/docs/tracking/custom-visitor-data/
+
+
+Identifying authenticated users
+-------------------------------
+
+If you have not set the ``woopra_name`` or ``woopra_email`` variables
+explicitly, the username and email address of an authenticated user are
+passed to Woopra automatically. See :ref:`identifying-visitors`.
+
+
+----
+
+Thanks go to Woopra for their support with the development of this
+application.