mirror of
https://github.com/jazzband/django-analytical.git
synced 2026-03-16 22:20:25 +00:00
Split off Geckoboard support into django-geckoboard
This commit is contained in:
parent
f6cbd4b744
commit
246c72cc03
8 changed files with 7 additions and 864 deletions
|
|
@ -1,3 +1,9 @@
|
|||
Version 0.5.0
|
||||
-------------
|
||||
* Split off Geckoboard support into django-geckoboard_.
|
||||
|
||||
.. _django-geckoboard: http://pypi.python.org/pypi/django-geckoboard
|
||||
|
||||
Version 0.4.0
|
||||
-------------
|
||||
* Added support for the Geckoboard service.
|
||||
|
|
|
|||
|
|
@ -22,7 +22,6 @@ Currently supported services:
|
|||
* `Chartbeat`_ traffic analysis
|
||||
* `Clicky`_ traffic analysis
|
||||
* `Crazy Egg`_ visual click tracking
|
||||
* `Geckoboard`_ status board
|
||||
* `Google Analytics`_ traffic analysis
|
||||
* `HubSpot`_ inbound marketing
|
||||
* `KISSinsights`_ feedback surveys
|
||||
|
|
@ -46,7 +45,6 @@ an issue to discuss your plans.
|
|||
.. _Chartbeat: http://www.chartbeat.com/
|
||||
.. _Clicky: http://getclicky.com/
|
||||
.. _`Crazy Egg`: http://www.crazyegg.com/
|
||||
.. _Geckoboard: http://www.geckoboard.com/
|
||||
.. _`Google Analytics`: http://www.google.com/analytics/
|
||||
.. _HubSpot: http://www.hubspot.com/
|
||||
.. _KISSinsights: http://www.kissinsights.com/
|
||||
|
|
|
|||
|
|
@ -10,6 +10,6 @@ Django_ project. See the ``docs`` directory for more information.
|
|||
|
||||
__author__ = "Joost Cassee"
|
||||
__email__ = "joost@cassee.net"
|
||||
__version__ = "0.4.0"
|
||||
__version__ = "0.5.0"
|
||||
__copyright__ = "Copyright (C) 2011 Joost Cassee"
|
||||
__license__ = "MIT License"
|
||||
|
|
|
|||
|
|
@ -1,296 +0,0 @@
|
|||
"""
|
||||
Geckoboard decorators.
|
||||
"""
|
||||
|
||||
import base64
|
||||
from xml.dom.minidom import Document
|
||||
|
||||
try:
|
||||
from functools import wraps
|
||||
except ImportError:
|
||||
from django.utils.functional import wraps # Python 2.4 fallback
|
||||
|
||||
from django.conf import settings
|
||||
from django.http import HttpResponse, HttpResponseForbidden
|
||||
from django.views.decorators.csrf import csrf_exempt
|
||||
from django.utils.datastructures import SortedDict
|
||||
from django.utils.decorators import available_attrs
|
||||
from django.utils import simplejson
|
||||
|
||||
|
||||
TEXT_NONE = 0
|
||||
TEXT_INFO = 2
|
||||
TEXT_WARN = 1
|
||||
|
||||
|
||||
class WidgetDecorator(object):
|
||||
"""
|
||||
Geckoboard widget decorator.
|
||||
|
||||
The decorated view must return a data structure suitable for
|
||||
serialization to XML or JSON for Geckoboard. See the Geckoboard
|
||||
API docs or the source of extending classes for details.
|
||||
|
||||
If the GECKOBOARD_API_KEY setting is used, the request must contain
|
||||
the correct API key, or a 403 Forbidden response is returned.
|
||||
"""
|
||||
def __call__(self, view_func):
|
||||
def _wrapped_view(request, *args, **kwargs):
|
||||
if not _is_api_key_correct(request):
|
||||
return HttpResponseForbidden("Geckoboard API key incorrect")
|
||||
view_result = view_func(request, *args, **kwargs)
|
||||
data = self._convert_view_result(view_result)
|
||||
content = _render(request, data)
|
||||
return HttpResponse(content)
|
||||
wrapper = wraps(view_func, assigned=available_attrs(view_func))
|
||||
return csrf_exempt(wrapper(_wrapped_view))
|
||||
|
||||
def _convert_view_result(self, data):
|
||||
# Extending classes do view result mangling here.
|
||||
return data
|
||||
|
||||
widget = WidgetDecorator()
|
||||
|
||||
|
||||
class NumberWidgetDecorator(WidgetDecorator):
|
||||
"""
|
||||
Geckoboard number widget decorator.
|
||||
|
||||
The decorated view must return either a single value, or a list or
|
||||
tuple with one or two values. The first (or only) value represents
|
||||
the current value, the second value represents the previous value.
|
||||
"""
|
||||
|
||||
def _convert_view_result(self, result):
|
||||
if not isinstance(result, (tuple, list)):
|
||||
result = [result]
|
||||
return {'item': [{'value': v} for v in result]}
|
||||
|
||||
number = NumberWidgetDecorator()
|
||||
|
||||
|
||||
class RAGWidgetDecorator(WidgetDecorator):
|
||||
"""
|
||||
Geckoboard red-amber-green widget decorator.
|
||||
|
||||
The decorated view must return a tuple or list with three values,
|
||||
or three tuples (value, text). The values represent numbers shown
|
||||
in red, amber and green respectively. The text parameter is
|
||||
optional and will be displayed next to the value in the dashboard.
|
||||
"""
|
||||
|
||||
def _convert_view_result(self, result):
|
||||
items = []
|
||||
for elem in result:
|
||||
if not isinstance(elem, (tuple, list)):
|
||||
elem = [elem]
|
||||
item = SortedDict()
|
||||
if elem[0] is None:
|
||||
item['value'] = ''
|
||||
else:
|
||||
item['value'] = elem[0]
|
||||
if len(elem) > 1:
|
||||
item['text'] = elem[1]
|
||||
items.append(item)
|
||||
return {'item': items}
|
||||
|
||||
rag = RAGWidgetDecorator()
|
||||
|
||||
|
||||
class TextWidgetDecorator(WidgetDecorator):
|
||||
"""
|
||||
Geckoboard text widget decorator.
|
||||
|
||||
The decorated view must return a list or tuple of strings, or tuples
|
||||
(string, type). The type parameter tells Geckoboard how to display
|
||||
the text. Use TEXT_INFO for informational messages, TEXT_WARN for
|
||||
warnings and TEXT_NONE for plain text (the default).
|
||||
"""
|
||||
|
||||
def _convert_view_result(self, result):
|
||||
items = []
|
||||
if not isinstance(result, (tuple, list)):
|
||||
result = [result]
|
||||
for elem in result:
|
||||
if not isinstance(elem, (tuple, list)):
|
||||
elem = [elem]
|
||||
item = SortedDict()
|
||||
item['text'] = elem[0]
|
||||
if len(elem) > 1:
|
||||
item['type'] = elem[1]
|
||||
else:
|
||||
item['type'] = TEXT_NONE
|
||||
items.append(item)
|
||||
return {'item': items}
|
||||
|
||||
text = TextWidgetDecorator()
|
||||
|
||||
|
||||
class PieChartWidgetDecorator(WidgetDecorator):
|
||||
"""
|
||||
Geckoboard pie chart widget decorator.
|
||||
|
||||
The decorated view must return a list or tuple of tuples
|
||||
(value, label, color). The color parameter is a string 'RRGGBB[TT]'
|
||||
representing red, green, blue and optionally transparency.
|
||||
"""
|
||||
|
||||
def _convert_view_result(self, result):
|
||||
items = []
|
||||
for elem in result:
|
||||
if not isinstance(elem, (tuple, list)):
|
||||
elem = [elem]
|
||||
item = SortedDict()
|
||||
item['value'] = elem[0]
|
||||
if len(elem) > 1:
|
||||
item['label'] = elem[1]
|
||||
if len(elem) > 2:
|
||||
item['colour'] = elem[2]
|
||||
items.append(item)
|
||||
return {'item': items}
|
||||
|
||||
pie_chart = PieChartWidgetDecorator()
|
||||
|
||||
|
||||
class LineChartWidgetDecorator(WidgetDecorator):
|
||||
"""
|
||||
Geckoboard line chart widget decorator.
|
||||
|
||||
The decorated view must return a tuple (values, x_axis, y_axis,
|
||||
color). The value parameter is a tuple or list of data points. The
|
||||
x-axis parameter is a label string, or a tuple or list of strings,
|
||||
that will be placed on the X-axis. The y-axis parameter works
|
||||
similarly for the Y-axis. If there are more axis labels, they are
|
||||
placed evenly along the axis. The color parameter is a string
|
||||
'RRGGBB[TT]' representing red, green, blue and optionally
|
||||
transparency.
|
||||
"""
|
||||
|
||||
def _convert_view_result(self, result):
|
||||
data = SortedDict()
|
||||
data['item'] = result[0]
|
||||
data['settings'] = SortedDict()
|
||||
|
||||
if len(result) > 1:
|
||||
x_axis = result[1]
|
||||
if x_axis is None:
|
||||
x_axis = ''
|
||||
if not isinstance(x_axis, (tuple, list)):
|
||||
x_axis = [x_axis]
|
||||
data['settings']['axisx'] = x_axis
|
||||
|
||||
if len(result) > 2:
|
||||
y_axis = result[2]
|
||||
if y_axis is None:
|
||||
y_axis = ''
|
||||
if not isinstance(y_axis, (tuple, list)):
|
||||
y_axis = [y_axis]
|
||||
data['settings']['axisy'] = y_axis
|
||||
|
||||
if len(result) > 3:
|
||||
data['settings']['colour'] = result[3]
|
||||
|
||||
return data
|
||||
|
||||
line_chart = LineChartWidgetDecorator()
|
||||
|
||||
|
||||
class GeckOMeterWidgetDecorator(WidgetDecorator):
|
||||
"""
|
||||
Geckoboard Geck-O-Meter widget decorator.
|
||||
|
||||
The decorated view must return a tuple (value, min, max). The value
|
||||
parameter represents the current value. The min and max parameters
|
||||
represent the minimum and maximum value respectively. They are
|
||||
either a value, or a tuple (value, text). If used, the text
|
||||
parameter will be displayed next to the minimum or maximum value.
|
||||
"""
|
||||
|
||||
def _convert_view_result(self, result):
|
||||
value, min, max = result
|
||||
data = SortedDict()
|
||||
data['item'] = value
|
||||
data['max'] = SortedDict()
|
||||
data['min'] = SortedDict()
|
||||
|
||||
if not isinstance(max, (tuple, list)):
|
||||
max = [max]
|
||||
data['max']['value'] = max[0]
|
||||
if len(max) > 1:
|
||||
data['max']['text'] = max[1]
|
||||
|
||||
if not isinstance(min, (tuple, list)):
|
||||
min = [min]
|
||||
data['min']['value'] = min[0]
|
||||
if len(min) > 1:
|
||||
data['min']['text'] = min[1]
|
||||
|
||||
return data
|
||||
|
||||
geck_o_meter = GeckOMeterWidgetDecorator()
|
||||
|
||||
|
||||
def _is_api_key_correct(request):
|
||||
"""Return whether the Geckoboard API key on the request is correct."""
|
||||
api_key = getattr(settings, 'GECKOBOARD_API_KEY', None)
|
||||
if api_key is None:
|
||||
return True
|
||||
auth = request.META.get('HTTP_AUTHORIZATION', '').split()
|
||||
if len(auth) == 2:
|
||||
if auth[0].lower() == 'basic':
|
||||
request_key = base64.b64decode(auth[1])
|
||||
return request_key == api_key
|
||||
return False
|
||||
|
||||
|
||||
def _render(request, data):
|
||||
format = request.POST.get('format', '')
|
||||
if not format:
|
||||
format = request.GET.get('format', '')
|
||||
if format == '2':
|
||||
return _render_json(data)
|
||||
else:
|
||||
return _render_xml(data)
|
||||
|
||||
def _render_json(data):
|
||||
return simplejson.dumps(data)
|
||||
|
||||
def _render_xml(data):
|
||||
doc = Document()
|
||||
root = doc.createElement('root')
|
||||
doc.appendChild(root)
|
||||
_build_xml(doc, root, data)
|
||||
return doc.toxml()
|
||||
|
||||
def _build_xml(doc, parent, data):
|
||||
if isinstance(data, (tuple, list)):
|
||||
_build_list_xml(doc, parent, data)
|
||||
elif isinstance(data, dict):
|
||||
_build_dict_xml(doc, parent, data)
|
||||
else:
|
||||
_build_str_xml(doc, parent, data)
|
||||
|
||||
def _build_str_xml(doc, parent, data):
|
||||
parent.appendChild(doc.createTextNode(str(data)))
|
||||
|
||||
def _build_list_xml(doc, parent, data):
|
||||
for item in data:
|
||||
_build_xml(doc, parent, item)
|
||||
|
||||
def _build_dict_xml(doc, parent, data):
|
||||
for tag, item in data.items():
|
||||
if isinstance(item, (list, tuple)):
|
||||
for subitem in item:
|
||||
elem = doc.createElement(tag)
|
||||
_build_xml(doc, elem, subitem)
|
||||
parent.appendChild(elem)
|
||||
else:
|
||||
elem = doc.createElement(tag)
|
||||
_build_xml(doc, elem, item)
|
||||
parent.appendChild(elem)
|
||||
|
||||
|
||||
class GeckoboardException(Exception):
|
||||
"""
|
||||
Represents an error with the Geckoboard decorators.
|
||||
"""
|
||||
|
|
@ -2,7 +2,6 @@
|
|||
Tests for django-analytical.
|
||||
"""
|
||||
|
||||
from analytical.tests.test_geckoboard import *
|
||||
from analytical.tests.test_tag_analytical import *
|
||||
from analytical.tests.test_tag_chartbeat import *
|
||||
from analytical.tests.test_tag_clicky import *
|
||||
|
|
|
|||
|
|
@ -1,317 +0,0 @@
|
|||
"""
|
||||
Tests for the Geckoboard decorators.
|
||||
"""
|
||||
|
||||
from django.http import HttpRequest, HttpResponseForbidden
|
||||
from django.utils.datastructures import SortedDict
|
||||
|
||||
from analytical import geckoboard
|
||||
from analytical.tests.utils import TestCase
|
||||
import base64
|
||||
|
||||
|
||||
class WidgetDecoratorTestCase(TestCase):
|
||||
"""
|
||||
Tests for the ``widget`` decorator.
|
||||
"""
|
||||
|
||||
def setUp(self):
|
||||
super(WidgetDecoratorTestCase, self).setUp()
|
||||
self.settings_manager.delete('GECKOBOARD_API_KEY')
|
||||
self.xml_request = HttpRequest()
|
||||
self.xml_request.POST['format'] = '1'
|
||||
self.json_request = HttpRequest()
|
||||
self.json_request.POST['format'] = '2'
|
||||
|
||||
def test_api_key(self):
|
||||
self.settings_manager.set(GECKOBOARD_API_KEY='abc')
|
||||
req = HttpRequest()
|
||||
req.META['HTTP_AUTHORIZATION'] = "basic %s" % base64.b64encode('abc')
|
||||
resp = geckoboard.widget(lambda r: "test")(req)
|
||||
self.assertEqual('<?xml version="1.0" ?><root>test</root>',
|
||||
resp.content)
|
||||
|
||||
def test_missing_api_key(self):
|
||||
self.settings_manager.set(GECKOBOARD_API_KEY='abc')
|
||||
req = HttpRequest()
|
||||
resp = geckoboard.widget(lambda r: "test")(req)
|
||||
self.assertTrue(isinstance(resp, HttpResponseForbidden), resp)
|
||||
self.assertEqual('Geckoboard API key incorrect', resp.content)
|
||||
|
||||
def test_wrong_api_key(self):
|
||||
self.settings_manager.set(GECKOBOARD_API_KEY='abc')
|
||||
req = HttpRequest()
|
||||
req.META['HTTP_AUTHORIZATION'] = "basic %s" % base64.b64encode('def')
|
||||
resp = geckoboard.widget(lambda r: "test")(req)
|
||||
self.assertTrue(isinstance(resp, HttpResponseForbidden), resp)
|
||||
self.assertEqual('Geckoboard API key incorrect', resp.content)
|
||||
|
||||
def test_xml_get(self):
|
||||
req = HttpRequest()
|
||||
req.GET['format'] = '1'
|
||||
resp = geckoboard.widget(lambda r: "test")(req)
|
||||
self.assertEqual('<?xml version="1.0" ?><root>test</root>',
|
||||
resp.content)
|
||||
|
||||
def test_json_get(self):
|
||||
req = HttpRequest()
|
||||
req.GET['format'] = '2'
|
||||
resp = geckoboard.widget(lambda r: "test")(req)
|
||||
self.assertEqual('"test"', resp.content)
|
||||
|
||||
def test_xml_post(self):
|
||||
req = HttpRequest()
|
||||
req.POST['format'] = '1'
|
||||
resp = geckoboard.widget(lambda r: "test")(req)
|
||||
self.assertEqual('<?xml version="1.0" ?><root>test</root>',
|
||||
resp.content)
|
||||
|
||||
def test_json_post(self):
|
||||
req = HttpRequest()
|
||||
req.POST['format'] = '2'
|
||||
resp = geckoboard.widget(lambda r: "test")(req)
|
||||
self.assertEqual('"test"', resp.content)
|
||||
|
||||
def test_scalar_xml(self):
|
||||
resp = geckoboard.widget(lambda r: "test")(self.xml_request)
|
||||
self.assertEqual('<?xml version="1.0" ?><root>test</root>',
|
||||
resp.content)
|
||||
|
||||
def test_scalar_json(self):
|
||||
resp = geckoboard.widget(lambda r: "test")(self.json_request)
|
||||
self.assertEqual('"test"', resp.content)
|
||||
|
||||
def test_dict_xml(self):
|
||||
widget = geckoboard.widget(lambda r: SortedDict([('a', 1), ('b', 2)]))
|
||||
resp = widget(self.xml_request)
|
||||
self.assertEqual('<?xml version="1.0" ?><root><a>1</a><b>2</b></root>',
|
||||
resp.content)
|
||||
|
||||
def test_dict_json(self):
|
||||
widget = geckoboard.widget(lambda r: SortedDict([('a', 1), ('b', 2)]))
|
||||
resp = widget(self.json_request)
|
||||
self.assertEqual('{"a": 1, "b": 2}', resp.content)
|
||||
|
||||
def test_list_xml(self):
|
||||
widget = geckoboard.widget(lambda r: {'list': [1, 2, 3]})
|
||||
resp = widget(self.xml_request)
|
||||
self.assertEqual('<?xml version="1.0" ?><root><list>1</list>'
|
||||
'<list>2</list><list>3</list></root>', resp.content)
|
||||
|
||||
def test_list_json(self):
|
||||
widget = geckoboard.widget(lambda r: {'list': [1, 2, 3]})
|
||||
resp = widget(self.json_request)
|
||||
self.assertEqual('{"list": [1, 2, 3]}', resp.content)
|
||||
|
||||
def test_dict_list_xml(self):
|
||||
widget = geckoboard.widget(lambda r: {'item': [
|
||||
{'value': 1, 'text': "test1"}, {'value': 2, 'text': "test2"}]})
|
||||
resp = widget(self.xml_request)
|
||||
self.assertEqual('<?xml version="1.0" ?><root>'
|
||||
'<item><text>test1</text><value>1</value></item>'
|
||||
'<item><text>test2</text><value>2</value></item></root>',
|
||||
resp.content)
|
||||
|
||||
def test_dict_list_json(self):
|
||||
widget = geckoboard.widget(lambda r: {'item': [
|
||||
SortedDict([('value', 1), ('text', "test1")]),
|
||||
SortedDict([('value', 2), ('text', "test2")])]})
|
||||
resp = widget(self.json_request)
|
||||
self.assertEqual('{"item": [{"value": 1, "text": "test1"}, '
|
||||
'{"value": 2, "text": "test2"}]}', resp.content)
|
||||
|
||||
|
||||
class NumberDecoratorTestCase(TestCase):
|
||||
"""
|
||||
Tests for the ``number`` decorator.
|
||||
"""
|
||||
|
||||
def setUp(self):
|
||||
super(NumberDecoratorTestCase, self).setUp()
|
||||
self.settings_manager.delete('GECKOBOARD_API_KEY')
|
||||
self.request = HttpRequest()
|
||||
self.request.POST['format'] = '2'
|
||||
|
||||
def test_scalar(self):
|
||||
widget = geckoboard.number(lambda r: 10)
|
||||
resp = widget(self.request)
|
||||
self.assertEqual('{"item": [{"value": 10}]}', resp.content)
|
||||
|
||||
def test_singe_value(self):
|
||||
widget = geckoboard.number(lambda r: [10])
|
||||
resp = widget(self.request)
|
||||
self.assertEqual('{"item": [{"value": 10}]}', resp.content)
|
||||
|
||||
def test_two_values(self):
|
||||
widget = geckoboard.number(lambda r: [10, 9])
|
||||
resp = widget(self.request)
|
||||
self.assertEqual('{"item": [{"value": 10}, {"value": 9}]}',
|
||||
resp.content)
|
||||
|
||||
|
||||
class RAGDecoratorTestCase(TestCase):
|
||||
"""
|
||||
Tests for the ``rag`` decorator.
|
||||
"""
|
||||
|
||||
def setUp(self):
|
||||
super(RAGDecoratorTestCase, self).setUp()
|
||||
self.settings_manager.delete('GECKOBOARD_API_KEY')
|
||||
self.request = HttpRequest()
|
||||
self.request.POST['format'] = '2'
|
||||
|
||||
def test_scalars(self):
|
||||
widget = geckoboard.rag(lambda r: (10, 5, 1))
|
||||
resp = widget(self.request)
|
||||
self.assertEqual(
|
||||
'{"item": [{"value": 10}, {"value": 5}, {"value": 1}]}',
|
||||
resp.content)
|
||||
|
||||
def test_tuples(self):
|
||||
widget = geckoboard.rag(lambda r: ((10, "ten"), (5, "five"),
|
||||
(1, "one")))
|
||||
resp = widget(self.request)
|
||||
self.assertEqual('{"item": [{"value": 10, "text": "ten"}, '
|
||||
'{"value": 5, "text": "five"}, {"value": 1, "text": "one"}]}',
|
||||
resp.content)
|
||||
|
||||
|
||||
class TextDecoratorTestCase(TestCase):
|
||||
"""
|
||||
Tests for the ``text`` decorator.
|
||||
"""
|
||||
|
||||
def setUp(self):
|
||||
super(TextDecoratorTestCase, self).setUp()
|
||||
self.settings_manager.delete('GECKOBOARD_API_KEY')
|
||||
self.request = HttpRequest()
|
||||
self.request.POST['format'] = '2'
|
||||
|
||||
def test_string(self):
|
||||
widget = geckoboard.text(lambda r: "test message")
|
||||
resp = widget(self.request)
|
||||
self.assertEqual('{"item": [{"text": "test message", "type": 0}]}',
|
||||
resp.content)
|
||||
|
||||
def test_list(self):
|
||||
widget = geckoboard.text(lambda r: ["test1", "test2"])
|
||||
resp = widget(self.request)
|
||||
self.assertEqual('{"item": [{"text": "test1", "type": 0}, '
|
||||
'{"text": "test2", "type": 0}]}', resp.content)
|
||||
|
||||
def test_list_tuples(self):
|
||||
widget = geckoboard.text(lambda r: [("test1", geckoboard.TEXT_NONE),
|
||||
("test2", geckoboard.TEXT_INFO),
|
||||
("test3", geckoboard.TEXT_WARN)])
|
||||
resp = widget(self.request)
|
||||
self.assertEqual('{"item": [{"text": "test1", "type": 0}, '
|
||||
'{"text": "test2", "type": 2}, '
|
||||
'{"text": "test3", "type": 1}]}', resp.content)
|
||||
|
||||
|
||||
class PieChartDecoratorTestCase(TestCase):
|
||||
"""
|
||||
Tests for the ``pie_chart`` decorator.
|
||||
"""
|
||||
|
||||
def setUp(self):
|
||||
super(PieChartDecoratorTestCase, self).setUp()
|
||||
self.settings_manager.delete('GECKOBOARD_API_KEY')
|
||||
self.request = HttpRequest()
|
||||
self.request.POST['format'] = '2'
|
||||
|
||||
def test_scalars(self):
|
||||
widget = geckoboard.pie_chart(lambda r: [1, 2, 3])
|
||||
resp = widget(self.request)
|
||||
self.assertEqual(
|
||||
'{"item": [{"value": 1}, {"value": 2}, {"value": 3}]}',
|
||||
resp.content)
|
||||
|
||||
def test_tuples(self):
|
||||
widget = geckoboard.pie_chart(lambda r: [(1, ), (2, ), (3, )])
|
||||
resp = widget(self.request)
|
||||
self.assertEqual(
|
||||
'{"item": [{"value": 1}, {"value": 2}, {"value": 3}]}',
|
||||
resp.content)
|
||||
|
||||
def test_2tuples(self):
|
||||
widget = geckoboard.pie_chart(lambda r: [(1, "one"), (2, "two"),
|
||||
(3, "three")])
|
||||
resp = widget(self.request)
|
||||
self.assertEqual('{"item": [{"value": 1, "label": "one"}, '
|
||||
'{"value": 2, "label": "two"}, '
|
||||
'{"value": 3, "label": "three"}]}', resp.content)
|
||||
|
||||
def test_3tuples(self):
|
||||
widget = geckoboard.pie_chart(lambda r: [(1, "one", "00112233"),
|
||||
(2, "two", "44556677"), (3, "three", "8899aabb")])
|
||||
resp = widget(self.request)
|
||||
self.assertEqual('{"item": ['
|
||||
'{"value": 1, "label": "one", "colour": "00112233"}, '
|
||||
'{"value": 2, "label": "two", "colour": "44556677"}, '
|
||||
'{"value": 3, "label": "three", "colour": "8899aabb"}]}',
|
||||
resp.content)
|
||||
|
||||
|
||||
class LineChartDecoratorTestCase(TestCase):
|
||||
"""
|
||||
Tests for the ``line_chart`` decorator.
|
||||
"""
|
||||
|
||||
def setUp(self):
|
||||
super(LineChartDecoratorTestCase, self).setUp()
|
||||
self.settings_manager.delete('GECKOBOARD_API_KEY')
|
||||
self.request = HttpRequest()
|
||||
self.request.POST['format'] = '2'
|
||||
|
||||
def test_values(self):
|
||||
widget = geckoboard.line_chart(lambda r: ([1, 2, 3],))
|
||||
resp = widget(self.request)
|
||||
self.assertEqual('{"item": [1, 2, 3], "settings": {}}', resp.content)
|
||||
|
||||
def test_x_axis(self):
|
||||
widget = geckoboard.line_chart(lambda r: ([1, 2, 3],
|
||||
["first", "last"]))
|
||||
resp = widget(self.request)
|
||||
self.assertEqual('{"item": [1, 2, 3], '
|
||||
'"settings": {"axisx": ["first", "last"]}}', resp.content)
|
||||
|
||||
def test_axes(self):
|
||||
widget = geckoboard.line_chart(lambda r: ([1, 2, 3],
|
||||
["first", "last"], ["low", "high"]))
|
||||
resp = widget(self.request)
|
||||
self.assertEqual('{"item": [1, 2, 3], "settings": '
|
||||
'{"axisx": ["first", "last"], "axisy": ["low", "high"]}}',
|
||||
resp.content)
|
||||
|
||||
def test_color(self):
|
||||
widget = geckoboard.line_chart(lambda r: ([1, 2, 3],
|
||||
["first", "last"], ["low", "high"], "00112233"))
|
||||
resp = widget(self.request)
|
||||
self.assertEqual('{"item": [1, 2, 3], "settings": '
|
||||
'{"axisx": ["first", "last"], "axisy": ["low", "high"], '
|
||||
'"colour": "00112233"}}', resp.content)
|
||||
|
||||
|
||||
class GeckOMeterDecoratorTestCase(TestCase):
|
||||
"""
|
||||
Tests for the ``line_chart`` decorator.
|
||||
"""
|
||||
|
||||
def setUp(self):
|
||||
super(GeckOMeterDecoratorTestCase, self).setUp()
|
||||
self.settings_manager.delete('GECKOBOARD_API_KEY')
|
||||
self.request = HttpRequest()
|
||||
self.request.POST['format'] = '2'
|
||||
|
||||
def test_scalars(self):
|
||||
widget = geckoboard.geck_o_meter(lambda r: (2, 1, 3))
|
||||
resp = widget(self.request)
|
||||
self.assertEqual('{"item": 2, "max": {"value": 3}, '
|
||||
'"min": {"value": 1}}', resp.content)
|
||||
|
||||
def test_tuples(self):
|
||||
widget = geckoboard.geck_o_meter(lambda r: (2, (1, "min"), (3, "max")))
|
||||
resp = widget(self.request)
|
||||
self.assertEqual('{"item": 2, "max": {"value": 3, "text": "max"}, '
|
||||
'"min": {"value": 1, "text": "min"}}', resp.content)
|
||||
|
|
@ -12,11 +12,6 @@ configuring the services you use in the project settings.
|
|||
#. `Adding the template tags to the base template`_
|
||||
#. `Enabling the services`_
|
||||
|
||||
.. note::
|
||||
|
||||
The Geckoboard integration differs from that of the other services.
|
||||
See :doc:`Geckoboard <services/geckoboard>` for installation
|
||||
instruction if you are not interested in the other services.
|
||||
|
||||
.. _installing-the-package:
|
||||
|
||||
|
|
|
|||
|
|
@ -1,242 +0,0 @@
|
|||
==========================
|
||||
Geckoboard -- status board
|
||||
==========================
|
||||
|
||||
Geckoboard_ is a hosted, real-time status board serving up indicators
|
||||
from web analytics, CRM, support, infrastructure, project management,
|
||||
sales, etc. It can be connected to virtually any source of quantitative
|
||||
data.
|
||||
|
||||
.. _Geckoboard: https://www.geckoboard.com/
|
||||
|
||||
|
||||
.. _geckoboard-installation:
|
||||
|
||||
Installation
|
||||
============
|
||||
|
||||
Geckoboard works differently from most other analytics services in that
|
||||
it pulls measurement data from your website periodically. You will not
|
||||
have to change anything in your existing templates, and there is no need
|
||||
to install the ``analytical`` application to use the integration.
|
||||
Instead you will use custom views to serve the data to Geckoboard custom
|
||||
widgets.
|
||||
|
||||
|
||||
.. _geckoboard-configuration:
|
||||
|
||||
Configuration
|
||||
=============
|
||||
|
||||
If you want to protect the data you send to Geckoboard from access by
|
||||
others, you can use an API key shared by Geckoboard and your widget
|
||||
views. Set :const:`GECKOBOARD_API_KEY` in the project
|
||||
:file:`settings.py` file::
|
||||
|
||||
GECKOBOARD_API_KEY = 'XXXXXXXXX'
|
||||
|
||||
Provide the API key to the custom widget configuration in Geckoboard.
|
||||
If you do not set an API key, anyone will be able to view the data by
|
||||
visiting the widget URL.
|
||||
|
||||
|
||||
Creating custom widgets and charts
|
||||
==================================
|
||||
|
||||
The available custom widgets are described in the Geckoboard support
|
||||
section, under `Geckoboard API`_. From the perspective of a Django
|
||||
project, a custom widget is just a view. The django-analytical
|
||||
application provides view decorators that render the correct response
|
||||
for the different widgets. When you create a custom widget, enter the
|
||||
following information:
|
||||
|
||||
URL data feed
|
||||
The view URL.
|
||||
|
||||
API key
|
||||
The content of the :const:`GECKOBOARD_API_KEY` setting, if you have
|
||||
set it.
|
||||
|
||||
Widget type
|
||||
*Custom*
|
||||
|
||||
Feed format
|
||||
Either *XML* or *JSON*. The view decorators will automatically
|
||||
detect and output the correct format.
|
||||
|
||||
Request type
|
||||
Either *GET* or *POST*. The view decorators accept both.
|
||||
|
||||
Then create a view using one of the decorators from the
|
||||
:mod:`analytical.geckoboard` module.
|
||||
|
||||
.. decorator:: number
|
||||
|
||||
Render a *Number & Secondary Stat* widget.
|
||||
|
||||
The decorated view must return either a single value, or a list or
|
||||
tuple with one or two values. The first (or only) value represents
|
||||
the current value, the second value represents the previous value.
|
||||
For example, to render a widget that shows the number of active
|
||||
users and the difference from last week::
|
||||
|
||||
from analytical import geckoboard
|
||||
from datetime import datetime, timedelta
|
||||
from django.contrib.auth.models import User
|
||||
|
||||
@geckoboard.number
|
||||
def user_count(request):
|
||||
last_week = datetime.now() - timedelta(weeks=1)
|
||||
users = User.objects.filter(is_active=True)
|
||||
last_week_users = users.filter(date_joined__lt=last_week)
|
||||
return (users.count(), last_week_users.count())
|
||||
|
||||
|
||||
.. decorator:: rag
|
||||
|
||||
Render a *RAG Column & Numbers* or *RAG Numbers* widget.
|
||||
|
||||
The decorated view must return a tuple or list with three values, or
|
||||
three tuples (value, text). The values represent numbers shown in
|
||||
red, amber and green respectively. The text parameter is optional
|
||||
and will be displayed next to the value in the dashboard. For
|
||||
example, to render a widget that shows the number of comments that
|
||||
were approved or deleted by moderators in the last 24 hours::
|
||||
|
||||
from analytical import geckoboard
|
||||
from datetime import datetime, timedelta
|
||||
from django.contrib.comments.models import Comment, CommentFlag
|
||||
|
||||
@geckoboard.rag
|
||||
def comments(request):
|
||||
start_time = datetime.now() - timedelta(hours=24)
|
||||
comments = Comment.objects.filter(submit_date__gt=start_time)
|
||||
total_count = comments.count()
|
||||
approved_count = comments.filter(
|
||||
flags__flag=CommentFlag.MODERATOR_APPROVAL).count()
|
||||
deleted_count = Comment.objects.filter(
|
||||
flags__flag=CommentFlag.MODERATOR_DELETION).count()
|
||||
pending_count = total_count - approved_count - deleted_count
|
||||
return (
|
||||
(deleted_count, "Deleted comments"),
|
||||
(pending_count, "Pending comments"),
|
||||
(approved_count, "Approved comments"),
|
||||
)
|
||||
|
||||
|
||||
.. decorator:: text
|
||||
|
||||
Render a *Text* widget.
|
||||
|
||||
The decorated view must return either a string, a list or tuple of
|
||||
strings, or a list or tuple of tuples (string, type). The type
|
||||
parameter tells Geckoboard how to display the text. Use
|
||||
:const:`TEXT_INFO` for informational messages, :const:`TEXT_WARN`
|
||||
for warnings and :const:`TEXT_NONE` for plain text (the default).
|
||||
For example, to render a widget showing the latest Geckoboard
|
||||
twitter updates::
|
||||
|
||||
from analytical import geckoboard
|
||||
import twitter
|
||||
|
||||
@geckoboard.text
|
||||
def twitter_status(request):
|
||||
twitter = twitter.Api()
|
||||
updates = twitter.GetUserTimeline('geckoboard')
|
||||
return [(u.text, geckoboard.TEXT_NONE) for u in updates]
|
||||
|
||||
|
||||
|
||||
.. decorator:: pie_chart
|
||||
|
||||
Render a *Pie chart* widget.
|
||||
|
||||
The decorated view must return a list or tuple of tuples
|
||||
(value, label, color). The color parameter is a string
|
||||
``'RRGGBB[TT]'`` representing red, green, blue and optionally
|
||||
transparency. For example, to render a widget showing the number
|
||||
of normal, staff and superusers::
|
||||
|
||||
from analytical import geckoboard
|
||||
from django.contrib.auth.models import User
|
||||
|
||||
@geckoboard.pie_chart
|
||||
def user_types(request):
|
||||
users = User.objects.filter(is_active=True)
|
||||
total_count = users.count()
|
||||
superuser_count = users.filter(is_superuser=True).count()
|
||||
staff_count = users.filter(is_staff=True,
|
||||
is_superuser=False).count()
|
||||
normal_count = total_count = superuser_count - staff_count
|
||||
return [
|
||||
(normal_count, "Normal users", "ff8800"),
|
||||
(staff_count, "Staff", "00ff88"),
|
||||
(superuser_count, "Superusers", "8800ff"),
|
||||
]
|
||||
|
||||
|
||||
.. decorator:: line_chart
|
||||
|
||||
Render a *Line chart* widget.
|
||||
|
||||
The decorated view must return a tuple (values, x_axis, y_axis,
|
||||
color). The value parameter is a tuple or list of data points. The
|
||||
x-axis parameter is a label string, or a tuple or list of strings,
|
||||
that will be placed on the X-axis. The y-axis parameter works
|
||||
similarly for the Y-axis. If there are more axis labels, they are
|
||||
placed evenly along the axis. The optional color parameter is a
|
||||
string ``'RRGGBB[TT]'`` representing red, green, blue and optionally
|
||||
transparency. For example, to render a widget showing the number
|
||||
of comments per day over the last four weeks (including today)::
|
||||
|
||||
from analytical import geckoboard
|
||||
from datetime import date, timedelta
|
||||
from django.contrib.comments.models import Comment
|
||||
|
||||
@geckoboard.line_chart
|
||||
def comment_trend(request):
|
||||
since = date.today() - timedelta(days=29)
|
||||
days = dict((since + timedelta(days=d), 0)
|
||||
for d in range(0, 29))
|
||||
comments = Comment.objects.filter(submit_date=since)
|
||||
for comment in comments:
|
||||
days[comment.submit_date.date()] += 1
|
||||
return (
|
||||
days.values(),
|
||||
[days[i] for i in range(0, 29, 7)],
|
||||
"Comments",
|
||||
)
|
||||
|
||||
|
||||
.. decorator:: geck_o_meter
|
||||
|
||||
Render a *Geck-O-Meter* widget.
|
||||
|
||||
The decorated view must return a tuple (value, min, max). The value
|
||||
parameter represents the current value. The min and max parameters
|
||||
represent the minimum and maximum value respectively. They are
|
||||
either a value, or a tuple (value, text). If used, the text
|
||||
parameter will be displayed next to the minimum or maximum value.
|
||||
For example, to render a widget showing the number of users that
|
||||
have logged in in the last 24 hours::
|
||||
|
||||
from analytical import geckoboard
|
||||
from datetime import datetime, timedelta
|
||||
from django.contrib.auth.models import User
|
||||
|
||||
@geckoboard.geck_o_meter
|
||||
def login_count(request):
|
||||
since = datetime.now() - timedelta(hours=24)
|
||||
users = User.objects.filter(is_active=True)
|
||||
total_count = users.count()
|
||||
logged_in_count = users.filter(last_login__gt=since).count()
|
||||
return (logged_in_count, 0, total_count)
|
||||
|
||||
|
||||
.. _`Geckoboard API`: http://geckoboard.zendesk.com/forums/207979-geckoboard-api
|
||||
|
||||
|
||||
----
|
||||
|
||||
Thanks go to Geckoboard for their support with the development of this
|
||||
application.
|
||||
Loading…
Reference in a new issue