diff --git a/analytical/clicky/__init__.py b/analytical/clicky/__init__.py new file mode 100644 index 0000000..c4354af --- /dev/null +++ b/analytical/clicky/__init__.py @@ -0,0 +1,146 @@ +""" +========================== +Clicky -- traffic analysis +========================== + +Clicky_ is an online web analytics tool. It is similar to Google +Analytics in that it provides statistics on who is visiting your website +and what they are doing. Clicky provides its data in real time and is +designed to be very easy to use. + +.. _Clicky: http://getclicky.com/ + + +.. clicky-template-tag: + +Installation +============ + +You only need to do perform these steps if you are not using the +generic :ttag:`analytical.*` tags. If you are, skip to +:ref:`clicky-configuration`. + +In order to use the template tag, you need to add +:mod:`analytical.clicky` to the installed applications list in the +project :file:`settings.py` file:: + + INSTALLED_APPS = [ + ... + 'analytical.clicky', + ... + ] + +The Clicky tracking code is inserted into templates using a template +tag. Load the :mod:`clicky` template tag library and insert the +:ttag:`clicky` 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:: + + {% load clicky %} + ... + + {% clicky %} + + + + +.. _clicky-configuration: + +Configuration +============= + +Before you can use the Clicky integration, you must first set your +website Site ID. You can also customize the data that Clicky tracks. + + +.. _clicky-site-id: + +The Site ID +----------- + +Every website you track with Clicky gets its own Site ID, and the +:ttag:`clicky` tag will include it in the rendered Javascript code. +You can find the Site ID in the *Info* tab of the website *Preferences* +page, in your Clicky account. Set :const:`CLICKY_SITE_ID` in the +project :file:`settings.py` file:: + + CLICKY_SITE_ID = '12345678' + +If you do not set a Site ID, the tracking code will not be rendered. + +Often you do not want to track clicks from your development or internal +IP addresses. By default, if the tag detects that the client comes from +any address in the :const:`INTERNAL_IPS` setting, the tracking code is +commented out. See :const:`ANALYTICAL_INTERNAL_IPS` for important +information about detecting the visitor IP address. + + +.. _clicky-custom-data: + +Custom data +----------- + +As described in the Clicky `customized tracking`_ documentation page, +the data that is tracked by Clicky can be customized by setting the +:data:`clicky_custom` Javascript variable before loading the tracking +code. Using template context variables, you can let the :ttag:`clicky` +tag pass custom data to Clicky automatically. You can set the context +variables in your view when your render a template containing the +tracking code:: + + context = RequestContext({'clicky_title': 'A better page title'}) + return some_template.render(context) + +It is annoying to do this for every view, so you may want to set custom +properties in a context processor that you add to the +:data:`TEMPLATE_CONTEXT_PROCESSORS` list in :file:`settings.py`:: + + def clicky_global_properties(request): + return {'clicky_timeout': 10} + +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. + +Here is a table with the most important variables. All variable listed +on the `customized tracking`_ documentation page can be set by replacing +``clicky_custom.`` with ``clicky_``. + +================ =============== ===================================== +Context variable Clicky property Description +================ =============== ===================================== +clicky_session session_ Session data. A dictionary + containing ``username`` and/or + ``group`` keys. +---------------- --------------- ------------------------------------- +clicky_goal goal_ A succeeded goal. A dictionary + containing ``id`` and ``revenue`` + keys. +---------------- --------------- ------------------------------------- +clicky_href href_ The URL as tracked by Clicky. Default + is the page URL. +---------------- --------------- ------------------------------------- +clicky_title title_ The page title as tracked by Clicky. + Default is the HTML title. +================ =============== ===================================== + +.. _`customized tracking`: http://getclicky.com/help/customization +.. _session: http://getclicky.com/help/customization#goal +.. _goal: http://getclicky.com/help/customization#goal +.. _href: http://getclicky.com/help/customization#href +.. _title: http://getclicky.com/help/customization#title + +By default, the username of an authenticated user is passed to Clicky +automatically in the ``session.username`` property, unless that property +was set explicitly. See :data:`ANALYTICAL_AUTO_IDENTIFY`. + + +---- + +Thanks go to Clicky for their support with the development of this +application. +""" + +clicky_service = { + 'body_bottom': 'analytical.clicky.templatetags.clicky.ClickyNode', +} diff --git a/analytical/clicky/templatetags/__init__.py b/analytical/clicky/templatetags/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/analytical/clicky/templatetags/clicky.py b/analytical/clicky/templatetags/clicky.py new file mode 100644 index 0000000..e2bca76 --- /dev/null +++ b/analytical/clicky/templatetags/clicky.py @@ -0,0 +1,68 @@ +""" +Clicky template tags. +""" + +import re + +from django.template import Library, Node, TemplateSyntaxError +from django.utils import simplejson + +from analytical.utils import get_required_setting, get_identity, \ + is_internal_ip, disable_html + + +SITE_ID_RE = re.compile(r'^\d{8}$') +TRACKING_CODE = """ + + +""" + + +register = Library() + + +@register.tag +def clicky(parser, token): + """ + Clicky tracking template tag. + + Renders Javascript code to track page visits. You must supply + your Clicky Site ID (as a string) in the ``CLICKY_SITE_ID`` + setting. + """ + bits = token.split_contents() + if len(bits) > 1: + raise TemplateSyntaxError("'%s' takes no arguments" % bits[0]) + return ClickyNode() + +class ClickyNode(Node): + def __init__(self): + self.site_id = get_required_setting('CLICKY_SITE_ID', SITE_ID_RE, + "must be a string containing an eight-digit number") + + def render(self, context): + custom = {} + for var, value in context.items(): + if var.startswith('clicky_'): + custom[var[7:]] = value + if 'username' not in custom.get('session', {}): + identity = get_identity(context) + if identity is not None: + custom.setdefault('session', {})['username'] = identity + + html = TRACKING_CODE % {'site_id': self.site_id, + 'custom': simplejson.dumps(custom)} + if is_internal_ip(context): + html = disable_html(html, 'Clicky') + return html diff --git a/analytical/services/clicky.py b/analytical/services/clicky.py deleted file mode 100644 index acc06b4..0000000 --- a/analytical/services/clicky.py +++ /dev/null @@ -1,47 +0,0 @@ -""" -Clicky service. -""" - -import re - -from django.utils import simplejson - -from analytical.services.base import AnalyticalService - - -SITE_ID_RE = re.compile(r'^\d{8}$') -SETUP_CODE = """ - - -""" -CUSTOM_CONTEXT_KEY = 'clicky_custom' - - -class ClickyService(AnalyticalService): - def __init__(self): - self.site_id = self.get_required_setting('CLICKY_SITE_ID', SITE_ID_RE, - "must be a string containing an eight-digit number") - - def render_body_bottom(self, context): - custom = { - 'session': { - 'username': self.get_identity(context), - } - } - custom.update(context.get(CUSTOM_CONTEXT_KEY, {})) - return SETUP_CODE % {'site_id': self.site_id, - 'custom': simplejson.dumps(custom)} - - def _convert_properties(self): - pass diff --git a/analytical/templatetags/analytical.py b/analytical/templatetags/analytical.py index 9d25a8f..7a4f5b8 100644 --- a/analytical/templatetags/analytical.py +++ b/analytical/templatetags/analytical.py @@ -1,20 +1,30 @@ """ Analytical template tags. """ -from __future__ import absolute_import + +import logging from django import template from django.conf import settings -from django.template import Node, TemplateSyntaxError, Variable - -from analytical.services import get_enabled_services +from django.core.exceptions import ImproperlyConfigured +from django.template import Node, TemplateSyntaxError +from django.utils.importlib import import_module -HTML_COMMENT_CODE = "" -JS_COMMENT_CODE = "/* %s */" -SCRIPT_CODE = """""" +DEFAULT_SERVICES = [ + 'analytical.chartbeat.ChartbeatService', + 'analytical.clicky.clicky_service', + 'analytical.crazy_egg.CrazyEggService', + 'analytical.google_analytics.GoogleAnalyticsService', + 'analytical.kiss_insights.KissInsightsService', + 'analytical.kiss_metrics.KissMetricsService', + 'analytical.mixpanel.MixpanelService', + 'analytical.optimizely.OptimizelyService', +] +LOCATIONS = ['head_top', 'head_bottom', 'body_top', 'body_bottom'] +_log = logging.getLogger(__name__) register = template.Library() @@ -26,74 +36,46 @@ def _location_tag(location): return AnalyticalNode(location) return tag -for l in ['head_top', 'head_bottom', 'body_top', 'body_bottom']: - register.tag('analytical_setup_%s' % l, _location_tag(l)) +for loc in LOCATIONS: + register.tag('analytical_%s' % loc, _location_tag(loc)) class AnalyticalNode(Node): def __init__(self, location): - self.location = location - self.render_func_name = "render_%s" % self.location - self.internal_ips = getattr(settings, 'ANALYTICAL_INTERNAL_IPS', - getattr(settings, 'INTERNAL_IPS', ())) + self.nodes = template_nodes[location] def render(self, context): - result = "".join([self._render_service(service, context) - for service in get_enabled_services()]) - if not result: - return "" - if self._is_internal_ip(context): - return HTML_COMMENT_CODE % result - return result + return "".join([node.render(context) for node in self.nodes]) - def _render_service(self, service, context): - func = getattr(service, self.render_func_name) - return func(context) - def _is_internal_ip(self, context): +def _load_template_nodes(): + location_nodes = dict((loc, []) for loc in LOCATIONS) + try: + service_paths = settings.ANALYTICAL_SERVICES + autoload = False + except AttributeError: + service_paths = DEFAULT_SERVICES + autoload = True + for path in service_paths: try: - request = context['request'] - remote_ip = request.META.get('HTTP_X_FORWARDED_FOR', - request.META.get('REMOTE_ADDR', '')) - return remote_ip in self.internal_ips - except KeyError, AttributeError: - return False + service = _import_path(path) + for location in LOCATIONS: + node_path = service.get(location) + if node_path is not None: + node_cls = _import_path(node_path) + node = node_cls() + location_nodes[location].append(node) + except ImproperlyConfigured, e: + if autoload: + _log.debug("not loading analytical service '%s': %s", + path, e) + else: + raise + return location_nodes +def _import_path(path): + mod_name, attr_name = path.rsplit('.', 1) + mod = import_module(mod_name) + return getattr(mod, attr_name) -def event(parser, token): - bits = token.split_contents() - if len(bits) < 2: - raise TemplateSyntaxError("'%s' tag takes at least one argument" - % bits[0]) - properties = _parse_properties(bits[0], bits[2:]) - return EventNode(bits[1], properties) - -register.tag('event', event) - - -class EventNode(Node): - def __init__(self, name, properties): - self.name = name - self.properties = properties - - def render(self, context): - props = dict((var, Variable(val).resolve(context)) - for var, val in self.properties) - result = "".join([service.render_js_event(props) - for service in get_enabled_services()]) - if not result: - return "" - if self._is_internal_ip(context): - return JS_COMMENT_CODE % result - return result - - -def _parse_properties(tag_name, bits): - properties = [] - for bit in bits: - try: - properties.append(bit.split('=', 1)) - except IndexError: - raise TemplateSyntaxError("'%s' tag argument must be of the form " - " property=value: '%s'" % (tag_name, bit)) - return properties +template_nodes = _load_template_nodes() diff --git a/analytical/utils.py b/analytical/utils.py new file mode 100644 index 0000000..17294cf --- /dev/null +++ b/analytical/utils.py @@ -0,0 +1,56 @@ +""" +Utility function for django-analytical. +""" + +from django.conf import settings +from django.core.exceptions import ImproperlyConfigured + + +IDENTITY_CONTEXT_KEY = 'analytical_identity' +HTML_COMMENT = "" + + +def get_required_setting(self, setting, value_re, invalid_msg): + try: + value = getattr(settings, setting) + except AttributeError: + raise ImproperlyConfigured("%s setting: not found" % setting) + value = str(value) + if not value_re.search(value): + raise ImproperlyConfigured("%s setting: %s: '%s'" + % (setting, invalid_msg, value)) + return value + + +def get_identity(context): + try: + return context[IDENTITY_CONTEXT_KEY] + except KeyError: + pass + if getattr(settings, 'ANALYTICAL_AUTO_IDENTIFY', True): + try: + try: + user = context['user'] + except KeyError: + request = context['request'] + user = request.user + if user.is_authenticated(): + return user.username + except (KeyError, AttributeError): + pass + return None + + +def is_internal_ip(context): + try: + request = context['request'] + remote_ip = request.META.get('HTTP_X_FORWARDED_FOR', + request.META.get('REMOTE_ADDR', '')) + return remote_ip in getattr(settings, 'ANALYTICAL_INTERNAL_IPS', + getattr(settings, 'INTERNAL_IPS', [])) + except KeyError, AttributeError: + return False + + +def disable_html(self, html, service): + return HTML_COMMENT % locals() diff --git a/docs/.ext/local.py b/docs/.ext/local.py new file mode 100644 index 0000000..ab91486 --- /dev/null +++ b/docs/.ext/local.py @@ -0,0 +1,21 @@ +def setup(app): + app.add_crossref_type( + directivename = "setting", + rolename = "setting", + indextemplate = "pair: %s; setting", + ) + app.add_crossref_type( + directivename = "templatetag", + rolename = "ttag", + indextemplate = "pair: %s; template tag" + ) + app.add_crossref_type( + directivename = "templatefilter", + rolename = "tfilter", + indextemplate = "pair: %s; template filter" + ) + app.add_crossref_type( + directivename = "fieldlookup", + rolename = "lookup", + indextemplate = "pair: %s; field lookup type", + ) diff --git a/docs/conf.py b/docs/conf.py index 365d83b..cecbcd0 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -19,7 +19,7 @@ release = analytical.__version__ # The short X.Y version. version = release.rsplit('.', 1)[0] -extensions = ['sphinx.ext.autodoc', 'sphinx.ext.intersphinx'] +extensions = ['sphinx.ext.autodoc', 'sphinx.ext.intersphinx', 'local'] templates_path = ['.templates'] source_suffix = '.rst' master_doc = 'index' diff --git a/docs/index.rst b/docs/index.rst index 0b55cad..388ca4c 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -9,7 +9,6 @@ into a Django_ project. :Package: http://pypi.python.org/pypi/django-analytical/ :Source: http://github.com/jcassee/django-analytical -:Issues: http://github.com/jcassee/django-analytical/issues Overview diff --git a/docs/install.rst b/docs/install.rst index d4871af..cd0d178 100644 --- a/docs/install.rst +++ b/docs/install.rst @@ -64,18 +64,18 @@ Your base template should look like this:: - {% analytical_setup_head_top %} + {% analytical_head_top %} ... - {% analytical_setup_head_bottom %} + {% analytical_head_bottom %} - {% analytical_setup_body_top %} + {% analytical_body_top %} ... - {% analytical_setup_body_bottom %} + {% analytical_body_bottom %} diff --git a/docs/services/clicky.rst b/docs/services/clicky.rst index 972ea9c..dc20e31 100644 --- a/docs/services/clicky.rst +++ b/docs/services/clicky.rst @@ -1,32 +1,3 @@ -Clicky -- traffic analysis -========================== +.. currentmodule:: analytical.clicky -Clicky_ is an online web analytics tool. It is similar to Google -Analytics in that it provides statistics on who is visiting your website -and what they are doing. Clicky provides its data in real time and is -designed to be very easy to use. - -.. _Clicky: http://getclicky.com/ - -The setup code is added to the bottom of the HTML body. By default, the -username of a logged-in user is passed to Clicky. See -:data:`ANALYTICAL_AUTO_IDENTIFY`. - - -Required settings ------------------ - -.. data:: CLICKY_SITE_ID - - The Clicky site identifier, or Site ID:: - - CLICKY_SITE_ID = '12345678' - - You can find the Site ID in the Info tab of the website Preferences - page on your Clicky account. - - ----- - -Thanks go to Clicky for their support with the development of this -application. +.. automodule:: analytical.clicky