Add UserVoice service

This commit is contained in:
Joost Cassee 2012-02-27 01:15:33 +01:00
parent 8e115b3c65
commit fb98371a01
8 changed files with 385 additions and 127 deletions

View file

@ -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)

View file

@ -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

View file

@ -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"

View file

@ -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 = """
<script type="text/javascript">
var uvOptions = %(options)s;
(function() {
var uv = document.createElement('script'); uv.type = 'text/javascript'; uv.async = true;
uv.src = ('https:' == document.location.protocol ? 'https://' : 'http://') + 'widget.uservoice.com/%(widget_key)s.js';
var s = document.getElementsByTagName('script')[0]; s.parentNode.insertBefore(uv, s);
})();
</script>
"""
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::
<a href="#" onclick="{% uservoice_popup %}; return false;">Feedback</a>
The tag accepts an optional argument specifying the key of the widget you
want to show::
<a href="#" onclick="{% uservoice_popup 'XXXXXXXXXXXXXXXXXXXX' %}; return false;">Helpdesk</a>
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)

View file

@ -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 *

View file

@ -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)

View file

@ -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)

View file

@ -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'
<script type="text/javascript">
var uvOptions = {};
(function() {
var uv = document.createElement('script'); uv.type = 'text/javascript'; uv.async = true;
uv.src = ('https:' == document.location.protocol ? 'https://' : 'http://') + 'widget.uservoice.com/XXXXXXXXXXXXXXXXXXXX.js';
var s = document.getElementsByTagName('script')[0]; s.parentNode.insertBefore(uv, s);
})();
</script>
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::
<a href="{% uservoice_link %}" title="Open feedback & support dialog (powered by UserVoice)">feedback & support</a>
<a href="#" onclick="{% uservoice_popup %}; return false;">Feedback</a>
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::
<a href="#" onclick="{% uservoice_popup 'XXXXXXXXXXXXXXXXXXXX' %}; return false;">Helpdesk</a>
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`.