Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse files

added http exceptions incl. middleware

added better resource URL handling
removed incomplete pagination code for now
misc refactoring and cleanup
  • Loading branch information...
commit 5ce2b618412af438b4ad4428ddf452c65c597c42 1 parent 6964447
@aehlke authored
View
40 catnap/django_urls.py
@@ -0,0 +1,40 @@
+# Taken from http://code.djangoproject.com/wiki/ReplacingGetAbsoluteUrl
+# See: http://github.com/jezdez/django-urls/
+
+from django.conf import settings
+import urlparse
+
+class UrlMixin(object):
+
+ def get_url(self):
+ if hasattr(self.get_url_path, 'dont_recurse'):
+ raise NotImplementedError
+ try:
+ path = self.get_url_path()
+ except NotImplementedError:
+ raise
+ # Should we look up a related site?
+ #if getattr(self._meta, 'url_by_site'):
+ prefix = getattr(settings, 'DEFAULT_URL_PREFIX', None)
+ if prefix is None:
+ prefix = u'http://localhost'
+ if 'django.contrib.sites' in settings.INSTALLED_APPS:
+ from django.contrib.sites.models import Site
+ try:
+ prefix = 'http://%s' % Site.objects.get_current().domain
+ except Site.DoesNotExist:
+ pass
+ return prefix + path
+ get_url.dont_recurse = True
+
+ def get_url_path(self):
+ if hasattr(self.get_url, 'dont_recurse'):
+ raise NotImplementedError
+ try:
+ url = self.get_url()
+ except NotImplementedError:
+ raise
+ bits = urlparse.urlparse(url)
+ return urlparse.urlunparse(('', '') + bits[2:])
+ get_url_path.dont_recurse = True
+
View
25 catnap/exceptions.py
@@ -0,0 +1,25 @@
+from django.http import (HttpResponseBadRequest, HttpResponseNotFound,
+ HttpResponseForbidden, HttpResponseGone)
+
+
+
+class HttpException(Exception):
+ def __init__(self, http_response):
+ self.response = http_response
+
+
+# HttpException should really only be used for the cases listed below, in general.
+# So instead of using it directly, use these instead.
+
+class HttpBadRequestException(HttpException):
+ def __init__(self, *args, **kwargs):
+ self.response = HttpResponseBadRequest(*args, **kwargs)
+
+class HttpForbiddenException(HttpException):
+ def __init__(self, *args, **kwargs):
+ self.response = HttpResponseForbidden(*args, **kwargs)
+
+class HttpGoneException(HttpException):
+ def __init__(self, *args, **kwargs):
+ self.response = HttpResponseGone(*args, **kwargs)
+
View
19 catnap/http.py
@@ -0,0 +1,19 @@
+'''
+This module adds missing django.http.HttpResponse subclasses for more
+status codes.
+'''
+from django.http import HttpResponse
+
+
+class HttpResponseNotAcceptable(HttpResponse):
+ status_code = 406
+
+class HttpResponseConflict(HttpResponse):
+ status_code = 409
+
+class HttpResponseRequestEntityTooLarge(HttpResponse):
+ status_code = 413
+
+class HttpResponseUnsupportedMediaType(HttpResponse):
+ status_code = 415
+
View
16 catnap/middleware.py
@@ -48,3 +48,19 @@ def process_request(self, request):
return HttpResponseNotAllowed(ALL_HTTP_METHODS)
return None
+
+class HttpExceptionMiddleware(object):
+ '''
+ Catches `HttpException` exceptions, which contain a `response`
+ property, which should be a subclass instace of HttpResponse.
+
+ This middleware simply returns the `response` member.
+
+ See `catnap.exceptions`.
+ '''
+ def process_exception(self, request, exception):
+ if (hasattr(exception, response)
+ and isinstance(exception, HttpResponse)):
+ return exception.response
+ return None
+
View
11 catnap/restresources.py
@@ -1,14 +1,19 @@
+from django.contrib.sites.models import Site
+from django.conf import settings
+import urlparse
+from django_urls import UrlMixin
-
-class RestResource(object):
+class RestResource(UrlMixin):
def get_data(self):
'''
Adds a `url` field if this class has a `get_url` method.
'''
data = {}
- if hasattr(self, 'get_url'):
+ try:
data['url'] = self.get_url()
+ except NotImplementedError:
+ pass
return data
View
230 catnap/restviews.py
@@ -1,16 +1,15 @@
from django.http import HttpResponse
from django.http import HttpResponseNotAllowed, HttpResponseBadRequest
+from catnap.http import HttpResponseNotAcceptable
from django.core.exceptions import ImproperlyConfigured
from django.utils import simplejson as json
-#from djclsview import View
from django.views.generic.base import View
from django.views.generic.detail import SingleObjectMixin, BaseDetailView
from django.views.generic.list import MultipleObjectMixin, BaseListView
from serializers import json_serialize
+from django_urls import UrlMixin
-class HttpResponseNotAcceptable(HttpResponse):
- status_code = 406
class RestView(View):
@@ -42,48 +41,6 @@ def get_response(self, content, **httpresponse_kwargs):
**httpresponse_kwargs)
-#class ResourceView(View):
-# '''
-# Currently we only support a single content type per resource.
-# '''
-# content_type = None
-
-# def __call__(self):
-# resp = self._route(self.request.method)
-# resp = self._process_response(resp)
-# return self.process_response(resp)
-
-# def _process_response(self, response):
-# if self.content_type:
-# response['Content-Type'] = self.content_type
-
-# # Make sure the `Accept` header matches our content type.
-# if self.content_type not in self.request.accept:
-# return HttpResponseNotAcceptable()
-
-# return response
-
-# def process_response(self, response):
-# '''
-# Called on each view method return value.
-# Override this in a sublcass to add filtering to the HTTP
-# response object.
-# '''
-# return response
-
-
-#class JsonMixin(object):
-# '''
-# View methods should return data structures which are
-# serializable into JSON. This serializes them and puts them
-# into an HttpResponse instance.
-
-# Sets `content_type` to "application/json" which should absolutely
-# be overridden with a more descriptive content type.
-# '''
-# # Override this for vendor-specific content types.
-# # e.g "application/vnd.mycompany.FooBar+json"
-# content_type = 'application/json'
class SerializableMultipleObjectMixin(MultipleObjectMixin):
@@ -93,11 +50,7 @@ class SerializableMultipleObjectMixin(MultipleObjectMixin):
For example, instead of having both `\{modelname\}_list` and
`object_list` items, it only has `\{modelname\}_list`.
-
- It also accepts a `fields` property which limits the queryset
- in the context to only including certain fields.
'''
- fields = None
def get_context_object_name(self, object_list):
'''
@@ -114,102 +67,117 @@ def get_context_data(self, **kwargs):
#queryset = kwargs.pop('object_list')
#import pdb;pdb.set_trace()
queryset = self.get_queryset()
- page_size = self.get_paginate_by(queryset)
- if page_size:
- paginator, page, queryset, is_paginated = self.paginate_queryset(
- queryset, page_size)
- context = {
- 'paginator': paginator,
- 'page_obj': page,
- 'is_paginated': is_paginated,
- }
- else:
- context = {
- 'paginator': None,
- 'page_obj': None,
- 'is_paginated': False,
- }
- context.update(kwargs)
-
- ## Limit our queryset?
- #if self.fields:
- # queryset = queryset.values(self.fields)
-
+ #TODO add pagination
+ #page_size = self.get_paginate_by(queryset)
+ #if page_size:
+ # paginator, page, queryset, is_paginated = self.paginate_queryset(
+ # queryset, page_size)
+ # context = {
+ # #'paginator': paginator,
+ # 'page_obj': page,
+ # 'is_paginated': is_paginated,
+ # }
+ #else:
+ # context = {
+ # 'is_paginated': False,
+ # }
+
+ context = {}
+
+ # Add the list of objects
context_object_name = self.get_context_object_name(queryset)
context[context_object_name] = queryset
- return context
-
+ context.update(kwargs)
-class RestMultipleObjectMixin(SerializableMultipleObjectMixin):
- resource = None
+ return context
- def get_context_data(self, object_list=None, **kwargs):
- context = super(RestMultipleObjectMixin, self).get_context_data(**kwargs)
+class ResourceClassDependencyMixin(object):
+ resource_class = None
- if not self.resource:
+ def get_resource_class(self):
+ if not self.resource_class:
raise ImproperlyConfigured(
- u"'%s' must define 'resource', a class which takes "
+ u"'%s' must define 'resource_class', a class which takes "
"a model instance and implements a 'get_data' method."
% self.__class__.__name__)
+ return self.resource_class
+
+class RestMultipleObjectMixin(SerializableMultipleObjectMixin,
+ ResourceClassDependencyMixin,
+ UrlMixin):
+ '''
+ Extends the `SerializableMultipleObjectMixin` class to instantiate
+ `Resource` objects for every object in the list.
+
+ Since this is a convenience class for representing a list of resources,
+ this list itself doesn't have its own corresponding `Resource` subclass,
+ but we still support a `get_url` method to add a `url` key to the
+ context, as `Resource` does. (Or `get_url_path`, which will be expanded
+ to become an absolute URL, as per `UrlMixin`'s behavior.)
+ '''
+
+ def get_context_data(self, object_list=None, **kwargs):
+ context = super(RestMultipleObjectMixin, self).get_context_data(**kwargs)
context_object_name = self.get_context_object_name(self.get_queryset())
- #import pdb;pdb.set_trace()
+
+ try:
+ context['url'] = self.get_url()
+ except NotImplementedError:
+ pass
+
+ resource_class = self.get_resource_class()
context[context_object_name] = list(
- self.resource(_).get_data()
+ resource_class(_).get_data()
for _ in context[context_object_name])
return context
-class SerializableSingleObjectMixin(SingleObjectMixin):
+class RestSingleObjectMixin(SingleObjectMixin,
+ ResourceClassDependencyMixin):
'''
- This is a version of SingleObjectMixin which is more careful
+ This is a version of `SingleObjectMixin` which is more careful
to avoid duplicate or otherwise unnecessary context items.
- For example, instead of having both `\{modelname\}_list` and
- `object_list` items, it only has `\{modelname\}_list`.
-
- It also accepts a `fields` property which limits the queryset
- in the context to only including certain fields.
+ Instantiates a `Resource` object for the given detail object, and uses
+ its `get_data` return value for the context. The entire context is
+ the object itself, instead of the default Django behavior of having
+ the object a level deep, e.g. `{"object": etc}`.
'''
- fields = None
- def get_context_object_name(self, obj):
- '''
- Get the name to use for the object.
+ def get_context_data(self, **kwargs):
'''
- return super(SerializableSingleObjectMixin, self).get_context_object_name(
- obj) or 'object'
+ Relies on `get_object()` to retrieve the desired object used to
+ instantiate a `Resource`. Don't pass an `object` kwarg to this
+ method -- if you want to specify an object rather than have this
+ class find one automatically from our queryset or model class,
+ then override the `get_object` method to return what you want.
- def get_context_data(self, **kwargs):
- context = kwargs
- #context_object_name = self.get_context_object_name(self.object)
- #if context_object_name:
- #context[context_object_name] = self.object
+ Any `kwargs` passed to this will be added to the context.
+ '''
+ resource_class = self.get_resource_class()
+ resource = resource_class(self.get_object())
+ context = resource.get_data()
+ context.update(kwargs)
return context
-class BaseSerializableDetailView(SingleObjectMixin, View):
- def get(self, request, **kwargs):
- self.object = self.get_object()
- context = self.get_context_data(object=self.object)
- return self.render_to_response(context)
-class BaseSerializableListView(BaseListView, RestMultipleObjectMixin):
- pass
-
+class DetailView(RestSingleObjectMixin, View):
+ def get(self, request, **kwargs):
+ context = self.get_context_data()
+ return self.render_to_response(context)
-class DetailView(BaseSerializableDetailView):
- pass
-class ListView(BaseSerializableListView):
+class ListView(RestMultipleObjectMixin, BaseListView):
pass
class JsonResponseMixin(object):
# Override this for vendor-specific content types.
# e.g "application/vnd.mycompany.FooBar+json"
- #content_type = 'application/json'
+ content_type = 'application/json'
def render_to_response(self, context):
'Returns a JSON response containing `context` as payload'
@@ -288,6 +256,48 @@ def content_type(self):
+#class ResourceView(View):
+# '''
+# Currently we only support a single content type per resource.
+# '''
+# content_type = None
+
+# def __call__(self):
+# resp = self._route(self.request.method)
+# resp = self._process_response(resp)
+# return self.process_response(resp)
+
+# def _process_response(self, response):
+# if self.content_type:
+# response['Content-Type'] = self.content_type
+
+# # Make sure the `Accept` header matches our content type.
+# if self.content_type not in self.request.accept:
+# return HttpResponseNotAcceptable()
+
+# return response
+
+# def process_response(self, response):
+# '''
+# Called on each view method return value.
+# Override this in a sublcass to add filtering to the HTTP
+# response object.
+# '''
+# return response
+
+
+#class JsonMixin(object):
+# '''
+# View methods should return data structures which are
+# serializable into JSON. This serializes them and puts them
+# into an HttpResponse instance.
+
+# Sets `content_type` to "application/json" which should absolutely
+# be overridden with a more descriptive content type.
+# '''
+# # Override this for vendor-specific content types.
+# # e.g "application/vnd.mycompany.FooBar+json"
+# content_type = 'application/json'
View
3  catnap/serializers.py
@@ -184,5 +184,6 @@ def json_serialize(data):
ret = base_serialize(data)
return json.dumps(ret,
- cls=DateTimeAwareJSONEncoder, ensure_ascii=False, indent=4)
+ cls=DateTimeAwareJSONEncoder,
+ ensure_ascii=False, sort_keys=True, indent=4)
View
6 catnap/tests/api/restresources.py
@@ -2,11 +2,13 @@
from django.core.urlresolvers import reverse
-
class UserResource(RestModelResource):
fields = ('username', 'first_name', 'last_name', 'is_staff',
'is_superuser', 'is_active', 'date_joined',)
+ #def get_url(self):
+ #return reverse('api-user', args=[self.obj.id])
+
def get_data(self):
return super(UserResource, self).get_data()
@@ -15,7 +17,7 @@ def get_data(self):
class DeckResource(RestModelResource):
fields = ('name', 'description', 'owner', 'created_at', 'modified_at',)
- def get_url(self):
+ def get_url_path(self):
return reverse('api-deck', args=[self.obj.id])
def get_data(self):
View
7 catnap/tests/api/tests.py
@@ -1,10 +1,3 @@
-"""
-This file demonstrates writing tests using the unittest module. These will pass
-when you run "manage.py test".
-
-Replace this with more appropriate tests for your application.
-"""
-
from django.test import TestCase
from django.core.urlresolvers import reverse
from django.utils import simplejson as json
View
1  catnap/tests/urls.py
@@ -10,6 +10,7 @@
url('^$', EntryPoint.as_view(), name='api-entry_point'),
url('^decks/$', DeckList.as_view(), name='api-deck_list'),
url('^decks/(?P<pk>\d+)/$', Deck.as_view(), name='api-deck'),
+ #url('^users/$', User
)
Please sign in to comment.
Something went wrong with that request. Please try again.