diff --git a/CHANGELOG.rst b/CHANGELOG.rst
index f0620f1..32903c0 100644
--- a/CHANGELOG.rst
+++ b/CHANGELOG.rst
@@ -1,3 +1,7 @@
+Version 0.12.0
+--------------
+* Add support for the UserVoice service.
+
Version 0.11.3
--------------
* Added support for Gaug.es (Steven Skoczen)
diff --git a/README.rst b/README.rst
index d9e32b1..9a14272 100644
--- a/README.rst
+++ b/README.rst
@@ -1,71 +1,73 @@
-django-analytical
-=================
-
-The django-analytical application integrates analytics services into a
-Django_ project.
-
-Using an analytics service with a Django project means adding Javascript
-tracking code to the project templates. Of course, every service has
-its own specific installation instructions. Furthermore, you need to
-include your unique identifiers, which then end up in the templates.
-Not very nice.
-
-This application hides the details of the different analytics services
-behind a generic interface, and keeps personal information and
-configuration out of the templates. Its goal is to make the basic
-set-up very simple, while allowing advanced users to customize tracking.
-Each service is set up as recommended by the services themselves, using
-an asynchronous version of the Javascript code if possible.
-
-Currently supported services:
-
-* `Chartbeat`_ traffic analysis
-* `Clicky`_ traffic analysis
-* `Crazy Egg`_ visual click tracking
-* `Gaug.es`_ realtime traffic tracking
-* `Google Analytics`_ traffic analysis
-* `GoSquared`_ traffic monitoring
-* `HubSpot`_ inbound marketing
-* `KISSinsights`_ feedback surveys
-* `KISSmetrics`_ funnel analysis
-* `Mixpanel`_ event tracking
-* `Olark`_ visitor chat
-* `Optimizely`_ A/B testing
-* `Performable`_ web analytics and landing pages
-* `Reinvigorate`_ visitor tracking
-* `SnapEngage`_ live chat
-* `Spring Metrics`_ conversion tracking
-* `Woopra`_ web analytics
-
-The documentation can be found in the ``docs`` directory or `read
-online`_. The source code and issue tracker are generously `hosted by
-GitHub`_.
-
-If you want to help out with the development of django-analytical, by
-posting detailed bug reports, proposing new features or other analytics
-services to support, or suggesting documentation improvements, use the
-`issue tracker`_. If you want to get your hands dirty, great! Clone
-the repository, make changes and send a pull request. Please do create
-an issue to discuss your plans.
-
-.. _Django: http://www.djangoproject.com/
-.. _Chartbeat: http://www.chartbeat.com/
-.. _Clicky: http://getclicky.com/
-.. _`Crazy Egg`: http://www.crazyegg.com/
-.. _`Google Analytics`: http://www.google.com/analytics/
-.. _`Gaug.es`: http://www.gaug.es/
-.. _GoSquared: http://www.gosquared.com/
-.. _HubSpot: http://www.hubspot.com/
-.. _KISSinsights: http://www.kissinsights.com/
-.. _KISSmetrics: http://www.kissmetrics.com/
-.. _Mixpanel: http://www.mixpanel.com/
-.. _Olark: http://www.olark.com/
-.. _Optimizely: http://www.optimizely.com/
-.. _Performable: http://www.performable.com/
-.. _Reinvigorate: http://www.reinvigorate.com/
-.. _SnapEngage: http://www.snapengage.com/
-.. _`Spring Metrics`: http://www.springmetrics.com/
-.. _Woopra: http://www.woopra.com/
-.. _`read online`: http://packages.python.org/django-analytical/
-.. _`hosted by GitHub`: http://github.com/jcassee/django-analytical
-.. _`issue tracker`: http://github.com/jcassee/django-analytical/issues
+django-analytical
+=================
+
+The django-analytical application integrates analytics services into a
+Django_ project.
+
+Using an analytics service with a Django project means adding Javascript
+tracking code to the project templates. Of course, every service has
+its own specific installation instructions. Furthermore, you need to
+include your unique identifiers, which then end up in the templates.
+Not very nice.
+
+This application hides the details of the different analytics services
+behind a generic interface, and keeps personal information and
+configuration out of the templates. Its goal is to make the basic
+set-up very simple, while allowing advanced users to customize tracking.
+Each service is set up as recommended by the services themselves, using
+an asynchronous version of the Javascript code if possible.
+
+Currently supported services:
+
+* `Chartbeat`_ traffic analysis
+* `Clicky`_ traffic analysis
+* `Crazy Egg`_ visual click tracking
+* `Gaug.es`_ realtime traffic tracking
+* `Google Analytics`_ traffic analysis
+* `GoSquared`_ traffic monitoring
+* `HubSpot`_ inbound marketing
+* `KISSinsights`_ feedback surveys
+* `KISSmetrics`_ funnel analysis
+* `Mixpanel`_ event tracking
+* `Olark`_ visitor chat
+* `Optimizely`_ A/B testing
+* `Performable`_ web analytics and landing pages
+* `Reinvigorate`_ visitor tracking
+* `SnapEngage`_ live chat
+* `Spring Metrics`_ conversion tracking
+* `UserVoice`_ user feedback and helpdesk
+* `Woopra`_ web analytics
+
+The documentation can be found in the ``docs`` directory or `read
+online`_. The source code and issue tracker are generously `hosted by
+GitHub`_.
+
+If you want to help out with the development of django-analytical, by
+posting detailed bug reports, proposing new features or other analytics
+services to support, or suggesting documentation improvements, use the
+`issue tracker`_. If you want to get your hands dirty, great! Clone
+the repository, make changes and send a pull request. Please do create
+an issue to discuss your plans.
+
+.. _`Django`: http://www.djangoproject.com/
+.. _`Chartbeat`: http://www.chartbeat.com/
+.. _`Clicky`: http://getclicky.com/
+.. _`Crazy Egg`: http://www.crazyegg.com/
+.. _`Google Analytics`: http://www.google.com/analytics/
+.. _`GoSquared`: http://www.gosquared.com/
+.. _`HubSpot`: http://www.hubspot.com/
+.. _`KISSinsights`: http://www.kissinsights.com/
+.. _`KISSmetrics`: http://www.kissmetrics.com/
+.. _`Mixpanel`: http://www.mixpanel.com/
+.. _`Olark`: http://www.olark.com/
+.. _`Optimizely`: http://www.optimizely.com/
+.. _`Performable`: http://www.performable.com/
+.. _`Reinvigorate`: http://www.reinvigorate.com/
+.. _`SnapEngage`: http://www.snapengage.com/
+.. _`Spring Metrics`: http://www.springmetrics.com/
+.. _`UserVoice`: http://www.uservoice.com/
+.. _`Woopra`: http://www.woopra.com/
+
+.. _`read online`: http://packages.python.org/django-analytical/
+.. _`hosted by GitHub`: http://github.com/jcassee/django-analytical
+.. _`issue tracker`: http://github.com/jcassee/django-analytical/issues
diff --git a/analytical/__init__.py b/analytical/__init__.py
index 9bd3a59..6e5bd52 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.11.3"
+__version__ = "0.12.0"
__copyright__ = "Copyright (C) 2011 Joost Cassee and others"
__license__ = "MIT License"
diff --git a/analytical/templatetags/uservoice.py b/analytical/templatetags/uservoice.py
new file mode 100644
index 0000000..0e38e0d
--- /dev/null
+++ b/analytical/templatetags/uservoice.py
@@ -0,0 +1,112 @@
+"""
+UserVoice template tags.
+"""
+
+from __future__ import absolute_import
+
+import re
+
+from django.template import Library, Node, TemplateSyntaxError, Variable
+from django.utils import simplejson
+
+from analytical.utils import get_identity, get_required_setting
+
+
+WIDGET_KEY_RE = re.compile(r'^[a-zA-Z0-9]*$')
+TRACKING_CODE = """
+
+"""
+LINK_CODE = "UserVoice.showPopupWidget(%s);"
+
+
+register = Library()
+
+
+@register.tag
+def uservoice(parser, token):
+ """
+ UserVoice tracking template tag.
+
+ Renders Javascript code to track page visits. You must supply
+ your UserVoice Widget Key in the ``USERVOICE_WIDGET_KEY``
+ setting or the ``uservoice_widget_key`` template context variable.
+ """
+ bits = token.split_contents()
+ if len(bits) > 1:
+ raise TemplateSyntaxError("'%s' takes no arguments" % bits[0])
+ return UserVoiceNode()
+
+class UserVoiceNode(Node):
+ def __init__(self):
+ self.default_widget_key = get_required_setting('USERVOICE_WIDGET_KEY',
+ WIDGET_KEY_RE, "must be an alphanumeric string")
+
+ def render(self, context):
+ widget_key = context.get('uservoice_widget_key')
+ if not widget_key:
+ widget_key = self.default_widget_key
+ if not widget_key:
+ return ''
+ options = {}
+ options['enabled'] = context.get('uservoice_show_tab', True)
+ options['custom_fields'] = context.get('uservoice_fields', {})
+ identity = get_identity(context, 'uservoice')
+ if identity is not None:
+ # Enable SSO
+ pass
+ html = TRACKING_CODE % {'widget_key': widget_key,
+ 'options': simplejson.dumps(options)}
+ return html
+
+
+@register.tag
+def uservoice_link(parser, token):
+ """
+ UserVoice link template tag.
+
+ Renders the Javascript link to launch the UserVoice widget. For
+ example::
+
+ Feedback
+
+ The tag accepts an optional argument specifying the key of the widget you
+ want to show::
+
+ Helpdesk
+
+ If you add this tag without a widget key, the default feedback tab will be
+ hidden.
+ """
+ bits = token.split_contents()
+ if len(bits) == 1:
+ return UserVoiceLinkNode()
+ if len(bits) == 2:
+ return UserVoiceKeyLinkNode(bits[1])
+ raise TemplateSyntaxError("'%s' takes at most one argument" % bits[0])
+
+class UserVoiceLinkNode(Node):
+ def render(self, context):
+ context['uservoice_show_tab'] = False
+ return LINK_CODE % ''
+
+class UserVoiceKeyLinkNode(Node):
+ def __init__(self, widget_key):
+ self.widget_key = Variable(widget_key)
+
+ def render(self, context):
+ vars = {}
+ if self.widget_key:
+ vars['widget_key'] = self.widget_key.resolve(context)
+ return LINK_CODE % simplejson.dumps(vars)
+
+
+def contribute_to_analytical(add_node):
+ UserVoiceNode() # ensure properly configured
+ add_node('body_bottom', UserVoiceNode)
diff --git a/analytical/tests/__init__.py b/analytical/tests/__init__.py
index 50c6ae6..1b1d4a7 100644
--- a/analytical/tests/__init__.py
+++ b/analytical/tests/__init__.py
@@ -19,5 +19,6 @@ from analytical.tests.test_tag_performable import *
from analytical.tests.test_tag_reinvigorate import *
from analytical.tests.test_tag_snapengage import *
from analytical.tests.test_tag_spring_metrics import *
+from analytical.tests.test_tag_uservoice import *
from analytical.tests.test_tag_woopra import *
from analytical.tests.test_utils import *
diff --git a/analytical/tests/test_tag_uservoice.py b/analytical/tests/test_tag_uservoice.py
new file mode 100644
index 0000000..e62c35c
--- /dev/null
+++ b/analytical/tests/test_tag_uservoice.py
@@ -0,0 +1,93 @@
+"""
+Tests for the UserVoice tags and filters.
+"""
+
+from django.contrib.auth.models import User, AnonymousUser
+from django.http import HttpRequest
+from django.template import Context
+
+from analytical.templatetags.uservoice import UserVoiceNode
+from analytical.tests.utils import TagTestCase, override_settings, \
+ SETTING_DELETED
+from analytical.utils import AnalyticalException
+
+
+@override_settings(USERVOICE_WIDGET_KEY='abcdefghijklmnopqrst')
+class UserVoiceTagTestCase(TagTestCase):
+ """
+ Tests for the ``uservoice`` template tag.
+ """
+
+ def test_node(self):
+ r = UserVoiceNode().render(Context())
+ self.assertTrue("'widget.uservoice.com/abcdefghijklmnopqrst.js'" in r,
+ r)
+
+ def test_tag(self):
+ r = self.render_tag('uservoice', 'uservoice')
+ self.assertTrue("'widget.uservoice.com/abcdefghijklmnopqrst.js'" in r,
+ r)
+
+ @override_settings(USERVOICE_WIDGET_KEY=SETTING_DELETED)
+ def test_no_key(self):
+ self.assertRaises(AnalyticalException, UserVoiceNode)
+
+ @override_settings(USERVOICE_WIDGET_KEY='abcdefgh ijklmnopqrst')
+ def test_invalid_key(self):
+ self.assertRaises(AnalyticalException, UserVoiceNode)
+
+ @override_settings(USERVOICE_WIDGET_KEY='')
+ def test_empty_key(self):
+ r = UserVoiceNode().render(Context())
+ self.assertFalse("widget.uservoice.com" in r, r)
+
+ @override_settings(USERVOICE_WIDGET_KEY='')
+ def test_overridden_empty_key(self):
+ vars = {'uservoice_widget_key': 'bcdefghijklmnopqrstu'}
+ r = UserVoiceNode().render(Context(vars))
+ self.assertTrue("'widget.uservoice.com/bcdefghijklmnopqrstu.js'" in r,
+ r)
+
+ def test_overridden_key(self):
+ vars = {'uservoice_widget_key': 'defghijklmnopqrstuvw'}
+ r = UserVoiceNode().render(Context(vars))
+ self.assertTrue("'widget.uservoice.com/defghijklmnopqrstuvw.js'" in r,
+ r)
+
+ def test_link(self):
+ r = self.render_tag('uservoice', 'uservoice_link')
+ self.assertEqual(r, "UserVoice.showPopupWidget();")
+
+ def test_link_with_key(self):
+ r = self.render_tag('uservoice',
+ 'uservoice_link "efghijklmnopqrstuvwx"')
+ self.assertEqual(r, 'UserVoice.showPopupWidget({"widget_key": '
+ '"efghijklmnopqrstuvwx"});')
+
+ def test_link_disables_tab(self):
+ r = self.render_template(
+ '{% load uservoice %}{% uservoice_link %}{% uservoice %}')
+ self.assertTrue("UserVoice.showPopupWidget();" in r, r)
+ self.assertTrue('"enabled": false' in r, r)
+ self.assertTrue("'widget.uservoice.com/abcdefghijklmnopqrst.js'" in r,
+ r)
+
+ def test_link_with_key_enables_tab(self):
+ r = self.render_template('{% load uservoice %}'
+ '{% uservoice_link "efghijklmnopqrstuvwx" %}{% uservoice %}')
+ self.assertTrue('UserVoice.showPopupWidget({"widget_key": '
+ '"efghijklmnopqrstuvwx"});' in r, r)
+ self.assertTrue('"enabled": true' in r, r)
+ self.assertTrue("'widget.uservoice.com/abcdefghijklmnopqrst.js'" in r,
+ r)
+
+ def test_custom_fields(self):
+ vars = {
+ 'uservoice_fields': {
+ 'field1': 'val1',
+ 'field2': 'val2',
+ }
+ }
+ r = UserVoiceNode().render(Context(vars))
+ self.assertTrue('"custom_fields": {"field2": "val2", "field1": "val1"}'
+ in r, r)
diff --git a/analytical/tests/utils.py b/analytical/tests/utils.py
index 8b0dd29..8c29fec 100644
--- a/analytical/tests/utils.py
+++ b/analytical/tests/utils.py
@@ -149,3 +149,13 @@ class TagTestCase(TestCase):
else:
context = Context(vars)
return t.render(context)
+
+ def render_template(self, template, vars=None, request=None):
+ if vars is None:
+ vars = {}
+ t = Template(template)
+ if request is not None:
+ context = RequestContext(request, vars)
+ else:
+ context = Context(vars)
+ return t.render(context)
diff --git a/docs/services/uservoice.rst b/docs/services/uservoice.rst
index 8e2b2a9..d2adf9f 100644
--- a/docs/services/uservoice.rst
+++ b/docs/services/uservoice.rst
@@ -3,13 +3,15 @@ UserVoice -- user feedback and helpdesk
=======================================
UserVoice_ makes it simple for your customers to give, discuss, and vote
-for feedback. An unobtrusive feedback button allows visitors to easily
+for feedback. An unobtrusive feedback tab allows visitors to easily
submit and discuss ideas without having to sign up for a new account.
The best ideas are delivered to you based on customer votes.
.. _UserVoice: http://www.uservoice.com/
+.. _uservoice-installation:
+
Installation
============
@@ -26,7 +28,7 @@ This step is only needed if you are not using the generic
The UserVoice Javascript code is inserted into templates using a
template tag. Load the :mod:`uservoice` template tag library and insert
the :ttag:`uservoice` tag. Because every page that you want to have
-the feedback button to appear on must have the tag, it is useful to add
+the feedback tab to appear on must have the tag, it is useful to add
it to your base template. Insert the tag at the bottom of the HTML
body::
@@ -42,61 +44,74 @@ body::
Configuration
=============
-Before you can use the UserVoice integration, you must first set your
-account name.
+Before you can use the UserVoice integration, you must first set the
+widget key.
-Setting the account name
-------------------------
+Setting the widget key
+----------------------
-In order to load the Javascript code, you need to set your UserVoice
-account name. The account name is the username you use to log into
-UserVoice with. Set :const:`USERVOICE_ACCOUNT_NAME` in the project
-:file:`settings.py` file::
+In order to use the feedback widget, you need to configure which widget
+you want to show. You can find the widget keys in the *Channels* tab on
+your UserVoice *Settings* page. Under the *Javascript Widget* heading,
+find the Javascript embed code of the widget. The widget key is the
+alphanumerical string contained in the URL of the script imported by the
+embed code::
- USERVOICE_ACCOUNT_NAME = 'XXXXX'
+
-If you do not set the account name, the feedback button will not be
-rendered.
+(The widget key is shown as ``XXXXXXXXXXXXXXXXXXXX``.)
+The default widget
+..................
-.. _uservoice-hide:
+Often you will use the same widget throughout your website. The default
+widget key is configured by setting :const:`USERVOICE_WIDGET_KEY` in
+the project :file:`settings.py` file::
-Hiding the feedback button
---------------------------
+ USERVOICE_WIDGET_KEY = 'XXXXXXXXXXXXXXXXXXXX'
-The feedback button is shown on every page that has the template tag.
-You can hide the button by default by setting :const:`USERVOICE_SHOW`
-in the project :file:`settings.py` file::
+If the setting is present but empty, no widget is shown by default. This
+is useful if you want to set a widget using a template context variable,
+as the setting must be present for the generic :ttag:`analytical.*` tags
+to work.
- USERVOICE_SHOW = False
+Per-view widget
+...............
-The feedback button is also automatically hidden if you add a custom
-link to launch the widget by using the :ttag:`uservoice_link` template
-tag. (See :ref:`uservoice-link`.) The :ttag:`uservoice` tag must
-appear below it in the template, but its preferredlocation is the bottom
-of the body HTML anyway.
+Iou can set the widget key in a view using the ``uservoice_widget_key``
+template context variable::
-You can hide the feedback button for a specific view you can do so by
-passing the ``uservoice_show`` context variable::
-
- context = RequestContext({'uservoice_show': False})
+ context = RequestContext({'uservoice_widget_key': 'XXXXXXXXXXXXXXXXXXXX'})
return some_template.render(context)
-If you show or hide the feedback button based on some computable
-condition, you may want to set variables in a context processor that you
-add to the :data:`TEMPLATE_CONTEXT_PROCESSORS` list in
-:file:`settings.py`::
+The widget key passed in the context variable overrides the default
+widget key.
- def uservoice_show_to_staff(request):
+Setting the widget key in a context processor
+.............................................
+
+You can also set the widget keys in a context processor that you add to
+the :data:`TEMPLATE_CONTEXT_PROCESSORS` list in :file:`settings.py`.
+For example, to show a specific widget to logged in users::
+
+ def uservoice_widget_key(request):
try:
- return {'uservoice_show': request.user.is_staff()}
+ if request.user.is_authenticated():
+ return {'uservoice_widget_key': 'XXXXXXXXXXXXXXXXXXXX'}
except AttributeError:
- return {}
+ pass
+ return {}
-Just remember that if you set the same context variable in the
-:class:`~django.template.context.RequestContext` constructor and in a
-context processor, the latter clobbers the former.
+The widget key passed in the context variable overrides both the default
+and the per-view widget key.
.. _uservoice-link:
@@ -104,26 +119,49 @@ context processor, the latter clobbers the former.
Using a custom link
-------------------
-Instead of showing the default button, you can make the UserVoice widget
-launch when a visitor clicks a link or on some other event occurs. Use
-the :ttag:`uservoice_link` in your template to render the Javascript
-code to launch the widget::
+Instead of showing the default feedback tab, you can make the UserVoice
+widget launch when a visitor clicks a link or when some other event
+occurs. Use the :ttag:`uservoice_popup` tag in your template to render
+the Javascript code to launch the widget::
- feedback & support
+ Feedback
If you use this tag and the :ttag:`uservoice` tag appears below it in
-the HTML, the default button is automatically hidden. See
-:ref:`uservoice-link`.
+the HTML, the default tab is automatically hidden. (The preferred
+location of the :ttag:`uservoice` is the bottom of the body HTML, so
+this usually works automatically. See :ref:`uservoice-installation`.)
+
+You can explicitly hide the feedback tab by setting the
+``uservoice_show_tab`` context variable to :const:``False``::
+
+ context = RequestContext({'uservoice_show_tab': False})
+ return some_template.render(context)
+
+However, instead consider only setting the widget key in the views you
+do want to show the widget on.
+
+
+Showing a second widget
+.......................
+
+Use the :ttag:`uservoice_popup` tag with a widget_key to display a
+different widget that the one configured in the
+:const:`USERVOICE_WIDGET_KEY` setting or the ``uservoice_widget_key``
+template context variable::
+
+ Helpdesk
+
+In this case, the default widget tab is not hidden.
Passing custom data into the helpdesk
-------------------------------------
You can pass custom data through your widget and into the ticketing
-system. First create custom fields in your `Ticket settings`_ page.
+system. First create custom fields in your *Tickets* settings page.
Deselect *Display on contact form* in the edit dialog for those fields
-you intend to use from Django. You can now pass values for this field
-by passing the :data:`uservoice_fields` context variables to the
+you intend to use from Django. You can set values for this field by
+passing the :data:`uservoice_fields` context variables to the
template::
uservoice_fields = {
@@ -139,15 +177,13 @@ context processor will clobber all fields set in the
:class:`~django.template.context.RequestContext` constructor.
-.. _`Ticket settings`: https://cassee.uservoice.com/admin/settings#/tickets
-
-
-
Using Single Sign-On
--------------------
-If your websites authenticates users, you can allow them to use
-UserVoice without having to create an account.
+If your websites authenticates users, you will be able to let them give
+feedback without having to create a UserVoice account.
+
+*This feature is in development*
See also :ref:`identifying-visitors`.