Commit f144b769 by Tom Christie

Lots of good form validation and default actions

parent 48c7171a
...@@ -4,12 +4,13 @@ import json ...@@ -4,12 +4,13 @@ import json
from utils import dict2xml from utils import dict2xml
class BaseEmitter(object): class BaseEmitter(object):
def __init__(self, resource, request, status, headers): def __init__(self, resource, request, status, headers, form):
self.request = request self.request = request
self.resource = resource self.resource = resource
self.status = status self.status = status
self.headers = headers self.headers = headers
self.form = form
def emit(self, output): def emit(self, output):
return output return output
...@@ -26,14 +27,13 @@ class TemplatedEmitter(BaseEmitter): ...@@ -26,14 +27,13 @@ class TemplatedEmitter(BaseEmitter):
'headers': self.headers, 'headers': self.headers,
'resource_name': self.resource.__class__.__name__, 'resource_name': self.resource.__class__.__name__,
'resource_doc': self.resource.__doc__, 'resource_doc': self.resource.__doc__,
'create_form': self.resource.create_form and self.resource.create_form() or None, 'create_form': self.form,
'update_form': self.resource.update_form and self.resource.update_form() or None, 'update_form': self.form,
'allowed_methods': self.resource.allowed_methods,
'request': self.request, 'request': self.request,
'resource': self.resource, 'resource': self.resource,
}) })
return template.render(context) return template.render(context)
class JSONEmitter(BaseEmitter): class JSONEmitter(BaseEmitter):
def emit(self, output): def emit(self, output):
return json.dumps(output) return json.dumps(output)
......
...@@ -17,6 +17,44 @@ class XMLParser(BaseParser): ...@@ -17,6 +17,44 @@ class XMLParser(BaseParser):
pass pass
class FormParser(BaseParser): class FormParser(BaseParser):
"""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): def parse(self, input):
return self.request.POST return self.data
...@@ -12,17 +12,17 @@ ...@@ -12,17 +12,17 @@
<h1>{{ resource_name }}</h1> <h1>{{ resource_name }}</h1>
<p>{{ resource_doc }}</p> <p>{{ resource_doc }}</p>
<pre>{% autoescape off %}<b>{{ status }} {{ reason }}</b> <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 %} {% endfor %}
{{ content|urlize_quoted_links }}{% endautoescape %} </pre> {{ content|urlize_quoted_links }}{% endautoescape %} </pre>
{% if 'GET' in allowed_methods %} {% if 'read' in resource.allowed_operations %}
<div class='action'> <div class='action'>
<a href='{{ request.path }}'>Read</a> <a href='{{ request.path }}'>Read</a>
</div> </div>
{% endif %} {% endif %}
{% if 'POST' in resource.allowed_methods %} {% if 'create' in resource.allowed_operations %}
<div class='action'> <div class='action'>
<form action="{{ request.path }}" method="POST"> <form action="{{ request.path }}" method="POST">
{% csrf_token %} {% csrf_token %}
...@@ -32,7 +32,7 @@ ...@@ -32,7 +32,7 @@
</div> </div>
{% endif %} {% endif %}
{% if 'PUT' in resource.allowed_methods %} {% if 'update' in resource.allowed_operations %}
<div class='action'> <div class='action'>
<form action="{{ request.path }}" method="POST"> <form action="{{ request.path }}" method="POST">
<input type="hidden" name="{{ resource.METHOD_PARAM}}" value="PUT" /> <input type="hidden" name="{{ resource.METHOD_PARAM}}" value="PUT" />
...@@ -43,7 +43,7 @@ ...@@ -43,7 +43,7 @@
</div> </div>
{% endif %} {% endif %}
{% if 'DELETE' in resource.allowed_methods %} {% if 'delete' in resource.allowed_operations %}
<div class='action'> <div class='action'>
<form action="{{ request.path }}" method="POST"> <form action="{{ request.path }}" method="POST">
{% csrf_token %} {% csrf_token %}
......
from django.db import models 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)
\ No newline at end of file
...@@ -9,7 +9,8 @@ from django.test import TestCase ...@@ -9,7 +9,8 @@ from django.test import TestCase
from django.core.urlresolvers import reverse from django.core.urlresolvers import reverse
from testapp import views from testapp import views
import json import json
from rest.utils import xml2dict, dict2xml #from rest.utils import xml2dict, dict2xml
class AcceptHeaderTests(TestCase): class AcceptHeaderTests(TestCase):
def assert_accept_mimetype(self, mimetype, expect=None, expect_match=True): def assert_accept_mimetype(self, mimetype, expect=None, expect_match=True):
...@@ -45,6 +46,10 @@ class AcceptHeaderTests(TestCase): ...@@ -45,6 +46,10 @@ class AcceptHeaderTests(TestCase):
def test_invalid_accept_header_returns_406(self): def test_invalid_accept_header_returns_406(self):
resp = self.client.get(reverse(views.ReadOnlyResource), HTTP_ACCEPT='invalid/invalid') resp = self.client.get(reverse(views.ReadOnlyResource), HTTP_ACCEPT='invalid/invalid')
self.assertEquals(resp.status_code, 406) self.assertEquals(resp.status_code, 406)
def test_prefer_specific(self):
self.fail("Test not implemented")
class AllowedMethodsTests(TestCase): class AllowedMethodsTests(TestCase):
def test_reading_read_only_allowed(self): def test_reading_read_only_allowed(self):
...@@ -63,6 +68,7 @@ class AllowedMethodsTests(TestCase): ...@@ -63,6 +68,7 @@ class AllowedMethodsTests(TestCase):
resp = self.client.put(reverse(views.WriteOnlyResource), {}) resp = self.client.put(reverse(views.WriteOnlyResource), {})
self.assertEquals(resp.status_code, 200) self.assertEquals(resp.status_code, 200)
class EncodeDecodeTests(TestCase): class EncodeDecodeTests(TestCase):
def setUp(self): def setUp(self):
super(self.__class__, self).setUp() super(self.__class__, self).setUp()
...@@ -70,36 +76,71 @@ class EncodeDecodeTests(TestCase): ...@@ -70,36 +76,71 @@ class EncodeDecodeTests(TestCase):
def test_encode_form_decode_json(self): def test_encode_form_decode_json(self):
content = self.input 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) output = json.loads(resp.content)
self.assertEquals(self.input, output) self.assertEquals(self.input, output)
def test_encode_json_decode_json(self): def test_encode_json_decode_json(self):
content = json.dumps(self.input) 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) output = json.loads(resp.content)
self.assertEquals(self.input, output) self.assertEquals(self.input, output)
def test_encode_xml_decode_json(self): #def test_encode_xml_decode_json(self):
content = dict2xml(self.input) # content = dict2xml(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', 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) output = json.loads(resp.content)
self.assertEquals(self.input, output) self.assertEquals(resp.status_code, 201)
self.assertEquals(output['name'], 'example')
def test_encode_form_decode_xml(self): self.assertEquals(set(output.keys()), set(('absolute_uri', 'name', 'key')))
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): class CreatedModelTests(TestCase):
content = json.dumps(self.input) def setUp(self):
resp = self.client.put(reverse(views.WriteOnlyResource), content, 'application/json', HTTP_ACCEPT='application/xml') content = json.dumps({'name': 'example'})
output = xml2dict(resp.content) resp = self.client.post(reverse(views.ContainerFactory), content, 'application/json', HTTP_ACCEPT='application/json')
self.assertEquals(self.input, output) self.container = json.loads(resp.content)
def test_encode_xml_decode_xml(self): def test_read_container(self):
content = dict2xml(self.input) resp = self.client.get(self.container["absolute_uri"])
resp = self.client.put(reverse(views.WriteOnlyResource), content, 'application/json', HTTP_ACCEPT='application/xml') self.assertEquals(resp.status_code, 200)
output = xml2dict(resp.content) container = json.loads(resp.content)
self.assertEquals(self.input, output) self.assertEquals(container, self.container)
\ No newline at end of file
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)
\ No newline at end of file
...@@ -5,4 +5,7 @@ urlpatterns = patterns('testapp.views', ...@@ -5,4 +5,7 @@ urlpatterns = patterns('testapp.views',
(r'^read-only$', 'ReadOnlyResource'), (r'^read-only$', 'ReadOnlyResource'),
(r'^write-only$', 'WriteOnlyResource'), (r'^write-only$', 'WriteOnlyResource'),
(r'^read-write$', 'ReadWriteResource'), (r'^read-write$', 'ReadWriteResource'),
(r'^model$', 'ModelFormResource'),
(r'^container$', 'ContainerFactory'),
(r'^container/((?P<key>[^/]+))$', 'ContainerInstance'),
) )
from rest.resource import Resource from rest.resource import Resource, ModelResource
from testapp.forms import ExampleForm from testapp.forms import ExampleForm
from testapp.models import ExampleModel, ExampleContainer
class RootResource(Resource): class RootResource(Resource):
"""This is my docstring """This is my docstring
""" """
allowed_methods = ('GET',) allowed_operations = ('read',)
def read(self, headers={}, *args, **kwargs): def read(self, headers={}, *args, **kwargs):
return (200, {'read-only-api': self.reverse(ReadOnlyResource), return (200, {'read-only-api': self.reverse(ReadOnlyResource),
'write-only-api': self.reverse(WriteOnlyResource), '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): class ReadOnlyResource(Resource):
"""This is my docstring """This is my docstring
""" """
allowed_methods = ('GET',) allowed_operations = ('read',)
def read(self, headers={}, *args, **kwargs): def read(self, headers={}, *args, **kwargs):
return (200, {'ExampleString': 'Example', return (200, {'ExampleString': 'Example',
...@@ -26,13 +29,35 @@ class ReadOnlyResource(Resource): ...@@ -26,13 +29,35 @@ class ReadOnlyResource(Resource):
class WriteOnlyResource(Resource): class WriteOnlyResource(Resource):
"""This is my docstring """This is my docstring
""" """
allowed_methods = ('PUT',) allowed_operations = ('update',)
def update(self, data, headers={}, *args, **kwargs): def update(self, data, headers={}, *args, **kwargs):
return (200, data, {}) return (200, data, {})
class ReadWriteResource(Resource): class ReadWriteResource(Resource):
allowed_methods = ('GET', 'PUT', 'DELETE') allowed_operations = ('read', 'update', 'delete')
create_form = ExampleForm create_form = ExampleForm
update_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',)
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment