Skip to content
Snippets Groups Projects
Commit 580f95f0 authored by Martin Burchell's avatar Martin Burchell
Browse files

Merge branch 'develop' into view_and_edit_technical_debt

parents aa2e08f6 ced0e23a
No related branches found
No related tags found
No related merge requests found
Showing
with 375 additions and 57 deletions
{% 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>
Missing template_name for widget type {{empty_type}}
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)
......@@ -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'
)
......@@ -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'):
......
_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):
......
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())
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']]
)
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
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)
]
)
......@@ -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 _
......@@ -295,3 +296,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'))
class QuestionChartWidget(object):
""" A horizontal bar chart used to display quantitative answers to
specific questions.
from collections import OrderedDict
from django.utils.translation import ugettext_lazy as _
from transport.taxonomies import term_itemcount
Eventually this should pull the questions and values from the
data API. For now we pass these in via the settings.
class TermCountChartWidget(object):
""" A horizontal bar chart used to display number of entries for each
term of a taxonomy.
Settings:
name: Name of the chart
questions: Dictionary of question to value
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 = [
......@@ -16,17 +23,65 @@ class QuestionChartWidget(object):
'hid/widgets/chart.js'
]
def get_context_data(self, **kwargs):
index = 0
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 = []
for question, answer in kwargs['questions'].items():
yticks.append([index, question])
values.append([answer, index])
index += 1
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 {
'name': kwargs['name'],
'title': title,
'options': {
'series': {
'bars': {
......
......@@ -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
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment