diff options
| author | tom christie tom@tomchristie.com | 2011-01-23 23:08:16 +0000 |
|---|---|---|
| committer | tom christie tom@tomchristie.com | 2011-01-23 23:08:16 +0000 |
| commit | 4100242fa2395bef8db0c5ffbab6f5d0cf95301d (patch) | |
| tree | 0c8efd4dfdbdc6af7dca234291f8f8c7e3b035ff /examples/blogpost | |
| parent | 99799032721a32220c32d4a74a950bdd07b13cb3 (diff) | |
| download | django-rest-framework-4100242fa2395bef8db0c5ffbab6f5d0cf95301d.tar.bz2 | |
Sphinx docs, examples, lots of refactoring
Diffstat (limited to 'examples/blogpost')
| -rw-r--r-- | examples/blogpost/__init__.py | 0 | ||||
| -rw-r--r-- | examples/blogpost/models.py | 68 | ||||
| -rw-r--r-- | examples/blogpost/tests.py | 163 | ||||
| -rw-r--r-- | examples/blogpost/urls.py | 11 | ||||
| -rw-r--r-- | examples/blogpost/views.py | 63 |
5 files changed, 305 insertions, 0 deletions
diff --git a/examples/blogpost/__init__.py b/examples/blogpost/__init__.py new file mode 100644 index 00000000..e69de29b --- /dev/null +++ b/examples/blogpost/__init__.py diff --git a/examples/blogpost/models.py b/examples/blogpost/models.py new file mode 100644 index 00000000..1690245c --- /dev/null +++ b/examples/blogpost/models.py @@ -0,0 +1,68 @@ +from django.db import models +from django.template.defaultfilters import slugify +import uuid + +def uuid_str(): + return str(uuid.uuid1()) + + +RATING_CHOICES = ((0, 'Awful'), + (1, 'Poor'), + (2, 'OK'), + (3, 'Good'), + (4, 'Excellent')) + +class BlogPost(models.Model): + key = models.CharField(primary_key=True, max_length=64, default=uuid_str, editable=False) + title = models.CharField(max_length=128) + content = models.TextField() + created = models.DateTimeField(auto_now_add=True) + slug = models.SlugField(editable=False, default='') + + class Meta: + ordering = ('created',) + + @models.permalink + def get_absolute_url(self): + return ('blogpost.views.BlogPostInstance', (), {'key': self.key}) + + @property + @models.permalink + def comments_url(self): + """Link to a resource which lists all comments for this blog post.""" + return ('blogpost.views.CommentList', (), {'blogpost_id': self.key}) + + @property + @models.permalink + def comment_url(self): + """Link to a resource which can create a comment for this blog post.""" + return ('blogpost.views.CommentCreator', (), {'blogpost_id': self.key}) + + def __unicode__(self): + return self.title + + def save(self, *args, **kwargs): + self.slug = slugify(self.title) + super(self.__class__, self).save(*args, **kwargs) + + +class Comment(models.Model): + blogpost = models.ForeignKey(BlogPost, editable=False, related_name='comments') + username = models.CharField(max_length=128) + comment = models.TextField() + rating = models.IntegerField(blank=True, null=True, choices=RATING_CHOICES, help_text='How did you rate this post?') + created = models.DateTimeField(auto_now_add=True) + + class Meta: + ordering = ('created',) + + @models.permalink + def get_absolute_url(self): + return ('blogpost.views.CommentInstance', (), {'blogpost': self.blogpost.key, 'id': self.id}) + + @property + @models.permalink + def blogpost_url(self): + """Link to the blog post resource which this comment corresponds to.""" + return ('blogpost.views.BlogPostInstance', (), {'key': self.blogpost.key}) + diff --git a/examples/blogpost/tests.py b/examples/blogpost/tests.py new file mode 100644 index 00000000..43789399 --- /dev/null +++ b/examples/blogpost/tests.py @@ -0,0 +1,163 @@ +"""Test a range of REST API usage of the example application. +""" + +from django.test import TestCase +from django.core.urlresolvers import reverse +from blogpost import views +#import json +#from rest.utils import xml2dict, dict2xml + + +class AcceptHeaderTests(TestCase): + """Test correct behaviour of the Accept header as specified by RFC 2616: + + http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.1""" + + 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: + expect = mimetype + + resp = self.client.get(reverse(views.RootResource), HTTP_ACCEPT=mimetype) + + self.assertEquals(resp['content-type'], expect) + + + def test_accept_json(self): + """Ensure server responds with Content-Type of JSON when requested.""" + self.assert_accept_mimetype('application/json') + + def test_accept_xml(self): + """Ensure server responds with Content-Type of XML when requested.""" + self.assert_accept_mimetype('application/xml') + + 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') + + def test_accept_xml_when_prefered_to_json(self): + """Ensure server responds with Content-Type of XML when it is the client's prefered choice.""" + self.assert_accept_mimetype('application/json;q=0.1, application/xml;q=0.9', 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): + """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) + + def test_prefer_specific_over_generic(self): # This test is broken right now + """More specific accept types have precedence over less specific types.""" + self.assert_accept_mimetype('application/xml, */*', expect='application/xml') + self.assert_accept_mimetype('*/*, application/xml', expect='application/xml') + + +class AllowedMethodsTests(TestCase): + """Basic tests to check that only allowed operations may be performed on a Resource""" + + 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) + + def test_writing_to_read_only_resource_is_not_allowed(self): + """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) +# +# def test_reading_write_only_not_allowed(self): +# resp = self.client.get(reverse(views.WriteOnlyResource)) +# self.assertEquals(resp.status_code, 405) +# +# def test_writing_write_only_allowed(self): +# resp = self.client.put(reverse(views.WriteOnlyResource), {}) +# self.assertEquals(resp.status_code, 200) +# +# +#class EncodeDecodeTests(TestCase): +# def setUp(self): +# super(self.__class__, self).setUp() +# self.input = {'a': 1, 'b': 'example'} +# +# def test_encode_form_decode_json(self): +# content = self.input +# 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') +# 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') +# # 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(resp.status_code, 201) +# self.assertEquals(output['name'], 'example') +# self.assertEquals(set(output.keys()), set(('absolute_uri', 'name', 'key'))) +# +#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_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_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) + diff --git a/examples/blogpost/urls.py b/examples/blogpost/urls.py new file mode 100644 index 00000000..eccbae15 --- /dev/null +++ b/examples/blogpost/urls.py @@ -0,0 +1,11 @@ +from django.conf.urls.defaults import patterns + +urlpatterns = patterns('blogpost.views', + (r'^$', 'RootResource'), + (r'^blog-posts/$', 'BlogPostList'), + (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'), +) diff --git a/examples/blogpost/views.py b/examples/blogpost/views.py new file mode 100644 index 00000000..05e795fa --- /dev/null +++ b/examples/blogpost/views.py @@ -0,0 +1,63 @@ +from flywheel.response import Response, status +from flywheel.resource import Resource +from flywheel.modelresource import ModelResource, QueryModelResource +from blogpost.models import BlogPost, Comment + +##### Root Resource ##### + +class RootResource(Resource): + """This is the top level resource for the API. + All the sub-resources are discoverable from here.""" + allowed_methods = ('GET',) + + def get(self, request, *args, **kwargs): + return Response(status.HTTP_200_OK, + {'blog-posts': self.reverse(BlogPostList), + 'blog-post': self.reverse(BlogPostCreator)}) + + +##### Blog Post Resources ##### + +BLOG_POST_FIELDS = ('created', 'title', 'slug', 'content', 'absolute_url', 'comment_url', 'comments_url') + +class BlogPostList(QueryModelResource): + """A resource which lists all existing blog posts.""" + allowed_methods = ('GET', ) + model = BlogPost + fields = BLOG_POST_FIELDS + +class BlogPostCreator(ModelResource): + """A resource with which blog posts may be created.""" + allowed_methods = ('POST',) + model = BlogPost + fields = BLOG_POST_FIELDS + +class BlogPostInstance(ModelResource): + """A resource which represents a single blog post.""" + allowed_methods = ('GET', 'PUT', 'DELETE') + model = BlogPost + fields = BLOG_POST_FIELDS + + +##### Comment Resources ##### + +COMMENT_FIELDS = ('username', 'comment', 'created', 'rating', 'absolute_url', 'blogpost_url') + +class CommentList(QueryModelResource): + """A resource which lists all existing comments for a given blog post.""" + allowed_methods = ('GET', ) + model = Comment + fields = COMMENT_FIELDS + +class CommentCreator(ModelResource): + """A resource with which blog comments may be created for a given blog post.""" + allowed_methods = ('POST',) + model = Comment + fields = COMMENT_FIELDS + +class CommentInstance(ModelResource): + """A resource which represents a single comment.""" + allowed_methods = ('GET', 'PUT', 'DELETE') + model = Comment + fields = COMMENT_FIELDS + |
