From fb98371a01b120e517a60b2d22ba868b703f0555 Mon Sep 17 00:00:00 2001 From: Joost Cassee Date: Mon, 27 Feb 2012 01:15:33 +0100 Subject: [PATCH] Add UserVoice service --- CHANGELOG.rst | 4 + README.rst | 144 ++++++++++++------------ analytical/__init__.py | 2 +- analytical/templatetags/uservoice.py | 112 +++++++++++++++++++ analytical/tests/__init__.py | 1 + analytical/tests/test_tag_uservoice.py | 93 ++++++++++++++++ analytical/tests/utils.py | 10 ++ docs/services/uservoice.rst | 146 +++++++++++++++---------- 8 files changed, 385 insertions(+), 127 deletions(-) create mode 100644 analytical/templatetags/uservoice.py create mode 100644 analytical/tests/test_tag_uservoice.py 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`.