From 4c2a39beda601fc835e84030adbab067695803ca Mon Sep 17 00:00:00 2001 From: Joost Cassee Date: Mon, 14 Feb 2011 08:40:27 +0100 Subject: [PATCH] Add support for Geckoboard --- README.rst | 3 +- analytical/geckoboard.py | 114 +++++----- analytical/tests/__init__.py | 1 + analytical/tests/test_geckoboard.py | 317 ++++++++++++++++++++++++++++ docs/_ext/local.py | 5 + docs/install.rst | 5 + docs/services/geckoboard.rst | 242 +++++++++++++++++++++ setup.py | 3 +- 8 files changed, 632 insertions(+), 58 deletions(-) create mode 100644 analytical/tests/test_geckoboard.py create mode 100644 docs/services/geckoboard.rst 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' + '23', 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()