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 | |
| parent | 99799032721a32220c32d4a74a950bdd07b13cb3 (diff) | |
| download | django-rest-framework-4100242fa2395bef8db0c5ffbab6f5d0cf95301d.tar.bz2 | |
Sphinx docs, examples, lots of refactoring
Diffstat (limited to 'examples')
| -rw-r--r-- | examples/__init__.py | 0 | ||||
| -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 | ||||
| -rw-r--r-- | examples/initial_data.json | 20 | ||||
| -rwxr-xr-x | examples/manage.py | 11 | ||||
| -rw-r--r-- | examples/objectstore/__init__.py | 0 | ||||
| -rw-r--r-- | examples/objectstore/models.py | 3 | ||||
| -rw-r--r-- | examples/objectstore/tests.py | 23 | ||||
| -rw-r--r-- | examples/objectstore/urls.py | 6 | ||||
| -rw-r--r-- | examples/objectstore/views.py | 54 | ||||
| -rw-r--r-- | examples/settings.py | 96 | ||||
| -rw-r--r-- | examples/urls.py | 11 | 
15 files changed, 529 insertions, 0 deletions
| diff --git a/examples/__init__.py b/examples/__init__.py new file mode 100644 index 00000000..e69de29b --- /dev/null +++ b/examples/__init__.py 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 + diff --git a/examples/initial_data.json b/examples/initial_data.json new file mode 100644 index 00000000..62103cf9 --- /dev/null +++ b/examples/initial_data.json @@ -0,0 +1,20 @@ +[ +    { +        "pk": 1,  +        "model": "auth.user",  +        "fields": { +            "username": "admin",  +            "first_name": "",  +            "last_name": "",  +            "is_active": true,  +            "is_superuser": true,  +            "is_staff": true,  +            "last_login": "2010-01-01 00:00:00",  +            "groups": [],  +            "user_permissions": [],  +            "password": "sha1$6cbce$e4e808893d586a3301ac3c14da6c84855999f1d8",  +            "email": "test@example.com",  +            "date_joined": "2010-01-01 00:00:00" +        } +    } +]
\ No newline at end of file diff --git a/examples/manage.py b/examples/manage.py new file mode 100755 index 00000000..5e78ea97 --- /dev/null +++ b/examples/manage.py @@ -0,0 +1,11 @@ +#!/usr/bin/env python +from django.core.management import execute_manager +try: +    import settings # Assumed to be in the same directory. +except ImportError: +    import sys +    sys.stderr.write("Error: Can't find the file 'settings.py' in the directory containing %r. It appears you've customized things.\nYou'll have to run django-admin.py, passing it your settings module.\n(If the file settings.py does indeed exist, it's causing an ImportError somehow.)\n" % __file__) +    sys.exit(1) + +if __name__ == "__main__": +    execute_manager(settings) diff --git a/examples/objectstore/__init__.py b/examples/objectstore/__init__.py new file mode 100644 index 00000000..e69de29b --- /dev/null +++ b/examples/objectstore/__init__.py diff --git a/examples/objectstore/models.py b/examples/objectstore/models.py new file mode 100644 index 00000000..71a83623 --- /dev/null +++ b/examples/objectstore/models.py @@ -0,0 +1,3 @@ +from django.db import models + +# Create your models here. diff --git a/examples/objectstore/tests.py b/examples/objectstore/tests.py new file mode 100644 index 00000000..2247054b --- /dev/null +++ b/examples/objectstore/tests.py @@ -0,0 +1,23 @@ +""" +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 + +class SimpleTest(TestCase): +    def test_basic_addition(self): +        """ +        Tests that 1 + 1 always equals 2. +        """ +        self.failUnlessEqual(1 + 1, 2) + +__test__ = {"doctest": """ +Another way to test that 1 + 1 is equal to 2. + +>>> 1 + 1 == 2 +True +"""} + diff --git a/examples/objectstore/urls.py b/examples/objectstore/urls.py new file mode 100644 index 00000000..c04e731e --- /dev/null +++ b/examples/objectstore/urls.py @@ -0,0 +1,6 @@ +from django.conf.urls.defaults import patterns + +urlpatterns = patterns('objectstore.views', +    (r'^$', 'ObjectStoreRoot'),  +    (r'^(?P<key>[A-Za-z0-9_-]{1,64})/$', 'StoredObject'), +) diff --git a/examples/objectstore/views.py b/examples/objectstore/views.py new file mode 100644 index 00000000..16c7f8e9 --- /dev/null +++ b/examples/objectstore/views.py @@ -0,0 +1,54 @@ +from django.conf import settings + +from flywheel.resource import Resource +from flywheel.response import Response, status + +import pickle +import os +import uuid + +OBJECT_STORE_DIR = os.path.join(settings.MEDIA_ROOT, 'objectstore') + + +class ObjectStoreRoot(Resource): +    """Root of the Object Store API. +    Allows the client to get a complete list of all the stored objects, or to create a new stored object.""" +    allowed_methods = ('GET', 'POST') + +    def get(self, request): +        """Return a list of all the stored object URLs.""" +        keys = sorted(os.listdir(OBJECT_STORE_DIR)) +        return [self.reverse(StoredObject, key=key) for key in keys] +     +    def post(self, request, content): +        """Create a new stored object, with a unique key.""" +        key = str(uuid.uuid1()) +        pathname = os.path.join(OBJECT_STORE_DIR, key) +        pickle.dump(content, open(pathname, 'wb')) +        return Response(status.HTTP_201_CREATED, content, {'Location': self.reverse(StoredObject, key=key)}) +  +         +class StoredObject(Resource): +    """Represents a stored object. +    The object may be any picklable content.""" +    allowed_methods = ('GET', 'PUT', 'DELETE') + +    def get(self, request, key): +        """Return a stored object, by unpickling the contents of a locally stored file.""" +        pathname = os.path.join(OBJECT_STORE_DIR, key) +        if not os.path.exists(pathname): +            return Response(status.HTTP_404_NOT_FOUND) +        return pickle.load(open(pathname, 'rb')) + +    def put(self, request, content, key): +        """Update/create a stored object, by pickling the request content to a locally stored file.""" +        pathname = os.path.join(OBJECT_STORE_DIR, key) +        pickle.dump(content, open(pathname, 'wb')) +        return content + +    def delete(self, request, key): +        """Delete a stored object, by removing it's pickled file.""" +        pathname = os.path.join(OBJECT_STORE_DIR, key) +        if not os.path.exists(pathname): +            return Response(status.HTTP_404_NOT_FOUND) +        os.remove(pathname) diff --git a/examples/settings.py b/examples/settings.py new file mode 100644 index 00000000..d1635104 --- /dev/null +++ b/examples/settings.py @@ -0,0 +1,96 @@ +# Django settings for src project. + +DEBUG = True +TEMPLATE_DEBUG = DEBUG + +ADMINS = ( +    # ('Your Name', 'your_email@domain.com'), +) + +MANAGERS = ADMINS + +DATABASES = { +    'default': { +        'ENGINE': 'django.db.backends.sqlite3', # Add 'postgresql_psycopg2', 'postgresql', 'mysql', 'sqlite3' or 'oracle'. +        'NAME': 'sqlite3.db',                   # Or path to database file if using sqlite3. +        'USER': '',                      # Not used with sqlite3. +        'PASSWORD': '',                  # Not used with sqlite3. +        'HOST': '',                      # Set to empty string for localhost. Not used with sqlite3. +        'PORT': '',                      # Set to empty string for default. Not used with sqlite3. +    } +} + +# Local time zone for this installation. Choices can be found here: +# http://en.wikipedia.org/wiki/List_of_tz_zones_by_name +# although not all choices may be available on all operating systems. +# On Unix systems, a value of None will cause Django to use the same +# timezone as the operating system. +# If running in a Windows environment this must be set to the same as your +# system time zone. +TIME_ZONE = 'Europe/London' + +# Language code for this installation. All choices can be found here: +# http://www.i18nguy.com/unicode/language-identifiers.html +LANGUAGE_CODE = 'en-uk' + +SITE_ID = 1 + +# If you set this to False, Django will make some optimizations so as not +# to load the internationalization machinery. +USE_I18N = True + +# If you set this to False, Django will not format dates, numbers and +# calendars according to the current locale +USE_L10N = True + +# Absolute filesystem path to the directory that will hold user-uploaded files. +# Example: "/home/media/media.lawrence.com/" +MEDIA_ROOT = '/Users/tomchristie/' + +# URL that handles the media served from MEDIA_ROOT. Make sure to use a +# trailing slash if there is a path component (optional in other cases). +# Examples: "http://media.lawrence.com", "http://example.com/media/" +MEDIA_URL = '' + +# URL prefix for admin media -- CSS, JavaScript and images. Make sure to use a +# trailing slash. +# Examples: "http://foo.com/media/", "/media/". +ADMIN_MEDIA_PREFIX = '/media/' + +# Make this unique, and don't share it with anybody. +SECRET_KEY = 't&9mru2_k$t8e2-9uq-wu2a1)9v*us&j3i#lsqkt(lbx*vh1cu' + +# List of callables that know how to import templates from various sources. +TEMPLATE_LOADERS = ( +    'django.template.loaders.filesystem.Loader', +    'django.template.loaders.app_directories.Loader', +#     'django.template.loaders.eggs.Loader', +) + +MIDDLEWARE_CLASSES = ( +    'django.middleware.common.CommonMiddleware', +    'django.contrib.sessions.middleware.SessionMiddleware', +    'django.middleware.csrf.CsrfViewMiddleware', +    'django.contrib.auth.middleware.AuthenticationMiddleware', +    'django.contrib.messages.middleware.MessageMiddleware', +) + +ROOT_URLCONF = 'urls' + +TEMPLATE_DIRS = ( +    # Put strings here, like "/home/html/django_templates" or "C:/www/django/templates". +    # Always use forward slashes, even on Windows. +    # Don't forget to use absolute paths, not relative paths. +) + +INSTALLED_APPS = ( +    'django.contrib.auth', +    'django.contrib.contenttypes', +    'django.contrib.sessions', +    'django.contrib.sites', +    'django.contrib.messages', +    'django.contrib.admin', +    'flywheel', +    'blogpost', +    'objectstore' +) diff --git a/examples/urls.py b/examples/urls.py new file mode 100644 index 00000000..03791084 --- /dev/null +++ b/examples/urls.py @@ -0,0 +1,11 @@ +from django.conf.urls.defaults import patterns, include +from django.contrib import admin + +admin.autodiscover() + +urlpatterns = patterns('', +    (r'^blog-post-example/', include('blogpost.urls')), +    (r'^object-store-example/', include('objectstore.urls')), +    (r'^admin/doc/', include('django.contrib.admindocs.urls')), +    (r'^admin/', include(admin.site.urls)), +) | 
