Lots of good form validation and default actions

This commit is contained in:
Tom Christie 2011-01-04 17:42:23 +00:00
parent 48c7171aa0
commit f144b769fe
10 changed files with 615 additions and 123 deletions

View File

@ -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)

View File

@ -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

View File

@ -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, '', {})

View File

@ -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.

View File

@ -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)

View File

@ -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)

View File

@ -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'),
)

View File

@ -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',)