mirror of
https://github.com/encode/django-rest-framework.git
synced 2025-02-03 05:04:31 +03:00
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.
This commit is contained in:
parent
b749b950a1
commit
805aa03ec1
8
AUTHORS
Normal file
8
AUTHORS
Normal file
|
@ -0,0 +1,8 @@
|
|||
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.
|
|
@ -1,4 +0,0 @@
|
|||
Thanks to...
|
||||
|
||||
Jesper Noehr & the django-piston contributors for providing the starting point for this project.
|
||||
Paul Bagwell - Suggestions & bugfixes.
|
|
@ -4,18 +4,23 @@ 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
|
||||
pip install -r requirements.txt # django, pip
|
||||
|
||||
# To build the documentation...
|
||||
# To run the tests...
|
||||
|
||||
pip install -r docs/requirements.txt
|
||||
sphinx-build -c docs -b html -d docs/build docs html
|
||||
cd testproject
|
||||
export PYTHONPATH=..
|
||||
python manage.py test djangorestframework
|
||||
|
||||
# To run the examples...
|
||||
|
||||
pip install -r examples/requirements.txt
|
||||
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
|
|
@ -1,17 +1,41 @@
|
|||
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.
|
||||
|
||||
|
@ -38,7 +62,9 @@ class BasicAuthenticator(BaseAuthenticator):
|
|||
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
|
||||
|
||||
|
|
31
djangorestframework/breadcrumbs.py
Normal file
31
djangorestframework/breadcrumbs.py
Normal file
|
@ -0,0 +1,31 @@
|
|||
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, [])
|
||||
|
128
djangorestframework/compat.py
Normal file
128
djangorestframework/compat.py
Normal file
|
@ -0,0 +1,128 @@
|
|||
"""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)
|
37
djangorestframework/description.py
Normal file
37
djangorestframework/description.py
Normal file
|
@ -0,0 +1,37 @@
|
|||
"""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 ''
|
|
@ -4,21 +4,145 @@ by serializing the output along with documentation regarding the Resource, outpu
|
|||
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,
|
||||
|
@ -51,7 +175,7 @@ class TemplateEmitter(BaseEmitter):
|
|||
if output is NoContent:
|
||||
return ''
|
||||
|
||||
context = RequestContext(self.resource.request, output)
|
||||
context = RequestContext(self.request, output)
|
||||
return self.template.render(context)
|
||||
|
||||
|
||||
|
@ -60,7 +184,7 @@ class DocumentingTemplateEmitter(BaseEmitter):
|
|||
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
|
||||
|
@ -88,21 +212,24 @@ class DocumentingTemplateEmitter(BaseEmitter):
|
|||
|
||||
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
|
||||
|
||||
|
@ -117,6 +244,11 @@ class DocumentingTemplateEmitter(BaseEmitter):
|
|||
"""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):
|
||||
|
@ -143,7 +275,7 @@ class DocumentingTemplateEmitter(BaseEmitter):
|
|||
|
||||
|
||||
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):
|
||||
|
@ -153,24 +285,36 @@ class DocumentingTemplateEmitter(BaseEmitter):
|
|||
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
|
||||
|
||||
|
||||
|
@ -217,5 +361,11 @@ class DocumentingPlainTextEmitter(DocumentingTemplateEmitter):
|
|||
Useful for browsing an API with command line tools."""
|
||||
media_type = 'text/plain'
|
||||
template = 'emitter.txt'
|
||||
|
||||
DEFAULT_EMITTERS = ( JSONEmitter,
|
||||
DocumentingHTMLEmitter,
|
||||
DocumentingXHTMLEmitter,
|
||||
DocumentingPlainTextEmitter,
|
||||
XMLEmitter )
|
||||
|
||||
|
||||
|
|
51
djangorestframework/markdownwrapper.py
Normal file
51
djangorestframework/markdownwrapper.py
Normal file
|
@ -0,0 +1,51 @@
|
|||
"""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
|
|
@ -4,13 +4,14 @@ from django.db.models import Model
|
|||
|
||||
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:
|
||||
|
||||
|
@ -40,50 +41,50 @@ class ModelResource(Resource):
|
|||
# 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)
|
||||
|
||||
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 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
|
||||
|
||||
|
||||
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):
|
||||
|
@ -121,7 +122,7 @@ class ModelResource(Resource):
|
|||
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
|
||||
|
||||
|
@ -308,9 +309,9 @@ class ModelResource(Resource):
|
|||
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
|
||||
|
||||
|
@ -346,7 +347,7 @@ class ModelResource(Resource):
|
|||
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):
|
||||
|
|
|
@ -117,13 +117,7 @@ class FormParser(BaseParser):
|
|||
|
||||
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'
|
||||
|
||||
|
||||
|
|
|
@ -1,15 +1,16 @@
|
|||
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
|
||||
|
@ -21,10 +22,10 @@ import re
|
|||
__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."""
|
||||
|
||||
|
@ -52,67 +53,21 @@ class Resource(ParserMixin, FormValidatorMixin, OverloadedContentMixin, Overload
|
|||
# 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."""
|
||||
|
@ -134,12 +89,6 @@ class Resource(ParserMixin, FormValidatorMixin, OverloadedContentMixin, Overload
|
|||
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."""
|
||||
|
@ -147,36 +96,6 @@ class Resource(ParserMixin, FormValidatorMixin, OverloadedContentMixin, Overload
|
|||
{'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."""
|
||||
|
||||
|
@ -198,76 +117,16 @@ class Resource(ParserMixin, FormValidatorMixin, OverloadedContentMixin, Overload
|
|||
"""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:
|
||||
|
@ -279,12 +138,23 @@ class Resource(ParserMixin, FormValidatorMixin, OverloadedContentMixin, Overload
|
|||
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.
|
||||
|
@ -301,51 +171,34 @@ class Resource(ParserMixin, FormValidatorMixin, OverloadedContentMixin, Overload
|
|||
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()
|
||||
|
||||
# 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
|
||||
return self.emit(response)
|
||||
|
||||
|
|
BIN
djangorestframework/static/favicon.ico
Normal file
BIN
djangorestframework/static/favicon.ico
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.3 KiB |
2
djangorestframework/static/robots.txt
Normal file
2
djangorestframework/static/robots.txt
Normal file
|
@ -0,0 +1,2 @@
|
|||
User-agent: *
|
||||
Disallow: /
|
48
djangorestframework/templates/api_login.html
Normal file
48
djangorestframework/templates/api_login.html
Normal file
|
@ -0,0 +1,48 @@
|
|||
<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> </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>
|
|
@ -3,49 +3,59 @@
|
|||
"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 %}›{% 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 ***
|
||||
|
@ -55,54 +65,63 @@
|
|||
|
||||
{% 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>
|
|
@ -1,6 +1,6 @@
|
|||
{{ 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 }}
|
||||
|
|
|
@ -1,98 +0,0 @@
|
|||
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')
|
||||
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
from django.test import TestCase
|
||||
from djangorestframework.tests.utils import RequestFactory
|
||||
from djangorestframework.compat import RequestFactory
|
||||
from djangorestframework.resource import Resource
|
||||
|
||||
|
||||
|
@ -24,6 +24,7 @@ class UserAgentMungingTest(TestCase):
|
|||
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,
|
||||
|
@ -32,19 +33,19 @@ class UserAgentMungingTest(TestCase):
|
|||
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):
|
||||
|
@ -56,7 +57,7 @@ class UserAgentMungingTest(TestCase):
|
|||
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')
|
||||
|
||||
|
||||
|
|
90
djangorestframework/tests/authentication.py
Normal file
90
djangorestframework/tests/authentication.py
Normal file
|
@ -0,0 +1,90 @@
|
|||
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)
|
67
djangorestframework/tests/breadcrumbs.py
Normal file
67
djangorestframework/tests/breadcrumbs.py
Normal file
|
@ -0,0 +1,67 @@
|
|||
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', '/')])
|
|
@ -1,5 +1,5 @@
|
|||
from django.test import TestCase
|
||||
from djangorestframework.tests.utils import RequestFactory
|
||||
from djangorestframework.compat import RequestFactory
|
||||
from djangorestframework.content import ContentMixin, StandardContentMixin, OverloadedContentMixin
|
||||
|
||||
|
||||
|
|
93
djangorestframework/tests/description.py
Normal file
93
djangorestframework/tests/description.py
Normal file
|
@ -0,0 +1,93 @@
|
|||
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)
|
75
djangorestframework/tests/emitters.py
Normal file
75
djangorestframework/tests/emitters.py
Normal file
|
@ -0,0 +1,75 @@
|
|||
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)
|
|
@ -1,5 +1,5 @@
|
|||
from django.test import TestCase
|
||||
from djangorestframework.tests.utils import RequestFactory
|
||||
from djangorestframework.compat import RequestFactory
|
||||
from djangorestframework.methods import MethodMixin, StandardMethodMixin, OverloadedPOSTMethodMixin
|
||||
|
||||
|
||||
|
|
|
@ -1,25 +1,16 @@
|
|||
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')
|
||||
|
||||
|
|
32
djangorestframework/tests/reverse.py
Normal file
32
djangorestframework/tests/reverse.py
Normal file
|
@ -0,0 +1,32 @@
|
|||
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')
|
|
@ -1,40 +0,0 @@
|
|||
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)
|
|
@ -1,134 +1,75 @@
|
|||
from django import forms
|
||||
from django.db import models
|
||||
from django.test import TestCase
|
||||
from djangorestframework.tests.utils import RequestFactory
|
||||
from djangorestframework.compat import RequestFactory
|
||||
from djangorestframework.validators import ValidatorMixin, FormValidatorMixin, ModelFormValidatorMixin
|
||||
from djangorestframework.response import ResponseException
|
||||
|
||||
|
||||
class TestValidatorMixins(TestCase):
|
||||
def setUp(self):
|
||||
self.req = RequestFactory()
|
||||
|
||||
class MockForm(forms.Form):
|
||||
qwerty = forms.CharField(required=True)
|
||||
|
||||
class MockValidator(FormValidatorMixin):
|
||||
form = MockForm
|
||||
|
||||
class DisabledValidator(FormValidatorMixin):
|
||||
form = None
|
||||
|
||||
self.MockValidator = MockValidator
|
||||
self.DisabledValidator = DisabledValidator
|
||||
|
||||
|
||||
# Interface tests
|
||||
class TestValidatorMixinInterfaces(TestCase):
|
||||
"""Basic tests to ensure that the ValidatorMixin classes expose the expected interfaces"""
|
||||
|
||||
def test_validator_mixin_interface(self):
|
||||
"""Ensure the ContentMixin interface is as expected."""
|
||||
"""Ensure the ValidatorMixin base class interface is as expected."""
|
||||
self.assertRaises(NotImplementedError, ValidatorMixin().validate, None)
|
||||
|
||||
def test_form_validator_mixin_interface(self):
|
||||
"""Ensure the OverloadedContentMixin interface is as expected."""
|
||||
"""Ensure the FormValidatorMixin interface is as expected."""
|
||||
self.assertTrue(issubclass(FormValidatorMixin, ValidatorMixin))
|
||||
getattr(FormValidatorMixin, 'form')
|
||||
getattr(FormValidatorMixin, 'validate')
|
||||
|
||||
def test_model_form_validator_mixin_interface(self):
|
||||
"""Ensure the OverloadedContentMixin interface is as expected."""
|
||||
"""Ensure the ModelFormValidatorMixin interface is as expected."""
|
||||
self.assertTrue(issubclass(ModelFormValidatorMixin, FormValidatorMixin))
|
||||
getattr(ModelFormValidatorMixin, 'model')
|
||||
getattr(ModelFormValidatorMixin, 'form')
|
||||
getattr(ModelFormValidatorMixin, 'fields')
|
||||
getattr(ModelFormValidatorMixin, 'exclude_fields')
|
||||
getattr(ModelFormValidatorMixin, 'validate')
|
||||
|
||||
# Behavioural tests - FormValidatorMixin
|
||||
|
||||
def test_validate_returns_content_unchanged_if_no_form_is_set(self):
|
||||
"""If the form attribute is None then validate(content) should just return the content unmodified."""
|
||||
|
||||
class TestDisabledValidations(TestCase):
|
||||
"""Tests on Validator Mixins with validation disabled by setting form to None"""
|
||||
|
||||
def test_disabled_form_validator_returns_content_unchanged(self):
|
||||
"""If the form attribute is None on FormValidatorMixin then validate(content) should just return the content unmodified."""
|
||||
class DisabledFormValidator(FormValidatorMixin):
|
||||
form = None
|
||||
|
||||
content = {'qwerty':'uiop'}
|
||||
self.assertEqual(self.DisabledValidator().validate(content), content)
|
||||
self.assertEqual(DisabledFormValidator().validate(content), content)
|
||||
|
||||
def test_disabled_form_validator_get_bound_form_returns_none(self):
|
||||
"""If the form attribute is None on FormValidatorMixin then get_bound_form(content) should just return None."""
|
||||
class DisabledFormValidator(FormValidatorMixin):
|
||||
form = None
|
||||
|
||||
def test_get_bound_form_returns_none_if_no_form_is_set(self):
|
||||
"""If the form attribute is None then get_bound_form(content) should just return None."""
|
||||
content = {'qwerty':'uiop'}
|
||||
self.assertEqual(self.DisabledValidator().get_bound_form(content), None)
|
||||
self.assertEqual(DisabledFormValidator().get_bound_form(content), None)
|
||||
|
||||
def test_validate_returns_content_unchanged_if_validates_and_does_not_need_cleanup(self):
|
||||
"""If the content is already valid and clean then validate(content) should just return the content unmodified."""
|
||||
content = {'qwerty':'uiop'}
|
||||
|
||||
self.assertEqual(self.MockValidator().validate(content), content)
|
||||
def test_disabled_model_form_validator_returns_content_unchanged(self):
|
||||
"""If the form attribute is None on FormValidatorMixin then validate(content) should just return the content unmodified."""
|
||||
class DisabledModelFormValidator(ModelFormValidatorMixin):
|
||||
form = None
|
||||
|
||||
def test_form_validation_failure_raises_response_exception(self):
|
||||
"""If form validation fails a ResourceException 400 (Bad Request) should be raised."""
|
||||
content = {}
|
||||
self.assertRaises(ResponseException, self.MockValidator().validate, content)
|
||||
content = {'qwerty':'uiop'}
|
||||
self.assertEqual(DisabledModelFormValidator().validate(content), content)
|
||||
|
||||
def test_validate_does_not_allow_extra_fields(self):
|
||||
"""If some (otherwise valid) content includes fields that are not in the form then validation should fail.
|
||||
It might be okay on normal form submission, but for Web APIs we oughta get strict, as it'll help show up
|
||||
broken clients more easily (eg submitting content with a misnamed field)"""
|
||||
content = {'qwerty': 'uiop', 'extra': 'extra'}
|
||||
self.assertRaises(ResponseException, self.MockValidator().validate, content)
|
||||
def test_disabled_model_form_validator_get_bound_form_returns_none(self):
|
||||
"""If the form attribute is None on FormValidatorMixin then get_bound_form(content) should just return None."""
|
||||
class DisabledModelFormValidator(ModelFormValidatorMixin):
|
||||
form = None
|
||||
|
||||
def test_validate_allows_extra_fields_if_explicitly_set(self):
|
||||
"""If we include an extra_fields paramater on _validate, then allow fields with those names."""
|
||||
content = {'qwerty': 'uiop', 'extra': 'extra'}
|
||||
self.MockValidator()._validate(content, extra_fields=('extra',))
|
||||
content = {'qwerty':'uiop'}
|
||||
self.assertEqual(DisabledModelFormValidator().get_bound_form(content), None)
|
||||
|
||||
def test_validate_checks_for_extra_fields_if_explicitly_set(self):
|
||||
"""If we include an extra_fields paramater on _validate, then fail unless we have fields with those names."""
|
||||
content = {'qwerty': 'uiop'}
|
||||
try:
|
||||
self.MockValidator()._validate(content, extra_fields=('extra',))
|
||||
except ResponseException, exc:
|
||||
self.assertEqual(exc.response.raw_content, {'field-errors': {'extra': ['This field is required.']}})
|
||||
else:
|
||||
self.fail('ResourceException was not raised') #pragma: no cover
|
||||
|
||||
def test_validate_failed_due_to_no_content_returns_appropriate_message(self):
|
||||
"""If validation fails due to no content, ensure the response contains a single non-field error"""
|
||||
content = {}
|
||||
try:
|
||||
self.MockValidator().validate(content)
|
||||
except ResponseException, exc:
|
||||
self.assertEqual(exc.response.raw_content, {'errors': ['No content was supplied.']})
|
||||
else:
|
||||
self.fail('ResourceException was not raised') #pragma: no cover
|
||||
class TestNonFieldErrors(TestCase):
|
||||
"""Tests against form validation errors caused by non-field errors. (eg as might be caused by some custom form validation)"""
|
||||
|
||||
def test_validate_failed_due_to_field_error_returns_appropriate_message(self):
|
||||
"""If validation fails due to a field error, ensure the response contains a single field error"""
|
||||
content = {'qwerty': ''}
|
||||
try:
|
||||
self.MockValidator().validate(content)
|
||||
except ResponseException, exc:
|
||||
self.assertEqual(exc.response.raw_content, {'field-errors': {'qwerty': ['This field is required.']}})
|
||||
else:
|
||||
self.fail('ResourceException was not raised') #pragma: no cover
|
||||
|
||||
def test_validate_failed_due_to_invalid_field_returns_appropriate_message(self):
|
||||
"""If validation fails due to an invalid field, ensure the response contains a single field error"""
|
||||
content = {'qwerty': 'uiop', 'extra': 'extra'}
|
||||
try:
|
||||
self.MockValidator().validate(content)
|
||||
except ResponseException, exc:
|
||||
self.assertEqual(exc.response.raw_content, {'field-errors': {'extra': ['This field does not exist.']}})
|
||||
else:
|
||||
self.fail('ResourceException was not raised') #pragma: no cover
|
||||
|
||||
def test_validate_failed_due_to_multiple_errors_returns_appropriate_message(self):
|
||||
"""If validation for multiple reasons, ensure the response contains each error"""
|
||||
content = {'qwerty': '', 'extra': 'extra'}
|
||||
try:
|
||||
self.MockValidator().validate(content)
|
||||
except ResponseException, exc:
|
||||
self.assertEqual(exc.response.raw_content, {'field-errors': {'qwerty': ['This field is required.'],
|
||||
'extra': ['This field does not exist.']}})
|
||||
else:
|
||||
self.fail('ResourceException was not raised') #pragma: no cover
|
||||
|
||||
def test_validate_failed_due_to_non_field_error_returns_appropriate_message(self):
|
||||
"""If validation for with a non-field error, ensure the response a non-field error"""
|
||||
"""If validation fails with a non-field error, ensure the response a non-field error"""
|
||||
class MockForm(forms.Form):
|
||||
field1 = forms.CharField(required=False)
|
||||
field2 = forms.CharField(required=False)
|
||||
|
@ -148,4 +89,203 @@ class TestValidatorMixins(TestCase):
|
|||
except ResponseException, exc:
|
||||
self.assertEqual(exc.response.raw_content, {'errors': [MockForm.ERROR_TEXT]})
|
||||
else:
|
||||
self.fail('ResourceException was not raised') #pragma: no cover
|
||||
self.fail('ResourceException was not raised') #pragma: no cover
|
||||
|
||||
|
||||
class TestFormValidation(TestCase):
|
||||
"""Tests which check basic form validation.
|
||||
Also includes the same set of tests with a ModelFormValidator for which the form has been explicitly set.
|
||||
(ModelFormValidatorMixin should behave as FormValidatorMixin if form is set rather than relying on the default ModelForm)"""
|
||||
def setUp(self):
|
||||
class MockForm(forms.Form):
|
||||
qwerty = forms.CharField(required=True)
|
||||
|
||||
class MockFormValidator(FormValidatorMixin):
|
||||
form = MockForm
|
||||
|
||||
class MockModelFormValidator(ModelFormValidatorMixin):
|
||||
form = MockForm
|
||||
|
||||
self.MockFormValidator = MockFormValidator
|
||||
self.MockModelFormValidator = MockModelFormValidator
|
||||
|
||||
|
||||
def validation_returns_content_unchanged_if_already_valid_and_clean(self, validator):
|
||||
"""If the content is already valid and clean then validate(content) should just return the content unmodified."""
|
||||
content = {'qwerty':'uiop'}
|
||||
self.assertEqual(validator.validate(content), content)
|
||||
|
||||
def validation_failure_raises_response_exception(self, validator):
|
||||
"""If form validation fails a ResourceException 400 (Bad Request) should be raised."""
|
||||
content = {}
|
||||
self.assertRaises(ResponseException, validator.validate, content)
|
||||
|
||||
def validation_does_not_allow_extra_fields_by_default(self, validator):
|
||||
"""If some (otherwise valid) content includes fields that are not in the form then validation should fail.
|
||||
It might be okay on normal form submission, but for Web APIs we oughta get strict, as it'll help show up
|
||||
broken clients more easily (eg submitting content with a misnamed field)"""
|
||||
content = {'qwerty': 'uiop', 'extra': 'extra'}
|
||||
self.assertRaises(ResponseException, validator.validate, content)
|
||||
|
||||
def validation_allows_extra_fields_if_explicitly_set(self, validator):
|
||||
"""If we include an allowed_extra_fields paramater on _validate, then allow fields with those names."""
|
||||
content = {'qwerty': 'uiop', 'extra': 'extra'}
|
||||
validator._validate(content, allowed_extra_fields=('extra',))
|
||||
|
||||
def validation_does_not_require_extra_fields_if_explicitly_set(self, validator):
|
||||
"""If we include an allowed_extra_fields paramater on _validate, then do not fail if we do not have fields with those names."""
|
||||
content = {'qwerty': 'uiop'}
|
||||
self.assertEqual(validator._validate(content, allowed_extra_fields=('extra',)), content)
|
||||
|
||||
def validation_failed_due_to_no_content_returns_appropriate_message(self, validator):
|
||||
"""If validation fails due to no content, ensure the response contains a single non-field error"""
|
||||
content = {}
|
||||
try:
|
||||
validator.validate(content)
|
||||
except ResponseException, exc:
|
||||
self.assertEqual(exc.response.raw_content, {'errors': ['No content was supplied.']})
|
||||
else:
|
||||
self.fail('ResourceException was not raised') #pragma: no cover
|
||||
|
||||
def validation_failed_due_to_field_error_returns_appropriate_message(self, validator):
|
||||
"""If validation fails due to a field error, ensure the response contains a single field error"""
|
||||
content = {'qwerty': ''}
|
||||
try:
|
||||
validator.validate(content)
|
||||
except ResponseException, exc:
|
||||
self.assertEqual(exc.response.raw_content, {'field-errors': {'qwerty': ['This field is required.']}})
|
||||
else:
|
||||
self.fail('ResourceException was not raised') #pragma: no cover
|
||||
|
||||
def validation_failed_due_to_invalid_field_returns_appropriate_message(self, validator):
|
||||
"""If validation fails due to an invalid field, ensure the response contains a single field error"""
|
||||
content = {'qwerty': 'uiop', 'extra': 'extra'}
|
||||
try:
|
||||
validator.validate(content)
|
||||
except ResponseException, exc:
|
||||
self.assertEqual(exc.response.raw_content, {'field-errors': {'extra': ['This field does not exist.']}})
|
||||
else:
|
||||
self.fail('ResourceException was not raised') #pragma: no cover
|
||||
|
||||
def validation_failed_due_to_multiple_errors_returns_appropriate_message(self, validator):
|
||||
"""If validation for multiple reasons, ensure the response contains each error"""
|
||||
content = {'qwerty': '', 'extra': 'extra'}
|
||||
try:
|
||||
validator.validate(content)
|
||||
except ResponseException, exc:
|
||||
self.assertEqual(exc.response.raw_content, {'field-errors': {'qwerty': ['This field is required.'],
|
||||
'extra': ['This field does not exist.']}})
|
||||
else:
|
||||
self.fail('ResourceException was not raised') #pragma: no cover
|
||||
|
||||
# Tests on FormValidtionMixin
|
||||
|
||||
def test_form_validation_returns_content_unchanged_if_already_valid_and_clean(self):
|
||||
self.validation_returns_content_unchanged_if_already_valid_and_clean(self.MockFormValidator())
|
||||
|
||||
def test_form_validation_failure_raises_response_exception(self):
|
||||
self.validation_failure_raises_response_exception(self.MockFormValidator())
|
||||
|
||||
def test_validation_does_not_allow_extra_fields_by_default(self):
|
||||
self.validation_does_not_allow_extra_fields_by_default(self.MockFormValidator())
|
||||
|
||||
def test_validation_allows_extra_fields_if_explicitly_set(self):
|
||||
self.validation_allows_extra_fields_if_explicitly_set(self.MockFormValidator())
|
||||
|
||||
def test_validation_does_not_require_extra_fields_if_explicitly_set(self):
|
||||
self.validation_does_not_require_extra_fields_if_explicitly_set(self.MockFormValidator())
|
||||
|
||||
def test_validation_failed_due_to_no_content_returns_appropriate_message(self):
|
||||
self.validation_failed_due_to_no_content_returns_appropriate_message(self.MockFormValidator())
|
||||
|
||||
def test_validation_failed_due_to_field_error_returns_appropriate_message(self):
|
||||
self.validation_failed_due_to_field_error_returns_appropriate_message(self.MockFormValidator())
|
||||
|
||||
def test_validation_failed_due_to_invalid_field_returns_appropriate_message(self):
|
||||
self.validation_failed_due_to_invalid_field_returns_appropriate_message(self.MockFormValidator())
|
||||
|
||||
def test_validation_failed_due_to_multiple_errors_returns_appropriate_message(self):
|
||||
self.validation_failed_due_to_multiple_errors_returns_appropriate_message(self.MockFormValidator())
|
||||
|
||||
# Same tests on ModelFormValidtionMixin
|
||||
|
||||
def test_modelform_validation_returns_content_unchanged_if_already_valid_and_clean(self):
|
||||
self.validation_returns_content_unchanged_if_already_valid_and_clean(self.MockModelFormValidator())
|
||||
|
||||
def test_modelform_validation_failure_raises_response_exception(self):
|
||||
self.validation_failure_raises_response_exception(self.MockModelFormValidator())
|
||||
|
||||
def test_modelform_validation_does_not_allow_extra_fields_by_default(self):
|
||||
self.validation_does_not_allow_extra_fields_by_default(self.MockModelFormValidator())
|
||||
|
||||
def test_modelform_validation_allows_extra_fields_if_explicitly_set(self):
|
||||
self.validation_allows_extra_fields_if_explicitly_set(self.MockModelFormValidator())
|
||||
|
||||
def test_modelform_validation_does_not_require_extra_fields_if_explicitly_set(self):
|
||||
self.validation_does_not_require_extra_fields_if_explicitly_set(self.MockModelFormValidator())
|
||||
|
||||
def test_modelform_validation_failed_due_to_no_content_returns_appropriate_message(self):
|
||||
self.validation_failed_due_to_no_content_returns_appropriate_message(self.MockModelFormValidator())
|
||||
|
||||
def test_modelform_validation_failed_due_to_field_error_returns_appropriate_message(self):
|
||||
self.validation_failed_due_to_field_error_returns_appropriate_message(self.MockModelFormValidator())
|
||||
|
||||
def test_modelform_validation_failed_due_to_invalid_field_returns_appropriate_message(self):
|
||||
self.validation_failed_due_to_invalid_field_returns_appropriate_message(self.MockModelFormValidator())
|
||||
|
||||
def test_modelform_validation_failed_due_to_multiple_errors_returns_appropriate_message(self):
|
||||
self.validation_failed_due_to_multiple_errors_returns_appropriate_message(self.MockModelFormValidator())
|
||||
|
||||
|
||||
class TestModelFormValidator(TestCase):
|
||||
"""Tests specific to ModelFormValidatorMixin"""
|
||||
|
||||
def setUp(self):
|
||||
"""Create a validator for a model with two fields and a property."""
|
||||
class MockModel(models.Model):
|
||||
qwerty = models.CharField(max_length=256)
|
||||
uiop = models.CharField(max_length=256, blank=True)
|
||||
|
||||
@property
|
||||
def readonly(self):
|
||||
return 'read only'
|
||||
|
||||
class MockValidator(ModelFormValidatorMixin):
|
||||
model = MockModel
|
||||
|
||||
self.MockValidator = MockValidator
|
||||
|
||||
|
||||
def test_property_fields_are_allowed_on_model_forms(self):
|
||||
"""Validation on ModelForms may include property fields that exist on the Model to be included in the input."""
|
||||
content = {'qwerty':'example', 'uiop': 'example', 'readonly': 'read only'}
|
||||
self.assertEqual(self.MockValidator().validate(content), content)
|
||||
|
||||
def test_property_fields_are_not_required_on_model_forms(self):
|
||||
"""Validation on ModelForms does not require property fields that exist on the Model to be included in the input."""
|
||||
content = {'qwerty':'example', 'uiop': 'example'}
|
||||
self.assertEqual(self.MockValidator().validate(content), content)
|
||||
|
||||
def test_extra_fields_not_allowed_on_model_forms(self):
|
||||
"""If some (otherwise valid) content includes fields that are not in the form then validation should fail.
|
||||
It might be okay on normal form submission, but for Web APIs we oughta get strict, as it'll help show up
|
||||
broken clients more easily (eg submitting content with a misnamed field)"""
|
||||
content = {'qwerty': 'example', 'uiop':'example', 'readonly': 'read only', 'extra': 'extra'}
|
||||
self.assertRaises(ResponseException, self.MockValidator().validate, content)
|
||||
|
||||
def test_validate_requires_fields_on_model_forms(self):
|
||||
"""If some (otherwise valid) content includes fields that are not in the form then validation should fail.
|
||||
It might be okay on normal form submission, but for Web APIs we oughta get strict, as it'll help show up
|
||||
broken clients more easily (eg submitting content with a misnamed field)"""
|
||||
content = {'readonly': 'read only'}
|
||||
self.assertRaises(ResponseException, self.MockValidator().validate, content)
|
||||
|
||||
def test_validate_does_not_require_blankable_fields_on_model_forms(self):
|
||||
"""Test standard ModelForm validation behaviour - fields with blank=True are not required."""
|
||||
content = {'qwerty':'example', 'readonly': 'read only'}
|
||||
self.MockValidator().validate(content)
|
||||
|
||||
def test_model_form_validator_uses_model_forms(self):
|
||||
self.assertTrue(isinstance(self.MockValidator().get_bound_form(), forms.ModelForm))
|
||||
|
||||
|
||||
|
|
43
djangorestframework/tests/views.py
Normal file
43
djangorestframework/tests/views.py
Normal file
|
@ -0,0 +1,43 @@
|
|||
from django.conf.urls.defaults import patterns, url
|
||||
from django.test import TestCase
|
||||
from django.test import Client
|
||||
|
||||
|
||||
urlpatterns = patterns('djangorestframework.views',
|
||||
url(r'^robots.txt$', 'deny_robots'),
|
||||
url(r'^favicon.ico$', 'favicon'),
|
||||
url(r'^accounts/login$', 'api_login'),
|
||||
url(r'^accounts/logout$', 'api_logout'),
|
||||
)
|
||||
|
||||
|
||||
class ViewTests(TestCase):
|
||||
"""Test the extra views djangorestframework provides"""
|
||||
urls = 'djangorestframework.tests.views'
|
||||
|
||||
def test_robots_view(self):
|
||||
"""Ensure the robots view exists"""
|
||||
response = self.client.get('/robots.txt')
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertEqual(response['Content-Type'], 'text/plain')
|
||||
|
||||
def test_favicon_view(self):
|
||||
"""Ensure the favicon view exists"""
|
||||
response = self.client.get('/favicon.ico')
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertEqual(response['Content-Type'], 'image/vnd.microsoft.icon')
|
||||
|
||||
def test_login_view(self):
|
||||
"""Ensure the login view exists"""
|
||||
response = self.client.get('/accounts/login')
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertEqual(response['Content-Type'].split(';')[0], 'text/html')
|
||||
|
||||
def test_logout_view(self):
|
||||
"""Ensure the logout view exists"""
|
||||
response = self.client.get('/accounts/logout')
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertEqual(response['Content-Type'].split(';')[0], 'text/html')
|
||||
|
||||
|
||||
# TODO: Add login/logout behaviour tests
|
|
@ -3,12 +3,18 @@ import xml.etree.ElementTree as ET
|
|||
from django.utils.encoding import smart_unicode
|
||||
from django.utils.xmlutils import SimplerXMLGenerator
|
||||
from django.core.urlresolvers import resolve
|
||||
from django.conf import settings
|
||||
try:
|
||||
import cStringIO as StringIO
|
||||
except ImportError:
|
||||
import StringIO
|
||||
|
||||
|
||||
#def admin_media_prefix(request):
|
||||
# """Adds the ADMIN_MEDIA_PREFIX to the request context."""
|
||||
# return {'ADMIN_MEDIA_PREFIX': settings.ADMIN_MEDIA_PREFIX}
|
||||
|
||||
|
||||
def as_tuple(obj):
|
||||
"""Given obj return a tuple"""
|
||||
if obj is None:
|
||||
|
|
|
@ -22,6 +22,7 @@ class FormValidatorMixin(ValidatorMixin):
|
|||
|
||||
"""The form class that should be used for validation, or None to turn off form validation."""
|
||||
form = None
|
||||
bound_form_instance = None
|
||||
|
||||
def validate(self, content):
|
||||
"""Given some content as input return some cleaned, validated content.
|
||||
|
@ -34,29 +35,39 @@ class FormValidatorMixin(ValidatorMixin):
|
|||
If the 'field-errors' key exists it is a dict of {field name as string: list of errors as strings}."""
|
||||
return self._validate(content)
|
||||
|
||||
def _validate(self, content, extra_fields=()):
|
||||
def _validate(self, content, allowed_extra_fields=()):
|
||||
"""Wrapped by validate to hide the extra_fields option that the ModelValidatorMixin uses.
|
||||
extra_fields is a list of fields which are not defined by the form, but which we still
|
||||
expect to see on the input."""
|
||||
if self.form is None:
|
||||
return content
|
||||
|
||||
bound_form = self.get_bound_form(content)
|
||||
|
||||
# In addition to regular validation we also ensure no additional fields are being passed in...
|
||||
unknown_fields = set(content.keys()) - set(self.form().fields.keys()) - set(extra_fields)
|
||||
if bound_form is None:
|
||||
return content
|
||||
|
||||
self.bound_form_instance = bound_form
|
||||
|
||||
# And that any extra fields we have specified are all present.
|
||||
missing_extra_fields = set(extra_fields) - set(content.keys())
|
||||
seen_fields_set = set(content.keys())
|
||||
form_fields_set = set(bound_form.fields.keys())
|
||||
allowed_extra_fields_set = set(allowed_extra_fields)
|
||||
|
||||
# In addition to regular validation we also ensure no additional fields are being passed in...
|
||||
unknown_fields = seen_fields_set - (form_fields_set | allowed_extra_fields_set)
|
||||
|
||||
# Check using both regular validation, and our stricter no additional fields rule
|
||||
if bound_form.is_valid() and not unknown_fields and not missing_extra_fields:
|
||||
return bound_form.cleaned_data
|
||||
if bound_form.is_valid() and not unknown_fields:
|
||||
# Validation succeeded...
|
||||
cleaned_data = bound_form.cleaned_data
|
||||
|
||||
# Add in any extra fields to the cleaned content...
|
||||
for key in (allowed_extra_fields_set & seen_fields_set) - set(cleaned_data.keys()):
|
||||
cleaned_data[key] = content[key]
|
||||
|
||||
return cleaned_data
|
||||
|
||||
# Validation failed...
|
||||
detail = {}
|
||||
|
||||
if not bound_form.errors and not unknown_fields and not missing_extra_fields:
|
||||
if not bound_form.errors and not unknown_fields:
|
||||
detail = {u'errors': [u'No content was supplied.']}
|
||||
|
||||
else:
|
||||
|
@ -70,10 +81,6 @@ class FormValidatorMixin(ValidatorMixin):
|
|||
# Add any unknown field errors
|
||||
for key in unknown_fields:
|
||||
field_errors[key] = [u'This field does not exist.']
|
||||
|
||||
# Add any missing fields that we required by the extra fields argument
|
||||
for key in missing_extra_fields:
|
||||
field_errors[key] = [u'This field is required.']
|
||||
|
||||
if field_errors:
|
||||
detail[u'field-errors'] = field_errors
|
||||
|
@ -105,8 +112,14 @@ class ModelFormValidatorMixin(FormValidatorMixin):
|
|||
model = None
|
||||
|
||||
"""The list of fields we expect to receive as input. Fields in this list will may be received with
|
||||
raising non-existent field errors, even if they do not exist as fields on the ModelForm."""
|
||||
raising non-existent field errors, even if they do not exist as fields on the ModelForm.
|
||||
|
||||
Setting the fields class attribute causes the exclude_fields class attribute to be disregarded."""
|
||||
fields = None
|
||||
|
||||
"""The list of fields to exclude from the Model. This is only used if the fields class attribute is not set."""
|
||||
exclude_fields = ('id', 'pk')
|
||||
|
||||
|
||||
# TODO: test the different validation here to allow for get get_absolute_url to be supplied on input and not bork out
|
||||
# TODO: be really strict on fields - check they match in the handler methods. (this isn't a validator thing tho.)
|
||||
|
@ -122,8 +135,7 @@ class ModelFormValidatorMixin(FormValidatorMixin):
|
|||
On failure the ResponseException content is a dict which may contain 'errors' and 'field-errors' keys.
|
||||
If the 'errors' key exists it is a list of strings of non-field errors.
|
||||
If the 'field-errors' key exists it is a dict of {field name as string: list of errors as strings}."""
|
||||
extra_fields = set(as_tuple(self.fields)) - set(self.get_bound_form().fields)
|
||||
return self._validate(content, extra_fields)
|
||||
return self._validate(content, allowed_extra_fields=self._property_fields_set)
|
||||
|
||||
|
||||
def get_bound_form(self, content=None):
|
||||
|
@ -131,23 +143,50 @@ class ModelFormValidatorMixin(FormValidatorMixin):
|
|||
|
||||
If the form class attribute has been explicitly set then use that class to create a Form,
|
||||
otherwise if model is set use that class to create a ModelForm, otherwise return None."""
|
||||
|
||||
if self.form:
|
||||
# Use explict Form
|
||||
return super(ModelFormValidatorMixin, self).get_bound_form(content)
|
||||
|
||||
elif self.model:
|
||||
# Fall back to ModelForm which we create on the fly
|
||||
class ModelForm(forms.ModelForm):
|
||||
class OnTheFlyModelForm(forms.ModelForm):
|
||||
class Meta:
|
||||
model = self.model
|
||||
fields = tuple(set.intersection(self.model._meta.fields, self.fields))
|
||||
|
||||
#fields = tuple(self._model_fields_set)
|
||||
|
||||
# Instantiate the ModelForm as appropriate
|
||||
if content and isinstance(content, models.Model):
|
||||
return ModelForm(instance=content)
|
||||
return OnTheFlyModelForm(instance=content)
|
||||
elif content:
|
||||
return ModelForm(content)
|
||||
return ModelForm()
|
||||
return OnTheFlyModelForm(content)
|
||||
return OnTheFlyModelForm()
|
||||
|
||||
# Both form and model not set? Okay bruv, whatevs...
|
||||
return None
|
||||
return None
|
||||
|
||||
|
||||
@property
|
||||
def _model_fields_set(self):
|
||||
"""Return a set containing the names of validated fields on the model."""
|
||||
model_fields = set(field.name for field in self.model._meta.fields)
|
||||
|
||||
if self.fields:
|
||||
return model_fields & set(as_tuple(self.fields))
|
||||
|
||||
return model_fields - set(as_tuple(self.exclude_fields))
|
||||
|
||||
@property
|
||||
def _property_fields_set(self):
|
||||
"""Returns a set containing the names of validated properties on the model."""
|
||||
property_fields = set(attr for attr in dir(self.model) if
|
||||
isinstance(getattr(self.model, attr, None), property)
|
||||
and not attr.startswith('_'))
|
||||
|
||||
if self.fields:
|
||||
return property_fields & set(as_tuple(self.fields))
|
||||
|
||||
return property_fields - set(as_tuple(self.exclude_fields))
|
||||
|
||||
|
||||
|
66
djangorestframework/views.py
Normal file
66
djangorestframework/views.py
Normal file
|
@ -0,0 +1,66 @@
|
|||
from django.contrib.auth.views import *
|
||||
#from django.contrib.sites.models import get_current_site
|
||||
from django.conf import settings
|
||||
from django.http import HttpResponse
|
||||
import base64
|
||||
|
||||
def deny_robots(request):
|
||||
return HttpResponse('User-agent: *\nDisallow: /', mimetype='text/plain')
|
||||
|
||||
def favicon(request):
|
||||
data = 'AAABAAEAEREAAAEAIADwBAAAFgAAACgAAAARAAAAIgAAAAEAIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADLy8tLy8vL3svLy1QAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAy8vLBsvLywkAAAAATkZFS1xUVPqhn57/y8vL0gAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAJmVlQ/GxcXiy8vL88vLy4FdVlXzTkZF/2RdXP/Ly8vty8vLtMvLy5DLy8vty8vLxgAAAAAAAAAAAAAAAAAAAABORkUJTkZF4lNMS/+Lh4f/cWtq/05GRf9ORkX/Vk9O/3JtbP+Ef3//Vk9O/2ljYv/Ly8v5y8vLCQAAAAAAAAAAAAAAAE5GRQlORkX2TkZF/05GRf9ORkX/TkZF/05GRf9ORkX/TkZF/05GRf9ORkX/UElI/8PDw5cAAAAAAAAAAAAAAAAAAAAAAAAAAE5GRZZORkX/TkZF/05GRf9ORkX/TkZF/05GRf9ORkX/TkZF/05GRf+Cfn3/y8vLvQAAAAAAAAAAAAAAAAAAAADLy8tIaWNi805GRf9ORkX/YVpZ/396eV7Ly8t7qaen9lZOTu5ORkX/TkZF/25oZ//Ly8v/y8vLycvLy0gAAAAATkZFSGNcXPpORkX/TkZF/05GRf+ysLDzTkZFe1NLSv6Oior/raur805GRf9ORkX/TkZF/2hiYf+npaX/y8vL5wAAAABORkXnTkZF/05GRf9ORkX/VU1M/8vLy/9PR0b1TkZF/1VNTP/Ly8uQT0dG+E5GRf9ORkX/TkZF/1hRUP3Ly8tmAAAAAE5GRWBORkXkTkZF/05GRf9ORkX/t7a2/355eOpORkX/TkZFkISAf1BORkX/TkZF/05GRf9XT075TkZFZgAAAAAAAAAAAAAAAAAAAABORkXDTkZF/05GRf9lX17/ubi4/8vLy/+2tbT/Yltb/05GRf9ORkX/a2Vk/8vLy5MAAAAAAAAAAAAAAAAAAAAAAAAAAFNLSqNORkX/TkZF/05GRf9ORkX/TkZF/05GRf9ORkX/TkZF/05GRf+Cfn3/y8vL+cvLyw8AAAAAAAAAAAAAAABORkUSTkZF+U5GRf9ORkX/TkZF/05GRf9ORkX/TkZF/05GRf9ORkX/TkZF/1BJSP/CwsLmy8vLDwAAAAAAAAAAAAAAAE5GRRJORkXtTkZF9FFJSJ1ORkXJTkZF/05GRf9ORkX/ZF5d9k5GRZ9ORkXtTkZF5HFsaxUAAAAAAAAAAAAAAAAAAAAAAAAAAE5GRQxORkUJAAAAAAAAAABORkXhTkZF/2JbWv7Ly8tgAAAAAAAAAABORkUGAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAE5GRWBORkX2TkZFYAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA//+AAP9/gAD+P4AA4AOAAMADgADAA4AAwAOAAMMBgACCAIAAAAGAAIBDgADAA4AAwAOAAMADgADAB4AA/H+AAP7/gAA='
|
||||
return HttpResponse(base64.b64decode(data), mimetype='image/vnd.microsoft.icon')
|
||||
|
||||
# BLERGH
|
||||
# Replicate django.contrib.auth.views.login simply so we don't have get users to update TEMPLATE_CONTEXT_PROCESSORS
|
||||
# to add ADMIN_MEDIA_PREFIX to the RequestContext. I don't like this but really really want users to not have to
|
||||
# be making settings changes in order to accomodate django-rest-framework
|
||||
@csrf_protect
|
||||
@never_cache
|
||||
def api_login(request, template_name='api_login.html',
|
||||
redirect_field_name=REDIRECT_FIELD_NAME,
|
||||
authentication_form=AuthenticationForm):
|
||||
"""Displays the login form and handles the login action."""
|
||||
|
||||
redirect_to = request.REQUEST.get(redirect_field_name, '')
|
||||
|
||||
if request.method == "POST":
|
||||
form = authentication_form(data=request.POST)
|
||||
if form.is_valid():
|
||||
# Light security check -- make sure redirect_to isn't garbage.
|
||||
if not redirect_to or ' ' in redirect_to:
|
||||
redirect_to = settings.LOGIN_REDIRECT_URL
|
||||
|
||||
# Heavier security check -- redirects to http://example.com should
|
||||
# not be allowed, but things like /view/?param=http://example.com
|
||||
# should be allowed. This regex checks if there is a '//' *before* a
|
||||
# question mark.
|
||||
elif '//' in redirect_to and re.match(r'[^\?]*//', redirect_to):
|
||||
redirect_to = settings.LOGIN_REDIRECT_URL
|
||||
|
||||
# Okay, security checks complete. Log the user in.
|
||||
auth_login(request, form.get_user())
|
||||
|
||||
if request.session.test_cookie_worked():
|
||||
request.session.delete_test_cookie()
|
||||
|
||||
return HttpResponseRedirect(redirect_to)
|
||||
|
||||
else:
|
||||
form = authentication_form(request)
|
||||
|
||||
request.session.set_test_cookie()
|
||||
|
||||
#current_site = get_current_site(request)
|
||||
|
||||
return render_to_response(template_name, {
|
||||
'form': form,
|
||||
redirect_field_name: redirect_to,
|
||||
#'site': current_site,
|
||||
#'site_name': current_site.name,
|
||||
'ADMIN_MEDIA_PREFIX': settings.ADMIN_MEDIA_PREFIX,
|
||||
}, context_instance=RequestContext(request))
|
||||
|
||||
|
||||
def api_logout(request, next_page=None, template_name='api_login.html', redirect_field_name=REDIRECT_FIELD_NAME):
|
||||
return logout(request, next_page, template_name, redirect_field_name)
|
|
@ -24,13 +24,13 @@ class BlogPost(models.Model):
|
|||
|
||||
@models.permalink
|
||||
def get_absolute_url(self):
|
||||
return ('blogpost.views.BlogPostInstance', (), {'key': self.key})
|
||||
return ('blog-post', (), {'key': self.key})
|
||||
|
||||
@property
|
||||
@models.permalink
|
||||
def comments_url(self):
|
||||
"""Link to a resource which lists all comments for this blog post."""
|
||||
return ('blogpost.views.CommentRoot', (), {'blogpost_id': self.key})
|
||||
return ('comments', (), {'blogpost_id': self.key})
|
||||
|
||||
def __unicode__(self):
|
||||
return self.title
|
||||
|
@ -52,11 +52,11 @@ class Comment(models.Model):
|
|||
|
||||
@models.permalink
|
||||
def get_absolute_url(self):
|
||||
return ('blogpost.views.CommentInstance', (), {'blogpost': self.blogpost.key, 'id': self.id})
|
||||
return ('comment', (), {'blogpost': self.blogpost.key, 'id': self.id})
|
||||
|
||||
@property
|
||||
@models.permalink
|
||||
def blogpost_url(self):
|
||||
"""Link to the blog post resource which this comment corresponds to."""
|
||||
return ('blogpost.views.BlogPostInstance', (), {'key': self.blogpost.key})
|
||||
return ('blog-post', (), {'key': self.blogpost.key})
|
||||
|
||||
|
|
|
@ -1,8 +1,9 @@
|
|||
from django.conf.urls.defaults import patterns
|
||||
from django.conf.urls.defaults import patterns, url
|
||||
from blogpost.views import BlogPosts, BlogPostInstance, Comments, CommentInstance
|
||||
|
||||
urlpatterns = patterns('blogpost.views',
|
||||
(r'^$', 'BlogPostRoot'),
|
||||
(r'^(?P<key>[^/]+)/$', 'BlogPostInstance'),
|
||||
(r'^(?P<blogpost_id>[^/]+)/comments/$', 'CommentRoot'),
|
||||
(r'^(?P<blogpost>[^/]+)/comments/(?P<id>[^/]+)/$', 'CommentInstance'),
|
||||
urlpatterns = patterns('',
|
||||
url(r'^$', BlogPosts.as_view(), name='blog-posts'),
|
||||
url(r'^(?P<key>[^/]+)/$', BlogPostInstance.as_view(), name='blog-post'),
|
||||
url(r'^(?P<blogpost_id>[^/]+)/comments/$', Comments.as_view(), name='comments'),
|
||||
url(r'^(?P<blogpost>[^/]+)/comments/(?P<id>[^/]+)/$', CommentInstance.as_view(), name='comment'),
|
||||
)
|
||||
|
|
|
@ -1,33 +1,33 @@
|
|||
from djangorestframework.response import Response, status
|
||||
from djangorestframework.resource import Resource
|
||||
from djangorestframework.modelresource import ModelResource, RootModelResource
|
||||
from blogpost.models import BlogPost, Comment
|
||||
from blogpost import models
|
||||
|
||||
BLOG_POST_FIELDS = ('created', 'title', 'slug', 'content', 'absolute_url', 'comment_url', 'comments_url')
|
||||
COMMENT_FIELDS = ('username', 'comment', 'created', 'rating', 'absolute_url', 'blogpost_url')
|
||||
|
||||
|
||||
class BlogPostRoot(RootModelResource):
|
||||
class BlogPosts(RootModelResource):
|
||||
"""A resource with which lists all existing blog posts and creates new blog posts."""
|
||||
allowed_methods = ('GET', 'POST',)
|
||||
model = BlogPost
|
||||
anon_allowed_methods = allowed_methods = ('GET', 'POST',)
|
||||
model = models.BlogPost
|
||||
fields = BLOG_POST_FIELDS
|
||||
|
||||
class BlogPostInstance(ModelResource):
|
||||
"""A resource which represents a single blog post."""
|
||||
allowed_methods = ('GET', 'PUT', 'DELETE')
|
||||
model = BlogPost
|
||||
anon_allowed_methods = allowed_methods = ('GET', 'PUT', 'DELETE')
|
||||
model = models.BlogPost
|
||||
fields = BLOG_POST_FIELDS
|
||||
|
||||
class CommentRoot(RootModelResource):
|
||||
class Comments(RootModelResource):
|
||||
"""A resource which lists all existing comments for a given blog post, and creates new blog comments for a given blog post."""
|
||||
allowed_methods = ('GET', 'POST',)
|
||||
model = Comment
|
||||
anon_allowed_methods = allowed_methods = ('GET', 'POST',)
|
||||
model = models.Comment
|
||||
fields = COMMENT_FIELDS
|
||||
|
||||
class CommentInstance(ModelResource):
|
||||
"""A resource which represents a single comment."""
|
||||
allowed_methods = ('GET', 'PUT', 'DELETE')
|
||||
model = Comment
|
||||
anon_allowed_methods = allowed_methods = ('GET', 'PUT', 'DELETE')
|
||||
model = models.Comment
|
||||
fields = COMMENT_FIELDS
|
||||
|
||||
|
|
0
examples/mixin/__init__.py
Normal file
0
examples/mixin/__init__.py
Normal file
23
examples/mixin/urls.py
Normal file
23
examples/mixin/urls.py
Normal file
|
@ -0,0 +1,23 @@
|
|||
from djangorestframework.compat import View # Use Django 1.3's django.views.generic.View, or fall back to a clone of that if Django < 1.3
|
||||
from djangorestframework.emitters import EmitterMixin, DEFAULT_EMITTERS
|
||||
from djangorestframework.response import Response
|
||||
|
||||
from django.conf.urls.defaults import patterns, url
|
||||
from django.core.urlresolvers import reverse
|
||||
|
||||
|
||||
class ExampleView(EmitterMixin, View):
|
||||
"""An example view using Django 1.3's class based views.
|
||||
Uses djangorestframework's EmitterMixin to provide support for multiple output formats."""
|
||||
emitters = DEFAULT_EMITTERS
|
||||
|
||||
def get(self, request):
|
||||
response = Response(200, {'description': 'Some example content',
|
||||
'url': reverse('mixin-view')})
|
||||
return self.emit(response)
|
||||
|
||||
|
||||
urlpatterns = patterns('',
|
||||
url(r'^$', ExampleView.as_view(), name='mixin-view'),
|
||||
)
|
||||
|
|
@ -19,5 +19,5 @@ class MyModel(models.Model):
|
|||
|
||||
@models.permalink
|
||||
def get_absolute_url(self):
|
||||
return ('modelresourceexample.views.MyModelResource', (self.pk,))
|
||||
return ('my-model-resource', (self.pk,))
|
||||
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
from django.conf.urls.defaults import patterns, url
|
||||
from modelresourceexample.views import MyModelRootResource, MyModelResource
|
||||
|
||||
urlpatterns = patterns('modelresourceexample.views',
|
||||
url(r'^$', 'MyModelRootResource'),
|
||||
url(r'^([0-9]+)/$', 'MyModelResource'),
|
||||
url(r'^$', MyModelRootResource.as_view(), name='my-model-root-resource'),
|
||||
url(r'^([0-9]+)/$', MyModelResource.as_view(), name='my-model-resource'),
|
||||
)
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
from django.conf.urls.defaults import patterns
|
||||
|
||||
from django.conf.urls.defaults import patterns, url
|
||||
from objectstore.views import ObjectStoreRoot, StoredObject
|
||||
|
||||
urlpatterns = patterns('objectstore.views',
|
||||
(r'^$', 'ObjectStoreRoot'),
|
||||
(r'^(?P<key>[A-Za-z0-9_-]{1,64})/$', 'StoredObject'),
|
||||
url(r'^$', ObjectStoreRoot.as_view(), name='object-store-root'),
|
||||
url(r'^(?P<key>[A-Za-z0-9_-]{1,64})/$', StoredObject.as_view(), name='stored-object'),
|
||||
)
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
from django.conf import settings
|
||||
from django.core.urlresolvers import reverse
|
||||
|
||||
from djangorestframework.resource import Resource
|
||||
from djangorestframework.response import Response, status
|
||||
|
@ -29,7 +30,7 @@ class ObjectStoreRoot(Resource):
|
|||
def get(self, request, auth):
|
||||
"""Return a list of all the stored object URLs."""
|
||||
keys = sorted(os.listdir(OBJECT_STORE_DIR))
|
||||
return [self.reverse(StoredObject, key=key) for key in keys]
|
||||
return [reverse('stored-object', kwargs={'key':key}) for key in keys]
|
||||
|
||||
def post(self, request, auth, content):
|
||||
"""Create a new stored object, with a unique key."""
|
||||
|
@ -37,9 +38,9 @@ class ObjectStoreRoot(Resource):
|
|||
pathname = os.path.join(OBJECT_STORE_DIR, key)
|
||||
pickle.dump(content, open(pathname, 'wb'))
|
||||
remove_oldest_files(OBJECT_STORE_DIR, MAX_FILES)
|
||||
return Response(status.HTTP_201_CREATED, content, {'Location': self.reverse(StoredObject, key=key)})
|
||||
|
||||
|
||||
return Response(status.HTTP_201_CREATED, content, {'Location': reverse('stored-object', kwargs={'key':key})})
|
||||
|
||||
|
||||
class StoredObject(Resource):
|
||||
"""Represents a stored object.
|
||||
The object may be any picklable content."""
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
from django.conf.urls.defaults import patterns
|
||||
from django.conf.urls.defaults import patterns, url
|
||||
from pygments_api.views import PygmentsRoot, PygmentsInstance
|
||||
|
||||
urlpatterns = patterns('pygments_api.views',
|
||||
(r'^$', 'PygmentsRoot'),
|
||||
(r'^([a-zA-Z0-9-]+)/$', 'PygmentsInstance'),
|
||||
urlpatterns = patterns('',
|
||||
url(r'^$', PygmentsRoot.as_view(), name='pygments-root'),
|
||||
url(r'^([a-zA-Z0-9-]+)/$', PygmentsInstance.as_view(), name='pygments-instance'),
|
||||
)
|
||||
|
|
|
@ -1,4 +1,6 @@
|
|||
from __future__ import with_statement # for python 2.5
|
||||
from django.conf import settings
|
||||
from django.core.urlresolvers import reverse
|
||||
|
||||
from djangorestframework.resource import Resource
|
||||
from djangorestframework.response import Response, status
|
||||
|
@ -41,7 +43,7 @@ class PygmentsRoot(Resource):
|
|||
def get(self, request, auth):
|
||||
"""Return a list of all currently existing snippets."""
|
||||
unique_ids = sorted(os.listdir(HIGHLIGHTED_CODE_DIR))
|
||||
return [self.reverse(PygmentsInstance, unique_id) for unique_id in unique_ids]
|
||||
return [reverse('pygments-instance', args=[unique_id]) for unique_id in unique_ids]
|
||||
|
||||
def post(self, request, auth, content):
|
||||
"""Create a new highlighed snippet and return it's location.
|
||||
|
@ -59,7 +61,7 @@ class PygmentsRoot(Resource):
|
|||
|
||||
remove_oldest_files(HIGHLIGHTED_CODE_DIR, MAX_FILES)
|
||||
|
||||
return Response(status.HTTP_201_CREATED, headers={'Location': self.reverse(PygmentsInstance, unique_id)})
|
||||
return Response(status.HTTP_201_CREATED, headers={'Location': reverse('pygments-instance', args=[unique_id])})
|
||||
|
||||
|
||||
class PygmentsInstance(Resource):
|
||||
|
|
|
@ -4,3 +4,5 @@ Django==1.2.4
|
|||
wsgiref==0.1.2
|
||||
Pygments==1.4
|
||||
httplib2==0.6.0
|
||||
Markdown==2.0.3
|
||||
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
from django.conf.urls.defaults import patterns, url
|
||||
from resourceexample.views import ExampleResource, AnotherExampleResource
|
||||
|
||||
urlpatterns = patterns('resourceexample.views',
|
||||
url(r'^$', 'ExampleResource'),
|
||||
url(r'^(?P<num>[0-9]+)/$', 'AnotherExampleResource'),
|
||||
urlpatterns = patterns('',
|
||||
url(r'^$', ExampleResource.as_view(), name='example-resource'),
|
||||
url(r'^(?P<num>[0-9]+)/$', AnotherExampleResource.as_view(), name='another-example-resource'),
|
||||
)
|
||||
|
|
|
@ -1,13 +1,14 @@
|
|||
from django.core.urlresolvers import reverse
|
||||
from djangorestframework.resource import Resource
|
||||
from djangorestframework.response import Response, status
|
||||
from resourceexample.forms import MyForm
|
||||
|
||||
class ExampleResource(Resource):
|
||||
"""A basic read only resource that points to 3 other resources."""
|
||||
"""A basic read-only resource that points to 3 other resources."""
|
||||
allowed_methods = anon_allowed_methods = ('GET',)
|
||||
|
||||
def get(self, request, auth):
|
||||
return {"Some other resources": [self.reverse(AnotherExampleResource, num=num) for num in range(3)]}
|
||||
return {"Some other resources": [reverse('another-example-resource', kwargs={'num':num}) for num in range(3)]}
|
||||
|
||||
class AnotherExampleResource(Resource):
|
||||
"""A basic GET-able/POST-able resource."""
|
||||
|
|
0
examples/sandbox/__init__.py
Normal file
0
examples/sandbox/__init__.py
Normal file
35
examples/sandbox/views.py
Normal file
35
examples/sandbox/views.py
Normal file
|
@ -0,0 +1,35 @@
|
|||
"""The root view for the examples provided with Django REST framework"""
|
||||
|
||||
from django.core.urlresolvers import reverse
|
||||
from djangorestframework.resource import Resource
|
||||
|
||||
|
||||
class Sandbox(Resource):
|
||||
"""This is the sandbox for the examples provided with [Django REST framework](http://django-rest-framework.org).
|
||||
|
||||
These examples are provided to help you get a better idea of the some of the features of RESTful APIs created using the framework.
|
||||
|
||||
All the example APIs allow anonymous access, and can be navigated either through the browser or from the command line...
|
||||
|
||||
bash: curl -X GET http://api.django-rest-framework.org/ # (Use default emitter)
|
||||
bash: curl -X GET http://api.django-rest-framework.org/ -H 'Accept: text/plain' # (Use plaintext documentation emitter)
|
||||
|
||||
The examples provided:
|
||||
|
||||
1. A basic example using the [Resource](http://django-rest-framework.org/library/resource.html) class.
|
||||
2. A basic example using the [ModelResource](http://django-rest-framework.org/library/modelresource.html) class.
|
||||
3. An basic example using Django 1.3's [class based views](http://docs.djangoproject.com/en/dev/topics/class-based-views/) and djangorestframework's [EmitterMixin](http://django-rest-framework.org/library/emitters.html).
|
||||
4. A generic object store API.
|
||||
5. A code highlighting API.
|
||||
6. A blog posts and comments API.
|
||||
|
||||
Please feel free to browse, create, edit and delete the resources in these examples."""
|
||||
allowed_methods = anon_allowed_methods = ('GET',)
|
||||
|
||||
def get(self, request, auth):
|
||||
return [{'name': 'Simple Resource example', 'url': reverse('example-resource')},
|
||||
{'name': 'Simple ModelResource example', 'url': reverse('my-model-root-resource')},
|
||||
{'name': 'Simple Mixin-only example', 'url': reverse('mixin-view')},
|
||||
{'name': 'Object store API', 'url': reverse('object-store-root')},
|
||||
{'name': 'Code highlighting API', 'url': reverse('pygments-root')},
|
||||
{'name': 'Blog posts API', 'url': reverse('blog-posts')}]
|
|
@ -1,7 +1,4 @@
|
|||
# Django settings for src project.
|
||||
import os
|
||||
|
||||
BASE_DIR = os.path.dirname(__file__)
|
||||
# Settings for djangorestframework examples project
|
||||
|
||||
DEBUG = True
|
||||
TEMPLATE_DEBUG = DEBUG
|
||||
|
@ -48,17 +45,23 @@ USE_L10N = True
|
|||
|
||||
# Absolute filesystem path to the directory that will hold user-uploaded files.
|
||||
# Example: "/home/media/media.lawrence.com/"
|
||||
# NOTE: Some of the djangorestframework examples use MEDIA_ROOT to store content.
|
||||
MEDIA_ROOT = 'media/'
|
||||
|
||||
# URL that handles the media served from MEDIA_ROOT. Make sure to use a
|
||||
# trailing slash if there is a path component (optional in other cases).
|
||||
# Examples: "http://media.lawrence.com", "http://example.com/media/"
|
||||
# NOTE: None of the djangorestframework examples serve media content via MEDIA_URL.
|
||||
MEDIA_URL = ''
|
||||
|
||||
# URL prefix for admin media -- CSS, JavaScript and images. Make sure to use a
|
||||
# trailing slash.
|
||||
# Examples: "http://foo.com/media/", "/media/".
|
||||
ADMIN_MEDIA_PREFIX = '/media/'
|
||||
# NOTE: djangorestframework does not require the admin app to be installed,
|
||||
# but it does require the admin media be served. Django's test server will do
|
||||
# this for you automatically, but in production you'll want to make sure you
|
||||
# serve the admin media from somewhere.
|
||||
ADMIN_MEDIA_PREFIX = '/admin-media/'
|
||||
|
||||
# Make this unique, and don't share it with anybody.
|
||||
SECRET_KEY = 't&9mru2_k$t8e2-9uq-wu2a1)9v*us&j3i#lsqkt(lbx*vh1cu'
|
||||
|
@ -84,16 +87,16 @@ TEMPLATE_DIRS = (
|
|||
# Put strings here, like "/home/html/django_templates" or "C:/www/django/templates".
|
||||
# Always use forward slashes, even on Windows.
|
||||
# Don't forget to use absolute paths, not relative paths.
|
||||
os.path.join(BASE_DIR, 'templates')
|
||||
)
|
||||
|
||||
|
||||
INSTALLED_APPS = (
|
||||
'django.contrib.auth',
|
||||
'django.contrib.contenttypes',
|
||||
'django.contrib.sessions',
|
||||
'django.contrib.sites',
|
||||
'django.contrib.messages',
|
||||
#'django.contrib.admin',
|
||||
|
||||
'djangorestframework',
|
||||
|
||||
'resourceexample',
|
||||
|
|
|
@ -1,7 +0,0 @@
|
|||
<html>
|
||||
<head>
|
||||
</head>
|
||||
<body>
|
||||
{% block content %}{% endblock %}
|
||||
</body>
|
||||
</html>
|
|
@ -1,26 +0,0 @@
|
|||
{% extends "base.html" %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
{% if form.errors %}
|
||||
<p>Your username and password didn't match. Please try again.</p>
|
||||
{% endif %}
|
||||
|
||||
<form method="post" action="{% url django.contrib.auth.views.login %}">
|
||||
{% csrf_token %}
|
||||
<table>
|
||||
<tr>
|
||||
<td>{{ form.username.label_tag }}</td>
|
||||
<td>{{ form.username }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{{ form.password.label_tag }}</td>
|
||||
<td>{{ form.password }}</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<input type="submit" value="login" />
|
||||
<input type="hidden" name="next" value="{{ next }}" />
|
||||
</form>
|
||||
|
||||
{% endblock %}
|
|
@ -1,37 +1,19 @@
|
|||
from django.conf.urls.defaults import patterns, include
|
||||
#from django.contrib import admin
|
||||
from djangorestframework.resource import Resource
|
||||
from django.conf.urls.defaults import patterns, include, url
|
||||
from sandbox.views import Sandbox
|
||||
|
||||
#admin.autodiscover()
|
||||
urlpatterns = patterns('djangorestframework.views',
|
||||
(r'robots.txt', 'deny_robots'),
|
||||
(r'favicon.ico', 'favicon'),
|
||||
|
||||
class RootResource(Resource):
|
||||
"""This is the sandbox for the examples provided with django-rest-framework.
|
||||
(r'^$', Sandbox.as_view()),
|
||||
|
||||
These examples are here to help you get a better idea of the some of the
|
||||
features of django-rest-framework API, such as automatic form and model validation,
|
||||
support for multiple input and output media types, etc...
|
||||
|
||||
Please feel free to browse, create, edit and delete the resources here, either
|
||||
in the browser, from the command line, or programmatically."""
|
||||
allowed_methods = anon_allowed_methods = ('GET',)
|
||||
|
||||
def get(self, request, auth):
|
||||
return {'Simple Resource example': self.reverse('resourceexample.views.ExampleResource'),
|
||||
'Simple ModelResource example': self.reverse('modelresourceexample.views.MyModelRootResource'),
|
||||
'Object store API (Resource)': self.reverse('objectstore.views.ObjectStoreRoot'),
|
||||
'A pygments pastebin API (Resource + forms)': self.reverse('pygments_api.views.PygmentsRoot'),
|
||||
'Blog posts API (ModelResource)': self.reverse('blogpost.views.BlogPostRoot'),}
|
||||
|
||||
|
||||
urlpatterns = patterns('',
|
||||
(r'^$', RootResource),
|
||||
(r'^model-resource-example/', include('modelresourceexample.urls')),
|
||||
(r'^resource-example/', include('resourceexample.urls')),
|
||||
(r'^model-resource-example/', include('modelresourceexample.urls')),
|
||||
(r'^mixin/', include('mixin.urls')),
|
||||
(r'^object-store/', include('objectstore.urls')),
|
||||
(r'^pygments/', include('pygments_api.urls')),
|
||||
(r'^blog-post/', include('blogpost.urls')),
|
||||
(r'^accounts/login/$', 'django.contrib.auth.views.login'),
|
||||
(r'^accounts/logout/$', 'django.contrib.auth.views.logout'),
|
||||
#(r'^admin/doc/', include('django.contrib.admindocs.urls')),
|
||||
#(r'^admin/', include(admin.site.urls)),
|
||||
|
||||
(r'^accounts/login/$', 'api_login'),
|
||||
(r'^accounts/logout/$', 'api_logout'),
|
||||
)
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
# Django and pip are required if installing into a virtualenv environment...
|
||||
# We need Django. Duh.
|
||||
|
||||
Django==1.2.4
|
||||
distribute==0.6.14
|
||||
wsgiref==0.1.2
|
||||
|
||||
|
|
0
testproject/__init__.py
Normal file
0
testproject/__init__.py
Normal file
11
testproject/manage.py
Executable file
11
testproject/manage.py
Executable file
|
@ -0,0 +1,11 @@
|
|||
#!/usr/bin/env python
|
||||
from django.core.management import execute_manager
|
||||
try:
|
||||
import settings # Assumed to be in the same directory.
|
||||
except ImportError:
|
||||
import sys
|
||||
sys.stderr.write("Error: Can't find the file 'settings.py' in the directory containing %r. It appears you've customized things.\nYou'll have to run django-admin.py, passing it your settings module.\n(If the file settings.py does indeed exist, it's causing an ImportError somehow.)\n" % __file__)
|
||||
sys.exit(1)
|
||||
|
||||
if __name__ == "__main__":
|
||||
execute_manager(settings)
|
97
testproject/settings.py
Normal file
97
testproject/settings.py
Normal file
|
@ -0,0 +1,97 @@
|
|||
# Django settings for testproject project.
|
||||
|
||||
DEBUG = True
|
||||
TEMPLATE_DEBUG = DEBUG
|
||||
|
||||
ADMINS = (
|
||||
# ('Your Name', 'your_email@domain.com'),
|
||||
)
|
||||
|
||||
MANAGERS = ADMINS
|
||||
|
||||
DATABASES = {
|
||||
'default': {
|
||||
'ENGINE': 'django.db.backends.sqlite3', # Add 'postgresql_psycopg2', 'postgresql', 'mysql', 'sqlite3' or 'oracle'.
|
||||
'NAME': 'sqlite.db', # Or path to database file if using sqlite3.
|
||||
'USER': '', # Not used with sqlite3.
|
||||
'PASSWORD': '', # Not used with sqlite3.
|
||||
'HOST': '', # Set to empty string for localhost. Not used with sqlite3.
|
||||
'PORT': '', # Set to empty string for default. Not used with sqlite3.
|
||||
}
|
||||
}
|
||||
|
||||
# Local time zone for this installation. Choices can be found here:
|
||||
# http://en.wikipedia.org/wiki/List_of_tz_zones_by_name
|
||||
# although not all choices may be available on all operating systems.
|
||||
# On Unix systems, a value of None will cause Django to use the same
|
||||
# timezone as the operating system.
|
||||
# If running in a Windows environment this must be set to the same as your
|
||||
# system time zone.
|
||||
TIME_ZONE = 'Europe/London'
|
||||
|
||||
# Language code for this installation. All choices can be found here:
|
||||
# http://www.i18nguy.com/unicode/language-identifiers.html
|
||||
LANGUAGE_CODE = 'en-uk'
|
||||
|
||||
SITE_ID = 1
|
||||
|
||||
# If you set this to False, Django will make some optimizations so as not
|
||||
# to load the internationalization machinery.
|
||||
USE_I18N = True
|
||||
|
||||
# If you set this to False, Django will not format dates, numbers and
|
||||
# calendars according to the current locale
|
||||
USE_L10N = True
|
||||
|
||||
# Absolute filesystem path to the directory that will hold user-uploaded files.
|
||||
# Example: "/home/media/media.lawrence.com/"
|
||||
MEDIA_ROOT = ''
|
||||
|
||||
# URL that handles the media served from MEDIA_ROOT. Make sure to use a
|
||||
# trailing slash if there is a path component (optional in other cases).
|
||||
# Examples: "http://media.lawrence.com", "http://example.com/media/"
|
||||
MEDIA_URL = ''
|
||||
|
||||
# URL prefix for admin media -- CSS, JavaScript and images. Make sure to use a
|
||||
# trailing slash.
|
||||
# Examples: "http://foo.com/media/", "/media/".
|
||||
ADMIN_MEDIA_PREFIX = '/media/'
|
||||
|
||||
# Make this unique, and don't share it with anybody.
|
||||
SECRET_KEY = 'u@x-aj9(hoh#rb-^ymf#g2jx_hp0vj7u5#b@ag1n^seu9e!%cy'
|
||||
|
||||
# List of callables that know how to import templates from various sources.
|
||||
TEMPLATE_LOADERS = (
|
||||
'django.template.loaders.filesystem.Loader',
|
||||
'django.template.loaders.app_directories.Loader',
|
||||
# 'django.template.loaders.eggs.Loader',
|
||||
)
|
||||
|
||||
MIDDLEWARE_CLASSES = (
|
||||
'django.middleware.common.CommonMiddleware',
|
||||
'django.contrib.sessions.middleware.SessionMiddleware',
|
||||
'django.middleware.csrf.CsrfViewMiddleware',
|
||||
'django.contrib.auth.middleware.AuthenticationMiddleware',
|
||||
'django.contrib.messages.middleware.MessageMiddleware',
|
||||
)
|
||||
|
||||
ROOT_URLCONF = 'urls'
|
||||
|
||||
TEMPLATE_DIRS = (
|
||||
# Put strings here, like "/home/html/django_templates" or "C:/www/django/templates".
|
||||
# Always use forward slashes, even on Windows.
|
||||
# Don't forget to use absolute paths, not relative paths.
|
||||
)
|
||||
|
||||
INSTALLED_APPS = (
|
||||
'django.contrib.auth',
|
||||
'django.contrib.contenttypes',
|
||||
'django.contrib.sessions',
|
||||
'django.contrib.sites',
|
||||
'django.contrib.messages',
|
||||
# Uncomment the next line to enable the admin:
|
||||
# 'django.contrib.admin',
|
||||
# Uncomment the next line to enable admin documentation:
|
||||
# 'django.contrib.admindocs',
|
||||
'djangorestframework',
|
||||
)
|
16
testproject/urls.py
Normal file
16
testproject/urls.py
Normal file
|
@ -0,0 +1,16 @@
|
|||
from django.conf.urls.defaults import *
|
||||
|
||||
# Uncomment the next two lines to enable the admin:
|
||||
# from django.contrib import admin
|
||||
# admin.autodiscover()
|
||||
|
||||
urlpatterns = patterns('',
|
||||
# Example:
|
||||
# (r'^testproject/', include('testproject.foo.urls')),
|
||||
|
||||
# Uncomment the admin/doc line below to enable admin documentation:
|
||||
# (r'^admin/doc/', include('django.contrib.admindocs.urls')),
|
||||
|
||||
# Uncomment the next line to enable the admin:
|
||||
# (r'^admin/', include(admin.site.urls)),
|
||||
)
|
Loading…
Reference in New Issue
Block a user