diff options
| -rw-r--r-- | AUTHORS | 6 | ||||
| -rw-r--r-- | djangorestframework/compat.py | 12 | ||||
| -rw-r--r-- | djangorestframework/mixins.py | 25 | ||||
| -rw-r--r-- | djangorestframework/renderers.py | 17 | ||||
| -rw-r--r-- | djangorestframework/resources.py | 6 | ||||
| -rw-r--r-- | djangorestframework/response.py | 4 | ||||
| -rw-r--r-- | djangorestframework/runtests/settings.py | 16 | ||||
| -rw-r--r-- | djangorestframework/templates/renderer.html | 8 | ||||
| -rw-r--r-- | djangorestframework/tests/content.py | 10 | ||||
| -rw-r--r-- | djangorestframework/tests/methods.py | 6 | ||||
| -rw-r--r-- | djangorestframework/tests/oauthentication.py | 399 | ||||
| -rw-r--r-- | djangorestframework/tests/renderers.py | 64 | ||||
| -rw-r--r-- | djangorestframework/tests/reverse.py | 6 | ||||
| -rw-r--r-- | examples/requirements.txt | 8 |
14 files changed, 353 insertions, 234 deletions
@@ -1,11 +1,13 @@ -Tom Christie <tomchristie> - tom@tomchristie.com, @thisneonsoul - +Tom Christie <tomchristie> - tom@tomchristie.com, @thisneonsoul - Author. Paul Bagwell <pbgwl> - Suggestions & bugfixes. Marko Tibold <markotibold> - Contributions & Providing the Jenkins CI Server. Sébastien Piquemal <sebpiq> - Contributions. Carmen Wick <cwick> - Bugfixes. Alex Ehlke <aehlke> - Design Contributions. Alen Mujezinovic <flashingpumpkin> - Contributions. +Carles Barrobés <txels> - HEAD support. +Michael Fötsch <mfoetsch> - File format support. +David Larlet <david> - OAuth support. THANKS TO: diff --git a/djangorestframework/compat.py b/djangorestframework/compat.py index 0274511a..827b4adf 100644 --- a/djangorestframework/compat.py +++ b/djangorestframework/compat.py @@ -67,6 +67,14 @@ except ImportError: # django.views.generic.View (Django >= 1.3) try: from django.views.generic import View + if not hasattr(View, 'head'): + # First implementation of Django class-based views did not include head method + # in base View class - https://code.djangoproject.com/ticket/15668 + class ViewPlusHead(View): + def head(self, request, *args, **kwargs): + return self.get(request, *args, **kwargs) + View = ViewPlusHead + except ImportError: from django import http from django.utils.functional import update_wrapper @@ -145,6 +153,8 @@ except ImportError: #) return http.HttpResponseNotAllowed(allowed_methods) + def head(self, request, *args, **kwargs): + return self.get(request, *args, **kwargs) try: import markdown @@ -193,4 +203,4 @@ try: return md.convert(text) except ImportError: - apply_markdown = None
\ No newline at end of file + apply_markdown = None diff --git a/djangorestframework/mixins.py b/djangorestframework/mixins.py index 910d06ae..1b3aa241 100644 --- a/djangorestframework/mixins.py +++ b/djangorestframework/mixins.py @@ -11,6 +11,7 @@ from django.http.multipartparser import LimitBytes from djangorestframework import status from djangorestframework.parsers import FormParser, MultiPartParser +from djangorestframework.renderers import BaseRenderer from djangorestframework.resources import Resource, FormResource, ModelResource from djangorestframework.response import Response, ErrorResponse from djangorestframework.utils import as_tuple, MSIE_USER_AGENT_REGEX @@ -290,7 +291,7 @@ class ResponseMixin(object): accept_list = [token.strip() for token in request.META["HTTP_ACCEPT"].split(',')] else: # No accept header specified - return (self._default_renderer(self), self._default_renderer.media_type) + accept_list = ['*/*'] # Check the acceptable media types against each renderer, # attempting more specific media types first @@ -298,12 +299,12 @@ class ResponseMixin(object): # Worst case is we're looping over len(accept_list) * len(self.renderers) renderers = [renderer_cls(self) for renderer_cls in self.renderers] - for media_type_lst in order_by_precedence(accept_list): + for accepted_media_type_lst in order_by_precedence(accept_list): for renderer in renderers: - for media_type in media_type_lst: - if renderer.can_handle_response(media_type): - return renderer, media_type - + for accepted_media_type in accepted_media_type_lst: + if renderer.can_handle_response(accepted_media_type): + return renderer, accepted_media_type + # No acceptable renderers were found raise ErrorResponse(status.HTTP_406_NOT_ACCEPTABLE, {'detail': 'Could not satisfy the client\'s Accept header', @@ -316,6 +317,13 @@ class ResponseMixin(object): Return an list of all the media types that this view can render. """ return [renderer.media_type for renderer in self.renderers] + + @property + def _rendered_formats(self): + """ + Return a list of all the formats that this view can render. + """ + return [renderer.format for renderer in self.renderers] @property def _default_renderer(self): @@ -486,7 +494,10 @@ class ReadModelMixin(object): instance = model.objects.get(pk=args[-1], **kwargs) else: # Otherwise assume the kwargs uniquely identify the model - instance = model.objects.get(**kwargs) + filtered_keywords = kwargs.copy() + if BaseRenderer._FORMAT_QUERY_PARAM in filtered_keywords: + del filtered_keywords[BaseRenderer._FORMAT_QUERY_PARAM] + instance = model.objects.get(**filtered_keywords) except model.DoesNotExist: raise ErrorResponse(status.HTTP_404_NOT_FOUND) diff --git a/djangorestframework/renderers.py b/djangorestframework/renderers.py index 13cd52f5..e09e2abc 100644 --- a/djangorestframework/renderers.py +++ b/djangorestframework/renderers.py @@ -40,8 +40,11 @@ class BaseRenderer(object): All renderers must extend this class, set the :attr:`media_type` attribute, and override the :meth:`render` method. """ + + _FORMAT_QUERY_PARAM = 'format' media_type = None + format = None def __init__(self, view): self.view = view @@ -58,6 +61,11 @@ class BaseRenderer(object): This may be overridden to provide for other behavior, but typically you'll instead want to just set the :attr:`media_type` attribute on the class. """ + format = self.view.kwargs.get(self._FORMAT_QUERY_PARAM, None) + if format is None: + format = self.view.request.GET.get(self._FORMAT_QUERY_PARAM, None) + if format is not None: + return format == self.format return media_type_matches(self.media_type, accept) def render(self, obj=None, media_type=None): @@ -84,6 +92,7 @@ class JSONRenderer(BaseRenderer): """ media_type = 'application/json' + format = 'json' def render(self, obj=None, media_type=None): """ @@ -111,6 +120,7 @@ class XMLRenderer(BaseRenderer): """ media_type = 'application/xml' + format = 'xml' def render(self, obj=None, media_type=None): """ @@ -289,12 +299,12 @@ class DocumentingTemplateRenderer(BaseRenderer): 'version': VERSION, 'markeddown': markeddown, 'breadcrumblist': breadcrumb_list, - 'available_media_types': self.view._rendered_media_types, + 'available_formats': self.view._rendered_formats, 'put_form': put_form_instance, 'post_form': post_form_instance, 'login_url': login_url, 'logout_url': logout_url, - 'ACCEPT_PARAM': getattr(self.view, '_ACCEPT_QUERY_PARAM', None), + 'FORMAT_PARAM': self._FORMAT_QUERY_PARAM, 'METHOD_PARAM': getattr(self.view, '_METHOD_PARAM', None), 'ADMIN_MEDIA_PREFIX': settings.ADMIN_MEDIA_PREFIX }) @@ -317,6 +327,7 @@ class DocumentingHTMLRenderer(DocumentingTemplateRenderer): """ media_type = 'text/html' + format = 'html' template = 'renderer.html' @@ -328,6 +339,7 @@ class DocumentingXHTMLRenderer(DocumentingTemplateRenderer): """ media_type = 'application/xhtml+xml' + format = 'xhtml' template = 'renderer.html' @@ -339,6 +351,7 @@ class DocumentingPlainTextRenderer(DocumentingTemplateRenderer): """ media_type = 'text/plain' + format = 'txt' template = 'renderer.txt' diff --git a/djangorestframework/resources.py b/djangorestframework/resources.py index 08f9e0ae..b42bd952 100644 --- a/djangorestframework/resources.py +++ b/djangorestframework/resources.py @@ -6,7 +6,7 @@ from django.db.models.fields.related import RelatedField from django.utils.encoding import smart_unicode from djangorestframework.response import ErrorResponse -from djangorestframework.serializer import Serializer +from djangorestframework.serializer import Serializer, _SkipField from djangorestframework.utils import as_tuple import decimal @@ -342,7 +342,7 @@ class ModelResource(FormResource): """ if not hasattr(self, 'view_callable'): - raise NoReverseMatch + raise _SkipField # dis does teh magicks... urlconf = get_urlconf() @@ -371,7 +371,7 @@ class ModelResource(FormResource): return reverse(self.view_callable[0], kwargs=instance_attrs) except NoReverseMatch: pass - raise NoReverseMatch + raise _SkipField @property diff --git a/djangorestframework/response.py b/djangorestframework/response.py index d68ececf..311e0bb7 100644 --- a/djangorestframework/response.py +++ b/djangorestframework/response.py @@ -16,13 +16,13 @@ class Response(object): An HttpResponse that may include content that hasn't yet been serialized. """ - def __init__(self, status=200, content=None, headers={}): + def __init__(self, status=200, content=None, headers=None): self.status = status self.media_type = None self.has_content_body = content is not None self.raw_content = content # content prior to filtering self.cleaned_content = content # content after filtering - self.headers = headers + self.headers = headers or {} @property def status_text(self): diff --git a/djangorestframework/runtests/settings.py b/djangorestframework/runtests/settings.py index 1b63256c..006727bc 100644 --- a/djangorestframework/runtests/settings.py +++ b/djangorestframework/runtests/settings.py @@ -2,6 +2,7 @@ DEBUG = True TEMPLATE_DEBUG = DEBUG +DEBUG_PROPAGATE_EXCEPTIONS = True ADMINS = ( # ('Your Name', 'your_email@domain.com'), @@ -83,7 +84,7 @@ TEMPLATE_DIRS = ( # Don't forget to use absolute paths, not relative paths. ) -INSTALLED_APPS = ( +INSTALLED_APPS = [ 'django.contrib.auth', 'django.contrib.contenttypes', 'django.contrib.sessions', @@ -94,10 +95,17 @@ INSTALLED_APPS = ( # Uncomment the next line to enable admin documentation: # 'django.contrib.admindocs', 'djangorestframework', - - 'oauth_provider', -) +] + +# OAuth support is optional, so we only test oauth if it's installed. +try: + import oauth_provider +except: + pass +else: + INSTALLED_APPS.append('oauth_provider') +# If we're running on the Jenkins server we want to archive the coverage reports as XML. import os if os.environ.get('HUDSON_URL', None): TEST_RUNNER = 'xmlrunner.extra.djangotestrunner.XMLTestRunner' diff --git a/djangorestframework/templates/renderer.html b/djangorestframework/templates/renderer.html index 97d3837a..5b32d1ec 100644 --- a/djangorestframework/templates/renderer.html +++ b/djangorestframework/templates/renderer.html @@ -48,9 +48,9 @@ <h2>GET {{ name }}</h2> <div class='submit-row' style='margin: 0; border: 0'> <a href='{{ request.get_full_path }}' rel="nofollow" style='float: left'>GET</a> - {% for media_type in available_media_types %} - {% with ACCEPT_PARAM|add:"="|add:media_type as param %} - [<a href='{{ request.get_full_path|add_query_param:param }}' rel="nofollow">{{ media_type }}</a>] + {% for format in available_formats %} + {% with FORMAT_PARAM|add:"="|add:format as param %} + [<a href='{{ request.get_full_path|add_query_param:param }}' rel="nofollow">{{ format }}</a>] {% endwith %} {% endfor %} </div> @@ -122,4 +122,4 @@ </div> </div> </body> -</html>
\ No newline at end of file +</html> diff --git a/djangorestframework/tests/content.py b/djangorestframework/tests/content.py index ee3597a4..83ad72d0 100644 --- a/djangorestframework/tests/content.py +++ b/djangorestframework/tests/content.py @@ -6,7 +6,6 @@ from djangorestframework.compat import RequestFactory from djangorestframework.mixins import RequestMixin from djangorestframework.parsers import FormParser, MultiPartParser, PlainTextParser - class TestContentParsing(TestCase): def setUp(self): self.req = RequestFactory() @@ -16,6 +15,11 @@ class TestContentParsing(TestCase): view.request = self.req.get('/') self.assertEqual(view.DATA, None) + def ensure_determines_no_content_HEAD(self, view): + """Ensure view.DATA returns None for HEAD request.""" + view.request = self.req.head('/') + self.assertEqual(view.DATA, None) + def ensure_determines_form_content_POST(self, view): """Ensure view.DATA returns content for POST request with form content.""" form_data = {'qwerty': 'uiop'} @@ -50,6 +54,10 @@ class TestContentParsing(TestCase): """Ensure view.DATA returns None for GET request with no content.""" self.ensure_determines_no_content_GET(RequestMixin()) + def test_standard_behaviour_determines_no_content_HEAD(self): + """Ensure view.DATA returns None for HEAD request.""" + self.ensure_determines_no_content_HEAD(RequestMixin()) + def test_standard_behaviour_determines_form_content_POST(self): """Ensure view.DATA returns content for POST request with form content.""" self.ensure_determines_form_content_POST(RequestMixin()) diff --git a/djangorestframework/tests/methods.py b/djangorestframework/tests/methods.py index d8f0d919..c3a3a28d 100644 --- a/djangorestframework/tests/methods.py +++ b/djangorestframework/tests/methods.py @@ -24,3 +24,9 @@ class TestMethodOverloading(TestCase): view = RequestMixin() view.request = self.req.post('/', {view._METHOD_PARAM: 'DELETE'}) self.assertEqual(view.method, 'DELETE') + + def test_HEAD_is_a_valid_method(self): + """HEAD requests identified""" + view = RequestMixin() + view.request = self.req.head('/') + self.assertEqual(view.method, 'HEAD') diff --git a/djangorestframework/tests/oauthentication.py b/djangorestframework/tests/oauthentication.py index 0604ddad..7f74b804 100644 --- a/djangorestframework/tests/oauthentication.py +++ b/djangorestframework/tests/oauthentication.py @@ -6,198 +6,207 @@ from django.test import Client, TestCase from djangorestframework.views import View -import oauth2 as oauth -from oauth_provider.decorators import oauth_required -from oauth_provider.models import Resource, Consumer, Token - - -class ClientView(View): - def get(self, request): - return {'resource': 'Protected!'} - -urlpatterns = patterns('', - url(r'^$', oauth_required(ClientView.as_view())), - url(r'^oauth/', include('oauth_provider.urls')), - url(r'^accounts/login/$', 'djangorestframework.utils.staticviews.api_login'), -) - - -class OAuthTests(TestCase): - """ - OAuth authentication: - * the user would like to access his API data from a third-party website - * the third-party website proposes a link to get that API data - * the user is redirected to the API and must log in if not authenticated - * the API displays a webpage to confirm that the user trusts the third-party website - * if confirmed, the user is redirected to the third-party website through the callback view - * the third-party website is able to retrieve data from the API - """ - urls = 'djangorestframework.tests.oauthentication' - - def setUp(self): - self.client = Client() - self.username = 'john' - self.email = 'lennon@thebeatles.com' - self.password = 'password' - self.user = User.objects.create_user(self.username, self.email, self.password) - - # OAuth requirements - self.resource = Resource(name='data', url='/') - self.resource.save() - self.CONSUMER_KEY = 'dpf43f3p2l4k3l03' - self.CONSUMER_SECRET = 'kd94hf93k423kf44' - self.consumer = Consumer(key=self.CONSUMER_KEY, secret=self.CONSUMER_SECRET, - name='api.example.com', user=self.user) - self.consumer.save() - - def test_oauth_invalid_and_anonymous_access(self): - """ - Verify that the resource is protected and the OAuth authorization view - require the user to be logged in. - """ - response = self.client.get('/') - self.assertEqual(response.content, 'Invalid request parameters.') - self.assertEqual(response.status_code, 401) - response = self.client.get('/oauth/authorize/', follow=True) - self.assertRedirects(response, '/accounts/login/?next=/oauth/authorize/') - - def test_oauth_authorize_access(self): - """ - Verify that once logged in, the user can access the authorization page - but can't display the page because the request token is not specified. - """ - self.client.login(username=self.username, password=self.password) - response = self.client.get('/oauth/authorize/', follow=True) - self.assertEqual(response.content, 'No request token specified.') - - def _create_request_token_parameters(self): - """ - A shortcut to create request's token parameters. - """ - return { - 'oauth_consumer_key': self.CONSUMER_KEY, - 'oauth_signature_method': 'PLAINTEXT', - 'oauth_signature': '%s&' % self.CONSUMER_SECRET, - 'oauth_timestamp': str(int(time.time())), - 'oauth_nonce': 'requestnonce', - 'oauth_version': '1.0', - 'oauth_callback': 'http://api.example.com/request_token_ready', - 'scope': 'data', - } - - def test_oauth_request_token_retrieval(self): - """ - Verify that the request token can be retrieved by the server. - """ - response = self.client.get("/oauth/request_token/", - self._create_request_token_parameters()) - self.assertEqual(response.status_code, 200) - token = list(Token.objects.all())[-1] - self.failIf(token.key not in response.content) - self.failIf(token.secret not in response.content) - - def test_oauth_user_request_authorization(self): - """ - Verify that the user can access the authorization page once logged in - and the request token has been retrieved. - """ - # Setup - response = self.client.get("/oauth/request_token/", - self._create_request_token_parameters()) - token = list(Token.objects.all())[-1] - - # Starting the test here - self.client.login(username=self.username, password=self.password) - parameters = {'oauth_token': token.key,} - response = self.client.get("/oauth/authorize/", parameters) - self.assertEqual(response.status_code, 200) - self.failIf(not response.content.startswith('Fake authorize view for api.example.com with params: oauth_token=')) - self.assertEqual(token.is_approved, 0) - parameters['authorize_access'] = 1 # fake authorization by the user - response = self.client.post("/oauth/authorize/", parameters) - self.assertEqual(response.status_code, 302) - self.failIf(not response['Location'].startswith('http://api.example.com/request_token_ready?oauth_verifier=')) - token = Token.objects.get(key=token.key) - self.failIf(token.key not in response['Location']) - self.assertEqual(token.is_approved, 1) - - def _create_access_token_parameters(self, token): - """ - A shortcut to create access' token parameters. - """ - return { - 'oauth_consumer_key': self.CONSUMER_KEY, - 'oauth_token': token.key, - 'oauth_signature_method': 'PLAINTEXT', - 'oauth_signature': '%s&%s' % (self.CONSUMER_SECRET, token.secret), - 'oauth_timestamp': str(int(time.time())), - 'oauth_nonce': 'accessnonce', - 'oauth_version': '1.0', - 'oauth_verifier': token.verifier, - 'scope': 'data', - } - - def test_oauth_access_token_retrieval(self): - """ - Verify that the request token can be retrieved by the server. - """ - # Setup - response = self.client.get("/oauth/request_token/", - self._create_request_token_parameters()) - token = list(Token.objects.all())[-1] - self.client.login(username=self.username, password=self.password) - parameters = {'oauth_token': token.key,} - response = self.client.get("/oauth/authorize/", parameters) - parameters['authorize_access'] = 1 # fake authorization by the user - response = self.client.post("/oauth/authorize/", parameters) - token = Token.objects.get(key=token.key) - - # Starting the test here - response = self.client.get("/oauth/access_token/", self._create_access_token_parameters(token)) - self.assertEqual(response.status_code, 200) - self.failIf(not response.content.startswith('oauth_token_secret=')) - access_token = list(Token.objects.filter(token_type=Token.ACCESS))[-1] - self.failIf(access_token.key not in response.content) - self.failIf(access_token.secret not in response.content) - self.assertEqual(access_token.user.username, 'john') - - def _create_access_parameters(self, access_token): - """ - A shortcut to create access' parameters. - """ - parameters = { - 'oauth_consumer_key': self.CONSUMER_KEY, - 'oauth_token': access_token.key, - 'oauth_signature_method': 'HMAC-SHA1', - 'oauth_timestamp': str(int(time.time())), - 'oauth_nonce': 'accessresourcenonce', - 'oauth_version': '1.0', - } - oauth_request = oauth.Request.from_token_and_callback(access_token, - http_url='http://testserver/', parameters=parameters) - signature_method = oauth.SignatureMethod_HMAC_SHA1() - signature = signature_method.sign(oauth_request, self.consumer, access_token) - parameters['oauth_signature'] = signature - return parameters - - def test_oauth_protected_resource_access(self): - """ - Verify that the request token can be retrieved by the server. - """ - # Setup - response = self.client.get("/oauth/request_token/", - self._create_request_token_parameters()) - token = list(Token.objects.all())[-1] - self.client.login(username=self.username, password=self.password) - parameters = {'oauth_token': token.key,} - response = self.client.get("/oauth/authorize/", parameters) - parameters['authorize_access'] = 1 # fake authorization by the user - response = self.client.post("/oauth/authorize/", parameters) - token = Token.objects.get(key=token.key) - response = self.client.get("/oauth/access_token/", self._create_access_token_parameters(token)) - access_token = list(Token.objects.filter(token_type=Token.ACCESS))[-1] - - # Starting the test here - response = self.client.get("/", self._create_access_token_parameters(access_token)) - self.assertEqual(response.status_code, 200) - self.assertEqual(response.content, '{"resource": "Protected!"}') +# Since oauth2 / django-oauth-plus are optional dependancies, we don't want to +# always run these tests. + +# Unfortunatly we can't skip tests easily until 2.7, se we'll just do this for now. +try: + import oauth2 as oauth + from oauth_provider.decorators import oauth_required + from oauth_provider.models import Resource, Consumer, Token + +except: + pass + +else: + # Alrighty, we're good to go here. + class ClientView(View): + def get(self, request): + return {'resource': 'Protected!'} + + urlpatterns = patterns('', + url(r'^$', oauth_required(ClientView.as_view())), + url(r'^oauth/', include('oauth_provider.urls')), + url(r'^accounts/login/$', 'djangorestframework.utils.staticviews.api_login'), + ) + + + class OAuthTests(TestCase): + """ + OAuth authentication: + * the user would like to access his API data from a third-party website + * the third-party website proposes a link to get that API data + * the user is redirected to the API and must log in if not authenticated + * the API displays a webpage to confirm that the user trusts the third-party website + * if confirmed, the user is redirected to the third-party website through the callback view + * the third-party website is able to retrieve data from the API + """ + urls = 'djangorestframework.tests.oauthentication' + + def setUp(self): + self.client = Client() + self.username = 'john' + self.email = 'lennon@thebeatles.com' + self.password = 'password' + self.user = User.objects.create_user(self.username, self.email, self.password) + + # OAuth requirements + self.resource = Resource(name='data', url='/') + self.resource.save() + self.CONSUMER_KEY = 'dpf43f3p2l4k3l03' + self.CONSUMER_SECRET = 'kd94hf93k423kf44' + self.consumer = Consumer(key=self.CONSUMER_KEY, secret=self.CONSUMER_SECRET, + name='api.example.com', user=self.user) + self.consumer.save() + + def test_oauth_invalid_and_anonymous_access(self): + """ + Verify that the resource is protected and the OAuth authorization view + require the user to be logged in. + """ + response = self.client.get('/') + self.assertEqual(response.content, 'Invalid request parameters.') + self.assertEqual(response.status_code, 401) + response = self.client.get('/oauth/authorize/', follow=True) + self.assertRedirects(response, '/accounts/login/?next=/oauth/authorize/') + + def test_oauth_authorize_access(self): + """ + Verify that once logged in, the user can access the authorization page + but can't display the page because the request token is not specified. + """ + self.client.login(username=self.username, password=self.password) + response = self.client.get('/oauth/authorize/', follow=True) + self.assertEqual(response.content, 'No request token specified.') + + def _create_request_token_parameters(self): + """ + A shortcut to create request's token parameters. + """ + return { + 'oauth_consumer_key': self.CONSUMER_KEY, + 'oauth_signature_method': 'PLAINTEXT', + 'oauth_signature': '%s&' % self.CONSUMER_SECRET, + 'oauth_timestamp': str(int(time.time())), + 'oauth_nonce': 'requestnonce', + 'oauth_version': '1.0', + 'oauth_callback': 'http://api.example.com/request_token_ready', + 'scope': 'data', + } + + def test_oauth_request_token_retrieval(self): + """ + Verify that the request token can be retrieved by the server. + """ + response = self.client.get("/oauth/request_token/", + self._create_request_token_parameters()) + self.assertEqual(response.status_code, 200) + token = list(Token.objects.all())[-1] + self.failIf(token.key not in response.content) + self.failIf(token.secret not in response.content) + + def test_oauth_user_request_authorization(self): + """ + Verify that the user can access the authorization page once logged in + and the request token has been retrieved. + """ + # Setup + response = self.client.get("/oauth/request_token/", + self._create_request_token_parameters()) + token = list(Token.objects.all())[-1] + + # Starting the test here + self.client.login(username=self.username, password=self.password) + parameters = {'oauth_token': token.key,} + response = self.client.get("/oauth/authorize/", parameters) + self.assertEqual(response.status_code, 200) + self.failIf(not response.content.startswith('Fake authorize view for api.example.com with params: oauth_token=')) + self.assertEqual(token.is_approved, 0) + parameters['authorize_access'] = 1 # fake authorization by the user + response = self.client.post("/oauth/authorize/", parameters) + self.assertEqual(response.status_code, 302) + self.failIf(not response['Location'].startswith('http://api.example.com/request_token_ready?oauth_verifier=')) + token = Token.objects.get(key=token.key) + self.failIf(token.key not in response['Location']) + self.assertEqual(token.is_approved, 1) + + def _create_access_token_parameters(self, token): + """ + A shortcut to create access' token parameters. + """ + return { + 'oauth_consumer_key': self.CONSUMER_KEY, + 'oauth_token': token.key, + 'oauth_signature_method': 'PLAINTEXT', + 'oauth_signature': '%s&%s' % (self.CONSUMER_SECRET, token.secret), + 'oauth_timestamp': str(int(time.time())), + 'oauth_nonce': 'accessnonce', + 'oauth_version': '1.0', + 'oauth_verifier': token.verifier, + 'scope': 'data', + } + + def test_oauth_access_token_retrieval(self): + """ + Verify that the request token can be retrieved by the server. + """ + # Setup + response = self.client.get("/oauth/request_token/", + self._create_request_token_parameters()) + token = list(Token.objects.all())[-1] + self.client.login(username=self.username, password=self.password) + parameters = {'oauth_token': token.key,} + response = self.client.get("/oauth/authorize/", parameters) + parameters['authorize_access'] = 1 # fake authorization by the user + response = self.client.post("/oauth/authorize/", parameters) + token = Token.objects.get(key=token.key) + + # Starting the test here + response = self.client.get("/oauth/access_token/", self._create_access_token_parameters(token)) + self.assertEqual(response.status_code, 200) + self.failIf(not response.content.startswith('oauth_token_secret=')) + access_token = list(Token.objects.filter(token_type=Token.ACCESS))[-1] + self.failIf(access_token.key not in response.content) + self.failIf(access_token.secret not in response.content) + self.assertEqual(access_token.user.username, 'john') + + def _create_access_parameters(self, access_token): + """ + A shortcut to create access' parameters. + """ + parameters = { + 'oauth_consumer_key': self.CONSUMER_KEY, + 'oauth_token': access_token.key, + 'oauth_signature_method': 'HMAC-SHA1', + 'oauth_timestamp': str(int(time.time())), + 'oauth_nonce': 'accessresourcenonce', + 'oauth_version': '1.0', + } + oauth_request = oauth.Request.from_token_and_callback(access_token, + http_url='http://testserver/', parameters=parameters) + signature_method = oauth.SignatureMethod_HMAC_SHA1() + signature = signature_method.sign(oauth_request, self.consumer, access_token) + parameters['oauth_signature'] = signature + return parameters + + def test_oauth_protected_resource_access(self): + """ + Verify that the request token can be retrieved by the server. + """ + # Setup + response = self.client.get("/oauth/request_token/", + self._create_request_token_parameters()) + token = list(Token.objects.all())[-1] + self.client.login(username=self.username, password=self.password) + parameters = {'oauth_token': token.key,} + response = self.client.get("/oauth/authorize/", parameters) + parameters['authorize_access'] = 1 # fake authorization by the user + response = self.client.post("/oauth/authorize/", parameters) + token = Token.objects.get(key=token.key) + response = self.client.get("/oauth/access_token/", self._create_access_token_parameters(token)) + access_token = list(Token.objects.filter(token_type=Token.ACCESS))[-1] + + # Starting the test here + response = self.client.get("/", self._create_access_token_parameters(access_token)) + self.assertEqual(response.status_code, 200) + self.assertEqual(response.content, '{"resource": "Protected!"}') diff --git a/djangorestframework/tests/renderers.py b/djangorestframework/tests/renderers.py index d403873f..bf135e55 100644 --- a/djangorestframework/tests/renderers.py +++ b/djangorestframework/tests/renderers.py @@ -2,6 +2,7 @@ from django.conf.urls.defaults import patterns, url from django import http from django.test import TestCase +from djangorestframework import status from djangorestframework.compat import View as DjangoView from djangorestframework.renderers import BaseRenderer, JSONRenderer from djangorestframework.parsers import JSONParser @@ -11,7 +12,7 @@ from djangorestframework.utils.mediatypes import add_media_type_param from StringIO import StringIO -DUMMYSTATUS = 200 +DUMMYSTATUS = status.HTTP_200_OK DUMMYCONTENT = 'dummycontent' RENDERER_A_SERIALIZER = lambda x: 'Renderer A: %s' % x @@ -19,12 +20,14 @@ RENDERER_B_SERIALIZER = lambda x: 'Renderer B: %s' % x class RendererA(BaseRenderer): media_type = 'mock/renderera' + format="formata" def render(self, obj=None, media_type=None): return RENDERER_A_SERIALIZER(obj) class RendererB(BaseRenderer): media_type = 'mock/rendererb' + format="formatb" def render(self, obj=None, media_type=None): return RENDERER_B_SERIALIZER(obj) @@ -32,11 +35,13 @@ class RendererB(BaseRenderer): class MockView(ResponseMixin, DjangoView): renderers = (RendererA, RendererB) - def get(self, request): + def get(self, request, **kwargs): response = Response(DUMMYSTATUS, DUMMYCONTENT) return self.render(response) + urlpatterns = patterns('', + url(r'^.*\.(?P<format>.+)$', MockView.as_view(renderers=[RendererA, RendererB])), url(r'^$', MockView.as_view(renderers=[RendererA, RendererB])), ) @@ -55,6 +60,13 @@ class RendererIntegrationTests(TestCase): self.assertEquals(resp.content, RENDERER_A_SERIALIZER(DUMMYCONTENT)) self.assertEquals(resp.status_code, DUMMYSTATUS) + def test_head_method_serializes_no_content(self): + """No response must be included in HEAD requests.""" + resp = self.client.head('/') + self.assertEquals(resp.status_code, DUMMYSTATUS) + self.assertEquals(resp['Content-Type'], RendererA.media_type) + self.assertEquals(resp.content, '') + def test_default_renderer_serializes_content_on_accept_any(self): """If the Accept header is set to */* the default renderer should serialize the response.""" resp = self.client.get('/', HTTP_ACCEPT='*/*') @@ -78,12 +90,58 @@ class RendererIntegrationTests(TestCase): self.assertEquals(resp.content, RENDERER_B_SERIALIZER(DUMMYCONTENT)) self.assertEquals(resp.status_code, DUMMYSTATUS) + def test_specified_renderer_serializes_content_on_accept_query(self): + """The '_accept' query string should behave in the same way as the Accept header.""" + resp = self.client.get('/?_accept=%s' % RendererB.media_type) + self.assertEquals(resp['Content-Type'], RendererB.media_type) + self.assertEquals(resp.content, RENDERER_B_SERIALIZER(DUMMYCONTENT)) + self.assertEquals(resp.status_code, DUMMYSTATUS) + def test_unsatisfiable_accept_header_on_request_returns_406_status(self): """If the Accept header is unsatisfiable we should return a 406 Not Acceptable response.""" resp = self.client.get('/', HTTP_ACCEPT='foo/bar') - self.assertEquals(resp.status_code, 406) + self.assertEquals(resp.status_code, status.HTTP_406_NOT_ACCEPTABLE) + + def test_specified_renderer_serializes_content_on_format_query(self): + """If a 'format' query is specified, the renderer with the matching + format attribute should serialize the response.""" + resp = self.client.get('/?format=%s' % RendererB.format) + self.assertEquals(resp['Content-Type'], RendererB.media_type) + self.assertEquals(resp.content, RENDERER_B_SERIALIZER(DUMMYCONTENT)) + self.assertEquals(resp.status_code, DUMMYSTATUS) + + def test_specified_renderer_serializes_content_on_format_kwargs(self): + """If a 'format' keyword arg is specified, the renderer with the matching + format attribute should serialize the response.""" + resp = self.client.get('/something.formatb') + self.assertEquals(resp['Content-Type'], RendererB.media_type) + self.assertEquals(resp.content, RENDERER_B_SERIALIZER(DUMMYCONTENT)) + self.assertEquals(resp.status_code, DUMMYSTATUS) + def test_specified_renderer_is_used_on_format_query_with_matching_accept(self): + """If both a 'format' query and a matching Accept header specified, + the renderer with the matching format attribute should serialize the response.""" + resp = self.client.get('/?format=%s' % RendererB.format, + HTTP_ACCEPT=RendererB.media_type) + self.assertEquals(resp['Content-Type'], RendererB.media_type) + self.assertEquals(resp.content, RENDERER_B_SERIALIZER(DUMMYCONTENT)) + self.assertEquals(resp.status_code, DUMMYSTATUS) + def test_conflicting_format_query_and_accept_ignores_accept(self): + """If a 'format' query is specified that does not match the Accept + header, we should only honor the 'format' query string.""" + resp = self.client.get('/?format=%s' % RendererB.format, + HTTP_ACCEPT='dummy') + self.assertEquals(resp['Content-Type'], RendererB.media_type) + self.assertEquals(resp.content, RENDERER_B_SERIALIZER(DUMMYCONTENT)) + self.assertEquals(resp.status_code, DUMMYSTATUS) + + def test_bla(self): + resp = self.client.get('/?format=formatb', + HTTP_ACCEPT='text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8') + self.assertEquals(resp['Content-Type'], RendererB.media_type) + self.assertEquals(resp.content, RENDERER_B_SERIALIZER(DUMMYCONTENT)) + self.assertEquals(resp.status_code, DUMMYSTATUS) _flat_repr = '{"foo": ["bar", "baz"]}' diff --git a/djangorestframework/tests/reverse.py b/djangorestframework/tests/reverse.py index b4b0a793..2d1ca79e 100644 --- a/djangorestframework/tests/reverse.py +++ b/djangorestframework/tests/reverse.py @@ -24,9 +24,5 @@ class ReverseTests(TestCase): urls = 'djangorestframework.tests.reverse' def test_reversed_urls_are_fully_qualified(self): - try: - response = self.client.get('/') - except: - import traceback - traceback.print_exc() + response = self.client.get('/') self.assertEqual(json.loads(response.content), 'http://testserver/another') diff --git a/examples/requirements.txt b/examples/requirements.txt index dffe0d99..0bcd8d43 100644 --- a/examples/requirements.txt +++ b/examples/requirements.txt @@ -1,8 +1,6 @@ -# For the examples we need Django, pygments and httplib2... +# Pygments for the code highlighting example, +# markdown for the docstring -> auto-documentation -Django==1.2.4 -wsgiref==0.1.2 Pygments==1.4 -httplib2==0.6.0 Markdown==2.0.3 -django-oauth-plus==2.0.0 + |
