Added formats, various form improvements, more refactoring/cleanup

This commit is contained in:
Tom Christie 2011-01-14 18:06:40 +00:00
parent 764fbe335f
commit b0ce3f92c6
9 changed files with 495 additions and 393 deletions

View File

@ -3,6 +3,8 @@ import json
from utils import dict2xml
class BaseEmitter(object):
uses_forms = False
def __init__(self, resource):
self.resource = resource
@ -24,11 +26,14 @@ class TemplatedEmitter(BaseEmitter):
'resource': self.resource,
})
ret = template.render(context)
# Munge DELETE Response code to allow us to return content
# (Do this *after* we've rendered the template so that we include the normal deletion response code in the output)
if self.resource.resp_status == 204:
self.resource.resp_status = 200
return template.render(context)
return ret
class JSONEmitter(BaseEmitter):
def emit(self, output):
@ -46,6 +51,7 @@ class XMLEmitter(BaseEmitter):
class HTMLEmitter(TemplatedEmitter):
template = 'emitter.html'
uses_forms = True
class TextEmitter(TemplatedEmitter):
template = 'emitter.txt'

346
src/rest/modelresource.py Normal file
View File

@ -0,0 +1,346 @@
"""TODO: docs
"""
from django.forms import ModelForm
from django.db.models.query import QuerySet
from django.db.models import Model
from rest.resource import Resource
import decimal
import inspect
import re
class ModelResource(Resource):
model = None
fields = None
form_fields = None
def get_bound_form(self, data=None, is_response=False):
"""Return a form that may be used in validation and/or rendering an html emitter"""
if self.form:
return super(self.__class__, self).get_bound_form(data, is_response=is_response)
elif self.model:
class NewModelForm(ModelForm):
class Meta:
model = self.model
fields = self.form_fields if self.form_fields else None #self.fields
if data and not is_response:
return NewModelForm(data)
elif data and is_response:
return NewModelForm(instance=data)
else:
return NewModelForm()
else:
return None
def cleanup_response(self, data):
"""A munging of Piston's pre-serialization. Returns a dict"""
def _any(thing, fields=()):
"""
Dispatch, all types are routed through here.
"""
ret = None
if isinstance(thing, QuerySet):
ret = _qs(thing, fields=fields)
elif isinstance(thing, (tuple, list)):
ret = _list(thing)
elif isinstance(thing, dict):
ret = _dict(thing)
elif isinstance(thing, int):
ret = thing
elif isinstance(thing, bool):
ret = thing
elif isinstance(thing, type(None)):
ret = thing
elif isinstance(thing, decimal.Decimal):
ret = str(thing)
elif isinstance(thing, Model):
ret = _model(thing, fields=fields)
#elif isinstance(thing, HttpResponse): TRC
# raise HttpStatusCode(thing)
elif inspect.isfunction(thing):
if not inspect.getargspec(thing)[0]:
ret = _any(thing())
elif hasattr(thing, '__emittable__'):
f = thing.__emittable__
if inspect.ismethod(f) and len(inspect.getargspec(f)[0]) == 1:
ret = _any(f())
else:
ret = str(thing) # TRC TODO: Change this back!
return ret
def _fk(data, field):
"""
Foreign keys.
"""
return _any(getattr(data, field.name))
def _related(data, fields=()):
"""
Foreign keys.
"""
return [ _model(m, fields) for m in data.iterator() ]
def _m2m(data, field, fields=()):
"""
Many to many (re-route to `_model`.)
"""
return [ _model(m, fields) for m in getattr(data, field.name).iterator() ]
def _method_fields(data, fields):
if not data:
return { }
has = dir(data)
ret = dict()
for field in fields:
if field in has:
ret[field] = getattr(data, field)
return ret
def _model(data, fields=()):
"""
Models. Will respect the `fields` and/or
`exclude` on the handler (see `typemapper`.)
"""
ret = { }
#handler = self.in_typemapper(type(data), self.anonymous) # TRC
handler = None # TRC
get_absolute_url = False
if handler or fields:
v = lambda f: getattr(data, f.attname)
if not fields:
"""
Fields was not specified, try to find teh correct
version in the typemapper we were sent.
"""
mapped = self.in_typemapper(type(data), self.anonymous)
get_fields = set(mapped.fields)
exclude_fields = set(mapped.exclude).difference(get_fields)
if not get_fields:
get_fields = set([ f.attname.replace("_id", "", 1)
for f in data._meta.fields ])
# sets can be negated.
for exclude in exclude_fields:
if isinstance(exclude, basestring):
get_fields.discard(exclude)
elif isinstance(exclude, re._pattern_type):
for field in get_fields.copy():
if exclude.match(field):
get_fields.discard(field)
get_absolute_url = True
else:
get_fields = set(fields)
if 'absolute_url' in get_fields: # MOVED (TRC)
get_absolute_url = True
met_fields = _method_fields(handler, get_fields) # TRC
for f in data._meta.local_fields:
if f.serialize and not any([ p in met_fields for p in [ f.attname, f.name ]]):
if not f.rel:
if f.attname in get_fields:
ret[f.attname] = _any(v(f))
get_fields.remove(f.attname)
else:
if f.attname[:-3] in get_fields:
ret[f.name] = _fk(data, f)
get_fields.remove(f.name)
for mf in data._meta.many_to_many:
if mf.serialize and mf.attname not in met_fields:
if mf.attname in get_fields:
ret[mf.name] = _m2m(data, mf)
get_fields.remove(mf.name)
# try to get the remainder of fields
for maybe_field in get_fields:
if isinstance(maybe_field, (list, tuple)):
model, fields = maybe_field
inst = getattr(data, model, None)
if inst:
if hasattr(inst, 'all'):
ret[model] = _related(inst, fields)
elif callable(inst):
if len(inspect.getargspec(inst)[0]) == 1:
ret[model] = _any(inst(), fields)
else:
ret[model] = _model(inst, fields)
elif maybe_field in met_fields:
# Overriding normal field which has a "resource method"
# so you can alter the contents of certain fields without
# using different names.
ret[maybe_field] = _any(met_fields[maybe_field](data))
else:
maybe = getattr(data, maybe_field, None)
if maybe:
if callable(maybe):
if len(inspect.getargspec(maybe)[0]) == 1:
ret[maybe_field] = _any(maybe())
else:
ret[maybe_field] = _any(maybe)
else:
pass # TRC
#handler_f = getattr(handler or self.handler, maybe_field, None)
#
#if handler_f:
# ret[maybe_field] = _any(handler_f(data))
else:
# Add absolute_url if it exists
get_absolute_url = True
# Add all the fields
for f in data._meta.fields:
if f.attname != 'id':
ret[f.attname] = _any(getattr(data, f.attname))
# Add all the propertiess
klass = data.__class__
for attr in dir(klass):
if not attr.startswith('_') and not attr in ('pk','id') and isinstance(getattr(klass, attr, None), property):
#if attr.endswith('_url') or attr.endswith('_uri'):
# ret[attr] = self.make_absolute(_any(getattr(data, attr)))
#else:
ret[attr] = _any(getattr(data, attr))
#fields = dir(data.__class__) + ret.keys()
#add_ons = [k for k in dir(data) if k not in fields and not k.startswith('_')]
#print add_ons
###print dir(data.__class__)
#from django.db.models import Model
#model_fields = dir(Model)
#for attr in dir(data):
## #if attr.startswith('_'):
## # continue
# if (attr in fields) and not (attr in model_fields) and not attr.startswith('_'):
# print attr, type(getattr(data, attr, None)), attr in fields, attr in model_fields
#for k in add_ons:
# ret[k] = _any(getattr(data, k))
# TRC
# resouce uri
#if self.in_typemapper(type(data), self.anonymous):
# handler = self.in_typemapper(type(data), self.anonymous)
# if hasattr(handler, 'resource_uri'):
# url_id, fields = handler.resource_uri()
# ret['resource_uri'] = permalink( lambda: (url_id,
# (getattr(data, f) for f in fields) ) )()
# TRC
#if hasattr(data, 'get_api_url') and 'resource_uri' not in ret:
# try: ret['resource_uri'] = data.get_api_url()
# except: pass
# absolute uri
if hasattr(data, 'get_absolute_url') and get_absolute_url:
try: ret['absolute_url'] = data.get_absolute_url()
except: pass
for key, val in ret.items():
if key.endswith('_url') or key.endswith('_uri'):
ret[key] = self.add_domain(val)
return ret
def _qs(data, fields=()):
"""
Querysets.
"""
return [ _any(v, fields) for v in data ]
def _list(data):
"""
Lists.
"""
return [ _any(v) for v in data ]
def _dict(data):
"""
Dictionaries.
"""
return dict([ (k, _any(v)) for k, v in data.iteritems() ])
# Kickstart the seralizin'.
return _any(data, self.fields)
def create(self, data, headers={}, *args, **kwargs):
# TODO: test creation on a non-existing resource url
all_kw_args = dict(data.items() + kwargs.items())
instance = self.model(**all_kw_args)
instance.save()
headers = {}
if hasattr(instance, 'get_absolute_url'):
headers['Location'] = self.add_domain(instance.get_absolute_url())
return (201, instance, headers)
def read(self, headers={}, *args, **kwargs):
try:
instance = self.model.objects.get(**kwargs)
except self.model.DoesNotExist:
return (404, None, {})
return (200, instance, {})
def update(self, data, headers={}, *args, **kwargs):
# TODO: update on the url of a non-existing resource url doesn't work correctly at the moment - will end up with a new url
try:
instance = self.model.objects.get(**kwargs)
for (key, val) in data.items():
setattr(instance, key, val)
except self.model.DoesNotExist:
instance = self.model(**data)
instance.save()
instance.save()
return (200, instance, {})
def delete(self, headers={}, *args, **kwargs):
try:
instance = self.model.objects.get(**kwargs)
except self.model.DoesNotExist:
return (404, None, {})
instance.delete()
return (204, None, {})
class QueryModelResource(ModelResource):
allowed_methods = ('read',)
queryset = None
def get_bound_form(self, data=None, is_response=False):
return None
def read(self, headers={}, *args, **kwargs):
if self.queryset:
return (200, self.queryset, {})
queryset = self.model.objects.all()
return (200, queryset, {})

View File

@ -1,10 +1,25 @@
from django.http import HttpResponse
from django.contrib.sites.models import Site
from django.core.urlresolvers import reverse
from django.core.handlers.wsgi import STATUS_CODE_TEXT
from rest import emitters, parsers
from decimal import Decimal
import re
# TODO: Display user login in top panel: http://stackoverflow.com/questions/806835/django-redirect-to-previous-page-after-login
# TODO: Return basic object, not tuple
# TODO: Take request, not headers
# TODO: Remove self.blah munging (Add a ResponseContext object)
# TODO: Erroring on non-existent fields
# TODO: Standard exception classes and module for status codes
# TODO: Figure how out references and named urls need to work nicely
# TODO: POST on existing 404 URL, PUT on existing 404 URL
# TODO: Authentication
#
# FUTURE: Erroring on read-only fields
# Documentation, Release
#
STATUS_400_BAD_REQUEST = 400
STATUS_405_METHOD_NOT_ALLOWED = 405
@ -45,15 +60,17 @@ class Resource(object):
# Map standard HTTP methods to RESTful operations
CALLMAP = { 'GET': 'read', 'POST': 'create',
'PUT': 'update', 'DELETE': 'delete' }
REVERSE_CALLMAP = dict([(val, key) for (key, val) in CALLMAP.items()])
# Some reserved parameters to allow us to use standard HTML forms with our resource.
METHOD_PARAM = '_method'
ACCEPT_PARAM = '_accept'
CSRF_PARAM = 'csrfmiddlewaretoken'
RESERVED_PARAMS = set((METHOD_PARAM, ACCEPT_PARAM, CSRF_PARAM))
# Some reserved parameters to allow us to use standard HTML forms with our resource
METHOD_PARAM = '_method' # Allow POST overloading
ACCEPT_PARAM = '_accept' # Allow override of Accept header in GET requests
CONTENTTYPE_PARAM = '_contenttype' # Allow override of Content-Type header (allows sending arbitrary content with standard forms)
CONTENT_PARAM = '_content' # Allow override of body content (allows sending arbitrary content with standard forms)
CSRF_PARAM = 'csrfmiddlewaretoken' # Django's CSRF token
USE_SITEMAP_FOR_ABSOLUTE_URLS = False
RESERVED_PARAMS = set((METHOD_PARAM, ACCEPT_PARAM, CONTENTTYPE_PARAM, CONTENT_PARAM, CSRF_PARAM))
def __new__(cls, request, *args, **kwargs):
@ -69,17 +86,20 @@ class Resource(object):
def name(self):
"""Provide a name for the resource.
By default this is the class name, with 'CamelCaseNames' converted to 'Camel Case Names',
although this behaviour may be overridden."""
By default this is the class name, with 'CamelCaseNames' converted to 'Camel Case Names'."""
class_name = self.__class__.__name__
return re.sub('(((?<=[a-z])[A-Z])|([A-Z](?![A-Z]|$)))', ' \\1', class_name).strip()
def description(self):
"""Provide a description for the resource.
By default this is the class's docstring,
although this behaviour may be overridden."""
return "%s" % self.__doc__
By default this is the class's docstring with leading line spaces stripped."""
return re.sub(re.compile('^ +', re.MULTILINE), '', self.__doc__)
def available_content_types(self):
"""Return a list of strings of all the content-types that this resource can emit."""
return [item[0] for item in self.emitters]
def resp_status_text(self):
@ -89,19 +109,22 @@ class Resource(object):
def reverse(self, view, *args, **kwargs):
"""Return a fully qualified URI for a given view or resource, using the current request as the base URI.
TODO: Add SITEMAP option.
Provided for convienience."""
return self.request.build_absolute_uri(reverse(view, *args, **kwargs))
"""Return a fully qualified URI for a given view or resource.
Use the Sites framework if possible, otherwise fallback to using the current request."""
return self.add_domain(reverse(view, *args, **kwargs))
def make_absolute(self, uri):
"""Given a relative URI, return an absolute URI using the current request as the base URI.
TODO: Add SITEMAP option.
def add_domain(self, path):
"""Given a path, return an fully qualified URI.
Use the Sites framework if possible, otherwise fallback to using the domain from the current request."""
try:
site = Site.objects.get_current()
if site.domain and site.domain != 'example.com':
return 'http://%s%s' % (site.domain, path)
except:
pass
Provided for convienience."""
return self.request.build_absolute_uri(uri)
return self.request.build_absolute_uri(path)
def read(self, headers={}, *args, **kwargs):
@ -134,17 +157,18 @@ class Resource(object):
def determine_method(self, request):
"""Determine the HTTP method that this request should be treated as.
Allow for PUT and DELETE tunneling via the _method parameter."""
method = request.method
method = request.method.upper()
if method == 'POST' and request.POST.has_key(self.METHOD_PARAM):
if method == 'POST' and self.METHOD_PARAM and request.POST.has_key(self.METHOD_PARAM):
method = request.POST[self.METHOD_PARAM].upper()
return method
def authenticate(self):
"""..."""
"""TODO"""
# user = ...
# if DEBUG and request is from localhost
# if anon_user and not anon_allowed_operations raise PermissionDenied
# return
@ -174,17 +198,15 @@ class Resource(object):
return None
def cleanup_request(self, data):
def cleanup_request(self, data, form_instance):
"""Perform any resource-specific data deserialization and/or validation
after the initial HTTP content-type deserialization has taken place.
Returns a tuple containing the cleaned up data, and optionally a form bound to that data.
By default this uses form validation to filter the basic input into the required types."""
if self.form is None:
return (data, None)
form_instance = self.get_bound_form(data)
if form_instance is None:
return data
if not form_instance.is_valid():
if not form_instance.errors:
@ -196,7 +218,7 @@ class Resource(object):
raise ResourceException(STATUS_400_BAD_REQUEST, {'detail': details})
return (form_instance.cleaned_data, form_instance)
return form_instance.cleaned_data
def cleanup_response(self, data):
@ -230,11 +252,17 @@ class Resource(object):
See: RFC 2616, Section 14 - http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html"""
default = self.emitters[0]
if not request.META.has_key('HTTP_ACCEPT'):
if self.ACCEPT_PARAM and request.GET.get(self.ACCEPT_PARAM, None):
# Use _accept parameter override
accept_list = [(request.GET.get(self.ACCEPT_PARAM),)]
elif request.META.has_key('HTTP_ACCEPT'):
# Use standard HTTP Accept negotiation
accept_list = [item.split(';') for item in request.META["HTTP_ACCEPT"].split(',')]
else:
# No accept header specified
return default
# Parse the accept header into a dict of {Priority: List of Mimetypes}
accept_list = [item.split(';') for item in request.META["HTTP_ACCEPT"].split(',')]
accept_dict = {}
for item in accept_list:
mimetype = item[0].strip()
@ -308,11 +336,13 @@ class Resource(object):
if method in ('PUT', 'POST'):
parser = self.determine_parser(request)
data = parser(self).parse(request.raw_post_data)
(data, self.form_instance) = self.cleanup_request(data)
self.form_instance = self.get_bound_form(data)
data = self.cleanup_request(data, self.form_instance)
(self.resp_status, ret, self.resp_headers) = func(data, request.META, *args, **kwargs)
else:
(self.resp_status, ret, self.resp_headers) = func(request.META, *args, **kwargs)
if emitter.uses_forms:
self.form_instance = self.get_bound_form(ret, is_response=True)
@ -320,7 +350,7 @@ class Resource(object):
(self.resp_status, ret, self.resp_headers) = (exc.status, exc.content, exc.headers)
if emitter is None:
mimetype, emitter = self.emitters[0]
if self.form_instance is None:
if self.form_instance is None and emitter.uses_forms:
self.form_instance = self.get_bound_form()
@ -338,341 +368,3 @@ class Resource(object):
return resp
from django.forms import ModelForm
from django.db.models.query import QuerySet
from django.db.models import Model
import decimal
import inspect
class ModelResource(Resource):
model = None
fields = None
form_fields = None
def get_bound_form(self, data=None, is_response=False):
"""Return a form that may be used in validation and/or rendering an html emitter"""
if self.form:
return super(self.__class__, self).get_bound_form(data, is_response=is_response)
elif self.model:
class NewModelForm(ModelForm):
class Meta:
model = self.model
fields = self.form_fields if self.form_fields else None #self.fields
if data and not is_response:
return NewModelForm(data)
elif data and is_response:
return NewModelForm(instance=data)
else:
return NewModelForm()
else:
return None
def cleanup_response(self, data):
"""
Recursively serialize a lot of types, and
in cases where it doesn't recognize the type,
it will fall back to Django's `smart_unicode`.
Returns `dict`.
"""
def _any(thing, fields=()):
"""
Dispatch, all types are routed through here.
"""
ret = None
if isinstance(thing, QuerySet):
ret = _qs(thing, fields=fields)
elif isinstance(thing, (tuple, list)):
ret = _list(thing)
elif isinstance(thing, dict):
ret = _dict(thing)
elif isinstance(thing, int):
ret = thing
elif isinstance(thing, bool):
ret = thing
elif isinstance(thing, type(None)):
ret = thing
elif isinstance(thing, decimal.Decimal):
ret = str(thing)
elif isinstance(thing, Model):
ret = _model(thing, fields=fields)
#elif isinstance(thing, HttpResponse): TRC
# raise HttpStatusCode(thing)
elif inspect.isfunction(thing):
if not inspect.getargspec(thing)[0]:
ret = _any(thing())
elif hasattr(thing, '__emittable__'):
f = thing.__emittable__
if inspect.ismethod(f) and len(inspect.getargspec(f)[0]) == 1:
ret = _any(f())
else:
ret = str(thing) # TRC TODO: Change this back!
return ret
def _fk(data, field):
"""
Foreign keys.
"""
return _any(getattr(data, field.name))
def _related(data, fields=()):
"""
Foreign keys.
"""
return [ _model(m, fields) for m in data.iterator() ]
def _m2m(data, field, fields=()):
"""
Many to many (re-route to `_model`.)
"""
return [ _model(m, fields) for m in getattr(data, field.name).iterator() ]
def _method_fields(data, fields):
if not data:
return { }
has = dir(data)
ret = dict()
for field in fields:
if field in has:
ret[field] = getattr(data, field)
return ret
def _model(data, fields=()):
"""
Models. Will respect the `fields` and/or
`exclude` on the handler (see `typemapper`.)
"""
ret = { }
#handler = self.in_typemapper(type(data), self.anonymous) # TRC
handler = None # TRC
get_absolute_url = False
if handler or fields:
v = lambda f: getattr(data, f.attname)
if not fields:
"""
Fields was not specified, try to find teh correct
version in the typemapper we were sent.
"""
mapped = self.in_typemapper(type(data), self.anonymous)
get_fields = set(mapped.fields)
exclude_fields = set(mapped.exclude).difference(get_fields)
if not get_fields:
get_fields = set([ f.attname.replace("_id", "", 1)
for f in data._meta.fields ])
# sets can be negated.
for exclude in exclude_fields:
if isinstance(exclude, basestring):
get_fields.discard(exclude)
elif isinstance(exclude, re._pattern_type):
for field in get_fields.copy():
if exclude.match(field):
get_fields.discard(field)
get_absolute_url = True
else:
get_fields = set(fields)
if 'absolute_url' in get_fields: # MOVED (TRC)
get_absolute_url = True
met_fields = _method_fields(handler, get_fields) # TRC
for f in data._meta.local_fields:
if f.serialize and not any([ p in met_fields for p in [ f.attname, f.name ]]):
if not f.rel:
if f.attname in get_fields:
ret[f.attname] = _any(v(f))
get_fields.remove(f.attname)
else:
if f.attname[:-3] in get_fields:
ret[f.name] = _fk(data, f)
get_fields.remove(f.name)
for mf in data._meta.many_to_many:
if mf.serialize and mf.attname not in met_fields:
if mf.attname in get_fields:
ret[mf.name] = _m2m(data, mf)
get_fields.remove(mf.name)
# try to get the remainder of fields
for maybe_field in get_fields:
if isinstance(maybe_field, (list, tuple)):
model, fields = maybe_field
inst = getattr(data, model, None)
if inst:
if hasattr(inst, 'all'):
ret[model] = _related(inst, fields)
elif callable(inst):
if len(inspect.getargspec(inst)[0]) == 1:
ret[model] = _any(inst(), fields)
else:
ret[model] = _model(inst, fields)
elif maybe_field in met_fields:
# Overriding normal field which has a "resource method"
# so you can alter the contents of certain fields without
# using different names.
ret[maybe_field] = _any(met_fields[maybe_field](data))
else:
maybe = getattr(data, maybe_field, None)
if maybe:
if callable(maybe):
if len(inspect.getargspec(maybe)[0]) == 1:
ret[maybe_field] = _any(maybe())
else:
ret[maybe_field] = _any(maybe)
else:
pass # TRC
#handler_f = getattr(handler or self.handler, maybe_field, None)
#
#if handler_f:
# ret[maybe_field] = _any(handler_f(data))
else:
# Add absolute_url if it exists
get_absolute_url = True
# Add all the fields
for f in data._meta.fields:
if f.attname != 'id':
ret[f.attname] = _any(getattr(data, f.attname))
# Add all the propertiess
klass = data.__class__
for attr in dir(klass):
if not attr.startswith('_') and not attr in ('pk','id') and isinstance(getattr(klass, attr, None), property):
#if attr.endswith('_url') or attr.endswith('_uri'):
# ret[attr] = self.make_absolute(_any(getattr(data, attr)))
#else:
ret[attr] = _any(getattr(data, attr))
#fields = dir(data.__class__) + ret.keys()
#add_ons = [k for k in dir(data) if k not in fields and not k.startswith('_')]
#print add_ons
###print dir(data.__class__)
#from django.db.models import Model
#model_fields = dir(Model)
#for attr in dir(data):
## #if attr.startswith('_'):
## # continue
# if (attr in fields) and not (attr in model_fields) and not attr.startswith('_'):
# print attr, type(getattr(data, attr, None)), attr in fields, attr in model_fields
#for k in add_ons:
# ret[k] = _any(getattr(data, k))
# TRC
# resouce uri
#if self.in_typemapper(type(data), self.anonymous):
# handler = self.in_typemapper(type(data), self.anonymous)
# if hasattr(handler, 'resource_uri'):
# url_id, fields = handler.resource_uri()
# ret['resource_uri'] = permalink( lambda: (url_id,
# (getattr(data, f) for f in fields) ) )()
# TRC
#if hasattr(data, 'get_api_url') and 'resource_uri' not in ret:
# try: ret['resource_uri'] = data.get_api_url()
# except: pass
# absolute uri
if hasattr(data, 'get_absolute_url') and get_absolute_url:
try: ret['absolute_url'] = self.make_absolute(data.get_absolute_url())
except: pass
for key, val in ret.items():
if key.endswith('_url') or key.endswith('_uri'):
ret[key] = self.make_absolute(val)
return ret
def _qs(data, fields=()):
"""
Querysets.
"""
return [ _any(v, fields) for v in data ]
def _list(data):
"""
Lists.
"""
return [ _any(v) for v in data ]
def _dict(data):
"""
Dictionaries.
"""
return dict([ (k, _any(v)) for k, v in data.iteritems() ])
# Kickstart the seralizin'.
return _any(data, self.fields)
def create(self, data, headers={}, *args, **kwargs):
all_kw_args = dict(data.items() + kwargs.items())
instance = self.model(**all_kw_args)
instance.save()
headers = {}
if hasattr(instance, 'get_absolute_url'):
headers['Location'] = self.make_absolute(instance.get_absolute_url())
return (201, instance, headers)
def read(self, headers={}, *args, **kwargs):
try:
instance = self.model.objects.get(**kwargs)
except self.model.DoesNotExist:
return (404, None, {})
return (200, instance, {})
def update(self, data, headers={}, *args, **kwargs):
try:
instance = self.model.objects.get(**kwargs)
for (key, val) in data.items():
setattr(instance, key, val)
except self.model.DoesNotExist:
instance = self.model(**data)
instance.save()
instance.save()
return (200, instance, {})
def delete(self, headers={}, *args, **kwargs):
instance = self.model.objects.get(**kwargs)
instance.delete()
return (204, None, {})
class QueryModelResource(ModelResource):
allowed_methods = ('read',)
def get_bound_form(self, data=None, is_response=False):
return None
def read(self, headers={}, *args, **kwargs):
query = self.model.objects.all()
return (200, query, {})

View File

@ -1,4 +1,4 @@
{% load urlize_quoted_links %}<?xml version="1.0" encoding="UTF-8"?>
{% load urlize_quoted_links %}{% load add_query_param %}<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
@ -6,12 +6,19 @@
<style>
pre {border: 1px solid black; padding: 1em; background: #ffd}
div.action {padding: 0.5em 1em; margin-bottom: 0.5em; background: #ddf}
ul.accepttypes {float: right; list-style-type: none; margin: 0; padding: 0}
ul.accepttypes li {display: inline;}
form div {margin: 0.5em 0}
form div * {vertical-align: top}
form ul.errorlist {display: inline; margin: 0; padding: 0}
form ul.errorlist li {display: inline; color: red;}
.clearing {display: block; margin: 0; padding: 0; clear: both;}
</style>
<title>API - {{ resource.name }}</title>
</head>
<body>
<h1>{{ resource.name }}</h1>
<p>{{ resource.description }}</p>
<p>{{ resource.description|linebreaksbr }}</p>
<pre><b>{{ resource.resp_status }} {{ resource.resp_status_text }}</b>{% autoescape off %}
{% for key, val in resource.resp_headers.items %}<b>{{ key }}:</b> {{ val|urlize_quoted_links }}
{% endfor %}
@ -20,14 +27,32 @@
{% if 'read' in resource.allowed_operations %}
<div class='action'>
<a href='{{ resource.request.path }}'>Read</a>
<ul class="accepttypes">
{% for content_type in resource.available_content_types %}
{% with resource.ACCEPT_PARAM|add:"="|add:content_type as param %}
<li>[<a href='{{ resource.request.path|add_query_param:param }}'>{{ content_type }}</a>]</li>
{% endwith %}
{% endfor %}
</ul>
<div class="clearing"></div>
</div>
{% endif %}
{% if 'create' in resource.allowed_operations %}
<div class='action'>
<form action="{{ resource.request.path }}" method="POST">
<form action="{{ resource.request.path }}" method="post">
{% csrf_token %}
{{ resource.form_instance.as_p }}
{% with resource.form_instance as form %}
{% for field in form %}
<div>
{{ field.label_tag }}:
{{ field }}
{{ field.help_text }}
{{ field.errors }}
</div>
{% endfor %}
{% endwith %}
<div class="clearing"></div>
<input type="submit" value="Create" />
</form>
</div>
@ -35,10 +60,20 @@
{% if 'update' in resource.allowed_operations %}
<div class='action'>
<form action="{{ resource.request.path }}" method="POST">
<form action="{{ resource.request.path }}" method="post">
<input type="hidden" name="{{ resource.METHOD_PARAM}}" value="PUT" />
{% csrf_token %}
{{ resource.form_instance.as_p }}
{% with resource.form_instance as form %}
{% for field in form %}
<div>
{{ field.label_tag }}:
{{ field }}
{{ field.help_text }}
{{ field.errors }}
</div>
{% endfor %}
{% endwith %}
<div class="clearing"></div>
<input type="submit" value="Update" />
</form>
</div>
@ -46,7 +81,7 @@
{% if 'delete' in resource.allowed_operations %}
<div class='action'>
<form action="{{ resource.request.path }}" method="POST">
<form action="{{ resource.request.path }}" method="post">
{% csrf_token %}
<input type="hidden" name="{{ resource.METHOD_PARAM}}" value="DELETE" />
<input type="submit" value="Delete" />

View File

@ -1,4 +1,5 @@
{{ resource.name }}
{{ resource.description }}
{% autoescape off %}HTTP/1.0 {{ resource.resp_status }} {{ resource.resp_status_text }}

View File

@ -0,0 +1,17 @@
from django.template import Library
from urlparse import urlparse, urlunparse
from urllib import quote
register = Library()
def add_query_param(url, param):
(key, val) = param.split('=')
param = '%s=%s' % (key, quote(val))
(scheme, netloc, path, params, query, fragment) = urlparse(url)
if query:
query += "&" + param
else:
query = param
return urlunparse((scheme, netloc, path, params, query, fragment))
register.filter('add_query_param', add_query_param)

View File

@ -140,9 +140,9 @@ class XMLEmitter():
def _to_xml(self, xml, data):
if isinstance(data, (list, tuple)):
for item in data:
xml.startElement("resource", {})
xml.startElement("list-item", {})
self._to_xml(xml, item)
xml.endElement("resource")
xml.endElement("list-item")
elif isinstance(data, dict):
for key, value in data.iteritems():
@ -158,11 +158,11 @@ class XMLEmitter():
xml = SimplerXMLGenerator(stream, "utf-8")
xml.startDocument()
xml.startElement("content", {})
xml.startElement("root", {})
self._to_xml(xml, data)
xml.endElement("content")
xml.endElement("root")
xml.endDocument()
return stream.getvalue()

View File

@ -40,8 +40,8 @@ RATING_CHOICES = ((0, 'Awful'),
class BlogPost(models.Model):
key = models.CharField(primary_key=True, max_length=64, default=uuid_str, editable=False)
title = models.CharField(max_length=128, help_text='The article title (Required)')
content = models.TextField(help_text='The article body (Required)')
title = models.CharField(max_length=128)
content = models.TextField()
created = models.DateTimeField(auto_now_add=True)
slug = models.SlugField(editable=False, default='')
@ -74,11 +74,14 @@ class BlogPost(models.Model):
class Comment(models.Model):
blogpost = models.ForeignKey(BlogPost, editable=False, related_name='comments')
username = models.CharField(max_length=128, help_text='Please enter a username (Required)')
comment = models.TextField(help_text='Enter your comment here (Required)')
rating = models.IntegerField(blank=True, null=True, choices=RATING_CHOICES, help_text='Please rate the blog post (Optional)')
username = models.CharField(max_length=128)
comment = models.TextField()
rating = models.IntegerField(blank=True, null=True, choices=RATING_CHOICES, help_text='How did you rate this post?')
created = models.DateTimeField(auto_now_add=True)
class Meta:
ordering = ('created',)
@models.permalink
def get_absolute_url(self):
return ('testapp.views.CommentInstance', (self.blogpost.key, self.id))
@ -86,5 +89,6 @@ class Comment(models.Model):
@property
@models.permalink
def blogpost_url(self):
"""Link to the blog post resource which this comment corresponds to."""
return ('testapp.views.BlogPostInstance', (self.blogpost.key,))

View File

@ -1,4 +1,5 @@
from rest.resource import Resource, ModelResource, QueryModelResource
from rest.resource import Resource
from rest.modelresource import ModelResource, QueryModelResource
from testapp.models import BlogPost, Comment
##### Root Resource #####