diff --git a/.hgignore b/.hgignore index f72e65867..48c9bb0d7 100644 --- a/.hgignore +++ b/.hgignore @@ -3,3 +3,5 @@ syntax: glob *.pyc *.db env +.project +.pydevproject diff --git a/README.txt b/README.txt new file mode 100644 index 000000000..00c5e8b8d --- /dev/null +++ b/README.txt @@ -0,0 +1,13 @@ +# To install django-rest-framework... +# +# Requirements: +# python2.6 +# virtualenv + +hg clone https://tomchristie@bitbucket.org/tomchristie/django-rest-framework +cd django-rest-framework/ +virtualenv --no-site-packages --distribute --python=python2.6 env +source ./env/bin/activate +pip install -r ./requirements.txt +python ./src/manage.py test + diff --git a/src/rest/resource.py b/src/rest/resource.py index 7e4a6cd54..18421a19c 100644 --- a/src/rest/resource.py +++ b/src/rest/resource.py @@ -4,6 +4,14 @@ from rest import emitters, parsers class Resource(object): + class HTTPException(Exception): + def __init__(self, status, content, headers): + self.status = status + self.content = content + self.headers = headers + + allowed_methods = ('GET',) + callmap = { 'GET': 'read', 'POST': 'create', 'PUT': 'update', 'DELETE': 'delete' } @@ -17,6 +25,7 @@ class Resource(object): 'application/xml': parsers.XMLParser, 'application/x-www-form-urlencoded': parsers.FormParser } + def __new__(cls, request, *args, **kwargs): self = object.__new__(cls) self.__init__() @@ -28,7 +37,6 @@ class Resource(object): def _determine_parser(self, request): """Return the appropriate parser for the input, given the client's 'Content-Type' header, and the content types that this Resource knows how to parse.""" - print request.META return self.parsers.values()[0] # TODO: Raise 415 Unsupported media type @@ -79,25 +87,40 @@ class Resource(object): (accept_mimetype == mimetype)): return (mimetype, emitter) - # TODO: Raise 406, Not Acceptable + raise self.HTTPException(406, {'status': 'Not Acceptable', + 'accepts': ','.join(item[0] for item in self.emitters)}, {}) + def _handle_request(self, request, *args, **kwargs): - meth = request.method + method = request.method + + try: + if not method in self.allowed_methods: + raise self.HTTPException(405, {'status': 'Method Not Allowed'}, {}) + + # Parse the HTTP Request content + func = getattr(self, self.callmap.get(method, '')) + + if method in ('PUT', 'POST'): + parser = self._determine_parser(request) + data = parser(self, request).parse(request.raw_post_data) + (status, ret, headers) = func(data, request.META, *args, **kwargs) + + else: + (status, ret, headers) = func(request.META, *args, **kwargs) + except self.HTTPException, exc: + (status, ret, headers) = (exc.status, exc.content, exc.headers) + + headers['Allow'] = ', '.join(self.allowed_methods) - # Parse the HTTP Request content - if meth in ('PUT', 'POST'): - parser = self._determine_parser(request) - data = parser(self, request).parse(request.raw_post_data) - - if meth == "POST": - (status, ret, headers) = self.handle_post(data, request.META, *args, **kwargs) - else: - (status, ret, headers) = self.handle_get(request.META, *args, **kwargs) - # Serialize the HTTP Response content - mimetype, emitter = self._determine_emitter(request) + try: + mimetype, emitter = self._determine_emitter(request) + except self.HTTPException, exc: + (status, ret, headers) = (exc.status, exc.content, exc.headers) + mimetype, emitter = self.emitters[0] + content = emitter(self, status, headers).emit(ret) - print mimetype, emitter, content # Build the HTTP Response resp = HttpResponse(content, mimetype=mimetype, status=status) @@ -106,8 +129,19 @@ class Resource(object): return resp - def handle_get(self): - raise NotImplementedError(self.handle_get) + def _not_implemented(self, operation): + resource_name = self.__class__.__name__ + return (500, {'status': 'Internal Server Error', + 'detail': '%s %s operation is permitted but has not been implemented' % (resource_name, operation)}, {}) - def handle_post(self): - raise NotImplementedError(self.handle_post) \ No newline at end of file + def read(self, headers={}, *args, **kwargs): + return self._not_implemented('read') + + def create(self, data=None, headers={}, *args, **kwargs): + return self._not_implemented('create') + + def update(self, data=None, headers={}, *args, **kwargs): + return self._not_implemented('update') + + def delete(self, headers={}, *args, **kwargs): + return self._not_implemented('delete') diff --git a/src/testarchive/templates/emitter.html b/src/rest/templates/emitter.html similarity index 100% rename from src/testarchive/templates/emitter.html rename to src/rest/templates/emitter.html diff --git a/src/testarchive/templates/emitter.txt b/src/rest/templates/emitter.txt similarity index 100% rename from src/testarchive/templates/emitter.txt rename to src/rest/templates/emitter.txt diff --git a/src/testarchive/templates/emitter.xhtml b/src/rest/templates/emitter.xhtml similarity index 100% rename from src/testarchive/templates/emitter.xhtml rename to src/rest/templates/emitter.xhtml diff --git a/src/settings.py b/src/settings.py index 2a55c76a9..fa3476dbb 100644 --- a/src/settings.py +++ b/src/settings.py @@ -75,7 +75,7 @@ MIDDLEWARE_CLASSES = ( 'django.contrib.messages.middleware.MessageMiddleware', ) -ROOT_URLCONF = 'src.urls' +ROOT_URLCONF = 'urls' TEMPLATE_DIRS = ( # Put strings here, like "/home/html/django_templates" or "C:/www/django/templates". @@ -93,5 +93,6 @@ INSTALLED_APPS = ( 'django.contrib.admin', # Uncomment the next line to enable admin documentation: # 'django.contrib.admindocs', - 'testarchive', + 'testapp', + 'rest', ) diff --git a/src/testarchive/__init__.py b/src/testapp/__init__.py similarity index 100% rename from src/testarchive/__init__.py rename to src/testapp/__init__.py diff --git a/src/testarchive/models.py b/src/testapp/models.py similarity index 100% rename from src/testarchive/models.py rename to src/testapp/models.py diff --git a/src/testapp/tests.py b/src/testapp/tests.py new file mode 100644 index 000000000..5543cd966 --- /dev/null +++ b/src/testapp/tests.py @@ -0,0 +1,54 @@ +""" +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.core.urlresolvers import reverse +from testapp.views import ReadOnlyResource, MirroringWriteResource + +class AcceptHeaderTests(TestCase): + def assert_accept_mimetype(self, mimetype, expect=None, expect_match=True): + """ + 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(ReadOnlyResource), HTTP_ACCEPT=mimetype) + + if expect_match: + 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): + self.assert_accept_mimetype('application/json') + + def test_accept_xml_prefered_to_json(self): + self.assert_accept_mimetype('application/xml,q=0.9;application/json,q=0.1', expect='application/xml') + + def test_accept_json_prefered_to_xml(self): + self.assert_accept_mimetype('application/json,q=0.9;application/xml,q=0.1', expect='application/json') + + def test_dont_accept_invalid(self): + self.assert_accept_mimetype('application/invalid', expect_match=False) + + def test_invalid_accept_header_returns_406(self): + resp = self.client.get(reverse(ReadOnlyResource), HTTP_ACCEPT='invalid/invalid') + self.assertEquals(resp.status_code, 406) + +class AllowedMethodsTests(TestCase): + def test_write_on_read_only_resource_returns_405(self): + resp = self.client.put(reverse(ReadOnlyResource), {}) + self.assertEquals(resp.status_code, 405) + + def test_read_on_write_only_resource_returns_405(self): + resp = self.client.get(reverse(MirroringWriteResource)) + self.assertEquals(resp.status_code, 405) diff --git a/src/testapp/urls.py b/src/testapp/urls.py new file mode 100644 index 000000000..a41c156bc --- /dev/null +++ b/src/testapp/urls.py @@ -0,0 +1,8 @@ +from django.conf.urls.defaults import patterns +from testapp.views import ReadOnlyResource, MirroringWriteResource + + +urlpatterns = patterns('', + (r'^read-only$', ReadOnlyResource), + (r'^mirroring-write$', MirroringWriteResource), +) diff --git a/src/testapp/views.py b/src/testapp/views.py new file mode 100644 index 000000000..f0174414b --- /dev/null +++ b/src/testapp/views.py @@ -0,0 +1,21 @@ +from decimal import Decimal +from rest.resource import Resource + +class ReadOnlyResource(Resource): + """This is my docstring + """ + allowed_methods = ('GET',) + + def read(self, headers={}, *args, **kwargs): + return (200, {'ExampleString': 'Example', + 'ExampleInt': 1, + 'ExampleDecimal': 1.0}, {}) + + +class MirroringWriteResource(Resource): + """This is my docstring + """ + allowed_methods = ('PUT',) + + def create(self, data, headers={}, *args, **kwargs): + return (200, data, {}) diff --git a/src/testarchive/tests.py b/src/testarchive/tests.py deleted file mode 100644 index 2aaf1bab8..000000000 --- a/src/testarchive/tests.py +++ /dev/null @@ -1,23 +0,0 @@ -""" -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, 3) - -__test__ = {"doctest": """ -Another way to test that 1 + 1 is equal to 2. - ->>> 1 + 1 == 2 -True -"""} - diff --git a/src/testarchive/urls.py b/src/testarchive/urls.py deleted file mode 100644 index 81aa0fa83..000000000 --- a/src/testarchive/urls.py +++ /dev/null @@ -1,7 +0,0 @@ -from django.conf.urls.defaults import patterns -from testarchive.views import RootResource - - -urlpatterns = patterns('', - (r'^$', RootResource), -) diff --git a/src/testarchive/views.py b/src/testarchive/views.py deleted file mode 100644 index a7812dc34..000000000 --- a/src/testarchive/views.py +++ /dev/null @@ -1,8 +0,0 @@ -from rest.resource import Resource - -class RootResource(Resource): - """This is my docstring - """ - - def handle_get(self, headers={}, *args, **kwargs): - return (200, {'Name': 'Test', 'Value': 1}, {'Location': 'BLAH'}) diff --git a/src/urls.py b/src/urls.py index f6c598ce6..f95e9afa4 100644 --- a/src/urls.py +++ b/src/urls.py @@ -5,7 +5,7 @@ admin.autodiscover() urlpatterns = patterns('', # Example: - (r'^testarchive/', include('testarchive.urls')), + (r'^testapp/', include('testapp.urls')), # Uncomment the admin/doc line below to enable admin documentation: (r'^admin/doc/', include('django.contrib.admindocs.urls')),