diff --git a/README.rst b/README.rst
index 06c8298..b095434 100644
--- a/README.rst
+++ b/README.rst
@@ -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/
diff --git a/analytical/geckoboard.py b/analytical/geckoboard.py
index 2626aed..fede327 100644
--- a/analytical/geckoboard.py
+++ b/analytical/geckoboard.py
@@ -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):
"""
diff --git a/analytical/tests/__init__.py b/analytical/tests/__init__.py
index 4ac739e..95bad00 100644
--- a/analytical/tests/__init__.py
+++ b/analytical/tests/__init__.py
@@ -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 *
diff --git a/analytical/tests/test_geckoboard.py b/analytical/tests/test_geckoboard.py
new file mode 100644
index 0000000..461637a
--- /dev/null
+++ b/analytical/tests/test_geckoboard.py
@@ -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('test',
+ 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('test',
+ 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('test',
+ 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('test',
+ 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('12',
+ 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('1
'
+ '2
3
', 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(''
+ '- test11
'
+ '- test22
',
+ 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)
diff --git a/docs/_ext/local.py b/docs/_ext/local.py
index ab91486..1441646 100644
--- a/docs/_ext/local.py
+++ b/docs/_ext/local.py
@@ -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",
+ )
diff --git a/docs/install.rst b/docs/install.rst
index 39c5ba1..048c4fb 100644
--- a/docs/install.rst
+++ b/docs/install.rst
@@ -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 ` for installation
+ instruction if you are not interested in the other services.
.. _installing-the-package:
diff --git a/docs/services/geckoboard.rst b/docs/services/geckoboard.rst
new file mode 100644
index 0000000..526c729
--- /dev/null
+++ b/docs/services/geckoboard.rst
@@ -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.
diff --git a/setup.py b/setup.py
index 38c2633..23bb209 100644
--- a/setup.py
+++ b/setup.py
@@ -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()