Last active
December 14, 2015 16:38
-
-
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
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
from project import api | |
from .resources import UserResource | |
api.resources.register(UserResource) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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() |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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") |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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