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.