commit 1fa0d7760d463ee2fce22064144f1f085d7c8ea0 Author: Joost Cassee Date: Fri Jan 21 02:01:40 2011 +0100 Create project diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..0870b90 --- /dev/null +++ b/.gitignore @@ -0,0 +1,9 @@ +/.* +!/.gitignore + +/build +/dist +/MANIFEST + +*.pyc +*.pyo diff --git a/LICENSE.txt b/LICENSE.txt new file mode 100644 index 0000000..c223404 --- /dev/null +++ b/LICENSE.txt @@ -0,0 +1,19 @@ +Copyright (C) 2011 Joost Cassee + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/MANIFEST.in b/MANIFEST.in new file mode 100644 index 0000000..42eb410 --- /dev/null +++ b/MANIFEST.in @@ -0,0 +1 @@ +include LICENSE.txt diff --git a/README.rst b/README.rst new file mode 100644 index 0000000..d045c3e --- /dev/null +++ b/README.rst @@ -0,0 +1,36 @@ +django-analytical +----------------- + +The django-analytical application integrates various analytics services +into a Django_ project. Currently supported services: + +* `Clicky`_ -- traffic analysis +* `Crazy Egg`_ -- visual click tracking +* `Google Analytics`_ traffic analysis +* `KISSinsights`_ -- feedback surveys +* `KISSmetrics`_ -- funnel analysis +* `Mixpanel`_ -- event tracking +* `Optimizely`_ -- A/B testing + +The documentation can be found in the ``docs`` directory or `read +online`_. The source code and issue tracker are `hosted on GitHub`_. + +Copyright (C) 2011 Joost Cassee . This software is +licensed under the MIT License (see LICENSE.txt). + +This application was inspired by and uses ideas from Analytical_, +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/ +.. _Clicky: http://getclicky.com/ +.. _`Crazy Egg`: http://www.crazyegg.com/ +.. _`Google Analytics`: http://www.google.com/analytics/ +.. _KISSinsights: http://www.kissinsights.com/ +.. _KISSmetrics: http://www.kissmetrics.com/ +.. _Mixpanel: http://www.mixpanel.com/ +.. _Optimizely: http://www.optimizely.com/ +.. _`read online`: http://packages.python.org/django-analytical/ +.. _`hosted on GitHub`: http://www.github.com/jcassee/django-analytical +.. _Analytical: https://github.com/jkrall/analytical +.. _`Bateau Knowledge`: http://www.bateauknowledge.nl/ diff --git a/analytical/__init__.py b/analytical/__init__.py new file mode 100644 index 0000000..f687ea4 --- /dev/null +++ b/analytical/__init__.py @@ -0,0 +1,14 @@ +""" +======================================== +Analytics service integration for Django +======================================== + +The django-clicky application integrates Clicky_ analytics into a +Django_ project. +""" + +__author__ = "Joost Cassee" +__email__ = "joost@cassee.net" +__version__ = "0.1.0alpha" +__copyright__ = "Copyright (C) 2011 Joost Cassee" +__license__ = "MIT License" diff --git a/analytical/models.py b/analytical/models.py new file mode 100644 index 0000000..e69de29 diff --git a/analytical/services/__init__.py b/analytical/services/__init__.py new file mode 100644 index 0000000..5031eec --- /dev/null +++ b/analytical/services/__init__.py @@ -0,0 +1,62 @@ +""" +Analytics services. +""" + +import logging + +from django.conf import settings +from django.utils.importlib import import_module +from django.core.exceptions import ImproperlyConfigured + + +_log = logging.getLogger(__name__) + +DEFAULT_SERVICES = [ + 'analytical.services.clicky.ClickyService', + 'analytical.services.crazyegg.CrazyEggService', + 'analytical.services.google_analytics.GoogleAnalyticsService', + 'analytical.services.kissinsights.KissInsightsService', + 'analytical.services.kissmetrics.KissMetricsService', + 'analytical.services.mixpanel.MixpanelService', + 'analytical.services.optimizely.OptimizelyService', +] + + +enabled_services = None +def get_enabled_services(reload=False): + global enabled_services + if enabled_services is None or reload: + enabled_services = load_services() + return enabled_services + +def load_services(): + enabled_services = [] + try: + service_paths = settings.ANALYTICAL_SERVICES + autoload = False + except AttributeError: + service_paths = DEFAULT_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)) + enabled_services.append(service) + except ImproperlyConfigured, e: + if autoload: + _log.debug('not loading analytical service "%s": %s', + path, e) + else: + raise + return enabled_services diff --git a/analytical/services/base.py b/analytical/services/base.py new file mode 100644 index 0000000..856632e --- /dev/null +++ b/analytical/services/base.py @@ -0,0 +1,41 @@ +""" +Base analytical service. +""" +from django.core.exceptions import ImproperlyConfigured +from django.conf import settings + + +class AnalyticalService(object): + """ + Analytics service. + """ + + def render(self, location, context): + 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): + return "" + + def render_head_bottom(self, context): + return "" + + def render_body_top(self, context): + return "" + + def render_body_bottom(self, context): + return "" + + def get_required_setting(self, setting, value_re, invalid_msg): + try: + value = getattr(settings, setting) + except AttributeError: + raise ImproperlyConfigured("%s setting: not found" % setting) + value = str(value) + if not value_re.search(value): + raise ImproperlyConfigured("%s setting: %s" % (value, invalid_msg)) + return value diff --git a/analytical/services/clicky.py b/analytical/services/clicky.py new file mode 100644 index 0000000..617af41 --- /dev/null +++ b/analytical/services/clicky.py @@ -0,0 +1,35 @@ +""" +Clicky service. +""" + +import re + +from analytical.services.base import AnalyticalService + + +SITE_ID_RE = re.compile(r'^\d{8}$') +TRACKING_CODE = """ + + +""" + + +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} diff --git a/analytical/services/console.py b/analytical/services/console.py new file mode 100644 index 0000000..634b49c --- /dev/null +++ b/analytical/services/console.py @@ -0,0 +1,30 @@ +""" +Console debugging service. +""" + +from analytical.services.base import AnalyticalService + + +DEBUG_CODE = """ + +""" + + +class ConsoleService(AnalyticalService): + KEY = 'console' + + def render_head_top(self, context): + return DEBUG_CODE % {'location': 'head_top'} + + def render_head_bottom(self, context): + return DEBUG_CODE % {'location': 'head_bottom'} + + def render_body_top(self, context): + return DEBUG_CODE % {'location': 'body_top'} + + def render_body_bottom(self, context): + return DEBUG_CODE % {'location': 'body_bottom'} diff --git a/analytical/services/crazy_egg.py b/analytical/services/crazy_egg.py new file mode 100644 index 0000000..7ec85af --- /dev/null +++ b/analytical/services/crazy_egg.py @@ -0,0 +1,24 @@ +""" +Crazy Egg service. +""" + +import re + +from analytical.services.base import AnalyticalService + + +ACCOUNT_NUMBER_RE = re.compile(r'^\d{8}$') +TRACK_CODE = """ +""" + + +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} diff --git a/analytical/services/kissinsights.py b/analytical/services/kissinsights.py new file mode 100644 index 0000000..15ac87d --- /dev/null +++ b/analytical/services/kissinsights.py @@ -0,0 +1,30 @@ +""" +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/kissmetrics.py b/analytical/services/kissmetrics.py new file mode 100644 index 0000000..bc5c0e3 --- /dev/null +++ b/analytical/services/kissmetrics.py @@ -0,0 +1,39 @@ +""" +KISSmetrics service. +""" + +import re + +from analytical.services.base import AnalyticalService + + +API_KEY_RE = re.compile(r'^[0-9a-f]{40}$') +TRACKING_CODE = """ + +""" + + +class KissMetricsService(AnalyticalService): + KEY = 'kissmetrics' + + def __init__(self): + self.api_key = self.get_required_setting('KISSMETRICS_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} diff --git a/analytical/services/mixpanel.py b/analytical/services/mixpanel.py new file mode 100644 index 0000000..c20e147 --- /dev/null +++ b/analytical/services/mixpanel.py @@ -0,0 +1,38 @@ +""" +Mixpanel service. +""" + +import re + +from analytical.services.base import AnalyticalService + + +MIXPANEL_TOKEN_RE = re.compile(r'^[0-9a-f]{32}$') +TRACKING_CODE = """ + + +""" + + +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} diff --git a/analytical/services/optimizely.py b/analytical/services/optimizely.py new file mode 100644 index 0000000..3209ec0 --- /dev/null +++ b/analytical/services/optimizely.py @@ -0,0 +1,23 @@ +""" +Optimizely service. +""" + +import re + +from analytical.services.base import AnalyticalService + + +ACCOUNT_NUMBER_RE = re.compile(r'^\d{7}$') +TRACKING_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} diff --git a/analytical/templatetags/__init__.py b/analytical/templatetags/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/analytical/templatetags/analytical.py b/analytical/templatetags/analytical.py new file mode 100644 index 0000000..f4f9479 --- /dev/null +++ b/analytical/templatetags/analytical.py @@ -0,0 +1,58 @@ +""" +Analytical template tags. +""" +from __future__ import absolute_import + +from django import template +from django.conf import settings +from django.template import Node, TemplateSyntaxError + +from analytical.services import get_enabled_services + + +DISABLE_CODE = "" + + +register = template.Library() + + +def _location_tag(location): + def tag(parser, token): + bits = token.split_contents() + if len(bits) > 1: + raise TemplateSyntaxError("'%s' tag takes no arguments" % bits[0]) + return AnalyticalNode(location) + return tag + +for l in ['head_top', 'head_bottom', 'body_top', 'body_bottom']: + register.tag('analytical_%s' % l, _location_tag(l)) + + +class AnalyticalNode(Node): + def __init__(self, location): + self.location = location + self.render_func_name = "render_%s" % self.location + self.internal_ips = getattr(settings, 'ANALYTICAL_INTERNAL_IPS', + getattr(settings, 'INTERNAL_IPS', ())) + + def render(self, context): + html = "".join([self._render_service(service, context) + for service in get_enabled_services()]) + if not html: + return "" +# if self._is_internal_ip(context): +# return DISABLE_CODE % html + return html + + def _render_service(self, service, context): + func = getattr(service, self.render_func_name) + return func(context) + + def _is_internal_ip(self, context): + try: + request = context['request'] + remote_ip = request.META.get('HTTP_X_FORWARDED_FOR', + request.META.get('REMOTE_ADDR', '')) + return remote_ip in self.internal_ips + except KeyError, AttributeError: + return False diff --git a/analytical/tests/__init__.py b/analytical/tests/__init__.py new file mode 100644 index 0000000..2648c6a --- /dev/null +++ b/analytical/tests/__init__.py @@ -0,0 +1,8 @@ +""" +Tests for django-analytical. +""" + +from analytical.tests.test_services import * +from analytical.tests.test_template_tags import * + +from analytical.tests.services import * diff --git a/analytical/tests/services/__init__.py b/analytical/tests/services/__init__.py new file mode 100644 index 0000000..c146338 --- /dev/null +++ b/analytical/tests/services/__init__.py @@ -0,0 +1,12 @@ +""" +Tests for the Analytical analytics services. +""" + +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_mixpanel import * +from analytical.tests.services.test_optimizely import * diff --git a/analytical/tests/services/test_clicky.py b/analytical/tests/services/test_clicky.py new file mode 100644 index 0000000..d50387d --- /dev/null +++ b/analytical/tests/services/test_clicky.py @@ -0,0 +1,44 @@ +""" +Tests for the Clicky service. +""" + +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_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) diff --git a/analytical/tests/services/test_console.py b/analytical/tests/services/test_console.py new file mode 100644 index 0000000..24068a3 --- /dev/null +++ b/analytical/tests/services/test_console.py @@ -0,0 +1,32 @@ +""" +Tests for the console debugging service. +""" + +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) + + def test_render_head_bottom(self): + r = self.service.render_head_bottom({}) + self.assertTrue('rendering analytical_head_bottom tag' 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) + + def test_render_body_bottom(self): + r = self.service.render_body_bottom({}) + self.assertTrue('rendering analytical_body_bottom tag' in r, r) diff --git a/analytical/tests/services/test_crazy_egg.py b/analytical/tests/services/test_crazy_egg.py new file mode 100644 index 0000000..9dc1b17 --- /dev/null +++ b/analytical/tests/services/test_crazy_egg.py @@ -0,0 +1,42 @@ +""" +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_id(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) diff --git a/analytical/tests/services/test_google_analytics.py b/analytical/tests/services/test_google_analytics.py new file mode 100644 index 0000000..37cb9d0 --- /dev/null +++ b/analytical/tests/services/test_google_analytics.py @@ -0,0 +1,40 @@ +""" +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_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) diff --git a/analytical/tests/services/test_kissinsights.py b/analytical/tests/services/test_kissinsights.py new file mode 100644 index 0000000..e89c781 --- /dev/null +++ b/analytical/tests/services/test_kissinsights.py @@ -0,0 +1,53 @@ +""" +Tests for the KISSinsights service. +""" + +from django.core.exceptions import ImproperlyConfigured +from django.test import TestCase + +from analytical.services.kissinsights 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(KISSINSIGHTS_ACCOUNT_NUMBER='12345') + self.settings_manager.set(KISSINSIGHTS_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('KISSINSIGHTS_ACCOUNT_NUMBER') + self.assertRaises(ImproperlyConfigured, KissInsightsService) + + def test_no_site_code(self): + self.settings_manager.delete('KISSINSIGHTS_SITE_CODE') + self.assertRaises(ImproperlyConfigured, KissInsightsService) + + def test_wrong_account_number(self): + self.settings_manager.set(KISSINSIGHTS_ACCOUNT_NUMBER='1234') + self.assertRaises(ImproperlyConfigured, KissInsightsService) + self.settings_manager.set(KISSINSIGHTS_ACCOUNT_NUMBER='123456') + self.assertRaises(ImproperlyConfigured, KissInsightsService) + + def test_wrong_site_id(self): + self.settings_manager.set(KISSINSIGHTS_SITE_CODE='ab') + self.assertRaises(ImproperlyConfigured, KissInsightsService) + self.settings_manager.set(KISSINSIGHTS_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) diff --git a/analytical/tests/services/test_kissmetrics.py b/analytical/tests/services/test_kissmetrics.py new file mode 100644 index 0000000..a6e78b3 --- /dev/null +++ b/analytical/tests/services/test_kissmetrics.py @@ -0,0 +1,46 @@ +""" +Tests for the KISSmetrics service. +""" + +from django.core.exceptions import ImproperlyConfigured +from django.test import TestCase + +from analytical.services.kissmetrics 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(KISSMETRICS_API_KEY='0123456789abcdef0123456' + '789abcdef01234567') + 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('KISSMETRICS_API_KEY') + self.assertRaises(ImproperlyConfigured, KissMetricsService) + + def test_wrong_api_key(self): + self.settings_manager.set(KISSMETRICS_API_KEY='0123456789abcdef0123456' + '789abcdef0123456') + self.assertRaises(ImproperlyConfigured, KissMetricsService) + self.settings_manager.set(KISSMETRICS_API_KEY='0123456789abcdef0123456' + '789abcdef012345678') + 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) diff --git a/analytical/tests/services/test_mixpanel.py b/analytical/tests/services/test_mixpanel.py new file mode 100644 index 0000000..1d2ae68 --- /dev/null +++ b/analytical/tests/services/test_mixpanel.py @@ -0,0 +1,46 @@ +""" +Tests for the Mixpanel service. +""" + +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_head_bottom({}), "") + self.assertEqual(self.service.render_body_top({}), "") + + 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_body_bottom({}) + self.assertTrue("MixpanelLib('0123456789abcdef0123456789abcdef')" in r, + r) diff --git a/analytical/tests/services/test_optimizely.py b/analytical/tests/services/test_optimizely.py new file mode 100644 index 0000000..feebd6f --- /dev/null +++ b/analytical/tests/services/test_optimizely.py @@ -0,0 +1,42 @@ +""" +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/settings.py b/analytical/tests/settings.py new file mode 100644 index 0000000..05b2b6c --- /dev/null +++ b/analytical/tests/settings.py @@ -0,0 +1,14 @@ +""" +django-analytical testing settings. +""" + +DATABASES = { + 'default': { + 'ENGINE': 'django.db.backends.sqlite3', + 'NAME': ':memory:', + } +} + +INSTALLED_APPS = [ + 'analytical', +] diff --git a/analytical/tests/test_services.py b/analytical/tests/test_services.py new file mode 100644 index 0000000..d11dbf6 --- /dev/null +++ b/analytical/tests/test_services.py @@ -0,0 +1,89 @@ +""" +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 = self._dummy_load_services_1 + + def tearDown(self): + services.load_services = load_services + + def test_no_reload(self): + result = services.get_enabled_services() + self.assertEqual(result, 'test1') + services.load_services = self._dummy_load_services_2 + 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' + + +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('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') + + def tearDown(self): + self.settings_manager.revert() + + 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_template_tags.py b/analytical/tests/test_template_tags.py new file mode 100644 index 0000000..de21617 --- /dev/null +++ b/analytical/tests/test_template_tags.py @@ -0,0 +1,19 @@ +""" +Tests for the template tags. +""" + +from django.test import TestCase + +from analytical.tests.utils import TestSettingsManager + + +class TemplateTagsTestCase(TestCase): + """ + Tests for the template tags. + """ + + def setUp(self): + self.settings_manager = TestSettingsManager() + + def tearDown(self): + self.settings_manager.revert() diff --git a/analytical/tests/utils.py b/analytical/tests/utils.py new file mode 100644 index 0000000..850ddab --- /dev/null +++ b/analytical/tests/utils.py @@ -0,0 +1,64 @@ +""" +Testing utilities. +""" + +from django.conf import settings +from django.core.management import call_command +from django.db.models import loading +from django.test.simple import run_tests as django_run_tests + + +def run_tests(): + """ + Use the Django test runner to run the tests. + """ + django_run_tests([], verbosity=1, interactive=True) + + +class TestSettingsManager(object): + """ + From: http://www.djangosnippets.org/snippets/1011/ + + A class which can modify some Django settings temporarily for a + test and then revert them to their original values later. + + Automatically handles resyncing the DB if INSTALLED_APPS is + modified. + """ + + NO_SETTING = ('!', None) + + def __init__(self): + self._original_settings = {} + + def set(self, **kwargs): + for k, v in kwargs.iteritems(): + self._original_settings.setdefault(k, getattr(settings, k, + self.NO_SETTING)) + setattr(settings, k, v) + if 'INSTALLED_APPS' in kwargs: + self.syncdb() + + def delete(self, *args): + for k in args: + try: + self._original_settings.setdefault(k, getattr(settings, k, + self.NO_SETTING)) + delattr(settings, k) + except AttributeError: + pass # setting did not exist + + def syncdb(self): + loading.cache.loaded = False + call_command('syncdb', verbosity=0, interactive=False) + + def revert(self): + for k,v in self._original_settings.iteritems(): + if v == self.NO_SETTING: + if hasattr(settings, k): + delattr(settings, k) + else: + setattr(settings, k, v) + if 'INSTALLED_APPS' in self._original_settings: + self.syncdb() + self._original_settings = {} diff --git a/docs/conf.py b/docs/conf.py new file mode 100644 index 0000000..8df0d38 --- /dev/null +++ b/docs/conf.py @@ -0,0 +1,44 @@ +# -*- coding: utf-8 -*- +# +# This file is execfile()d with the current directory set to its containing +# directory. + +import sys, os +sys.path.append(os.path.dirname(os.path.abspath('.'))) + +import analytical + + +# -- General configuration ----------------------------------------------------- + +project = u'django-analytical' +copyright = u'2011, Joost Cassee ' + +release = analytical.__version__ +# The short X.Y version. +version = release.rsplit('.', 1)[0] + +extensions = ['sphinx.ext.autodoc', 'sphinx.ext.intersphinx'] +templates_path = ['.templates'] +source_suffix = '.rst' +master_doc = 'index' + +add_function_parentheses = True +pygments_style = 'sphinx' + +intersphinx_mapping = {'http://docs.python.org/': None} + + +# -- Options for HTML output --------------------------------------------------- + +html_theme = 'default' +html_static_path = ['.static'] +htmlhelp_basename = 'analyticaldoc' + + +# -- Options for LaTeX output -------------------------------------------------- + +latex_documents = [ + ('index', 'django-analytical.tex', u'Documentation for django-analytical', + u'Joost Cassee', 'manual'), +] diff --git a/docs/history.rst b/docs/history.rst new file mode 100644 index 0000000..2797554 --- /dev/null +++ b/docs/history.rst @@ -0,0 +1,23 @@ +History and credits +=================== + +Changelog +--------- + +0.1.0 + First project release. + +Credits +------- + +django-analytical was written by `Joost Cassee`_. The project source +code is hosted generously hosted by GitHub_. + +This application was inspired by and uses ideas from Analytical_, +Joshua Krall's all-purpose analytics front-end for Rails. The work on +Crazy Egg was made possible by `Bateau Knowledge`_. + +.. _`Joost Cassee`: mailto:joost@cassee.net +.. _GitHub: http://github.com/ +.. _Analytical: https://github.com/jkrall/analytical +.. _`Bateau Knowledge`: http://www.bateauknowledge.nl/ diff --git a/docs/index.rst b/docs/index.rst new file mode 100644 index 0000000..77ad0ad --- /dev/null +++ b/docs/index.rst @@ -0,0 +1,44 @@ +======================================== +Analytics service integration for Django +======================================== + +The django-analytical application integrates various analytics services +into a Django_ project. + +.. _Django: http://www.djangoproject.com/ + +:Download: http://pypi.python.org/pypi/django-analytical/ +:Source: http://github.com/jcassee/django-analytical + +Overview +======== + +If your want to integrating an analytics service into a Django project, +you need to add Javascript tracking code to the project templates. +Unfortunately, every services has its own specific installation +instructions. Furthermore, you need to specify your unique identifiers +which would end up in templates. This application hides the details of +the different analytics services behind a generic interface. It is +designed to make the common case easy while allowing advanced users to +customize tracking. + + +Features +-------- + +* Easy installation. See the :doc:`quick`. +* Supports many services. See :doc:`services/index`. +* Automatically identifies logged-in users (not implemented yet) +* Disables tracking on internal IP addresses (not implemented yet) + + +Contents +======== + +.. toctree:: + :maxdepth: 2 + + quick + install + services/index + history diff --git a/docs/install.rst b/docs/install.rst new file mode 100644 index 0000000..9b872a5 --- /dev/null +++ b/docs/install.rst @@ -0,0 +1,85 @@ +Installation and global configuration +===================================== + +Integration of your analytics service is very simple. There are four +steps: installing the package, adding it to the list of installed Django +applications, adding the template tags to your base template, and adding +the identifiers for the services you use to the project settings. + +#. `Installing the Python package`_ +#. `Installing the Django application`_ +#. `Adding the template tags to the base template`_ +#. `Configuring global settings`_ + + +Installing the Python package +----------------------------- + +To install django-analytical the ``analytical`` package must be added to +the Python path. You can install it directly from PyPI using +``easy_install``:: + + $ easy_install django-analytical + +You can also install directly from source. Download either the latest +stable version from PyPI_ or any release from GitHub_, or use Git to +get the development code:: + + $ git clone https://github.com/jcassee/django-analytical.git + +.. _PyPI: http://pypi.python.org/pypi/django-analytical/ +.. _GitHub: http://github.com/jcassee/django-analytical + +Then install by running the setup script:: + + $ cd django-analytical + $ python setup.py install + + +Installing the Django application +--------------------------------- + +After you install django-analytical, add the ``analytical`` Django +application to the list of installed applications in the ``settings.py`` +file of your project:: + + INSTALLED_APPS = [ + ... + 'analytical', + ... + ] + + +Adding the template tags to the base template +--------------------------------------------- + +Because every analytics service has uses own specific Javascript code +that should be added to the top or bottom of either the head or body +of every HTML page, the django-analytical provides four general-purpose +tags that will render the code needed for the services you are using. +Your base template should look like this:: + + {% load analytical %} + + + + {% analytical_head_top %} + + ... + + {% analytical_head_bottom %} + + + {% analytical_body_top %} + + ... + + {% analytical_body_bottom %} + + + + +Configuring global settings +--------------------------- + +The next step is to :doc:`configure the services `. diff --git a/docs/quick.rst b/docs/quick.rst new file mode 100644 index 0000000..e98de8d --- /dev/null +++ b/docs/quick.rst @@ -0,0 +1,57 @@ +Quick Start Guide +================= + +If you do not need any advanced analytics tracking, installing +django-analytical is very simple. To install django-analytical the +``analytical`` package must be added to the Python path. You can +install it directly from PyPI using ``easy_install``:: + + $ easy_install django-analytical + +After you install django-analytical, add the ``analytical`` Django +application to the list of installed applications in the ``settings.py`` +file of your project:: + + INSTALLED_APPS = [ + ... + 'analytical', + ... + ] + +Now add the django-analytical template tags to your base template:: + + {% load analytical %} + + + + {% analytical_head_top %} + + ... + + {% analytical_head_bottom %} + + + {% analytical_body_top %} + + ... + + {% analytical_body_bottom %} + + + +Finally, configure the analytics services you use in the project +``settings.py`` file. This is a list of the settings required for the +different services:: + + CLICKY_SITE_ID = 'xxxxxxxx' + CRAZY_EGG_ACCOUNT_NUMBER = 'xxxxxxxx' + GOOGLE_ANALYTICS_ACCOUNT_NUMBER = 'UA-xxxxxx-x' + KISSINSIGHTS_ACCOUNT_NUMBER = 'xxxxx' + KISSINSIGHTS_ACCOUNT_NUMBER = 'xxx' + KISSINSIGHTS_ACCOUNT_NUMBER = 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx' + MIXPANEL_TOKEN = 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx' + OPTIMIZELY_ACCOUNT_NUMBER = 'xxxxxx' + +Your analytics services are now installed. Take a look at the rest of +the documentation for more information about further configuration and +customization of the various services. diff --git a/docs/services/clicky.rst b/docs/services/clicky.rst new file mode 100644 index 0000000..afca37b --- /dev/null +++ b/docs/services/clicky.rst @@ -0,0 +1,22 @@ +Clicky -- traffic analysis +========================== + +Clicky_ is an online web analytics tool. It is similar to Google +Analytics in that it provides statistics on who is visiting your website +and what they are doing. Clicky provides its data in real time and is +designed to be very easy to use. + +.. _Clicky: http://getclicky.com/ + + +Required settings +----------------- + +.. data:: CLICKY_SITE_ID + + The Clicky site identifier, or Site ID:: + + CLICKY_SITE_ID = '12345678' + + You can find the Site ID in the Info tab of the website Preferences + page on your Clicky account. diff --git a/docs/services/crazy_egg.rst b/docs/services/crazy_egg.rst new file mode 100644 index 0000000..652582a --- /dev/null +++ b/docs/services/crazy_egg.rst @@ -0,0 +1,21 @@ +Crazy Egg -- visual click tracking +================================== + +`Crazy Egg`_ is an easy to use hosted web application that visualizes +website clicks using heatmaps. It allows you to discover the areas of +web pages that are most important to your visitors. + +.. _`Crazy Egg`: http://www.crazyegg.com/ + + +Required settings +----------------- + +.. data:: CRAZY_EGG_ACCOUNT_NUMBER + + Your Crazy Egg account number:: + + CRAZY_EGG_ACCOUNT_NUMBER = '12345678' + + You can find the account number by clicking the link named "What's my + code?" in the dashboard of your Crazy Egg account. diff --git a/docs/services/google_analytics.rst b/docs/services/google_analytics.rst new file mode 100644 index 0000000..45aeacc --- /dev/null +++ b/docs/services/google_analytics.rst @@ -0,0 +1,21 @@ +Google Analytics -- traffic analysis +==================================== + +`Google Analytics`_ is the well-known web analytics service from +Google. The product is aimed more at marketers than webmasters or +technologists, supporting integration with AdWords and other e-commence +features. + +.. _`Google Analytics`: http://www.google.com/analytics/ + + +Required settings +----------------- + +.. data:: GOOGLE_ANALYTICS_PROPERTY_ID + + The Google Analytics web property ID:: + + GOOGLE_ANALYTICS_PROPERTY_ID = 'UA-123456-1' + + You can find the web property ID on the overview page of your account. diff --git a/docs/services/index.rst b/docs/services/index.rst new file mode 100644 index 0000000..1b2de4f --- /dev/null +++ b/docs/services/index.rst @@ -0,0 +1,10 @@ +Services +======== + +A number of analytics services is supported. + +.. toctree:: + :maxdepth: 1 + :glob: + + * diff --git a/docs/services/kissinsights.rst b/docs/services/kissinsights.rst new file mode 100644 index 0000000..b295531 --- /dev/null +++ b/docs/services/kissinsights.rst @@ -0,0 +1,30 @@ +KISSinsights -- feedback surveys +================================ + +KISSinsights_ provides unobtrusive surveys that pop up from the bottom +right-hand corner of your website. Asking specific questions gets you +the targeted, actionable feedback you need to make your site better. + +.. _KISSinsights: http://www.kissinsights.com/ + + +Required settings +----------------- + +.. data:: KISSINSIGHTS_ACCOUNT_NUMBER + + The KISSinsights account number:: + + KISSINSIGHTS_ACCOUNT_NUMBER = '12345' + +.. data:: KISSINSIGHTS_SITE_CODE + + The KISSinsights website code:: + + KISSINSIGHTS_SITE_CODE = 'abc' + +You can find the account number and website code by visiting the code +installation page of the website you want to place the surveys on. You +will see some HTML code with a Javascript tag with a ``src`` attribute +containing ``//s3.amazonaws.com/ki.js/XXXXX/YYY.js``. Here ``XXXXX`` is +the account number and ``YYY`` the website code. diff --git a/docs/services/kissmetrics.rst b/docs/services/kissmetrics.rst new file mode 100644 index 0000000..96777fb --- /dev/null +++ b/docs/services/kissmetrics.rst @@ -0,0 +1,22 @@ +KISSmetrics -- funnel analysis +============================== + +KISSmetrics_ is an easy to implement analytics solution that provides a +powerful visual representation of your customer lifecycle. Discover how +many visitors go from your landing page to pricing to sign up, and how +many drop out at each stage. + +.. _KISSmetrics: http://www.kissmetrics.com/ + + +Required settings +----------------- + +.. data:: KISSMETRICS_API_KEY + + The website API key:: + + KISSMETRICS_API_KEY = '1234567890abcdef1234567890abcdef12345678' + + You can find the website API key by visiting the website `Product + center` on your KISSmetrics dashboard. diff --git a/docs/services/mixpanel.rst b/docs/services/mixpanel.rst new file mode 100644 index 0000000..2fc8f60 --- /dev/null +++ b/docs/services/mixpanel.rst @@ -0,0 +1,20 @@ +Mixpanel -- event tracking +========================== + +Mixpanel_ tracks events and actions to see what features users are using +the most and how they are trending. You could use it for real-time +analysis of visitor retention or funnels. + +.. _Mixpanel: http://www.mixpanel.com/ + + +Required settings +----------------- + +.. data:: MIXPANEL_TOKEN + + The website project token :: + + MIXPANEL_TOKEN = '1234567890abcdef1234567890abcdef' + + You can find the project token on the Mixpanel `projects` page. diff --git a/docs/services/optimizely.rst b/docs/services/optimizely.rst new file mode 100644 index 0000000..c31d327 --- /dev/null +++ b/docs/services/optimizely.rst @@ -0,0 +1,25 @@ +Optimizely -- A/B testing +========================= + +Optimizely_ is an easy way to implement A/B testing. Try different +decisions, images, layouts, and copy without touching your website code +and see exactly how your experiments are affecting pagevieuws, +retention and sales. + +.. _Optimizely: http://www.optimizely.com/ + + +Required settings +----------------- + +.. data:: OPTIMIZELY_ACCOUNT_NUMBER + + The website project token :: + + OPTIMIZELY_ACCOUNT_NUMBER = '1234567' + + You can find your account number by clicking the `Implementation` link + in the top right-hand corner of the Optimizely website. A pop-up + window will appear containing HTML code looking like this: + ````. + The number ``XXXXXXX`` is your account number. diff --git a/setup.cfg b/setup.cfg new file mode 100644 index 0000000..e9db078 --- /dev/null +++ b/setup.cfg @@ -0,0 +1,7 @@ +[build_sphinx] +source-dir = docs +build-dir = build/docs +all_files = 1 + +[upload_sphinx] +upload-dir = build/docs/html diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..397dab8 --- /dev/null +++ b/setup.py @@ -0,0 +1,67 @@ +from distutils.core import setup, Command + +cmdclass = {} + +try: + from sphinx.setup_command import BuildDoc + cmdclass['build_sphinx'] = BuildDoc +except ImportError: + pass + +try: + from sphinx_pypi_upload import UploadDoc + cmdclass['upload_sphinx'] = UploadDoc +except ImportError: + pass + + +class TestCommand(Command): + user_options = [] + + def initialize_options(self): + pass + + def finalize_options(self): + pass + + def run(self): + import os + os.environ['DJANGO_SETTINGS_MODULE'] = 'analytical.tests.settings' + from analytical.tests.utils import run_tests + run_tests() + +cmdclass['test'] = TestCommand + + +import analytical + +setup( + name = 'django-analytical', + version = analytical.__version__, + license = analytical.__license__, + description = 'Analytics services for Django projects', + long_description = analytical.__doc__, + author = analytical.__author__, + author_email = analytical.__email__, + packages = [ + 'analytical', + 'analytical.templatetags', + 'analytical.tests', + ], + keywords = ['django', 'analytics]'], + classifiers = [ + 'Development Status :: 5 - Production/Stable', + 'Environment :: Web Environment', + 'Framework :: Django', + 'Intended Audience :: Developers', + 'License :: OSI Approved :: MIT License', + 'Operating System :: OS Independent', + 'Programming Language :: Python', + 'Topic :: Internet :: WWW/HTTP', + 'Topic :: Software Development :: Libraries :: Python Modules', + ], + platforms = ['any'], + url = 'http://github.com/jcassee/django-analytical', + download_url = 'http://github.com/jcassee/django-analytical/archives/master', + cmdclass = cmdclass, +)