Add support for Geckoboard

This commit is contained in:
Joost Cassee 2011-02-14 08:40:27 +01:00
parent 77452ca267
commit 4c2a39beda
8 changed files with 632 additions and 58 deletions

View file

@ -22,7 +22,7 @@ Currently supported services:
* `Chartbeat`_ traffic analysis
* `Clicky`_ traffic analysis
* `Crazy Egg`_ visual click tracking
* `Geckoboard`_ dashboard
* `Geckoboard`_ status board
* `Google Analytics`_ traffic analysis
* `HubSpot`_ inbound marketing
* `KISSinsights`_ feedback surveys
@ -46,6 +46,7 @@ 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/

View file

@ -6,13 +6,12 @@ import base64
from xml.dom.minidom import Document
try:
from functools import update_wrapper, wraps
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,\
HttpResponseBadRequest
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
@ -44,7 +43,7 @@ class WidgetDecorator(object):
content = _render(request, data)
return HttpResponse(content)
wrapper = wraps(view_func, assigned=available_attrs(view_func))
return wrapper(_wrapped_view)
return csrf_exempt(wrapper(_wrapped_view))
def _convert_view_result(self, data):
# Extending classes do view result mangling here.
@ -81,19 +80,19 @@ class RAGWidgetDecorator(WidgetDecorator):
"""
def _convert_view_result(self, result):
data = {'item': []}
items = []
for elem in result:
if not isinstance(elem, (tuple, list)):
elem = [elem]
item = {}
item = SortedDict()
if elem[0] is None:
item['value'] = ''
else:
item['value'] = elem[0]
if len(elem) > 1:
item['text'] = elem[1]
data.append(item)
return data
items.append(item)
return {'item': items}
rag = RAGWidgetDecorator()
@ -109,18 +108,20 @@ class TextWidgetDecorator(WidgetDecorator):
"""
def _convert_view_result(self, result):
data = {'item': []}
items = []
if not isinstance(result, (tuple, list)):
result = [result]
for elem in result:
if not isinstance(elem, (tuple, list)):
elem = [elem]
item = {}
item = SortedDict()
item['text'] = elem[0]
if len(elem) > 1:
item['type'] = elem[1]
else:
item['type'] = TEXT_NONE
data.append(item)
return data
items.append(item)
return {'item': items}
text = TextWidgetDecorator()
@ -135,18 +136,18 @@ class PieChartWidgetDecorator(WidgetDecorator):
"""
def _convert_view_result(self, result):
data = {'item': []}
items = []
for elem in result:
if not isinstance(elem, (tuple, list)):
elem = [elem]
item = {}
item = SortedDict()
item['value'] = elem[0]
if len(elem) > 1:
item['label'] = elem[1]
if len(elem) > 2:
item['colour'] = elem[2]
data.append(item)
return data
items.append(item)
return {'item': items}
pie_chart = PieChartWidgetDecorator()
@ -166,23 +167,27 @@ class LineChartWidgetDecorator(WidgetDecorator):
"""
def _convert_view_result(self, result):
data = {'item': result[0], 'settings': {}}
data = SortedDict()
data['item'] = result[0]
data['settings'] = SortedDict()
x_axis = result[1]
if x_axis in None:
x_axis = ''
if not isinstance(x_axis, (tuple, list)):
x_axis = [x_axis]
data['settings']['axisx'] = x_axis
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
y_axis = result[2]
if y_axis in None:
y_axis = ''
if not isinstance(y_axis, (tuple, list)):
y_axis = [y_axis]
data['settings']['axisy'] = y_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 and result[3] is not None:
if len(result) > 3:
data['settings']['colour'] = result[3]
return data
@ -203,21 +208,22 @@ class GeckOMeterWidgetDecorator(WidgetDecorator):
def _convert_view_result(self, result):
value, min, max = result
data = {'item': value}
data = SortedDict()
data['item'] = value
data['max'] = SortedDict()
data['min'] = SortedDict()
if isinstance(min, (tuple, list)):
min = [min]
min_data = {'value': min[0]}
if len(min) > 1:
min_data['text'] = min[1]
data['min'] = min_data
if isinstance(max, (tuple, list)):
if not isinstance(max, (tuple, list)):
max = [max]
max_data = {'value': max[0]}
data['max']['value'] = max[0]
if len(max) > 1:
max_data['text'] = max[1]
data['max'] = max_data
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
@ -238,7 +244,10 @@ def _is_api_key_correct(request):
def _render(request, data):
if request.POST.get('format') == '2':
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)
@ -254,8 +263,12 @@ def _render_xml(data):
return doc.toxml()
def _build_xml(doc, parent, data):
func = _build_xml_vtable.get(type(data), _build_str_xml)
func(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)))
@ -276,17 +289,6 @@ def _build_dict_xml(doc, parent, data):
_build_xml(doc, elem, item)
parent.appendChild(elem)
_build_xml_vtable = {
list: _build_list_xml,
tuple: _build_list_xml,
dict: _build_dict_xml,
}
_render_vtable = {
'1': _render_xml,
'2': _render_json,
}
class GeckoboardException(Exception):
"""

View file

@ -2,6 +2,7 @@
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 *

View file

@ -0,0 +1,317 @@
"""
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)

View file

@ -19,3 +19,8 @@ def setup(app):
rolename = "lookup",
indextemplate = "pair: %s; field lookup type",
)
app.add_description_unit(
directivename = "decorator",
rolename = "dec",
indextemplate = "pair: %s; function decorator",
)

View file

@ -12,6 +12,11 @@ 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:

View file

@ -0,0 +1,242 @@
==========================
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.

View file

@ -1,6 +1,8 @@
from distutils.core import setup, Command
import os
os.environ['DJANGO_SETTINGS_MODULE'] = 'analytical.tests.settings'
cmdclass = {}
try:
@ -27,7 +29,6 @@ class TestCommand(Command):
pass
def run(self):
os.environ['DJANGO_SETTINGS_MODULE'] = 'analytical.tests.settings'
from analytical.tests.utils import run_tests
run_tests()