Anonymous avatar Anonymous committed fb97e18

Yowzers. Final big bunch of refactoring for 0.1 release. Now support Django 1.3's views, admin style api is all polished off, loads of tests, new test project for running the test. All sorts of goodness. Getting ready to push this out now.

Comments (0)

Files changed (59)

+Project Owner...
+
+Tom Christie <tomchristie> - tom@tomchristie.com
+
+Thanks to...
+
+Jesper Noehr <jespern> & the django-piston contributors for providing the starting point for this project.
+Paul Bagwell <pbgwl> - Suggestions & bugfixes.

CREDITS.txt

-Thanks to...
-
-Jesper Noehr & the django-piston contributors for providing the starting point for this project.
-Paul Bagwell - Suggestions & bugfixes.
+# To install django-rest-framework in a virtualenv environment...
+
+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 # django, pip
+
+# To run the tests...
+
+cd testproject
+export PYTHONPATH=..
+python manage.py test djangorestframework
+
+# To run the examples...
+
+pip install -r examples/requirements.txt # pygments, httplib2, markdown
+cd examples
+export PYTHONPATH=..
+python manage.py syncdb
+python manage.py runserver
+
+# To build the documentation...
+
+pip install -r docs/requirements.txt   # sphinx
+sphinx-build -c docs -b html -d docs/build docs html

README.txt

-# To install django-rest-framework in a virtualenv environment...
-
-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
-
-# To build the documentation...
-
-pip install -r docs/requirements.txt
-sphinx-build -c docs -b html -d docs/build docs html
-
-# To run the examples...
-
-pip install -r examples/requirements.txt
-cd examples
-export PYTHONPATH=..
-python manage.py syncdb
-python manage.py runserver
-

djangorestframework/authenticators.py

 from django.contrib.auth import authenticate
+from django.middleware.csrf import CsrfViewMiddleware
+from djangorestframework.utils import as_tuple
 import base64
 
+
+class AuthenticatorMixin(object):
+    authenticators = None
+
+    def authenticate(self, request):
+        """Attempt to authenticate the request, returning an authentication context or None.
+        An authentication context may be any object, although in many cases it will be a User instance."""
+        
+        # Attempt authentication against each authenticator in turn,
+        # and return None if no authenticators succeed in authenticating the request.
+        for authenticator in as_tuple(self.authenticators):
+            auth_context = authenticator(self).authenticate(request)
+            if auth_context:
+                return auth_context
+
+        return None
+
+
 class BaseAuthenticator(object):
     """All authenticators should extend BaseAuthenticator."""
 
-    def __init__(self, resource):
-        """Initialise the authenticator with the Resource instance as state,
-        in case the authenticator needs to access any metadata on the Resource object."""
-        self.resource = resource
+    def __init__(self, mixin):
+        """Initialise the authenticator with the mixin instance as state,
+        in case the authenticator needs to access any metadata on the mixin object."""
+        self.mixin = mixin
 
     def authenticate(self, request):
         """Authenticate the request and return the authentication context or None.
 
+        An authentication context might be something as simple as a User object, or it might
+        be some more complicated token, for example authentication tokens which are signed
+        against a particular set of permissions for a given user, over a given timeframe.
+
         The default permission checking on Resource will use the allowed_methods attribute
         for permissions if the authentication context is not None, and use anon_allowed_methods otherwise.
 
 class UserLoggedInAuthenticator(BaseAuthenticator):
     """Use Djagno's built-in request session for authentication."""
     def authenticate(self, request):
-        if getattr(request, 'user', None) and request.user.is_active:
-            return request.user
+        if getattr(request, 'user', None) and request.user.is_active:                
+            resp = CsrfViewMiddleware().process_view(request, None, (), {})
+            if resp is None:  # csrf passed
+                return request.user
         return None
     

djangorestframework/breadcrumbs.py

+from django.core.urlresolvers import resolve
+from djangorestframework.description import get_name
+
+def get_breadcrumbs(url):
+    """Given a url returns a list of breadcrumbs, which are each a tuple of (name, url)."""
+    
+    def breadcrumbs_recursive(url, breadcrumbs_list):
+        """Add tuples of (name, url) to the breadcrumbs list, progressively chomping off parts of the url."""
+        
+        # This is just like compsci 101 all over again...
+        try:
+            (view, unused_args, unused_kwargs) = resolve(url)
+        except:
+            pass
+        else:
+            if callable(view):
+                breadcrumbs_list.insert(0, (get_name(view), url))
+        
+        if url == '':
+            # All done
+            return breadcrumbs_list
+    
+        elif url.endswith('/'):
+            # Drop trailing slash off the end and continue to try to resolve more breadcrumbs
+            return breadcrumbs_recursive(url.rstrip('/'), breadcrumbs_list)
+    
+        # Drop trailing non-slash off the end and continue to try to resolve more breadcrumbs
+        return breadcrumbs_recursive(url[:url.rfind('/') + 1], breadcrumbs_list)
+
+    return breadcrumbs_recursive(url, [])
+

djangorestframework/compat.py

+"""Compatability module to provide support for backwards compatability with older versions of django/python"""
+
+# django.test.client.RequestFactory (Django >= 1.3) 
+try:
+    from django.test.client import RequestFactory
+
+except ImportError:
+    from django.test import Client
+    from django.core.handlers.wsgi import WSGIRequest
+    
+    # From: http://djangosnippets.org/snippets/963/
+    # Lovely stuff
+    class RequestFactory(Client):
+        """
+        Class that lets you create mock Request objects for use in testing.
+        
+        Usage:
+        
+        rf = RequestFactory()
+        get_request = rf.get('/hello/')
+        post_request = rf.post('/submit/', {'foo': 'bar'})
+        
+        This class re-uses the django.test.client.Client interface, docs here:
+        http://www.djangoproject.com/documentation/testing/#the-test-client
+        
+        Once you have a request object you can pass it to any view function, 
+        just as if that view had been hooked up using a URLconf.
+        
+        """
+        def request(self, **request):
+            """
+            Similar to parent class, but returns the request object as soon as it
+            has created it.
+            """
+            environ = {
+                'HTTP_COOKIE': self.cookies,
+                'PATH_INFO': '/',
+                'QUERY_STRING': '',
+                'REQUEST_METHOD': 'GET',
+                'SCRIPT_NAME': '',
+                'SERVER_NAME': 'testserver',
+                'SERVER_PORT': 80,
+                'SERVER_PROTOCOL': 'HTTP/1.1',
+            }
+            environ.update(self.defaults)
+            environ.update(request)
+            return WSGIRequest(environ)
+
+# django.views.generic.View (Django >= 1.3)
+try:
+    from django.views.generic import View
+except:
+    from django import http
+    from django.utils.functional import update_wrapper
+    # from django.utils.log import getLogger
+    # from django.utils.decorators import classonlymethod
+    
+    # logger = getLogger('django.request') - We'll just drop support for logger if running Django <= 1.2
+    # Might be nice to fix this up sometime to allow djangorestframework.compat.View to match 1.3's View more closely
+    
+    class View(object):
+        """
+        Intentionally simple parent class for all views. Only implements
+        dispatch-by-method and simple sanity checking.
+        """
+    
+        http_method_names = ['get', 'post', 'put', 'delete', 'head', 'options', 'trace']
+    
+        def __init__(self, **kwargs):
+            """
+            Constructor. Called in the URLconf; can contain helpful extra
+            keyword arguments, and other things.
+            """
+            # Go through keyword arguments, and either save their values to our
+            # instance, or raise an error.
+            for key, value in kwargs.iteritems():
+                setattr(self, key, value)
+    
+        # @classonlymethod - We'll just us classmethod instead if running Django <= 1.2
+        @classmethod
+        def as_view(cls, **initkwargs):
+            """
+            Main entry point for a request-response process.
+            """
+            # sanitize keyword arguments
+            for key in initkwargs:
+                if key in cls.http_method_names:
+                    raise TypeError(u"You tried to pass in the %s method name as a "
+                                    u"keyword argument to %s(). Don't do that."
+                                    % (key, cls.__name__))
+                if not hasattr(cls, key):
+                    raise TypeError(u"%s() received an invalid keyword %r" % (
+                        cls.__name__, key))
+    
+            def view(request, *args, **kwargs):
+                self = cls(**initkwargs)
+                return self.dispatch(request, *args, **kwargs)
+    
+            # take name and docstring from class
+            update_wrapper(view, cls, updated=())
+    
+            # and possible attributes set by decorators
+            # like csrf_exempt from dispatch
+            update_wrapper(view, cls.dispatch, assigned=())
+            return view
+    
+        def dispatch(self, request, *args, **kwargs):
+            # Try to dispatch to the right method; if a method doesn't exist,
+            # defer to the error handler. Also defer to the error handler if the
+            # request method isn't on the approved list.
+            if request.method.lower() in self.http_method_names:
+                handler = getattr(self, request.method.lower(), self.http_method_not_allowed)
+            else:
+                handler = self.http_method_not_allowed
+            self.request = request
+            self.args = args
+            self.kwargs = kwargs
+            return handler(request, *args, **kwargs)
+    
+        def http_method_not_allowed(self, request, *args, **kwargs):
+            allowed_methods = [m for m in self.http_method_names if hasattr(self, m)]
+            #logger.warning('Method Not Allowed (%s): %s' % (request.method, request.path),
+            #    extra={
+            #        'status_code': 405,
+            #        'request': self.request
+            #    }
+            #)
+            return http.HttpResponseNotAllowed(allowed_methods)

djangorestframework/description.py

+"""Get a descriptive name and description for a view,
+based on class name and docstring, and override-able by 'name' and 'description' attributes"""
+import re
+
+def get_name(view):
+    """Return a name for the view.
+    
+    If view has a name attribute, use that, otherwise use the view's class name, with 'CamelCaseNames' converted to 'Camel Case Names'."""
+    if getattr(view, 'name', None) is not None:
+        return view.name
+
+    if getattr(view, '__name__', None) is not None:
+        name = view.__name__
+    elif getattr(view, '__class__', None) is not None:  # TODO: should be able to get rid of this case once refactoring to 1.3 class views is complete
+        name = view.__class__.__name__
+    else:
+        return ''
+
+    return re.sub('(((?<=[a-z])[A-Z])|([A-Z](?![A-Z]|$)))', ' \\1', name).strip()
+
+def get_description(view):
+    """Provide a description for the view.
+
+    By default this is the view's docstring with nice unindention applied."""
+    if getattr(view, 'description', None) is not None:
+        return getattr(view, 'description')
+
+    if getattr(view, '__doc__', None) is not None:
+        whitespace_counts = [len(line) - len(line.lstrip(' ')) for line in view.__doc__.splitlines()[1:] if line.lstrip()]
+
+        if whitespace_counts:
+            whitespace_pattern = '^' + (' ' * min(whitespace_counts))
+            return re.sub(re.compile(whitespace_pattern, re.MULTILINE), '', view.__doc__)
+
+        return view.__doc__
+    
+    return ''

djangorestframework/emitters.py

 and providing forms and links depending on the allowed methods, emitters and parsers on the Resource. 
 """
 from django.conf import settings
+from django.http import HttpResponse
 from django.template import RequestContext, loader
 from django import forms
 
-from djangorestframework.response import NoContent
+from djangorestframework.response import NoContent, ResponseException, status
 from djangorestframework.validators import FormValidatorMixin
 from djangorestframework.utils import dict2xml, url_resolves
+from djangorestframework.markdownwrapper import apply_markdown
+from djangorestframework.breadcrumbs import get_breadcrumbs
+from djangorestframework.content import OverloadedContentMixin
+from djangorestframework.description import get_name, get_description
 
 from urllib import quote_plus
 import string
+import re
+from decimal import Decimal
+
 try:
     import json
 except ImportError:
     import simplejson as json
 
 
+_MSIE_USER_AGENT = re.compile(r'^Mozilla/[0-9]+\.[0-9]+ \([^)]*; MSIE [0-9]+\.[0-9]+[a-z]?;[^)]*\)(?!.* Opera )')
+
+
+class EmitterMixin(object):
+    ACCEPT_QUERY_PARAM = '_accept'        # Allow override of Accept header in URL query params
+    REWRITE_IE_ACCEPT_HEADER = True
+
+    request = None
+    response = None
+    emitters = ()
+
+    def emit(self, response):
+        self.response = response
+
+        try:
+            emitter = self._determine_emitter(self.request)
+        except ResponseException, exc:
+            emitter = self.default_emitter
+            response = exc.response
+        
+        # Serialize the response content
+        if response.has_content_body:
+            content = emitter(self).emit(output=response.cleaned_content)
+        else:
+            content = emitter(self).emit()
+        
+        # Munge DELETE Response code to allow us to return content
+        # (Do this *after* we've rendered the template so that we include the normal deletion response code in the output)
+        if response.status == 204:
+            response.status = 200
+        
+        # Build the HTTP Response
+        # TODO: Check if emitter.mimetype is underspecified, or if a content-type header has been set
+        resp = HttpResponse(content, mimetype=emitter.media_type, status=response.status)
+        for (key, val) in response.headers.items():
+            resp[key] = val
+
+        return resp
+
+
+    def _determine_emitter(self, request):
+        """Return the appropriate emitter for the output, given the client's 'Accept' header,
+        and the content types that this Resource knows how to serve.
+        
+        See: RFC 2616, Section 14 - http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html"""
+
+        if self.ACCEPT_QUERY_PARAM and request.GET.get(self.ACCEPT_QUERY_PARAM, None):
+            # Use _accept parameter override
+            accept_list = [request.GET.get(self.ACCEPT_QUERY_PARAM)]
+        elif self.REWRITE_IE_ACCEPT_HEADER and request.META.has_key('HTTP_USER_AGENT') and _MSIE_USER_AGENT.match(request.META['HTTP_USER_AGENT']):
+            accept_list = ['text/html', '*/*']
+        elif request.META.has_key('HTTP_ACCEPT'):
+            # Use standard HTTP Accept negotiation
+            accept_list = request.META["HTTP_ACCEPT"].split(',')
+        else:
+            # No accept header specified
+            return self.default_emitter
+        
+        # Parse the accept header into a dict of {qvalue: set of media types}
+        # We ignore mietype parameters
+        accept_dict = {}    
+        for token in accept_list:
+            components = token.split(';')
+            mimetype = components[0].strip()
+            qvalue = Decimal('1.0')
+            
+            if len(components) > 1:
+                # Parse items that have a qvalue eg text/html;q=0.9
+                try:
+                    (q, num) = components[-1].split('=')
+                    if q == 'q':
+                        qvalue = Decimal(num)
+                except:
+                    # Skip malformed entries
+                    continue
+
+            if accept_dict.has_key(qvalue):
+                accept_dict[qvalue].add(mimetype)
+            else:
+                accept_dict[qvalue] = set((mimetype,))
+        
+        # Convert to a list of sets ordered by qvalue (highest first)
+        accept_sets = [accept_dict[qvalue] for qvalue in sorted(accept_dict.keys(), reverse=True)]
+       
+        for accept_set in accept_sets:
+            # Return any exact match
+            for emitter in self.emitters:
+                if emitter.media_type in accept_set:
+                    return emitter
+
+            # Return any subtype match
+            for emitter in self.emitters:
+                if emitter.media_type.split('/')[0] + '/*' in accept_set:
+                    return emitter
+
+            # Return default
+            if '*/*' in accept_set:
+                return self.default_emitter
+      
+
+        raise ResponseException(status.HTTP_406_NOT_ACCEPTABLE,
+                                {'detail': 'Could not statisfy the client\'s Accept header',
+                                 'available_types': self.emitted_media_types})
+
+    @property
+    def emitted_media_types(self):
+        """Return an list of all the media types that this resource can emit."""
+        return [emitter.media_type for emitter in self.emitters]
+
+    @property
+    def default_emitter(self):
+        """Return the resource's most prefered emitter.
+        (This emitter is used if the client does not send and Accept: header, or sends Accept: */*)"""
+        return self.emitters[0]
+
+
 
 # TODO: Rename verbose to something more appropriate
 # TODO: NoContent could be handled more cleanly.  It'd be nice if it was handled by default,
         if output is NoContent:
             return ''
 
-        context = RequestContext(self.resource.request, output)
+        context = RequestContext(self.request, output)
         return self.template.render(context)
 
 
     Implementing classes should extend this class and set the template attribute."""
     template = None
 
-    def _get_content(self, resource, output):
+    def _get_content(self, resource, request, output):
         """Get the content as if it had been emitted by a non-documenting emitter.
 
         (Typically this will be the content as it would have been if the Resource had been
 
         form_instance = None
 
-        if isinstance(self, FormValidatorMixin):
-            # Otherwise if this isn't an error response
-            # then attempt to get a form bound to the response object
+        if isinstance(resource, FormValidatorMixin):
+            # If we already have a bound form instance (IE provided by the input parser, then use that)
+            if resource.bound_form_instance is not None:
+                form_instance = resource.bound_form_instance
+                
+            # Otherwise if we have a response that is valid against the form then use that
             if not form_instance and resource.response.has_content_body:
                 try:
                     form_instance = resource.get_bound_form(resource.response.raw_content)
-                    if form_instance:
-                        form_instance.is_valid()
+                    if form_instance and not form_instance.is_valid():
+                        form_instance = None
                 except:
                     form_instance = None
             
             # If we still don't have a form instance then try to get an unbound form
             if not form_instance:
                 try:
-                    form_instance = self.resource.get_bound_form()
+                    form_instance = resource.get_bound_form()
                 except:
                     pass
 
         """Returns a form that allows for arbitrary content types to be tunneled via standard HTML forms
         (Which are typically application/x-www-form-urlencoded)"""
 
+        # If we're not using content overloading there's no point in supplying a generic form,
+        # as the resource won't treat the form's value as the content of the request.
+        if not isinstance(resource, OverloadedContentMixin):
+            return None
+
         # NB. http://jacobian.org/writing/dynamic-form-generation/
         class GenericContentForm(forms.Form):
             def __init__(self, resource):
 
 
     def emit(self, output=NoContent):
-        content = self._get_content(self.resource, output)
+        content = self._get_content(self.resource, self.resource.request, output)
         form_instance = self._get_form_instance(self.resource)
 
         if url_resolves(settings.LOGIN_URL) and url_resolves(settings.LOGOUT_URL):
             login_url = None
             logout_url = None
 
+        name = get_name(self.resource)
+        description = get_description(self.resource)
+
+        markeddown = None
+        if apply_markdown:
+            try:
+                markeddown = apply_markdown(description)
+            except AttributeError:  # TODO: possibly split the get_description / get_name into a mixin class
+                markeddown = None
+
+        breadcrumb_list = get_breadcrumbs(self.resource.request.path)
+
         template = loader.get_template(self.template)
         context = RequestContext(self.resource.request, {
             'content': content,
             'resource': self.resource,
             'request': self.resource.request,
             'response': self.resource.response,
+            'description': description,
+            'name': name,
+            'markeddown': markeddown,
+            'breadcrumblist': breadcrumb_list,
             'form': form_instance,
             'login_url': login_url,
             'logout_url': logout_url,
+            'ADMIN_MEDIA_PREFIX': settings.ADMIN_MEDIA_PREFIX
         })
         
         ret = template.render(context)
 
-        # Munge DELETE Response code to allow us to return content
-        # (Do this *after* we've rendered the template so that we include the normal deletion response code in the output)
-        if self.resource.response.status == 204:
-            self.resource.response.status = 200
-
         return ret
 
 
     Useful for browsing an API with command line tools."""
     media_type = 'text/plain'
     template = 'emitter.txt'
+    
+DEFAULT_EMITTERS = ( JSONEmitter,
+                     DocumentingHTMLEmitter,
+                     DocumentingXHTMLEmitter,
+                     DocumentingPlainTextEmitter,
+                     XMLEmitter )
 
 

djangorestframework/markdownwrapper.py

+"""If python-markdown is installed expose an apply_markdown(text) function,
+to convert markeddown text into html.  Otherwise just set apply_markdown to None.
+
+See: http://www.freewisdom.org/projects/python-markdown/
+"""
+
+__all__ = ['apply_markdown']
+
+try:
+    import markdown
+    import re
+    
+    class CustomSetextHeaderProcessor(markdown.blockprocessors.BlockProcessor):
+        """Override markdown's SetextHeaderProcessor, so that ==== headers are <h2> and ---- headers are <h3>.
+        
+        We use <h1> for the resource name."""
+    
+        # Detect Setext-style header. Must be first 2 lines of block.
+        RE = re.compile(r'^.*?\n[=-]{3,}', re.MULTILINE)
+    
+        def test(self, parent, block):
+            return bool(self.RE.match(block))
+    
+        def run(self, parent, blocks):
+            lines = blocks.pop(0).split('\n')
+            # Determine level. ``=`` is 1 and ``-`` is 2.
+            if lines[1].startswith('='):
+                level = 2
+            else:
+                level = 3
+            h = markdown.etree.SubElement(parent, 'h%d' % level)
+            h.text = lines[0].strip()
+            if len(lines) > 2:
+                # Block contains additional lines. Add to  master blocks for later.
+                blocks.insert(0, '\n'.join(lines[2:]))
+            
+    def apply_markdown(text):
+        """Simple wrapper around markdown.markdown to apply our CustomSetextHeaderProcessor,
+        and also set the base level of '#' style headers to <h2>."""
+        extensions = ['headerid(level=2)']
+        safe_mode = False,
+        output_format = markdown.DEFAULT_OUTPUT_FORMAT
+
+        md = markdown.Markdown(extensions=markdown.load_extensions(extensions),
+                               safe_mode=safe_mode, 
+                               output_format=output_format)
+        md.parser.blockprocessors['setextheader'] = CustomSetextHeaderProcessor(md.parser)
+        return md.convert(text)
+
+except:
+    apply_markdown = None

djangorestframework/modelresource.py

 
 from djangorestframework.response import status, Response, ResponseException
 from djangorestframework.resource import Resource
+from djangorestframework.validators import ModelFormValidatorMixin
 
 import decimal
 import inspect
 import re
 
 
-class ModelResource(Resource):
+class ModelResource(Resource, ModelFormValidatorMixin):
     """A specialized type of Resource, for resources that map directly to a Django Model.
     Useful things this provides:
 
     # By default the set of input fields will be the same as the set of output fields
     # If you wish to override this behaviour you should explicitly set the
     # form_fields attribute on your class. 
-    form_fields = None
+    #form_fields = None
 
 
-    def get_form(self, content=None):
-        """Return a form that may be used in validation and/or rendering an html emitter"""
-        if self.form:
-            return super(self.__class__, self).get_form(content)
+    #def get_form(self, content=None):
+    #    """Return a form that may be used in validation and/or rendering an html emitter"""
+    #    if self.form:
+    #        return super(self.__class__, self).get_form(content)
+    #
+    #    elif self.model:
+    #
+    #        class NewModelForm(ModelForm):
+    #            class Meta:
+    #                model = self.model
+    #                fields = self.form_fields if self.form_fields else None
+    #
+    #        if content and isinstance(content, Model):
+    #            return NewModelForm(instance=content)
+    #        elif content:
+    #            return NewModelForm(content)
+    #        
+    #        return NewModelForm()
+    #
+    #    return None
 
-        elif self.model:
 
-            class NewModelForm(ModelForm):
-                class Meta:
-                    model = self.model
-                    fields = self.form_fields if self.form_fields else None
-
-            if content and isinstance(content, Model):
-                return NewModelForm(instance=content)
-            elif content:
-                return NewModelForm(content)
-            
-            return NewModelForm()
-
-        return None
-
-
-    def cleanup_request(self, data, form_instance):
-        """Override cleanup_request to drop read-only fields from the input prior to validation.
-        This ensures that we don't error out with 'non-existent field' when these fields are supplied,
-        and allows for a pragmatic approach to resources which include read-only elements.
-
-        I would actually like to be strict and verify the value of correctness of the values in these fields,
-        although that gets tricky as it involves validating at the point that we get the model instance.
-        
-        See here for another example of this approach:
-        http://fedoraproject.org/wiki/Cloud_APIs_REST_Style_Guide
-        https://www.redhat.com/archives/rest-practices/2010-April/thread.html#00041"""
-        read_only_fields = set(self.fields) - set(self.form_instance.fields)
-        input_fields = set(data.keys())
-
-        clean_data = {}
-        for key in input_fields - read_only_fields:
-            clean_data[key] = data[key]
-
-        return super(ModelResource, self).cleanup_request(clean_data, form_instance)
+    #def cleanup_request(self, data, form_instance):
+    #    """Override cleanup_request to drop read-only fields from the input prior to validation.
+    #    This ensures that we don't error out with 'non-existent field' when these fields are supplied,
+    #    and allows for a pragmatic approach to resources which include read-only elements.
+    #
+    #    I would actually like to be strict and verify the value of correctness of the values in these fields,
+    #    although that gets tricky as it involves validating at the point that we get the model instance.
+    #    
+    #    See here for another example of this approach:
+    #    http://fedoraproject.org/wiki/Cloud_APIs_REST_Style_Guide
+    #    https://www.redhat.com/archives/rest-practices/2010-April/thread.html#00041"""
+    #    read_only_fields = set(self.fields) - set(self.form_instance.fields)
+    #    input_fields = set(data.keys())
+    #
+    #    clean_data = {}
+    #    for key in input_fields - read_only_fields:
+    #        clean_data[key] = data[key]
+    #
+    #    return super(ModelResource, self).cleanup_request(clean_data, form_instance)
 
 
     def cleanup_response(self, data):
                 if inspect.ismethod(f) and len(inspect.getargspec(f)[0]) == 1:
                     ret = _any(f())
             else:
-                ret = unicode(thing)  # TRC  TODO: Change this back!
+                ret = str(thing)  # TRC  TODO: Change this back!
 
             return ret
 
                 try: ret['absolute_url'] = data.get_absolute_url()
                 except: pass
             
-            for key, val in ret.items():
-                if key.endswith('_url') or key.endswith('_uri'):
-                    ret[key] = self.add_domain(val)
+            #for key, val in ret.items():
+            #    if key.endswith('_url') or key.endswith('_uri'):
+            #        ret[key] = self.add_domain(val)
 
             return ret
         
         instance.save()
         headers = {}
         if hasattr(instance, 'get_absolute_url'):
-            headers['Location'] = self.add_domain(instance.get_absolute_url())
+            headers['Location'] = instance.get_absolute_url()
         return Response(status.HTTP_201_CREATED, instance, headers)
 
     def get(self, request, auth, *args, **kwargs):

djangorestframework/parsers.py

         
         return data
 
-
-# TODO: Allow parsers to specify multiple media types
+# TODO: Allow parsers to specify multiple media_types
 class MultipartParser(FormParser):
-    """The default parser for multipart form data.
-    Return a dict containing a single value for each non-reserved parameter.
-    """
-    
     media_type = 'multipart/form-data'
 
-

djangorestframework/resource.py

-from django.contrib.sites.models import Site
-from django.core.urlresolvers import reverse
-from django.http import HttpResponse
+from django.core.urlresolvers import set_script_prefix
+from django.views.decorators.csrf import csrf_exempt
 
+from djangorestframework.compat import View
+from djangorestframework.emitters import EmitterMixin
 from djangorestframework.parsers import ParserMixin
+from djangorestframework.authenticators import AuthenticatorMixin
 from djangorestframework.validators import FormValidatorMixin
 from djangorestframework.content import OverloadedContentMixin
 from djangorestframework.methods import OverloadedPOSTMethodMixin 
 from djangorestframework import emitters, parsers, authenticators
 from djangorestframework.response import status, Response, ResponseException
 
-from decimal import Decimal
 import re
 
 # TODO: Figure how out references and named urls need to work nicely
 __all__ = ['Resource']
 
 
-_MSIE_USER_AGENT = re.compile(r'^Mozilla/[0-9]+\.[0-9]+ \([^)]*; MSIE [0-9]+\.[0-9]+[a-z]?;[^)]*\)(?!.* Opera )')
 
 
-class Resource(ParserMixin, FormValidatorMixin, OverloadedContentMixin, OverloadedPOSTMethodMixin):
+class Resource(EmitterMixin, ParserMixin, AuthenticatorMixin, FormValidatorMixin,
+               OverloadedContentMixin, OverloadedPOSTMethodMixin, View):
     """Handles incoming requests and maps them to REST operations,
     performing authentication, input deserialization, input validation, output serialization."""
 
     # Optional form for input validation and presentation of HTML formatted responses.
     form = None
 
+    # Allow name and description for the Resource to be set explicitly,
+    # overiding the default classname/docstring behaviour.
+    # These are used for documentation in the standard html and text emitters.
+    name = None
+    description = None
+
     # Map standard HTTP methods to function calls
     callmap = { 'GET': 'get', 'POST': 'post', 
                 'PUT': 'put', 'DELETE': 'delete' }
 
+
     # Some reserved parameters to allow us to use standard HTML forms with our resource
     # Override any/all of these with None to disable them, or override them with another value to rename them.
-    ACCEPT_QUERY_PARAM = '_accept'        # Allow override of Accept header in URL query params    CONTENTTYPE_PARAM = '_contenttype'    # Allow override of Content-Type header in form params (allows sending arbitrary content with standard forms)
     CSRF_PARAM = 'csrfmiddlewaretoken'    # Django's CSRF token used in form params
 
-    _MUNGE_IE_ACCEPT_HEADER = True
-
-    def __new__(cls, *args, **kwargs):
-        """Make the class callable so it can be used as a Django view."""
-        self = object.__new__(cls)
-        if args:
-            request = args[0]
-            self.__init__(request)
-            return self._handle_request(request, *args[1:], **kwargs)
-        else:
-            self.__init__()
-            return self
-
-
-    def __init__(self, request=None):
-        """"""
-        # Setup the resource context
-        self.request = request
-        self.response = None
-        self.form_instance = None
-
-        # These sets are determined now so that overridding classes can modify the various parameter names,
-        # or set them to None to disable them. 
-        self.RESERVED_FORM_PARAMS = set((self.METHOD_PARAM, self.CONTENTTYPE_PARAM, self.CONTENT_PARAM, self.CSRF_PARAM))
-        self.RESERVED_QUERY_PARAMS = set((self.ACCEPT_QUERY_PARAM))
-        self.RESERVED_FORM_PARAMS.discard(None)
-        self.RESERVED_QUERY_PARAMS.discard(None)
-
-
-    @property
-    def name(self):
-        """Provide a name for the resource.
-        By default this is the class name, with 'CamelCaseNames' converted to 'Camel Case Names'."""
-        class_name = self.__class__.__name__
-        return re.sub('(((?<=[a-z])[A-Z])|([A-Z](?![A-Z]|$)))', ' \\1', class_name).strip()
-
-    @property
-    def description(self):
-        """Provide a description for the resource.
-        By default this is the class's docstring with leading line spaces stripped."""
-        return re.sub(re.compile('^ +', re.MULTILINE), '', self.__doc__)
-
-    @property
-    def emitted_media_types(self):
-        """Return an list of all the media types that this resource can emit."""
-        return [emitter.media_type for emitter in self.emitters]
-
-    @property
-    def default_emitter(self):
-        """Return the resource's most prefered emitter.
-        (This emitter is used if the client does not send and Accept: header, or sends Accept: */*)"""
-        return self.emitters[0]
 
     def get(self, request, auth, *args, **kwargs):
         """Must be subclassed to be implemented."""
         self.not_implemented('DELETE')
 
 
-    def reverse(self, view, *args, **kwargs):
-        """Return a fully qualified URI for a given view or resource.
-        Add the domain using the Sites framework if possible, otherwise fallback to using the current request."""
-        return self.add_domain(reverse(view, args=args, kwargs=kwargs))
-
-
     def not_implemented(self, operation):
         """Return an HTTP 500 server error if an operation is called which has been allowed by
         allowed_methods, but which has not been implemented."""
                                 {'detail': '%s operation on this resource has not been implemented' % (operation, )})
 
 
-    def add_domain(self, path):
-        """Given a path, return an fully qualified URI.
-        Use the Sites framework if possible, otherwise fallback to using the domain from the current request."""
-
-        # Note that out-of-the-box the Sites framework uses the reserved domain 'example.com'
-        # See RFC 2606 - http://www.faqs.org/rfcs/rfc2606.html
-        try:
-            site = Site.objects.get_current()
-            if site.domain and site.domain != 'example.com':
-                return 'http://%s%s' % (site.domain, path)
-        except:
-            pass
-
-        return self.request.build_absolute_uri(path)
-
-
-    def authenticate(self, request):
-        """Attempt to authenticate the request, returning an authentication context or None.
-        An authentication context may be any object, although in many cases it will be a User instance."""
-        
-        # Attempt authentication against each authenticator in turn,
-        # and return None if no authenticators succeed in authenticating the request.
-        for authenticator in self.authenticators:
-            auth_context = authenticator(self).authenticate(request)
-            if auth_context:
-                return auth_context
-
-        return None
-
-
     def check_method_allowed(self, method, auth):
         """Ensure the request method is permitted for this resource, raising a ResourceException if it is not."""
 
         """Perform any resource-specific data filtering prior to the standard HTTP
         content-type serialization.
 
-        Eg filter complex objects that cannot be serialized by json/xml/etc into basic objects that can."""
+        Eg filter complex objects that cannot be serialized by json/xml/etc into basic objects that can.
+        
+        TODO: This is going to be removed.  I think that the 'fields' behaviour is going to move into
+        the EmitterMixin and Emitter classes."""
         return data
 
+    # Session based authentication is explicitly CSRF validated, all other authentication is CSRF exempt.
 
-    def determine_emitter(self, request):
-        """Return the appropriate emitter for the output, given the client's 'Accept' header,
-        and the content types that this Resource knows how to serve.
-        
-        See: RFC 2616, Section 14 - http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html"""
-
-        if self.ACCEPT_QUERY_PARAM and request.GET.get(self.ACCEPT_QUERY_PARAM, None):
-            # Use _accept parameter override
-            accept_list = [request.GET.get(self.ACCEPT_QUERY_PARAM)]
-        elif self._MUNGE_IE_ACCEPT_HEADER and request.META.has_key('HTTP_USER_AGENT') and _MSIE_USER_AGENT.match(request.META['HTTP_USER_AGENT']):
-            accept_list = ['text/html', '*/*']
-        elif request.META.has_key('HTTP_ACCEPT'):
-            # Use standard HTTP Accept negotiation
-            accept_list = request.META["HTTP_ACCEPT"].split(',')
-        else:
-            # No accept header specified
-            return self.default_emitter
-        
-        # Parse the accept header into a dict of {qvalue: set of media types}
-        # We ignore mietype parameters
-        accept_dict = {}    
-        for token in accept_list:
-            components = token.split(';')
-            mimetype = components[0].strip()
-            qvalue = Decimal('1.0')
-            
-            if len(components) > 1:
-                # Parse items that have a qvalue eg text/html;q=0.9
-                try:
-                    (q, num) = components[-1].split('=')
-                    if q == 'q':
-                        qvalue = Decimal(num)
-                except:
-                    # Skip malformed entries
-                    continue
-
-            if accept_dict.has_key(qvalue):
-                accept_dict[qvalue].add(mimetype)
-            else:
-                accept_dict[qvalue] = set((mimetype,))
-        
-        # Convert to a list of sets ordered by qvalue (highest first)
-        accept_sets = [accept_dict[qvalue] for qvalue in sorted(accept_dict.keys(), reverse=True)]
-       
-        for accept_set in accept_sets:
-            # Return any exact match
-            for emitter in self.emitters:
-                if emitter.media_type in accept_set:
-                    return emitter
-
-            # Return any subtype match
-            for emitter in self.emitters:
-                if emitter.media_type.split('/')[0] + '/*' in accept_set:
-                    return emitter
-
-            # Return default
-            if '*/*' in accept_set:
-                return self.default_emitter
-      
-
-        raise ResponseException(status.HTTP_406_NOT_ACCEPTABLE,
-                                {'detail': 'Could not statisfy the client\'s Accept header',
-                                 'available_types': self.emitted_media_types})
-
-
-    def _handle_request(self, request, *args, **kwargs):
+    @csrf_exempt
+    def dispatch(self, request, *args, **kwargs):
         """This method is the core of Resource, through which all requests are passed.
 
         Broadly this consists of the following procedure:
         4. cleanup the response data
         5. serialize response data into response content, using standard HTTP content negotiation
         """
-        emitter = None
+        
+        self.request = request
+
+        # 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)
+
+        # These sets are determined now so that overridding classes can modify the various parameter names,
+        # or set them to None to disable them. 
+        self.RESERVED_FORM_PARAMS = set((self.METHOD_PARAM, self.CONTENTTYPE_PARAM, self.CONTENT_PARAM, self.CSRF_PARAM))
+        self.RESERVED_QUERY_PARAMS = set((self.ACCEPT_QUERY_PARAM))
+        self.RESERVED_FORM_PARAMS.discard(None)
+        self.RESERVED_QUERY_PARAMS.discard(None)
+        
         method = self.determine_method(request)
 
         try:
-            # Before we attempt anything else determine what format to emit our response data with.
-            emitter = self.determine_emitter(request)
 
             # Authenticate the request, and store any context so that the resource operations can
             # do more fine grained authentication if required.
             func = getattr(self, self.callmap.get(method, None))
     
             # Either generate the response data, deserializing and validating any request data
-            # TODO: Add support for message bodys on other HTTP methods, as it is valid.
+            # TODO: Add support for message bodys on other HTTP methods, as it is valid (although non-conventional).
             if method in ('PUT', 'POST'):
                 (content_type, content) = self.determine_content(request)
                 parser_content = self.parse(content_type, content)
                 cleaned_content = self.validate(parser_content)
-                response = func(request, auth_context, cleaned_content, *args, **kwargs)
+                response_obj = func(request, auth_context, cleaned_content, *args, **kwargs)
 
             else:
-                response = func(request, auth_context, *args, **kwargs)
+                response_obj = func(request, auth_context, *args, **kwargs)
 
             # Allow return value to be either Response, or an object, or None
-            if isinstance(response, Response):
-                self.response = response
-            elif response is not None:
-                self.response = Response(status.HTTP_200_OK, response)
+            if isinstance(response_obj, Response):
+                response = response_obj
+            elif response_obj is not None:
+                response = Response(status.HTTP_200_OK, response_obj)
             else:
-                self.response = Response(status.HTTP_204_NO_CONTENT)
+                response = Response(status.HTTP_204_NO_CONTENT)
 
             # Pre-serialize filtering (eg filter complex objects into natively serializable types)
-            self.response.cleaned_content = self.cleanup_response(self.response.raw_content)
+            response.cleaned_content = self.cleanup_response(response.raw_content)
 
 
         except ResponseException, exc:
-            self.response = exc.response
-
-            # Fall back to the default emitter if we failed to perform content negotiation
-            if emitter is None:
-                emitter = self.default_emitter
-
+            response = exc.response
 
         # Always add these headers
-        self.response.headers['Allow'] = ', '.join(self.allowed_methods)
-        self.response.headers['Vary'] = 'Authenticate, Allow'
+        response.headers['Allow'] = ', '.join(self.allowed_methods)
+        response.headers['Vary'] = 'Authenticate, Allow'
 
-        # Serialize the response content
-        if self.response.has_content_body:
-            content = emitter(self).emit(output=self.response.cleaned_content)
-        else:
-            content = emitter(self).emit()
+        return self.emit(response)
 
-        # Build the HTTP Response
-        # TODO: Check if emitter.mimetype is underspecified, or if a content-type header has been set
-        resp = HttpResponse(content, mimetype=emitter.media_type, status=self.response.status)
-        for (key, val) in self.response.headers.items():
-            resp[key] = val
-
-        return resp
-
Add a comment to this file

djangorestframework/static/favicon.ico

Added
New image

djangorestframework/static/robots.txt

+User-agent: *
+Disallow: /

djangorestframework/templates/api_login.html

+<html>
+  <head>
+     <link rel="stylesheet" type="text/css" href='{{ADMIN_MEDIA_PREFIX}}css/base.css'/>
+     <link rel="stylesheet" type="text/css" href='{{ADMIN_MEDIA_PREFIX}}css/forms.css'/>
+     <link rel="stylesheet" type="text/css" href='{{ADMIN_MEDIA_PREFIX}}css/login.css' />
+     <style>
+     .form-row {border-bottom: 0.25em !important}</style>
+  </head>
+  <body class="login">
+<div id="container">
+  <div id="header">
+        <div id="branding">
+          <h1 id="site-name">Django REST framework</h1>
+        </div>      
+    </div>
+
+
+<div id="content" class="colM">
+        
+<div id="content-main">
+<form method="post" action="{% url djangorestframework.views.api_login %}" id="login-form">
+{% csrf_token %}
+  <div class="form-row">
+    <label for="id_username">Username:</label> {{ form.username }}
+  </div>
+  <div class="form-row">
+    <label for="id_password">Password:</label> {{ form.password }}
+    <input type="hidden" name="next" value="{{ next }}" />
+  </div>
+  <div class="form-row">
+    <label>&nbsp;</label><input type="submit" value="Log in">
+  </div>
+</form>
+
+<script type="text/javascript">
+document.getElementById('id_username').focus()
+</script>
+</div>
+
+        
+        <br class="clear">
+    </div>
+
+    <div id="footer"></div> 
+
+</div>
+</body>
+</html>

djangorestframework/templates/emitter.html

         "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
 <html xmlns="http://www.w3.org/1999/xhtml">
   <head>
-    <style>
-      pre {border: 1px solid black; padding: 1em; background: #ffd}
-      body {margin: 0; border:0; padding: 0;}
-      span.api {margin: 0.5em 1em}
-      span.auth {float: right; margin-right: 1em}
-      div.header {margin: 0; border:0; padding: 0.25em 0; background: #ddf}
-      div.content {margin: 0 1em;}
-      div.action {border: 1px solid black; padding: 0.5em 1em; margin-bottom: 0.5em; background: #ddf}
-      ul.accepttypes {float: right; list-style-type: none; margin: 0; padding: 0}
-      ul.accepttypes li {display: inline;}
-      form div {margin: 0.5em 0}
-	  form div * {vertical-align: top}
-	  form ul.errorlist {display: inline; margin: 0; padding: 0}
-	  form ul.errorlist li {display: inline; color: red;}
-	  .clearing {display: block; margin: 0; padding: 0; clear: both;}
-    </style>
-    <title>API - {{ resource.name }}</title>
+     <style>
+       /* Override some of the Django admin styling */
+       #site-name a {color: #F4F379 !important;}
+       .errorlist {display: inline !important}
+       .errorlist li {display: inline !important; background: white !important; color: black !important; border: 0 !important;}
+     </style>
+     <link rel="stylesheet" type="text/css" href='{{ADMIN_MEDIA_PREFIX}}css/base.css'/>
+     <link rel="stylesheet" type="text/css" href='{{ADMIN_MEDIA_PREFIX}}css/forms.css'/>
+    <title>Django REST framework - {{ name }}</title>
   </head>
   <body>
-	<div class='header'>
-		<span class='api'><a href='http://django-rest-framework.org'>Django REST framework</a></span>
-		<span class='auth'>{% if user.is_active %}Welcome, {{ user }}.{% if logout_url %} <a href='{{ logout_url }}'>Log out</a>{% endif %}{% else %}Not logged in {% if login_url %}<a href='{{ login_url }}'>Log in</a>{% endif %}{% endif %}</span>
+  <div id="container">
+  
+	<div id="header">
+		<div id="branding">
+		  <h1 id="site-name"><a href='http://django-rest-framework.org'>Django REST framework</a></h1>
+		</div>
+		<div id="user-tools">
+		  {% if user.is_active %}Welcome, {{ user }}.{% if logout_url %} <a href='{{ logout_url }}'>Log out</a>{% endif %}{% else %}Anonymous {% if login_url %}<a href='{{ login_url }}'>Log in</a>{% endif %}{% endif %}
+		</div>
 	</div>
-	<div class='content'>
-	    <h1>{{ resource.name }}</h1>
-	    <p>{{ resource.description|linebreaksbr }}</p>
+	
+	<div class="breadcrumbs">
+	{% for breadcrumb_name, breadcrumb_url in breadcrumblist %}
+    <a href="{{breadcrumb_url}}">{{breadcrumb_name}}</a> {% if not forloop.last %}&rsaquo;{% endif %}
+    {% endfor %}
+    </div>
+
+    <div id="content" class="{% block coltype %}colM{% endblock %}">
+
+	<div class='content-main'>
+	    <h1>{{ name }}</h1>
+	    <p>{% if markeddown %}{% autoescape off %}{{ markeddown }}{% endautoescape %}{% else %}{{ description|linebreaksbr }}{% endif %}</p>
+	    <div class='module'>
 	    <pre><b>{{ response.status }} {{ response.status_text }}</b>{% autoescape off %}
 {% for key, val in response.headers.items %}<b>{{ key }}:</b> {{ val|urlize_quoted_links }}
 {% endfor %}
-{{ content|urlize_quoted_links }}</pre>{% endautoescape %}
-	
+{{ content|urlize_quoted_links }}</pre>{% endautoescape %}</div>
+
 	{% if 'GET' in resource.allowed_methods %}
-		<div class='action'>
-			<a href='{{ request.path }}' rel="nofollow">GET</a>
-			<ul class="accepttypes">
-			{% for media_type in resource.emitted_media_types %}
-			  {% with resource.ACCEPT_QUERY_PARAM|add:"="|add:media_type as param %}
-			    <li>[<a href='{{ request.path|add_query_param:param }}' rel="nofollow">{{ media_type }}</a>]</li>
-			  {% endwith %}
-			{% endfor %}
-			</ul>
-			<div class="clearing"></div>
-		</div>
+			<form>
+				<fieldset class='module aligned'>
+				<h2>GET {{ name }}</h2>
+				<div class='submit-row' style='margin: 0; border: 0'>
+				<a href='{{ request.path }}' rel="nofollow" style='float: left'>GET</a>
+				{% for media_type in resource.emitted_media_types %}
+				  {% with resource.ACCEPT_QUERY_PARAM|add:"="|add:media_type as param %}
+				    [<a href='{{ request.path|add_query_param:param }}' rel="nofollow">{{ media_type }}</a>]
+				  {% endwith %}
+				{% endfor %}
+				</div>
+				</fieldset>
+			</form>
 	{% endif %}
 	
 	{% comment %} *** Only display the POST/PUT/DELETE forms if we have a bound form, and if method     ***
 	
 	{% if resource.METHOD_PARAM and form %}
 		{% if 'POST' in resource.allowed_methods %}
-			<div class='action'>
 				<form action="{{ request.path }}" method="post">
+				<fieldset class='module aligned'>
+					<h2>POST {{ name }}</h2>
 				    {% csrf_token %}
 				    {{ form.non_field_errors }}
 					{% for field in form %}
-					<div>
-					    {{ field.label_tag }}:
+					<div class='form-row'>
+					    {{ field.label_tag }}
 					    {{ field }}
-					    {{ field.help_text }}
+					    <span class='help'>{{ field.help_text }}</span>
 					    {{ field.errors }}
 					</div>
 					{% endfor %}
-					<div class="clearing"></div>	
-					<input type="submit" value="POST" />
+					<div class='submit-row' style='margin: 0; border: 0'>
+						<input type="submit" value="POST" class="default" />
+					</div>
+				</fieldset>
 				</form>
-			</div>
 		{% endif %}
 		
 		{% if 'PUT' in resource.allowed_methods %}
-			<div class='action'>
 				<form action="{{ request.path }}" method="post">
+				<fieldset class='module aligned'>
+					<h2>PUT {{ name }}</h2>
 					<input type="hidden" name="{{ resource.METHOD_PARAM }}" value="PUT" />
 					{% csrf_token %}
 					{{ form.non_field_errors }}
 					{% for field in form %}
-					<div>
-					    {{ field.label_tag }}:
+					<div class='form-row'>
+					    {{ field.label_tag }}
 					    {{ field }}
-					    {{ field.help_text }}
+					    <span class='help'>{{ field.help_text }}</span>
 					    {{ field.errors }}			    
 					</div>
 					{% endfor %}
-					<div class="clearing"></div>	
-					<input type="submit" value="PUT" />
+					<div class='submit-row' style='margin: 0; border: 0'>			
+					  <input type="submit" value="PUT" class="default" />
+					</div>
+				</fieldset>
 				</form>
-			</div>
 		{% endif %}
 		
 		{% if 'DELETE' in resource.allowed_methods %}
-			<div class='action'>
 				<form action="{{ request.path }}" method="post">
+				<fieldset class='module aligned'>			
+					<h2>DELETE {{ name }}</h2>
 				    {% csrf_token %}
 					<input type="hidden" name="{{ resource.METHOD_PARAM }}" value="DELETE" />
-					<input type="submit" value="DELETE" />
+					<div class='submit-row' style='margin: 0; border: 0'>			
+					  <input type="submit" value="DELETE" class="default" />
+					</div>
+				</fieldset>
 				</form>
-			</div>
 		{% endif %}
 	{% endif %}
 	</div>
+	</div>
+	</div>
   </body>
 </html>

djangorestframework/templates/emitter.txt

-{{ resource.name }}
+{{ name }}
 
-{{ resource.description }}
+{{ description }}
 
 {% autoescape off %}HTTP/1.0 {{ response.status }} {{ response.status_text }}
 {% for key, val in response.headers.items %}{{ key }}: {{ val }}

djangorestframework/tests.py

-from django.test import Client, TestCase
-from django.core.handlers.wsgi import WSGIRequest
-from djangorestframework.resource import Resource
-
-# From: http://djangosnippets.org/snippets/963/
-class RequestFactory(Client):
-    """
-    Class that lets you create mock Request objects for use in testing.
-    
-    Usage:
-    
-    rf = RequestFactory()
-    get_request = rf.get('/hello/')
-    post_request = rf.post('/submit/', {'foo': 'bar'})
-    
-    This class re-uses the django.test.client.Client interface, docs here:
-    http://www.djangoproject.com/documentation/testing/#the-test-client
-    
-    Once you have a request object you can pass it to any view function, 
-    just as if that view had been hooked up using a URLconf.
-    
-    """
-    def request(self, **request):
-        """
-        Similar to parent class, but returns the request object as soon as it
-        has created it.
-        """
-        environ = {
-            'HTTP_COOKIE': self.cookies,
-            'PATH_INFO': '/',
-            'QUERY_STRING': '',
-            'REQUEST_METHOD': 'GET',
-            'SCRIPT_NAME': '',
-            'SERVER_NAME': 'testserver',
-            'SERVER_PORT': 80,
-            'SERVER_PROTOCOL': 'HTTP/1.1',
-        }
-        environ.update(self.defaults)
-        environ.update(request)
-        return WSGIRequest(environ)
-
-# See: http://www.useragentstring.com/
-MSIE_9_USER_AGENT = 'Mozilla/5.0 (Windows; U; MSIE 9.0; WIndows NT 9.0; en-US))'
-MSIE_8_USER_AGENT = 'Mozilla/5.0 (compatible; MSIE 8.0; Windows NT 5.2; Trident/4.0; Media Center PC 4.0; SLCC1; .NET CLR 3.0.04320)'
-MSIE_7_USER_AGENT = 'Mozilla/5.0 (Windows; U; MSIE 7.0; Windows NT 6.0; en-US)'
-FIREFOX_4_0_USER_AGENT = 'Mozilla/5.0 (Windows; U; Windows NT 6.1; ru; rv:1.9.2.3) Gecko/20100401 Firefox/4.0 (.NET CLR 3.5.30729)'
-CHROME_11_0_USER_AGENT = 'Mozilla/5.0 (Windows; U; Windows NT 6.1; en-US) AppleWebKit/534.17 (KHTML, like Gecko) Chrome/11.0.655.0 Safari/534.17'
-SAFARI_5_0_USER_AGENT = 'Mozilla/5.0 (X11; U; Linux x86_64; en-ca) AppleWebKit/531.2+ (KHTML, like Gecko) Version/5.0 Safari/531.2+'
-OPERA_11_0_MSIE_USER_AGENT = 'Mozilla/4.0 (compatible; MSIE 8.0; X11; Linux x86_64; pl) Opera 11.00'
-OPERA_11_0_OPERA_USER_AGENT = 'Opera/9.80 (X11; Linux x86_64; U; pl) Presto/2.7.62 Version/11.00'
-
-class UserAgentMungingTest(TestCase):
-    """We need to fake up the accept headers when we deal with MSIE.  Blergh.
-    http://www.gethifi.com/blog/browser-rest-http-accept-headers"""
-
-    def setUp(self):
-        class MockResource(Resource):
-            anon_allowed_methods = allowed_methods = ('GET',)
-            def get(self, request, auth):
-                return {'a':1, 'b':2, 'c':3}
-        self.rf = RequestFactory()
-        self.MockResource = MockResource
-
-    def test_munge_msie_accept_header(self):
-        """Send MSIE user agent strings and ensure that we get an HTML response,
-        even if we set a */* accept header."""
-        for user_agent in (MSIE_9_USER_AGENT,
-                           MSIE_8_USER_AGENT,
-                           MSIE_7_USER_AGENT):
-            req = self.rf.get('/', HTTP_ACCEPT='*/*', HTTP_USER_AGENT=user_agent)
-            resp = self.MockResource(req)
-            self.assertEqual(resp['Content-Type'], 'text/html')
-
-    def test_dont_munge_msie_accept_header(self):
-        """Turn off _MUNGE_IE_ACCEPT_HEADER, send MSIE user agent strings and ensure
-        that we get a JSON response if we set a */* accept header."""
-        self.MockResource._MUNGE_IE_ACCEPT_HEADER = False
-
-        for user_agent in (MSIE_9_USER_AGENT,
-                           MSIE_8_USER_AGENT,
-                           MSIE_7_USER_AGENT):
-            req = self.rf.get('/', HTTP_ACCEPT='*/*', HTTP_USER_AGENT=user_agent)
-            resp = self.MockResource(req)
-            self.assertEqual(resp['Content-Type'], 'application/json')
-    
-    def test_dont_munge_nice_browsers_accept_header(self):
-        """Send Non-MSIE user agent strings and ensure that we get a JSON response,
-        if we set a */* Accept header.  (Other browsers will correctly set the Accept header)"""
-        for user_agent in (FIREFOX_4_0_USER_AGENT,
-                           CHROME_11_0_USER_AGENT,
-                           SAFARI_5_0_USER_AGENT,
-                           OPERA_11_0_MSIE_USER_AGENT,
-                           OPERA_11_0_OPERA_USER_AGENT):
-            req = self.rf.get('/', HTTP_ACCEPT='*/*', HTTP_USER_AGENT=user_agent)
-            resp = self.MockResource(req)
-            self.assertEqual(resp['Content-Type'], 'application/json')
-    
-    

djangorestframework/tests/accept.py

 from django.test import TestCase
-from djangorestframework.tests.utils import RequestFactory
+from djangorestframework.compat import RequestFactory
 from djangorestframework.resource import Resource
 
 
                 return {'a':1, 'b':2, 'c':3}
         self.req = RequestFactory()
         self.MockResource = MockResource
+        self.view = MockResource.as_view()
 
     def test_munge_msie_accept_header(self):
         """Send MSIE user agent strings and ensure that we get an HTML response,
                            MSIE_8_USER_AGENT,
                            MSIE_7_USER_AGENT):
             req = self.req.get('/', HTTP_ACCEPT='*/*', HTTP_USER_AGENT=user_agent)
-            resp = self.MockResource(req)
+            resp = self.view(req)
             self.assertEqual(resp['Content-Type'], 'text/html')
 
-    def test_dont_munge_msie_accept_header(self):
-        """Turn off _MUNGE_IE_ACCEPT_HEADER, send MSIE user agent strings and ensure
+    def test_dont_rewrite_msie_accept_header(self):
+        """Turn off REWRITE_IE_ACCEPT_HEADER, send MSIE user agent strings and ensure
         that we get a JSON response if we set a */* accept header."""
-        self.MockResource._MUNGE_IE_ACCEPT_HEADER = False
+        view = self.MockResource.as_view(REWRITE_IE_ACCEPT_HEADER=False)
 
         for user_agent in (MSIE_9_USER_AGENT,
                            MSIE_8_USER_AGENT,
                            MSIE_7_USER_AGENT):
             req = self.req.get('/', HTTP_ACCEPT='*/*', HTTP_USER_AGENT=user_agent)
-            resp = self.MockResource(req)
+            resp = view(req)
             self.assertEqual(resp['Content-Type'], 'application/json')
     
     def test_dont_munge_nice_browsers_accept_header(self):
                            OPERA_11_0_MSIE_USER_AGENT,
                            OPERA_11_0_OPERA_USER_AGENT):
             req = self.req.get('/', HTTP_ACCEPT='*/*', HTTP_USER_AGENT=user_agent)
-            resp = self.MockResource(req)
+            resp = self.view(req)
             self.assertEqual(resp['Content-Type'], 'application/json')
 
 

djangorestframework/tests/authentication.py

+from django.conf.urls.defaults import patterns
+from django.test import TestCase
+from django.test import Client
+from djangorestframework.compat import RequestFactory
+from djangorestframework.resource import Resource
+from django.contrib.auth.models import User
+from django.contrib.auth import login
+
+import base64
+try:
+    import json
+except ImportError:
+    import simplejson as json
+
+class MockResource(Resource):
+    allowed_methods = ('POST',)
+
+    def post(self, request, auth, content):
+        return {'a':1, 'b':2, 'c':3}
+
+urlpatterns = patterns('',
+    (r'^$', MockResource.as_view()),
+)
+
+
+class BasicAuthTests(TestCase):
+    """Basic authentication"""
+    urls = 'djangorestframework.tests.authentication'
+
+    def setUp(self):
+        self.csrf_client = Client(enforce_csrf_checks=True)
+        self.username = 'john'
+        self.email = 'lennon@thebeatles.com'
+        self.password = 'password'
+        self.user = User.objects.create_user(self.username, self.email, self.password)       
+
+    def test_post_form_passing_basic_auth(self):
+        """Ensure POSTing json over basic auth with correct credentials passes and does not require CSRF"""
+        auth = 'Basic %s' % base64.encodestring('%s:%s' % (self.username, self.password)).strip()
+        response = self.csrf_client.post('/', {'example': 'example'}, HTTP_AUTHORIZATION=auth)
+        self.assertEqual(response.status_code, 200)
+
+    def test_post_json_passing_basic_auth(self):
+        """Ensure POSTing form over basic auth with correct credentials passes and does not require CSRF"""
+        auth = 'Basic %s' % base64.encodestring('%s:%s' % (self.username, self.password)).strip()
+        response = self.csrf_client.post('/', json.dumps({'example': 'example'}), 'application/json', HTTP_AUTHORIZATION=auth)
+        self.assertEqual(response.status_code, 200)
+
+    def test_post_form_failing_basic_auth(self):
+        """Ensure POSTing form over basic auth without correct credentials fails"""
+        response = self.csrf_client.post('/', {'example': 'example'})
+        self.assertEqual(response.status_code, 403)
+
+    def test_post_json_failing_basic_auth(self):
+        """Ensure POSTing json over basic auth without correct credentials fails"""
+        response = self.csrf_client.post('/', json.dumps({'example': 'example'}), 'application/json')
+        self.assertEqual(response.status_code, 403)
+
+
+class SessionAuthTests(TestCase):
+    """User session authentication"""
+    urls = 'djangorestframework.tests.authentication'
+
+    def setUp(self):
+        self.csrf_client = Client(enforce_csrf_checks=True)
+        self.non_csrf_client = Client(enforce_csrf_checks=False)
+        self.username = 'john'
+        self.email = 'lennon@thebeatles.com'
+        self.password = 'password'
+        self.user = User.objects.create_user(self.username, self.email, self.password)       
+
+    def tearDown(self):
+        self.csrf_client.logout()
+
+    def test_post_form_session_auth_failing_csrf(self):
+        """Ensure POSTing form over session authentication without CSRF token fails."""
+        self.csrf_client.login(username=self.username, password=self.password)
+        response = self.csrf_client.post('/', {'example': 'example'})
+        self.assertEqual(response.status_code, 403)
+
+    def test_post_form_session_auth_passing(self):
+        """Ensure POSTing form over session authentication with logged in user and CSRF token passes."""
+        self.non_csrf_client.login(username=self.username, password=self.password)
+        response = self.non_csrf_client.post('/', {'example': 'example'})
+        self.assertEqual(response.status_code, 200)
+
+    def test_post_form_session_auth_failing(self):
+        """Ensure POSTing form over session authentication without logged in user fails."""
+        response = self.csrf_client.post('/', {'example': 'example'})
+        self.assertEqual(response.status_code, 403)

djangorestframework/tests/breadcrumbs.py

+from django.conf.urls.defaults import patterns, url
+from django.test import TestCase
+from djangorestframework.breadcrumbs import get_breadcrumbs
+from djangorestframework.resource import Resource
+
+class Root(Resource):
+    pass
+
+class ResourceRoot(Resource):
+    pass
+
+class ResourceInstance(Resource):
+    pass
+
+class NestedResourceRoot(Resource):
+    pass
+
+class NestedResourceInstance(Resource):
+    pass
+
+urlpatterns = patterns('',
+    url(r'^$', Root),
+    url(r'^resource/$', ResourceRoot),
+    url(r'^resource/(?P<key>[0-9]+)$', ResourceInstance),
+    url(r'^resource/(?P<key>[0-9]+)/$', NestedResourceRoot),
+    url(r'^resource/(?P<key>[0-9]+)/(?P<other>[A-Za-z]+)$', NestedResourceInstance),
+)
+
+
+class BreadcrumbTests(TestCase):
+    """Tests the breadcrumb functionality used by the HTML emitter."""
+
+    urls = 'djangorestframework.tests.breadcrumbs'
+
+    def test_root_breadcrumbs(self):
+        url = '/'
+        self.assertEqual(get_breadcrumbs(url), [('Root', '/')])
+
+    def test_resource_root_breadcrumbs(self):
+        url = '/resource/'
+        self.assertEqual(get_breadcrumbs(url), [('Root', '/'),
+                                            ('Resource Root', '/resource/')])
+
+    def test_resource_instance_breadcrumbs(self):
+        url = '/resource/123'
+        self.assertEqual(get_breadcrumbs(url), [('Root', '/'),
+                                            ('Resource Root', '/resource/'),
+                                            ('Resource Instance', '/resource/123')])
+
+    def test_nested_resource_breadcrumbs(self):
+        url = '/resource/123/'
+        self.assertEqual(get_breadcrumbs(url), [('Root', '/'),
+                                            ('Resource Root', '/resource/'),
+                                            ('Resource Instance', '/resource/123'),
+                                            ('Nested Resource Root', '/resource/123/')])
+
+    def test_nested_resource_instance_breadcrumbs(self):
+        url = '/resource/123/abc'
+        self.assertEqual(get_breadcrumbs(url), [('Root', '/'),
+                                            ('Resource Root', '/resource/'),
+                                            ('Resource Instance', '/resource/123'),
+                                            ('Nested Resource Root', '/resource/123/'),
+                                            ('Nested Resource Instance', '/resource/123/abc')])
+
+    def test_broken_url_breadcrumbs_handled_gracefully(self):
+        url = '/foobar'
+        self.assertEqual(get_breadcrumbs(url), [('Root', '/')])

djangorestframework/tests/content.py

 from django.test import TestCase
-from djangorestframework.tests.utils import RequestFactory
+from djangorestframework.compat import RequestFactory
 from djangorestframework.content import ContentMixin, StandardContentMixin, OverloadedContentMixin
 
 

djangorestframework/tests/description.py

+from django.test import TestCase
+from djangorestframework.resource import Resource
+from djangorestframework.markdownwrapper import apply_markdown
+from djangorestframework.description import get_name, get_description
+
+# We check that docstrings get nicely un-indented.
+DESCRIPTION = """an example docstring
+====================
+
+* list
+* list
+
+another header
+--------------
+
+    code block
+
+indented
+
+# hash style header #"""
+
+# If markdown is installed we also test it's working (and that our wrapped forces '=' to h2 and '-' to h3)
+MARKED_DOWN = """<h2>an example docstring</h2>
+<ul>
+<li>list</li>
+<li>list</li>
+</ul>
+<h3>another header</h3>
+<pre><code>code block
+</code></pre>
+<p>indented</p>
+<h2 id="hash_style_header">hash style header</h2>"""
+
+
+class TestResourceNamesAndDescriptions(TestCase):
+    def test_resource_name_uses_classname_by_default(self):
+        """Ensure Resource names are based on the classname by default."""
+        class MockResource(Resource):
+            pass
+        self.assertEquals(get_name(MockResource()), 'Mock Resource')
+
+    def test_resource_name_can_be_set_explicitly(self):
+        """Ensure Resource names can be set using the 'name' class attribute."""
+        example = 'Some Other Name'
+        class MockResource(Resource):
+            name = example
+        self.assertEquals(get_name(MockResource()), example)
+
+    def test_resource_description_uses_docstring_by_default(self):
+        """Ensure Resource names are based on the docstring by default."""
+        class MockResource(Resource):
+            """an example docstring
+            ====================
+
+            * list
+            * list
+            
+            another header
+            --------------
+
+                code block
+
+            indented
+            
+            # hash style header #"""
+        
+        self.assertEquals(get_description(MockResource()), DESCRIPTION)
+
+    def test_resource_description_can_be_set_explicitly(self):
+        """Ensure Resource descriptions can be set using the 'description' class attribute."""
+        example = 'Some other description'
+        class MockResource(Resource):
+            """docstring"""
+            description = example
+        self.assertEquals(get_description(MockResource()), example)
+ 
+    def test_resource_description_does_not_require_docstring(self):
+        """Ensure that empty docstrings do not affect the Resource's description if it has been set using the 'description' class attribute."""
+        example = 'Some other description'
+        class MockResource(Resource):
+            description = example
+        self.assertEquals(get_description(MockResource()), example)
+
+    def test_resource_description_can_be_empty(self):
+        """Ensure that if a resource has no doctring or 'description' class attribute, then it's description is the empty string"""
+        class MockResource(Resource):
+            pass
+        self.assertEquals(get_description(MockResource()), '')
+  
+    def test_markdown(self):
+        """Ensure markdown to HTML works as expected"""
+        if apply_markdown:
+            self.assertEquals(apply_markdown(DESCRIPTION), MARKED_DOWN)

djangorestframework/tests/emitters.py

+from django.conf.urls.defaults import patterns, url
+from django import http
+from django.test import TestCase
+from djangorestframework.compat import View
+from djangorestframework.emitters import EmitterMixin, BaseEmitter
+from djangorestframework.response import Response
+
+DUMMYSTATUS = 200
+DUMMYCONTENT = 'dummycontent'
+
+EMITTER_A_SERIALIZER = lambda x: 'Emitter A: %s' % x
+EMITTER_B_SERIALIZER = lambda x: 'Emitter B: %s' % x
+
+class MockView(EmitterMixin, View):
+    def get(self, request):
+        response = Response(DUMMYSTATUS, DUMMYCONTENT)
+        return self.emit(response)
+
+class EmitterA(BaseEmitter):
+    media_type = 'mock/emittera'
+
+    def emit(self, output, verbose=False):
+        return EMITTER_A_SERIALIZER(output)
+
+class EmitterB(BaseEmitter):
+    media_type = 'mock/emitterb'
+
+    def emit(self, output, verbose=False):
+        return EMITTER_B_SERIALIZER(output)
+
+
+urlpatterns = patterns('',
+    url(r'^$', MockView.as_view(emitters=[EmitterA, EmitterB])),
+)
+
+
+class EmitterIntegrationTests(TestCase):
+    """End-to-end testing of emitters using an EmitterMixin on a generic view."""
+
+    urls = 'djangorestframework.tests.emitters'
+
+    def test_default_emitter_serializes_content(self):
+        """If the Accept header is not set the default emitter should serialize the response."""
+        resp = self.client.get('/')
+        self.assertEquals(resp['Content-Type'], EmitterA.media_type)
+        self.assertEquals(resp.content, EMITTER_A_SERIALIZER(DUMMYCONTENT))
+        self.assertEquals(resp.status_code, DUMMYSTATUS)
+
+    def test_default_emitter_serializes_content_on_accept_any(self):
+        """If the Accept header is set to */* the default emitter should serialize the response."""
+        resp = self.client.get('/', HTTP_ACCEPT='*/*')
+        self.assertEquals(resp['Content-Type'], EmitterA.media_type)
+        self.assertEquals(resp.content, EMITTER_A_SERIALIZER(DUMMYCONTENT))
+        self.assertEquals(resp.status_code, DUMMYSTATUS)
+
+    def test_specified_emitter_serializes_content_default_case(self):
+        """If the Accept header is set the specified emitter should serialize the response.
+        (In this case we check that works for the default emitter)"""
+        resp = self.client.get('/', HTTP_ACCEPT=EmitterA.media_type)
+        self.assertEquals(resp['Content-Type'], EmitterA.media_type)
+        self.assertEquals(resp.content, EMITTER_A_SERIALIZER(DUMMYCONTENT))
+        self.assertEquals(resp.status_code, DUMMYSTATUS)
+
+    def test_specified_emitter_serializes_content_non_default_case(self):
+        """If the Accept header is set the specified emitter should serialize the response.
+        (In this case we check that works for a non-default emitter)"""
+        resp = self.client.get('/', HTTP_ACCEPT=EmitterB.media_type)
+        self.assertEquals(resp['Content-Type'], EmitterB.media_type)
+        self.assertEquals(resp.content, EMITTER_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)

djangorestframework/tests/methods.py

 from django.test import TestCase
-from djangorestframework.tests.utils import RequestFactory
+from djangorestframework.compat import RequestFactory
 from djangorestframework.methods import MethodMixin, StandardMethodMixin, OverloadedPOSTMethodMixin
 
 

djangorestframework/tests/response.py

 from django.test import TestCase
 from djangorestframework.response import Response
 
-try:
-    import unittest2
-except:
-    unittest2 = None
-else:
-    import warnings
-    warnings.filterwarnings("ignore")
 
-if unittest2:
-    class TestResponse(TestCase, unittest2.TestCase): 
-    
-        # Interface tests
-    
-        # This is mainly to remind myself that the Response interface needs to change slightly
-        @unittest2.expectedFailure
-        def test_response_interface(self):
-            """Ensure the Response interface is as expected."""
-            response = Response()
-            getattr(response, 'status')
-            getattr(response, 'content')
-            getattr(response, 'headers')
+class TestResponse(TestCase): 
 
+    # Interface tests
+
+    # This is mainly to remind myself that the Response interface needs to change slightly
+    def test_response_interface(self):
+        """Ensure the Response interface is as expected."""
+        response = Response()
+        getattr(response, 'status')
+        getattr(response, 'content')
+        getattr(response, 'headers')
+

djangorestframework/tests/reverse.py

+from django.conf.urls.defaults import patterns, url
+from django.core.urlresolvers import reverse
+from django.test import TestCase
+
+from djangorestframework.resource import Resource
+
+try:
+    import json
+except ImportError:
+    import simplejson as json
+
+
+class MockResource(Resource):
+    """Mock resource which simply returns a URL, so that we can ensure that reversed URLs are fully qualified"""
+    anon_allowed_methods = ('GET',)
+
+    def get(self, request, auth):
+        return reverse('another')
+
+urlpatterns = patterns('',
+    url(r'^$', MockResource.as_view()),
+    url(r'^another$', MockResource.as_view(), name='another'),
+)
+
+
+class ReverseTests(TestCase):
+    """Tests for """
+    urls = 'djangorestframework.tests.reverse'
+
+    def test_reversed_urls_are_fully_qualified(self):
+        response = self.client.get('/')
+        self.assertEqual(json.loads(response.content), 'http://testserver/another')

djangorestframework/tests/utils.py

-from django.test import Client
-from django.core.handlers.wsgi import WSGIRequest
-
-# From: http://djangosnippets.org/snippets/963/
-# Lovely stuff
-class RequestFactory(Client):
-    """
-    Class that lets you create mock Request objects for use in testing.
-    
-    Usage:
-    
-    rf = RequestFactory()
-    get_request = rf.get('/hello/')
-    post_request = rf.post('/submit/', {'foo': 'bar'})
-    
-    This class re-uses the django.test.client.Client interface, docs here:
-    http://www.djangoproject.com/documentation/testing/#the-test-client
-    
-    Once you have a request object you can pass it to any view function, 
-    just as if that view had been hooked up using a URLconf.
-