diff --git a/.travis.yml b/.travis.yml
index d1cc97e..989706d 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -1,12 +1,14 @@
language: python
python: "3.5"
install:
+ # continue to support Python 3.2 (see issue #84)
+ - pip install "virtualenv<14.0.0"
- pip install coveralls tox
script:
- tox
-# NOTE: To generate (update) the env list run
-# $ tox -l | sort | xargs -I ITEM echo " - TOXENV="ITEM
env:
+ # NOTE: To generate (update) the env list run
+ # $ tox -l | sort | xargs -I ITEM echo " - TOXENV="ITEM
- TOXENV=py27-django17
- TOXENV=py27-django18
- TOXENV=py27-django19
diff --git a/AUTHORS.rst b/AUTHORS.rst
index b9e4f4d..9c8b416 100644
--- a/AUTHORS.rst
+++ b/AUTHORS.rst
@@ -3,7 +3,8 @@ contributions from `Eric Davis`_, `Paul Oswald`_, `Uros Trebec`_,
`Steven Skoczen`_, `Piet Delport`_, `Sandra Mau`_, `Simon Ye`_,
`Tinnet Coronam`_, `Philippe O. Wagner`_, `Max Arnold`_ , `Martín
Gaitán`_, `Craig Bruce`_, `Peter Bittner`_, `Scott Adams`_, `Eric Amador`_,
-`Alexandre Pocquet`_, `Brad Pitcher`_, `Hugo Osvaldo Barrera`_ and others.
+`Alexandre Pocquet`_, `Brad Pitcher`_, `Hugo Osvaldo Barrera`_,
+`Nikolay Korotkiy`_, `Steve Schwarz`_, `Aleck Landgraf`_ and others.
Included Javascript code snippets for integration of the analytics
services were written by the respective service providers.
@@ -33,6 +34,9 @@ The work on Intercom was made possible by `GreenKahuna`_.
.. _`Alexandre Pocquet`: https://github.com/apocquet
.. _`Brad Pitcher`: https://github.com/brad
.. _`Hugo Osvaldo Barrera`: https://github.com/hobarrera
+.. _`Nikolay Korotkiy`: https://github.com/sikmir
+.. _`Steve Schwarz`: https://github.com/saschwarz
+.. _`Aleck Landgraf`: https://github.com/alecklandgraf
.. _`Analytical`: https://github.com/jkrall/analytical
.. _`Bateau Knowledge`: http://www.bateauknowledge.nl/
.. _`GreenKahuna`: http://www.greenkahuna.com/
diff --git a/CHANGELOG.rst b/CHANGELOG.rst
index f08386d..bd88d91 100644
--- a/CHANGELOG.rst
+++ b/CHANGELOG.rst
@@ -1,3 +1,14 @@
+Version 2.2.0
+-------------
+* Update Woopra JavaScript snippet (Aleck Landgraf)
+
+Version 2.1.0
+-------------
+* Support Rating\@mail.ru (Nikolay Korotkiy)
+* Support Yandex.Metrica (Nikolay Korotkiy)
+* Add support for extra Google Analytics variables (Steve Schwarz)
+* Remove support for Reinvigorate (service shut down)
+
Version 2.0.0
-------------
* Support Django 1.9, drop support for Django < 1.7 (Hugo Osvaldo Barrera)
diff --git a/README.rst b/README.rst
index f2a1180..5ce9748 100644
--- a/README.rst
+++ b/README.rst
@@ -1,7 +1,7 @@
django-analytical |latest-version|
==================================
-|travis-ci| |coveralls| |health| |downloads| |license| |gitter|
+|travis-ci| |coveralls| |health| |python-support| |downloads| |license| |gitter|
The django-analytical application integrates analytics services into a
Django_ project.
@@ -35,6 +35,9 @@ an asynchronous version of the Javascript code if possible.
.. |health| image:: https://landscape.io/github/jcassee/django-analytical/master/landscape.svg?style=flat
:target: https://landscape.io/github/jcassee/django-analytical/master
:alt: Code health
+.. |python-support| image:: https://img.shields.io/pypi/pyversions/django-analytical.svg
+ :target: https://pypi.python.org/pypi/django-analytical
+ :alt: Python versions
.. |downloads| image:: https://img.shields.io/pypi/dm/django-analytical.svg
:alt: Monthly downloads from PyPI
:target: https://pypi.python.org/pypi/django-analytical
@@ -65,11 +68,12 @@ Currently Supported Services
* `Optimizely`_ A/B testing
* `Performable`_ web analytics and landing pages
* `Piwik`_ open source web analytics
-* `Reinvigorate`_ visitor tracking
+* `Rating\@Mail.ru`_ web analytics
* `SnapEngage`_ live chat
* `Spring Metrics`_ conversion tracking
* `UserVoice`_ user feedback and helpdesk
* `Woopra`_ web analytics
+* `Yandex.Metrica`_ web analytics
.. _`Chartbeat`: http://www.chartbeat.com/
.. _`Clickmap`: http://getclickmap.com/
@@ -87,11 +91,12 @@ Currently Supported Services
.. _`Optimizely`: http://www.optimizely.com/
.. _`Performable`: http://www.performable.com/
.. _`Piwik`: http://www.piwik.org/
-.. _`Reinvigorate`: http://www.reinvigorate.net/
+.. _`Rating\@Mail.ru`: http://top.mail.ru/
.. _`SnapEngage`: http://www.snapengage.com/
.. _`Spring Metrics`: http://www.springmetrics.com/
.. _`UserVoice`: http://www.uservoice.com/
.. _`Woopra`: http://www.woopra.com/
+.. _`Yandex.Metrica`: http://metrica.yandex.com
Documentation and Support
-------------------------
diff --git a/analytical/__init__.py b/analytical/__init__.py
index f3a35e8..1c312c3 100644
--- a/analytical/__init__.py
+++ b/analytical/__init__.py
@@ -10,6 +10,6 @@ Django_ project. See the ``docs`` directory for more information.
__author__ = "Joost Cassee"
__email__ = "joost@cassee.net"
-__version__ = "2.0.0"
+__version__ = "2.2.0"
__copyright__ = "Copyright (C) 2011-2016 Joost Cassee and others"
__license__ = "MIT License"
diff --git a/analytical/templatetags/analytical.py b/analytical/templatetags/analytical.py
index c6ba6d6..ad4fba7 100644
--- a/analytical/templatetags/analytical.py
+++ b/analytical/templatetags/analytical.py
@@ -8,10 +8,8 @@ import logging
from django import template
from django.template import Node, TemplateSyntaxError
-try:
- from importlib import import_module
-except ImportError: # Python 2.6
- from django.utils.importlib import import_module
+from importlib import import_module
+
from analytical.utils import AnalyticalException
@@ -34,11 +32,12 @@ TAG_MODULES = [
'analytical.optimizely',
'analytical.performable',
'analytical.piwik',
- 'analytical.reinvigorate',
+ 'analytical.rating_mailru',
'analytical.snapengage',
'analytical.spring_metrics',
'analytical.uservoice',
'analytical.woopra',
+ 'analytical.yandex_metrica',
]
logger = logging.getLogger(__name__)
diff --git a/analytical/templatetags/crazy_egg.py b/analytical/templatetags/crazy_egg.py
index 5f0d830..99a58c5 100644
--- a/analytical/templatetags/crazy_egg.py
+++ b/analytical/templatetags/crazy_egg.py
@@ -12,7 +12,10 @@ from analytical.utils import is_internal_ip, disable_html, get_required_setting
ACCOUNT_NUMBER_RE = re.compile(r'^\d+$')
-SETUP_CODE = """"""
+SETUP_CODE = ''.\
+ format(placeholder_url='//dnn506yrbagrg.cloudfront.net/pages/scripts/'
+ '%(account_nr_1)s/%(account_nr_2)s.js')
USERVAR_CODE = "CE2.set(%(varnr)d, '%(value)s');"
@@ -36,19 +39,25 @@ def crazy_egg(parser, token):
class CrazyEggNode(Node):
def __init__(self):
- self.account_nr = get_required_setting('CRAZY_EGG_ACCOUNT_NUMBER',
- ACCOUNT_NUMBER_RE, "must be (a string containing) a number")
+ self.account_nr = get_required_setting(
+ 'CRAZY_EGG_ACCOUNT_NUMBER',
+ ACCOUNT_NUMBER_RE, "must be (a string containing) a number"
+ )
def render(self, context):
- html = SETUP_CODE % {'account_nr_1': self.account_nr[:4],
- 'account_nr_2': self.account_nr[4:]}
+ html = SETUP_CODE % {
+ 'account_nr_1': self.account_nr[:4],
+ 'account_nr_2': self.account_nr[4:],
+ }
values = (context.get('crazy_egg_var%d' % i) for i in range(1, 6))
- vars = [(i, v) for i, v in enumerate(values, 1) if v is not None]
- if vars:
- js = " ".join(USERVAR_CODE % {'varnr': varnr, 'value': value}
- for (varnr, value) in vars)
- html = '%s\n' \
- % (html, js)
+ params = [(i, v) for i, v in enumerate(values, 1) if v is not None]
+ if params:
+ js = " ".join(USERVAR_CODE % {
+ 'varnr': varnr,
+ 'value': value,
+ } for (varnr, value) in params)
+ html = '%s\n' \
+ '' % (html, js)
if is_internal_ip(context, 'CRAZY_EGG'):
html = disable_html(html, 'Crazy Egg')
return html
diff --git a/analytical/templatetags/google_analytics.py b/analytical/templatetags/google_analytics.py
index fb84772..f3a9f57 100644
--- a/analytical/templatetags/google_analytics.py
+++ b/analytical/templatetags/google_analytics.py
@@ -10,17 +10,13 @@ import re
from django.conf import settings
from django.template import Library, Node, TemplateSyntaxError
-from analytical.utils import is_internal_ip, disable_html, \
- get_required_setting, get_domain, AnalyticalException
-
-
-def enumerate(sequence, start=0):
- """Copy of the Python 2.6 `enumerate` builtin for compatibility."""
- n = start
- for elem in sequence:
- yield n, elem
- n += 1
-
+from analytical.utils import (
+ AnalyticalException,
+ disable_html,
+ get_domain,
+ get_required_setting,
+ is_internal_ip,
+)
TRACK_SINGLE_DOMAIN = 1
TRACK_MULTIPLE_SUBDOMAINS = 2
@@ -50,7 +46,7 @@ DOMAIN_CODE = "_gaq.push(['_setDomainName', '%s']);"
NO_ALLOW_HASH_CODE = "_gaq.push(['_setAllowHash', false]);"
ALLOW_LINKER_CODE = "_gaq.push(['_setAllowLinker', true]);"
CUSTOM_VAR_CODE = "_gaq.push(['_setCustomVar', %(index)s, '%(name)s', " \
- "'%(value)s', %(scope)s]);"
+ "'%(value)s', %(scope)s]);"
SITE_SPEED_CODE = "_gaq.push(['_trackPageLoadTime']);"
ANONYMIZE_IP_CODE = "_gaq.push (['_gat._anonymizeIp']);"
SAMPLE_RATE_CODE = "_gaq.push (['_gat._setSampleRate', '%s']);"
@@ -84,8 +80,8 @@ def google_analytics(parser, token):
class GoogleAnalyticsNode(Node):
def __init__(self):
self.property_id = get_required_setting(
- 'GOOGLE_ANALYTICS_PROPERTY_ID', PROPERTY_ID_RE,
- "must be a string looking like 'UA-XXXXXX-Y'")
+ 'GOOGLE_ANALYTICS_PROPERTY_ID', PROPERTY_ID_RE,
+ "must be a string looking like 'UA-XXXXXX-Y'")
def render(self, context):
commands = self._get_domain_commands(context)
@@ -106,14 +102,15 @@ class GoogleAnalyticsNode(Node):
def _get_domain_commands(self, context):
commands = []
tracking_type = getattr(settings, 'GOOGLE_ANALYTICS_TRACKING_STYLE',
- TRACK_SINGLE_DOMAIN)
+ TRACK_SINGLE_DOMAIN)
if tracking_type == TRACK_SINGLE_DOMAIN:
pass
else:
domain = get_domain(context, 'google_analytics')
if domain is None:
- raise AnalyticalException("tracking multiple domains with"
- " Google Analytics requires a domain name")
+ raise AnalyticalException(
+ "tracking multiple domains with Google Analytics"
+ " requires a domain name")
commands.append(DOMAIN_CODE % domain)
commands.append(NO_ALLOW_HASH_CODE)
if tracking_type == TRACK_MULTIPLE_DOMAINS:
@@ -121,18 +118,24 @@ class GoogleAnalyticsNode(Node):
return commands
def _get_custom_var_commands(self, context):
- 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]
+ values = (
+ context.get('google_analytics_var%s' % i) for i in range(1, 6)
+ )
+ params = [(i, v) for i, v in enumerate(values, 1) if v is not None]
commands = []
- for index, var in vars:
+ for index, var in params:
name = var[0]
value = var[1]
try:
scope = var[2]
except IndexError:
scope = SCOPE_PAGE
- commands.append(CUSTOM_VAR_CODE % locals())
+ commands.append(CUSTOM_VAR_CODE % {
+ 'index': index,
+ 'name': name,
+ 'value': value,
+ 'scope': scope,
+ })
return commands
def _get_other_commands(self, context):
diff --git a/analytical/templatetags/gosquared.py b/analytical/templatetags/gosquared.py
index b159d9c..72edf6c 100644
--- a/analytical/templatetags/gosquared.py
+++ b/analytical/templatetags/gosquared.py
@@ -6,10 +6,9 @@ from __future__ import absolute_import
import re
-from django.conf import settings
from django.template import Library, Node, TemplateSyntaxError
-from analytical.utils import get_identity, get_user_from_context, \
+from analytical.utils import get_identity, \
is_internal_ip, disable_html, get_required_setting
@@ -51,7 +50,8 @@ def gosquared(parser, token):
class GoSquaredNode(Node):
def __init__(self):
- self.site_token = get_required_setting('GOSQUARED_SITE_TOKEN', TOKEN_RE,
+ self.site_token = get_required_setting(
+ 'GOSQUARED_SITE_TOKEN', TOKEN_RE,
"must be a string looking like XXX-XXXXXX-X")
def render(self, context):
diff --git a/analytical/templatetags/intercom.py b/analytical/templatetags/intercom.py
index 5b88586..b9e7dd7 100644
--- a/analytical/templatetags/intercom.py
+++ b/analytical/templatetags/intercom.py
@@ -9,8 +9,8 @@ import re
from django.template import Library, Node, TemplateSyntaxError
-from analytical.utils import disable_html, get_required_setting, is_internal_ip,\
- get_user_from_context, get_identity
+from analytical.utils import disable_html, get_required_setting, \
+ is_internal_ip, get_user_from_context, get_identity
APP_ID_RE = re.compile(r'[\da-f]+$')
TRACKING_CODE = """
@@ -51,33 +51,37 @@ class IntercomNode(Node):
return name
def _get_custom_attrs(self, context):
- vars = {}
+ params = {}
for dict_ in context:
for var, val in dict_.items():
if var.startswith('intercom_'):
- vars[var[9:]] = val
+ params[var[9:]] = val
user = get_user_from_context(context)
if user is not None and user.is_authenticated():
- if 'name' not in vars:
- vars['name'] = get_identity(context, 'intercom', self._identify, user)
- if 'email' not in vars and user.email:
- vars['email'] = user.email
+ if 'name' not in params:
+ params['name'] = get_identity(
+ context, 'intercom', self._identify, user)
+ if 'email' not in params and user.email:
+ params['email'] = user.email
- vars['created_at'] = int(time.mktime(user.date_joined.timetuple()))
+ params['created_at'] = int(time.mktime(
+ user.date_joined.timetuple()))
else:
- vars['created_at'] = None
+ params['created_at'] = None
- return vars
+ return params
def render(self, context):
- html = ""
user = get_user_from_context(context)
- vars = self._get_custom_attrs(context)
- vars["app_id"] = self.app_id
- html = TRACKING_CODE % {"settings_json": json.dumps(vars, sort_keys=True)}
+ params = self._get_custom_attrs(context)
+ params["app_id"] = self.app_id
+ html = TRACKING_CODE % {
+ "settings_json": json.dumps(params, sort_keys=True)
+ }
- if is_internal_ip(context, 'INTERCOM') or not user or not user.is_authenticated():
+ if is_internal_ip(context, 'INTERCOM') \
+ or not user or not user.is_authenticated():
# Intercom is disabled for non-logged in users.
html = disable_html(html, 'Intercom')
return html
diff --git a/analytical/templatetags/rating_mailru.py b/analytical/templatetags/rating_mailru.py
new file mode 100644
index 0000000..d495824
--- /dev/null
+++ b/analytical/templatetags/rating_mailru.py
@@ -0,0 +1,71 @@
+"""
+Rating@Mail.ru template tags and filters.
+"""
+
+from __future__ import absolute_import
+
+import json
+import re
+
+from django.conf import settings
+from django.template import Library, Node, TemplateSyntaxError
+
+from analytical.utils import is_internal_ip, disable_html, \
+ get_required_setting
+
+
+COUNTER_ID_RE = re.compile(r'^\d{7}$')
+COUNTER_CODE = """
+
+
+"""
+
+
+register = Library()
+
+
+@register.tag
+def rating_mailru(parser, token):
+ """
+ Rating@Mail.ru counter template tag.
+
+ Renders Javascript code to track page visits. You must supply
+ your website counter ID (as a string) in the
+ ``RATING_MAILRU_COUNTER_ID`` setting.
+ """
+ bits = token.split_contents()
+ if len(bits) > 1:
+ raise TemplateSyntaxError("'%s' takes no arguments" % bits[0])
+ return RatingMailruNode()
+
+
+class RatingMailruNode(Node):
+ def __init__(self):
+ self.counter_id = get_required_setting(
+ 'RATING_MAILRU_COUNTER_ID', COUNTER_ID_RE,
+ "must be (a string containing) a number'")
+
+ def render(self, context):
+ html = COUNTER_CODE % {
+ 'counter_id': self.counter_id,
+ }
+ if is_internal_ip(context, 'RATING_MAILRU_METRICA'):
+ html = disable_html(html, 'Rating@Mail.ru')
+ return html
+
+
+def contribute_to_analytical(add_node):
+ RatingMailruNode() # ensure properly configured
+ add_node('head_bottom', RatingMailruNode)
diff --git a/analytical/templatetags/reinvigorate.py b/analytical/templatetags/reinvigorate.py
deleted file mode 100644
index 4f577bc..0000000
--- a/analytical/templatetags/reinvigorate.py
+++ /dev/null
@@ -1,81 +0,0 @@
-"""
-Reinvigorate template tags and filters.
-"""
-
-from __future__ import absolute_import
-
-import json
-import re
-
-from django.template import Library, Node, TemplateSyntaxError
-
-from analytical.utils import get_identity, is_internal_ip, disable_html, \
- get_required_setting
-
-
-TRACKING_ID_RE = re.compile(r'^[\w\d]+-[\w\d]+$')
-TRACKING_CODE = """
-
-
-"""
-
-
-register = Library()
-
-
-@register.tag
-def reinvigorate(parser, token):
- """
- Reinvigorate tracking template tag.
-
- Renders Javascript code to track page visits. You must supply
- your Reinvigorate tracking ID (as a string) in the
- ``REINVIGORATE_TRACKING_ID`` setting.
- """
- bits = token.split_contents()
- if len(bits) > 1:
- raise TemplateSyntaxError("'%s' takes no arguments" % bits[0])
- return ReinvigorateNode()
-
-
-class ReinvigorateNode(Node):
- def __init__(self):
- self.tracking_id = get_required_setting('REINVIGORATE_TRACKING_ID',
- TRACKING_ID_RE,
- "must be a string looking like XXXXX-XXXXXXXXXX")
-
- def render(self, context):
- re_vars = {}
- for dict_ in context:
- for var, val in dict_.items():
- if var.startswith('reinvigorate_'):
- re_vars[var[13:]] = val
- if 'name' not in re_vars:
- identity = get_identity(context, 'reinvigorate',
- lambda u: u.get_full_name())
- if identity is not None:
- re_vars['name'] = identity
- if 'context' not in re_vars:
- email = get_identity(context, 'reinvigorate', lambda u: u.email)
- if email is not None:
- re_vars['context'] = email
- tags = " ".join("var re_%s_tag = %s;" % (tag, json.dumps(value, sort_keys=True))
- for tag, value in re_vars.items())
-
- html = TRACKING_CODE % {'tracking_id': self.tracking_id,
- 'tags': tags}
- if is_internal_ip(context, 'REINVIGORATE'):
- html = disable_html(html, 'Reinvigorate')
- return html
-
-
-def contribute_to_analytical(add_node):
- ReinvigorateNode() # ensure properly configured
- add_node('body_bottom', ReinvigorateNode)
diff --git a/analytical/templatetags/spring_metrics.py b/analytical/templatetags/spring_metrics.py
index 74e2767..ed2f063 100644
--- a/analytical/templatetags/spring_metrics.py
+++ b/analytical/templatetags/spring_metrics.py
@@ -52,8 +52,9 @@ def spring_metrics(parser, token):
class SpringMetricsNode(Node):
def __init__(self):
- self.tracking_id = get_required_setting('SPRING_METRICS_TRACKING_ID',
- TRACKING_ID_RE, "must be a hexadecimal string")
+ self.tracking_id = get_required_setting(
+ 'SPRING_METRICS_TRACKING_ID',
+ TRACKING_ID_RE, "must be a hexadecimal string")
def render(self, context):
custom = {}
@@ -63,23 +64,25 @@ class SpringMetricsNode(Node):
custom[var[15:]] = val
if 'email' not in custom:
identity = get_identity(context, 'spring_metrics',
- lambda u: u.email)
+ lambda u: u.email)
if identity is not None:
custom['email'] = identity
- html = TRACKING_CODE % {'tracking_id': self.tracking_id,
- 'custom_commands': self._generate_custom_javascript(custom)}
+ html = TRACKING_CODE % {
+ 'tracking_id': self.tracking_id,
+ 'custom_commands': self._generate_custom_javascript(custom),
+ }
if is_internal_ip(context, 'SPRING_METRICS'):
html = disable_html(html, 'Spring Metrics')
return html
- def _generate_custom_javascript(self, vars):
+ def _generate_custom_javascript(self, params):
commands = []
- convert = vars.pop('convert', None)
+ convert = params.pop('convert', None)
if convert is not None:
commands.append("_springMetq.push(['convert', '%s'])" % convert)
commands.extend("_springMetq.push(['setdata', {'%s': '%s'}]);"
- % (var, val) for var, val in vars.items())
+ % (var, val) for var, val in params.items())
return " ".join(commands)
diff --git a/analytical/templatetags/woopra.py b/analytical/templatetags/woopra.py
index e5e2ad4..a18a71d 100644
--- a/analytical/templatetags/woopra.py
+++ b/analytical/templatetags/woopra.py
@@ -10,27 +10,26 @@ import re
from django.conf import settings
from django.template import Library, Node, TemplateSyntaxError
-from analytical.utils import get_identity, get_user_from_context, \
- is_internal_ip, disable_html, get_required_setting
-
+from analytical.utils import (
+ disable_html,
+ get_identity,
+ get_required_setting,
+ get_user_from_context,
+ is_internal_ip,
+)
DOMAIN_RE = re.compile(r'^\S+$')
TRACKING_CODE = """
"""
-
register = Library()
@@ -50,8 +49,9 @@ def woopra(parser, token):
class WoopraNode(Node):
def __init__(self):
- self.domain = get_required_setting('WOOPRA_DOMAIN', DOMAIN_RE,
- "must be a domain name")
+ self.domain = get_required_setting(
+ 'WOOPRA_DOMAIN', DOMAIN_RE,
+ "must be a domain name")
def render(self, context):
settings = self._get_settings(context)
@@ -66,27 +66,27 @@ class WoopraNode(Node):
return html
def _get_settings(self, context):
- vars = {'domain': self.domain}
+ variables = {'domain': self.domain}
try:
- vars['idle_timeout'] = str(settings.WOOPRA_IDLE_TIMEOUT)
+ variables['idle_timeout'] = str(settings.WOOPRA_IDLE_TIMEOUT)
except AttributeError:
pass
- return vars
+ return variables
def _get_visitor(self, context):
- vars = {}
+ params = {}
for dict_ in context:
for var, val in dict_.items():
if var.startswith('woopra_'):
- vars[var[7:]] = val
- if 'name' not in vars and 'email' not in vars:
+ params[var[7:]] = val
+ if 'name' not in params and 'email' not in params:
user = get_user_from_context(context)
if user is not None and user.is_authenticated():
- vars['name'] = get_identity(context, 'woopra',
- self._identify, user)
+ params['name'] = get_identity(
+ context, 'woopra', self._identify, user)
if user.email:
- vars['email'] = user.email
- return vars
+ params['email'] = user.email
+ return params
def _identify(self, user):
name = user.get_full_name()
diff --git a/analytical/templatetags/yandex_metrica.py b/analytical/templatetags/yandex_metrica.py
new file mode 100644
index 0000000..e2ec20b
--- /dev/null
+++ b/analytical/templatetags/yandex_metrica.py
@@ -0,0 +1,93 @@
+"""
+Yandex.Metrica template tags and filters.
+"""
+
+from __future__ import absolute_import
+
+import json
+import re
+
+from django.conf import settings
+from django.template import Library, Node, TemplateSyntaxError
+
+from analytical.utils import is_internal_ip, disable_html, \
+ get_required_setting
+
+
+COUNTER_ID_RE = re.compile(r'^\d{8}$')
+COUNTER_CODE = """
+
+
+"""
+
+
+register = Library()
+
+
+@register.tag
+def yandex_metrica(parser, token):
+ """
+ Yandex.Metrica counter template tag.
+
+ Renders Javascript code to track page visits. You must supply
+ your website counter ID (as a string) in the
+ ``YANDEX_METRICA_COUNTER_ID`` setting.
+ """
+ bits = token.split_contents()
+ if len(bits) > 1:
+ raise TemplateSyntaxError("'%s' takes no arguments" % bits[0])
+ return YandexMetricaNode()
+
+
+class YandexMetricaNode(Node):
+ def __init__(self):
+ self.counter_id = get_required_setting(
+ 'YANDEX_METRICA_COUNTER_ID', COUNTER_ID_RE,
+ "must be (a string containing) a number'")
+
+ def render(self, context):
+ options = {
+ 'id': int(self.counter_id),
+ 'clickmap': True,
+ 'trackLinks': True,
+ 'accurateTrackBounce': True
+ }
+ if getattr(settings, 'YANDEX_METRICA_WEBVISOR', False):
+ options['webvisor'] = True
+ if getattr(settings, 'YANDEX_METRICA_TRACKHASH', False):
+ options['trackHash'] = True
+ if getattr(settings, 'YANDEX_METRICA_NOINDEX', False):
+ options['ut'] = 'noindex'
+ if getattr(settings, 'YANDEX_METRICA_ECOMMERCE', False):
+ options['ecommerce'] = 'dataLayer'
+ html = COUNTER_CODE % {
+ 'counter_id': self.counter_id,
+ 'options': json.dumps(options),
+ }
+ if is_internal_ip(context, 'YANDEX_METRICA'):
+ html = disable_html(html, 'Yandex.Metrica')
+ return html
+
+
+def contribute_to_analytical(add_node):
+ YandexMetricaNode() # ensure properly configured
+ add_node('head_bottom', YandexMetricaNode)
diff --git a/analytical/tests/test_tag_rating_mailru.py b/analytical/tests/test_tag_rating_mailru.py
new file mode 100644
index 0000000..2e91a60
--- /dev/null
+++ b/analytical/tests/test_tag_rating_mailru.py
@@ -0,0 +1,47 @@
+"""
+Tests for the Rating@Mail.ru template tags and filters.
+"""
+
+import re
+
+from django.contrib.auth.models import User, AnonymousUser
+from django.http import HttpRequest
+from django.template import Context
+from django.test.utils import override_settings
+
+from analytical.templatetags.rating_mailru import RatingMailruNode
+from analytical.tests.utils import TagTestCase
+from analytical.utils import AnalyticalException
+
+
+@override_settings(RATING_MAILRU_COUNTER_ID='1234567')
+class RatingMailruTagTestCase(TagTestCase):
+ """
+ Tests for the ``rating_mailru`` template tag.
+ """
+
+ def test_tag(self):
+ r = self.render_tag('rating_mailru', 'rating_mailru')
+ self.assertTrue("counter?id=1234567;js=na" in r, r)
+
+ def test_node(self):
+ r = RatingMailruNode().render(Context({}))
+ self.assertTrue("counter?id=1234567;js=na" in r, r)
+
+ @override_settings(RATING_MAILRU_COUNTER_ID=None)
+ def test_no_site_id(self):
+ self.assertRaises(AnalyticalException, RatingMailruNode)
+
+ @override_settings(RATING_MAILRU_COUNTER_ID='1234abc')
+ def test_wrong_site_id(self):
+ self.assertRaises(AnalyticalException, RatingMailruNode)
+
+ @override_settings(ANALYTICAL_INTERNAL_IPS=['1.1.1.1'])
+ def test_render_internal_ip(self):
+ req = HttpRequest()
+ req.META['REMOTE_ADDR'] = '1.1.1.1'
+ context = Context({'request': req})
+ r = RatingMailruNode().render(context)
+ self.assertTrue(r.startswith(
+ ''), r)
diff --git a/analytical/tests/test_tag_reinvigorate.py b/analytical/tests/test_tag_reinvigorate.py
deleted file mode 100644
index c23febd..0000000
--- a/analytical/tests/test_tag_reinvigorate.py
+++ /dev/null
@@ -1,67 +0,0 @@
-"""
-Tests for the Reinvigorate template tags and filters.
-"""
-
-import re
-
-from django.contrib.auth.models import User, AnonymousUser
-from django.http import HttpRequest
-from django.template import Context
-from django.test.utils import override_settings
-
-from analytical.templatetags.reinvigorate import ReinvigorateNode
-from analytical.tests.utils import TagTestCase
-from analytical.utils import AnalyticalException
-
-
-@override_settings(REINVIGORATE_TRACKING_ID='12345-abcdefghij')
-class ReinvigorateTagTestCase(TagTestCase):
- """
- Tests for the ``reinvigorate`` template tag.
- """
-
- def test_tag(self):
- r = self.render_tag('reinvigorate', 'reinvigorate')
- self.assertTrue('reinvigorate.track("12345-abcdefghij");' in r, r)
-
- def test_node(self):
- r = ReinvigorateNode().render(Context({}))
- self.assertTrue('reinvigorate.track("12345-abcdefghij");' in r, r)
-
- @override_settings(REINVIGORATE_TRACKING_ID=None)
- def test_no_tracking_id(self):
- self.assertRaises(AnalyticalException, ReinvigorateNode)
-
- @override_settings(REINVIGORATE_TRACKING_ID='123abc')
- def test_wrong_tracking_id(self):
- self.assertRaises(AnalyticalException, ReinvigorateNode)
-
- @override_settings(ANALYTICAL_AUTO_IDENTIFY=True)
- def test_identify(self):
- r = ReinvigorateNode().render(Context({'user':
- User(username='test', first_name='Test', last_name='User',
- email='test@example.com')}))
- self.assertTrue('var re_name_tag = "Test User";' in r, r)
- self.assertTrue('var re_context_tag = "test@example.com";' in r, r)
-
- @override_settings(ANALYTICAL_AUTO_IDENTIFY=True)
- def test_identify_anonymous_user(self):
- r = ReinvigorateNode().render(Context({'user': AnonymousUser()}))
- self.assertFalse('var re_name_tag = ' in r, r)
- self.assertFalse('var re_context_tag = ' in r, r)
-
- def test_tags(self):
- r = ReinvigorateNode().render(Context({'reinvigorate_var1': 'val1',
- 'reinvigorate_var2': 2}))
- self.assertTrue(re.search('var re_var1_tag = "val1";', r), r)
- self.assertTrue(re.search('var re_var2_tag = 2;', r), r)
-
- @override_settings(ANALYTICAL_INTERNAL_IPS=['1.1.1.1'])
- def test_render_internal_ip(self):
- req = HttpRequest()
- req.META['REMOTE_ADDR'] = '1.1.1.1'
- context = Context({'request': req})
- r = ReinvigorateNode().render(context)
- self.assertTrue(r.startswith(
- ''), r)
diff --git a/analytical/tests/test_tag_yandex_metrica.py b/analytical/tests/test_tag_yandex_metrica.py
new file mode 100644
index 0000000..f85924a
--- /dev/null
+++ b/analytical/tests/test_tag_yandex_metrica.py
@@ -0,0 +1,47 @@
+"""
+Tests for the Yandex.Metrica template tags and filters.
+"""
+
+import re
+
+from django.contrib.auth.models import User, AnonymousUser
+from django.http import HttpRequest
+from django.template import Context
+from django.test.utils import override_settings
+
+from analytical.templatetags.yandex_metrica import YandexMetricaNode
+from analytical.tests.utils import TagTestCase
+from analytical.utils import AnalyticalException
+
+
+@override_settings(YANDEX_METRICA_COUNTER_ID='12345678')
+class YandexMetricaTagTestCase(TagTestCase):
+ """
+ Tests for the ``yandex_metrica`` template tag.
+ """
+
+ def test_tag(self):
+ r = self.render_tag('yandex_metrica', 'yandex_metrica')
+ self.assertTrue("w.yaCounter12345678 = new Ya.Metrika" in r, r)
+
+ def test_node(self):
+ r = YandexMetricaNode().render(Context({}))
+ self.assertTrue("w.yaCounter12345678 = new Ya.Metrika" in r, r)
+
+ @override_settings(YANDEX_METRICA_COUNTER_ID=None)
+ def test_no_site_id(self):
+ self.assertRaises(AnalyticalException, YandexMetricaNode)
+
+ @override_settings(YANDEX_METRICA_COUNTER_ID='1234abcd')
+ def test_wrong_site_id(self):
+ self.assertRaises(AnalyticalException, YandexMetricaNode)
+
+ @override_settings(ANALYTICAL_INTERNAL_IPS=['1.1.1.1'])
+ def test_render_internal_ip(self):
+ req = HttpRequest()
+ req.META['REMOTE_ADDR'] = '1.1.1.1'
+ context = Context({'request': req})
+ r = YandexMetricaNode().render(context)
+ self.assertTrue(r.startswith(
+ ''), r)
diff --git a/analytical/tests/test_utils.py b/analytical/tests/test_utils.py
index 4a4ba6a..0f4ad58 100644
--- a/analytical/tests/test_utils.py
+++ b/analytical/tests/test_utils.py
@@ -1,29 +1,23 @@
"""
Tests for the analytical.utils module.
"""
-import django
+# import django
-from django.conf import settings
+from django.contrib.auth.models import AbstractBaseUser
from django.db import models
from django.http import HttpRequest
from django.template import Context
from django.test.utils import override_settings
from analytical.utils import (
- get_domain, get_identity, is_internal_ip, get_required_setting,
- AnalyticalException)
+ AnalyticalException,
+ get_domain,
+ get_identity,
+ get_required_setting,
+ is_internal_ip,
+)
from analytical.tests.utils import TestCase
-try:
- from unittest import skipIf
-except ImportError: # Python 2.6 fallback
- from unittest2 import skipIf
-
-try:
- from django.contrib.auth.models import AbstractBaseUser
-except ImportError: # Django < 1.5 fallback
- AbstractBaseUser = models.Model
-
class SettingDeletedTestCase(TestCase):
@@ -52,7 +46,6 @@ class MyUser(AbstractBaseUser):
class GetIdentityTestCase(TestCase):
- @skipIf(django.VERSION < (1, 5,), 'Custom usernames not supported in Django < 1.5')
def test_custom_username_field(self):
get_id = get_identity(Context({}), user=MyUser(identity='fake_id'))
self.assertEqual(get_id, 'fake_id')
diff --git a/analytical/utils.py b/analytical/utils.py
index da9413a..d915cf4 100644
--- a/analytical/utils.py
+++ b/analytical/utils.py
@@ -75,10 +75,7 @@ def get_identity(context, prefix=None, identity_func=None, user=None):
if identity_func is not None:
return identity_func(user)
else:
- try:
- return user.get_username()
- except AttributeError: # Django < 1.5 fallback
- return user.username
+ return user.get_username()
except (KeyError, AttributeError):
pass
return None
diff --git a/docs/install.rst b/docs/install.rst
index b29fec3..0b25521 100644
--- a/docs/install.rst
+++ b/docs/install.rst
@@ -20,23 +20,29 @@ 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``:
- $ easy_install django-analytical
+.. code-block:: bash
+
+ $ 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::
+get the development code:
- $ git clone https://github.com/jcassee/django-analytical.git
+.. code-block:: bash
+
+ $ 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 the package by running the setup script::
+Then install the package by running the setup script:
- $ cd django-analytical
- $ python setup.py install
+.. code-block:: bash
+
+ $ cd django-analytical
+ $ python setup.py install
.. _installing-the-application:
@@ -46,13 +52,15 @@ Installing the Django application
After you installed django-analytical, add the ``analytical`` Django
application to the list of installed applications in the ``settings.py``
-file of your project::
+file of your project:
- INSTALLED_APPS = [
- ...
- 'analytical',
- ...
- ]
+.. code-block:: python
+
+ INSTALLED_APPS = [
+ ...
+ 'analytical',
+ ...
+ ]
.. _adding-the-template-tags:
@@ -64,7 +72,9 @@ Because every analytics service uses own specific Javascript code that
should be added to the top or bottom of either the head or body of the
HTML page, django-analytical provides four general-purpose template tags
that will render the code needed for the services you are using. Your
-base template should look like this::
+base template should look like this:
+
+.. code-block:: html
{% load analytical %}
@@ -101,27 +111,27 @@ settings required to enable each service are listed here:
* :doc:`Chartbeat `::
- CHARTBEAT_USER_ID = '12345'
+ CHARTBEAT_USER_ID = '12345'
* :doc:`Clickmap `::
- CLICKMAP_TRACKER_CODE = '12345678....912'
+ CLICKMAP_TRACKER_CODE = '12345678....912'
* :doc:`Clicky `::
- CLICKY_SITE_ID = '12345678'
+ CLICKY_SITE_ID = '12345678'
* :doc:`Crazy Egg `::
- CRAZY_EGG_ACCOUNT_NUMBER = '12345678'
+ CRAZY_EGG_ACCOUNT_NUMBER = '12345678'
* :doc:`Gaug.es `::
- GAUGES_SITE_ID = '0123456789abcdef0123456789abcdef'
+ GAUGES_SITE_ID = '0123456789abcdef0123456789abcdef'
* :doc:`Google Analytics `::
- GOOGLE_ANALYTICS_PROPERTY_ID = 'UA-1234567-8'
+ GOOGLE_ANALYTICS_PROPERTY_ID = 'UA-1234567-8'
* :doc:`HubSpot `::
@@ -134,16 +144,16 @@ settings required to enable each service are listed here:
* :doc:`KISSinsights `::
- KISS_INSIGHTS_ACCOUNT_NUMBER = '12345'
- KISS_INSIGHTS_SITE_CODE = 'abc'
+ KISS_INSIGHTS_ACCOUNT_NUMBER = '12345'
+ KISS_INSIGHTS_SITE_CODE = 'abc'
* :doc:`KISSmetrics `::
- KISS_METRICS_API_KEY = '0123456789abcdef0123456789abcdef01234567'
+ KISS_METRICS_API_KEY = '0123456789abcdef0123456789abcdef01234567'
* :doc:`Mixpanel `::
- MIXPANEL_API_TOKEN = '0123456789abcdef0123456789abcdef'
+ MIXPANEL_API_TOKEN = '0123456789abcdef0123456789abcdef'
* :doc:`Olark `::
@@ -151,7 +161,7 @@ settings required to enable each service are listed here:
* :doc:`Optimizely `::
- OPTIMIZELY_ACCOUNT_NUMBER = '1234567'
+ OPTIMIZELY_ACCOUNT_NUMBER = '1234567'
* :doc:`Performable `::
@@ -162,14 +172,17 @@ settings required to enable each service are listed here:
PIWIK_DOMAIN_PATH = 'your.piwik.server/optional/path'
PIWIK_SITE_ID = '123'
-* :doc:`Reinvigorate `::
+* :doc:`Rating\@Mail.ru `::
- REINVIGORATE_TRACKING_ID = '12345-abcdefghij'
+ RATING_MAILRU_COUNTER_ID = '1234567'
* :doc:`Woopra `::
WOOPRA_DOMAIN = 'abcde.com'
+* :doc:`Yandex.Metrica `::
+
+ YANDEX_METRICA_COUNTER_ID = '12345678'
----
diff --git a/docs/services/google_analytics.rst b/docs/services/google_analytics.rst
index 7add8b7..c34ba36 100644
--- a/docs/services/google_analytics.rst
+++ b/docs/services/google_analytics.rst
@@ -220,7 +220,7 @@ You can configure the `Sample Rate`_ feature by setting the
The value is a percentage and can be between 0 and 100 and can be a string or
decimal value of with up to two decimal places.
-.. _`Sample Rate`_: https://developers.google.com/analytics/devguides/collection/gajs/methods/gaJSApiBasicConfiguration#_setsamplerate
+.. _`Sample Rate`: https://developers.google.com/analytics/devguides/collection/gajs/methods/gaJSApiBasicConfiguration#_setsamplerate
.. _google-analytics-site-speed-sample-rate:
@@ -236,7 +236,7 @@ You can configure the `Site Speed Sample Rate`_ feature by setting the
The value is a percentage and can be between 0 and 100 and can be a string or
decimal value of with up to two decimal places.
-.. _`Site Speed Sample Rate`_: https://developers.google.com/analytics/devguides/collection/gajs/methods/gaJSApiBasicConfiguration#_setsitespeedsamplerate
+.. _`Site Speed Sample Rate`: https://developers.google.com/analytics/devguides/collection/gajs/methods/gaJSApiBasicConfiguration#_setsitespeedsamplerate
.. _google-analytics-session-cookie-timeout:
@@ -251,7 +251,7 @@ You can configure the `Session Cookie Timeout`_ feature by setting the
The value is the session cookie timeout in milliseconds or 0 to delete the cookie when the browser is closed.
-.. _`Session Cookie Timeout`_: https://developers.google.com/analytics/devguides/collection/gajs/methods/gaJSApiBasicConfiguration#_setsessioncookietimeout
+.. _`Session Cookie Timeout`: https://developers.google.com/analytics/devguides/collection/gajs/methods/gaJSApiBasicConfiguration#_setsessioncookietimeout
.. _google-analytics-visitor-cookie-timeout:
@@ -266,4 +266,4 @@ You can configure the `Visitor Cookie Timeout`_ feature by setting the
The value is the visitor cookie timeout in milliseconds or 0 to delete the cookie when the browser is closed.
-.. _`Visitor Cookie Timeout`_: https://developers.google.com/analytics/devguides/collection/gajs/methods/gaJSApiBasicConfiguration#_setvisitorcookietimeout
+.. _`Visitor Cookie Timeout`: https://developers.google.com/analytics/devguides/collection/gajs/methods/gaJSApiBasicConfiguration#_setvisitorcookietimeout
diff --git a/docs/services/rating_mailru.rst b/docs/services/rating_mailru.rst
new file mode 100644
index 0000000..e905e4a
--- /dev/null
+++ b/docs/services/rating_mailru.rst
@@ -0,0 +1,73 @@
+===================================
+Rating\@Mail.ru -- traffic analysis
+===================================
+
+`Rating\@Mail.ru`_ is an analytics tool like as google analytics.
+
+.. _`Rating\@Mail.ru`: http://top.mail.ru/
+
+
+.. rating-mailru-installation:
+
+Installation
+============
+
+To start using the Rating\@Mail.ru integration, you must have installed the
+django-analytical package and have added the ``analytical`` application
+to :const:`INSTALLED_APPS` in your project :file:`settings.py` file.
+See :doc:`../install` for details.
+
+Next you need to add the Rating\@Mail.ru template tag to your templates. This
+step is only needed if you are not using the generic
+:ttag:`analytical.*` tags. If you are, skip to
+:ref:`rating-mailru-configuration`.
+
+The Rating\@Mail.ru counter code is inserted into templates using a template
+tag. Load the :mod:`rating_mailru` template tag library and insert the
+:ttag:`rating_mailru` tag. Because every page that you want to track must
+have the tag, it is useful to add it to your base template. Insert
+the tag at the bottom of the HTML head::
+
+ {% load rating_mailru %}
+
+
+ ...
+ {% rating_mailru %}
+
+ ...
+
+
+.. _rating-mailru-configuration:
+
+Configuration
+=============
+
+Before you can use the Rating\@Mail.ru integration, you must first set
+your website counter ID.
+
+
+.. _rating-mailru-counter-id:
+
+Setting the counter ID
+----------------------
+
+Every website you track with Rating\@Mail.ru gets its own counter ID,
+and the :ttag:`rating_mailru` tag will include it in the rendered
+Javascript code. You can find the web counter ID on the overview page
+of your account. Set :const:`RATING_MAILRU_COUNTER_ID` in the
+project :file:`settings.py` file::
+
+ RATING_MAILRU_COUNTER_ID = '1234567'
+
+If you do not set a counter ID, the counter code will not be rendered.
+
+Internal IP addresses
+---------------------
+
+Usually you do not want to track clicks from your development or
+internal IP addresses. By default, if the tags detect that the client
+comes from any address in the :const:`RATING_MAILRU_INTERNAL_IPS` setting,
+the tracking code is commented out. It takes the value of
+:const:`ANALYTICAL_INTERNAL_IPS` by default (which in turn is
+:const:`INTERNAL_IPS` by default). See :ref:`identifying-visitors` for
+important information about detecting the visitor IP address.
diff --git a/docs/services/reinvigorate.rst b/docs/services/reinvigorate.rst
deleted file mode 100644
index 44051d7..0000000
--- a/docs/services/reinvigorate.rst
+++ /dev/null
@@ -1,157 +0,0 @@
-================================
-Reinvigorate -- visitor tracking
-================================
-
-Reinvigorate_ gives you real-time traffic analysis, visitor activity,
-search and referrer information and click heatmaps. A system tray /
-system status bar application for your desktop notifies you when
-interesting events occur.
-
-.. _Reinvigorate: http://www.reinvigorate.net/
-
-
-.. reinvigorate-installation:
-
-Installation
-============
-
-To start using the Reinvigorate integration, you must have installed the
-django-analytical package and have added the ``analytical`` application
-to :const:`INSTALLED_APPS` in your project :file:`settings.py` file.
-See :doc:`../install` for details.
-
-Next you need to add the Reinvigorate template tag to your templates.
-This step is only needed if you are not using the generic
-:ttag:`analytical.*` tags. If you are, skip to
-:ref:`reinvigorate-configuration`.
-
-The Reinvigorate tracking code is inserted into templates using a
-template tag. Load the :mod:`reinvigorate` template tag library and
-insert the :ttag:`reinvigorate` tag. Because every page that you want
-to track must have the tag, it is useful to add it to your base
-template. Insert the tag somewhere within the HTML body::
-
- {% load reinvigorate %}
- ...
- {% reinvigorate %}
-
-