diff --git a/AUTHORS.rst b/AUTHORS.rst
index 8829a34..3afd6a5 100644
--- a/AUTHORS.rst
+++ b/AUTHORS.rst
@@ -11,6 +11,7 @@ The 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`_.
+The work on Intercom was made possible by `GreenKahuna`_.
.. _`Joost Cassee`: mailto:joost@cassee.net
.. _`Eric Davis`: https://github.com/edavis
@@ -24,5 +25,6 @@ The work on Crazy Egg was made possible by `Bateau Knowledge`_.
.. _`Philippe O. Wagner`: mailto:admin@arteria.ch
.. _`Max Arnold`: https://github.com/max-arnold
.. _`Martín Gaitán`: https://github.com/mgaitan
-.. _Analytical: https://github.com/jkrall/analytical
+.. _`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 98283dd..eb332ca 100644
--- a/CHANGELOG.rst
+++ b/CHANGELOG.rst
@@ -1,6 +1,7 @@
Version 0.17.0
--------------
* Update UserVoice support (Martín Gaitán)
+* Add support for Intercom.io (Steven Skoczen)
Version 0.16.0
--------------
diff --git a/README.rst b/README.rst
index 6f7fa1f..a690b02 100644
--- a/README.rst
+++ b/README.rst
@@ -27,6 +27,7 @@ Currently supported services:
* `Google Analytics`_ traffic analysis
* `GoSquared`_ traffic monitoring
* `HubSpot`_ inbound marketing
+* `Intercom`_ live chat and support
* `KISSinsights`_ feedback surveys
* `KISSmetrics`_ funnel analysis
* `Mixpanel`_ event tracking
@@ -59,6 +60,7 @@ an issue to discuss your plans.
.. _`Google Analytics`: http://www.google.com/analytics/
.. _`GoSquared`: http://www.gosquared.com/
.. _`HubSpot`: http://www.hubspot.com/
+.. _`Intercom`: http://www.intercom.io/
.. _`KISSinsights`: http://www.kissinsights.com/
.. _`KISSmetrics`: http://www.kissmetrics.com/
.. _`Mixpanel`: http://www.mixpanel.com/
diff --git a/analytical/__init__.py b/analytical/__init__.py
index 07cae5d..3faa945 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__ = "0.16.0"
+__version__ = "0.17.0"
__copyright__ = "Copyright (C) 2011-2012 Joost Cassee and others"
__license__ = "MIT License"
diff --git a/analytical/templatetags/analytical.py b/analytical/templatetags/analytical.py
index 51ee382..d0cb1fc 100644
--- a/analytical/templatetags/analytical.py
+++ b/analytical/templatetags/analytical.py
@@ -23,6 +23,7 @@ TAG_MODULES = [
'analytical.google_analytics',
'analytical.gosquared',
'analytical.hubspot',
+ 'analytical.intercom',
'analytical.kiss_insights',
'analytical.kiss_metrics',
'analytical.mixpanel',
diff --git a/analytical/templatetags/chartbeat.py b/analytical/templatetags/chartbeat.py
index a9baf1b..26d2c7d 100644
--- a/analytical/templatetags/chartbeat.py
+++ b/analytical/templatetags/chartbeat.py
@@ -86,7 +86,7 @@ class ChartbeatBottomNode(Node):
domain = _get_domain(context)
if domain is not None:
config['domain'] = domain
- html = SETUP_CODE % {'config': json.dumps(config)}
+ html = SETUP_CODE % {'config': json.dumps(config, sort_keys=True)}
if is_internal_ip(context, 'CHARTBEAT'):
html = disable_html(html, 'Chartbeat')
return html
diff --git a/analytical/templatetags/clicky.py b/analytical/templatetags/clicky.py
index ce150eb..a2bf1c3 100644
--- a/analytical/templatetags/clicky.py
+++ b/analytical/templatetags/clicky.py
@@ -66,7 +66,7 @@ class ClickyNode(Node):
custom.setdefault('session', {})['username'] = identity
html = TRACKING_CODE % {'site_id': self.site_id,
- 'custom': json.dumps(custom)}
+ 'custom': json.dumps(custom, sort_keys=True)}
if is_internal_ip(context, 'CLICKY'):
html = disable_html(html, 'Clicky')
return html
diff --git a/analytical/templatetags/intercom.py b/analytical/templatetags/intercom.py
new file mode 100644
index 0000000..aa83828
--- /dev/null
+++ b/analytical/templatetags/intercom.py
@@ -0,0 +1,88 @@
+"""
+intercom.io template tags and filters.
+"""
+
+from __future__ import absolute_import
+import json
+import time
+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
+
+APP_ID_RE = re.compile(r'[\da-f]+$')
+TRACKING_CODE = """
+
+
+"""
+
+register = Library()
+
+
+@register.tag
+def intercom(parser, token):
+ """
+ Intercom.io template tag.
+
+ Renders Javascript code to intercom.io testing. You must supply
+ your APP ID account number in the ``INTERCOM_APP_ID``
+ setting.
+ """
+ bits = token.split_contents()
+ if len(bits) > 1:
+ raise TemplateSyntaxError("'%s' takes no arguments" % bits[0])
+ return IntercomNode()
+
+
+class IntercomNode(Node):
+ def __init__(self):
+ self.app_id = get_required_setting(
+ 'INTERCOM_APP_ID', APP_ID_RE,
+ "must be a string looking like 'XXXXXXX'")
+
+ def _identify(self, user):
+ name = user.get_full_name()
+ if not name:
+ name = user.username
+ return name
+
+ def _get_custom_attrs(self, context):
+ vars = {}
+ for dict_ in context:
+ for var, val in dict_.items():
+ if var.startswith('intercom_'):
+ vars[var[9:]] = val
+
+ user = get_user_from_context(context)
+ if user is not None and user.is_authenticated():
+ if 'full_name' not in vars:
+ vars['full_name'] = get_identity(context, 'intercom', self._identify, user)
+ if 'email' not in vars and user.email:
+ vars['email'] = user.email
+
+ vars['created_at'] = int(time.mktime(user.date_joined.timetuple()))
+ else:
+ vars['created_at'] = None
+
+ return vars
+
+ 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)}
+
+ 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
+
+
+def contribute_to_analytical(add_node):
+ IntercomNode()
+ add_node('body_bottom', IntercomNode)
diff --git a/analytical/templatetags/kiss_metrics.py b/analytical/templatetags/kiss_metrics.py
index 8f234ac..d53d11c 100644
--- a/analytical/templatetags/kiss_metrics.py
+++ b/analytical/templatetags/kiss_metrics.py
@@ -78,13 +78,13 @@ class KissMetricsNode(Node):
try:
name, properties = context[EVENT_CONTEXT_KEY]
commands.append(EVENT_CODE % {'name': name,
- 'properties': json.dumps(properties)})
+ 'properties': json.dumps(properties, sort_keys=True)})
except KeyError:
pass
try:
properties = context[PROPERTY_CONTEXT_KEY]
commands.append(PROPERTY_CODE % {
- 'properties': json.dumps(properties)})
+ 'properties': json.dumps(properties, sort_keys=True)})
except KeyError:
pass
html = TRACKING_CODE % {'api_key': self.api_key,
diff --git a/analytical/templatetags/mixpanel.py b/analytical/templatetags/mixpanel.py
index 5d7d532..fff984b 100644
--- a/analytical/templatetags/mixpanel.py
+++ b/analytical/templatetags/mixpanel.py
@@ -56,7 +56,7 @@ class MixpanelNode(Node):
try:
name, properties = context[EVENT_CONTEXT_KEY]
commands.append(EVENT_CODE % {'name': name,
- 'properties': json.dumps(properties)})
+ 'properties': json.dumps(properties, sort_keys=True)})
except KeyError:
pass
html = TRACKING_CODE % {'token': self.token,
diff --git a/analytical/templatetags/olark.py b/analytical/templatetags/olark.py
index c9e9232..a42ae8c 100644
--- a/analytical/templatetags/olark.py
+++ b/analytical/templatetags/olark.py
@@ -64,7 +64,7 @@ class OlarkNode(Node):
extra_code.append(NICKNAME_CODE % identity)
try:
extra_code.append(STATUS_CODE %
- json.dumps(context[STATUS_CONTEXT_KEY]))
+ json.dumps(context[STATUS_CONTEXT_KEY], sort_keys=True))
except KeyError:
pass
extra_code.extend(self._get_configuration(context))
diff --git a/analytical/templatetags/reinvigorate.py b/analytical/templatetags/reinvigorate.py
index aeda778..dd16c95 100644
--- a/analytical/templatetags/reinvigorate.py
+++ b/analytical/templatetags/reinvigorate.py
@@ -65,7 +65,7 @@ class ReinvigorateNode(Node):
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))
+ 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,
diff --git a/analytical/templatetags/uservoice.py b/analytical/templatetags/uservoice.py
index b4c74ef..7970ee7 100644
--- a/analytical/templatetags/uservoice.py
+++ b/analytical/templatetags/uservoice.py
@@ -66,7 +66,7 @@ class UserVoiceNode(Node):
getattr(settings, 'USERVOICE_ADD_TRIGGER', True))
html = TRACKING_CODE % {'widget_key': widget_key,
- 'options': json.dumps(options),
+ 'options': json.dumps(options, sort_keys=True),
'trigger': TRIGGER if trigger else ''}
return html
diff --git a/analytical/templatetags/woopra.py b/analytical/templatetags/woopra.py
index d70ed5b..8018e78 100644
--- a/analytical/templatetags/woopra.py
+++ b/analytical/templatetags/woopra.py
@@ -57,8 +57,8 @@ class WoopraNode(Node):
visitor = self._get_visitor(context)
html = TRACKING_CODE % {
- 'settings': json.dumps(settings),
- 'visitor': json.dumps(visitor),
+ 'settings': json.dumps(settings, sort_keys=True),
+ 'visitor': json.dumps(visitor, sort_keys=True),
}
if is_internal_ip(context, 'WOOPRA'):
html = disable_html(html, 'Woopra')
diff --git a/analytical/tests/__init__.py b/analytical/tests/__init__.py
index 634d29d..13f7b5f 100644
--- a/analytical/tests/__init__.py
+++ b/analytical/tests/__init__.py
@@ -11,6 +11,7 @@ from analytical.tests.test_tag_gauges import *
from analytical.tests.test_tag_google_analytics import *
from analytical.tests.test_tag_gosquared import *
from analytical.tests.test_tag_hubspot import *
+from analytical.tests.test_tag_intercom import *
from analytical.tests.test_tag_kiss_insights import *
from analytical.tests.test_tag_kiss_metrics import *
from analytical.tests.test_tag_mixpanel import *
diff --git a/analytical/tests/test_tag_intercom.py b/analytical/tests/test_tag_intercom.py
new file mode 100644
index 0000000..edd69ba
--- /dev/null
+++ b/analytical/tests/test_tag_intercom.py
@@ -0,0 +1,94 @@
+"""
+Tests for the intercom template tags and filters.
+"""
+
+import datetime
+
+from django.contrib.auth.models import User, AnonymousUser
+from django.http import HttpRequest
+from django.template import Context
+
+from analytical.templatetags.intercom import IntercomNode
+from analytical.tests.utils import TagTestCase, override_settings, SETTING_DELETED
+from analytical.utils import AnalyticalException
+
+
+@override_settings(INTERCOM_APP_ID='1234567890abcdef0123456789')
+class IntercomTagTestCase(TagTestCase):
+ """
+ Tests for the ``intercom`` template tag.
+ """
+
+ def test_tag(self):
+ rendered_tag = self.render_tag('intercom', 'intercom')
+ self.assertTrue(rendered_tag.startswith('
+ {% intercom %}
+