Refactoring services, step 1

Move services into their own Django apps, leaving the analytics app a
small wrapper.  First service converted is Clicky.

This commit has not been tested.
This commit is contained in:
Joost Cassee 2011-01-26 22:28:16 +01:00
parent 4a81b49320
commit d715c16244
11 changed files with 347 additions and 151 deletions

View file

@ -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 %}
</body>
</html>
.. _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',
}

View file

@ -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 = """
<script type="text/javascript">
var clicky = { log: function(){ return; }, goal: function(){ return; }};
var clicky_site_id = %(site_id)s;
var clicky_custom = %(custom)s;
(function() {
var s = document.createElement('script');
s.type = 'text/javascript';
s.async = true;
s.src = ( document.location.protocol == 'https:' ? 'https://static.getclicky.com/js' : 'http://static.getclicky.com/js' );
( document.getElementsByTagName('head')[0] || document.getElementsByTagName('body')[0] ).appendChild( s );
})();
</script>
<noscript><p><img alt="Clicky" width="1" height="1" src="http://in.getclicky.com/%(site_id)sns.gif" /></p></noscript>
"""
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

View file

@ -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 = """
<script type="text/javascript">
var clicky = { log: function(){ return; }, goal: function(){ return; }};
var clicky_site_id = %(site_id)s;
var clicky_custom = %(custom)s;
(function() {
var s = document.createElement('script');
s.type = 'text/javascript';
s.async = true;
s.src = ( document.location.protocol == 'https:' ? 'https://static.getclicky.com/js' : 'http://static.getclicky.com/js' );
( document.getElementsByTagName('head')[0] || document.getElementsByTagName('body')[0] ).appendChild( s );
})();
</script>
<noscript><p><img alt="Clicky" width="1" height="1" src="http://in.getclicky.com/%(site_id)sns.gif" /></p></noscript>
"""
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

View file

@ -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 = "<!-- Analytical disabled on internal IP address\n%s\n-->"
JS_COMMENT_CODE = "/* %s */"
SCRIPT_CODE = """<script type="text/javascript">%s</script>"""
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()

56
analytical/utils.py Normal file
View file

@ -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 = "<!-- %(service)s disabled on internal IP address\n%(html)\n-->"
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()

21
docs/.ext/local.py Normal file
View file

@ -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",
)

View file

@ -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'

View file

@ -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

View file

@ -64,18 +64,18 @@ Your base template should look like this::
<!DOCTYPE ... >
<html>
<head>
{% analytical_setup_head_top %}
{% analytical_head_top %}
...
{% analytical_setup_head_bottom %}
{% analytical_head_bottom %}
</head>
<body>
{% analytical_setup_body_top %}
{% analytical_body_top %}
...
{% analytical_setup_body_bottom %}
{% analytical_body_bottom %}
</body>
</html>

View file

@ -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