Commits

mfoetsch committed c804bcd

Allow .json .html .xml style urls and also allow these formats to be specified in a "?format=..." query string.

Comments (0)

Files changed (6)

djangorestframework/mixins.py

 
 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
             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
         #     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',
         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):
                 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)
 

djangorestframework/renderers.py

     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
         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):
     """
 
     media_type = 'application/json'
+    format = 'json'
 
     def render(self, obj=None, media_type=None):
         """
     """
 
     media_type = 'application/xml'
+    format = 'xml'
 
     def render(self, obj=None, media_type=None):
         """
             '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
         })
     """
 
     media_type = 'text/html'
+    format = 'html'
     template = 'renderer.html'
 
 
     """
 
     media_type = 'application/xhtml+xml'
+    format = 'xhtml'
     template = 'renderer.html'
 
 
     """
 
     media_type = 'text/plain'
+    format = 'txt'
     template = 'renderer.txt'
 
 

djangorestframework/runtests/settings.py

 
 DEBUG = True
 TEMPLATE_DEBUG = DEBUG
+DEBUG_PROPAGATE_EXCEPTIONS = True
 
 ADMINS = (
     # ('Your Name', 'your_email@domain.com'),

djangorestframework/templates/renderer.html

 				<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>
 	</div>
 	</div>
   </body>
-</html>
+</html>

djangorestframework/tests/renderers.py

 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
 
 from StringIO import StringIO
 
-DUMMYSTATUS = 200
+DUMMYSTATUS = status.HTTP_200_OK
 DUMMYCONTENT = 'dummycontent'
 
 RENDERER_A_SERIALIZER = lambda x: 'Renderer A: %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)
 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])),
 )
 
         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"]}'
 

djangorestframework/views.py

     # all other authentication is CSRF exempt.
     @csrf_exempt
     def dispatch(self, request, *args, **kwargs):
-        self.request = request
-        self.args = args
-        self.kwargs = kwargs
-        self.headers = {}
-
-        # Calls to 'reverse' will not be fully qualified unless we set the scheme/host/port here.
-        prefix = '%s://%s' % (request.is_secure() and 'https' or 'http', request.get_host())
-        set_script_prefix(prefix)
-
         try:
-            self.initial(request, *args, **kwargs)
+            self.request = request
+            self.args = args
+            self.kwargs = kwargs
+            self.headers = {}
+    
+            # Calls to 'reverse' will not be fully qualified unless we set the scheme/host/port here.
+            prefix = '%s://%s' % (request.is_secure() and 'https' or 'http', request.get_host())
+            set_script_prefix(prefix)
+    
+            try:
+                self.initial(request, *args, **kwargs)
+            
+                # Authenticate and check request has the relevant permissions
+                self._check_permissions()
+    
+                # Get the appropriate handler method
+                if self.method.lower() in self.http_method_names:
+                    handler = getattr(self, self.method.lower(), self.http_method_not_allowed)
+                else:
+                    handler = self.http_method_not_allowed
+    
+                response_obj = handler(request, *args, **kwargs)
+    
+                # Allow return value to be either HttpResponse, Response, or an object, or None
+                if isinstance(response_obj, HttpResponse):
+                    return response_obj
+                elif isinstance(response_obj, Response):
+                    response = response_obj
+                elif response_obj is not None:
+                    response = Response(status.HTTP_200_OK, response_obj)
+                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)
         
-            # Authenticate and check request has the relevant permissions
-            self._check_permissions()
-
-            # Get the appropriate handler method
-            if self.method.lower() in self.http_method_names:
-                handler = getattr(self, self.method.lower(), self.http_method_not_allowed)
-            else:
-                handler = self.http_method_not_allowed
-
-            response_obj = handler(request, *args, **kwargs)
-
-            # Allow return value to be either HttpResponse, Response, or an object, or None
-            if isinstance(response_obj, HttpResponse):
-                return response_obj
-            elif isinstance(response_obj, Response):
-                response = response_obj
-            elif response_obj is not None:
-                response = Response(status.HTTP_200_OK, response_obj)
-            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)
-    
-        except ErrorResponse, exc:
-            response = exc.response
-        
-        # Always add these headers.
-        #
-        # TODO - this isn't actually the correct way to set the vary header,
-        # also it's currently sub-obtimal for HTTP caching - need to sort that out. 
-        response.headers['Allow'] = ', '.join(self.allowed_methods)
-        response.headers['Vary'] = 'Authenticate, Accept'
-        
-        # merge with headers possibly set at some point in the view
-        response.headers.update(self.headers)
-        
-        return self.render(response)    
-
+            except ErrorResponse, exc:
+                response = exc.response
+            
+            # Always add these headers.
+            #
+            # TODO - this isn't actually the correct way to set the vary header,
+            # also it's currently sub-obtimal for HTTP caching - need to sort that out. 
+            response.headers['Allow'] = ', '.join(self.allowed_methods)
+            response.headers['Vary'] = 'Authenticate, Accept'
+            
+            # merge with headers possibly set at some point in the view
+            response.headers.update(self.headers)
+            
+            return self.render(response)    
+        except:
+            import traceback
+            traceback.print_exc()
+            raise
 
 class ModelView(View):
     """A RESTful view that maps to a model in the database."""