Skip to content

Instantly share code, notes, and snippets.

@mariocesar
Last active December 14, 2015 16:38
Show Gist options
  • Save mariocesar/5116314 to your computer and use it in GitHub Desktop.
Save mariocesar/5116314 to your computer and use it in GitHub Desktop.
A generic view to create a web resource, suitable to use with Backbone #django #backbone #generic-views
from project import api
from .resources import UserResource
api.resources.register(UserResource)
from functools import update_wrapper
from django.conf import settings
from django.conf.urls import url, patterns
from django.core.urlresolvers import reverse
from django.views.generic.base import View
from django.db.models.query import QuerySet
from django.utils import simplejson
from django.utils.cache import patch_cache_control
from django.utils.html import conditional_escape
from django.core.serializers.json import DjangoJSONEncoder
from django.core.exceptions import ObjectDoesNotExist, MultipleObjectsReturned
from django.core.paginator import Paginator, EmptyPage, PageNotAnInteger
from django.http import (HttpResponse, HttpResponseBadRequest, HttpResponseNotFound,
HttpResponseServerError)
class JsonForm(object):
def as_json(self):
output = {
'errors': {
'_all': self.non_field_errors()
}
}
for name, field in self.fields.items():
bf = self[name]
bf_errors = self.error_class([conditional_escape(error) for error in bf.errors])
output['errors'][name] = bf_errors
return simplejson.dumps(output)
def __unicode__(self):
return self.as_json()
class HttpResponseCreated(HttpResponse):
status_code = 201
class HttpResponseAccepted(HttpResponse):
status_code = 202
class HttpResponseDeleted(HttpResponse):
status_code = 204
@property
def content(self):
# 204 says "Don't send a body!"
return ''
class HttpResponseNotAcceptable(HttpResponse):
status_code = 406
class HttpResponseDuplicateEntry(HttpResponse):
status_code = 409
class HttpResponseGone(HttpResponse):
status_code = 410
class HttpResponseNotImplemented(HttpResponse):
status_code = 501
class HttpResponseThrottled(HttpResponse):
status_code = 503
class ResourceBase(type):
pass
class Resource(View):
__metaclass__ = ResourceBase
http_method_names = ['get', 'post', 'put', 'patch', 'delete',
'head', 'options', 'trace']
resource_identifier = 'resource_id'
resource_methods = ('get', 'put', 'patch', 'delete')
collection_methods = ('get', 'post')
name = None
def list(self, request):
# GET /collection/
raise NotImplementedError()
def create(self, request, data):
# POST /collection/
raise NotImplementedError()
def read(self, request, resource_id):
# GET /collection/id/
raise NotImplementedError()
def update(self, request, resource_id, data):
# PUT /collection/id/ {**data}
# PATCH /collection/id/ {**data}
raise NotImplementedError()
def delete(self, request, resource_id):
# DELETE /collection/id
raise NotImplementedError()
def dispatch_wrap(self, view):
def wrapper(request, *args, **kwargs):
self.request = request
self.args = args
self.kwargs = kwargs
if not settings.DEBUG:
if not request.is_ajax():
return HttpResponseBadRequest('Bad Request')
if request.method.lower() not in self.http_method_names:
return self.http_method_not_allowed(request)
try:
response = view(request, *args, **kwargs)
except NotImplementedError:
return HttpResponseNotImplemented('Not Implemented')
except ObjectDoesNotExist:
return HttpResponseNotFound('Not found')
except MultipleObjectsReturned:
return HttpResponseDuplicateEntry('Conflict/Duplicate')
except Exception as detail:
if settings.DEBUG:
raise
return HttpResponseServerError(str(detail))
patch_cache_control(response, no_cache=True) # Disable Browser Cache
return response
return update_wrapper(wrapper, view)
def dispatch_collection(self, request, *args, **kwargs):
"""
Dispatch a collection
"""
if request.method.lower() in self.collection_methods:
handler = getattr(self, request.method.lower(),
self.http_method_not_allowed)
else:
handler = self.http_method_not_allowed
return handler(request, *args, **kwargs)
def dispatch_resource(self, request, *args, **kwargs):
"""
Dispatch a resource
"""
resource_identifier = kwargs.pop(self.resource_identifier)
if request.method.lower() in self.resource_methods:
handler = getattr(self, request.method.lower(),
self.http_method_not_allowed)
else:
handler = self.http_method_not_allowed
return handler(request, resource_identifier, *args, **kwargs)
def serialize(self, data):
if isinstance(data, QuerySet):
data = list(data)
if 'results' in data:
data['results'] = list(data['results'])
indent = 2 if settings.DEBUG else None
return simplejson.dumps(data, ensure_ascii=False,
cls=DjangoJSONEncoder, indent=indent)
def deserialize(self, content):
if isinstance(content, dict):
return content
return simplejson.loads(content)
@property
def urls(self):
urlpatterns = patterns('',
url(r"^$",
self.dispatch_wrap(
self.dispatch_collection),
name='%s_collection' % self.get_name()),
url(r"^(?P<%s>.+)/$" % self.resource_identifier,
self.dispatch_wrap(self.dispatch_resource),
name='%s_resource' % self.get_name()),
)
return urlpatterns
def get(self, request, *args, **kwargs):
resource_id = self.kwargs.get(self.resource_identifier, None)
if resource_id:
return self.read(request, resource_id)
else:
return self.list(request)
def post(self, request, *args, **kwargs):
data = self.deserialize(self.request.raw_post_data)
return self.create(request, data)
def put(self, request, *args, **kwargs):
resource_id = self.kwargs.get(self.resource_identifier, None)
data = self.deserialize(self.request.raw_post_data)
return self.update(request, resource_id, data)
def patch(self, request, *args, **kwargs):
return self.put(request, *args, **kwargs)
def options(self, request, *args, **kwargs):
allows = ','.join(map(str.upper, self.http_method_names))
response = HttpResponse(allows)
response['Allow'] = allows
return response
def get_name(self):
if not self.name:
# UserResource -> user
self.name = ''.join(self.__class__.__name__.lower().rsplit('resource', 1)[:-1])
return self.name
def paginate(request, queryset, *args, **kwargs):
paginator = Paginator(queryset, *args, **kwargs)
p = request.GET.get('page', 1)
try:
page = paginator.page(p)
except PageNotAnInteger:
# If page is not an integer, deliver first page.
page = paginator.page(1)
except EmptyPage:
# If page is out of range (e.g. 9999), deliver last page of results.
raise ObjectDoesNotExist()
return {'page': p, 'results': page.object_list}
class AlreadyRegistered(Exception):
pass
class Resources(object):
def __init__(self):
self._registry = set()
def register(self, resource_or_iterable):
if isinstance(resource_or_iterable, ResourceBase):
resource_or_iterable = [resource_or_iterable]
for resource in resource_or_iterable:
if resource in self._registry:
raise AlreadyRegistered('The model %s is already registered' % resource.__name__)
self._registry.update([resource()])
def index(self, request):
meta = dict()
for resource in self._registry:
meta[resource.get_name()] = {
'collection': reverse('%s_collection' % resource.get_name()),
'resource': '%s:resource_id/' % reverse('%s_collection' % resource.get_name())
}
return HttpResponse(resource.serialize(meta), mimetype='application/json')
def get_urls(self):
from django.conf.urls import patterns, url, include
# Admin-site-wide views.
urlpatterns = patterns('',
url(r'^$', self.index),
)
for resource in self._registry:
urlpatterns += patterns('',
url(r'^%s/' % resource.get_name(), include(resource.urls))
)
return urlpatterns
@property
def urls(self):
return self.get_urls()
resources = Resources()
from .resources import Resource
from django import forms
from django.http import HttpResponse
from django.contrib.auth.models import User
from wolf.core.api import HttpResponseCreated, HttpResponseNotAcceptable
from wolf.core.api import Resource, JSONForm, paginate
class UserForm(forms.ModelForm, JSONForm):
class Meta:
model = User
class UserResource(Resource):
def list(self, request):
users = paginate(request, User.objects.all().values('id', 'username'))
return HttpResponse(self.serialize(users),
mimetype="application/json; charset=utf-8")
def read(self, request, resource_id):
user = User.objects.get(id=resource_id)
return HttpResponse(self.serialize(user),
mimetype="application/json; charset=utf-8")
def create(self, request, data):
form = UserForm(data)
if form.is_valid():
user = form.save()
return HttpResponseCreated(self.serialize(user),
mimetype="application/json; charset=utf-8")
return HttpResponseNotAcceptable(self.serialize(form),
mimetype="application/json; charset=utf-8")
from django.conf.urls import patterns, url
from django.contrib import admin
admin.autodiscover()
urlpatterns = patterns('',
url(r'^admin/doc/', include('django.contrib.admindocs.urls')),
url(r'^admin/', include(admin.site.urls)),
url(r'^api/1/', include(api.resources.urls)),
)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment