Skip to content

Commit

Permalink
Tidied exception response handling and implemented features for Detai…
Browse files Browse the repository at this point in the history
…lView tests to pass.
  • Loading branch information
alexdutton committed Jan 7, 2012
1 parent 3c35670 commit c12954d
Show file tree
Hide file tree
Showing 3 changed files with 157 additions and 71 deletions.
5 changes: 5 additions & 0 deletions openorg_timeseries/forms.py
Expand Up @@ -55,3 +55,8 @@ class ArchiveForm(forms.Form):
count = forms.IntegerField()

ArchiveFormSet = formset_factory(ArchiveForm, extra=3)

class TimeSeriesForm(forms.ModelForm):
class Meta:
model = models.TimeSeries
fields = ('title', 'notes', 'is_public')
3 changes: 3 additions & 0 deletions openorg_timeseries/models.py
Expand Up @@ -134,6 +134,9 @@ def delete(self, *args, **kwargs):
database_client.delete(self.slug)
super(TimeSeries, self).delete(*args, **kwargs)

def append(self, readings):
database_client = get_client()
database_client.append(self.slug, readings)

def get_absolute_url(self):
return reverse('timeseries:detail', args=[self.slug])
Expand Down
220 changes: 149 additions & 71 deletions openorg_timeseries/views/admin.py
@@ -1,24 +1,25 @@
import calendar
import csv
import datetime
import httplib
import urllib
import urlparse

try:
import json
except ImportError:
import simplejson as json

import dateutil
from django.db import IntegrityError
from django.conf import settings
from django.core.urlresolvers import reverse
from django.core.exceptions import PermissionDenied
from django.http import HttpResponsePermanentRedirect, Http404, HttpResponseBadRequest
from django.http import HttpResponsePermanentRedirect, HttpResponse
from django.views.generic import View
from django.db.models.query import QuerySet
from django.shortcuts import get_object_or_404
from django_conneg.views import JSONView, HTMLView, JSONPView, TextView
from django_conneg.http import HttpResponseSeeOther
from django_conneg.support import login_required

import pytz

import openorg_timeseries
from openorg_timeseries import forms
Expand All @@ -31,13 +32,41 @@ def dispatch(self, request, context, template_name):
template_name = (template_name, 'timeseries-admin/error')
return self.render(request, context, template_name)


class TimeSeriesView(JSONView):
_error_view = staticmethod(ErrorView.as_view())
_error = staticmethod(ErrorView.as_view())
_default_format = 'json'

def error(self, status_code, **kwargs):
return self._error(self.request,
dict(status_code=status_code, **kwargs),
'timeseries-admin/error')

def lacking_privilege(self, action):
return self._error(self.request,
{'status_code': httplib.FORBIDDEN,
'error': 'lacking-privilege',
'message': 'The authenticated user lacks the necessary privilege to %s' % action},
'timeseries-admin/error-lacking-privilege')

def invalid_json(self, exception):
return self._error(self.request,
{'status_code': httplib.BAD_REQUEST,
'error': 'invalid-json',
'message': 'Could not parse request body as JSON',
'detail': unicode(exception)},
'timeseries-admin/error-invalid-json')

def bad_request(self, error, message):
return self._error(self.request,
{'status_code': httplib.BAD_REQUEST,
'error': error,
'message': message},
'timeseries-admin/error-bad-request')

_json_indent = 2
def simplify(self, value):
if isinstance(value, datetime.datetime):
return 1000 * calendar.timegm(value.astimezone(pytz.utc).timetuple())
if isinstance(value, QuerySet):
return map(self.simplify, value)
if isinstance(value, TimeSeries):
Expand All @@ -52,7 +81,7 @@ def simplify(self, value):
data['equation'] = value.equation
else:
data['config'] = value.config
return data
return self.simplify(data)
else:
return super(TimeSeriesView, self).simplify(value)

Expand All @@ -69,30 +98,20 @@ def get(self, request):
series = [s for s in series if request.user.has_perm('view_timeseries', s)]
context = {
'series': series,
'server': {'name': 'openorg_timeseries.admin',
'version': openorg_timeseries.__version__},
}
return self.render(request, context, 'timeseries-admin/index')

def post(self, request):
if not request.user.has_perm('openorg_timeseries.add_timeseries'):
return self._error_view(request,
{'status_code': 403,
'error': 'lacking-privilege',
'message': 'The authenticated user lacks the necessary privilege to create a new time-series'},
'timeseries-admin/index-lacking-privilege')
return self.lacking_privilege('create a new time-series')
if request.META.get('CONTENT_TYPE') != 'application/json':
return self._error_view(request,
{'status_code': 400,
'error': 'wrong-content-type',
'message': 'Content-Type must be "application/json"'},
'timeseries-admin/index-wrong-content-type')
return self.bad_request('wrong-content-type', 'Content-Type must be "application/json"')
try:
data = json.load(request)
except ValueError:
return self._error_view(request,
{'status_code': 400,
'error': 'invalid-json',
'message': 'Could not parse request body as JSON'},
'timeseries-admin/index-parse-failure')
except ValueError, e:
return self._error.invalid_json(request, e)


time_series = TimeSeries()
Expand All @@ -101,21 +120,17 @@ def post(self, request):
try:
setattr(time_series, key, data[key])
except KeyError:
return self._error_view(request,
{'status_code': 400,
'error': 'missing-field',
'field': key,
'message': 'Field "%s" was missing.' % key},
'timeseries-admin/index-missing-field')
return self.error(httplib.BAD_REQUEST,
error='missing-field',
field=key,
message='Field "%s" was missing.' % key)
try:
time_series.save()
except IntegrityError:
# A time-series already exists with the desired slug
return self._error_view(request,
{'status_code': httplib.CONFLICT,
'error': 'already-exists',
'message': 'A time-series already exists with the slug %r.' % time_series.slug},
'timeseries-admin/index-already-exists')
return self.error(httplib.CONFLICT,
error='already-exists',
message='A time-series already exists with the slug %r.' % time_series.slug)
for perm in ('view_timeseries', 'append_timeseries', 'change_timeseries', 'delete_timeseries'):
request.user.grant(perm, time_series)
return self.render(request,
Expand All @@ -133,11 +148,7 @@ def common(self, request):
@login_required
def dispatch(self, request):
if not request.user.has_perm('openorg_timeseries.add_timeseries'):
return self._error_view(request,
{'status_code': 403,
'error': 'lacking-privilege',
'message': 'The authenticated user lacks the necessary privilege to create a new time-series'},
'timeseries-admin/index-lacking-privilege')
return self._error.lacking_privilege(request, 'create a new time-series')
super(CreateView, self).dispatch(request, self.common(request))

def get(self, request, context):
Expand All @@ -160,19 +171,6 @@ def post(self, request, context):
time_series.save()
return HttpResponseSeeOther(time_series.get_absolute_url())


class DetailView(HTMLView, JSONPView, TimeSeriesView):
def common(self, request, slug):
context = {}
context['series'] = get_object_or_404(TimeSeries, slug=slug)
return context

def get(self, request, slug):
context = self.common(request, slug)
if not context['series'].is_public:
raise Http404
return self.render(request, context, 'timeseries/rest-detail')

class SecureView(View):
force_https = getattr(settings, 'FORCE_ADMIN_HTTPS', True)

Expand All @@ -183,24 +181,15 @@ def dispatch(self, request, *args, **kwargs):
return HttpResponsePermanentRedirect(url)
return super(SecureView, self).dispatch(request, *args, **kwargs)

class AuthenticatedView(SecureView):
def dispatch(self, request, *args, **kwargs):
if not request.user.is_authenticated():
url = '%s?%s' % (settings.LOGIN_URL,
urllib.urlencode({'next': request.build_absolute_uri()}))
return HttpResponseSeeOther(url)
return super(AuthenticatedView, self).dispatch(request, *args, **kwargs)

class AdminListView(HTMLView, JSONPView, AuthenticatedView):
pass

class AdminDetailView(AuthenticatedView, DetailView):
class DetailView(TimeSeriesView, HTMLView):
def common(self, request, slug):
context = super(AdminDetailView, self).common(request, slug)
if not request.user.has_perm('view_timeseries', context['series']):
raise PermissionDenied
form_class = VirtualTimeSeriesForm if context['series'].is_virtual else RealTimeSeriesForm
context['form'] = form_class(request.POST or None, instance=context['series'])
context = {
'series': get_object_or_404(TimeSeries, slug=slug),
}
if not request.user.get_perms(context['series']):
return self.lacking_privilege('view this time-series')
context['form'] = forms.TimeSeriesForm(request.POST or None,
instance=context['series'])
return context

def get(self, request, slug):
Expand All @@ -209,5 +198,94 @@ def get(self, request, slug):

def post(self, request, slug):
context = self.common(request, slug)
return self.render(request, context, 'timeseries/admin-detail')
series = context['series']

if request.META.get('CONTENT_TYPE') == 'application/json':
try:
request.json_data = json.load(request)
except ValueError, e:
return self.invalid_json(e)
else:
request.json_data = None

context = {}
try:
readings = self.get_readings(request)
except ValueError, e:
return self.error(httplib.BAD_REQUEST, type='value-error', message=e.args[0])
if readings and series.is_virtual:
return self.bad_request("append-to-virtual", "You cannot append readings to a virtual time-series")
if readings and not request.user.has_perm('append_timeseries', series):
return self.lacking_privilege("append to this time-series")
elif readings:
context['readings'] = {'count': len(readings)}
series.append(readings)

editable_fields = ('title', 'notes')
if request.json_data and any(f in request.json_data for f in editable_fields):
context['updated'] = []
if not request.user.has_perm('change_timeseries', series):
return self.lacking_privilege("modify this time-series")
for f in editable_fields:
if f in request.json_data:
if not isinstance(request.json_data[f], unicode):
return self.bad_request("type-error", "Field '%s' must be a string" % f)
setattr(series, f, request.json_data[f])
context['updated'].append(f)
series.save()

return self.render(request, context, 'timeseries-admin/detail-post')


def get_readings(self, request):
readings = []

if request.json_data and 'readings' in request.json_data:
if not isinstance(request.json_data['readings'], list):
raise ValueError('"readings" member should be a list.')
for i, reading in enumerate(request.json_data['readings']):
try:
if isinstance(reading, dict):
reading = reading['ts'], reading['val']
elif isinstance(reading, list) and len(reading) == 2:
pass
else:
raise ValueError("Reading %i must be either an object with 'ts' and 'val' members, or a two-element list." % i)

readings.append(reading)

except KeyError, e:
raise ValueError("Reading %i was missing a '%s' member" % (i, e.args[0]))
except ValueError, e:
raise ValueError("Reading %i: %s" % (i, e.args[0]))
elif request.META.get('CONTENT_TYPE') == 'text/csv':
reader = csv.reader(request)
for i, row in enumerate(reader):
if len(row) != 2:
raise ValueError("Row %i doesn't have two columns" % i)
row[1] = float(row[1])
readings.append(row)

parsed_readings = []
for reading in readings:
if isinstance(reading[0], basestring):
ts = dateutil.parser.parse(reading[0])
elif isinstance(reading[0], (int, float)):
ts = pytz.utc.localize(datetime.datetime.utcfromtimestamp(reading[0] / 1000))
if not ts.tzinfo:
raise ValueError("Timestamp in reading %i is missing a timezone part.")
parsed_readings.append((ts, reading[1]))

return parsed_readings

def append_readings(self, request, slug, readings):
pass

def delete(self, request, slug):
context = self.common(request, slug)
if not request.user.has_perm('delete_timeseries', context['series']):
return self._error.lacking_privilege(request, 'delete this time-series')
context['series'].delete()
return HttpResponse('', status=httplib.NO_CONTENT)


0 comments on commit c12954d

Please sign in to comment.