mirror of
https://github.com/encode/django-rest-framework.git
synced 2025-06-16 11:33:19 +03:00
Sample example working
This commit is contained in:
parent
95ac2396d6
commit
42825e44e1
|
@ -18,7 +18,7 @@ class TemplatedEmitter(BaseEmitter):
|
||||||
template = None
|
template = None
|
||||||
|
|
||||||
def emit(self, output):
|
def emit(self, output):
|
||||||
content = json.dumps(output, indent=4)
|
content = json.dumps(output, indent=4, sort_keys=True)
|
||||||
template = loader.get_template(self.template)
|
template = loader.get_template(self.template)
|
||||||
context = RequestContext(self.request, {
|
context = RequestContext(self.request, {
|
||||||
'content': content,
|
'content': content,
|
||||||
|
|
|
@ -137,15 +137,20 @@ class Resource(object):
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def determine_form(self, data=None):
|
def determine_form(self, input_data=None, return_data=None):
|
||||||
"""Optionally return a Django Form instance, which may be used for validation
|
"""Optionally return a Django Form instance, which may be used for validation
|
||||||
and/or rendered by an HTML/XHTML emitter.
|
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
|
The input_data or return_data arguments can be used to bind the form either to the deserialized input,
|
||||||
input data, or Null if the form is required to be unbound.
|
or to a return object.
|
||||||
"""
|
"""
|
||||||
if self.form:
|
if self.form:
|
||||||
return self.form(data)
|
if input_data:
|
||||||
|
return self.form(input_data)
|
||||||
|
elif return_data:
|
||||||
|
return self.form(return_data)
|
||||||
|
else:
|
||||||
|
return self.form()
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
@ -259,12 +264,13 @@ class Resource(object):
|
||||||
if method in ('PUT', 'POST'):
|
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 = parser(self, request).parse(request.raw_post_data)
|
||||||
form = self.determine_form(data)
|
form = self.determine_form(input_data=data)
|
||||||
data = self.cleanup_request(data, form)
|
data = self.cleanup_request(data, form)
|
||||||
(status, ret, headers) = func(data, request.META, *args, **kwargs)
|
(status, ret, headers) = func(data, request.META, *args, **kwargs)
|
||||||
|
|
||||||
else:
|
else:
|
||||||
(status, ret, headers) = func(request.META, *args, **kwargs)
|
(status, ret, headers) = func(request.META, *args, **kwargs)
|
||||||
|
form = self.determine_form(return_data=ret)
|
||||||
|
|
||||||
|
|
||||||
except ResourceException, exc:
|
except ResourceException, exc:
|
||||||
|
@ -274,7 +280,7 @@ class Resource(object):
|
||||||
if emitter is None:
|
if emitter is None:
|
||||||
mimetype, emitter = self.emitters[0]
|
mimetype, emitter = self.emitters[0]
|
||||||
|
|
||||||
# Use a form unbound to any data if one has not yet been created
|
# Create an unbound form if one has not yet been created
|
||||||
if form is None:
|
if form is None:
|
||||||
form = self.determine_form()
|
form = self.determine_form()
|
||||||
|
|
||||||
|
@ -284,7 +290,6 @@ class Resource(object):
|
||||||
# Serialize the response content
|
# Serialize the response content
|
||||||
ret = self.cleanup_response(ret)
|
ret = self.cleanup_response(ret)
|
||||||
content = emitter(self, request, status, headers, form).emit(ret)
|
content = emitter(self, request, status, headers, form).emit(ret)
|
||||||
print content
|
|
||||||
|
|
||||||
# Build the HTTP Response
|
# Build the HTTP Response
|
||||||
resp = HttpResponse(content, mimetype=mimetype, status=status)
|
resp = HttpResponse(content, mimetype=mimetype, status=status)
|
||||||
|
@ -308,7 +313,7 @@ class ModelResource(Resource):
|
||||||
fields = None
|
fields = None
|
||||||
form_fields = None
|
form_fields = None
|
||||||
|
|
||||||
def determine_form(self, data=None):
|
def determine_form(self, input_data=None, return_data=None):
|
||||||
"""Return a form that may be used in validation and/or rendering an html emitter"""
|
"""Return a form that may be used in validation and/or rendering an html emitter"""
|
||||||
if self.form:
|
if self.form:
|
||||||
return self.form
|
return self.form
|
||||||
|
@ -317,12 +322,14 @@ class ModelResource(Resource):
|
||||||
class NewModelForm(ModelForm):
|
class NewModelForm(ModelForm):
|
||||||
class Meta:
|
class Meta:
|
||||||
model = self.model
|
model = self.model
|
||||||
fields = self.form_fields if self.form_fields else self.fields
|
fields = self.form_fields if self.form_fields else None #self.fields
|
||||||
|
|
||||||
if data is None:
|
if input_data:
|
||||||
return NewModelForm()
|
return NewModelForm(input_data)
|
||||||
|
elif return_data:
|
||||||
|
return NewModelForm(instance=return_data)
|
||||||
else:
|
else:
|
||||||
return NewModelForm(data)
|
return NewModelForm()
|
||||||
|
|
||||||
else:
|
else:
|
||||||
return None
|
return None
|
||||||
|
@ -359,6 +366,12 @@ class ModelResource(Resource):
|
||||||
ret = _list(thing)
|
ret = _list(thing)
|
||||||
elif isinstance(thing, dict):
|
elif isinstance(thing, dict):
|
||||||
ret = _dict(thing)
|
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):
|
elif isinstance(thing, decimal.Decimal):
|
||||||
ret = str(thing)
|
ret = str(thing)
|
||||||
elif isinstance(thing, Model):
|
elif isinstance(thing, Model):
|
||||||
|
@ -417,7 +430,7 @@ class ModelResource(Resource):
|
||||||
ret = { }
|
ret = { }
|
||||||
#handler = self.in_typemapper(type(data), self.anonymous) # TRC
|
#handler = self.in_typemapper(type(data), self.anonymous) # TRC
|
||||||
handler = None # TRC
|
handler = None # TRC
|
||||||
get_absolute_uri = False
|
get_absolute_url = False
|
||||||
|
|
||||||
if handler or fields:
|
if handler or fields:
|
||||||
v = lambda f: getattr(data, f.attname)
|
v = lambda f: getattr(data, f.attname)
|
||||||
|
@ -445,11 +458,12 @@ class ModelResource(Resource):
|
||||||
if exclude.match(field):
|
if exclude.match(field):
|
||||||
get_fields.discard(field)
|
get_fields.discard(field)
|
||||||
|
|
||||||
|
get_absolute_url = True
|
||||||
|
|
||||||
else:
|
else:
|
||||||
get_fields = set(fields)
|
get_fields = set(fields)
|
||||||
|
if 'absolute_url' in get_fields: # MOVED (TRC)
|
||||||
if 'absolute_uri' in get_fields: # MOVED (TRC)
|
get_absolute_url = True
|
||||||
get_absolute_uri = True
|
|
||||||
|
|
||||||
met_fields = _method_fields(handler, get_fields) # TRC
|
met_fields = _method_fields(handler, get_fields) # TRC
|
||||||
|
|
||||||
|
@ -508,14 +522,37 @@ class ModelResource(Resource):
|
||||||
# ret[maybe_field] = _any(handler_f(data))
|
# ret[maybe_field] = _any(handler_f(data))
|
||||||
|
|
||||||
else:
|
else:
|
||||||
|
# Add absolute_url if it exists
|
||||||
|
get_absolute_url = True
|
||||||
|
|
||||||
|
# Add all the fields
|
||||||
for f in data._meta.fields:
|
for f in data._meta.fields:
|
||||||
ret[f.attname] = _any(getattr(data, f.attname))
|
if f.attname != 'id':
|
||||||
|
ret[f.attname] = _any(getattr(data, f.attname))
|
||||||
|
|
||||||
fields = dir(data.__class__) + ret.keys()
|
# Add all the propertiess
|
||||||
add_ons = [k for k in dir(data) if k not in fields]
|
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 k in add_ons:
|
#for attr in dir(data):
|
||||||
ret[k] = _any(getattr(data, k))
|
## #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
|
# TRC
|
||||||
# resouce uri
|
# resouce uri
|
||||||
|
@ -532,10 +569,14 @@ class ModelResource(Resource):
|
||||||
# except: pass
|
# except: pass
|
||||||
|
|
||||||
# absolute uri
|
# absolute uri
|
||||||
if hasattr(data, 'get_absolute_url') and get_absolute_uri:
|
if hasattr(data, 'get_absolute_url') and get_absolute_url:
|
||||||
try: ret['absolute_uri'] = self.make_absolute(data.get_absolute_url())
|
try: ret['absolute_url'] = self.make_absolute(data.get_absolute_url())
|
||||||
except: pass
|
except: pass
|
||||||
|
|
||||||
|
for key, val in ret.items():
|
||||||
|
if key.endswith('_url') or key.endswith('_uri'):
|
||||||
|
ret[key] = self.make_absolute(val)
|
||||||
|
|
||||||
return ret
|
return ret
|
||||||
|
|
||||||
def _qs(data, fields=()):
|
def _qs(data, fields=()):
|
||||||
|
@ -560,8 +601,9 @@ class ModelResource(Resource):
|
||||||
return _any(data, self.fields)
|
return _any(data, self.fields)
|
||||||
|
|
||||||
|
|
||||||
def create(self, data, headers={}):
|
def create(self, data, headers={}, *args, **kwargs):
|
||||||
instance = self.model(**data)
|
all_kw_args = dict(data.items() + kwargs.items())
|
||||||
|
instance = self.model(**all_kw_args)
|
||||||
instance.save()
|
instance.save()
|
||||||
headers = {}
|
headers = {}
|
||||||
if hasattr(instance, 'get_absolute_url'):
|
if hasattr(instance, 'get_absolute_url'):
|
||||||
|
@ -569,13 +611,22 @@ class ModelResource(Resource):
|
||||||
return (201, instance, headers)
|
return (201, instance, headers)
|
||||||
|
|
||||||
def read(self, headers={}, *args, **kwargs):
|
def read(self, headers={}, *args, **kwargs):
|
||||||
instance = self.model.objects.get(**kwargs)
|
try:
|
||||||
|
instance = self.model.objects.get(**kwargs)
|
||||||
|
except self.model.DoesNotExist:
|
||||||
|
return (404, '', {})
|
||||||
|
|
||||||
return (200, instance, {})
|
return (200, instance, {})
|
||||||
|
|
||||||
def update(self, data, headers={}, *args, **kwargs):
|
def update(self, data, headers={}, *args, **kwargs):
|
||||||
instance = self.model.objects.get(**kwargs)
|
try:
|
||||||
for (key, val) in data.items():
|
instance = self.model.objects.get(**kwargs)
|
||||||
setattr(instance, key, val)
|
for (key, val) in data.items():
|
||||||
|
setattr(instance, key, val)
|
||||||
|
except self.model.DoesNotExist:
|
||||||
|
instance = self.model(**data)
|
||||||
|
instance.save()
|
||||||
|
|
||||||
instance.save()
|
instance.save()
|
||||||
return (200, instance, {})
|
return (200, instance, {})
|
||||||
|
|
||||||
|
@ -583,3 +634,14 @@ class ModelResource(Resource):
|
||||||
instance = self.model.objects.get(**kwargs)
|
instance = self.model.objects.get(**kwargs)
|
||||||
instance.delete()
|
instance.delete()
|
||||||
return (204, '', {})
|
return (204, '', {})
|
||||||
|
|
||||||
|
|
||||||
|
class QueryModelResource(ModelResource):
|
||||||
|
allowed_methods = ('read',)
|
||||||
|
|
||||||
|
def determine_form(self, input_data=None, return_data=None):
|
||||||
|
return None
|
||||||
|
|
||||||
|
def read(self, headers={}, *args, **kwargs):
|
||||||
|
query = self.model.objects.all()
|
||||||
|
return (200, query, {})
|
||||||
|
|
|
@ -11,10 +11,10 @@
|
||||||
<body>
|
<body>
|
||||||
<h1>{{ resource_name }}</h1>
|
<h1>{{ resource_name }}</h1>
|
||||||
<p>{{ resource_doc }}</p>
|
<p>{{ resource_doc }}</p>
|
||||||
<pre>{% autoescape off %}<b>{{ status }} {{ reason }}</b>
|
<pre><b>{{ status }} {{ reason }}</b>{% autoescape off %}
|
||||||
{% for key, val in headers.items %}<b>{{ key }}:</b> {{ val|urlize_quoted_links }}
|
{% 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 }} </pre>{% endautoescape %}
|
||||||
|
|
||||||
{% if 'read' in resource.allowed_operations %}
|
{% if 'read' in resource.allowed_operations %}
|
||||||
<div class='action'>
|
<div class='action'>
|
||||||
|
|
|
@ -33,7 +33,7 @@ html_gunk_re = re.compile(r'(?:<br clear="all">|<i><\/i>|<b><\/b>|<em><\/em>|<st
|
||||||
hard_coded_bullets_re = re.compile(r'((?:<p>(?:%s).*?[a-zA-Z].*?</p>\s*)+)' % '|'.join([re.escape(x) for x in DOTS]), re.DOTALL)
|
hard_coded_bullets_re = re.compile(r'((?:<p>(?:%s).*?[a-zA-Z].*?</p>\s*)+)' % '|'.join([re.escape(x) for x in DOTS]), re.DOTALL)
|
||||||
trailing_empty_content_re = re.compile(r'(?:<p>(?: |\s|<br \/>)*?</p>\s*)+\Z')
|
trailing_empty_content_re = re.compile(r'(?:<p>(?: |\s|<br \/>)*?</p>\s*)+\Z')
|
||||||
|
|
||||||
def urlize_quoted_links(text, trim_url_limit=None, nofollow=False, autoescape=False):
|
def urlize_quoted_links(text, trim_url_limit=None, nofollow=False, autoescape=True):
|
||||||
"""
|
"""
|
||||||
Converts any URLs in text into clickable links.
|
Converts any URLs in text into clickable links.
|
||||||
|
|
||||||
|
@ -90,6 +90,10 @@ def urlize_quoted_links(text, trim_url_limit=None, nofollow=False, autoescape=Fa
|
||||||
words[i] = escape(word)
|
words[i] = escape(word)
|
||||||
return u''.join(words)
|
return u''.join(words)
|
||||||
|
|
||||||
|
|
||||||
|
#urlize_quoted_links.needs_autoescape = True
|
||||||
|
urlize_quoted_links.is_safe = True
|
||||||
|
|
||||||
# Register urlize_quoted_links as a custom filter
|
# Register urlize_quoted_links as a custom filter
|
||||||
# http://docs.djangoproject.com/en/dev/howto/custom-template-tags/
|
# http://docs.djangoproject.com/en/dev/howto/custom-template-tags/
|
||||||
register = template.Library()
|
register = template.Library()
|
||||||
|
|
Binary file not shown.
|
@ -1,63 +1,90 @@
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.template.defaultfilters import slugify
|
from django.template.defaultfilters import slugify
|
||||||
from datetime import datetime
|
|
||||||
import uuid
|
import uuid
|
||||||
|
|
||||||
def uuid_str():
|
def uuid_str():
|
||||||
return str(uuid.uuid1())
|
return str(uuid.uuid1())
|
||||||
|
|
||||||
class ExampleModel(models.Model):
|
#class ExampleModel(models.Model):
|
||||||
num = models.IntegerField(default=2, choices=((1,'one'), (2, 'two')))
|
# num = models.IntegerField(default=2, choices=((1,'one'), (2, 'two')))
|
||||||
hidden_num = models.IntegerField(verbose_name='Something', help_text='HELP')
|
# hidden_num = models.IntegerField(verbose_name='Something', help_text='HELP')
|
||||||
text = models.TextField(blank=False)
|
# text = models.TextField(blank=False)
|
||||||
another = models.CharField(max_length=10)
|
# another = models.CharField(max_length=10)
|
||||||
|
|
||||||
|
|
||||||
class ExampleContainer(models.Model):
|
#class ExampleContainer(models.Model):
|
||||||
"""Container. Has a key, a name, and some internal data, and contains a set of items."""
|
# """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)
|
# key = models.CharField(primary_key=True, default=uuid_str, max_length=36, editable=False)
|
||||||
name = models.CharField(max_length=256)
|
# name = models.CharField(max_length=256)
|
||||||
internal = models.IntegerField(default=0)
|
# internal = models.IntegerField(default=0)
|
||||||
|
|
||||||
@models.permalink
|
# @models.permalink
|
||||||
def get_absolute_url(self):
|
# def get_absolute_url(self):
|
||||||
return ('testapp.views.ContainerInstance', [self.key])
|
# return ('testapp.views.ContainerInstance', [self.key])
|
||||||
|
|
||||||
|
|
||||||
class ExampleItem(models.Model):
|
#class ExampleItem(models.Model):
|
||||||
"""Item. Belongs to a container and has an index number and a note.
|
# """Item. Belongs to a container and has an index number and a note.
|
||||||
Items are uniquely identified by their container and index number."""
|
# Items are uniquely identified by their container and index number."""
|
||||||
container = models.ForeignKey(ExampleContainer, related_name='items')
|
# container = models.ForeignKey(ExampleContainer, related_name='items')
|
||||||
index = models.IntegerField()
|
# index = models.IntegerField()
|
||||||
note = models.CharField(max_length=1024)
|
# note = models.CharField(max_length=1024)
|
||||||
unique_together = (container, index)
|
# unique_together = (container, index)
|
||||||
|
|
||||||
|
|
||||||
|
RATING_CHOICES = ((0, 'Awful'),
|
||||||
|
(1, 'Poor'),
|
||||||
|
(2, 'OK'),
|
||||||
|
(3, 'Good'),
|
||||||
|
(4, 'Excellent'))
|
||||||
|
|
||||||
class BlogPost(models.Model):
|
class BlogPost(models.Model):
|
||||||
slug = models.SlugField(editable=False, primary_key=True, default='blah')
|
key = models.CharField(primary_key=True, max_length=64, default=uuid_str, editable=False)
|
||||||
title = models.CharField(max_length=128)
|
title = models.CharField(max_length=128, help_text='The article title (Required)')
|
||||||
content = models.TextField()
|
content = models.TextField(help_text='The article body (Required)')
|
||||||
when = models.DateTimeField(editable=False)
|
created = models.DateTimeField(auto_now_add=True)
|
||||||
|
slug = models.SlugField(editable=False, default='')
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
ordering = ('created',)
|
||||||
|
|
||||||
@models.permalink
|
@models.permalink
|
||||||
def get_absolute_url(self):
|
def get_absolute_url(self):
|
||||||
return ('testapp.views.BlogPostInstance', (self.slug,))
|
return ('testapp.views.BlogPostInstance', (self.key,))
|
||||||
|
|
||||||
|
@property
|
||||||
|
@models.permalink
|
||||||
|
def comments_url(self):
|
||||||
|
"""Link to a resource which lists all comments for this blog post."""
|
||||||
|
return ('testapp.views.CommentList', (self.key,))
|
||||||
|
|
||||||
|
@property
|
||||||
|
@models.permalink
|
||||||
|
def comment_url(self):
|
||||||
|
"""Link to a resource which can create a comment for this blog post."""
|
||||||
|
return ('testapp.views.CommentCreator', (self.key,))
|
||||||
|
|
||||||
|
def __unicode__(self):
|
||||||
|
return self.title
|
||||||
|
|
||||||
def save(self, *args, **kwargs):
|
def save(self, *args, **kwargs):
|
||||||
self.slug = slugify(self.title)
|
self.slug = slugify(self.title)
|
||||||
self.when = datetime.now()
|
|
||||||
super(self.__class__, self).save(*args, **kwargs)
|
super(self.__class__, self).save(*args, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
class Comment(models.Model):
|
class Comment(models.Model):
|
||||||
blogpost = models.ForeignKey(BlogPost, related_name='comments')
|
blogpost = models.ForeignKey(BlogPost, editable=False, related_name='comments')
|
||||||
name = models.CharField(max_length=128)
|
username = models.CharField(max_length=128, help_text='Please enter a username (Required)')
|
||||||
content = models.TextField()
|
comment = models.TextField(help_text='Enter your comment here (Required)')
|
||||||
when = models.DateTimeField(auto_now_add=True)
|
rating = models.IntegerField(blank=True, null=True, choices=RATING_CHOICES, help_text='Please rate the blog post (Optional)')
|
||||||
|
created = models.DateTimeField(auto_now_add=True)
|
||||||
|
|
||||||
@models.permalink
|
@models.permalink
|
||||||
def get_absolute_url(self):
|
def get_absolute_url(self):
|
||||||
return ('testapp.views.CommentInstance', (self.blogpost.slug, self.id))
|
return ('testapp.views.CommentInstance', (self.blogpost.key, self.id))
|
||||||
|
|
||||||
|
@property
|
||||||
|
@models.permalink
|
||||||
|
def blogpost_url(self):
|
||||||
|
return ('testapp.views.BlogPostInstance', (self.blogpost.key,))
|
||||||
|
|
||||||
def save(self):
|
|
||||||
self.index = self.blogpost.comments.count()
|
|
|
@ -1,8 +1,4 @@
|
||||||
"""
|
"""Test a range of REST API usage of the example application.
|
||||||
This file demonstrates two different styles of tests (one doctest and one
|
|
||||||
unittest). These will both pass when you run "manage.py test".
|
|
||||||
|
|
||||||
Replace these with more appropriate tests for your application.
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from django.test import TestCase
|
from django.test import TestCase
|
||||||
|
@ -13,134 +9,154 @@ import json
|
||||||
|
|
||||||
|
|
||||||
class AcceptHeaderTests(TestCase):
|
class AcceptHeaderTests(TestCase):
|
||||||
def assert_accept_mimetype(self, mimetype, expect=None, expect_match=True):
|
"""Test correct behaviour of the Accept header as specified by RFC 2616:
|
||||||
"""
|
|
||||||
Assert that a request with given mimetype in the accept header,
|
http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.1"""
|
||||||
gives a response with the appropriate content-type.
|
|
||||||
"""
|
def assert_accept_mimetype(self, mimetype, expect=None):
|
||||||
|
"""Assert that a request with given mimetype in the accept header,
|
||||||
|
gives a response with the appropriate content-type."""
|
||||||
if expect is None:
|
if expect is None:
|
||||||
expect = mimetype
|
expect = mimetype
|
||||||
|
|
||||||
resp = self.client.get(reverse(views.ReadOnlyResource), HTTP_ACCEPT=mimetype)
|
resp = self.client.get(reverse(views.RootResource), HTTP_ACCEPT=mimetype)
|
||||||
|
|
||||||
if expect_match:
|
self.assertEquals(resp['content-type'], expect)
|
||||||
self.assertEquals(resp['content-type'], expect)
|
|
||||||
else:
|
|
||||||
self.assertNotEquals(resp['content-type'], expect)
|
|
||||||
|
|
||||||
def test_accept_xml(self):
|
|
||||||
self.assert_accept_mimetype('application/xml')
|
|
||||||
|
|
||||||
def test_accept_json(self):
|
def test_accept_json(self):
|
||||||
|
"""Ensure server responds with Content-Type of JSON when requested."""
|
||||||
self.assert_accept_mimetype('application/json')
|
self.assert_accept_mimetype('application/json')
|
||||||
|
|
||||||
def test_accept_xml_prefered_to_json(self):
|
def test_accept_xml(self):
|
||||||
self.assert_accept_mimetype('application/xml,q=0.9;application/json,q=0.1', expect='application/xml')
|
"""Ensure server responds with Content-Type of XML when requested."""
|
||||||
|
self.assert_accept_mimetype('application/xml')
|
||||||
|
|
||||||
def test_accept_json_prefered_to_xml(self):
|
def test_accept_json_when_prefered_to_xml(self):
|
||||||
|
"""Ensure server responds with Content-Type of JSON when it is the client's prefered choice."""
|
||||||
self.assert_accept_mimetype('application/json,q=0.9;application/xml,q=0.1', expect='application/json')
|
self.assert_accept_mimetype('application/json,q=0.9;application/xml,q=0.1', expect='application/json')
|
||||||
|
|
||||||
def test_dont_accept_invalid(self):
|
def test_accept_xml_when_prefered_to_json(self):
|
||||||
self.assert_accept_mimetype('application/invalid', expect_match=False)
|
"""Ensure server responds with Content-Type of XML when it is the client's prefered choice."""
|
||||||
|
self.assert_accept_mimetype('application/xml,q=0.9;application/json,q=0.1', expect='application/xml')
|
||||||
|
|
||||||
|
def test_default_json_prefered(self):
|
||||||
|
"""Ensure server responds with JSON in preference to XML."""
|
||||||
|
self.assert_accept_mimetype('application/json;application/xml', expect='application/json')
|
||||||
|
|
||||||
|
def test_accept_generic_subtype_format(self):
|
||||||
|
"""Ensure server responds with an appropriate type, when the subtype is left generic."""
|
||||||
|
self.assert_accept_mimetype('text/*', expect='text/html')
|
||||||
|
|
||||||
|
def test_accept_generic_type_format(self):
|
||||||
|
"""Ensure server responds with an appropriate type, when the type and subtype are left generic."""
|
||||||
|
self.assert_accept_mimetype('*/*', expect='application/json')
|
||||||
|
|
||||||
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')
|
"""Ensure server returns a 406 (not acceptable) response if we set the Accept header to junk."""
|
||||||
|
resp = self.client.get(reverse(views.RootResource), HTTP_ACCEPT='invalid/invalid')
|
||||||
|
self.assertNotEquals(resp['content-type'], 'invalid/invalid')
|
||||||
self.assertEquals(resp.status_code, 406)
|
self.assertEquals(resp.status_code, 406)
|
||||||
|
|
||||||
def test_prefer_specific(self):
|
def test_prefer_specific_over_generic(self): # This test is broken right now
|
||||||
self.fail("Test not implemented")
|
"""More specific accept types have precedence over less specific types."""
|
||||||
|
self.assert_accept_mimetype('application/xml;*/*', expect='application/xml')
|
||||||
|
|
||||||
|
|
||||||
class AllowedMethodsTests(TestCase):
|
class AllowedMethodsTests(TestCase):
|
||||||
def test_reading_read_only_allowed(self):
|
"""Basic tests to check that only allowed operations may be performed on a Resource"""
|
||||||
resp = self.client.get(reverse(views.ReadOnlyResource))
|
|
||||||
|
def test_reading_a_read_only_resource_is_allowed(self):
|
||||||
|
"""GET requests on a read only resource should default to a 200 (OK) response"""
|
||||||
|
resp = self.client.get(reverse(views.RootResource))
|
||||||
self.assertEquals(resp.status_code, 200)
|
self.assertEquals(resp.status_code, 200)
|
||||||
|
|
||||||
def test_writing_read_only_not_allowed(self):
|
def test_writing_to_read_only_resource_is_not_allowed(self):
|
||||||
resp = self.client.put(reverse(views.ReadOnlyResource), {})
|
"""PUT requests on a read only resource should default to a 405 (method not allowed) response"""
|
||||||
|
resp = self.client.put(reverse(views.RootResource), {})
|
||||||
self.assertEquals(resp.status_code, 405)
|
self.assertEquals(resp.status_code, 405)
|
||||||
|
#
|
||||||
def test_reading_write_only_not_allowed(self):
|
# def test_reading_write_only_not_allowed(self):
|
||||||
resp = self.client.get(reverse(views.WriteOnlyResource))
|
# resp = self.client.get(reverse(views.WriteOnlyResource))
|
||||||
self.assertEquals(resp.status_code, 405)
|
# self.assertEquals(resp.status_code, 405)
|
||||||
|
#
|
||||||
def test_writing_write_only_allowed(self):
|
# def test_writing_write_only_allowed(self):
|
||||||
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()
|
||||||
self.input = {'a': 1, 'b': 'example'}
|
# self.input = {'a': 1, 'b': 'example'}
|
||||||
|
#
|
||||||
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)
|
# 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')
|
# 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)
|
# # output = json.loads(resp.content)
|
||||||
# self.assertEquals(self.input, output)
|
# # self.assertEquals(self.input, output)
|
||||||
|
#
|
||||||
#def test_encode_form_decode_xml(self):
|
# #def test_encode_form_decode_xml(self):
|
||||||
# content = self.input
|
# # content = self.input
|
||||||
# resp = self.client.put(reverse(views.WriteOnlyResource), content, HTTP_ACCEPT='application/xml')
|
# # resp = self.client.put(reverse(views.WriteOnlyResource), content, HTTP_ACCEPT='application/xml')
|
||||||
# output = xml2dict(resp.content)
|
# # output = xml2dict(resp.content)
|
||||||
# self.assertEquals(self.input, output)
|
# # self.assertEquals(self.input, output)
|
||||||
|
#
|
||||||
#def test_encode_json_decode_xml(self):
|
# #def test_encode_json_decode_xml(self):
|
||||||
# content = json.dumps(self.input)
|
# # content = json.dumps(self.input)
|
||||||
# resp = self.client.put(reverse(views.WriteOnlyResource), content, 'application/json', HTTP_ACCEPT='application/xml')
|
# # resp = self.client.put(reverse(views.WriteOnlyResource), content, 'application/json', HTTP_ACCEPT='application/xml')
|
||||||
# output = xml2dict(resp.content)
|
# # output = xml2dict(resp.content)
|
||||||
# self.assertEquals(self.input, output)
|
# # self.assertEquals(self.input, output)
|
||||||
|
#
|
||||||
#def test_encode_xml_decode_xml(self):
|
# #def test_encode_xml_decode_xml(self):
|
||||||
# content = dict2xml(self.input)
|
# # content = dict2xml(self.input)
|
||||||
# resp = self.client.put(reverse(views.WriteOnlyResource), content, 'application/json', HTTP_ACCEPT='application/xml')
|
# # resp = self.client.put(reverse(views.WriteOnlyResource), content, 'application/json', HTTP_ACCEPT='application/xml')
|
||||||
# output = xml2dict(resp.content)
|
# # output = xml2dict(resp.content)
|
||||||
# self.assertEquals(self.input, output)
|
# # self.assertEquals(self.input, output)
|
||||||
|
#
|
||||||
class ModelTests(TestCase):
|
#class ModelTests(TestCase):
|
||||||
def test_create_container(self):
|
# def test_create_container(self):
|
||||||
content = json.dumps({'name': 'example'})
|
# content = json.dumps({'name': 'example'})
|
||||||
resp = self.client.post(reverse(views.ContainerFactory), content, 'application/json')
|
# resp = self.client.post(reverse(views.ContainerFactory), content, 'application/json')
|
||||||
output = json.loads(resp.content)
|
# output = json.loads(resp.content)
|
||||||
self.assertEquals(resp.status_code, 201)
|
# self.assertEquals(resp.status_code, 201)
|
||||||
self.assertEquals(output['name'], 'example')
|
# self.assertEquals(output['name'], 'example')
|
||||||
self.assertEquals(set(output.keys()), set(('absolute_uri', 'name', 'key')))
|
# self.assertEquals(set(output.keys()), set(('absolute_uri', 'name', 'key')))
|
||||||
|
#
|
||||||
class CreatedModelTests(TestCase):
|
#class CreatedModelTests(TestCase):
|
||||||
def setUp(self):
|
# def setUp(self):
|
||||||
content = json.dumps({'name': 'example'})
|
# content = json.dumps({'name': 'example'})
|
||||||
resp = self.client.post(reverse(views.ContainerFactory), content, 'application/json', HTTP_ACCEPT='application/json')
|
# resp = self.client.post(reverse(views.ContainerFactory), content, 'application/json', HTTP_ACCEPT='application/json')
|
||||||
self.container = json.loads(resp.content)
|
# self.container = json.loads(resp.content)
|
||||||
|
#
|
||||||
def test_read_container(self):
|
# def test_read_container(self):
|
||||||
resp = self.client.get(self.container["absolute_uri"])
|
# resp = self.client.get(self.container["absolute_uri"])
|
||||||
self.assertEquals(resp.status_code, 200)
|
# self.assertEquals(resp.status_code, 200)
|
||||||
container = json.loads(resp.content)
|
# container = json.loads(resp.content)
|
||||||
self.assertEquals(container, self.container)
|
# self.assertEquals(container, self.container)
|
||||||
|
#
|
||||||
def test_delete_container(self):
|
# def test_delete_container(self):
|
||||||
resp = self.client.delete(self.container["absolute_uri"])
|
# resp = self.client.delete(self.container["absolute_uri"])
|
||||||
self.assertEquals(resp.status_code, 204)
|
# self.assertEquals(resp.status_code, 204)
|
||||||
self.assertEquals(resp.content, '')
|
# self.assertEquals(resp.content, '')
|
||||||
|
#
|
||||||
def test_update_container(self):
|
# def test_update_container(self):
|
||||||
self.container['name'] = 'new'
|
# self.container['name'] = 'new'
|
||||||
content = json.dumps(self.container)
|
# content = json.dumps(self.container)
|
||||||
resp = self.client.put(self.container["absolute_uri"], content, 'application/json')
|
# resp = self.client.put(self.container["absolute_uri"], content, 'application/json')
|
||||||
self.assertEquals(resp.status_code, 200)
|
# self.assertEquals(resp.status_code, 200)
|
||||||
container = json.loads(resp.content)
|
# container = json.loads(resp.content)
|
||||||
self.assertEquals(container, self.container)
|
# self.assertEquals(container, self.container)
|
||||||
|
|
||||||
|
|
|
@ -2,13 +2,18 @@ from django.conf.urls.defaults import patterns
|
||||||
|
|
||||||
urlpatterns = patterns('testapp.views',
|
urlpatterns = patterns('testapp.views',
|
||||||
(r'^$', 'RootResource'),
|
(r'^$', 'RootResource'),
|
||||||
(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'^model$', 'ModelFormResource'),
|
||||||
(r'^container$', 'ContainerFactory'),
|
#(r'^container$', 'ContainerFactory'),
|
||||||
(r'^container/((?P<key>[^/]+))$', 'ContainerInstance'),
|
#(r'^container/((?P<key>[^/]+))$', 'ContainerInstance'),
|
||||||
|
|
||||||
(r'^blogpost/create$', 'BlogPostCreator'),
|
(r'^blog-posts/$', 'BlogPostList'),
|
||||||
(r'^blogposts/(?P<slug>[^/]+)', 'BlogPostInstance'),
|
(r'^blog-post/$', 'BlogPostCreator'),
|
||||||
|
(r'^blog-post/(?P<key>[^/]+)/$', 'BlogPostInstance'),
|
||||||
|
|
||||||
|
(r'^blog-post/(?P<blogpost_id>[^/]+)/comments/$', 'CommentList'),
|
||||||
|
(r'^blog-post/(?P<blogpost_id>[^/]+)/comment/$', 'CommentCreator'),
|
||||||
|
(r'^blog-post/(?P<blogpost>[^/]+)/comments/(?P<id>[^/]+)/$', 'CommentInstance'),
|
||||||
)
|
)
|
||||||
|
|
|
@ -1,78 +1,111 @@
|
||||||
from rest.resource import Resource, ModelResource
|
from rest.resource import Resource, ModelResource, QueryModelResource
|
||||||
from testapp.forms import ExampleForm
|
from testapp.models import BlogPost, Comment
|
||||||
from testapp.models import ExampleModel, ExampleContainer, BlogPost, Comment
|
|
||||||
|
|
||||||
class RootResource(Resource):
|
class RootResource(Resource):
|
||||||
"""This is my docstring
|
"""This is the top level resource for the API.
|
||||||
"""
|
All the sub-resources are discoverable from here."""
|
||||||
allowed_operations = ('read',)
|
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, {'blog-posts': self.reverse(BlogPostList),
|
||||||
'write-only-api': self.reverse(WriteOnlyResource),
|
'blog-post': self.reverse(BlogPostCreator)}, {})
|
||||||
'read-write-api': self.reverse(ReadWriteResource),
|
|
||||||
'model-api': self.reverse(ModelFormResource),
|
|
||||||
'create-container': self.reverse(ContainerFactory),
|
|
||||||
'blog-post-creator': self.reverse(BlogPostCreator)}, {})
|
|
||||||
|
|
||||||
|
|
||||||
class ReadOnlyResource(Resource):
|
# Blog Post Resources
|
||||||
"""This is my docstring
|
|
||||||
"""
|
|
||||||
allowed_operations = ('read',)
|
|
||||||
|
|
||||||
def read(self, headers={}, *args, **kwargs):
|
class BlogPostList(QueryModelResource):
|
||||||
return (200, {'ExampleString': 'Example',
|
"""A resource which lists all existing blog posts."""
|
||||||
'ExampleInt': 1,
|
allowed_operations = ('read', )
|
||||||
'ExampleDecimal': 1.0}, {})
|
model = BlogPost
|
||||||
|
|
||||||
|
|
||||||
class WriteOnlyResource(Resource):
|
|
||||||
"""This is my docstring
|
|
||||||
"""
|
|
||||||
allowed_operations = ('update',)
|
|
||||||
|
|
||||||
def update(self, data, headers={}, *args, **kwargs):
|
|
||||||
return (200, data, {})
|
|
||||||
|
|
||||||
|
|
||||||
class ReadWriteResource(Resource):
|
|
||||||
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',)
|
|
||||||
|
|
||||||
#######################
|
|
||||||
|
|
||||||
|
|
||||||
class BlogPostCreator(ModelResource):
|
class BlogPostCreator(ModelResource):
|
||||||
"""A Resource with which blog posts may be created.
|
"""A resource with which blog posts may be created."""
|
||||||
This is distinct from blog post instance so that it is discoverable by the client.
|
|
||||||
(ie the client doens't need to know how to form a blog post url in order to create a blog post)"""
|
|
||||||
allowed_operations = ('create',)
|
allowed_operations = ('create',)
|
||||||
model = BlogPost
|
model = BlogPost
|
||||||
|
fields = ('created', 'title', 'slug', 'content', 'absolute_url', 'comment_url', 'comments_url')
|
||||||
|
|
||||||
|
|
||||||
class BlogPostInstance(ModelResource):
|
class BlogPostInstance(ModelResource):
|
||||||
"""Represents a single Blog Post."""
|
"""A resource which represents a single blog post."""
|
||||||
allowed_operations = ('read', 'update', 'delete')
|
allowed_operations = ('read', 'update', 'delete')
|
||||||
model = BlogPost
|
model = BlogPost
|
||||||
|
fields = ('created', 'title', 'slug', 'content', 'absolute_url', 'comment_url', 'comments_url')
|
||||||
|
|
||||||
|
|
||||||
|
# Comment Resources
|
||||||
|
|
||||||
|
class CommentList(QueryModelResource):
|
||||||
|
"""A resource which lists all existing comments for a given blog post."""
|
||||||
|
allowed_operations = ('read', )
|
||||||
|
model = Comment
|
||||||
|
|
||||||
|
|
||||||
|
class CommentCreator(ModelResource):
|
||||||
|
"""A resource with which blog comments may be created for a given blog post."""
|
||||||
|
allowed_operations = ('create',)
|
||||||
|
model = Comment
|
||||||
|
fields = ('username', 'comment', 'created', 'rating', 'absolute_url', 'blogpost_url')
|
||||||
|
|
||||||
|
|
||||||
|
class CommentInstance(ModelResource):
|
||||||
|
"""A resource which represents a single comment."""
|
||||||
|
allowed_operations = ('read', 'update', 'delete')
|
||||||
|
model = Comment
|
||||||
|
fields = ('username', 'comment', 'created', 'rating', 'absolute_url', 'blogpost_url')
|
||||||
|
|
||||||
|
#
|
||||||
|
#'read-only-api': self.reverse(ReadOnlyResource),
|
||||||
|
# 'write-only-api': self.reverse(WriteOnlyResource),
|
||||||
|
# 'read-write-api': self.reverse(ReadWriteResource),
|
||||||
|
# 'model-api': self.reverse(ModelFormResource),
|
||||||
|
# 'create-container': self.reverse(ContainerFactory),
|
||||||
|
#
|
||||||
|
#class ReadOnlyResource(Resource):
|
||||||
|
# """This is my docstring
|
||||||
|
# """
|
||||||
|
# allowed_operations = ('read',)
|
||||||
|
#
|
||||||
|
# def read(self, headers={}, *args, **kwargs):
|
||||||
|
# return (200, {'ExampleString': 'Example',
|
||||||
|
# 'ExampleInt': 1,
|
||||||
|
# 'ExampleDecimal': 1.0}, {})
|
||||||
|
#
|
||||||
|
#
|
||||||
|
#class WriteOnlyResource(Resource):
|
||||||
|
# """This is my docstring
|
||||||
|
# """
|
||||||
|
# allowed_operations = ('update',)
|
||||||
|
#
|
||||||
|
# def update(self, data, headers={}, *args, **kwargs):
|
||||||
|
# return (200, data, {})
|
||||||
|
#
|
||||||
|
#
|
||||||
|
#class ReadWriteResource(Resource):
|
||||||
|
# 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