mirror of
https://github.com/jazzband/django-analytical.git
synced 2026-05-11 08:43:16 +00:00
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:
parent
4a81b49320
commit
d715c16244
11 changed files with 347 additions and 151 deletions
146
analytical/clicky/__init__.py
Normal file
146
analytical/clicky/__init__.py
Normal 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',
|
||||
}
|
||||
0
analytical/clicky/templatetags/__init__.py
Normal file
0
analytical/clicky/templatetags/__init__.py
Normal file
68
analytical/clicky/templatetags/clicky.py
Normal file
68
analytical/clicky/templatetags/clicky.py
Normal 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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
56
analytical/utils.py
Normal 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
21
docs/.ext/local.py
Normal 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",
|
||||
)
|
||||
|
|
@ -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'
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Reference in a new issue