diff --git a/rest_framework/generics.py b/rest_framework/generics.py index dd8dfcf8d..69ae53ae9 100644 --- a/rest_framework/generics.py +++ b/rest_framework/generics.py @@ -213,3 +213,23 @@ class RetrieveUpdateDestroyAPIView(mixins.RetrieveModelMixin, def delete(self, request, *args, **kwargs): return self.destroy(request, *args, **kwargs) + +class RedirectAPIView(mixins.RedirectMixin, + GenericAPIView): + """ + Redirect View used to redirect requests to a different/moved endpoint + """ + def get(self, request, *args, **kwargs): + return self.redirect(request, *args, **kwargs) + + def post(self, request, *args, **kwargs): + return self.redirect(request, *args, **kwargs) + + def options(self, request, *args, **kwargs): + return self.redirect(request, *args, **kwargs) + + def delete(self, request, *args, **kwargs): + return self.redirect(request, *args, **kwargs) + + def put(self, request, *args, **kwargs): + return self.redirect(request, *args, **kwargs) \ No newline at end of file diff --git a/rest_framework/mixins.py b/rest_framework/mixins.py index 1edcfa5c9..69ba1b63b 100644 --- a/rest_framework/mixins.py +++ b/rest_framework/mixins.py @@ -7,6 +7,7 @@ which allows mixin classes to be composed in interesting ways. from django.http import Http404 from rest_framework import status from rest_framework.response import Response +from rest_framework.reverse import reverse class CreateModelMixin(object): @@ -123,3 +124,42 @@ class DestroyModelMixin(object): self.object = self.get_object() self.object.delete() return Response(status=status.HTTP_204_NO_CONTENT) + +class RedirectMixin(object): + """ + A Mixin that provides a redirect method, + redirecting to a different resource endpoint + """ + redirect_permanent = True + redirect_with_query_string = True + redirect_to_view_name = None + redirect_to_url = None + + def get_redirect_url(self, request, *args, **kwargs): + """ + Returning where to redirect to. + To url, view-name or nowhere + """ + if self.redirect_to_url: + url = self.redirect_to_url % kwargs + else: + try: + url = reverse(self.redirect_view_name, args=args, kwargs=kwargs, request=request) + except: + return None + + query_string = self.request.META.get('QUERY_STRING', '') + if query_string and self.redirect_with_query_string: + url = '%(url)s?%(query_string)s' % {'url': url, 'query_string': query_string} + return url + + def redirect(self, request, *args, **kwargs): + url = self.get_redirect_url(request, *args, **kwargs) + if url: + headers = {'Location': url} + if self.redirect_permanent: + return Response(status=status.HTTP_301_MOVED_PERMANENTLY, headers=headers) + else: + return Response(status=status.HTTP_302_FOUND, headers=headers) + else: + return Response(status=status.HTTP_410_GONE) diff --git a/rest_framework/tests/generics.py b/rest_framework/tests/generics.py index a8279ef2b..760ed0047 100644 --- a/rest_framework/tests/generics.py +++ b/rest_framework/tests/generics.py @@ -1,4 +1,5 @@ from django.test import TestCase +from django.conf.urls.defaults import patterns, url from django.test.client import RequestFactory from django.utils import simplejson as json from rest_framework import generics, serializers, status @@ -14,13 +15,19 @@ class RootView(generics.ListCreateAPIView): """ model = BasicModel - class InstanceView(generics.RetrieveUpdateDestroyAPIView): """ Example description for OPTIONS. """ model = BasicModel +class RedirectToRootView(generics.RedirectAPIView): + redirect_permanent = False + redirect_view_name = 'root-view' + +class RedirectToURL(generics.RedirectAPIView): + redirect_to_url = 'http://foo.bar' + redirect_with_query_string = False class SlugSerializer(serializers.ModelSerializer): slug = serializers.Field() # read only @@ -38,6 +45,10 @@ class SlugBasedInstanceView(InstanceView): serializer_class = SlugSerializer +urlpatterns = patterns('', + url(r'^root/$', RootView.as_view(), name='root-view'), +) + class TestRootView(TestCase): def setUp(self): """ @@ -301,3 +312,28 @@ class TestCreateModelWithAutoNowAddField(TestCase): self.assertEquals(response.status_code, status.HTTP_201_CREATED) created = self.objects.get(id=1) self.assertEquals(created.content, 'foobar') + +class RedirectViewTests(TestCase): + urls = 'rest_framework.tests.generics' + + def test_redirect(self): + request = factory.get('/redirect_to_root/?foo=bar') + response = RedirectToRootView.as_view()(request) + + self.assertEquals(response.status_code, status.HTTP_302_FOUND) + self.assertEquals(response['Location'], 'http://testserver/root/?foo=bar') + + def test_redirect_to_nowhere(self): + request = factory.get('/redirect_to_nowhere/') + response = generics.RedirectAPIView.as_view()(request) + + self.assertEquals(response.status_code, status.HTTP_410_GONE) + + def test_redirect_to_url(self): + request = factory.get('/redirect_to_url/?foo=bar') + response = RedirectToURL.as_view()(request) + + self.assertEquals(response.status_code, status.HTTP_301_MOVED_PERMANENTLY) + self.assertEquals(response['Location'], 'http://foo.bar') + + \ No newline at end of file diff --git a/rest_framework/tests/views.py b/rest_framework/tests/views.py index 72eacd62e..91c76a533 100644 --- a/rest_framework/tests/views.py +++ b/rest_framework/tests/views.py @@ -1,12 +1,11 @@ import copy -from django.conf.urls.defaults import patterns, url from django.test import TestCase from django.test.client import RequestFactory from rest_framework import status from rest_framework.decorators import api_view from rest_framework.response import Response from rest_framework.settings import api_settings -from rest_framework.views import APIView, RedirectAPIView +from rest_framework.views import APIView factory = RequestFactory() @@ -18,16 +17,6 @@ class BasicView(APIView): def post(self, request, *args, **kwargs): return Response({'method': 'POST', 'data': request.DATA}) -class RedirectBasicView(RedirectAPIView): - permanent = False - view_name = 'basic-view' - - -urlpatterns = patterns('', - url(r'^basic/$', BasicView.as_view(), name='basic-view'), - url(r'^redirect_to_basic/$', RedirectBasicView.as_view(), name='old-basic-view'), -) - @api_view(['GET', 'POST', 'PUT']) def basic_view(request): @@ -105,17 +94,4 @@ class FunctionBasedViewIntegrationTests(TestCase): 'detail': u'JSON parse error - No JSON object could be decoded' } self.assertEquals(response.status_code, status.HTTP_400_BAD_REQUEST) - self.assertEquals(sanitise_json_error(response.data), expected) - -class RedirectViewTests(TestCase): - urls = 'rest_framework.tests.views' - - def setUp(self): - self.view = RedirectBasicView.as_view() - - def test_redirect(self): - request = factory.get('/redirect_to_basic/?foo=bar', content_type='application/json') - response = self.view(request) - - self.assertEquals(response.status_code, status.HTTP_302_FOUND) - self.assertEquals(response['Location'], 'http://testserver/basic/?foo=bar') \ No newline at end of file + self.assertEquals(sanitise_json_error(response.data), expected) \ No newline at end of file diff --git a/rest_framework/views.py b/rest_framework/views.py index ea07ed7b9..09b515b72 100644 --- a/rest_framework/views.py +++ b/rest_framework/views.py @@ -13,7 +13,6 @@ from rest_framework.compat import View, apply_markdown from rest_framework.response import Response from rest_framework.request import Request from rest_framework.settings import api_settings -from rest_framework.reverse import reverse def _remove_trailing_string(content, trailing): @@ -372,53 +371,4 @@ class APIView(View): We may as well implement this as Django will otherwise provide a less useful default implementation. """ - return Response(self.metadata(request), status=status.HTTP_200_OK) - -class RedirectAPIView(APIView): - """ - A view that provides a redirect to a different resource endpoint - """ - permanent = True - view_name = None - - def get_redirect_url(self, request, *args, **kwargs): - """ - Return the URL redirect to. Arguments and Keyword arguments from the - URL pattern match generating the redirect request - are provided as kwargs to this method. - """ - try: - url = reverse(self.view_name, args=args, kwargs=kwargs, request=request) - except: - return None - - query_string = self.request.META.get('QUERY_STRING', '') - if query_string: - url = '%(url)s?%(query_string)s' % {'url': url, 'query_string': query_string} - return url - - def get(self, request, *args, **kwargs): - url = self.get_redirect_url(request, *args, **kwargs) - if url: - headers = {'Location': url} - if self.permanent: - return Response(status=status.HTTP_301_MOVED_PERMANENTLY, headers=headers) - else: - return Response(status=status.HTTP_302_FOUND, headers=headers) - else: - return Response(status=status.HTTP_410_GONE) - - def head(self, request, *args, **kwargs): - return self.get(request, *args, **kwargs) - - def post(self, request, *args, **kwargs): - return self.get(request, *args, **kwargs) - - def options(self, request, *args, **kwargs): - return self.get(request, *args, **kwargs) - - def delete(self, request, *args, **kwargs): - return self.get(request, *args, **kwargs) - - def put(self, request, *args, **kwargs): - return self.get(request, *args, **kwargs) + return Response(self.metadata(request), status=status.HTTP_200_OK) \ No newline at end of file