diff --git a/djangorestframework/mixins.py b/djangorestframework/mixins.py index bb26ad963..a34b92303 100644 --- a/djangorestframework/mixins.py +++ b/djangorestframework/mixins.py @@ -452,7 +452,10 @@ class ResourceMixin(object): return self._resource.filter_response(obj) def get_bound_form(self, content=None, method=None): - return self._resource.get_bound_form(content, method=method) + if hasattr(self._resource, 'get_bound_form'): + return self._resource.get_bound_form(content, method=method) + else: + return None @@ -566,7 +569,7 @@ class UpdateModelMixin(object): # TODO: update on the url of a non-existing resource url doesn't work correctly at the moment - will end up with a new url try: if args: - # If we have any none kwargs then assume the last represents the primrary key + # If we have any none kwargs then assume the last represents the primary key self.model_instance = model.objects.get(pk=args[-1], **kwargs) else: # Otherwise assume the kwargs uniquely identify the model diff --git a/djangorestframework/tests/renderers.py b/djangorestframework/tests/renderers.py index d2046212f..e7091c69a 100644 --- a/djangorestframework/tests/renderers.py +++ b/djangorestframework/tests/renderers.py @@ -223,4 +223,4 @@ if YAMLRenderer: content = renderer.render(obj, 'application/yaml') (data, files) = parser.parse(StringIO(content)) - self.assertEquals(obj, data) \ No newline at end of file + self.assertEquals(obj, data) diff --git a/djangorestframework/tests/views.py b/djangorestframework/tests/views.py index 598712d2e..b0f9d6d47 100644 --- a/djangorestframework/tests/views.py +++ b/djangorestframework/tests/views.py @@ -1,17 +1,109 @@ from django.conf.urls.defaults import patterns, url from django.test import TestCase from django.test import Client +from django import forms +from django.db import models +from djangorestframework.views import View +from djangorestframework.parsers import JSONParser +from djangorestframework.resources import ModelResource +from djangorestframework.views import ListOrCreateModelView, InstanceModelView + +from StringIO import StringIO + + +class MockView(View): + """This is a basic mock view""" + pass + +class ResourceMockView(View): + """This is a resource-based mock view""" + + class MockForm(forms.Form): + foo = forms.BooleanField(required=False) + bar = forms.IntegerField(help_text='Must be an integer.') + baz = forms.CharField(max_length=32) + + form = MockForm + +class MockResource(ModelResource): + """This is a mock model-based resource""" + + class MockResourceModel(models.Model): + foo = models.BooleanField() + bar = models.IntegerField(help_text='Must be an integer.') + baz = models.CharField(max_length=32, help_text='Free text. Max length 32 chars.') + + model = MockResourceModel + fields = ('foo', 'bar', 'baz') urlpatterns = patterns('djangorestframework.utils.staticviews', url(r'^robots.txt$', 'deny_robots'), url(r'^favicon.ico$', 'favicon'), url(r'^accounts/login$', 'api_login'), url(r'^accounts/logout$', 'api_logout'), + url(r'^mock/$', MockView.as_view()), + url(r'^resourcemock/$', ResourceMockView.as_view()), + url(r'^model/$', ListOrCreateModelView.as_view(resource=MockResource)), + url(r'^model/(?P[^/]+)/$', InstanceModelView.as_view(resource=MockResource)), ) +class BaseViewTests(TestCase): + """Test the base view class of djangorestframework""" + urls = 'djangorestframework.tests.views' + + def test_options_method_simple_view(self): + response = self.client.options('/mock/') + self._verify_options_response(response, + name='Mock', + description='This is a basic mock view') + + def test_options_method_resource_view(self): + response = self.client.options('/resourcemock/') + self._verify_options_response(response, + name='Resource Mock', + description='This is a resource-based mock view', + fields={'foo':'BooleanField', + 'bar':'IntegerField', + 'baz':'CharField', + }) + + def test_options_method_model_resource_list_view(self): + response = self.client.options('/model/') + self._verify_options_response(response, + name='Mock List', + description='This is a mock model-based resource', + fields={'foo':'BooleanField', + 'bar':'IntegerField', + 'baz':'CharField', + }) -class ViewTests(TestCase): + def test_options_method_model_resource_detail_view(self): + response = self.client.options('/model/0/') + self._verify_options_response(response, + name='Mock Instance', + description='This is a mock model-based resource', + fields={'foo':'BooleanField', + 'bar':'IntegerField', + 'baz':'CharField', + }) + + def _verify_options_response(self, response, name, description, fields=None, status=200, + mime_type='application/json'): + self.assertEqual(response.status_code, status) + self.assertEqual(response['Content-Type'].split(';')[0], mime_type) + parser = JSONParser(None) + (data, files) = parser.parse(StringIO(response.content)) + self.assertTrue('application/json' in data['renders']) + self.assertEqual(name, data['name']) + self.assertEqual(description, data['description']) + if fields is None: + self.assertFalse(hasattr(data, 'fields')) + else: + self.assertEqual(data['fields'], fields) + + +class ExtraViewsTests(TestCase): """Test the extra views djangorestframework provides""" urls = 'djangorestframework.tests.views' @@ -39,5 +131,5 @@ class ViewTests(TestCase): self.assertEqual(response.status_code, 200) self.assertEqual(response['Content-Type'].split(';')[0], 'text/html') - # TODO: Add login/logout behaviour tests + diff --git a/djangorestframework/views.py b/djangorestframework/views.py index 5f8e84cd2..ffb389d9a 100644 --- a/djangorestframework/views.py +++ b/djangorestframework/views.py @@ -13,6 +13,7 @@ from djangorestframework.compat import View as DjangoView from djangorestframework.response import Response, ErrorResponse from djangorestframework.mixins import * from djangorestframework import resources, renderers, parsers, authentication, permissions, status +from djangorestframework.utils.description import get_name, get_description __all__ = ( @@ -140,8 +141,13 @@ class View(ResourceMixin, RequestMixin, ResponseMixin, AuthMixin, DjangoView): else: response = Response(status.HTTP_204_NO_CONTENT) - # Pre-serialize filtering (eg filter complex objects into natively serializable types) - response.cleaned_content = self.filter_response(response.raw_content) + if request.method == 'OPTIONS': + # do not filter the response for HTTP OPTIONS, else the response fields are lost, + # as they do not correspond with model fields + response.cleaned_content = response.raw_content + else: + # Pre-serialize filtering (eg filter complex objects into natively serializable types) + response.cleaned_content = self.filter_response(response.raw_content) except ErrorResponse, exc: response = exc.response @@ -156,7 +162,23 @@ class View(ResourceMixin, RequestMixin, ResponseMixin, AuthMixin, DjangoView): # merge with headers possibly set at some point in the view response.headers.update(self.headers) - return self.render(response) + return self.render(response) + + + def options(self, request, *args, **kwargs): + response_obj = { + 'name' : get_name(self), + 'description' : get_description(self), + 'renders': self._rendered_media_types, + 'parses': self._parsed_media_types, + } + form = self.get_bound_form() + if form is not None: + field_name_types = {} + for name, field in form.fields.iteritems(): + field_name_types[name] = field.__class__.__name__ + response_obj['fields'] = field_name_types + return response_obj class ModelView(View):