diff --git a/django/website/dashboard/templates/dashboard/widget-error.html b/django/website/dashboard/templates/dashboard/widget-error.html
new file mode 100644
index 0000000000000000000000000000000000000000..38d75f8dc4c387627734c312d64064064ed2e00f
--- /dev/null
+++ b/django/website/dashboard/templates/dashboard/widget-error.html
@@ -0,0 +1,10 @@
+{% load i18n %}
+<div class='panel panel-default'>
+    <div class='panel-heading'>
+        <span class='fa fa-warning fa-fw'></span>
+        <h2>{% trans "Error" %}</h2>
+    </div>
+    <div class='panel-body'>
+        {{ error }}
+    </div>
+</div>
diff --git a/django/website/dashboard/templates/dashboard/widget-missing-template.html b/django/website/dashboard/templates/dashboard/widget-missing-template.html
deleted file mode 100644
index 3f976032f5542e73a9d294e177c6b49c2f763c5c..0000000000000000000000000000000000000000
--- a/django/website/dashboard/templates/dashboard/widget-missing-template.html
+++ /dev/null
@@ -1 +0,0 @@
-Missing template_name for widget type {{empty_type}}
diff --git a/django/website/dashboard/templatetags/render_widget.py b/django/website/dashboard/templatetags/render_widget.py
index d090438b51cd8816e0753490785cd3bfec9e0c7f..e9de818e4e26ce5f3f65760b9103bdc03bf3ac60 100644
--- a/django/website/dashboard/templatetags/render_widget.py
+++ b/django/website/dashboard/templatetags/render_widget.py
@@ -1,9 +1,13 @@
+import logging
+
 from django import template
 from django.template.loader import render_to_string
+from django.utils.translation import ugettext_lazy as _
 
-from dashboard.widget_pool import get_widget
+from dashboard.widget_pool import get_widget, MissingWidgetType
 
 
+logger = logging.getLogger(__name__)
 register = template.Library()
 
 
@@ -16,18 +20,43 @@ def render_widget(widget_instance):
     get_context_data then that is used to generate the
     template context.
     """
-    widget = get_widget(widget_instance.widget_type)
+    widget = None
+    # Get settings, if any
     if widget_instance.settings:
         settings = widget_instance.settings
     else:
         settings = {}
+    # Get widget
     try:
-        context = widget.get_context_data(**settings)
-    except AttributeError:
+        widget = get_widget(widget_instance.widget_type)
+    except MissingWidgetType:
+        template_name = 'dashboard/widget-error.html'
         context = {}
-    try:
-        template_name = widget.template_name
-    except AttributeError:
-        template_name = 'dashboard/widget-missing-template.html'
+        context['error'] = _('Unknown widget type %(widget_type)s') % {
+            'widget_type': widget_instance.widget_type
+        }
         context['empty_type'] = widget_instance.widget_type
+    if widget:
+        # Get template
+        try:
+            template_name = widget.template_name
+        except AttributeError:
+            template_name = 'dashboard/widget-error.html'
+            context = {}
+            context['error'] = _('Missing template for %(widget_type)s') % {
+                'widget_type': widget_instance.widget_type
+            }
+            widget = None
+    if widget:
+        # Get context
+        try:
+            context = widget.get_context_data(**settings)
+        except AttributeError:
+            context = {}
+        except Exception as e:
+            logger.exception('Error while fetching widget context data: %s', e)
+            template_name = 'dashboard/widget-error.html'
+            context = {}
+            context['error'] = _('Widget error. See error logs.')
+
     return render_to_string(template_name, context)
diff --git a/django/website/dashboard/tests/widget_tests.py b/django/website/dashboard/tests/widget_tests.py
index 10c8026752780e9d5523af7973202b1c2f70b630..2b69bc3326e97cee9a4961087bde2f175c569fcf 100644
--- a/django/website/dashboard/tests/widget_tests.py
+++ b/django/website/dashboard/tests/widget_tests.py
@@ -150,5 +150,5 @@ class WidgetPoolTestCase(TestCase):
                 mock, 'template_name'
             )
         self.assertEqual(
-            template_name, 'dashboard/widget-missing-template.html'
+            template_name, 'dashboard/widget-error.html'
         )
diff --git a/django/website/dashboard/views.py b/django/website/dashboard/views.py
index 73f4a3b61eb2844a04d7ae25e9ca26e655b2eb7a..b20136dce9475c8ae26cbc59ff3ff10d075123f0 100644
--- a/django/website/dashboard/views.py
+++ b/django/website/dashboard/views.py
@@ -3,7 +3,7 @@ from django.views.generic import TemplateView
 from hid.assets import require_assets
 
 from dashboard.models import Dashboard
-from dashboard.widget_pool import get_widget
+from dashboard.widget_pool import get_widget, MissingWidgetType
 
 
 class DashboardView(TemplateView):
@@ -43,7 +43,10 @@ class DashboardView(TemplateView):
         # Ensure we have all the javascript & css dependencies
         require_assets('dashboard/dashboard.css')
         for widget in widgets:
-            widget_type = get_widget(widget.widget_type)
+            try:
+                widget_type = get_widget(widget.widget_type)
+            except MissingWidgetType:
+                continue
             if hasattr(widget_type, 'javascript'):
                 require_assets(*widget_type.javascript)
             if hasattr(widget_type, 'css'):
diff --git a/django/website/dashboard/widget_pool.py b/django/website/dashboard/widget_pool.py
index 7615b0bb5cb4fbccddd2acc08db5208ba15f47e4..98cc08bc6c92c660e979490e8a1074b516db8f9e 100644
--- a/django/website/dashboard/widget_pool.py
+++ b/django/website/dashboard/widget_pool.py
@@ -1,6 +1,11 @@
 _pool = {}
 
 
+class MissingWidgetType(Exception):
+    """ Exception raised when a widget type is missing """
+    pass
+
+
 def register_widget(name, widget):
     """ Register a new widget type
 
@@ -24,10 +29,13 @@ def get_widget(name):
     Returns:
         The widget object as registered with register_widget
     Raises:
-        KeyError: If the widget type does not exist
+        MissingWidgetType: If the widget type does not exist
     """
     global _pool
-    return _pool[name]
+    try:
+        return _pool[name]
+    except KeyError:
+        raise MissingWidgetType()
 
 
 class BasicTextWidget(object):
diff --git a/django/website/hid/__init__.py b/django/website/hid/__init__.py
index ec07e109f57010e2b83ee066c51f721e960e164b..9948ca8850f47ac83269031849f020bb65cd17fd 100644
--- a/django/website/hid/__init__.py
+++ b/django/website/hid/__init__.py
@@ -1,7 +1,7 @@
 from dashboard.widget_pool import register_widget
-from hid.widgets.chart import QuestionChartWidget
+from hid.widgets.term_count_chart import TermCountChartWidget
 from hid.widgets.table import TableWidget
 
 
-register_widget('question-chart-widget', QuestionChartWidget())
+register_widget('term-count-chart', TermCountChartWidget())
 register_widget('table-widget', TableWidget())
diff --git a/django/website/hid/tests/chart_widget_tests.py b/django/website/hid/tests/chart_widget_tests.py
deleted file mode 100644
index 3cc5ad03b2982aa6ec5dd24f405ed964240c0b2c..0000000000000000000000000000000000000000
--- a/django/website/hid/tests/chart_widget_tests.py
+++ /dev/null
@@ -1,41 +0,0 @@
-from django.test import TestCase
-from hid.widgets.chart import QuestionChartWidget
-
-
-class TestQuestionChartWidget(TestCase):
-    def test_context_data_includes_widget_name(self):
-        widget = QuestionChartWidget()
-        context_data = widget.get_context_data(name='test-name', questions={})
-        self.assertEqual(context_data['name'], 'test-name')
-
-    def test_context_data_includes_flot_options(self):
-        widget = QuestionChartWidget()
-        context_data = widget.get_context_data(name='test-name', questions={})
-        self.assertTrue('options' in context_data)
-
-    def test_context_data_includes_flot_data(self):
-        widget = QuestionChartWidget()
-        context_data = widget.get_context_data(name='test-name', questions={})
-        self.assertTrue('data' in context_data)
-
-    def test_context_data_includes_correct_data(self):
-        widget = QuestionChartWidget()
-        context_data = widget.get_context_data(name='test-name', questions={
-            'question one': 345,
-            'question two': 782
-        })
-        self.assertEqual(
-            context_data['data'],
-            [[[345, 0], [782, 1]]]
-        )
-
-    def test_chart_questions_are_set_as_yaxis_value_labels(self):
-        widget = QuestionChartWidget()
-        context_data = widget.get_context_data(name='test-name', questions={
-            'question one': 345,
-            'question two': 782
-        })
-        self.assertEqual(
-            context_data['options']['yaxis']['ticks'],
-            [[0, 'question one'], [1, 'question two']]
-        )
diff --git a/django/website/hid/tests/login_tests.py b/django/website/hid/tests/login_tests.py
new file mode 100644
index 0000000000000000000000000000000000000000..09195b61c888da7a837d56fe87223e70253aa9e9
--- /dev/null
+++ b/django/website/hid/tests/login_tests.py
@@ -0,0 +1,37 @@
+from __future__ import unicode_literals, absolute_import
+
+import pytest
+
+from django.core.urlresolvers import reverse
+from django.test import Client
+from django.utils.six.moves.urllib.parse import urlsplit
+
+from users.models import User
+
+@pytest.mark.django_db
+def test_user_directed_to_login_page_when_csrf_error():
+    username = 'william'
+    password = 'passw0rd'
+
+    User.objects.create_user(username, 'william@example.com', password)
+
+    client = Client(enforce_csrf_checks=True)
+    data = {'username': username,
+            'password': password,
+            'csrfmiddlewaretoken': 'notavalidtoken'}
+    response = client.post(reverse('login'),
+                           data=data, follow=True)
+
+    assert hasattr(response, 'redirect_chain')
+    assert len(response.redirect_chain) > 0, "Response didn't redirect"
+
+    assert response.redirect_chain[0][1] == 302
+    url, _ = response.redirect_chain[-1]
+    scheme, netloc, path, query, fragment = urlsplit(url)
+    assert path == reverse('login')
+
+    url, _ = response.redirect_chain[-2]
+    scheme, netloc, path, query, fragment = urlsplit(url)
+    assert path == reverse('dashboard')
+
+    assert response.status_code == 200
diff --git a/django/website/hid/tests/term_count_widget_tests.py b/django/website/hid/tests/term_count_widget_tests.py
new file mode 100644
index 0000000000000000000000000000000000000000..6d8e2ddec5a1fea598f9ba653a4866d6da735393
--- /dev/null
+++ b/django/website/hid/tests/term_count_widget_tests.py
@@ -0,0 +1,138 @@
+from mock import patch
+from django.test import TestCase
+from hid.widgets.term_count_chart import TermCountChartWidget
+
+
+class TestTermCountChartWidget(TestCase):
+    def test_context_data_includes_widget_title(self):
+        widget = TermCountChartWidget()
+        with patch('hid.widgets.term_count_chart.term_itemcount') as itemcount:
+            itemcount.return_value = []
+            context_data = widget.get_context_data(
+                title='test-name', taxonomy='tax'
+            )
+        self.assertEqual(context_data['title'], 'test-name')
+
+    def test_context_data_includes_flot_options(self):
+        widget = TermCountChartWidget()
+        with patch('hid.widgets.term_count_chart.term_itemcount') as itemcount:
+            itemcount.return_value = []
+            context_data = widget.get_context_data(
+                title='test-name', taxonomy='tax'
+            )
+        self.assertTrue('options' in context_data)
+
+    def test_context_data_includes_flot_data(self):
+        widget = TermCountChartWidget()
+        with patch('hid.widgets.term_count_chart.term_itemcount') as itemcount:
+            itemcount.return_value = []
+            context_data = widget.get_context_data(
+                title='test-name', taxonomy='tax'
+            )
+        self.assertTrue('data' in context_data)
+
+    def test_context_data_includes_correct_data(self):
+        widget = TermCountChartWidget()
+        with patch('hid.widgets.term_count_chart.term_itemcount') as itemcount:
+            itemcount.return_value = [
+                {
+                    'name': 'name one',
+                    'long_name': 'long name one',
+                    'count': 345
+                },
+                {
+                    'name': 'name two',
+                    'long_name': 'long name two',
+                    'count': 782
+                },
+            ]
+            context_data = widget.get_context_data(
+                title='test-name', taxonomy='tax'
+            )
+        self.assertEqual(
+            context_data['data'],
+            [[[345, 2], [782, 1]]]
+        )
+
+    def test_chart_questions_are_set_as_yaxis_value_labels(self):
+        widget = TermCountChartWidget()
+        with patch('hid.widgets.term_count_chart.term_itemcount') as itemcount:
+            itemcount.return_value = [
+                {
+                    'name': 'name one',
+                    'long_name': 'long name one',
+                    'count': 345
+                },
+                {
+                    'name': 'name two',
+                    'long_name': 'long name two',
+                    'count': 782
+                },
+            ]
+            context_data = widget.get_context_data(
+                title='test-name', taxonomy='tax'
+            )
+        self.assertEqual(
+            context_data['options']['yaxis']['ticks'],
+            [[2, 'long name one'], [1, 'long name two']]
+        )
+
+    def test_fetch_counts_orders_by_long_name(self):
+        widget = TermCountChartWidget()
+
+        with patch('hid.widgets.term_count_chart.term_itemcount') as itemcount:
+            itemcount.return_value = [
+                {
+                    'name': 'aaa-name',
+                    'long_name': 'zzz-long-name',
+                    'count': 0
+                },
+                {
+                    'name': 'zzz-name',
+                    'long_name': 'aaa-long-name',
+                    'count': 1000
+                },
+            ]
+            counts = widget._fetch_counts('tax', 0, 'Others')
+
+        self.assertEqual(
+            counts.items(),
+            [('aaa-long-name', 1000), ('zzz-long-name', 0)]
+        )
+
+    def test_fetch_counts_gets_n_larger_and_aggregates_others_items(self):
+        widget = TermCountChartWidget()
+
+        with patch('hid.widgets.term_count_chart.term_itemcount') as itemcount:
+            itemcount.return_value = [
+                {
+                    'name': 'name one',
+                    'long_name': 'long-name one',
+                    'count': 1
+                },
+                {
+                    'name': 'name two',
+                    'long_name': 'long-name two',
+                    'count': 10
+                },
+                {
+                    'name': 'name three',
+                    'long_name': 'long-name three',
+                    'count': 20
+                },
+                {
+                    'name': 'name four',
+                    'long_name': 'long-name four',
+                    'count': 30
+                },
+
+            ]
+            counts = widget._fetch_counts('tax', 3, 'Others')
+
+        self.assertEqual(
+            counts.items(),
+            [
+                ('long-name four', 30), ('long-name three', 20),
+                ('Others', 11)
+            ]
+        )
diff --git a/django/website/hid/views.py b/django/website/hid/views.py
index d2a9d02a142795d5bb799213dee2e226cec89830..3c65464d1030b15f35df6f679d505d4fbc049984 100644
--- a/django/website/hid/views.py
+++ b/django/website/hid/views.py
@@ -3,6 +3,7 @@ import re
 from collections import OrderedDict
 
 from django.contrib import messages
+from django.contrib.auth.views import login
 from django.core.urlresolvers import reverse
 from django.http import HttpResponseRedirect, QueryDict
 from django.utils.translation import ugettext as _
@@ -305,3 +306,13 @@ def process_items(request):
             messages.error(request, _('Unknown action'))
 
     return HttpResponseRedirect(redirect_url)
+
+
+def csrf_failure(request, reason=''):
+    # If the user presses the back button in the browser to go back to the
+    # login page and logs in again, they will get a CSRF error page because
+    # the token will be wrong.
+    # We override this with a redirect to the dashboard, which if not already
+    # logged in, will redirect to the login page (with a fresh token).
+
+    return HttpResponseRedirect(reverse('dashboard'))
diff --git a/django/website/hid/widgets/chart.py b/django/website/hid/widgets/chart.py
deleted file mode 100644
index 7e28ab4ad73e3a85e0d212292b88ff41c337829c..0000000000000000000000000000000000000000
--- a/django/website/hid/widgets/chart.py
+++ /dev/null
@@ -1,68 +0,0 @@
-class QuestionChartWidget(object):
-    """ A horizontal bar chart used to display quantitative answers to
-        specific questions.
-
-        Eventually this should pull the questions and values from the
-        data API. For now we pass these in via the settings.
-
-        Settings:
-            name: Name of the chart
-            questions: Dictionary of question to value
-    """
-    template_name = 'hid/widgets/chart.html'
-    javascript = [
-        'flot/jquery.flot.js',
-        'flot/jquery.flot.resize.js',
-        'hid/widgets/chart.js'
-    ]
-
-    def get_context_data(self, **kwargs):
-        index = 0
-        yticks = []
-        values = []
-        for question, answer in kwargs['questions'].items():
-            yticks.append([index, question])
-            values.append([answer, index])
-            index += 1
-
-        return {
-            'name': kwargs['name'],
-            'options': {
-                'series': {
-                    'bars': {
-                        'show': True,
-                        'fillColor': '#f29e30'
-                    },
-                    'color': 'transparent'
-                },
-                'bars': {
-                    'horizontal': True,
-                    'barWidth': 0.6,
-                    'align': 'center',
-                    'fill': True,
-                    'lineWidth': 0,
-                },
-                'yaxis': {
-                    'ticks': yticks,
-                    'tickLength': 0,
-                    'color': '#333333',
-                    'font': {
-                        'size': 12,
-                        'style': 'normal',
-                        'weight': 'normal',
-                        'family': 'sans-serif'
-                    }
-                },
-                'xaxis': {
-                    'autoscaleMargin': 0.1
-                },
-                'grid': {
-                    'hoverable': True,
-                    'borderWidth': 0,
-                    'margin': 10,
-                    'labelMargin': 20,
-                    'backgroundColor': '#fafafa'
-                }
-            },
-            'data': [values]
-        }
diff --git a/django/website/hid/widgets/term_count_chart.py b/django/website/hid/widgets/term_count_chart.py
new file mode 100644
index 0000000000000000000000000000000000000000..136650decd0b1317935d007359e79fa3d054e0e3
--- /dev/null
+++ b/django/website/hid/widgets/term_count_chart.py
@@ -0,0 +1,123 @@
+from collections import OrderedDict
+from django.utils.translation import ugettext_lazy as _
+from transport.taxonomies import term_itemcount
+
+
+class TermCountChartWidget(object):
+    """ A horizontal bar chart used to display number of entries for each
+        term of a taxonomy.
+
+        Settings:
+            title: Name of the widget
+            taxonomy: Slug of the taxonomy
+            count: Maximum number of terms to display.
+                   If this is >0, then only the countth most
+                   used terms are displayed, and all others
+                   are aggregated under 'Others'
+            other_label: Optional label to use instead of 'Others'
+    """
+    template_name = 'hid/widgets/chart.html'
+    javascript = [
+        'flot/jquery.flot.js',
+        'flot/jquery.flot.resize.js',
+        'hid/widgets/chart.js'
+    ]
+
+    def _fetch_counts(self, taxonomy, count, other_label):
+        """ Given a taxonomy, fetch the count per term.
+
+        Args:
+            - taxonomy: Taxonomy slug
+            - count: If >0, maximum number of rows to returns. If the data
+                     has more terms, all other terms are aggregated under
+                     an 'others' section
+            - other_label: Label for the 'Others' section
+        """
+        itemcount = term_itemcount(taxonomy)
+        itemcount.sort(key=lambda k: int(k['count']), reverse=True)
+        if count > 0:
+            head = itemcount[0:count-1]
+            tail = itemcount[count-1:]
+        else:
+            head = itemcount
+            tail = []
+        head.sort(key=lambda k: k['long_name'])
+        counts = OrderedDict()
+        for item in head:
+            counts[item['long_name']] = item['count']
+        if len(tail) > 0:
+            agg = 0
+            for item in tail:
+                agg = agg + item['count']
+            counts[other_label] = agg
+        return counts
+
+    def _create_axis_values(self, counts):
+        """ Given a dictionary of label to value, create the Y and X axis values
+            to be used in flot.
+
+        Args:
+            - chart: A dictionary of label to value
+
+        Returns:
+           A tuple containing (X Axis data, Y Axis data)
+        """
+        yticks = []
+        values = []
+        index = len(counts)
+        for label, value in counts.items():
+            yticks.append([index, label])
+            values.append([value, index])
+            index -= 1
+
+        return values, yticks
+
+    def get_context_data(self, **kwargs):
+        title = kwargs.get('title', _('(missing title)'))
+        taxonomy = kwargs.get('taxonomy')
+        count = kwargs.get('count', 0)
+        other_label = kwargs.get('other_label', 'Others')
+
+        counts = self._fetch_counts(taxonomy, count, other_label)
+        (values, yticks) = self._create_axis_values(counts)
+        return {
+            'title': title,
+            'options': {
+                'series': {
+                    'bars': {
+                        'show': True,
+                        'fillColor': '#f29e30'
+                    },
+                    'color': 'transparent'
+                },
+                'bars': {
+                    'horizontal': True,
+                    'barWidth': 0.6,
+                    'align': 'center',
+                    'fill': True,
+                    'lineWidth': 0,
+                },
+                'yaxis': {
+                    'ticks': yticks,
+                    'tickLength': 0,
+                    'color': '#333333',
+                    'font': {
+                        'size': 12,
+                        'style': 'normal',
+                        'weight': 'normal',
+                        'family': 'sans-serif'
+                    }
+                },
+                'xaxis': {
+                    'autoscaleMargin': 0.1
+                },
+                'grid': {
+                    'hoverable': True,
+                    'borderWidth': 0,
+                    'margin': 10,
+                    'labelMargin': 20,
+                    'backgroundColor': '#fafafa'
+                }
+            },
+            'data': [values]
+        }
diff --git a/django/website/settings.py b/django/website/settings.py
index 3493ef3b7112ddbdcfb56c864ce5e7ffad5cf557..96f2f289b9e81a2e8dc5e925aec4b31d983ca6fc 100644
--- a/django/website/settings.py
+++ b/django/website/settings.py
@@ -390,5 +390,6 @@ else:
     )
 ########## END TEMPLATE CONFIGURATION
 
+CSRF_FAILURE_VIEW = 'hid.views.csrf_failure'
 
 ########## Your stuff: Below this line define 3rd party libary settings