diff --git a/analytical/__init__.py b/analytical/__init__.py index 29462f7..aebc2c2 100644 --- a/analytical/__init__.py +++ b/analytical/__init__.py @@ -13,10 +13,3 @@ __email__ = "joost@cassee.net" __version__ = "0.2.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 deleted file mode 100644 index ccd09d8..0000000 --- a/analytical/context_processors.py +++ /dev/null @@ -1,33 +0,0 @@ -""" -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/templatetags/analytical.py b/analytical/templatetags/analytical.py index c5a917f..4f20cf9 100644 --- a/analytical/templatetags/analytical.py +++ b/analytical/templatetags/analytical.py @@ -5,36 +5,24 @@ Analytical template tags and filters. import logging from django import template -from django.conf import settings from django.core.exceptions import ImproperlyConfigured from django.template import Node, TemplateSyntaxError from django.utils.importlib import import_module -from analytical.templatetags import chartbeat, clicky, crazy_egg, \ - google_analytics, hubspot, kiss_insights, kiss_metrics, mixpanel, \ - optimizely - -TAG_NODES = { - 'head_top': [ - chartbeat.ChartbeatTopNode, # Chartbeat should come first - kiss_metrics.KissMetricsNode, - optimizely.OptimizelyNode, - ], - 'head_bottom': [ - google_analytics.GoogleAnalyticsNode, - mixpanel.MixpanelNode, - ], - 'body_top': [ - kiss_insights.KissInsightsNode, - ], - 'body_bottom': [ - clicky.ClickyNode, - crazy_egg.CrazyEggNode, - hubspot.HubSpotNode, - chartbeat.ChartbeatBottomNode, # Chartbeat should come last - ], -} +TAG_LOCATIONS = ['head_top', 'head_bottom', 'body_top', 'body_bottom'] +TAG_POSITIONS = ['first', None, 'last'] +TAG_MODULES = [ + 'analytical.chartbeat', + 'analytical.clicky', + 'analytical.crazy_egg', + 'analytical.google_analytics', + 'analytical.hubspot', + 'analytical.kiss_insights', + 'analytical.kiss_metrics', + 'analytical.mixpanel', + 'analytical.optimizely' +] logger = logging.getLogger(__name__) @@ -49,29 +37,36 @@ def _location_tag(location): return AnalyticalNode(location) return analytical_tag -for loc in TAG_NODES.keys(): +for loc in TAG_LOCATIONS: register.tag('analytical_%s' % loc, _location_tag(loc)) class AnalyticalNode(Node): def __init__(self, location): - self.nodes = template_nodes[location] + self.nodes = [node_cls() for node_cls in template_nodes[location]] def render(self, context): return "".join([node.render(context) for node in self.nodes]) def _load_template_nodes(): - location_nodes = {} - for location, node_classes in TAG_NODES.items(): - location_nodes[location] = [] - for node_class in node_classes: - try: - node = node_class() - except ImproperlyConfigured, e: - logger.debug("not loading analytical service '%s': %s", - node_class.name, e) - location_nodes.append(node) - return location_nodes + template_nodes = dict((l, dict((p, []) for p in TAG_POSITIONS)) + for l in TAG_LOCATIONS) + def add_node_cls(location, node, position=None): + template_nodes[location][position].append(node) + for path in TAG_MODULES: + module = _import_tag_module(path) + try: + module.contribute_to_analytical(add_node_cls) + except ImproperlyConfigured, e: + logger.debug("not loading tags from '%s': %s", path, e) + for location in TAG_LOCATIONS: + template_nodes[location] = sum((template_nodes[location][p] + for p in TAG_POSITIONS), []) + return template_nodes + +def _import_tag_module(path): + app_name, lib_name = path.rsplit('.', 1) + return import_module("%s.templatetags.%s" % (app_name, lib_name)) template_nodes = _load_template_nodes() diff --git a/analytical/templatetags/chartbeat.py b/analytical/templatetags/chartbeat.py index c1b00bd..a49e7c2 100644 --- a/analytical/templatetags/chartbeat.py +++ b/analytical/templatetags/chartbeat.py @@ -12,7 +12,8 @@ from django.core.exceptions import ImproperlyConfigured from django.template import Library, Node, TemplateSyntaxError from django.utils import simplejson -from analytical.utils import is_internal_ip, disable_html +from analytical.utils import is_internal_ip, disable_html, validate_setting, \ + get_required_setting USER_ID_RE = re.compile(r'^\d{5}$') @@ -56,8 +57,6 @@ def chartbeat_top(parser, token): return ChartbeatTopNode() class ChartbeatTopNode(Node): - name = 'Chartbeat top code' - def render(self, context): if is_internal_ip(context): return disable_html(INIT_CODE, self.name) @@ -79,10 +78,8 @@ def chartbeat_bottom(parser, token): return ChartbeatBottomNode() class ChartbeatBottomNode(Node): - name = 'Chartbeat bottom code' - def __init__(self): - self.user_id = self.get_required_setting( + self.user_id = get_required_setting( 'CHARTBEAT_USER_ID', USER_ID_RE, "must be a string containing an five-digit number") @@ -97,6 +94,13 @@ class ChartbeatBottomNode(Node): if domain is not None: config['domain'] = domain html = SETUP_CODE % {'config': simplejson.dumps(config)} - if is_internal_ip(context): - html = disable_html(html, self.name) + if is_internal_ip(context, 'CHARTBEAT'): + html = disable_html(html, 'Chartbeat') return html + + +def contribute_to_analytical(add_node): + validate_setting('CHARTBEAT_USER_ID', USER_ID_RE, + "must be a string containing an five-digit number") + add_node('head_top', ChartbeatTopNode, 'first') + add_node('body_bottom', ChartbeatBottomNode, 'last') diff --git a/analytical/templatetags/clicky.py b/analytical/templatetags/clicky.py index a4e86b2..9cdc939 100644 --- a/analytical/templatetags/clicky.py +++ b/analytical/templatetags/clicky.py @@ -9,8 +9,8 @@ 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 +from analytical.utils import get_identity, is_internal_ip, disable_html, \ + validate_setting, get_required_setting SITE_ID_RE = re.compile(r'^\d{8}$') @@ -49,24 +49,29 @@ def clicky(parser, token): return ClickyNode() class ClickyNode(Node): - name = 'Clicky' - 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 + for dict_ in context: + for var, val in dict_.items(): + if var.startswith('clicky_'): + custom[var[7:]] = val if 'username' not in custom.get('session', {}): - identity = get_identity(context) + identity = get_identity(context, 'clicky') 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, self.name) + if is_internal_ip(context, 'CLICKY'): + html = disable_html(html, 'Clicky') return html + + +def contribute_to_analytical(add_node): + validate_setting('CLICKY_SITE_ID', SITE_ID_RE, + "must be a string containing an eight-digit number") + add_node('body_bottom', ClickyNode) diff --git a/analytical/templatetags/crazy_egg.py b/analytical/templatetags/crazy_egg.py index 5e19a61..00abd39 100644 --- a/analytical/templatetags/crazy_egg.py +++ b/analytical/templatetags/crazy_egg.py @@ -8,7 +8,8 @@ import re from django.template import Library, Node, TemplateSyntaxError -from analytical.utils import is_internal_ip, disable_html +from analytical.utils import is_internal_ip, disable_html, validate_setting, \ + get_required_setting ACCOUNT_NUMBER_RE = re.compile(r'^\d{8}$') @@ -34,10 +35,8 @@ def crazy_egg(parser, token): return CrazyEggNode() class CrazyEggNode(Node): - name = 'Crazy Egg' - def __init__(self): - self.account_nr = self.get_required_setting('CRAZY_EGG_ACCOUNT_NUMBER', + self.account_nr = get_required_setting('CRAZY_EGG_ACCOUNT_NUMBER', ACCOUNT_NUMBER_RE, "must be a string containing an eight-digit number") @@ -51,6 +50,12 @@ class CrazyEggNode(Node): for (varnr, value) in vars) html = '%s\n' \ % (html, js) - if is_internal_ip(context): - html = disable_html(html, self.name) + if is_internal_ip(context, 'CRAZY_EGG'): + html = disable_html(html, 'Crazy Egg') return html + + +def contribute_to_analytical(add_node): + validate_setting('CRAZY_EGG_ACCOUNT_NUMBER', ACCOUNT_NUMBER_RE, + "must be a string containing an eight-digit number") + add_node('body_bottom', CrazyEggNode) diff --git a/analytical/templatetags/google_analytics.py b/analytical/templatetags/google_analytics.py index f2490d8..8c67846 100644 --- a/analytical/templatetags/google_analytics.py +++ b/analytical/templatetags/google_analytics.py @@ -8,7 +8,8 @@ import re from django.template import Library, Node, TemplateSyntaxError -from analytical.utils import is_internal_ip, disable_html +from analytical.utils import is_internal_ip, disable_html, validate_setting, \ + get_required_setting SCOPE_VISITOR = 1 SCOPE_SESSION = 2 @@ -30,8 +31,8 @@ SETUP_CODE = """ """ -CUSTOM_VAR_CODE = "_gaq.push(['_setCustomVar', %(index)d, '%(name)s', " \ - "'%(value)s', %(scope)d]);" +CUSTOM_VAR_CODE = "_gaq.push(['_setCustomVar', %(index)s, '%(name)s', " \ + "'%(value)s', %(scope)s]);" register = Library() @@ -52,10 +53,8 @@ def google_analytics(parser, token): return GoogleAnalyticsNode() class GoogleAnalyticsNode(Node): - name = 'Google Analytics' - def __init__(self): - self.property_id = self.get_required_setting( + self.property_id = get_required_setting( 'GOOGLE_ANALYTICS_PROPERTY_ID', PROPERTY_ID_RE, "must be a string looking like 'UA-XXXXXX-Y'") @@ -63,21 +62,27 @@ class GoogleAnalyticsNode(Node): commands = self._get_custom_var_commands(context) html = SETUP_CODE % {'property_id': self.property_id, 'commands': " ".join(commands)} - if is_internal_ip(context): - html = disable_html(html, self.name) + if is_internal_ip(context, 'GOOGLE_ANALYTICS'): + html = disable_html(html, 'Google Analytics') return html def _get_custom_var_commands(self, context): - values = (context.get('google_analytics_var%d' % i) + values = (context.get('google_analytics_var%s' % i) for i in range(1, 6)) vars = [(i, v) for i, v in enumerate(values, 1) if v is not None] commands = [] - for index, value in vars: - name = value[0] - value = value[1] + for index, var in vars: + name = var[0] + value = var[1] try: - scope = value[2] + scope = var[2] except IndexError: scope = SCOPE_PAGE commands.append(CUSTOM_VAR_CODE % locals()) return commands + + +def contribute_to_analytical(add_node): + validate_setting('GOOGLE_ANALYTICS_PROPERTY_ID', PROPERTY_ID_RE, + "must be a string looking like 'UA-XXXXXX-Y'") + add_node('head_bottom', GoogleAnalyticsNode) diff --git a/analytical/templatetags/hubspot.py b/analytical/templatetags/hubspot.py index 5284943..d958c2b 100644 --- a/analytical/templatetags/hubspot.py +++ b/analytical/templatetags/hubspot.py @@ -8,7 +8,8 @@ import re from django.template import Library, Node, TemplateSyntaxError -from analytical.utils import get_required_setting, is_internal_ip, disable_html +from analytical.utils import is_internal_ip, disable_html, validate_setting, \ + get_required_setting PORTAL_ID_RE = re.compile(r'^\d+$') @@ -18,7 +19,7 @@ TRACKING_CODE = """ var hs_portalid = %(portal_id)s; var hs_salog_version = "2.00"; var hs_ppa = "%(domain)s"; - document.write(unescape("%3Cscript src='" + document.location.protocol + "//" + hs_ppa + "/salog.js.aspx' type='text/javascript'%3E%3C/script%3E")); + document.write(unescape("%%3Cscript src='" + document.location.protocol + "//" + hs_ppa + "/salog.js.aspx' type='text/javascript'%%3E%%3C/script%%3E")); """ @@ -41,17 +42,23 @@ def hubspot(parser, token): return HubSpotNode() class HubSpotNode(Node): - name = 'HubSpot' - def __init__(self): - self.site_id = get_required_setting('HUPSPOT_PORTAL_ID', + self.portal_id = get_required_setting('HUBSPOT_PORTAL_ID', PORTAL_ID_RE, "must be a (string containing a) number") - self.domain = get_required_setting('HUPSPOT_DOMAIN', + self.domain = get_required_setting('HUBSPOT_DOMAIN', DOMAIN_RE, "must be an internet domain name") def render(self, context): html = TRACKING_CODE % {'portal_id': self.portal_id, 'domain': self.domain} - if is_internal_ip(context): + if is_internal_ip(context, 'HUBSPOT'): html = disable_html(html, self.name) return html + + +def contribute_to_analytical(add_node): + validate_setting('HUBSPOT_PORTAL_ID', PORTAL_ID_RE, + "must be a (string containing a) number") + validate_setting('HUBSPOT_DOMAIN', DOMAIN_RE, + "must be an internet domain name") + add_node('body_bottom', HubSpotNode) diff --git a/analytical/templatetags/kiss_insights.py b/analytical/templatetags/kiss_insights.py index 7f68d9f..1c93d7a 100644 --- a/analytical/templatetags/kiss_insights.py +++ b/analytical/templatetags/kiss_insights.py @@ -8,6 +8,9 @@ import re from django.template import Library, Node, TemplateSyntaxError +from analytical.utils import validate_setting, get_identity, \ + get_required_setting + ACCOUNT_NUMBER_RE = re.compile(r'^\d+$') SITE_CODE_RE = re.compile(r'^[\w]{3}$') @@ -39,18 +42,16 @@ def kiss_insights(parser, token): return KissInsightsNode() class KissInsightsNode(Node): - name = 'KISSinsights' - def __init__(self): - self.account_number = self.get_required_setting( + self.account_number = get_required_setting( 'KISS_INSIGHTS_ACCOUNT_NUMBER', ACCOUNT_NUMBER_RE, "must be (a string containing) a number") - self.site_code = self.get_required_setting('KISS_INSIGHTS_SITE_CODE', + self.site_code = get_required_setting('KISS_INSIGHTS_SITE_CODE', SITE_CODE_RE, "must be a string containing three characters") def render(self, context): commands = [] - identity = self.get_identity(context) + identity = get_identity(context, 'kiss_insights') if identity is not None: commands.append(IDENTIFY_CODE % identity) try: @@ -61,3 +62,9 @@ class KissInsightsNode(Node): html = SETUP_CODE % {'account_number': self.account_number, 'site_code': self.site_code, 'commands': " ".join(commands)} return html + + +def contribute_to_analytical(add_node): + validate_setting('KISS_INSIGHTS_ACCOUNT_NUMBER', ACCOUNT_NUMBER_RE, + "must be (a string containing) a number") + add_node('body_top', KissInsightsNode) diff --git a/analytical/templatetags/kiss_metrics.py b/analytical/templatetags/kiss_metrics.py index 61ce903..69ce2e0 100644 --- a/analytical/templatetags/kiss_metrics.py +++ b/analytical/templatetags/kiss_metrics.py @@ -9,7 +9,8 @@ import re from django.template import Library, Node, TemplateSyntaxError from django.utils import simplejson -from analytical.utils import is_internal_ip, disable_html +from analytical.utils import is_internal_ip, disable_html, get_identity, \ + validate_setting, get_required_setting API_KEY_RE = re.compile(r'^[0-9a-f]{40}$') @@ -53,16 +54,14 @@ def kiss_metrics(parser, token): return KissMetricsNode() class KissMetricsNode(Node): - name = 'KISSmetrics' - def __init__(self): - self.api_key = self.get_required_setting('KISS_METRICS_API_KEY', + self.api_key = get_required_setting('KISS_METRICS_API_KEY', API_KEY_RE, "must be a string containing a 40-digit hexadecimal number") def render(self, context): commands = [] - identity = self.get_identity(context) + identity = get_identity(context, 'kiss_metrics') if identity is not None: commands.append(IDENTIFY_CODE % identity) try: @@ -73,6 +72,12 @@ class KissMetricsNode(Node): pass html = TRACKING_CODE % {'api_key': self.api_key, 'commands': " ".join(commands)} - if is_internal_ip(context): - html = disable_html(html, self.name) + if is_internal_ip(context, 'KISS_METRICS'): + html = disable_html(html, 'KISSmetrics') return html + + +def contribute_to_analytical(add_node): + validate_setting('KISS_METRICS_API_KEY', API_KEY_RE, + "must be a string containing a 40-digit hexadecimal number") + add_node('head_top', KissMetricsNode) diff --git a/analytical/templatetags/mixpanel.py b/analytical/templatetags/mixpanel.py index 36daa5c..d3846b8 100644 --- a/analytical/templatetags/mixpanel.py +++ b/analytical/templatetags/mixpanel.py @@ -9,7 +9,8 @@ import re from django.template import Library, Node, TemplateSyntaxError from django.utils import simplejson -from analytical.utils import is_internal_ip, disable_html +from analytical.utils import is_internal_ip, disable_html, get_identity, \ + validate_setting, get_required_setting MIXPANEL_TOKEN_RE = re.compile(r'^[0-9a-f]{32}$') @@ -27,7 +28,7 @@ TRACKING_CODE = """ """ IDENTIFY_CODE = "mpq.push(['identify', '%s']);" EVENT_CODE = "mpq.push(['track', '%(name)s', %(properties)s]);" -EVENT_CONTEXT_KEY = 'mixpanel_metrics_event' +EVENT_CONTEXT_KEY = 'mixpanel_event' register = Library() @@ -46,16 +47,14 @@ def mixpanel(parser, token): return MixpanelNode() class MixpanelNode(Node): - name = 'Mixpanel' - def __init__(self): - self.token = self.get_required_setting('MIXPANEL_TOKEN', + self.token = get_required_setting('MIXPANEL_TOKEN', MIXPANEL_TOKEN_RE, "must be a string containing a 32-digit hexadecimal number") def render(self, context): commands = [] - identity = self.get_identity(context) + identity = get_identity(context, 'mixpanel') if identity is not None: commands.append(IDENTIFY_CODE % identity) try: @@ -66,6 +65,12 @@ class MixpanelNode(Node): pass html = TRACKING_CODE % {'token': self.token, 'commands': " ".join(commands)} - if is_internal_ip(context): - html = disable_html(html, self.name) + if is_internal_ip(context, 'MIXPANEL'): + html = disable_html(html, 'Mixpanel') return html + + +def contribute_to_analytical(add_node): + validate_setting('MIXPANEL_TOKEN', MIXPANEL_TOKEN_RE, + "must be a string containing a 32-digit hexadecimal number") + add_node('head_bottom', MixpanelNode) diff --git a/analytical/templatetags/optimizely.py b/analytical/templatetags/optimizely.py index ea91f81..99e7323 100644 --- a/analytical/templatetags/optimizely.py +++ b/analytical/templatetags/optimizely.py @@ -8,7 +8,8 @@ import re from django.template import Library, Node, TemplateSyntaxError -from analytical.utils import is_internal_ip, disable_html +from analytical.utils import is_internal_ip, disable_html, validate_setting, \ + get_required_setting ACCOUNT_NUMBER_RE = re.compile(r'^\d{7}$') @@ -33,15 +34,19 @@ def optimizely(parser, token): return OptimizelyNode() class OptimizelyNode(Node): - name = 'Optimizely' - def __init__(self): - self.account_number = self.get_required_setting( + self.account_number = get_required_setting( 'OPTIMIZELY_ACCOUNT_NUMBER', ACCOUNT_NUMBER_RE, "must be a string containing an seven-digit number") def render(self, context): html = SETUP_CODE % {'account_number': self.account_number} - if is_internal_ip(context): + if is_internal_ip(context, 'OPTIMIZELY'): html = disable_html(html, self.name) return html + + +def contribute_to_analytical(add_node): + validate_setting('OPTIMIZELY_ACCOUNT_NUMBER', ACCOUNT_NUMBER_RE, + "must be a string containing an seven-digit number") + add_node('head_top', OptimizelyNode) diff --git a/analytical/tests/__init__.py b/analytical/tests/__init__.py index 2648c6a..dd969f2 100644 --- a/analytical/tests/__init__.py +++ b/analytical/tests/__init__.py @@ -2,7 +2,14 @@ Tests for django-analytical. """ -from analytical.tests.test_services import * -from analytical.tests.test_template_tags import * - -from analytical.tests.services import * +from analytical.tests.test_tag_analytical import * +from analytical.tests.test_tag_chartbeat import * +from analytical.tests.test_tag_clicky import * +from analytical.tests.test_tag_crazy_egg import * +from analytical.tests.test_tag_google_analytics import * +from analytical.tests.test_tag_hubspot import * +from analytical.tests.test_tag_kiss_insights import * +from analytical.tests.test_tag_kiss_metrics import * +from analytical.tests.test_tag_mixpanel import * +from analytical.tests.test_tag_optimizely import * +from analytical.tests.test_utils import * diff --git a/analytical/tests/services/__init__.py b/analytical/tests/services/__init__.py deleted file mode 100644 index 6907280..0000000 --- a/analytical/tests/services/__init__.py +++ /dev/null @@ -1,14 +0,0 @@ -""" -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_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 deleted file mode 100644 index 23dd147..0000000 --- a/analytical/tests/services/test_base.py +++ /dev/null @@ -1,74 +0,0 @@ -""" -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_clicky.py b/analytical/tests/services/test_clicky.py deleted file mode 100644 index 68e6bf7..0000000 --- a/analytical/tests/services/test_clicky.py +++ /dev/null @@ -1,60 +0,0 @@ -""" -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 - -from analytical.services.clicky import ClickyService -from analytical.tests.utils import TestSettingsManager - - -class ClickyTestCase(TestCase): - """ - Tests for the Clicky service. - """ - - def setUp(self): - self.settings_manager = TestSettingsManager() - self.settings_manager.set(CLICKY_SITE_ID='12345678') - self.service = ClickyService() - - def tearDown(self): - self.settings_manager.revert() - - 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({}), "") - - def test_no_site_id(self): - self.settings_manager.delete('CLICKY_SITE_ID') - self.assertRaises(ImproperlyConfigured, ClickyService) - - 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') - self.assertRaises(ImproperlyConfigured, ClickyService) - - def test_rendering(self): - r = self.service.render_body_bottom({}) - 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 deleted file mode 100644 index d9e5eb5..0000000 --- a/analytical/tests/services/test_console.py +++ /dev/null @@ -1,45 +0,0 @@ -""" -Tests for the console debugging service. -""" - -from django.contrib.auth.models import User -from django.test import TestCase - -from analytical.services.console import ConsoleService - - -class ConsoleTestCase(TestCase): - """ - Tests for the console debugging service. - """ - - def setUp(self): - self.service = ConsoleService() - - 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 deleted file mode 100644 index 9663440..0000000 --- a/analytical/tests/services/test_crazy_egg.py +++ /dev/null @@ -1,48 +0,0 @@ -""" -Tests for the Crazy Egg service. -""" - -from django.core.exceptions import ImproperlyConfigured -from django.test import TestCase - -from analytical.services.crazy_egg import CrazyEggService -from analytical.tests.utils import TestSettingsManager - - -class CrazyEggTestCase(TestCase): - """ - Tests for the Crazy Egg service. - """ - - def setUp(self): - self.settings_manager = TestSettingsManager() - self.settings_manager.set(CRAZY_EGG_ACCOUNT_NUMBER='12345678') - self.service = CrazyEggService() - - def tearDown(self): - self.settings_manager.revert() - - 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({}), "") - - def test_no_account_number(self): - self.settings_manager.delete('CRAZY_EGG_ACCOUNT_NUMBER') - self.assertRaises(ImproperlyConfigured, CrazyEggService) - - 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') - self.assertRaises(ImproperlyConfigured, CrazyEggService) - - 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 deleted file mode 100644 index 03c01b1..0000000 --- a/analytical/tests/services/test_google_analytics.py +++ /dev/null @@ -1,56 +0,0 @@ -""" -Tests for the Google Analytics service. -""" - -from django.core.exceptions import ImproperlyConfigured -from django.test import TestCase - -from analytical.services.google_analytics import GoogleAnalyticsService -from analytical.tests.utils import TestSettingsManager - - -class GoogleAnalyticsTestCase(TestCase): - """ - Tests for the Google Analytics service. - """ - - def setUp(self): - self.settings_manager = TestSettingsManager() - self.settings_manager.set(GOOGLE_ANALYTICS_PROPERTY_ID='UA-123456-7') - self.service = GoogleAnalyticsService() - - def tearDown(self): - self.settings_manager.revert() - - def test_empty_locations(self): - self.assertEqual(self.service.render_head_top({}), "") - self.assertEqual(self.service.render_body_top({}), "") - self.assertEqual(self.service.render_body_bottom({}), "") - - def test_no_property_id(self): - self.settings_manager.delete('GOOGLE_ANALYTICS_PROPERTY_ID') - self.assertRaises(ImproperlyConfigured, GoogleAnalyticsService) - - 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_kiss_insights.py b/analytical/tests/services/test_kiss_insights.py deleted file mode 100644 index 8ac4da4..0000000 --- a/analytical/tests/services/test_kiss_insights.py +++ /dev/null @@ -1,63 +0,0 @@ -""" -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.kiss_insights import KissInsightsService -from analytical.tests.utils import TestSettingsManager - - -class KissInsightsTestCase(TestCase): - """ - Tests for the KISSinsights service. - """ - - def setUp(self): - self.settings_manager = TestSettingsManager() - self.settings_manager.set(KISS_INSIGHTS_ACCOUNT_NUMBER='12345') - self.settings_manager.set(KISS_INSIGHTS_SITE_CODE='abc') - self.service = KissInsightsService() - - def tearDown(self): - self.settings_manager.revert() - - 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_bottom({}), "") - - def test_no_account_number(self): - self.settings_manager.delete('KISS_INSIGHTS_ACCOUNT_NUMBER') - self.assertRaises(ImproperlyConfigured, KissInsightsService) - - def test_no_site_code(self): - self.settings_manager.delete('KISS_INSIGHTS_SITE_CODE') - self.assertRaises(ImproperlyConfigured, KissInsightsService) - - def test_wrong_account_number(self): - self.settings_manager.set(KISS_INSIGHTS_ACCOUNT_NUMBER='1234') - self.assertRaises(ImproperlyConfigured, KissInsightsService) - self.settings_manager.set(KISS_INSIGHTS_ACCOUNT_NUMBER='123456') - self.assertRaises(ImproperlyConfigured, KissInsightsService) - - def test_wrong_site_id(self): - self.settings_manager.set(KISS_INSIGHTS_SITE_CODE='ab') - self.assertRaises(ImproperlyConfigured, KissInsightsService) - 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_kiss_metrics.py b/analytical/tests/services/test_kiss_metrics.py deleted file mode 100644 index 5829e23..0000000 --- a/analytical/tests/services/test_kiss_metrics.py +++ /dev/null @@ -1,58 +0,0 @@ -""" -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.kiss_metrics import KissMetricsService -from analytical.tests.utils import TestSettingsManager - - -class KissMetricsTestCase(TestCase): - """ - Tests for the KISSmetrics service. - """ - - def setUp(self): - self.settings_manager = TestSettingsManager() - self.settings_manager.set(KISS_METRICS_API_KEY='0123456789abcdef012345' - '6789abcdef01234567') - self.service = KissMetricsService() - - 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({}), "") - self.assertEqual(self.service.render_body_bottom({}), "") - - def test_no_api_key(self): - self.settings_manager.delete('KISS_METRICS_API_KEY') - self.assertRaises(ImproperlyConfigured, KissMetricsService) - - def test_wrong_api_key(self): - self.settings_manager.set(KISS_METRICS_API_KEY='0123456789abcdef012345' - '6789abcdef0123456') - self.assertRaises(ImproperlyConfigured, KissMetricsService) - 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 deleted file mode 100644 index 92226c7..0000000 --- a/analytical/tests/services/test_mixpanel.py +++ /dev/null @@ -1,59 +0,0 @@ -""" -Tests for the Mixpanel service. -""" - -from django.contrib.auth.models import User -from django.core.exceptions import ImproperlyConfigured -from django.test import TestCase - -from analytical.services.mixpanel import MixpanelService -from analytical.tests.utils import TestSettingsManager - - -class MixpanelTestCase(TestCase): - """ - Tests for the Mixpanel service. - """ - - def setUp(self): - self.settings_manager = TestSettingsManager() - self.settings_manager.set( - MIXPANEL_TOKEN='0123456789abcdef0123456789abcdef') - self.service = MixpanelService() - - def tearDown(self): - self.settings_manager.revert() - - def test_empty_locations(self): - self.assertEqual(self.service.render_head_top({}), "") - 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') - self.assertRaises(ImproperlyConfigured, MixpanelService) - - def test_wrong_token(self): - self.settings_manager.set( - MIXPANEL_TOKEN='0123456789abcdef0123456789abcde') - self.assertRaises(ImproperlyConfigured, MixpanelService) - self.settings_manager.set( - MIXPANEL_TOKEN='0123456789abcdef0123456789abcdef0') - self.assertRaises(ImproperlyConfigured, MixpanelService) - - def test_rendering(self): - 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/services/test_optimizely.py b/analytical/tests/services/test_optimizely.py deleted file mode 100644 index feebd6f..0000000 --- a/analytical/tests/services/test_optimizely.py +++ /dev/null @@ -1,42 +0,0 @@ -""" -Tests for the Optimizely service. -""" - -from django.core.exceptions import ImproperlyConfigured -from django.test import TestCase - -from analytical.services.optimizely import OptimizelyService -from analytical.tests.utils import TestSettingsManager - - -class OptimizelyTestCase(TestCase): - """ - Tests for the Optimizely service. - """ - - def setUp(self): - self.settings_manager = TestSettingsManager() - self.settings_manager.set(OPTIMIZELY_ACCOUNT_NUMBER='1234567') - self.service = OptimizelyService() - - 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({}), "") - self.assertEqual(self.service.render_body_bottom({}), "") - - def test_no_account_number(self): - self.settings_manager.delete('OPTIMIZELY_ACCOUNT_NUMBER') - self.assertRaises(ImproperlyConfigured, OptimizelyService) - - def test_wrong_account_number(self): - self.settings_manager.set(OPTIMIZELY_ACCOUNT_NUMBER='123456') - self.assertRaises(ImproperlyConfigured, OptimizelyService) - self.settings_manager.set(OPTIMIZELY_ACCOUNT_NUMBER='12345678') - self.assertRaises(ImproperlyConfigured, OptimizelyService) - - def test_rendering(self): - self.assertEqual(self.service.render_head_top({}), - '') diff --git a/analytical/tests/templatetags/__init__.py b/analytical/tests/templatetags/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/analytical/tests/templatetags/dummy.py b/analytical/tests/templatetags/dummy.py new file mode 100644 index 0000000..40d9781 --- /dev/null +++ b/analytical/tests/templatetags/dummy.py @@ -0,0 +1,38 @@ +""" +Dummy testing template tags and filters. +""" + +from __future__ import absolute_import + +from django.template import Library, Node, TemplateSyntaxError + +from analytical.templatetags.analytical import TAG_LOCATIONS + + +register = Library() + + +def _location_node(location): + class DummyNode(Node): + def render(self, context): + return "" % location + return DummyNode + +_location_nodes = dict((l, _location_node(l)) for l in TAG_LOCATIONS) + + +def _location_tag(location): + def dummy_tag(parser, token): + bits = token.split_contents() + if len(bits) > 1: + raise TemplateSyntaxError("'%s' tag takes no arguments" % bits[0]) + return _location_nodes[location] + return dummy_tag + +for loc in TAG_LOCATIONS: + register.tag('dummy_%s' % loc, _location_tag(loc)) + + +def contribute_to_analytical(add_node_cls): + for location in TAG_LOCATIONS: + add_node_cls(location, _location_nodes[location]) diff --git a/analytical/tests/test_services.py b/analytical/tests/test_services.py deleted file mode 100644 index beb7187..0000000 --- a/analytical/tests/test_services.py +++ /dev/null @@ -1,80 +0,0 @@ -""" -Tests for the services package. -""" - -from django.core.exceptions import ImproperlyConfigured -from django.test import TestCase - -from analytical import services -from analytical.services import load_services -from analytical.tests.utils import TestSettingsManager -from analytical.services.google_analytics import GoogleAnalyticsService - - -class GetEnabledServicesTestCase(TestCase): - """ - Tests for get_enabled_services. - """ - - def setUp(self): - services.enabled_services = None - services.load_services = lambda: 'test' - - def tearDown(self): - services.enabled_services = None - services.load_services = load_services - - def test_get_enabled_services(self): - result = services.get_enabled_services() - self.assertEqual(result, 'test') - services.load_services = lambda: 'test2' - result = services.get_enabled_services() - self.assertEqual(result, 'test') - - -class LoadServicesTestCase(TestCase): - """ - Tests for load_services. - """ - - def setUp(self): - 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') - self.settings_manager.delete('KISSINSIGHTS_SITE_CODE') - 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(), []) - - def test_enabled_service(self): - self.settings_manager.set(GOOGLE_ANALYTICS_PROPERTY_ID='UA-1234567-8') - results = load_services() - self.assertEqual(len(results), 1, results) - self.assertTrue(isinstance(results[0], GoogleAnalyticsService), - results) - - def test_explicit_service(self): - self.settings_manager.set(ANALYTICAL_SERVICES=[ - 'analytical.services.google_analytics.GoogleAnalyticsService']) - self.settings_manager.set(GOOGLE_ANALYTICS_PROPERTY_ID='UA-1234567-8') - results = load_services() - self.assertEqual(len(results), 1, results) - self.assertTrue(isinstance(results[0], GoogleAnalyticsService), - results) - - def test_explicit_service_misconfigured(self): - self.settings_manager.set(ANALYTICAL_SERVICES=[ - 'analytical.services.google_analytics.GoogleAnalyticsService']) - self.assertRaises(ImproperlyConfigured, load_services) diff --git a/analytical/tests/test_tag_analytical.py b/analytical/tests/test_tag_analytical.py new file mode 100644 index 0000000..8439197 --- /dev/null +++ b/analytical/tests/test_tag_analytical.py @@ -0,0 +1,37 @@ +""" +Tests for the generic template tags and filters. +""" + +from django.template import Context, Template + +from analytical.templatetags import analytical +from analytical.tests.utils import TagTestCase + + +class AnalyticsTagTestCase(TagTestCase): + """ + Tests for the ``analytical`` template tags. + """ + + def setUp(self): + super(AnalyticsTagTestCase, self).setUp() + self._tag_modules = analytical.TAG_MODULES + analytical.TAG_MODULES = ['analytical.tests.dummy'] + analytical.template_nodes = analytical._load_template_nodes() + + def tearDown(self): + analytical.TAG_MODULES = self._tag_modules + analytical.template_nodes = analytical._load_template_nodes() + super(AnalyticsTagTestCase, self).tearDown() + + def render_location_tag(self, location, vars=None): + if vars is None: + vars = {} + t = Template("{%% load analytical %%}{%% analytical_%s %%}" + % location) + return t.render(Context(vars)) + + def test_location_tags(self): + for l in ['head_top', 'head_bottom', 'body_top', 'body_bottom']: + r = self.render_location_tag(l) + self.assertTrue('dummy_%s' % l in r, r) diff --git a/analytical/tests/services/test_chartbeat.py b/analytical/tests/test_tag_chartbeat.py similarity index 50% rename from analytical/tests/services/test_chartbeat.py rename to analytical/tests/test_tag_chartbeat.py index 4afe7fd..fa3bbbd 100644 --- a/analytical/tests/services/test_chartbeat.py +++ b/analytical/tests/test_tag_chartbeat.py @@ -1,62 +1,69 @@ """ -Tests for the Chartbeat service. +Tests for the Chartbeat template tags and filters. """ import re 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 django.template import Context -from analytical.services.chartbeat import ChartbeatService -from analytical.tests.utils import TestSettingsManager +from analytical.templatetags.chartbeat import ChartbeatTopNode, \ + ChartbeatBottomNode +from analytical.tests.utils import TagTestCase +from analytical.utils import AnalyticalException -class ChartbeatTestCase(TestCase): +class ChartbeatTagTestCase(TagTestCase): """ - Tests for the Chartbeat service. + Tests for the ``chartbeat`` template tag. """ def setUp(self): - self.settings_manager = TestSettingsManager() + super(ChartbeatTagTestCase, self).setUp() 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({}) + def test_top_tag(self): + r = self.render_tag('chartbeat', 'chartbeat_top', + {'chartbeat_domain': "test.com"}) 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"}) + def test_bottom_tag(self): + r = self.render_tag('chartbeat', 'chartbeat_bottom', + {'chartbeat_domain': "test.com"}) self.assertTrue(re.search( 'var _sf_async_config={.*"uid": "12345".*};', r), r) self.assertTrue(re.search( 'var _sf_async_config={.*"domain": "test.com".*};', r), r) + def test_top_node(self): + r = ChartbeatTopNode().render( + Context({'chartbeat_domain': "test.com"})) + self.assertTrue('var _sf_startpt=(new Date()).getTime()' in r, r) + + def test_bottom_node(self): + r = ChartbeatBottomNode().render( + Context({'chartbeat_domain': "test.com"})) + self.assertTrue(re.search( + 'var _sf_async_config={.*"uid": "12345".*};', r), r) + self.assertTrue(re.search( + 'var _sf_async_config={.*"domain": "test.com".*};', r), r) + + def test_no_user_id(self): + self.settings_manager.delete('CHARTBEAT_USER_ID') + self.assertRaises(AnalyticalException, ChartbeatBottomNode) + + def test_wrong_user_id(self): + self.settings_manager.set(CHARTBEAT_USER_ID='1234') + self.assertRaises(AnalyticalException, ChartbeatBottomNode) + self.settings_manager.set(CHARTBEAT_USER_ID='123456') + self.assertRaises(AnalyticalException, ChartbeatBottomNode) + def test_rendering_setup_no_site(self): installed_apps = [a for a in settings.INSTALLED_APPS if a != 'django.contrib.sites'] self.settings_manager.set(INSTALLED_APPS=installed_apps) - r = self.service.render_body_bottom({}) + r = ChartbeatBottomNode().render(Context()) self.assertTrue('var _sf_async_config={"uid": "12345"};' in r, r) def test_rendering_setup_site(self): @@ -65,7 +72,7 @@ class ChartbeatTestCase(TestCase): 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({}) + r = ChartbeatBottomNode().render(Context()) self.assertTrue(re.search( 'var _sf_async_config={.*"uid": "12345".*};', r), r) self.assertTrue(re.search( diff --git a/analytical/tests/test_tag_clicky.py b/analytical/tests/test_tag_clicky.py new file mode 100644 index 0000000..2130dbb --- /dev/null +++ b/analytical/tests/test_tag_clicky.py @@ -0,0 +1,57 @@ +""" +Tests for the Clicky template tags and filters. +""" + +import re + +from django.contrib.auth.models import User +from django.template import Context + +from analytical.templatetags.clicky import ClickyNode +from analytical.tests.utils import TagTestCase +from analytical.utils import AnalyticalException + + +class ClickyTagTestCase(TagTestCase): + """ + Tests for the ``clicky`` template tag. + """ + + def setUp(self): + super(ClickyTagTestCase, self).setUp() + self.settings_manager.set(CLICKY_SITE_ID='12345678') + + def test_tag(self): + r = self.render_tag('clicky', 'clicky') + self.assertTrue('var clicky_site_id = 12345678;' in r, r) + self.assertTrue('src="http://in.getclicky.com/12345678ns.gif"' in r, + r) + + def test_node(self): + r = ClickyNode().render(Context({})) + self.assertTrue('var clicky_site_id = 12345678;' in r, r) + self.assertTrue('src="http://in.getclicky.com/12345678ns.gif"' in r, + r) + + def test_no_site_id(self): + self.settings_manager.delete('CLICKY_SITE_ID') + self.assertRaises(AnalyticalException, ClickyNode) + + def test_wrong_site_id(self): + self.settings_manager.set(CLICKY_SITE_ID='1234567') + self.assertRaises(AnalyticalException, ClickyNode) + self.settings_manager.set(CLICKY_SITE_ID='123456789') + self.assertRaises(AnalyticalException, ClickyNode) + + def test_identify(self): + self.settings_manager.set(ANALYTICAL_AUTO_IDENTIFY=True) + r = ClickyNode().render(Context({'user': User(username='test')})) + self.assertTrue( + 'var clicky_custom = {"session": {"username": "test"}};' in r, + r) + + def test_custom(self): + r = ClickyNode().render(Context({'clicky_var1': 'val1', + 'clicky_var2': 'val2'})) + self.assertTrue(re.search('var clicky_custom = {.*' + '"var1": "val1", "var2": "val2".*};', r), r) diff --git a/analytical/tests/test_tag_crazy_egg.py b/analytical/tests/test_tag_crazy_egg.py new file mode 100644 index 0000000..755d2e2 --- /dev/null +++ b/analytical/tests/test_tag_crazy_egg.py @@ -0,0 +1,43 @@ +""" +Tests for the Crazy Egg template tags and filters. +""" + +from django.template import Context + +from analytical.templatetags.crazy_egg import CrazyEggNode +from analytical.tests.utils import TagTestCase +from analytical.utils import AnalyticalException + + +class CrazyEggTagTestCase(TagTestCase): + """ + Tests for the ``crazy_egg`` template tag. + """ + + def setUp(self): + super(CrazyEggTagTestCase, self).setUp() + self.settings_manager.set(CRAZY_EGG_ACCOUNT_NUMBER='12345678') + + def test_tag(self): + r = self.render_tag('crazy_egg', 'crazy_egg') + self.assertTrue('/1234/5678.js' in r, r) + + def test_node(self): + r = CrazyEggNode().render(Context()) + self.assertTrue('/1234/5678.js' in r, r) + + def test_no_account_number(self): + self.settings_manager.delete('CRAZY_EGG_ACCOUNT_NUMBER') + self.assertRaises(AnalyticalException, CrazyEggNode) + + def test_wrong_account_number(self): + self.settings_manager.set(CRAZY_EGG_ACCOUNT_NUMBER='1234567') + self.assertRaises(AnalyticalException, CrazyEggNode) + self.settings_manager.set(CRAZY_EGG_ACCOUNT_NUMBER='123456789') + self.assertRaises(AnalyticalException, CrazyEggNode) + + def test_uservars(self): + context = Context({'crazy_egg_var1': 'foo', 'crazy_egg_var2': 'bar'}) + r = CrazyEggNode().render(context) + self.assertTrue("CE2.set(1, 'foo');" in r, r) + self.assertTrue("CE2.set(2, 'bar');" in r, r) diff --git a/analytical/tests/test_tag_google_analytics.py b/analytical/tests/test_tag_google_analytics.py new file mode 100644 index 0000000..119d441 --- /dev/null +++ b/analytical/tests/test_tag_google_analytics.py @@ -0,0 +1,46 @@ +""" +Tests for the Google Analytics template tags and filters. +""" + +from django.template import Context + +from analytical.templatetags.google_analytics import GoogleAnalyticsNode +from analytical.tests.utils import TagTestCase +from analytical.utils import AnalyticalException + + +class GoogleAnalyticsTagTestCase(TagTestCase): + """ + Tests for the ``google_analytics`` template tag. + """ + + def setUp(self): + super(GoogleAnalyticsTagTestCase, self).setUp() + self.settings_manager.set(GOOGLE_ANALYTICS_PROPERTY_ID='UA-123456-7') + + def test_tag(self): + r = self.render_tag('google_analytics', 'google_analytics') + self.assertTrue("_gaq.push(['_setAccount', 'UA-123456-7']);" in r, r) + self.assertTrue("_gaq.push(['_trackPageview']);" in r, r) + + def test_node(self): + r = GoogleAnalyticsNode().render(Context()) + self.assertTrue("_gaq.push(['_setAccount', 'UA-123456-7']);" in r, r) + self.assertTrue("_gaq.push(['_trackPageview']);" in r, r) + + def test_no_property_id(self): + self.settings_manager.delete('GOOGLE_ANALYTICS_PROPERTY_ID') + self.assertRaises(AnalyticalException, GoogleAnalyticsNode) + + def test_wrong_property_id(self): + self.settings_manager.set(GOOGLE_ANALYTICS_PROPERTY_ID='wrong') + self.assertRaises(AnalyticalException, GoogleAnalyticsNode) + + def test_custom_vars(self): + context = Context({'google_analytics_var1': ('test1', 'foo'), + 'google_analytics_var5': ('test2', 'bar', 1)}) + r = GoogleAnalyticsNode().render(context) + self.assertTrue("_gaq.push(['_setCustomVar', 1, 'test1', 'foo', 3]);" + in r, r) + self.assertTrue("_gaq.push(['_setCustomVar', 5, 'test2', 'bar', 1]);" + in r, r) diff --git a/analytical/tests/test_tag_hubspot.py b/analytical/tests/test_tag_hubspot.py new file mode 100644 index 0000000..9d2374e --- /dev/null +++ b/analytical/tests/test_tag_hubspot.py @@ -0,0 +1,46 @@ +""" +Tests for the HubSpot template tags and filters. +""" + +from django.template import Context + +from analytical.templatetags.hubspot import HubSpotNode +from analytical.tests.utils import TagTestCase +from analytical.utils import AnalyticalException + + +class HubSpotTagTestCase(TagTestCase): + """ + Tests for the ``hubspot`` template tag. + """ + + def setUp(self): + super(HubSpotTagTestCase, self).setUp() + self.settings_manager.set(HUBSPOT_PORTAL_ID='1234') + self.settings_manager.set(HUBSPOT_DOMAIN='example.com') + + def test_tag(self): + r = self.render_tag('hubspot', 'hubspot') + self.assertTrue('var hs_portalid = 1234;' in r, r) + self.assertTrue('var hs_ppa = "example.com";' in r, r) + + def test_node(self): + r = HubSpotNode().render(Context()) + self.assertTrue('var hs_portalid = 1234;' in r, r) + self.assertTrue('var hs_ppa = "example.com";' in r, r) + + def test_no_portal_id(self): + self.settings_manager.delete('HUBSPOT_PORTAL_ID') + self.assertRaises(AnalyticalException, HubSpotNode) + + def test_wrong_portal_id(self): + self.settings_manager.set(HUBSPOT_PORTAL_ID='wrong') + self.assertRaises(AnalyticalException, HubSpotNode) + + def test_no_domain(self): + self.settings_manager.delete('HUBSPOT_DOMAIN') + self.assertRaises(AnalyticalException, HubSpotNode) + + def test_wrong_domain(self): + self.settings_manager.set(HUBSPOT_DOMAIN='wrong domain') + self.assertRaises(AnalyticalException, HubSpotNode) diff --git a/analytical/tests/test_tag_kiss_insights.py b/analytical/tests/test_tag_kiss_insights.py new file mode 100644 index 0000000..b678bea --- /dev/null +++ b/analytical/tests/test_tag_kiss_insights.py @@ -0,0 +1,57 @@ +""" +Tests for the KISSinsights template tags and filters. +""" + +from django.contrib.auth.models import User +from django.template import Context + +from analytical.templatetags.kiss_insights import KissInsightsNode +from analytical.tests.utils import TagTestCase +from analytical.utils import AnalyticalException + + +class KissInsightsTagTestCase(TagTestCase): + """ + Tests for the ``kiss_insights`` template tag. + """ + + def setUp(self): + super(KissInsightsTagTestCase, self).setUp() + self.settings_manager.set(KISS_INSIGHTS_ACCOUNT_NUMBER='12345') + self.settings_manager.set(KISS_INSIGHTS_SITE_CODE='abc') + + def test_tag(self): + r = self.render_tag('kiss_insights', 'kiss_insights') + self.assertTrue("//s3.amazonaws.com/ki.js/12345/abc.js" in r, r) + + def test_node(self): + r = KissInsightsNode().render(Context()) + self.assertTrue("//s3.amazonaws.com/ki.js/12345/abc.js" in r, r) + + def test_no_account_number(self): + self.settings_manager.delete('KISS_INSIGHTS_ACCOUNT_NUMBER') + self.assertRaises(AnalyticalException, KissInsightsNode) + + def test_no_site_code(self): + self.settings_manager.delete('KISS_INSIGHTS_SITE_CODE') + self.assertRaises(AnalyticalException, KissInsightsNode) + + def test_wrong_account_number(self): + self.settings_manager.set(KISS_INSIGHTS_ACCOUNT_NUMBER='abcde') + self.assertRaises(AnalyticalException, KissInsightsNode) + + def test_wrong_site_id(self): + self.settings_manager.set(KISS_INSIGHTS_SITE_CODE='ab') + self.assertRaises(AnalyticalException, KissInsightsNode) + self.settings_manager.set(KISS_INSIGHTS_SITE_CODE='abcd') + self.assertRaises(AnalyticalException, KissInsightsNode) + + def test_identify(self): + self.settings_manager.set(ANALYTICAL_AUTO_IDENTIFY=True) + r = KissInsightsNode().render(Context({'user': User(username='test')})) + self.assertTrue("_kiq.push(['identify', 'test']);" in r, r) + + def test_show_survey(self): + r = KissInsightsNode().render( + Context({'kiss_insights_show_survey': 1234})) + self.assertTrue("_kiq.push(['showSurvey', 1234]);" in r, r) diff --git a/analytical/tests/test_tag_kiss_metrics.py b/analytical/tests/test_tag_kiss_metrics.py new file mode 100644 index 0000000..5f9ed16 --- /dev/null +++ b/analytical/tests/test_tag_kiss_metrics.py @@ -0,0 +1,54 @@ +""" +Tests for the KISSmetrics tags and filters. +""" + +from django.contrib.auth.models import User +from django.template import Context + +from analytical.templatetags.kiss_metrics import KissMetricsNode +from analytical.tests.utils import TagTestCase +from analytical.utils import AnalyticalException + + +class KissMetricsTagTestCase(TagTestCase): + """ + Tests for the ``kiss_metrics`` template tag. + """ + + def setUp(self): + super(KissMetricsTagTestCase, self).setUp() + self.settings_manager.set(KISS_METRICS_API_KEY='0123456789abcdef012345' + '6789abcdef01234567') + + def test_tag(self): + r = self.render_tag('kiss_metrics', 'kiss_metrics') + self.assertTrue("//doug1izaerwt3.cloudfront.net/0123456789abcdef012345" + "6789abcdef01234567.1.js" in r, r) + + def test_node(self): + r = KissMetricsNode().render(Context()) + self.assertTrue("//doug1izaerwt3.cloudfront.net/0123456789abcdef012345" + "6789abcdef01234567.1.js" in r, r) + + def test_no_api_key(self): + self.settings_manager.delete('KISS_METRICS_API_KEY') + self.assertRaises(AnalyticalException, KissMetricsNode) + + def test_wrong_api_key(self): + self.settings_manager.set(KISS_METRICS_API_KEY='0123456789abcdef012345' + '6789abcdef0123456') + self.assertRaises(AnalyticalException, KissMetricsNode) + self.settings_manager.set(KISS_METRICS_API_KEY='0123456789abcdef012345' + '6789abcdef012345678') + self.assertRaises(AnalyticalException, KissMetricsNode) + + def test_identify(self): + self.settings_manager.set(ANALYTICAL_AUTO_IDENTIFY=True) + r = KissMetricsNode().render(Context({'user': User(username='test')})) + self.assertTrue("_kmq.push(['identify', 'test']);" in r, r) + + def test_event(self): + r = KissMetricsNode().render(Context({'kiss_metrics_event': + ('test_event', {'prop1': 'val1', 'prop2': 'val2'})})) + self.assertTrue("_kmq.push(['record', 'test_event', " + '{"prop1": "val1", "prop2": "val2"}]);' in r, r) diff --git a/analytical/tests/test_tag_mixpanel.py b/analytical/tests/test_tag_mixpanel.py new file mode 100644 index 0000000..40d3fb2 --- /dev/null +++ b/analytical/tests/test_tag_mixpanel.py @@ -0,0 +1,57 @@ +""" +Tests for the Mixpanel tags and filters. +""" + +from django.contrib.auth.models import User +from django.template import Context + +from analytical.templatetags.mixpanel import MixpanelNode +from analytical.tests.utils import TagTestCase +from analytical.utils import AnalyticalException + + +class MixpanelTagTestCase(TagTestCase): + """ + Tests for the ``mixpanel`` template tag. + """ + + def setUp(self): + super(MixpanelTagTestCase, self).setUp() + self.settings_manager.set(OPTIMIZELY_ACCOUNT_NUMBER='1234567') + self.settings_manager.set( + MIXPANEL_TOKEN='0123456789abcdef0123456789abcdef') + + def test_tag(self): + r = self.render_tag('mixpanel', 'mixpanel') + self.assertTrue( + "mpq.push(['init', '0123456789abcdef0123456789abcdef']);" in r, + r) + + def test_node(self): + r = MixpanelNode().render(Context()) + self.assertTrue( + "mpq.push(['init', '0123456789abcdef0123456789abcdef']);" in r, + r) + + def test_no_token(self): + self.settings_manager.delete('MIXPANEL_TOKEN') + self.assertRaises(AnalyticalException, MixpanelNode) + + def test_wrong_token(self): + self.settings_manager.set( + MIXPANEL_TOKEN='0123456789abcdef0123456789abcde') + self.assertRaises(AnalyticalException, MixpanelNode) + self.settings_manager.set( + MIXPANEL_TOKEN='0123456789abcdef0123456789abcdef0') + self.assertRaises(AnalyticalException, MixpanelNode) + + def test_identify(self): + self.settings_manager.set(ANALYTICAL_AUTO_IDENTIFY=True) + r = MixpanelNode().render(Context({'user': User(username='test')})) + self.assertTrue("mpq.push(['identify', 'test']);" in r, r) + + def test_event(self): + r = MixpanelNode().render(Context({'mixpanel_event': + ('test_event', {'prop1': 'val1', 'prop2': 'val2'})})) + self.assertTrue("mpq.push(['track', 'test_event', " + '{"prop1": "val1", "prop2": "val2"}]);' in r, r) diff --git a/analytical/tests/test_tag_optimizely.py b/analytical/tests/test_tag_optimizely.py new file mode 100644 index 0000000..34742bf --- /dev/null +++ b/analytical/tests/test_tag_optimizely.py @@ -0,0 +1,39 @@ +""" +Tests for the Optimizely template tags and filters. +""" + +from django.template import Context + +from analytical.templatetags.optimizely import OptimizelyNode +from analytical.tests.utils import TagTestCase +from analytical.utils import AnalyticalException + + +class OptimizelyTagTestCase(TagTestCase): + """ + Tests for the ``optimizely`` template tag. + """ + + def setUp(self): + super(OptimizelyTagTestCase, self).setUp() + self.settings_manager.set(OPTIMIZELY_ACCOUNT_NUMBER='1234567') + + def test_tag(self): + self.assertEqual( + '', + self.render_tag('optimizely', 'optimizely')) + + def test_node(self): + self.assertEqual( + '', + OptimizelyNode().render(Context())) + + def test_no_account_number(self): + self.settings_manager.delete('OPTIMIZELY_ACCOUNT_NUMBER') + self.assertRaises(AnalyticalException, OptimizelyNode) + + def test_wrong_account_number(self): + self.settings_manager.set(OPTIMIZELY_ACCOUNT_NUMBER='123456') + self.assertRaises(AnalyticalException, OptimizelyNode) + self.settings_manager.set(OPTIMIZELY_ACCOUNT_NUMBER='12345678') + self.assertRaises(AnalyticalException, OptimizelyNode) diff --git a/analytical/tests/test_template_tags.py b/analytical/tests/test_template_tags.py deleted file mode 100644 index f6a9d8e..0000000 --- a/analytical/tests/test_template_tags.py +++ /dev/null @@ -1,90 +0,0 @@ -""" -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 - - -class TemplateTagsTestCase(TestCase): - """ - Tests for the template tags. - """ - - 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('" -def get_required_setting(self, setting, value_re, invalid_msg): +def validate_setting(setting, value_re, invalid_msg): + try: + get_required_setting(setting, value_re, invalid_msg) + except AnalyticalException, e: + raise ImproperlyConfigured(e) + + +def get_required_setting(setting, value_re, invalid_msg): try: value = getattr(settings, setting) except AttributeError: - raise ImproperlyConfigured("%s setting: not found" % setting) + raise AnalyticalException("%s setting: not found" % setting) value = str(value) if not value_re.search(value): - raise ImproperlyConfigured("%s setting: %s: '%s'" + raise AnalyticalException("%s setting: %s: '%s'" % (setting, invalid_msg, value)) return value -def get_identity(context): +def get_identity(context, prefix=None): + if prefix is not None: + try: + return context['%s_identity' % prefix] + except KeyError: + pass try: - return context[IDENTITY_CONTEXT_KEY] + return context['analytical_identity'] except KeyError: pass if getattr(settings, 'ANALYTICAL_AUTO_IDENTIFY', True): @@ -41,16 +52,33 @@ def get_identity(context): return None -def is_internal_ip(context): +def is_internal_ip(context, prefix=None): 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', [])) + remote_ip = request.META.get('HTTP_X_FORWARDED_FOR', '') + if not remote_ip: + remote_ip = request.META.get('HTTP_X_FORWARDED_FOR', '') + + internal_ips = '' + if prefix is not None: + internal_ips = getattr(settings, '%s_INTERNAL_IPS' % prefix, '') + if not internal_ips: + internal_ips = getattr(settings, 'ANALYTICAL_INTERNAL_IPS', '') + if not internal_ips: + internal_ips = getattr(settings, 'INTERNAL_IPS', '') + + return remote_ip in internal_ips except KeyError, AttributeError: return False def disable_html(self, html, service): return HTML_COMMENT % locals() + + +class AnalyticalException(Exception): + """ + Raised when an exception occurs in any django-analytical code that should + be silenced in templates. + """ + silent_variable_failure = True diff --git a/setup.py b/setup.py index 4de3786..6cedbbc 100644 --- a/setup.py +++ b/setup.py @@ -17,6 +17,7 @@ except ImportError: class TestCommand(Command): + description = "run package tests" user_options = [] def initialize_options(self):