From 39c205f546183b9e793b45ecd2ac977b3539ba67 Mon Sep 17 00:00:00 2001 From: Joost Cassee Date: Tue, 25 Jan 2011 00:13:44 +0100 Subject: [PATCH] Updated analytical * Added Chartbeat. * Added settings context processor. * Added identification. * Prepared events. * Updated tests. --- README.rst | 2 + analytical/__init__.py | 14 +++- analytical/context_processors.py | 33 ++++++++ analytical/services/__init__.py | 40 +++++---- analytical/services/base.py | 53 +++++++++++- analytical/services/chartbeat.py | 66 +++++++++++++++ analytical/services/clicky.py | 20 ++++- analytical/services/console.py | 26 ++++-- analytical/services/crazy_egg.py | 15 +++- analytical/services/google_analytics.py | 33 ++++++-- analytical/services/kiss_insights.py | 39 +++++++++ .../{kissmetrics.py => kiss_metrics.py} | 22 +++-- analytical/services/kissinsights.py | 30 ------- analytical/services/mixpanel.py | 43 ++++++---- analytical/services/optimizely.py | 6 +- analytical/templatetags/analytical.py | 57 +++++++++++-- analytical/tests/services/__init__.py | 6 +- analytical/tests/services/test_base.py | 74 +++++++++++++++++ analytical/tests/services/test_chartbeat.py | 66 +++++++++++++++ analytical/tests/services/test_clicky.py | 18 +++- analytical/tests/services/test_console.py | 13 +++ analytical/tests/services/test_crazy_egg.py | 8 +- .../tests/services/test_google_analytics.py | 18 +++- ..._kissinsights.py => test_kiss_insights.py} | 28 +++++-- ...st_kissmetrics.py => test_kiss_metrics.py} | 28 +++++-- analytical/tests/services/test_mixpanel.py | 19 ++++- analytical/tests/test_services.py | 27 ++---- analytical/tests/test_template_tags.py | 71 ++++++++++++++++ docs/conf.py | 6 +- docs/index.rst | 18 ++-- docs/install.rst | 83 +++++++++++++++---- docs/quick.rst | 57 ------------- docs/services/chartbeat.rst | 31 +++++++ .../{kissinsights.rst => kiss_insights.rst} | 0 .../{kissmetrics.rst => kiss_metrics.rst} | 0 docs/settings.rst | 69 +++++++++++++++ setup.py | 6 +- 37 files changed, 910 insertions(+), 235 deletions(-) create mode 100644 analytical/context_processors.py create mode 100644 analytical/services/chartbeat.py create mode 100644 analytical/services/kiss_insights.py rename analytical/services/{kissmetrics.py => kiss_metrics.py} (56%) delete mode 100644 analytical/services/kissinsights.py create mode 100644 analytical/tests/services/test_base.py create mode 100644 analytical/tests/services/test_chartbeat.py rename analytical/tests/services/{test_kissinsights.py => test_kiss_insights.py} (57%) rename analytical/tests/services/{test_kissmetrics.py => test_kiss_metrics.py} (53%) delete mode 100644 docs/quick.rst create mode 100644 docs/services/chartbeat.rst rename docs/services/{kissinsights.rst => kiss_insights.rst} (100%) rename docs/services/{kissmetrics.rst => kiss_metrics.rst} (100%) create mode 100644 docs/settings.rst diff --git a/README.rst b/README.rst index d045c3e..f54b586 100644 --- a/README.rst +++ b/README.rst @@ -4,6 +4,7 @@ django-analytical The django-analytical application integrates various analytics services into a Django_ project. Currently supported services: +* `Chartbeat`_ -- traffic analysis * `Clicky`_ -- traffic analysis * `Crazy Egg`_ -- visual click tracking * `Google Analytics`_ traffic analysis @@ -23,6 +24,7 @@ Joshua Krall's all-purpose analytics front-end for Rails. The work on Crazy Egg was made possible by `Bateau Knowledge`_. .. _Django: http://www.djangoproject.com/ +.. _Chartbeat: http://www.chartbeat.com/ .. _Clicky: http://getclicky.com/ .. _`Crazy Egg`: http://www.crazyegg.com/ .. _`Google Analytics`: http://www.google.com/analytics/ diff --git a/analytical/__init__.py b/analytical/__init__.py index f687ea4..4fc48e4 100644 --- a/analytical/__init__.py +++ b/analytical/__init__.py @@ -1,10 +1,11 @@ """ -======================================== Analytics service integration for Django ======================================== -The django-clicky application integrates Clicky_ analytics into a -Django_ project. +The django-analytical application integrates analytics services into a +Django_ project. See the ``docs`` directory for more information. + +.. _Django: http://www.djangoproject.com/ """ __author__ = "Joost Cassee" @@ -12,3 +13,10 @@ __email__ = "joost@cassee.net" __version__ = "0.1.0alpha" __copyright__ = "Copyright (C) 2011 Joost Cassee" __license__ = "MIT License" + +try: + from collections import namedtuple +except ImportError: + namedtuple = lambda name, fields: lambda *values: values + +Property = namedtuple('Property', ['num', 'name', 'value']) diff --git a/analytical/context_processors.py b/analytical/context_processors.py new file mode 100644 index 0000000..ccd09d8 --- /dev/null +++ b/analytical/context_processors.py @@ -0,0 +1,33 @@ +""" +Context processors for django-analytical. +""" + +from django.conf import settings + + +IMPORT_SETTINGS = [ + 'ANALYTICAL_INTERNAL_IPS', + 'ANALYTICAL_SERVICES', + 'CHARTBEAT_USER_ID', + 'CLICKY_SITE_ID', + 'CRAZY_EGG_ACCOUNT_NUMBER', + 'GOOGLE_ANALYTICS_PROPERTY_ID', + 'KISS_INSIGHTS_ACCOUNT_NUMBER', + 'KISS_INSIGHTS_SITE_CODE', + 'KISS_METRICS_API_KEY', + 'MIXPANEL_TOKEN', + 'OPTIMIZELY_ACCOUNT_NUMBER', +] + + +def settings(request): + """ + Import all django-analytical settings into the template context. + """ + vars = {} + for setting in IMPORT_SETTINGS: + try: + vars[setting] = getattr(settings, setting) + except AttributeError: + pass + return vars diff --git a/analytical/services/__init__.py b/analytical/services/__init__.py index 5031eec..5f7d3e0 100644 --- a/analytical/services/__init__.py +++ b/analytical/services/__init__.py @@ -12,20 +12,21 @@ from django.core.exceptions import ImproperlyConfigured _log = logging.getLogger(__name__) DEFAULT_SERVICES = [ + 'analytical.services.chartbeat.ChartbeatService', 'analytical.services.clicky.ClickyService', - 'analytical.services.crazyegg.CrazyEggService', + 'analytical.services.crazy_egg.CrazyEggService', 'analytical.services.google_analytics.GoogleAnalyticsService', - 'analytical.services.kissinsights.KissInsightsService', - 'analytical.services.kissmetrics.KissMetricsService', + 'analytical.services.kiss_insights.KissInsightsService', + 'analytical.services.kiss_metrics.KissMetricsService', 'analytical.services.mixpanel.MixpanelService', 'analytical.services.optimizely.OptimizelyService', ] enabled_services = None -def get_enabled_services(reload=False): +def get_enabled_services(): global enabled_services - if enabled_services is None or reload: + if enabled_services is None: enabled_services = load_services() return enabled_services @@ -39,19 +40,7 @@ def load_services(): autoload = True for path in service_paths: try: - module, attr = path.rsplit('.', 1) - try: - mod = import_module(module) - except ImportError, e: - raise ImproperlyConfigured( - 'error importing analytical service %s: "%s"' - % (module, e)) - try: - service = getattr(mod, attr)() - except AttributeError: - raise ImproperlyConfigured( - 'module "%s" does not define service "%s"' - % (module, attr)) + service = _load_service(path) enabled_services.append(service) except ImproperlyConfigured, e: if autoload: @@ -60,3 +49,18 @@ def load_services(): else: raise return enabled_services + +def _load_service(path): + module, attr = path.rsplit('.', 1) + try: + mod = import_module(module) + except ImportError, e: + raise ImproperlyConfigured( + 'error importing analytical service %s: "%s"' % (module, e)) + try: + service = getattr(mod, attr)() + except (AttributeError, TypeError): + raise ImproperlyConfigured( + 'module "%s" does not define callable service "%s"' + % (module, attr)) + return service diff --git a/analytical/services/base.py b/analytical/services/base.py index 856632e..0f47f50 100644 --- a/analytical/services/base.py +++ b/analytical/services/base.py @@ -5,6 +5,12 @@ from django.core.exceptions import ImproperlyConfigured from django.conf import settings + +HTML_COMMENT = "" +JS_COMMENT = "*/ %(message):%(sep)s%(html)s%(sep)s*/" +IDENTITY_CONTEXT_KEY = 'analytical_identity' + + class AnalyticalService(object): """ Analytics service. @@ -14,8 +20,6 @@ class AnalyticalService(object): func_name = "render_%s" % location func = getattr(self, func_name) html = func(context) - if self.is_initialized(context): - pass return html def render_head_top(self, context): @@ -30,6 +34,9 @@ class AnalyticalService(object): def render_body_bottom(self, context): return "" + def render_event(self, name, properties): + return "" + def get_required_setting(self, setting, value_re, invalid_msg): try: value = getattr(settings, setting) @@ -37,5 +44,45 @@ class AnalyticalService(object): raise ImproperlyConfigured("%s setting: not found" % setting) value = str(value) if not value_re.search(value): - raise ImproperlyConfigured("%s setting: %s" % (value, invalid_msg)) + raise ImproperlyConfigured("%s setting: %s: '%s'" + % (setting, invalid_msg, value)) return value + + def get_identity(self, 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 get_events(self, context): + return context.get('analytical_events', {}) + + def get_properties(self, context): + return context.get('analytical_properties', {}) + + def _html_comment(self, html, message=""): + return self._comment(HTML_COMMENT, html, message) + + def _js_comment(self, html, message=""): + return self._comment(JS_COMMENT, html, message) + + def _comment(self, format, html, message): + if not message: + message = "Disabled" + if message.find('\n') > -1: + sep = '\n' + else: + sep = ' ' + return format % {'message': message, 'html': html, 'sep': sep} diff --git a/analytical/services/chartbeat.py b/analytical/services/chartbeat.py new file mode 100644 index 0000000..22885af --- /dev/null +++ b/analytical/services/chartbeat.py @@ -0,0 +1,66 @@ +""" +Chartbeat service. +""" + +import re + +from django.contrib.sites.models import Site, RequestSite +from django.core.exceptions import ImproperlyConfigured + +from analytical.services.base import AnalyticalService + + +USER_ID_RE = re.compile(r'^\d{5}$') +INIT_CODE = """""" +SETUP_CODE = """ + +""" +DOMAIN_CONTEXT_KEY = 'chartbeat_domain' + + +class ChartbeatService(AnalyticalService): + def __init__(self): + self.user_id = self.get_required_setting( + 'CHARTBEAT_USER_ID', USER_ID_RE, + "must be a string containing an five-digit number") + + def render_head_top(self, context): + return INIT_CODE + + def render_body_bottom(self, context): + return SETUP_CODE % {'user_id': self.user_id, + 'domain': self._get_domain(context)} + + def _get_domain(self, context): + try: + return context[DOMAIN_CONTEXT_KEY] + except KeyError: + pass + try: + return Site.objects.get_current().domain + except ImproperlyConfigured: + pass + try: + request = context['request'] + return RequestSite(request).domain + except (KeyError, AttributeError): + raise KeyError("could not find access either '%s' or 'request' " + "in the template context and 'django.contrib.sites' is " + "not in INSTALLED_APPS" % DOMAIN_CONTEXT_KEY) diff --git a/analytical/services/clicky.py b/analytical/services/clicky.py index 617af41..acc06b4 100644 --- a/analytical/services/clicky.py +++ b/analytical/services/clicky.py @@ -4,14 +4,17 @@ Clicky service. import re +from django.utils import simplejson + from analytical.services.base import AnalyticalService SITE_ID_RE = re.compile(r'^\d{8}$') -TRACKING_CODE = """ +SETUP_CODE = """ """ +CUSTOM_CONTEXT_KEY = 'clicky_custom' class ClickyService(AnalyticalService): - KEY = 'clicky' - 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): - return TRACKING_CODE % {'site_id': self.site_id} + 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/services/console.py b/analytical/services/console.py index 634b49c..7bde9f4 100644 --- a/analytical/services/console.py +++ b/analytical/services/console.py @@ -8,23 +8,35 @@ from analytical.services.base import AnalyticalService DEBUG_CODE = """ """ +LOG_CODE_ANONYMOUS = """ + console.log('Analytical: rendering analytical_%(location)s tag'); +""" +LOG_CODE_IDENTIFIED = """ + console.log('Analytical: rendering analytical_%(location)s tag for user %(identity)s'); +""" class ConsoleService(AnalyticalService): - KEY = 'console' - def render_head_top(self, context): - return DEBUG_CODE % {'location': 'head_top'} + return self._render_code('head_top', context) def render_head_bottom(self, context): - return DEBUG_CODE % {'location': 'head_bottom'} + return self._render_code('head_bottom', context) def render_body_top(self, context): - return DEBUG_CODE % {'location': 'body_top'} + return self._render_code('body_top', context) def render_body_bottom(self, context): - return DEBUG_CODE % {'location': 'body_bottom'} + return self._render_code('body_bottom', context) + + def _render_code(self, location, context): + vars = {'location': location, 'identity': self.get_identity(context)} + if vars['identity'] is None: + debug_code = DEBUG_CODE % LOG_CODE_ANONYMOUS + else: + debug_code = DEBUG_CODE % LOG_CODE_IDENTIFIED + return debug_code % vars diff --git a/analytical/services/crazy_egg.py b/analytical/services/crazy_egg.py index 7ec85af..22a629e 100644 --- a/analytical/services/crazy_egg.py +++ b/analytical/services/crazy_egg.py @@ -8,17 +8,24 @@ from analytical.services.base import AnalyticalService ACCOUNT_NUMBER_RE = re.compile(r'^\d{8}$') -TRACK_CODE = """""" +USERVAR_CODE = "CE2.set(%(varnr)d, '%(value)s');" +USERVAR_CONTEXT_VAR = 'crazy_egg_uservars' class CrazyEggService(AnalyticalService): - KEY = 'crazy_egg' - def __init__(self): self.account_nr = self.get_required_setting('CRAZY_EGG_ACCOUNT_NUMBER', ACCOUNT_NUMBER_RE, "must be a string containing an eight-digit number") def render_body_bottom(self, context): - return TRACK_CODE % {'account_nr_1': self.account_nr[:4], + html = SETUP_CODE % {'account_nr_1': self.account_nr[:4], 'account_nr_2': self.account_nr[4:]} + uservars = context.get(USERVAR_CONTEXT_VAR, {}) + if uservars: + js = "".join(USERVAR_CODE % {'varnr': varnr, 'value': value} + for (varnr, value) in uservars.items()) + html = '%s\n' \ + % (html, js) + return html diff --git a/analytical/services/google_analytics.py b/analytical/services/google_analytics.py index f104795..851c57f 100644 --- a/analytical/services/google_analytics.py +++ b/analytical/services/google_analytics.py @@ -8,12 +8,12 @@ from analytical.services.base import AnalyticalService PROPERTY_ID_RE = re.compile(r'^UA-\d+-\d+$') -TRACKING_CODE = """ +SETUP_CODE = """ """ - +TRACK_CODE = "_gaq.push(['_trackPageview']);" +CUSTOM_VARS_CONTEXT_KEY = "google_analytics_custom_vars" +CUSTOM_VAR_CODE = "_gaq.push(['_setCustomVar', %(index)d, '%(name)s', " \ + "'%(value)s', %(scope)d]);" class GoogleAnalyticsService(AnalyticalService): - KEY = 'google_analytics' - def __init__(self): self.property_id = self.get_required_setting( 'GOOGLE_ANALYTICS_PROPERTY_ID', PROPERTY_ID_RE, "must be a string looking like 'UA-XXXXXX-Y'") def render_head_bottom(self, context): - return TRACKING_CODE % {'property_id': self.property_id} + commands = self._get_custom_var_commands(context) + commands.append(TRACK_CODE) + return SETUP_CODE % {'property_id': self.property_id, + 'commands': " ".join(commands)} + + def _get_custom_var_commands(self, context): + commands = [] + vardefs = context.get(CUSTOM_VARS_CONTEXT_KEY, []) + for vardef in vardefs: + index = vardef[0] + if not 1 <= index <= 5: + raise ValueError("Google Analytics custom variable index must " + "be between 1 and 5: %s" % index) + name = vardef[1] + value = vardef[2] + if len(vardef) >= 4: + scope = vardef[3] + else: + scope = 2 + commands.append(CUSTOM_VAR_CODE % locals()) + return commands diff --git a/analytical/services/kiss_insights.py b/analytical/services/kiss_insights.py new file mode 100644 index 0000000..7143021 --- /dev/null +++ b/analytical/services/kiss_insights.py @@ -0,0 +1,39 @@ +""" +KISSinsights service. +""" + +import re + +from analytical.services.base import AnalyticalService + + +ACCOUNT_NUMBER_RE = re.compile(r'^\d{5}$') +SITE_CODE_RE = re.compile(r'^[\d\w]{3}$') +SETUP_CODE = """ + + +""" +IDENTIFY_CODE = "_kiq.push(['identify', '%s']);" +SHOW_SURVEY_CODE = "_kiq.push(['showSurvey', %s]);" +SHOW_SURVEY_CONTEXT_KEY = 'kiss_insights_show_survey' + +class KissInsightsService(AnalyticalService): + def __init__(self): + self.account_number = self.get_required_setting( + 'KISS_INSIGHTS_ACCOUNT_NUMBER', ACCOUNT_NUMBER_RE, + "must be a string containing an five-digit number") + self.site_code = self.get_required_setting('KISS_INSIGHTS_SITE_CODE', + SITE_CODE_RE, "must be a string containing three characters") + + def render_body_top(self, context): + commands = [] + identity = self.get_identity(context) + if identity is not None: + commands.append(IDENTIFY_CODE % identity) + try: + commands.append(SHOW_SURVEY_CODE + % context[SHOW_SURVEY_CONTEXT_KEY]) + except KeyError: + pass + return SETUP_CODE % {'account_number': self.account_number, + 'site_code': self.site_code, 'commands': " ".join(commands)} diff --git a/analytical/services/kissmetrics.py b/analytical/services/kiss_metrics.py similarity index 56% rename from analytical/services/kissmetrics.py rename to analytical/services/kiss_metrics.py index bc5c0e3..2868560 100644 --- a/analytical/services/kissmetrics.py +++ b/analytical/services/kiss_metrics.py @@ -4,13 +4,16 @@ KISSmetrics service. import re +from django.utils import simplejson + from analytical.services.base import AnalyticalService API_KEY_RE = re.compile(r'^[0-9a-f]{40}$') -TRACKING_CODE = """ +SETUP_CODE = """ """ +IDENTIFY_CODE = "_kmq.push(['identify', '%s']);" +JS_EVENT_CODE = "_kmq.push(['record', '%(name)s', %(properties)s]);" class KissMetricsService(AnalyticalService): - KEY = 'kissmetrics' - def __init__(self): - self.api_key = self.get_required_setting('KISSMETRICS_API_KEY', + self.api_key = self.get_required_setting('KISS_METRICS_API_KEY', API_KEY_RE, "must be a string containing a 40-digit hexadecimal number") def render_head_top(self, context): - return TRACKING_CODE % {'api_key': self.api_key} + commands = [] + identity = self.get_identity(context) + if identity is not None: + commands.append(IDENTIFY_CODE % identity) + return SETUP_CODE % {'api_key': self.api_key, + 'commands': commands} + + def render_event(self, name, properties): + return JS_EVENT_CODE % {'name': name, + 'properties': simplejson.dumps(properties)} diff --git a/analytical/services/kissinsights.py b/analytical/services/kissinsights.py deleted file mode 100644 index 15ac87d..0000000 --- a/analytical/services/kissinsights.py +++ /dev/null @@ -1,30 +0,0 @@ -""" -KISSinsights service. -""" - -import re - -from analytical.services.base import AnalyticalService - - -ACCOUNT_NUMBER_RE = re.compile(r'^\d{5}$') -SITE_CODE_RE = re.compile(r'^[\d\w]{3}$') -TRACKING_CODE = """ - - -""" - - -class KissInsightsService(AnalyticalService): - KEY = 'kissinsights' - - def __init__(self): - self.account_number = self.get_required_setting( - 'KISSINSIGHTS_ACCOUNT_NUMBER', ACCOUNT_NUMBER_RE, - "must be a string containing an five-digit number") - self.site_code = self.get_required_setting('KISSINSIGHTS_SITE_CODE', - SITE_CODE_RE, "must be a string containing three characters") - - def render_body_top(self, context): - return TRACKING_CODE % {'account_number': self.account_number, - 'site_code': self.site_code} diff --git a/analytical/services/mixpanel.py b/analytical/services/mixpanel.py index c20e147..82d5c06 100644 --- a/analytical/services/mixpanel.py +++ b/analytical/services/mixpanel.py @@ -4,35 +4,42 @@ Mixpanel service. import re +from django.utils import simplejson + from analytical.services.base import AnalyticalService MIXPANEL_TOKEN_RE = re.compile(r'^[0-9a-f]{32}$') -TRACKING_CODE = """ - - """ +IDENTIFY_CODE = "mpq.push(['identify', '%s']);" +EVENT_CODE = "mpq.push(['track', '%(name)s', %(properties)s]);" class MixpanelService(AnalyticalService): - KEY = 'mixpanel' - def __init__(self): self.token = self.get_required_setting('MIXPANEL_TOKEN', MIXPANEL_TOKEN_RE, "must be a string containing a 32-digit hexadecimal number") - def render_body_bottom(self, context): - return TRACKING_CODE % {'token': self.token} + def render_head_bottom(self, context): + commands = [] + identity = self.get_identity(context) + if identity is not None: + commands.append(IDENTIFY_CODE % identity) + return SETUP_CODE % {'token': self.token, + 'commands': " ".join(commands)} + + def render_event(self, name, properties): + return EVENT_CODE % {'name': name, + 'properties': simplejson.dumps(properties)} diff --git a/analytical/services/optimizely.py b/analytical/services/optimizely.py index 3209ec0..e4b9ae0 100644 --- a/analytical/services/optimizely.py +++ b/analytical/services/optimizely.py @@ -8,16 +8,14 @@ from analytical.services.base import AnalyticalService ACCOUNT_NUMBER_RE = re.compile(r'^\d{7}$') -TRACKING_CODE = """""" +SETUP_CODE = """""" class OptimizelyService(AnalyticalService): - KEY = 'optimizely' - def __init__(self): self.account_number = self.get_required_setting( 'OPTIMIZELY_ACCOUNT_NUMBER', ACCOUNT_NUMBER_RE, "must be a string containing an seven-digit number") def render_head_top(self, context): - return TRACKING_CODE % {'account_number': self.account_number} + return SETUP_CODE % {'account_number': self.account_number} diff --git a/analytical/templatetags/analytical.py b/analytical/templatetags/analytical.py index f4f9479..9d25a8f 100644 --- a/analytical/templatetags/analytical.py +++ b/analytical/templatetags/analytical.py @@ -5,12 +5,14 @@ from __future__ import absolute_import from django import template from django.conf import settings -from django.template import Node, TemplateSyntaxError +from django.template import Node, TemplateSyntaxError, Variable from analytical.services import get_enabled_services -DISABLE_CODE = "" +HTML_COMMENT_CODE = "" +JS_COMMENT_CODE = "/* %s */" +SCRIPT_CODE = """""" register = template.Library() @@ -25,7 +27,7 @@ def _location_tag(location): return tag for l in ['head_top', 'head_bottom', 'body_top', 'body_bottom']: - register.tag('analytical_%s' % l, _location_tag(l)) + register.tag('analytical_setup_%s' % l, _location_tag(l)) class AnalyticalNode(Node): @@ -36,13 +38,13 @@ class AnalyticalNode(Node): getattr(settings, 'INTERNAL_IPS', ())) def render(self, context): - html = "".join([self._render_service(service, context) + result = "".join([self._render_service(service, context) for service in get_enabled_services()]) - if not html: + if not result: return "" -# if self._is_internal_ip(context): -# return DISABLE_CODE % html - return html + if self._is_internal_ip(context): + return HTML_COMMENT_CODE % result + return result def _render_service(self, service, context): func = getattr(service, self.render_func_name) @@ -56,3 +58,42 @@ class AnalyticalNode(Node): return remote_ip in self.internal_ips except KeyError, AttributeError: return False + + +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 diff --git a/analytical/tests/services/__init__.py b/analytical/tests/services/__init__.py index c146338..6907280 100644 --- a/analytical/tests/services/__init__.py +++ b/analytical/tests/services/__init__.py @@ -2,11 +2,13 @@ Tests for the Analytical analytics services. """ +from analytical.tests.services.test_base import * +from analytical.tests.services.test_chartbeat import * from analytical.tests.services.test_clicky import * from analytical.tests.services.test_console import * from analytical.tests.services.test_crazy_egg import * from analytical.tests.services.test_google_analytics import * -from analytical.tests.services.test_kissinsights import * -from analytical.tests.services.test_kissmetrics import * +from analytical.tests.services.test_kiss_insights import * +from analytical.tests.services.test_kiss_metrics import * from analytical.tests.services.test_mixpanel import * from analytical.tests.services.test_optimizely import * diff --git a/analytical/tests/services/test_base.py b/analytical/tests/services/test_base.py new file mode 100644 index 0000000..23dd147 --- /dev/null +++ b/analytical/tests/services/test_base.py @@ -0,0 +1,74 @@ +""" +Tests for the base service. +""" + +import re + +from django.contrib.auth.models import User, AnonymousUser +from django.core.exceptions import ImproperlyConfigured +from django.http import HttpRequest +from django.test import TestCase + +from analytical.services.base import AnalyticalService +from analytical.tests.utils import TestSettingsManager + + +class DummyService(AnalyticalService): + def render_test(self, context): + return context + + +class BaseServiceTestCase(TestCase): + """ + Tests for the base service. + """ + + def setUp(self): + self.settings_manager = TestSettingsManager() + self.service = DummyService() + + def tearDown(self): + self.settings_manager.revert() + + def test_render(self): + r = self.service.render('test', 'foo') + self.assertEqual('foo', r) + + def test_get_required_setting(self): + self.settings_manager.set(TEST='test') + r = self.service.get_required_setting('TEST', re.compile('es'), 'fail') + self.assertEqual('test', r) + + def test_get_required_setting_missing(self): + self.settings_manager.delete('TEST') + self.assertRaises(ImproperlyConfigured, + self.service.get_required_setting, 'TEST', re.compile('es'), + 'fail') + + def test_get_required_setting_wrong(self): + self.settings_manager.set(TEST='test') + self.assertRaises(ImproperlyConfigured, + self.service.get_required_setting, 'TEST', re.compile('foo'), + 'fail') + + def test_get_identity_none(self): + context = {} + self.assertEqual(None, self.service.get_identity(context)) + + def test_get_identity_authenticated(self): + context = {'user': User(username='test')} + self.assertEqual('test', self.service.get_identity(context)) + + def test_get_identity_authenticated_request(self): + req = HttpRequest() + req.user = User(username='test') + context = {'request': req} + self.assertEqual('test', self.service.get_identity(context)) + + def test_get_identity_anonymous(self): + context = {'user': AnonymousUser()} + self.assertEqual(None, self.service.get_identity(context)) + + def test_get_identity_non_user(self): + context = {'user': object()} + self.assertEqual(None, self.service.get_identity(context)) diff --git a/analytical/tests/services/test_chartbeat.py b/analytical/tests/services/test_chartbeat.py new file mode 100644 index 0000000..3f31fbb --- /dev/null +++ b/analytical/tests/services/test_chartbeat.py @@ -0,0 +1,66 @@ +""" +Tests for the Chartbeat service. +""" + +from django.conf import settings +from django.contrib.sites.models import Site +from django.core.exceptions import ImproperlyConfigured +from django.http import HttpRequest +from django.test import TestCase + +from analytical.services.chartbeat import ChartbeatService +from analytical.tests.utils import TestSettingsManager + + +class ChartbeatTestCase(TestCase): + """ + Tests for the Chartbeat service. + """ + + def setUp(self): + self.settings_manager = TestSettingsManager() + self.settings_manager.set(CHARTBEAT_USER_ID='12345') + self.service = ChartbeatService() + + def tearDown(self): + self.settings_manager.revert() + + def test_empty_locations(self): + self.assertEqual(self.service.render_head_bottom({}), "") + self.assertEqual(self.service.render_body_top({}), "") + + def test_no_user_id(self): + self.settings_manager.delete('CHARTBEAT_USER_ID') + self.assertRaises(ImproperlyConfigured, ChartbeatService) + + def test_wrong_user_id(self): + self.settings_manager.set(CHARTBEAT_USER_ID='1234') + self.assertRaises(ImproperlyConfigured, ChartbeatService) + self.settings_manager.set(CHARTBEAT_USER_ID='123456') + self.assertRaises(ImproperlyConfigured, ChartbeatService) + + def test_rendering_init(self): + r = self.service.render_head_top({}) + self.assertTrue('var _sf_startpt=(new Date()).getTime()' in r, r) + + def test_rendering_setup(self): + r = self.service.render_body_bottom({'chartbeat_domain': "test.com"}) + self.assertTrue('var _sf_async_config={uid:12345,domain:"test.com"};' + in r, r) + + def test_rendering_setup_request_domain(self): + req = HttpRequest() + req.META['HTTP_HOST'] = 'test.com' + r = self.service.render_body_bottom({'request': req}) + self.assertTrue('var _sf_async_config={uid:12345,domain:"test.com"};' + in r, r) + + def test_rendering_setup_site(self): + installed_apps = list(settings.INSTALLED_APPS) + installed_apps.append('django.contrib.sites') + self.settings_manager.set(INSTALLED_APPS=installed_apps) + site = Site.objects.create(domain="test.com", name="test") + self.settings_manager.set(SITE_ID=site.id) + r = self.service.render_body_bottom({}) + self.assertTrue('var _sf_async_config={uid:12345,domain:"test.com"};' + in r, r) diff --git a/analytical/tests/services/test_clicky.py b/analytical/tests/services/test_clicky.py index d50387d..68e6bf7 100644 --- a/analytical/tests/services/test_clicky.py +++ b/analytical/tests/services/test_clicky.py @@ -2,6 +2,9 @@ Tests for the Clicky service. """ +import re + +from django.contrib.auth.models import User from django.core.exceptions import ImproperlyConfigured from django.test import TestCase @@ -31,7 +34,7 @@ class ClickyTestCase(TestCase): self.settings_manager.delete('CLICKY_SITE_ID') self.assertRaises(ImproperlyConfigured, ClickyService) - def test_wrong_id(self): + def test_wrong_site_id(self): self.settings_manager.set(CLICKY_SITE_ID='1234567') self.assertRaises(ImproperlyConfigured, ClickyService) self.settings_manager.set(CLICKY_SITE_ID='123456789') @@ -42,3 +45,16 @@ class ClickyTestCase(TestCase): self.assertTrue('var clicky_site_id = 12345678;' in r, r) self.assertTrue('src="http://in.getclicky.com/12345678ns.gif"' in r, r) + + def test_identify(self): + self.settings_manager.set(ANALYTICAL_AUTO_IDENTIFY=True) + r = self.service.render_body_bottom({'user': User(username='test')}) + self.assertTrue( + 'var clicky_custom = {"session": {"username": "test"}};' in r, + r) + + def test_custom(self): + custom = {'var1': 'val1', 'var2': 'val2'} + r = self.service.render_body_bottom({'clicky_custom': custom}) + self.assertTrue(re.search('var clicky_custom = {.*' + '"var1": "val1", "var2": "val2".*};', r), r) diff --git a/analytical/tests/services/test_console.py b/analytical/tests/services/test_console.py index 24068a3..d9e5eb5 100644 --- a/analytical/tests/services/test_console.py +++ b/analytical/tests/services/test_console.py @@ -2,6 +2,7 @@ Tests for the console debugging service. """ +from django.contrib.auth.models import User from django.test import TestCase from analytical.services.console import ConsoleService @@ -18,15 +19,27 @@ class ConsoleTestCase(TestCase): def test_render_head_top(self): r = self.service.render_head_top({}) self.assertTrue('rendering analytical_head_top tag' in r, r) + r = self.service.render_head_top({'user': User(username='test')}) + self.assertTrue('rendering analytical_head_top tag for user test' + in r, r) def test_render_head_bottom(self): r = self.service.render_head_bottom({}) self.assertTrue('rendering analytical_head_bottom tag' in r, r) + r = self.service.render_head_bottom({'user': User(username='test')}) + self.assertTrue('rendering analytical_head_bottom tag for user test' + in r, r) def test_render_body_top(self): r = self.service.render_body_top({}) self.assertTrue('rendering analytical_body_top tag' in r, r) + r = self.service.render_body_top({'user': User(username='test')}) + self.assertTrue('rendering analytical_body_top tag for user test' + in r, r) def test_render_body_bottom(self): r = self.service.render_body_bottom({}) self.assertTrue('rendering analytical_body_bottom tag' in r, r) + r = self.service.render_body_bottom({'user': User(username='test')}) + self.assertTrue('rendering analytical_body_bottom tag for user test' + in r, r) diff --git a/analytical/tests/services/test_crazy_egg.py b/analytical/tests/services/test_crazy_egg.py index 9dc1b17..9663440 100644 --- a/analytical/tests/services/test_crazy_egg.py +++ b/analytical/tests/services/test_crazy_egg.py @@ -31,7 +31,7 @@ class CrazyEggTestCase(TestCase): self.settings_manager.delete('CRAZY_EGG_ACCOUNT_NUMBER') self.assertRaises(ImproperlyConfigured, CrazyEggService) - def test_wrong_id(self): + def test_wrong_account_number(self): self.settings_manager.set(CRAZY_EGG_ACCOUNT_NUMBER='1234567') self.assertRaises(ImproperlyConfigured, CrazyEggService) self.settings_manager.set(CRAZY_EGG_ACCOUNT_NUMBER='123456789') @@ -40,3 +40,9 @@ class CrazyEggTestCase(TestCase): def test_rendering(self): r = self.service.render_body_bottom({}) self.assertTrue('/1234/5678.js' in r, r) + + def test_uservars(self): + context = {'crazy_egg_uservars': {1: 'foo', 2: 'bar'}} + r = self.service.render_body_bottom(context) + self.assertTrue("CE2.set(1, 'foo');" in r, r) + self.assertTrue("CE2.set(2, 'bar');" in r, r) diff --git a/analytical/tests/services/test_google_analytics.py b/analytical/tests/services/test_google_analytics.py index 37cb9d0..03c01b1 100644 --- a/analytical/tests/services/test_google_analytics.py +++ b/analytical/tests/services/test_google_analytics.py @@ -31,10 +31,26 @@ class GoogleAnalyticsTestCase(TestCase): self.settings_manager.delete('GOOGLE_ANALYTICS_PROPERTY_ID') self.assertRaises(ImproperlyConfigured, GoogleAnalyticsService) - def test_wrong_id(self): + def test_wrong_property_id(self): self.settings_manager.set(GOOGLE_ANALYTICS_PROPERTY_ID='wrong') self.assertRaises(ImproperlyConfigured, GoogleAnalyticsService) def test_rendering(self): r = self.service.render_head_bottom({}) self.assertTrue("_gaq.push(['_setAccount', 'UA-123456-7']);" in r, r) + self.assertTrue("_gaq.push(['_trackPageview']);" in r, r) + + def test_custom_vars(self): + context = {'google_analytics_custom_vars': [ + (1, 'test1', 'foo'), + (5, 'test2', 'bar', 1), + ]} + r = self.service.render_head_bottom(context) + self.assertTrue("_gaq.push(['_setCustomVar', 1, 'test1', 'foo', 2]);" + in r, r) + self.assertTrue("_gaq.push(['_setCustomVar', 5, 'test2', 'bar', 1]);" + in r, r) + self.assertRaises(ValueError, self.service.render_head_bottom, + {'google_analytics_custom_vars': [(0, 'test', 'test')]}) + self.assertRaises(ValueError, self.service.render_head_bottom, + {'google_analytics_custom_vars': [(6, 'test', 'test')]}) diff --git a/analytical/tests/services/test_kissinsights.py b/analytical/tests/services/test_kiss_insights.py similarity index 57% rename from analytical/tests/services/test_kissinsights.py rename to analytical/tests/services/test_kiss_insights.py index e89c781..8ac4da4 100644 --- a/analytical/tests/services/test_kissinsights.py +++ b/analytical/tests/services/test_kiss_insights.py @@ -2,10 +2,11 @@ Tests for the KISSinsights service. """ +from django.contrib.auth.models import User from django.core.exceptions import ImproperlyConfigured from django.test import TestCase -from analytical.services.kissinsights import KissInsightsService +from analytical.services.kiss_insights import KissInsightsService from analytical.tests.utils import TestSettingsManager @@ -16,8 +17,8 @@ class KissInsightsTestCase(TestCase): def setUp(self): self.settings_manager = TestSettingsManager() - self.settings_manager.set(KISSINSIGHTS_ACCOUNT_NUMBER='12345') - self.settings_manager.set(KISSINSIGHTS_SITE_CODE='abc') + self.settings_manager.set(KISS_INSIGHTS_ACCOUNT_NUMBER='12345') + self.settings_manager.set(KISS_INSIGHTS_SITE_CODE='abc') self.service = KissInsightsService() def tearDown(self): @@ -29,25 +30,34 @@ class KissInsightsTestCase(TestCase): self.assertEqual(self.service.render_body_bottom({}), "") def test_no_account_number(self): - self.settings_manager.delete('KISSINSIGHTS_ACCOUNT_NUMBER') + self.settings_manager.delete('KISS_INSIGHTS_ACCOUNT_NUMBER') self.assertRaises(ImproperlyConfigured, KissInsightsService) def test_no_site_code(self): - self.settings_manager.delete('KISSINSIGHTS_SITE_CODE') + self.settings_manager.delete('KISS_INSIGHTS_SITE_CODE') self.assertRaises(ImproperlyConfigured, KissInsightsService) def test_wrong_account_number(self): - self.settings_manager.set(KISSINSIGHTS_ACCOUNT_NUMBER='1234') + self.settings_manager.set(KISS_INSIGHTS_ACCOUNT_NUMBER='1234') self.assertRaises(ImproperlyConfigured, KissInsightsService) - self.settings_manager.set(KISSINSIGHTS_ACCOUNT_NUMBER='123456') + self.settings_manager.set(KISS_INSIGHTS_ACCOUNT_NUMBER='123456') self.assertRaises(ImproperlyConfigured, KissInsightsService) def test_wrong_site_id(self): - self.settings_manager.set(KISSINSIGHTS_SITE_CODE='ab') + self.settings_manager.set(KISS_INSIGHTS_SITE_CODE='ab') self.assertRaises(ImproperlyConfigured, KissInsightsService) - self.settings_manager.set(KISSINSIGHTS_SITE_CODE='abcd') + self.settings_manager.set(KISS_INSIGHTS_SITE_CODE='abcd') self.assertRaises(ImproperlyConfigured, KissInsightsService) def test_rendering(self): r = self.service.render_body_top({}) self.assertTrue("//s3.amazonaws.com/ki.js/12345/abc.js" in r, r) + + def test_identify(self): + self.settings_manager.set(ANALYTICAL_AUTO_IDENTIFY=True) + r = self.service.render_body_top({'user': User(username='test')}) + self.assertTrue("_kiq.push(['identify', 'test']);" in r, r) + + def test_show_survey(self): + r = self.service.render_body_top({'kiss_insights_show_survey': 1234}) + self.assertTrue("_kiq.push(['showSurvey', 1234]);" in r, r) diff --git a/analytical/tests/services/test_kissmetrics.py b/analytical/tests/services/test_kiss_metrics.py similarity index 53% rename from analytical/tests/services/test_kissmetrics.py rename to analytical/tests/services/test_kiss_metrics.py index a6e78b3..5829e23 100644 --- a/analytical/tests/services/test_kissmetrics.py +++ b/analytical/tests/services/test_kiss_metrics.py @@ -2,10 +2,11 @@ Tests for the KISSmetrics service. """ +from django.contrib.auth.models import User from django.core.exceptions import ImproperlyConfigured from django.test import TestCase -from analytical.services.kissmetrics import KissMetricsService +from analytical.services.kiss_metrics import KissMetricsService from analytical.tests.utils import TestSettingsManager @@ -16,8 +17,8 @@ class KissMetricsTestCase(TestCase): def setUp(self): self.settings_manager = TestSettingsManager() - self.settings_manager.set(KISSMETRICS_API_KEY='0123456789abcdef0123456' - '789abcdef01234567') + self.settings_manager.set(KISS_METRICS_API_KEY='0123456789abcdef012345' + '6789abcdef01234567') self.service = KissMetricsService() def tearDown(self): @@ -29,18 +30,29 @@ class KissMetricsTestCase(TestCase): self.assertEqual(self.service.render_body_bottom({}), "") def test_no_api_key(self): - self.settings_manager.delete('KISSMETRICS_API_KEY') + self.settings_manager.delete('KISS_METRICS_API_KEY') self.assertRaises(ImproperlyConfigured, KissMetricsService) def test_wrong_api_key(self): - self.settings_manager.set(KISSMETRICS_API_KEY='0123456789abcdef0123456' - '789abcdef0123456') + self.settings_manager.set(KISS_METRICS_API_KEY='0123456789abcdef012345' + '6789abcdef0123456') self.assertRaises(ImproperlyConfigured, KissMetricsService) - self.settings_manager.set(KISSMETRICS_API_KEY='0123456789abcdef0123456' - '789abcdef012345678') + self.settings_manager.set(KISS_METRICS_API_KEY='0123456789abcdef012345' + '6789abcdef012345678') self.assertRaises(ImproperlyConfigured, KissMetricsService) def test_rendering(self): r = self.service.render_head_top({}) self.assertTrue("//doug1izaerwt3.cloudfront.net/0123456789abcdef012345" "6789abcdef01234567.1.js" in r, r) + + def test_identify(self): + self.settings_manager.set(ANALYTICAL_AUTO_IDENTIFY=True) + r = self.service.render_head_top({'user': User(username='test')}) + self.assertTrue("_kmq.push(['identify', 'test']);" in r, r) + + def test_event(self): + r = self.service.render_event('test_event', {'prop1': 'val1', + 'prop2': 'val2'}) + self.assertEqual(r, "_kmq.push(['record', 'test_event', " + '{"prop1": "val1", "prop2": "val2"}]);') diff --git a/analytical/tests/services/test_mixpanel.py b/analytical/tests/services/test_mixpanel.py index 1d2ae68..92226c7 100644 --- a/analytical/tests/services/test_mixpanel.py +++ b/analytical/tests/services/test_mixpanel.py @@ -2,6 +2,7 @@ Tests for the Mixpanel service. """ +from django.contrib.auth.models import User from django.core.exceptions import ImproperlyConfigured from django.test import TestCase @@ -25,8 +26,8 @@ class MixpanelTestCase(TestCase): def test_empty_locations(self): self.assertEqual(self.service.render_head_top({}), "") - self.assertEqual(self.service.render_head_bottom({}), "") self.assertEqual(self.service.render_body_top({}), "") + self.assertEqual(self.service.render_body_bottom({}), "") def test_no_token(self): self.settings_manager.delete('MIXPANEL_TOKEN') @@ -41,6 +42,18 @@ class MixpanelTestCase(TestCase): self.assertRaises(ImproperlyConfigured, MixpanelService) def test_rendering(self): - r = self.service.render_body_bottom({}) - self.assertTrue("MixpanelLib('0123456789abcdef0123456789abcdef')" in r, + r = self.service.render_head_bottom({}) + self.assertTrue( + "mpq.push(['init', '0123456789abcdef0123456789abcdef']);" in r, r) + + def test_identify(self): + self.settings_manager.set(ANALYTICAL_AUTO_IDENTIFY=True) + r = self.service.render_head_bottom({'user': User(username='test')}) + self.assertTrue("mpq.push(['identify', 'test']);" in r, r) + + def test_event(self): + r = self.service.render_event('test_event', {'prop1': 'val1', + 'prop2': 'val2'}) + self.assertEqual(r, "mpq.push(['track', 'test_event', " + '{"prop1": "val1", "prop2": "val2"}]);') diff --git a/analytical/tests/test_services.py b/analytical/tests/test_services.py index d11dbf6..beb7187 100644 --- a/analytical/tests/test_services.py +++ b/analytical/tests/test_services.py @@ -18,30 +18,18 @@ class GetEnabledServicesTestCase(TestCase): def setUp(self): services.enabled_services = None - services.load_services = self._dummy_load_services_1 + services.load_services = lambda: 'test' def tearDown(self): + services.enabled_services = None services.load_services = load_services - def test_no_reload(self): + def test_get_enabled_services(self): result = services.get_enabled_services() - self.assertEqual(result, 'test1') - services.load_services = self._dummy_load_services_2 + self.assertEqual(result, 'test') + services.load_services = lambda: 'test2' result = services.get_enabled_services() - self.assertEqual(result, 'test1') - - def test_reload(self): - result = services.get_enabled_services() - self.assertEqual(result, 'test1') - services.load_services = self._dummy_load_services_2 - result = services.get_enabled_services(reload=True) - self.assertEqual(result, 'test2') - - def _dummy_load_services_1(self): - return 'test1' - - def _dummy_load_services_2(self): - return 'test2' + self.assertEqual(result, 'test') class LoadServicesTestCase(TestCase): @@ -53,6 +41,7 @@ class LoadServicesTestCase(TestCase): self.settings_manager = TestSettingsManager() self.settings_manager.delete('ANALYTICAL_SERVICES') self.settings_manager.delete('CLICKY_SITE_ID') + self.settings_manager.delete('CHARTBEAT_USER_ID') self.settings_manager.delete('CRAZY_EGG_ACCOUNT_NUMBER') self.settings_manager.delete('GOOGLE_ANALYTICS_PROPERTY_ID') self.settings_manager.delete('KISSINSIGHTS_ACCOUNT_NUMBER') @@ -60,9 +49,11 @@ class LoadServicesTestCase(TestCase): self.settings_manager.delete('KISSMETRICS_API_KEY') self.settings_manager.delete('MIXPANEL_TOKEN') self.settings_manager.delete('OPTIMIZELY_ACCOUNT_NUMBER') + services.enabled_services = None def tearDown(self): self.settings_manager.revert() + services.enabled_services = None def test_no_services(self): self.assertEqual(load_services(), []) diff --git a/analytical/tests/test_template_tags.py b/analytical/tests/test_template_tags.py index de21617..f6a9d8e 100644 --- a/analytical/tests/test_template_tags.py +++ b/analytical/tests/test_template_tags.py @@ -2,8 +2,11 @@ Tests for the template tags. """ +from django.http import HttpRequest +from django import template from django.test import TestCase +from analytical import services from analytical.tests.utils import TestSettingsManager @@ -14,6 +17,74 @@ class TemplateTagsTestCase(TestCase): def setUp(self): self.settings_manager = TestSettingsManager() + self.settings_manager.set(ANALYTICAL_SERVICES=[ + 'analytical.services.console.ConsoleService']) + services.enabled_services = None def tearDown(self): self.settings_manager.revert() + services.enabled_services = None + + def render_location_tag(self, location, context=None): + if context is None: context = {} + t = template.Template( + "{%% load analytical %%}{%% analytical_setup_%s %%}" + % location) + return t.render(template.Context(context)) + + def test_location_tags(self): + for l in ['head_top', 'head_bottom', 'body_top', 'body_bottom']: + r = self.render_location_tag(l) + self.assertTrue('rendering analytical_%s tag' % l 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' + for l in ['head_top', 'head_bottom', 'body_top', 'body_bottom']: + r = self.render_location_tag(l, {'request': req}) + self.assertTrue('