mirror of
https://github.com/encode/django-rest-framework.git
synced 2024-11-22 09:36:49 +03:00
Lots of good form validation and default actions
This commit is contained in:
parent
48c7171aa0
commit
f144b769fe
|
@ -4,12 +4,13 @@ import json
|
|||
from utils import dict2xml
|
||||
|
||||
class BaseEmitter(object):
|
||||
def __init__(self, resource, request, status, headers):
|
||||
def __init__(self, resource, request, status, headers, form):
|
||||
self.request = request
|
||||
self.resource = resource
|
||||
self.status = status
|
||||
self.headers = headers
|
||||
|
||||
self.form = form
|
||||
|
||||
def emit(self, output):
|
||||
return output
|
||||
|
||||
|
@ -26,14 +27,13 @@ class TemplatedEmitter(BaseEmitter):
|
|||
'headers': self.headers,
|
||||
'resource_name': self.resource.__class__.__name__,
|
||||
'resource_doc': self.resource.__doc__,
|
||||
'create_form': self.resource.create_form and self.resource.create_form() or None,
|
||||
'update_form': self.resource.update_form and self.resource.update_form() or None,
|
||||
'allowed_methods': self.resource.allowed_methods,
|
||||
'create_form': self.form,
|
||||
'update_form': self.form,
|
||||
'request': self.request,
|
||||
'resource': self.resource,
|
||||
})
|
||||
return template.render(context)
|
||||
|
||||
|
||||
class JSONEmitter(BaseEmitter):
|
||||
def emit(self, output):
|
||||
return json.dumps(output)
|
||||
|
|
|
@ -17,6 +17,44 @@ class XMLParser(BaseParser):
|
|||
pass
|
||||
|
||||
class FormParser(BaseParser):
|
||||
def parse(self, input):
|
||||
return self.request.POST
|
||||
"""The default parser for form data.
|
||||
Return a dict containing a single value for each non-reserved parameter
|
||||
"""
|
||||
def __init__(self, resource, request):
|
||||
|
||||
if request.method == 'PUT':
|
||||
# Fix from piston to force Django to give PUT requests the same
|
||||
# form processing that POST requests get...
|
||||
#
|
||||
# Bug fix: if _load_post_and_files has already been called, for
|
||||
# example by middleware accessing request.POST, the below code to
|
||||
# pretend the request is a POST instead of a PUT will be too late
|
||||
# to make a difference. Also calling _load_post_and_files will result
|
||||
# in the following exception:
|
||||
# AttributeError: You cannot set the upload handlers after the upload has been processed.
|
||||
# The fix is to check for the presence of the _post field which is set
|
||||
# the first time _load_post_and_files is called (both by wsgi.py and
|
||||
# modpython.py). If it's set, the request has to be 'reset' to redo
|
||||
# the query value parsing in POST mode.
|
||||
if hasattr(request, '_post'):
|
||||
del request._post
|
||||
del request._files
|
||||
|
||||
try:
|
||||
request.method = "POST"
|
||||
request._load_post_and_files()
|
||||
request.method = "PUT"
|
||||
except AttributeError:
|
||||
request.META['REQUEST_METHOD'] = 'POST'
|
||||
request._load_post_and_files()
|
||||
request.META['REQUEST_METHOD'] = 'PUT'
|
||||
|
||||
#
|
||||
self.data = {}
|
||||
for (key, val) in request.POST.items():
|
||||
if key not in resource.RESERVED_PARAMS:
|
||||
self.data[key] = val
|
||||
|
||||
def parse(self, input):
|
||||
return self.data
|
||||
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
from django.http import HttpResponse
|
||||
from django.core.urlresolvers import reverse
|
||||
from rest import emitters, parsers, utils
|
||||
from rest import emitters, parsers
|
||||
from decimal import Decimal
|
||||
|
||||
#
|
||||
|
@ -20,44 +20,103 @@ class ResourceException(Exception):
|
|||
|
||||
|
||||
class Resource(object):
|
||||
# List of RESTful operations which may be performed on this resource.
|
||||
allowed_operations = ('read',)
|
||||
|
||||
allowed_methods = ('GET',)
|
||||
|
||||
callmap = { 'GET': 'read', 'POST': 'create',
|
||||
'PUT': 'update', 'DELETE': 'delete' }
|
||||
|
||||
emitters = [ ('application/json', emitters.JSONEmitter),
|
||||
# List of content-types the resource can respond with, ordered by preference
|
||||
emitters = ( ('application/json', emitters.JSONEmitter),
|
||||
('text/html', emitters.HTMLEmitter),
|
||||
('application/xhtml+xml', emitters.HTMLEmitter),
|
||||
('text/plain', emitters.TextEmitter),
|
||||
('application/xml', emitters.XMLEmitter), ]
|
||||
('application/xml', emitters.XMLEmitter), )
|
||||
|
||||
# List of content-types the resource can read from
|
||||
parsers = { 'application/json': parsers.JSONParser,
|
||||
'application/xml': parsers.XMLParser,
|
||||
'application/x-www-form-urlencoded': parsers.FormParser,
|
||||
'multipart/form-data': parsers.FormParser }
|
||||
|
||||
create_form = None
|
||||
update_form = None
|
||||
# Optional form for input validation and presentation of HTML formatted responses.
|
||||
form = None
|
||||
|
||||
# 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))
|
||||
|
||||
USE_SITEMAP_FOR_ABSOLUTE_URLS = False
|
||||
|
||||
|
||||
def __new__(cls, request, *args, **kwargs):
|
||||
"""Make the class callable so it can be used as a Django view."""
|
||||
self = object.__new__(cls)
|
||||
self.__init__()
|
||||
self._request = request
|
||||
return self._handle_request(request, *args, **kwargs)
|
||||
try:
|
||||
return self._handle_request(request, *args, **kwargs)
|
||||
except:
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
raise
|
||||
|
||||
|
||||
|
||||
def __init__(self):
|
||||
pass
|
||||
|
||||
|
||||
def _determine_method(self, request):
|
||||
"""Determine the HTTP method that this request should be treated as,
|
||||
allowing for PUT and DELETE tunneling via the _method parameter."""
|
||||
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))
|
||||
|
||||
|
||||
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.
|
||||
|
||||
Provided for convienience."""
|
||||
return self._request.build_absolute_uri(uri)
|
||||
|
||||
|
||||
def read(self, headers={}, *args, **kwargs):
|
||||
"""RESTful read on the resource, which must be subclassed to be implemented. Should be a safe operation."""
|
||||
self.not_implemented('read')
|
||||
|
||||
|
||||
def create(self, data=None, headers={}, *args, **kwargs):
|
||||
"""RESTful create on the resource, which must be subclassed to be implemented."""
|
||||
self.not_implemented('create')
|
||||
|
||||
|
||||
def update(self, data=None, headers={}, *args, **kwargs):
|
||||
"""RESTful update on the resource, which must be subclassed to be implemented. Should be an idempotent operation."""
|
||||
self.not_implemented('update')
|
||||
|
||||
|
||||
def delete(self, headers={}, *args, **kwargs):
|
||||
"""RESTful delete on the resource, which must be subclassed to be implemented. Should be an idempotent operation."""
|
||||
self.not_implemented('delete')
|
||||
|
||||
|
||||
def not_implemented(self, operation):
|
||||
"""Return an HTTP 500 server error if an operation is called which has been allowed by
|
||||
allowed_operations, but which has not been implemented."""
|
||||
raise ResourceException(STATUS_500_INTERNAL_SERVER_ERROR,
|
||||
{'detail': '%s operation on this resource has not been implemented' % (operation, )})
|
||||
|
||||
|
||||
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
|
||||
|
||||
if method == 'POST' and request.POST.has_key(self.METHOD_PARAM):
|
||||
|
@ -66,17 +125,47 @@ class Resource(object):
|
|||
return method
|
||||
|
||||
|
||||
def _check_method_allowed(self, method):
|
||||
if not method in self.allowed_methods:
|
||||
raise ResourceException(STATUS_405_METHOD_NOT_ALLOWED,
|
||||
{'detail': 'Method \'%s\' not allowed on this resource.' % method})
|
||||
|
||||
if not method in self.callmap.keys():
|
||||
def check_method_allowed(self, method):
|
||||
"""Ensure the request method is acceptable fot this resource."""
|
||||
if not method in self.CALLMAP.keys():
|
||||
raise ResourceException(STATUS_501_NOT_IMPLEMENTED,
|
||||
{'detail': 'Unknown or unsupported method \'%s\'' % method})
|
||||
|
||||
if not self.CALLMAP[method] in self.allowed_operations:
|
||||
raise ResourceException(STATUS_405_METHOD_NOT_ALLOWED,
|
||||
{'detail': 'Method \'%s\' not allowed on this resource.' % method})
|
||||
|
||||
|
||||
def _determine_parser(self, request):
|
||||
|
||||
def determine_form(self, data=None):
|
||||
"""Optionally return a Django Form instance, which may be used for validation
|
||||
and/or rendered by an HTML/XHTML emitter.
|
||||
|
||||
The data argument will be non Null if the form is required to be bound to some deserialized
|
||||
input data, or Null if the form is required to be unbound.
|
||||
"""
|
||||
if self.form:
|
||||
return self.form(data)
|
||||
return None
|
||||
|
||||
|
||||
def cleanup_request(self, data, form=None):
|
||||
"""Perform any resource-specific data deserialization and/or validation
|
||||
after the initial HTTP content-type deserialization has taken place.
|
||||
|
||||
Optionally this may use a Django Form which will have been bound to the data,
|
||||
rather than using the data directly.
|
||||
"""
|
||||
return data
|
||||
|
||||
|
||||
def cleanup_response(self, data):
|
||||
"""Perform any resource-specific data filtering prior to the standard HTTP
|
||||
content-type serialization."""
|
||||
return data
|
||||
|
||||
|
||||
def determine_parser(self, request):
|
||||
"""Return the appropriate parser for the input, given the client's 'Content-Type' header,
|
||||
and the content types that this Resource knows how to parse."""
|
||||
content_type = request.META.get('CONTENT_TYPE', 'application/x-www-form-urlencoded')
|
||||
|
@ -90,14 +179,13 @@ class Resource(object):
|
|||
except KeyError:
|
||||
raise ResourceException(STATUS_415_UNSUPPORTED_MEDIA_TYPE,
|
||||
{'detail': 'Unsupported content type \'%s\'' % content_type})
|
||||
|
||||
def _determine_emitter(self, request):
|
||||
|
||||
|
||||
def determine_emitter(self, request):
|
||||
"""Return the appropriate emitter for the output, given the client's 'Accept' header,
|
||||
and the content types that this Resource knows how to serve.
|
||||
|
||||
See: RFC 2616, Section 14 - http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html
|
||||
"""
|
||||
|
||||
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'):
|
||||
|
@ -141,61 +229,61 @@ class Resource(object):
|
|||
{'detail': 'Could not statisfy the client\'s accepted content type',
|
||||
'accepted_types': [item[0] for item in self.emitters]})
|
||||
|
||||
|
||||
def _validate_data(self, method, data):
|
||||
"""If there is an appropriate form to deal with this operation,
|
||||
then validate the data and return the resulting dictionary.
|
||||
"""
|
||||
if method == 'PUT' and self.update_form:
|
||||
form = self.update_form(data)
|
||||
elif method == 'POST' and self.create_form:
|
||||
form = self.create_form(data)
|
||||
else:
|
||||
return data
|
||||
|
||||
if not form.is_valid():
|
||||
raise ResourceException(STATUS_400_BAD_REQUEST,
|
||||
{'detail': dict((k, map(unicode, v))
|
||||
for (k,v) in form.errors.iteritems())})
|
||||
|
||||
return form.cleaned_data
|
||||
|
||||
|
||||
def _handle_request(self, request, *args, **kwargs):
|
||||
"""
|
||||
|
||||
Broadly this consists of the following procedure:
|
||||
|
||||
# Hack to ensure PUT requests get the same form treatment as POST requests
|
||||
utils.coerce_put_post(request)
|
||||
|
||||
# Get the request method, allowing for PUT and DELETE tunneling
|
||||
method = self._determine_method(request)
|
||||
|
||||
0. ensure the operation is permitted
|
||||
1. deserialize request content into request data, using standard HTTP content types (PUT/POST only)
|
||||
2. cleanup and validate request data (PUT/POST only)
|
||||
3. call the core method to get the response data
|
||||
4. cleanup the response data
|
||||
5. serialize response data into response content, using standard HTTP content negotiation
|
||||
"""
|
||||
method = self.determine_method(request)
|
||||
emitter = None
|
||||
form = None
|
||||
try:
|
||||
self._check_method_allowed(method)
|
||||
|
||||
# Parse the HTTP Request content
|
||||
func = getattr(self, self.callmap.get(method, ''))
|
||||
# Before we attempt anything else determine what format to emit our response data with.
|
||||
mimetype, emitter = self.determine_emitter(request)
|
||||
|
||||
# Ensure the requested operation is permitted on this resource
|
||||
self.check_method_allowed(method)
|
||||
|
||||
# Get the appropriate create/read/update/delete function
|
||||
func = getattr(self, self.CALLMAP.get(method, ''))
|
||||
|
||||
# Either generate the response data, deserializing and validating any request data
|
||||
if method in ('PUT', 'POST'):
|
||||
parser = self._determine_parser(request)
|
||||
parser = self.determine_parser(request)
|
||||
data = parser(self, request).parse(request.raw_post_data)
|
||||
data = self._validate_data(method, data)
|
||||
form = self.determine_form(data)
|
||||
data = self.cleanup_request(data, form)
|
||||
(status, ret, headers) = func(data, request.META, *args, **kwargs)
|
||||
|
||||
else:
|
||||
(status, ret, headers) = func(request.META, *args, **kwargs)
|
||||
|
||||
|
||||
except ResourceException, exc:
|
||||
(status, ret, headers) = (exc.status, exc.content, exc.headers)
|
||||
|
||||
headers['Allow'] = ', '.join(self.allowed_methods)
|
||||
|
||||
# Serialize the HTTP Response content
|
||||
try:
|
||||
mimetype, emitter = self._determine_emitter(request)
|
||||
except ResourceException, exc:
|
||||
(status, ret, headers) = (exc.status, exc.content, exc.headers)
|
||||
# Use a default emitter if request failed without being able to determine an acceptable emitter
|
||||
if emitter is None:
|
||||
mimetype, emitter = self.emitters[0]
|
||||
|
||||
# Use a form unbound to any data if one has not yet been created
|
||||
if form is None:
|
||||
form = self.determine_form()
|
||||
|
||||
# Always add the allow header
|
||||
headers['Allow'] = ', '.join([self.REVERSE_CALLMAP[operation] for operation in self.allowed_operations])
|
||||
|
||||
content = emitter(self, request, status, headers).emit(ret)
|
||||
# Serialize the response content
|
||||
ret = self.cleanup_response(ret)
|
||||
content = emitter(self, request, status, headers, form).emit(ret)
|
||||
|
||||
# Build the HTTP Response
|
||||
resp = HttpResponse(content, mimetype=mimetype, status=status)
|
||||
|
@ -204,24 +292,293 @@ class Resource(object):
|
|||
|
||||
return resp
|
||||
|
||||
def _not_implemented(self, operation):
|
||||
resource_name = self.__class__.__name__
|
||||
raise ResourceException(STATUS_500_INTERNAL_SERVER_ERROR,
|
||||
{'detail': '%s operation on this resource has not been implemented' % (operation, )})
|
||||
|
||||
|
||||
|
||||
from django.forms import ModelForm
|
||||
from django.db.models.query import QuerySet
|
||||
from django.db.models import Model
|
||||
import decimal
|
||||
import inspect
|
||||
import re
|
||||
|
||||
class ModelResource(Resource):
|
||||
model = None
|
||||
fields = None
|
||||
form_fields = None
|
||||
|
||||
def determine_form(self, data=None):
|
||||
"""Return a form that may be used in validation and/or rendering an html emitter"""
|
||||
if self.form:
|
||||
return self.form
|
||||
|
||||
elif self.model:
|
||||
class NewModelForm(ModelForm):
|
||||
class Meta:
|
||||
model = self.model
|
||||
fields = self.form_fields if self.form_fields else self.fields
|
||||
|
||||
if data is None:
|
||||
return NewModelForm()
|
||||
else:
|
||||
return NewModelForm(data)
|
||||
|
||||
else:
|
||||
return None
|
||||
|
||||
def cleanup_request(self, data, form=None):
|
||||
"""Filter data into form-cleaned data, performing validation and type coercion."""
|
||||
if form is None:
|
||||
return data
|
||||
|
||||
if not form.is_valid():
|
||||
details = dict((key, map(unicode, val)) for (key, val) in form.errors.iteritems())
|
||||
raise ResourceException(STATUS_400_BAD_REQUEST, {'detail': details})
|
||||
|
||||
return form.cleaned_data
|
||||
|
||||
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, 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_uri = 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)
|
||||
|
||||
else:
|
||||
get_fields = set(fields)
|
||||
|
||||
if 'absolute_uri' in get_fields: # MOVED (TRC)
|
||||
get_absolute_uri = 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:
|
||||
for f in data._meta.fields:
|
||||
ret[f.attname] = _any(getattr(data, f.attname))
|
||||
|
||||
fields = dir(data.__class__) + ret.keys()
|
||||
add_ons = [k for k in dir(data) if k not in 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_uri:
|
||||
try: ret['absolute_uri'] = self.make_absolute(data.get_absolute_url())
|
||||
except: pass
|
||||
|
||||
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={}):
|
||||
instance = self.model(**data)
|
||||
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):
|
||||
self._not_implemented('read')
|
||||
instance = self.model.objects.get(**kwargs)
|
||||
return (200, instance, {})
|
||||
|
||||
def create(self, data=None, headers={}, *args, **kwargs):
|
||||
self._not_implemented('create')
|
||||
|
||||
def update(self, data=None, headers={}, *args, **kwargs):
|
||||
self._not_implemented('update')
|
||||
def update(self, data, headers={}, *args, **kwargs):
|
||||
instance = self.model.objects.get(**kwargs)
|
||||
for (key, val) in data.items():
|
||||
setattr(instance, key, val)
|
||||
instance.save()
|
||||
return (200, instance, {})
|
||||
|
||||
def delete(self, headers={}, *args, **kwargs):
|
||||
self._not_implemented('delete')
|
||||
|
||||
def reverse(self, view, *args, **kwargs):
|
||||
"""Return a fully qualified URI for a view, using the current request as the base URI.
|
||||
"""
|
||||
return self._request.build_absolute_uri(reverse(view, *args, **kwargs))
|
||||
instance = self.model.objects.get(**kwargs)
|
||||
instance.delete()
|
||||
return (204, '', {})
|
|
@ -12,17 +12,17 @@
|
|||
<h1>{{ resource_name }}</h1>
|
||||
<p>{{ resource_doc }}</p>
|
||||
<pre>{% autoescape off %}<b>{{ status }} {{ reason }}</b>
|
||||
{% for key, val in headers.items %}<b>{{ key }}:</b> {{ val }}
|
||||
{% for key, val in headers.items %}<b>{{ key }}:</b> {{ val|urlize_quoted_links }}
|
||||
{% endfor %}
|
||||
{{ content|urlize_quoted_links }}{% endautoescape %} </pre>
|
||||
|
||||
{% if 'GET' in allowed_methods %}
|
||||
{% if 'read' in resource.allowed_operations %}
|
||||
<div class='action'>
|
||||
<a href='{{ request.path }}'>Read</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if 'POST' in resource.allowed_methods %}
|
||||
{% if 'create' in resource.allowed_operations %}
|
||||
<div class='action'>
|
||||
<form action="{{ request.path }}" method="POST">
|
||||
{% csrf_token %}
|
||||
|
@ -32,7 +32,7 @@
|
|||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if 'PUT' in resource.allowed_methods %}
|
||||
{% if 'update' in resource.allowed_operations %}
|
||||
<div class='action'>
|
||||
<form action="{{ request.path }}" method="POST">
|
||||
<input type="hidden" name="{{ resource.METHOD_PARAM}}" value="PUT" />
|
||||
|
@ -43,7 +43,7 @@
|
|||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if 'DELETE' in resource.allowed_methods %}
|
||||
{% if 'delete' in resource.allowed_operations %}
|
||||
<div class='action'>
|
||||
<form action="{{ request.path }}" method="POST">
|
||||
{% csrf_token %}
|
||||
|
|
Binary file not shown.
Binary file not shown.
|
@ -1,3 +1,31 @@
|
|||
from django.db import models
|
||||
import uuid
|
||||
|
||||
# Create your models here.
|
||||
def uuid_str():
|
||||
return str(uuid.uuid1())
|
||||
|
||||
class ExampleModel(models.Model):
|
||||
num = models.IntegerField(default=2, choices=((1,'one'), (2, 'two')))
|
||||
hidden_num = models.IntegerField(verbose_name='Something', help_text='HELP')
|
||||
text = models.TextField(blank=False)
|
||||
another = models.CharField(max_length=10)
|
||||
|
||||
|
||||
class ExampleContainer(models.Model):
|
||||
"""Container. Has a key, a name, and some internal data, and contains a set of items."""
|
||||
key = models.CharField(primary_key=True, default=uuid_str, max_length=36, editable=False)
|
||||
name = models.CharField(max_length=256)
|
||||
internal = models.IntegerField(default=0)
|
||||
|
||||
@models.permalink
|
||||
def get_absolute_url(self):
|
||||
return ('testapp.views.ContainerInstance', [self.key])
|
||||
|
||||
|
||||
class ExampleItem(models.Model):
|
||||
"""Item. Belongs to a container and has an index number and a note.
|
||||
Items are uniquely identified by their container and index number."""
|
||||
container = models.ForeignKey(ExampleContainer, related_name='items')
|
||||
index = models.IntegerField()
|
||||
note = models.CharField(max_length=1024)
|
||||
unique_together = (container, index)
|
|
@ -9,7 +9,8 @@ from django.test import TestCase
|
|||
from django.core.urlresolvers import reverse
|
||||
from testapp import views
|
||||
import json
|
||||
from rest.utils import xml2dict, dict2xml
|
||||
#from rest.utils import xml2dict, dict2xml
|
||||
|
||||
|
||||
class AcceptHeaderTests(TestCase):
|
||||
def assert_accept_mimetype(self, mimetype, expect=None, expect_match=True):
|
||||
|
@ -45,6 +46,10 @@ class AcceptHeaderTests(TestCase):
|
|||
def test_invalid_accept_header_returns_406(self):
|
||||
resp = self.client.get(reverse(views.ReadOnlyResource), HTTP_ACCEPT='invalid/invalid')
|
||||
self.assertEquals(resp.status_code, 406)
|
||||
|
||||
def test_prefer_specific(self):
|
||||
self.fail("Test not implemented")
|
||||
|
||||
|
||||
class AllowedMethodsTests(TestCase):
|
||||
def test_reading_read_only_allowed(self):
|
||||
|
@ -63,6 +68,7 @@ class AllowedMethodsTests(TestCase):
|
|||
resp = self.client.put(reverse(views.WriteOnlyResource), {})
|
||||
self.assertEquals(resp.status_code, 200)
|
||||
|
||||
|
||||
class EncodeDecodeTests(TestCase):
|
||||
def setUp(self):
|
||||
super(self.__class__, self).setUp()
|
||||
|
@ -70,36 +76,71 @@ class EncodeDecodeTests(TestCase):
|
|||
|
||||
def test_encode_form_decode_json(self):
|
||||
content = self.input
|
||||
resp = self.client.put(reverse(views.WriteOnlyResource), content, HTTP_ACCEPT='application/json')
|
||||
resp = self.client.put(reverse(views.WriteOnlyResource), content)
|
||||
output = json.loads(resp.content)
|
||||
self.assertEquals(self.input, output)
|
||||
|
||||
def test_encode_json_decode_json(self):
|
||||
content = json.dumps(self.input)
|
||||
resp = self.client.put(reverse(views.WriteOnlyResource), content, 'application/json', HTTP_ACCEPT='application/json')
|
||||
resp = self.client.put(reverse(views.WriteOnlyResource), content, 'application/json')
|
||||
output = json.loads(resp.content)
|
||||
self.assertEquals(self.input, output)
|
||||
|
||||
def test_encode_xml_decode_json(self):
|
||||
content = dict2xml(self.input)
|
||||
resp = self.client.put(reverse(views.WriteOnlyResource), content, 'application/json', HTTP_ACCEPT='application/json')
|
||||
#def test_encode_xml_decode_json(self):
|
||||
# content = dict2xml(self.input)
|
||||
# resp = self.client.put(reverse(views.WriteOnlyResource), content, 'application/json', HTTP_ACCEPT='application/json')
|
||||
# output = json.loads(resp.content)
|
||||
# self.assertEquals(self.input, output)
|
||||
|
||||
#def test_encode_form_decode_xml(self):
|
||||
# content = self.input
|
||||
# resp = self.client.put(reverse(views.WriteOnlyResource), content, HTTP_ACCEPT='application/xml')
|
||||
# output = xml2dict(resp.content)
|
||||
# self.assertEquals(self.input, output)
|
||||
|
||||
#def test_encode_json_decode_xml(self):
|
||||
# content = json.dumps(self.input)
|
||||
# resp = self.client.put(reverse(views.WriteOnlyResource), content, 'application/json', HTTP_ACCEPT='application/xml')
|
||||
# output = xml2dict(resp.content)
|
||||
# self.assertEquals(self.input, output)
|
||||
|
||||
#def test_encode_xml_decode_xml(self):
|
||||
# content = dict2xml(self.input)
|
||||
# resp = self.client.put(reverse(views.WriteOnlyResource), content, 'application/json', HTTP_ACCEPT='application/xml')
|
||||
# output = xml2dict(resp.content)
|
||||
# self.assertEquals(self.input, output)
|
||||
|
||||
class ModelTests(TestCase):
|
||||
def test_create_container(self):
|
||||
content = json.dumps({'name': 'example'})
|
||||
resp = self.client.post(reverse(views.ContainerFactory), content, 'application/json')
|
||||
output = json.loads(resp.content)
|
||||
self.assertEquals(self.input, output)
|
||||
self.assertEquals(resp.status_code, 201)
|
||||
self.assertEquals(output['name'], 'example')
|
||||
self.assertEquals(set(output.keys()), set(('absolute_uri', 'name', 'key')))
|
||||
|
||||
def test_encode_form_decode_xml(self):
|
||||
content = self.input
|
||||
resp = self.client.put(reverse(views.WriteOnlyResource), content, HTTP_ACCEPT='application/xml')
|
||||
output = xml2dict(resp.content)
|
||||
self.assertEquals(self.input, output)
|
||||
class CreatedModelTests(TestCase):
|
||||
def setUp(self):
|
||||
content = json.dumps({'name': 'example'})
|
||||
resp = self.client.post(reverse(views.ContainerFactory), content, 'application/json', HTTP_ACCEPT='application/json')
|
||||
self.container = json.loads(resp.content)
|
||||
|
||||
def test_encode_json_decode_xml(self):
|
||||
content = json.dumps(self.input)
|
||||
resp = self.client.put(reverse(views.WriteOnlyResource), content, 'application/json', HTTP_ACCEPT='application/xml')
|
||||
output = xml2dict(resp.content)
|
||||
self.assertEquals(self.input, output)
|
||||
def test_read_container(self):
|
||||
resp = self.client.get(self.container["absolute_uri"])
|
||||
self.assertEquals(resp.status_code, 200)
|
||||
container = json.loads(resp.content)
|
||||
self.assertEquals(container, self.container)
|
||||
|
||||
def test_encode_xml_decode_xml(self):
|
||||
content = dict2xml(self.input)
|
||||
resp = self.client.put(reverse(views.WriteOnlyResource), content, 'application/json', HTTP_ACCEPT='application/xml')
|
||||
output = xml2dict(resp.content)
|
||||
self.assertEquals(self.input, output)
|
||||
def test_delete_container(self):
|
||||
resp = self.client.delete(self.container["absolute_uri"])
|
||||
self.assertEquals(resp.status_code, 204)
|
||||
self.assertEquals(resp.content, '')
|
||||
|
||||
def test_update_container(self):
|
||||
self.container['name'] = 'new'
|
||||
content = json.dumps(self.container)
|
||||
resp = self.client.put(self.container["absolute_uri"], content, 'application/json')
|
||||
self.assertEquals(resp.status_code, 200)
|
||||
container = json.loads(resp.content)
|
||||
self.assertEquals(container, self.container)
|
||||
|
|
@ -5,4 +5,7 @@ urlpatterns = patterns('testapp.views',
|
|||
(r'^read-only$', 'ReadOnlyResource'),
|
||||
(r'^write-only$', 'WriteOnlyResource'),
|
||||
(r'^read-write$', 'ReadWriteResource'),
|
||||
(r'^model$', 'ModelFormResource'),
|
||||
(r'^container$', 'ContainerFactory'),
|
||||
(r'^container/((?P<key>[^/]+))$', 'ContainerInstance'),
|
||||
)
|
||||
|
|
|
@ -1,21 +1,24 @@
|
|||
from rest.resource import Resource
|
||||
from rest.resource import Resource, ModelResource
|
||||
from testapp.forms import ExampleForm
|
||||
from testapp.models import ExampleModel, ExampleContainer
|
||||
|
||||
class RootResource(Resource):
|
||||
"""This is my docstring
|
||||
"""
|
||||
allowed_methods = ('GET',)
|
||||
allowed_operations = ('read',)
|
||||
|
||||
def read(self, headers={}, *args, **kwargs):
|
||||
return (200, {'read-only-api': self.reverse(ReadOnlyResource),
|
||||
'write-only-api': self.reverse(WriteOnlyResource),
|
||||
'read-write-api': self.reverse(ReadWriteResource)}, {})
|
||||
'read-write-api': self.reverse(ReadWriteResource),
|
||||
'model-api': self.reverse(ModelFormResource),
|
||||
'create-container': self.reverse(ContainerFactory)}, {})
|
||||
|
||||
|
||||
class ReadOnlyResource(Resource):
|
||||
"""This is my docstring
|
||||
"""
|
||||
allowed_methods = ('GET',)
|
||||
allowed_operations = ('read',)
|
||||
|
||||
def read(self, headers={}, *args, **kwargs):
|
||||
return (200, {'ExampleString': 'Example',
|
||||
|
@ -26,13 +29,35 @@ class ReadOnlyResource(Resource):
|
|||
class WriteOnlyResource(Resource):
|
||||
"""This is my docstring
|
||||
"""
|
||||
allowed_methods = ('PUT',)
|
||||
allowed_operations = ('update',)
|
||||
|
||||
def update(self, data, headers={}, *args, **kwargs):
|
||||
return (200, data, {})
|
||||
|
||||
|
||||
class ReadWriteResource(Resource):
|
||||
allowed_methods = ('GET', 'PUT', 'DELETE')
|
||||
allowed_operations = ('read', 'update', 'delete')
|
||||
create_form = ExampleForm
|
||||
update_form = ExampleForm
|
||||
|
||||
|
||||
class ModelFormResource(ModelResource):
|
||||
allowed_operations = ('read', 'update', 'delete')
|
||||
model = ExampleModel
|
||||
|
||||
# Nice things: form validation is applied to any input type
|
||||
# html forms for output
|
||||
# output always serialized nicely
|
||||
class ContainerFactory(ModelResource):
|
||||
allowed_operations = ('create',)
|
||||
model = ExampleContainer
|
||||
fields = ('absolute_uri', 'name', 'key')
|
||||
form_fields = ('name',)
|
||||
|
||||
|
||||
class ContainerInstance(ModelResource):
|
||||
allowed_operations = ('read', 'update', 'delete')
|
||||
model = ExampleContainer
|
||||
fields = ('absolute_uri', 'name', 'key')
|
||||
form_fields = ('name',)
|
||||
|
||||
|
|
Loading…
Reference in New Issue
Block a user