Change package name: djangorestframework -> rest_framework

This commit is contained in:
Tom Christie 2012-09-20 13:06:27 +01:00
parent a1bcfbfe92
commit 4b691c4027
105 changed files with 9797 additions and 79 deletions

2
.gitignore vendored
View File

@ -7,7 +7,7 @@ html/
coverage/
build/
dist/
djangorestframework.egg-info/
rest_framework.egg-info/
MANIFEST
!.gitignore

View File

@ -1,5 +1,5 @@
recursive-include djangorestframework/static *.ico *.txt *.css
recursive-include djangorestframework/templates *.txt *.html
recursive-include rest_framework/static *.ico *.txt *.css
recursive-include rest_framework/templates *.txt *.html
recursive-include examples .keep *.py *.txt
recursive-include docs *.py *.rst *.html *.txt
include AUTHORS LICENSE CHANGELOG.rst requirements.txt tox.ini

View File

@ -28,7 +28,7 @@ For more information, check out [the documentation][docs], in particular, the tu
Install using `pip`...
pip install djangorestframework
pip install rest_framework
...or clone the project from github.
@ -47,7 +47,7 @@ To build the docs.
To run the tests.
./djangorestframework/runtests/runtests.py
./rest_framework/runtests/runtests.py
# Changelog

View File

@ -28,10 +28,10 @@ The value of `request.user` and `request.auth` for unauthenticated requests can
The default authentication policy may be set globally, using the `DEFAULT_AUTHENTICATION` setting. For example.
API_SETTINGS = {
REST_FRAMEWORK = {
'DEFAULT_AUTHENTICATION': (
'djangorestframework.authentication.UserBasicAuthentication',
'djangorestframework.authentication.SessionAuthentication',
'rest_framework.authentication.UserBasicAuthentication',
'rest_framework.authentication.SessionAuthentication',
)
}
@ -75,11 +75,11 @@ If successfully authenticated, `BasicAuthentication` provides the following cred
This policy uses a simple token-based HTTP Authentication scheme. Token authentication is appropriate for client-server setups, such as native desktop and mobile clients.
To use the `TokenAuthentication` policy, include `djangorestframework.authtoken` in your `INSTALLED_APPS` setting.
To use the `TokenAuthentication` policy, include `rest_framework.authtoken` in your `INSTALLED_APPS` setting.
You'll also need to create tokens for your users.
from djangorestframework.authtoken.models import Token
from rest_framework.authtoken.models import Token
token = Token.objects.create(user=...)
print token.key
@ -91,7 +91,7 @@ For clients to authenticate, the token key should be included in the `Authorizat
If successfully authenticated, `TokenAuthentication` provides the following credentials.
* `request.user` will be a `django.contrib.auth.models.User` instance.
* `request.auth` will be a `djangorestframework.tokenauth.models.BasicToken` instance.
* `request.auth` will be a `rest_framework.tokenauth.models.BasicToken` instance.
**Note:** If you use `TokenAuthentication` in production you must ensure that your API is only available over `https` only.
@ -102,7 +102,7 @@ This policy uses the [OAuth 2.0][oauth] protocol to authenticate requests. OAut
If successfully authenticated, `OAuthAuthentication` provides the following credentials.
* `request.user` will be a `django.contrib.auth.models.User` instance.
* `request.auth` will be a `djangorestframework.models.OAuthToken` instance.
* `request.auth` will be a `rest_framework.models.OAuthToken` instance.
## SessionAuthentication

View File

@ -27,9 +27,9 @@ Object level permissions are run by REST framework's generic views when `.get_ob
The default permission policy may be set globally, using the `DEFAULT_PERMISSIONS` setting. For example.
API_SETTINGS = {
REST_FRAMEWORK = {
'DEFAULT_PERMISSIONS': (
'djangorestframework.permissions.IsAuthenticated',
'rest_framework.permissions.IsAuthenticated',
)
}

View File

@ -49,7 +49,7 @@ This allows you to support file uploads from multiple content-types. For exampl
`request.parsers` may no longer be altered once `request.DATA`, `request.FILES` or `request.POST` have been accessed.
If you're using the `djangorestframework.views.View` class... **[TODO]**
If you're using the `rest_framework.views.View` class... **[TODO]**
## .stream
@ -63,6 +63,6 @@ You will not typically need to access `request.stream`, unless you're writing a
`request.authentication` may no longer be altered once `request.user` or `request.auth` have been accessed.
If you're using the `djangorestframework.views.View` class... **[TODO]**
If you're using the `rest_framework.views.View` class... **[TODO]**
[cite]: https://groups.google.com/d/topic/django-developers/dxI4qVzrBY4/discussion

View File

@ -23,8 +23,8 @@ There's no requirement for you to use them, but if you do then the self-describi
Has the same behavior as [`django.core.urlresolvers.reverse`][reverse], except that it returns a fully qualified URL, using the request to determine the host and port.
from djangorestframework.utils import reverse
from djangorestframework.views import APIView
from rest_framework.utils import reverse
from rest_framework.views import APIView
class MyView(APIView):
def get(self, request):

View File

@ -6,16 +6,16 @@
>
> — [The Zen of Python][cite]
Configuration for REST framework is all namespaced inside a single Django setting, named `API_SETTINGS`.
Configuration for REST framework is all namespaced inside a single Django setting, named `REST_FRAMEWORK`.
For example your project's `settings.py` file might include something like this:
API_SETTINGS = {
REST_FRAMEWORK = {
'DEFAULT_RENDERERS': (
'djangorestframework.renderers.YAMLRenderer',
'rest_framework.renderers.YAMLRenderer',
)
'DEFAULT_PARSERS': (
'djangorestframework.parsers.YAMLParser',
'rest_framework.parsers.YAMLParser',
)
}
@ -24,7 +24,7 @@ For example your project's `settings.py` file might include something like this:
If you need to access the values of REST framework's API settings in your project,
you should use the `api_settings` object. For example.
from djangorestframework.settings import api_settings
from rest_framework.settings import api_settings
print api_settings.DEFAULT_AUTHENTICATION
@ -37,9 +37,9 @@ A list or tuple of renderer classes, that determines the default set of renderer
Default:
(
'djangorestframework.renderers.JSONRenderer',
'djangorestframework.renderers.DocumentingHTMLRenderer'
'djangorestframework.renderers.TemplateHTMLRenderer'
'rest_framework.renderers.JSONRenderer',
'rest_framework.renderers.DocumentingHTMLRenderer'
'rest_framework.renderers.TemplateHTMLRenderer'
)
## DEFAULT_PARSERS
@ -49,8 +49,8 @@ A list or tuple of parser classes, that determines the default set of parsers us
Default:
(
'djangorestframework.parsers.JSONParser',
'djangorestframework.parsers.FormParser'
'rest_framework.parsers.JSONParser',
'rest_framework.parsers.FormParser'
)
## DEFAULT_AUTHENTICATION
@ -60,8 +60,8 @@ A list or tuple of authentication classes, that determines the default set of au
Default:
(
'djangorestframework.authentication.SessionAuthentication',
'djangorestframework.authentication.UserBasicAuthentication'
'rest_framework.authentication.SessionAuthentication',
'rest_framework.authentication.UserBasicAuthentication'
)
## DEFAULT_PERMISSIONS
@ -80,13 +80,13 @@ Default: `()`
**TODO**
Default: `djangorestframework.serializers.ModelSerializer`
Default: `rest_framework.serializers.ModelSerializer`
## DEFAULT_PAGINATION_SERIALIZER
**TODO**
Default: `djangorestframework.pagination.PaginationSerializer`
Default: `rest_framework.pagination.PaginationSerializer`
## FORMAT_SUFFIX_KWARG

View File

@ -8,7 +8,7 @@
Using bare status codes in your responses isn't recommended. REST framework includes a set of named constants that you can use to make more code more obvious and readable.
from djangorestframework import status
from rest_framework import status
def empty_view(self):
content = {'please move along': 'nothing to see here'}

View File

@ -29,10 +29,10 @@ If any throttle check fails an `exceptions.Throttled` exception will be raised,
The default throttling policy may be set globally, using the `DEFAULT_THROTTLES` and `DEFAULT_THROTTLE_RATES` settings. For example.
API_SETTINGS = {
REST_FRAMEWORK = {
'DEFAULT_THROTTLES': (
'djangorestframework.throttles.AnonThrottle',
'djangorestframework.throttles.UserThrottle',
'rest_framework.throttles.AnonThrottle',
'rest_framework.throttles.UserThrottle',
)
'DEFAULT_THROTTLE_RATES': {
'anon': '100/day',
@ -95,7 +95,7 @@ For example, multiple user throttle rates could be implemented by using the foll
...and the following settings.
API_SETTINGS = {
REST_FRAMEWORK = {
'DEFAULT_THROTTLES': (
'example.throttles.BurstRateThrottle',
'example.throttles.SustainedRateThrottle',
@ -130,9 +130,9 @@ For example, given the following views...
...and the following settings.
API_SETTINGS = {
REST_FRAMEWORK = {
'DEFAULT_THROTTLES': (
'djangorestframework.throttles.ScopedRateThrottle',
'rest_framework.throttles.ScopedRateThrottle',
)
'DEFAULT_THROTTLE_RATES': {
'contacts': '1000/day',

View File

@ -40,20 +40,22 @@ Install using `pip`, including any optional packages you want...
pip install -r requirements.txt
pip install -r optionals.txt
Add `djangorestframework` to your `INSTALLED_APPS`.
Add `rest_framework` to your `INSTALLED_APPS`.
INSTALLED_APPS = (
...
'djangorestframework',
'rest_framework',
)
If you're intending to use the browserable API you'll want to add REST framework's login and logout views. Add the following to your root `urls.py` file.
urlpatterns = patterns('',
...
url(r'^api-auth/', include('djangorestframework.urls', namespace='djangorestframework'))
url(r'^api-auth/', include('rest_framework.urls', namespace='rest_framework'))
)
Note that the base URL can be whatever you want, but you must include `rest_framework.urls` with the `rest_framework` namespace.
## Quickstart
**TODO**
@ -110,7 +112,7 @@ Build the docs:
Run the tests:
./djangorestframework/runtests/runtests.py
./rest_framework/runtests/runtests.py
## License

View File

@ -4,7 +4,7 @@ API may stand for Application *Programming* Interface, but humans have to be abl
## URLs
If you include fully-qualified URLs in your resource output, they will be 'urlized' and made clickable for easy browsing by humans. The `djangorestframework` package includes a [`reverse`][drfreverse] helper for this purpose.
If you include fully-qualified URLs in your resource output, they will be 'urlized' and made clickable for easy browsing by humans. The `rest_framework` package includes a [`reverse`][drfreverse] helper for this purpose.
## Formats
@ -14,7 +14,7 @@ By default, the API will return the format specified by the headers, which in th
## Customizing
To customize the look-and-feel, create a template called `api.html` and add it to your project, eg: `templates/djangorestframework/api.html`, that extends the `djangorestframework/base.html` template.
To customize the look-and-feel, create a template called `api.html` and add it to your project, eg: `templates/rest_framework/api.html`, that extends the `rest_framework/base.html` template.
The included browsable API template is built with [Bootstrap (2.1.1)][bootstrap], making it easy to customize the look-and-feel.

View File

@ -45,11 +45,11 @@ The simplest way to get up and running will probably be to use an `sqlite3` data
}
}
We'll also need to add our new `blog` app and the `djangorestframework` app to `INSTALLED_APPS`.
We'll also need to add our new `blog` app and the `rest_framework` app to `INSTALLED_APPS`.
INSTALLED_APPS = (
...
'djangorestframework',
'rest_framework',
'blog'
)
@ -81,7 +81,7 @@ Don't forget to sync the database for the first time.
We're going to create a simple Web API that we can use to edit these comment objects with. The first thing we need is a way of serializing and deserializing the objects into representations such as `json`. We do this by declaring serializers, that work very similarly to Django's forms. Create a file in the project named `serializers.py` and add the following.
from blog import models
from djangorestframework import serializers
from rest_framework import serializers
class CommentSerializer(serializers.Serializer):
@ -114,8 +114,8 @@ Okay, once we've got a few imports out of the way, we'd better create a few comm
from blog.models import Comment
from blog.serializers import CommentSerializer
from djangorestframework.renderers import JSONRenderer
from djangorestframework.parsers import JSONParser
from rest_framework.renderers import JSONRenderer
from rest_framework.parsers import JSONParser
c1 = Comment(email='leila@example.com', content='nothing to say')
c2 = Comment(email='tom@example.com', content='foo bar')
@ -159,8 +159,8 @@ Edit the `blog/views.py` file, and add the following.
from blog.models import Comment
from blog.serializers import CommentSerializer
from djangorestframework.renderers import JSONRenderer
from djangorestframework.parsers import JSONParser
from rest_framework.renderers import JSONRenderer
from rest_framework.parsers import JSONParser
from django.http import HttpResponse

View File

@ -40,9 +40,9 @@ We don't need our `JSONResponse` class anymore, so go ahead and delete that. On
from blog.models import Comment
from blog.serializers import CommentSerializer
from djangorestframework import status
from djangorestframework.decorators import api_view
from djangorestframework.response import Response
from rest_framework import status
from rest_framework.decorators import api_view
from rest_framework.response import Response
@api_view(['GET', 'POST'])
def comment_root(request):
@ -112,7 +112,7 @@ and
Now update the `urls.py` file slightly, to append a set of `format_suffix_patterns` in addition to the existing URLs.
from django.conf.urls import patterns, url
from djangorestframework.urlpatterns import format_suffix_patterns
from rest_framework.urlpatterns import format_suffix_patterns
urlpatterns = patterns('blogpost.views',
url(r'^$', 'comment_root'),

View File

@ -9,9 +9,9 @@ We'll start by rewriting the root view as a class based view. All this involves
from blog.models import Comment
from blog.serializers import CommentSerializer
from django.http import Http404
from djangorestframework.views import APIView
from djangorestframework.response import Response
from djangorestframework import status
from rest_framework.views import APIView
from rest_framework.response import Response
from rest_framework import status
class CommentRoot(APIView):
@ -68,7 +68,7 @@ That's looking good. Again, it's still pretty similar to the function based vie
We'll also need to refactor our URLconf slightly now we're using class based views.
from django.conf.urls import patterns, url
from djangorestframework.urlpatterns import format_suffix_patterns
from rest_framework.urlpatterns import format_suffix_patterns
from blogpost import views
urlpatterns = patterns('',
@ -90,8 +90,8 @@ Let's take a look at how we can compose our views by using the mixin classes.
from blog.models import Comment
from blog.serializers import CommentSerializer
from djangorestframework import mixins
from djangorestframework import generics
from rest_framework import mixins
from rest_framework import generics
class CommentRoot(mixins.ListModelMixin,
mixins.CreateModelMixin,
@ -133,7 +133,7 @@ Using the mixin classes we've rewritten the views to use slightly less code than
from blog.models import Comment
from blog.serializers import CommentSerializer
from djangorestframework import generics
from rest_framework import generics
class CommentRoot(generics.RootAPIView):

View File

@ -50,7 +50,7 @@ The handler methods only get bound to the actions when we define the URLConf. He
Right now that hasn't really saved us a lot of code. However, now that we're using Resources rather than Views, we actually don't need to design the urlconf ourselves. The conventions for wiring up resources into views and urls can be handled automatically, using `Router` classes. All we need to do is register the appropriate resources with a router, and let it do the rest. Here's our re-wired `urls.py` file.
from blog import resources
from djangorestframework.routers import DefaultRouter
from rest_framework.routers import DefaultRouter
router = DefaultRouter()
router.register(resources.BlogPostResource)

View File

@ -24,7 +24,7 @@ else:
main_header = '<li class="main"><a href="#{{ anchor }}">{{ title }}</a></li>'
sub_header = '<li><a href="#{{ anchor }}">{{ title }}</a></li>'
code_label = r'<a class="github" href="https://github.com/tomchristie/django-rest-framework/blob/restframework2/djangorestframework/\1"><span class="label label-info">\1</span></a>'
code_label = r'<a class="github" href="https://github.com/tomchristie/django-rest-framework/blob/restframework2/rest_framework/\1"><span class="label label-info">\1</span></a>'
page = open(os.path.join(docs_dir, 'template.html'), 'r').read()

View File

@ -0,0 +1,3 @@
__version__ = '2.0.0'
VERSION = __version__ # synonym

View File

@ -0,0 +1,132 @@
"""
The :mod:`authentication` module provides a set of pluggable authentication classes.
Authentication behavior is provided by mixing the :class:`mixins.RequestMixin` class into a :class:`View` class.
"""
from django.contrib.auth import authenticate
from rest_framework.compat import CsrfViewMiddleware
from rest_framework.authtoken.models import Token
import base64
class BaseAuthentication(object):
"""
All authentication classes should extend BaseAuthentication.
"""
def authenticate(self, request):
"""
Authenticate the :obj:`request` and return a :obj:`User` or :const:`None`. [*]_
.. [*] The authentication context *will* typically be a :obj:`User`,
but it need not be. It can be any user-like object so long as the
permissions classes (see the :mod:`permissions` module) on the view can
handle the object and use it to determine if the request has the required
permissions or not.
This can be an important distinction if you're implementing some token
based authentication mechanism, where the authentication context
may be more involved than simply mapping to a :obj:`User`.
"""
return None
class BasicAuthentication(BaseAuthentication):
"""
Base class for HTTP Basic authentication.
Subclasses should implement `.authenticate_credentials()`.
"""
def authenticate(self, request):
"""
Returns a `User` if a correct username and password have been supplied
using HTTP Basic authentication. Otherwise returns `None`.
"""
from django.utils.encoding import smart_unicode, DjangoUnicodeDecodeError
if 'HTTP_AUTHORIZATION' in request.META:
auth = request.META['HTTP_AUTHORIZATION'].split()
if len(auth) == 2 and auth[0].lower() == "basic":
try:
auth_parts = base64.b64decode(auth[1]).partition(':')
except TypeError:
return None
try:
userid, password = smart_unicode(auth_parts[0]), smart_unicode(auth_parts[2])
except DjangoUnicodeDecodeError:
return None
return self.authenticate_credentials(userid, password)
def authenticate_credentials(self, userid, password):
"""
Given the Basic authentication userid and password, authenticate
and return a user instance.
"""
raise NotImplementedError('.authenticate_credentials() must be overridden')
class UserBasicAuthentication(BasicAuthentication):
def authenticate_credentials(self, userid, password):
"""
Authenticate the userid and password against username and password.
"""
user = authenticate(username=userid, password=password)
if user is not None and user.is_active:
return (user, None)
class SessionAuthentication(BaseAuthentication):
"""
Use Django's session framework for authentication.
"""
def authenticate(self, request):
"""
Returns a :obj:`User` if the request session currently has a logged in user.
Otherwise returns :const:`None`.
"""
user = getattr(request._request, 'user', None)
if user and user.is_active:
# Enforce CSRF validation for session based authentication.
resp = CsrfViewMiddleware().process_view(request, None, (), {})
if resp is None: # csrf passed
return (user, None)
class TokenAuthentication(BaseAuthentication):
"""
Simple token based authentication.
Clients should authenticate by passing the token key in the "Authorization"
HTTP header, prepended with the string "Token ". For example:
Authorization: Token 401f7ac837da42b97f613d789819ff93537bee6a
"""
model = Token
"""
A custom token model may be used, but must have the following properties.
* key -- The string identifying the token
* user -- The user to which the token belongs
"""
def authenticate(self, request):
auth = request.META.get('HTTP_AUTHORIZATION', '').split()
if len(auth) == 2 and auth[0].lower() == "token":
key = auth[1]
try:
token = self.model.objects.get(key=key)
except self.model.DoesNotExist:
return None
if token.user.is_active and not getattr(token, 'revoked', False):
return (token.user, token)
# TODO: OAuthAuthentication

View File

View File

@ -0,0 +1,72 @@
# -*- coding: utf-8 -*-
import datetime
from south.db import db
from south.v2 import SchemaMigration
from django.db import models
class Migration(SchemaMigration):
def forwards(self, orm):
# Adding model 'Token'
db.create_table('authtoken_token', (
('key', self.gf('django.db.models.fields.CharField')(max_length=40, primary_key=True)),
('user', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['auth.User'])),
('revoked', self.gf('django.db.models.fields.BooleanField')(default=False)),
('created', self.gf('django.db.models.fields.DateTimeField')(auto_now_add=True, blank=True)),
))
db.send_create_signal('authtoken', ['Token'])
def backwards(self, orm):
# Deleting model 'Token'
db.delete_table('authtoken_token')
models = {
'auth.group': {
'Meta': {'object_name': 'Group'},
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}),
'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'})
},
'auth.permission': {
'Meta': {'ordering': "('content_type__app_label', 'content_type__model', 'codename')", 'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'},
'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '50'})
},
'auth.user': {
'Meta': {'object_name': 'User'},
'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}),
'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}),
'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'})
},
'authtoken.token': {
'Meta': {'object_name': 'Token'},
'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
'key': ('django.db.models.fields.CharField', [], {'max_length': '40', 'primary_key': 'True'}),
'revoked': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"})
},
'contenttypes.contenttype': {
'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"},
'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '100'})
}
}
complete_apps = ['authtoken']

View File

@ -0,0 +1,23 @@
import uuid
import hmac
from hashlib import sha1
from django.db import models
class Token(models.Model):
"""
The default authorization token model.
"""
key = models.CharField(max_length=40, primary_key=True)
user = models.ForeignKey('auth.User')
revoked = models.BooleanField(default=False)
created = models.DateTimeField(auto_now_add=True)
def save(self, *args, **kwargs):
if not self.key:
self.key = self.generate_key()
return super(Token, self).save(*args, **kwargs)
def generate_key(self):
unique = str(uuid.uuid4())
return hmac.new(unique, digestmod=sha1).hexdigest()

View File

480
rest_framework/compat.py Normal file
View File

@ -0,0 +1,480 @@
"""
The :mod:`compat` module provides support for backwards compatibility with older versions of django/python.
"""
import django
# cStringIO only if it's available, otherwise StringIO
try:
import cStringIO as StringIO
except ImportError:
import StringIO
# parse_qs from 'urlparse' module unless python 2.5, in which case from 'cgi'
try:
# python >= 2.6
from urlparse import parse_qs
except ImportError:
# python < 2.6
from cgi import parse_qs
# django.test.client.RequestFactory (Required for 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 :obj:`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 :class:`django.test.client.Client` interface. Of which
you can find the docs here__.
__ http://www.djangoproject.com/documentation/testing/#the-test-client
Once you have a `request` object you can pass it to any :func:`view` function,
just as if that :func:`view` had been hooked up using a URLconf.
"""
def request(self, **request):
"""
Similar to parent class, but returns the :obj:`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
if not hasattr(View, 'head'):
# First implementation of Django class-based views did not include head method
# in base View class - https://code.djangoproject.com/ticket/15668
class ViewPlusHead(View):
def head(self, request, *args, **kwargs):
return self.get(request, *args, **kwargs)
View = ViewPlusHead
except ImportError:
from django import http
from django.utils.functional import update_wrapper
# 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 rest_framework.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)
def head(self, request, *args, **kwargs):
return self.get(request, *args, **kwargs)
# PUT, DELETE do not require CSRF until 1.4. They should. Make it better.
if django.VERSION >= (1, 4):
from django.middleware.csrf import CsrfViewMiddleware
else:
import hashlib
import re
import random
import logging
import urlparse
from django.conf import settings
from django.core.urlresolvers import get_callable
try:
from logging import NullHandler
except ImportError:
class NullHandler(logging.Handler):
def emit(self, record):
pass
logger = logging.getLogger('django.request')
if not logger.handlers:
logger.addHandler(NullHandler())
def same_origin(url1, url2):
"""
Checks if two URLs are 'same-origin'
"""
p1, p2 = urlparse.urlparse(url1), urlparse.urlparse(url2)
return p1[0:2] == p2[0:2]
def constant_time_compare(val1, val2):
"""
Returns True if the two strings are equal, False otherwise.
The time taken is independent of the number of characters that match.
"""
if len(val1) != len(val2):
return False
result = 0
for x, y in zip(val1, val2):
result |= ord(x) ^ ord(y)
return result == 0
# Use the system (hardware-based) random number generator if it exists.
if hasattr(random, 'SystemRandom'):
randrange = random.SystemRandom().randrange
else:
randrange = random.randrange
_MAX_CSRF_KEY = 18446744073709551616L # 2 << 63
REASON_NO_REFERER = "Referer checking failed - no Referer."
REASON_BAD_REFERER = "Referer checking failed - %s does not match %s."
REASON_NO_CSRF_COOKIE = "CSRF cookie not set."
REASON_BAD_TOKEN = "CSRF token missing or incorrect."
def _get_failure_view():
"""
Returns the view to be used for CSRF rejections
"""
return get_callable(settings.CSRF_FAILURE_VIEW)
def _get_new_csrf_key():
return hashlib.md5("%s%s" % (randrange(0, _MAX_CSRF_KEY), settings.SECRET_KEY)).hexdigest()
def get_token(request):
"""
Returns the the CSRF token required for a POST form. The token is an
alphanumeric value.
A side effect of calling this function is to make the the csrf_protect
decorator and the CsrfViewMiddleware add a CSRF cookie and a 'Vary: Cookie'
header to the outgoing response. For this reason, you may need to use this
function lazily, as is done by the csrf context processor.
"""
request.META["CSRF_COOKIE_USED"] = True
return request.META.get("CSRF_COOKIE", None)
def _sanitize_token(token):
# Allow only alphanum, and ensure we return a 'str' for the sake of the post
# processing middleware.
token = re.sub('[^a-zA-Z0-9]', '', str(token.decode('ascii', 'ignore')))
if token == "":
# In case the cookie has been truncated to nothing at some point.
return _get_new_csrf_key()
else:
return token
class CsrfViewMiddleware(object):
"""
Middleware that requires a present and correct csrfmiddlewaretoken
for POST requests that have a CSRF cookie, and sets an outgoing
CSRF cookie.
This middleware should be used in conjunction with the csrf_token template
tag.
"""
# The _accept and _reject methods currently only exist for the sake of the
# requires_csrf_token decorator.
def _accept(self, request):
# Avoid checking the request twice by adding a custom attribute to
# request. This will be relevant when both decorator and middleware
# are used.
request.csrf_processing_done = True
return None
def _reject(self, request, reason):
return _get_failure_view()(request, reason=reason)
def process_view(self, request, callback, callback_args, callback_kwargs):
if getattr(request, 'csrf_processing_done', False):
return None
try:
csrf_token = _sanitize_token(request.COOKIES[settings.CSRF_COOKIE_NAME])
# Use same token next time
request.META['CSRF_COOKIE'] = csrf_token
except KeyError:
csrf_token = None
# Generate token and store it in the request, so it's available to the view.
request.META["CSRF_COOKIE"] = _get_new_csrf_key()
# Wait until request.META["CSRF_COOKIE"] has been manipulated before
# bailing out, so that get_token still works
if getattr(callback, 'csrf_exempt', False):
return None
# Assume that anything not defined as 'safe' by RC2616 needs protection.
if request.method not in ('GET', 'HEAD', 'OPTIONS', 'TRACE'):
if getattr(request, '_dont_enforce_csrf_checks', False):
# Mechanism to turn off CSRF checks for test suite. It comes after
# the creation of CSRF cookies, so that everything else continues to
# work exactly the same (e.g. cookies are sent etc), but before the
# any branches that call reject()
return self._accept(request)
if request.is_secure():
# Suppose user visits http://example.com/
# An active network attacker,(man-in-the-middle, MITM) sends a
# POST form which targets https://example.com/detonate-bomb/ and
# submits it via javascript.
#
# The attacker will need to provide a CSRF cookie and token, but
# that is no problem for a MITM and the session independent
# nonce we are using. So the MITM can circumvent the CSRF
# protection. This is true for any HTTP connection, but anyone
# using HTTPS expects better! For this reason, for
# https://example.com/ we need additional protection that treats
# http://example.com/ as completely untrusted. Under HTTPS,
# Barth et al. found that the Referer header is missing for
# same-domain requests in only about 0.2% of cases or less, so
# we can use strict Referer checking.
referer = request.META.get('HTTP_REFERER')
if referer is None:
logger.warning('Forbidden (%s): %s' % (REASON_NO_REFERER, request.path),
extra={
'status_code': 403,
'request': request,
}
)
return self._reject(request, REASON_NO_REFERER)
# Note that request.get_host() includes the port
good_referer = 'https://%s/' % request.get_host()
if not same_origin(referer, good_referer):
reason = REASON_BAD_REFERER % (referer, good_referer)
logger.warning('Forbidden (%s): %s' % (reason, request.path),
extra={
'status_code': 403,
'request': request,
}
)
return self._reject(request, reason)
if csrf_token is None:
# No CSRF cookie. For POST requests, we insist on a CSRF cookie,
# and in this way we can avoid all CSRF attacks, including login
# CSRF.
logger.warning('Forbidden (%s): %s' % (REASON_NO_CSRF_COOKIE, request.path),
extra={
'status_code': 403,
'request': request,
}
)
return self._reject(request, REASON_NO_CSRF_COOKIE)
# check non-cookie token for match
request_csrf_token = ""
if request.method == "POST":
request_csrf_token = request.POST.get('csrfmiddlewaretoken', '')
if request_csrf_token == "":
# Fall back to X-CSRFToken, to make things easier for AJAX,
# and possible for PUT/DELETE
request_csrf_token = request.META.get('HTTP_X_CSRFTOKEN', '')
if not constant_time_compare(request_csrf_token, csrf_token):
logger.warning('Forbidden (%s): %s' % (REASON_BAD_TOKEN, request.path),
extra={
'status_code': 403,
'request': request,
}
)
return self._reject(request, REASON_BAD_TOKEN)
return self._accept(request)
# timezone support is new in Django 1.4
try:
from django.utils import timezone
except ImportError:
timezone = None
# dateparse is ALSO new in Django 1.4
try:
from django.utils.dateparse import parse_date, parse_datetime
except ImportError:
import datetime
import re
date_re = re.compile(
r'(?P<year>\d{4})-(?P<month>\d{1,2})-(?P<day>\d{1,2})$'
)
datetime_re = re.compile(
r'(?P<year>\d{4})-(?P<month>\d{1,2})-(?P<day>\d{1,2})'
r'[T ](?P<hour>\d{1,2}):(?P<minute>\d{1,2})'
r'(?::(?P<second>\d{1,2})(?:\.(?P<microsecond>\d{1,6})\d{0,6})?)?'
r'(?P<tzinfo>Z|[+-]\d{1,2}:\d{1,2})?$'
)
time_re = re.compile(
r'(?P<hour>\d{1,2}):(?P<minute>\d{1,2})'
r'(?::(?P<second>\d{1,2})(?:\.(?P<microsecond>\d{1,6})\d{0,6})?)?'
)
def parse_date(value):
match = date_re.match(value)
if match:
kw = dict((k, int(v)) for k, v in match.groupdict().iteritems())
return datetime.date(**kw)
def parse_time(value):
match = time_re.match(value)
if match:
kw = match.groupdict()
if kw['microsecond']:
kw['microsecond'] = kw['microsecond'].ljust(6, '0')
kw = dict((k, int(v)) for k, v in kw.iteritems() if v is not None)
return datetime.time(**kw)
def parse_datetime(value):
"""Parse datetime, but w/o the timezone awareness in 1.4"""
match = datetime_re.match(value)
if match:
kw = match.groupdict()
if kw['microsecond']:
kw['microsecond'] = kw['microsecond'].ljust(6, '0')
kw = dict((k, int(v)) for k, v in kw.iteritems() if v is not None)
return datetime.datetime(**kw)
# Markdown is optional
try:
import markdown
def apply_markdown(text):
"""
Simple wrapper around :func:`markdown.markdown` to set the base level
of '#' style headers to <h2>.
"""
extensions = ['headerid(level=2)']
safe_mode = False,
md = markdown.Markdown(extensions=extensions, safe_mode=safe_mode)
return md.convert(text)
except ImportError:
apply_markdown = None
# Yaml is optional
try:
import yaml
except ImportError:
yaml = None
import unittest
try:
import unittest.skip
except ImportError: # python < 2.7
from unittest import TestCase
import functools
def skip(reason):
# Pasted from py27/lib/unittest/case.py
"""
Unconditionally skip a test.
"""
def decorator(test_item):
if not (isinstance(test_item, type) and issubclass(test_item, TestCase)):
@functools.wraps(test_item)
def skip_wrapper(*args, **kwargs):
pass
test_item = skip_wrapper
test_item.__unittest_skip__ = True
test_item.__unittest_skip_why__ = reason
return test_item
return decorator
unittest.skip = skip
# xml.etree.parse only throws ParseError for python >= 2.7
try:
from xml.etree import ParseError as ETParseError
except ImportError: # python < 2.7
ETParseError = None

View File

@ -0,0 +1,53 @@
from functools import wraps
from django.http import Http404
from django.utils.decorators import available_attrs
from django.core.exceptions import PermissionDenied
from rest_framework import exceptions
from rest_framework import status
from rest_framework.response import Response
from rest_framework.request import Request
from rest_framework.settings import api_settings
def api_view(allowed_methods):
"""
Decorator for function based views.
@api_view(['GET', 'POST'])
def my_view(request):
# request will be an instance of `Request`
# `Response` objects will have .request set automatically
# APIException instances will be handled
"""
allowed_methods = [method.upper() for method in allowed_methods]
def decorator(func):
@wraps(func, assigned=available_attrs(func))
def inner(request, *args, **kwargs):
try:
request = Request(request)
if request.method not in allowed_methods:
raise exceptions.MethodNotAllowed(request.method)
response = func(request, *args, **kwargs)
if isinstance(response, Response):
response.request = request
if api_settings.FORMAT_SUFFIX_KWARG:
response.format = kwargs.get(api_settings.FORMAT_SUFFIX_KWARG, None)
return response
except exceptions.APIException as exc:
return Response({'detail': exc.detail}, status=exc.status_code)
except Http404 as exc:
return Response({'detail': 'Not found'},
status=status.HTTP_404_NOT_FOUND)
except PermissionDenied as exc:
return Response({'detail': 'Permission denied'},
status=status.HTTP_403_FORBIDDEN)
return inner
return decorator

View File

@ -0,0 +1,79 @@
"""
Handled exceptions raised by REST framework.
In addition Django's built in 403 and 404 exceptions are handled.
(`django.http.Http404` and `django.core.exceptions.PermissionDenied`)
"""
from rest_framework import status
class APIException(Exception):
"""
Base class for REST framework exceptions.
Subclasses should provide `.status_code` and `.detail` properties.
"""
pass
class ParseError(APIException):
status_code = status.HTTP_400_BAD_REQUEST
default_detail = 'Malformed request.'
def __init__(self, detail=None):
self.detail = detail or self.default_detail
class PermissionDenied(APIException):
status_code = status.HTTP_403_FORBIDDEN
default_detail = 'You do not have permission to perform this action.'
def __init__(self, detail=None):
self.detail = detail or self.default_detail
class InvalidFormat(APIException):
status_code = status.HTTP_404_NOT_FOUND
default_detail = "Format suffix '.%s' not found."
def __init__(self, format, detail=None):
self.detail = (detail or self.default_detail) % format
class MethodNotAllowed(APIException):
status_code = status.HTTP_405_METHOD_NOT_ALLOWED
default_detail = "Method '%s' not allowed."
def __init__(self, method, detail=None):
self.detail = (detail or self.default_detail) % method
class NotAcceptable(APIException):
status_code = status.HTTP_406_NOT_ACCEPTABLE
default_detail = "Could not satisfy the request's Accept header"
def __init__(self, detail=None, available_renderers=None):
self.detail = detail or self.default_detail
self.available_renderers = available_renderers
class UnsupportedMediaType(APIException):
status_code = status.HTTP_415_UNSUPPORTED_MEDIA_TYPE
default_detail = "Unsupported media type '%s' in request."
def __init__(self, media_type, detail=None):
self.detail = (detail or self.default_detail) % media_type
class Throttled(APIException):
status_code = status.HTTP_429_TOO_MANY_REQUESTS
default_detail = "Request was throttled."
extra_detail = "Expected available in %d second%s."
def __init__(self, wait=None, detail=None):
import math
self.wait = wait and math.ceil(wait) or None
if wait is not None:
format = detail or self.default_detail + self.extra_detail
self.detail = format % (self.wait, self.wait != 1 and 's' or '')
else:
self.detail = detail or self.default_detail

446
rest_framework/fields.py Normal file
View File

@ -0,0 +1,446 @@
import copy
import datetime
import inspect
import warnings
from django.core import validators
from django.core.exceptions import ValidationError
from django.conf import settings
from django.db import DEFAULT_DB_ALIAS
from django.db.models.related import RelatedObject
from django.utils.encoding import is_protected_type, smart_unicode
from django.utils.translation import ugettext_lazy as _
from rest_framework.compat import parse_date, parse_datetime
from rest_framework.compat import timezone
def is_simple_callable(obj):
"""
True if the object is a callable that takes no arguments.
"""
return (
(inspect.isfunction(obj) and not inspect.getargspec(obj)[0]) or
(inspect.ismethod(obj) and len(inspect.getargspec(obj)[0]) <= 1)
)
class Field(object):
creation_counter = 0
default_validators = []
default_error_messages = {
'required': _('This field is required.'),
'invalid': _('Invalid value.'),
}
empty = ''
def __init__(self, source=None, readonly=False, required=None,
validators=[], error_messages=None):
self.parent = None
self.creation_counter = Field.creation_counter
Field.creation_counter += 1
self.source = source
self.readonly = readonly
self.required = not(readonly)
messages = {}
for c in reversed(self.__class__.__mro__):
messages.update(getattr(c, 'default_error_messages', {}))
messages.update(error_messages or {})
self.error_messages = messages
self.validators = self.default_validators + validators
def initialize(self, parent, model_field=None):
"""
Called to set up a field prior to field_to_native or field_from_native.
parent - The parent serializer.
model_field - The model field this field corrosponds to, if one exists.
"""
self.parent = parent
self.root = parent.root or parent
self.context = self.root.context
if model_field:
self.model_field = model_field
def validate(self, value):
pass
# if value in validators.EMPTY_VALUES and self.required:
# raise ValidationError(self.error_messages['required'])
def run_validators(self, value):
if value in validators.EMPTY_VALUES:
return
errors = []
for v in self.validators:
try:
v(value)
except ValidationError as e:
if hasattr(e, 'code') and e.code in self.error_messages:
message = self.error_messages[e.code]
if e.params:
message = message % e.params
errors.append(message)
else:
errors.extend(e.messages)
if errors:
raise ValidationError(errors)
def field_from_native(self, data, field_name, into):
"""
Given a dictionary and a field name, updates the dictionary `into`,
with the field and it's deserialized value.
"""
if self.readonly:
return
try:
native = data[field_name]
except KeyError:
return # TODO Consider validation behaviour, 'required' opt etc...
value = self.from_native(native)
if self.source == '*':
if value:
into.update(value)
else:
self.validate(value)
self.run_validators(value)
into[self.source or field_name] = value
def from_native(self, value):
"""
Reverts a simple representation back to the field's value.
"""
if hasattr(self, 'model_field'):
try:
return self.model_field.rel.to._meta.get_field(self.model_field.rel.field_name).to_python(value)
except:
return self.model_field.to_python(value)
return value
def field_to_native(self, obj, field_name):
"""
Given and object and a field name, returns the value that should be
serialized for that field.
"""
if obj is None:
return self.empty
if self.source == '*':
return self.to_native(obj)
self.obj = obj # Need to hang onto this in the case of model fields
if hasattr(self, 'model_field'):
return self.to_native(self.model_field._get_val_from_obj(obj))
return self.to_native(getattr(obj, self.source or field_name))
def to_native(self, value):
"""
Converts the field's value into it's simple representation.
"""
if is_simple_callable(value):
value = value()
if is_protected_type(value):
return value
elif hasattr(self, 'model_field'):
return self.model_field.value_to_string(self.obj)
return smart_unicode(value)
def attributes(self):
"""
Returns a dictionary of attributes to be used when serializing to xml.
"""
try:
return {
"type": self.model_field.get_internal_type()
}
except AttributeError:
return {}
class RelatedField(Field):
"""
A base class for model related fields or related managers.
Subclass this and override `convert` to define custom behaviour when
serializing related objects.
"""
def field_to_native(self, obj, field_name):
obj = getattr(obj, field_name)
if obj.__class__.__name__ in ('RelatedManager', 'ManyRelatedManager'):
return [self.to_native(item) for item in obj.all()]
return self.to_native(obj)
def attributes(self):
try:
return {
"rel": self.model_field.rel.__class__.__name__,
"to": smart_unicode(self.model_field.rel.to._meta)
}
except AttributeError:
return {}
class PrimaryKeyRelatedField(RelatedField):
"""
Serializes a model related field or related manager to a pk value.
"""
# Note the we use ModelRelatedField's implementation, as we want to get the
# raw database value directly, since that won't involve another
# database lookup.
#
# An alternative implementation would simply be this...
#
# class PrimaryKeyRelatedField(RelatedField):
# def to_native(self, obj):
# return obj.pk
def to_native(self, pk):
"""
Simply returns the object's pk. You can subclass this method to
provide different serialization behavior of the pk.
(For example returning a URL based on the model's pk.)
"""
return pk
def field_to_native(self, obj, field_name):
try:
obj = obj.serializable_value(field_name)
except AttributeError:
field = obj._meta.get_field_by_name(field_name)[0]
obj = getattr(obj, field_name)
if obj.__class__.__name__ == 'RelatedManager':
return [self.to_native(item.pk) for item in obj.all()]
elif isinstance(field, RelatedObject):
return self.to_native(obj.pk)
raise
if obj.__class__.__name__ == 'ManyRelatedManager':
return [self.to_native(item.pk) for item in obj.all()]
return self.to_native(obj)
def field_from_native(self, data, field_name, into):
value = data.get(field_name)
if hasattr(value, '__iter__'):
into[field_name] = [self.from_native(item) for item in value]
else:
into[field_name + '_id'] = self.from_native(value)
class NaturalKeyRelatedField(RelatedField):
"""
Serializes a model related field or related manager to a natural key value.
"""
is_natural_key = True # XML renderer handles these differently
def to_native(self, obj):
if hasattr(obj, 'natural_key'):
return obj.natural_key()
return obj
def field_from_native(self, data, field_name, into):
value = data.get(field_name)
into[self.model_field.attname] = self.from_native(value)
def from_native(self, value):
# TODO: Support 'using' : db = options.pop('using', DEFAULT_DB_ALIAS)
manager = self.model_field.rel.to._default_manager
manager = manager.db_manager(DEFAULT_DB_ALIAS)
return manager.get_by_natural_key(*value).pk
class BooleanField(Field):
default_error_messages = {
'invalid': _(u"'%s' value must be either True or False."),
}
def from_native(self, value):
if value in (True, False):
# if value is 1 or 0 than it's equal to True or False, but we want
# to return a true bool for semantic reasons.
return bool(value)
if value in ('t', 'True', '1'):
return True
if value in ('f', 'False', '0'):
return False
raise ValidationError(self.error_messages['invalid'] % value)
class CharField(Field):
def __init__(self, max_length=None, min_length=None, *args, **kwargs):
self.max_length, self.min_length = max_length, min_length
super(CharField, self).__init__(*args, **kwargs)
if min_length is not None:
self.validators.append(validators.MinLengthValidator(min_length))
if max_length is not None:
self.validators.append(validators.MaxLengthValidator(max_length))
def from_native(self, value):
if isinstance(value, basestring) or value is None:
return value
return smart_unicode(value)
class EmailField(CharField):
default_error_messages = {
'invalid': _('Enter a valid e-mail address.'),
}
default_validators = [validators.validate_email]
def from_native(self, value):
return super(EmailField, self).from_native(value).strip()
def __deepcopy__(self, memo):
result = copy.copy(self)
memo[id(self)] = result
#result.widget = copy.deepcopy(self.widget, memo)
result.validators = self.validators[:]
return result
class DateField(Field):
default_error_messages = {
'invalid': _(u"'%s' value has an invalid date format. It must be "
u"in YYYY-MM-DD format."),
'invalid_date': _(u"'%s' value has the correct format (YYYY-MM-DD) "
u"but it is an invalid date."),
}
empty = None
def from_native(self, value):
if value is None:
return value
if isinstance(value, datetime.datetime):
if timezone and settings.USE_TZ and timezone.is_aware(value):
# Convert aware datetimes to the default time zone
# before casting them to dates (#17742).
default_timezone = timezone.get_default_timezone()
value = timezone.make_naive(value, default_timezone)
return value.date()
if isinstance(value, datetime.date):
return value
try:
parsed = parse_date(value)
if parsed is not None:
return parsed
except ValueError:
msg = self.error_messages['invalid_date'] % value
raise ValidationError(msg)
msg = self.error_messages['invalid'] % value
raise ValidationError(msg)
class DateTimeField(Field):
default_error_messages = {
'invalid': _(u"'%s' value has an invalid format. It must be in "
u"YYYY-MM-DD HH:MM[:ss[.uuuuuu]][TZ] format."),
'invalid_date': _(u"'%s' value has the correct format "
u"(YYYY-MM-DD) but it is an invalid date."),
'invalid_datetime': _(u"'%s' value has the correct format "
u"(YYYY-MM-DD HH:MM[:ss[.uuuuuu]][TZ]) "
u"but it is an invalid date/time."),
}
empty = None
def from_native(self, value):
if value is None:
return value
if isinstance(value, datetime.datetime):
return value
if isinstance(value, datetime.date):
value = datetime.datetime(value.year, value.month, value.day)
if settings.USE_TZ:
# For backwards compatibility, interpret naive datetimes in
# local time. This won't work during DST change, but we can't
# do much about it, so we let the exceptions percolate up the
# call stack.
warnings.warn(u"DateTimeField received a naive datetime (%s)"
u" while time zone support is active." % value,
RuntimeWarning)
default_timezone = timezone.get_default_timezone()
value = timezone.make_aware(value, default_timezone)
return value
try:
parsed = parse_datetime(value)
if parsed is not None:
return parsed
except ValueError:
msg = self.error_messages['invalid_datetime'] % value
raise ValidationError(msg)
try:
parsed = parse_date(value)
if parsed is not None:
return datetime.datetime(parsed.year, parsed.month, parsed.day)
except ValueError:
msg = self.error_messages['invalid_date'] % value
raise ValidationError(msg)
msg = self.error_messages['invalid'] % value
raise ValidationError(msg)
class IntegerField(Field):
default_error_messages = {
'invalid': _('Enter a whole number.'),
'max_value': _('Ensure this value is less than or equal to %(limit_value)s.'),
'min_value': _('Ensure this value is greater than or equal to %(limit_value)s.'),
}
def __init__(self, max_value=None, min_value=None, *args, **kwargs):
self.max_value, self.min_value = max_value, min_value
super(IntegerField, self).__init__(*args, **kwargs)
if max_value is not None:
self.validators.append(validators.MaxValueValidator(max_value))
if min_value is not None:
self.validators.append(validators.MinValueValidator(min_value))
def from_native(self, value):
if value in validators.EMPTY_VALUES:
return None
try:
value = int(str(value))
except (ValueError, TypeError):
raise ValidationError(self.error_messages['invalid'])
return value
class FloatField(Field):
default_error_messages = {
'invalid': _("'%s' value must be a float."),
}
def from_native(self, value):
if value is None:
return value
try:
return float(value)
except (TypeError, ValueError):
msg = self.error_messages['invalid'] % value
raise ValidationError(msg)
# field_mapping = {
# models.AutoField: IntegerField,
# models.BooleanField: BooleanField,
# models.CharField: CharField,
# models.DateTimeField: DateTimeField,
# models.DateField: DateField,
# models.BigIntegerField: IntegerField,
# models.IntegerField: IntegerField,
# models.PositiveIntegerField: IntegerField,
# models.FloatField: FloatField
# }
# def modelfield_to_serializerfield(field):
# return field_mapping.get(type(field), Field)

113
rest_framework/generics.py Normal file
View File

@ -0,0 +1,113 @@
"""
Generic views that provide commmonly needed behaviour.
"""
from rest_framework import views, mixins
from django.views.generic.detail import SingleObjectMixin
from django.views.generic.list import MultipleObjectMixin
### Base classes for the generic views ###
class BaseView(views.APIView):
"""
Base class for all other generic views.
"""
serializer_class = None
def get_serializer(self, data=None, files=None, instance=None):
# TODO: add support for files
# TODO: add support for seperate serializer/deserializer
context = {
'request': self.request,
'format': self.kwargs.get('format', None)
}
return self.serializer_class(data, instance=instance, context=context)
class MultipleObjectBaseView(MultipleObjectMixin, BaseView):
"""
Base class for generic views onto a queryset.
"""
pass
class SingleObjectBaseView(SingleObjectMixin, BaseView):
"""
Base class for generic views onto a model instance.
"""
def get_object(self):
"""
Override default to add support for object-level permissions.
"""
obj = super(SingleObjectBaseView, self).get_object()
self.check_permissions(self.request, obj)
return obj
### Concrete view classes that provide method handlers ###
### by composing the mixin classes with a base view. ###
class ListAPIView(mixins.ListModelMixin,
mixins.MetadataMixin,
MultipleObjectBaseView):
"""
Concrete view for listing a queryset.
"""
def get(self, request, *args, **kwargs):
return self.list(request, *args, **kwargs)
def options(self, request, *args, **kwargs):
return self.metadata(request, *args, **kwargs)
class RootAPIView(mixins.ListModelMixin,
mixins.CreateModelMixin,
mixins.MetadataMixin,
MultipleObjectBaseView):
"""
Concrete view for listing a queryset or creating a model instance.
"""
def get(self, request, *args, **kwargs):
return self.list(request, *args, **kwargs)
def post(self, request, *args, **kwargs):
return self.create(request, *args, **kwargs)
def options(self, request, *args, **kwargs):
return self.metadata(request, *args, **kwargs)
class DetailAPIView(mixins.RetrieveModelMixin,
mixins.MetadataMixin,
SingleObjectBaseView):
"""
Concrete view for retrieving a model instance.
"""
def get(self, request, *args, **kwargs):
return self.retrieve(request, *args, **kwargs)
def options(self, request, *args, **kwargs):
return self.metadata(request, *args, **kwargs)
class InstanceAPIView(mixins.RetrieveModelMixin,
mixins.UpdateModelMixin,
mixins.DestroyModelMixin,
mixins.MetadataMixin,
SingleObjectBaseView):
"""
Concrete view for retrieving, updating or deleting a model instance.
"""
def get(self, request, *args, **kwargs):
return self.retrieve(request, *args, **kwargs)
def put(self, request, *args, **kwargs):
return self.update(request, *args, **kwargs)
def delete(self, request, *args, **kwargs):
return self.destroy(request, *args, **kwargs)
def options(self, request, *args, **kwargs):
return self.metadata(request, *args, **kwargs)

97
rest_framework/mixins.py Normal file
View File

@ -0,0 +1,97 @@
"""
Basic building blocks for generic class based views.
We don't bind behaviour to http method handlers yet,
which allows mixin classes to be composed in interesting ways.
Eg. Use mixins to build a Resource class, and have a Router class
perform the binding of http methods to actions for us.
"""
from rest_framework import status
from rest_framework.response import Response
class CreateModelMixin(object):
"""
Create a model instance.
Should be mixed in with any `BaseView`.
"""
def create(self, request, *args, **kwargs):
serializer = self.get_serializer(data=request.DATA)
if serializer.is_valid():
self.object = serializer.object
self.object.save()
return Response(serializer.data, status=status.HTTP_201_CREATED)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
class ListModelMixin(object):
"""
List a queryset.
Should be mixed in with `MultipleObjectBaseView`.
"""
def list(self, request, *args, **kwargs):
self.object_list = self.get_queryset()
serializer = self.get_serializer(instance=self.object_list)
return Response(serializer.data)
class RetrieveModelMixin(object):
"""
Retrieve a model instance.
Should be mixed in with `SingleObjectBaseView`.
"""
def retrieve(self, request, *args, **kwargs):
self.object = self.get_object()
serializer = self.get_serializer(instance=self.object)
return Response(serializer.data)
class UpdateModelMixin(object):
"""
Update a model instance.
Should be mixed in with `SingleObjectBaseView`.
"""
def update(self, request, *args, **kwargs):
self.object = self.get_object()
serializer = self.get_serializer(data=request.DATA, instance=self.object)
if serializer.is_valid():
self.object = serializer.object
self.object.save()
return Response(serializer.data)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
class DestroyModelMixin(object):
"""
Destroy a model instance.
Should be mixed in with `SingleObjectBaseView`.
"""
def destroy(self, request, *args, **kwargs):
self.object = self.get_object()
self.object.delete()
return Response(status=status.HTTP_204_NO_CONTENT)
class MetadataMixin(object):
"""
Return a dicitonary of view metadata.
Should be mixed in with any `BaseView`.
This mixin is typically used for the HTTP 'OPTIONS' method.
"""
def metadata(self, request, *args, **kwargs):
content = {
'name': self.get_name(),
'description': self.get_description(),
'renders': self._rendered_media_types,
'parses': self._parsed_media_types,
}
# TODO: Add 'fields', from serializer info.
# form = self.get_bound_form()
# if form is not None:
# field_name_types = {}
# for name, field in form.fields.iteritems():
# field_name_types[name] = field.__class__.__name__
# content['fields'] = field_name_types
return Response(content, status=status.HTTP_200_OK)

1
rest_framework/models.py Normal file
View File

@ -0,0 +1 @@
# Just to keep things like ./manage.py test happy

View File

@ -0,0 +1,74 @@
from rest_framework import exceptions
from rest_framework.settings import api_settings
from rest_framework.utils.mediatypes import order_by_precedence
class BaseContentNegotiation(object):
def negotiate(self, request, renderers, format=None, force=False):
raise NotImplementedError('.negotiate() must be implemented')
class DefaultContentNegotiation(object):
settings = api_settings
def negotiate(self, request, renderers, format=None, force=False):
"""
Given a request and a list of renderers, return a two-tuple of:
(renderer, media type).
If force is set, then suppress exceptions, and forcibly return a
fallback renderer and media_type.
"""
try:
return self.unforced_negotiate(request, renderers, format)
except (exceptions.InvalidFormat, exceptions.NotAcceptable):
if force:
return (renderers[0], renderers[0].media_type)
raise
def unforced_negotiate(self, request, renderers, format=None):
"""
As `.negotiate()`, but does not take the optional `force` agument,
or suppress exceptions.
"""
# Allow URL style format override. eg. "?format=json
format = format or request.GET.get(self.settings.URL_FORMAT_OVERRIDE)
if format:
renderers = self.filter_renderers(renderers, format)
accepts = self.get_accept_list(request)
# Check the acceptable media types against each renderer,
# attempting more specific media types first
# NB. The inner loop here isn't as bad as it first looks :)
# Worst case is we're looping over len(accept_list) * len(self.renderers)
for media_type_set in order_by_precedence(accepts):
for renderer in renderers:
for media_type in media_type_set:
if renderer.can_handle_media_type(media_type):
return renderer, media_type
raise exceptions.NotAcceptable(available_renderers=renderers)
def filter_renderers(self, renderers, format):
"""
If there is a '.json' style format suffix, filter the renderers
so that we only negotiation against those that accept that format.
"""
renderers = [renderer for renderer in renderers
if renderer.can_handle_format(format)]
if not renderers:
raise exceptions.InvalidFormat(format)
return renderers
def get_accept_list(self, request):
"""
Given the incoming request, return a tokenised list of media
type strings.
Allows URL style accept override. eg. "?accept=application/json"
"""
header = request.META.get('HTTP_ACCEPT', '*/*')
header = request.GET.get(self.settings.URL_ACCEPT_OVERRIDE, header)
return [token.strip() for token in header.split(',')]

260
rest_framework/parsers.py Normal file
View File

@ -0,0 +1,260 @@
"""
Django supports parsing the content of an HTTP request, but only for form POST requests.
That behavior is sufficient for dealing with standard HTML forms, but it doesn't map well
to general HTTP requests.
We need a method to be able to:
1.) Determine the parsed content on a request for methods other than POST (eg typically also PUT)
2.) Determine the parsed content on a request for media types other than application/x-www-form-urlencoded
and multipart/form-data. (eg also handle multipart/json)
"""
from django.http import QueryDict
from django.http.multipartparser import MultiPartParser as DjangoMultiPartParser
from django.http.multipartparser import MultiPartParserError
from django.utils import simplejson as json
from rest_framework.compat import yaml
from rest_framework.exceptions import ParseError
from rest_framework.utils.mediatypes import media_type_matches
from xml.etree import ElementTree as ET
from rest_framework.compat import ETParseError
from xml.parsers.expat import ExpatError
import datetime
import decimal
from io import BytesIO
__all__ = (
'BaseParser',
'JSONParser',
'PlainTextParser',
'FormParser',
'MultiPartParser',
'YAMLParser',
'XMLParser'
)
class DataAndFiles(object):
def __init__(self, data, files):
self.data = data
self.files = files
class BaseParser(object):
"""
All parsers should extend :class:`BaseParser`, specifying a :attr:`media_type` attribute,
and overriding the :meth:`parse` method.
"""
media_type = None
def can_handle_request(self, content_type):
"""
Returns :const:`True` if this parser is able to deal with the given *content_type*.
The default implementation for this function is to check the *content_type*
argument against the :attr:`media_type` attribute set on the class to see if
they match.
This may be overridden to provide for other behavior, but typically you'll
instead want to just set the :attr:`media_type` attribute on the class.
"""
return media_type_matches(self.media_type, content_type)
def parse(self, string_or_stream, **opts):
"""
The main entry point to parsers. This is a light wrapper around
`parse_stream`, that instead handles both string and stream objects.
"""
if isinstance(string_or_stream, basestring):
stream = BytesIO(string_or_stream)
else:
stream = string_or_stream
return self.parse_stream(stream, **opts)
def parse_stream(self, stream, **opts):
"""
Given a *stream* to read from, return the deserialized output.
Should return parsed data, or a DataAndFiles object consisting of the
parsed data and files.
"""
raise NotImplementedError(".parse_stream() must be overridden.")
class JSONParser(BaseParser):
"""
Parses JSON-serialized data.
"""
media_type = 'application/json'
def parse_stream(self, stream, **opts):
"""
Returns a 2-tuple of `(data, files)`.
`data` will be an object which is the parsed content of the response.
`files` will always be `None`.
"""
try:
return json.load(stream)
except ValueError, exc:
raise ParseError('JSON parse error - %s' % unicode(exc))
class YAMLParser(BaseParser):
"""
Parses YAML-serialized data.
"""
media_type = 'application/yaml'
def parse_stream(self, stream, **opts):
"""
Returns a 2-tuple of `(data, files)`.
`data` will be an object which is the parsed content of the response.
`files` will always be `None`.
"""
try:
return yaml.safe_load(stream)
except (ValueError, yaml.parser.ParserError), exc:
raise ParseError('YAML parse error - %s' % unicode(exc))
class PlainTextParser(BaseParser):
"""
Plain text parser.
"""
media_type = 'text/plain'
def parse_stream(self, stream, **opts):
"""
Returns a 2-tuple of `(data, files)`.
`data` will simply be a string representing the body of the request.
`files` will always be `None`.
"""
return stream.read()
class FormParser(BaseParser):
"""
Parser for form data.
"""
media_type = 'application/x-www-form-urlencoded'
def parse_stream(self, stream, **opts):
"""
Returns a 2-tuple of `(data, files)`.
`data` will be a :class:`QueryDict` containing all the form parameters.
`files` will always be :const:`None`.
"""
data = QueryDict(stream.read())
return data
class MultiPartParser(BaseParser):
"""
Parser for multipart form data, which may include file data.
"""
media_type = 'multipart/form-data'
def parse_stream(self, stream, **opts):
"""
Returns a DataAndFiles object.
`.data` will be a `QueryDict` containing all the form parameters.
`.files` will be a `QueryDict` containing all the form files.
"""
meta = opts['meta']
upload_handlers = opts['upload_handlers']
try:
parser = DjangoMultiPartParser(meta, stream, upload_handlers)
data, files = parser.parse()
return DataAndFiles(data, files)
except MultiPartParserError, exc:
raise ParseError('Multipart form parse error - %s' % unicode(exc))
class XMLParser(BaseParser):
"""
XML parser.
"""
media_type = 'application/xml'
def parse_stream(self, stream, **opts):
try:
tree = ET.parse(stream)
except (ExpatError, ETParseError, ValueError), exc:
raise ParseError('XML parse error - %s' % unicode(exc))
data = self._xml_convert(tree.getroot())
return data
def _xml_convert(self, element):
"""
convert the xml `element` into the corresponding python object
"""
children = element.getchildren()
if len(children) == 0:
return self._type_convert(element.text)
else:
# if the fist child tag is list-item means all children are list-item
if children[0].tag == "list-item":
data = []
for child in children:
data.append(self._xml_convert(child))
else:
data = {}
for child in children:
data[child.tag] = self._xml_convert(child)
return data
def _type_convert(self, value):
"""
Converts the value returned by the XMl parse into the equivalent
Python type
"""
if value is None:
return value
try:
return datetime.datetime.strptime(value, '%Y-%m-%d %H:%M:%S')
except ValueError:
pass
try:
return int(value)
except ValueError:
pass
try:
return decimal.Decimal(value)
except decimal.InvalidOperation:
pass
return value
DEFAULT_PARSERS = (
JSONParser,
FormParser,
MultiPartParser,
XMLParser
)
if yaml:
DEFAULT_PARSERS += (YAMLParser, )
else:
YAMLParser = None

View File

@ -0,0 +1,116 @@
"""
The :mod:`permissions` module bundles a set of permission classes that are used
for checking if a request passes a certain set of constraints.
Permission behavior is provided by mixing the :class:`mixins.PermissionsMixin` class into a :class:`View` class.
"""
__all__ = (
'BasePermission',
'FullAnonAccess',
'IsAuthenticated',
'IsAdminUser',
'IsUserOrIsAnonReadOnly',
'PerUserThrottling',
'PerViewThrottling',
)
SAFE_METHODS = ['GET', 'HEAD', 'OPTIONS']
class BasePermission(object):
"""
A base class from which all permission classes should inherit.
"""
def __init__(self, view):
"""
Permission classes are always passed the current view on creation.
"""
self.view = view
def has_permission(self, request, obj=None):
"""
Should simply return, or raise an :exc:`response.ImmediateResponse`.
"""
raise NotImplementedError(".has_permission() must be overridden.")
class IsAuthenticated(BasePermission):
"""
Allows access only to authenticated users.
"""
def has_permission(self, request, obj=None):
if request.user and request.user.is_authenticated():
return True
return False
class IsAdminUser(BasePermission):
"""
Allows access only to admin users.
"""
def has_permission(self, request, obj=None):
if request.user and request.user.is_staff:
return True
return False
class IsAuthenticatedOrReadOnly(BasePermission):
"""
The request is authenticated as a user, or is a read-only request.
"""
def has_permission(self, request, obj=None):
if (request.method in SAFE_METHODS or
request.user and
request.user.is_authenticated()):
return True
return False
class DjangoModelPermissions(BasePermission):
"""
The request is authenticated using `django.contrib.auth` permissions.
See: https://docs.djangoproject.com/en/dev/topics/auth/#permissions
It ensures that the user is authenticated, and has the appropriate
`add`/`change`/`delete` permissions on the model.
This permission should only be used on views with a `ModelResource`.
"""
# Map methods into required permission codes.
# Override this if you need to also provide 'view' permissions,
# or if you want to provide custom permission codes.
perms_map = {
'GET': [],
'OPTIONS': [],
'HEAD': [],
'POST': ['%(app_label)s.add_%(model_name)s'],
'PUT': ['%(app_label)s.change_%(model_name)s'],
'PATCH': ['%(app_label)s.change_%(model_name)s'],
'DELETE': ['%(app_label)s.delete_%(model_name)s'],
}
def get_required_permissions(self, method, model_cls):
"""
Given a model and an HTTP method, return the list of permission
codes that the user is required to have.
"""
kwargs = {
'app_label': model_cls._meta.app_label,
'model_name': model_cls._meta.module_name
}
return [perm % kwargs for perm in self.perms_map[method]]
def has_permission(self, request, obj=None):
model_cls = self.view.model
perms = self.get_required_permissions(request.method, model_cls)
if (request.user and
request.user.is_authenticated() and
request.user.has_perms(perms, obj)):
return True
return False

402
rest_framework/renderers.py Normal file
View File

@ -0,0 +1,402 @@
"""
Renderers are used to serialize a View's output into specific media types.
Django REST framework also provides HTML and PlainText renderers that help self-document the API,
by serializing the output along with documentation regarding the View, output status and headers,
and providing forms and links depending on the allowed methods, renderers and parsers on the View.
"""
from django import forms
from django.template import RequestContext, loader
from django.utils import simplejson as json
from rest_framework.compat import yaml
from rest_framework.settings import api_settings
from rest_framework.utils import dict2xml
from rest_framework.utils import encoders
from rest_framework.utils.breadcrumbs import get_breadcrumbs
from rest_framework.utils.mediatypes import get_media_type_params, add_media_type_param, media_type_matches
from rest_framework import VERSION
from rest_framework.fields import FloatField, IntegerField, DateTimeField, DateField, EmailField, CharField, BooleanField
import string
__all__ = (
'BaseRenderer',
'TemplateRenderer',
'JSONRenderer',
'JSONPRenderer',
'DocumentingHTMLRenderer',
'DocumentingXHTMLRenderer',
'DocumentingPlainTextRenderer',
'XMLRenderer',
'YAMLRenderer'
)
class BaseRenderer(object):
"""
All renderers must extend this class, set the :attr:`media_type` attribute,
and override the :meth:`render` method.
"""
_FORMAT_QUERY_PARAM = 'format'
media_type = None
format = None
def __init__(self, view=None):
self.view = view
def can_handle_format(self, format):
return format == self.format
def can_handle_media_type(self, media_type):
"""
Returns `True` if this renderer is able to deal with the given
media type.
The default implementation for this function is to check the media type
argument against the media_type attribute set on the class to see if
they match.
This may be overridden to provide for other behavior, but typically
you'll instead want to just set the `media_type` attribute on the class.
"""
return media_type_matches(self.media_type, media_type)
def render(self, obj=None, media_type=None):
"""
Given an object render it into a string.
The requested media type is also passed to this method,
as it may contain parameters relevant to how the parser
should render the output.
EG: ``application/json; indent=4``
By default render simply returns the output as-is.
Override this method to provide for other behavior.
"""
if obj is None:
return ''
return str(obj)
class JSONRenderer(BaseRenderer):
"""
Renderer which serializes to JSON
"""
media_type = 'application/json'
format = 'json'
encoder_class = encoders.JSONEncoder
def render(self, obj=None, media_type=None):
"""
Renders *obj* into serialized JSON.
"""
if obj is None:
return ''
# If the media type looks like 'application/json; indent=4', then
# pretty print the result.
indent = get_media_type_params(media_type).get('indent', None)
sort_keys = False
try:
indent = max(min(int(indent), 8), 0)
sort_keys = True
except (ValueError, TypeError):
indent = None
return json.dumps(obj, cls=self.encoder_class, indent=indent, sort_keys=sort_keys)
class JSONPRenderer(JSONRenderer):
"""
Renderer which serializes to JSONP
"""
media_type = 'application/javascript'
format = 'jsonp'
renderer_class = JSONRenderer
callback_parameter = 'callback'
def _get_callback(self):
return self.view.request.GET.get(self.callback_parameter, self.callback_parameter)
def _get_renderer(self):
return self.renderer_class(self.view)
def render(self, obj=None, media_type=None):
callback = self._get_callback()
json = self._get_renderer().render(obj, media_type)
return "%s(%s);" % (callback, json)
class XMLRenderer(BaseRenderer):
"""
Renderer which serializes to XML.
"""
media_type = 'application/xml'
format = 'xml'
def render(self, obj=None, media_type=None):
"""
Renders *obj* into serialized XML.
"""
if obj is None:
return ''
return dict2xml(obj)
class YAMLRenderer(BaseRenderer):
"""
Renderer which serializes to YAML.
"""
media_type = 'application/yaml'
format = 'yaml'
def render(self, obj=None, media_type=None):
"""
Renders *obj* into serialized YAML.
"""
if obj is None:
return ''
return yaml.safe_dump(obj)
class TemplateRenderer(BaseRenderer):
"""
A Base class provided for convenience.
Render the object simply by using the given template.
To create a template renderer, subclass this class, and set
the :attr:`media_type` and :attr:`template` attributes.
"""
media_type = None
template = None
def render(self, obj=None, media_type=None):
"""
Renders *obj* using the :attr:`template` specified on the class.
"""
if obj is None:
return ''
template = loader.get_template(self.template)
context = RequestContext(self.view.request, {'object': obj})
return template.render(context)
class DocumentingTemplateRenderer(BaseRenderer):
"""
Base class for renderers used to self-document the API.
Implementing classes should extend this class and set the template attribute.
"""
template = None
def _get_content(self, view, request, obj, media_type):
"""
Get the content as if it had been rendered by a non-documenting renderer.
(Typically this will be the content as it would have been if the Resource had been
requested with an 'Accept: */*' header, although with verbose style formatting if appropriate.)
"""
# Find the first valid renderer and render the content. (Don't use another documenting renderer.)
renderers = [renderer for renderer in view.renderer_classes
if not issubclass(renderer, DocumentingTemplateRenderer)]
if not renderers:
return '[No renderers were found]'
media_type = add_media_type_param(media_type, 'indent', '4')
content = renderers[0](view).render(obj, media_type)
if not all(char in string.printable for char in content):
return '[%d bytes of binary content]'
return content
def _get_form_instance(self, view, method):
"""
Get a form, possibly bound to either the input or output data.
In the absence on of the Resource having an associated form then
provide a form that can be used to submit arbitrary content.
"""
if not hasattr(self.view, 'get_serializer'): # No serializer, no form.
return
# We need to map our Fields to Django's Fields.
field_mapping = dict([
[FloatField.__name__, forms.FloatField],
[IntegerField.__name__, forms.IntegerField],
[DateTimeField.__name__, forms.DateTimeField],
[DateField.__name__, forms.DateField],
[EmailField.__name__, forms.EmailField],
[CharField.__name__, forms.CharField],
[BooleanField.__name__, forms.BooleanField]
])
# Creating an on the fly form see: http://stackoverflow.com/questions/3915024/dynamically-creating-classes-python
fields = {}
object, data = None, None
if hasattr(self.view, 'object'):
object = self.view.object
serializer = self.view.get_serializer(instance=object)
for k, v in serializer.fields.items():
fields[k] = field_mapping[v.__class__.__name__]()
OnTheFlyForm = type("OnTheFlyForm", (forms.Form,), fields)
if object and not self.view.request.method == 'DELETE': # Don't fill in the form when the object is deleted
data = serializer.data
form_instance = OnTheFlyForm(data)
return form_instance
def _get_generic_content_form(self, view):
"""
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 view won't treat the form's value as the content of the request.
if not getattr(view.request, '_USE_FORM_OVERLOADING', False):
return None
# NB. http://jacobian.org/writing/dynamic-form-generation/
class GenericContentForm(forms.Form):
def __init__(self, view, request):
"""We don't know the names of the fields we want to set until the point the form is instantiated,
as they are determined by the Resource the form is being created against.
Add the fields dynamically."""
super(GenericContentForm, self).__init__()
contenttype_choices = [(media_type, media_type) for media_type in view._parsed_media_types]
initial_contenttype = view._default_parser.media_type
self.fields[request._CONTENTTYPE_PARAM] = forms.ChoiceField(label='Content Type',
choices=contenttype_choices,
initial=initial_contenttype)
self.fields[request._CONTENT_PARAM] = forms.CharField(label='Content',
widget=forms.Textarea)
# If either of these reserved parameters are turned off then content tunneling is not possible
if self.view.request._CONTENTTYPE_PARAM is None or self.view.request._CONTENT_PARAM is None:
return None
# Okey doke, let's do it
return GenericContentForm(view, view.request)
def get_name(self):
try:
return self.view.get_name()
except AttributeError:
return self.view.__doc__
def get_description(self, html=None):
if html is None:
html = bool('html' in self.format)
try:
return self.view.get_description(html)
except AttributeError:
return self.view.__doc__
def render(self, obj=None, media_type=None):
"""
Renders *obj* using the :attr:`template` set on the class.
The context used in the template contains all the information
needed to self-document the response to this request.
"""
content = self._get_content(self.view, self.view.request, obj, media_type)
put_form_instance = self._get_form_instance(self.view, 'put')
post_form_instance = self._get_form_instance(self.view, 'post')
name = self.get_name()
description = self.get_description()
breadcrumb_list = get_breadcrumbs(self.view.request.path)
template = loader.get_template(self.template)
context = RequestContext(self.view.request, {
'content': content,
'view': self.view,
'request': self.view.request,
'response': self.view.response,
'description': description,
'name': name,
'version': VERSION,
'breadcrumblist': breadcrumb_list,
'allowed_methods': self.view.allowed_methods,
'available_formats': self.view._rendered_formats,
'put_form': put_form_instance,
'post_form': post_form_instance,
'FORMAT_PARAM': self._FORMAT_QUERY_PARAM,
'METHOD_PARAM': getattr(self.view, '_METHOD_PARAM', None),
'api_settings': api_settings
})
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.view.response.status_code == 204:
self.view.response.status_code = 200
return ret
class DocumentingHTMLRenderer(DocumentingTemplateRenderer):
"""
Renderer which provides a browsable HTML interface for an API.
See the examples at http://api.django-rest-framework.org to see this in action.
"""
media_type = 'text/html'
format = 'html'
template = 'rest_framework/api.html'
class DocumentingXHTMLRenderer(DocumentingTemplateRenderer):
"""
Identical to DocumentingHTMLRenderer, except with an xhtml media type.
We need this to be listed in preference to xml in order to return HTML to WebKit based browsers,
given their Accept headers.
"""
media_type = 'application/xhtml+xml'
format = 'xhtml'
template = 'rest_framework/api.html'
class DocumentingPlainTextRenderer(DocumentingTemplateRenderer):
"""
Renderer that serializes the object with the default renderer, but also provides plain-text
documentation of the returned status and headers, and of the resource's name and description.
Useful for browsing an API with command line tools.
"""
media_type = 'text/plain'
format = 'txt'
template = 'rest_framework/api.txt'
DEFAULT_RENDERERS = (
JSONRenderer,
JSONPRenderer,
DocumentingHTMLRenderer,
DocumentingXHTMLRenderer,
DocumentingPlainTextRenderer,
XMLRenderer
)
if yaml:
DEFAULT_RENDERERS += (YAMLRenderer, )
else:
YAMLRenderer = None

284
rest_framework/request.py Normal file
View File

@ -0,0 +1,284 @@
"""
The :mod:`request` module provides a :class:`Request` class used to wrap the standard `request`
object received in all the views.
The wrapped request then offers a richer API, in particular :
- content automatically parsed according to `Content-Type` header,
and available as :meth:`.DATA<Request.DATA>`
- full support of PUT method, including support for file uploads
- form overloading of HTTP method, content type and content
"""
from StringIO import StringIO
from rest_framework import exceptions
from rest_framework.settings import api_settings
from rest_framework.utils.mediatypes import is_form_media_type
__all__ = ('Request',)
class Empty(object):
"""
Placeholder for unset attributes.
Cannot use `None`, as that may be a valid value.
"""
pass
def _hasattr(obj, name):
return not getattr(obj, name) is Empty
class Request(object):
"""
Wrapper allowing to enhance a standard `HttpRequest` instance.
Kwargs:
- request(HttpRequest). The original request instance.
- parsers_classes(list/tuple). The parsers to use for parsing the
request content.
- authentication_classes(list/tuple). The authentications used to try
authenticating the request's user.
"""
_METHOD_PARAM = api_settings.FORM_METHOD_OVERRIDE
_CONTENT_PARAM = api_settings.FORM_CONTENT_OVERRIDE
_CONTENTTYPE_PARAM = api_settings.FORM_CONTENTTYPE_OVERRIDE
def __init__(self, request, parser_classes=None, authentication_classes=None):
self._request = request
self.parser_classes = parser_classes or ()
self.authentication_classes = authentication_classes or ()
self._data = Empty
self._files = Empty
self._method = Empty
self._content_type = Empty
self._stream = Empty
def get_parsers(self):
"""
Instantiates and returns the list of parsers the request will use.
"""
return [parser() for parser in self.parser_classes]
def get_authentications(self):
"""
Instantiates and returns the list of parsers the request will use.
"""
return [authentication() for authentication in self.authentication_classes]
@property
def method(self):
"""
Returns the HTTP method.
This allows the `method` to be overridden by using a hidden `form`
field on a form POST request.
"""
if not _hasattr(self, '_method'):
self._load_method_and_content_type()
return self._method
@property
def content_type(self):
"""
Returns the content type header.
This should be used instead of ``request.META.get('HTTP_CONTENT_TYPE')``,
as it allows the content type to be overridden by using a hidden form
field on a form POST request.
"""
if not _hasattr(self, '_content_type'):
self._load_method_and_content_type()
return self._content_type
@property
def stream(self):
"""
Returns an object that may be used to stream the request content.
"""
if not _hasattr(self, '_stream'):
self._load_stream()
return self._stream
@property
def DATA(self):
"""
Parses the request body and returns the data.
Similar to usual behaviour of `request.POST`, except that it handles
arbitrary parsers, and also works on methods other than POST (eg PUT).
"""
if not _hasattr(self, '_data'):
self._load_data_and_files()
return self._data
@property
def FILES(self):
"""
Parses the request body and returns any files uploaded in the request.
Similar to usual behaviour of `request.FILES`, except that it handles
arbitrary parsers, and also works on methods other than POST (eg PUT).
"""
if not _hasattr(self, '_files'):
self._load_data_and_files()
return self._files
@property
def user(self):
"""
Returns the user associated with the current request, as authenticated
by the authentication classes provided to the request.
"""
if not hasattr(self, '_user'):
self._user, self._auth = self._authenticate()
return self._user
@property
def auth(self):
"""
Returns any non-user authentication information associated with the
request, such as an authentication token.
"""
if not hasattr(self, '_auth'):
self._user, self._auth = self._authenticate()
return self._auth
def _load_data_and_files(self):
"""
Parses the request content into self.DATA and self.FILES.
"""
if not _hasattr(self, '_content_type'):
self._load_method_and_content_type()
if not _hasattr(self, '_data'):
self._data, self._files = self._parse()
def _load_method_and_content_type(self):
"""
Sets the method and content_type, and then check if they've
been overridden.
"""
self._content_type = self.META.get('HTTP_CONTENT_TYPE',
self.META.get('CONTENT_TYPE', ''))
self._perform_form_overloading()
# if the HTTP method was not overloaded, we take the raw HTTP method
if not _hasattr(self, '_method'):
self._method = self._request.method
def _load_stream(self):
"""
Return the content body of the request, as a stream.
"""
try:
content_length = int(self.META.get('CONTENT_LENGTH',
self.META.get('HTTP_CONTENT_LENGTH')))
except (ValueError, TypeError):
content_length = 0
if content_length == 0:
self._stream = None
elif hasattr(self._request, 'read'):
self._stream = self._request
else:
self._stream = StringIO(self.raw_post_data)
def _perform_form_overloading(self):
"""
If this is a form POST request, then we need to check if the method and
content/content_type have been overridden by setting them in hidden
form fields or not.
"""
USE_FORM_OVERLOADING = (
self._METHOD_PARAM or
(self._CONTENT_PARAM and self._CONTENTTYPE_PARAM)
)
# We only need to use form overloading on form POST requests.
if (not USE_FORM_OVERLOADING
or self._request.method != 'POST'
or not is_form_media_type(self._content_type)):
return
# At this point we're committed to parsing the request as form data.
self._data = self._request.POST
self._files = self._request.FILES
# Method overloading - change the method and remove the param from the content.
if (self._METHOD_PARAM and
self._METHOD_PARAM in self._data):
self._method = self._data[self._METHOD_PARAM].upper()
self._data.pop(self._METHOD_PARAM)
# Content overloading - modify the content type, and re-parse.
if (self._CONTENT_PARAM and
self._CONTENTTYPE_PARAM and
self._CONTENT_PARAM in self._data and
self._CONTENTTYPE_PARAM in self._data):
self._content_type = self._data[self._CONTENTTYPE_PARAM]
self._stream = StringIO(self._data[self._CONTENT_PARAM])
self._data.pop(self._CONTENTTYPE_PARAM)
self._data.pop(self._CONTENT_PARAM)
self._data, self._files = self._parse()
def _parse(self):
"""
Parse the request content, returning a two-tuple of (data, files)
May raise an `UnsupportedMediaType`, or `ParseError` exception.
"""
if self.stream is None or self.content_type is None:
return (None, None)
for parser in self.get_parsers():
if parser.can_handle_request(self.content_type):
parsed = parser.parse(self.stream, meta=self.META,
upload_handlers=self.upload_handlers)
# Parser classes may return the raw data, or a
# DataAndFiles object. Unpack the result as required.
try:
return (parsed.data, parsed.files)
except AttributeError:
return (parsed, None)
raise exceptions.UnsupportedMediaType(self._content_type)
def _authenticate(self):
"""
Attempt to authenticate the request using each authentication instance in turn.
Returns a two-tuple of (user, authtoken).
"""
for authentication in self.get_authentications():
user_auth_tuple = authentication.authenticate(self)
if not user_auth_tuple is None:
return user_auth_tuple
return self._not_authenticated()
def _not_authenticated(self):
"""
Return a two-tuple of (user, authtoken), representing an
unauthenticated request.
By default this will be (AnonymousUser, None).
"""
if api_settings.UNAUTHENTICATED_USER:
user = api_settings.UNAUTHENTICATED_USER()
else:
user = None
if api_settings.UNAUTHENTICATED_TOKEN:
auth = api_settings.UNAUTHENTICATED_TOKEN()
else:
auth = None
return (user, auth)
def __getattr__(self, attr):
"""
Proxy other attributes to the underlying HttpRequest object.
"""
return getattr(self._request, attr)

View File

@ -0,0 +1,87 @@
from functools import update_wrapper
import inspect
from django.utils.decorators import classonlymethod
from djanorestframework import views, generics
def wrapped(source, dest):
"""
Copy public, non-method attributes from source to dest, and return dest.
"""
for attr in [attr for attr in dir(source)
if not attr.startswith('_') and not inspect.ismethod(attr)]:
setattr(dest, attr, getattr(source, attr))
return dest
class ResourceMixin(object):
"""
Clone Django's `View.as_view()` behaviour *except* using REST framework's
'method -> action' binding for resources.
"""
@classonlymethod
def as_view(cls, actions, **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("You tried to pass in the %s method name as a "
"keyword argument to %s(). Don't do that."
% (key, cls.__name__))
if not hasattr(cls, key):
raise TypeError("%s() received an invalid keyword %r" % (
cls.__name__, key))
def view(request, *args, **kwargs):
self = cls(**initkwargs)
# Bind methods to actions
for method, action in actions.items():
handler = getattr(self, action)
setattr(self, method, handler)
# As you were, solider.
if hasattr(self, 'get') and not hasattr(self, 'head'):
self.head = self.get
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
class Resource(ResourceMixin, views.APIView):
pass
class ModelResource(ResourceMixin, views.APIView):
root_class = generics.RootAPIView
detail_class = generics.InstanceAPIView
def root_view(self):
return wrapped(self, self.root_class())
def detail_view(self):
return wrapped(self, self.detail_class())
def list(self, request, *args, **kwargs):
return self.root_view().list(request, args, kwargs)
def create(self, request, *args, **kwargs):
return self.root_view().create(request, args, kwargs)
def retrieve(self, request, *args, **kwargs):
return self.detail_view().retrieve(request, args, kwargs)
def update(self, request, *args, **kwargs):
return self.detail_view().update(request, args, kwargs)
def destroy(self, request, *args, **kwargs):
return self.detail_view().destroy(request, args, kwargs)

View File

@ -0,0 +1,39 @@
from django.template.response import SimpleTemplateResponse
from django.core.handlers.wsgi import STATUS_CODE_TEXT
class Response(SimpleTemplateResponse):
"""
An HttpResponse that allows it's data to be rendered into
arbitrary media types.
"""
def __init__(self, data=None, status=None, headers=None,
renderer=None, media_type=None):
"""
Alters the init arguments slightly.
For example, drop 'template_name', and instead use 'data'.
Setting 'renderer' and 'media_type' will typically be defered,
For example being set automatically by the `APIView`.
"""
super(Response, self).__init__(None, status=status)
self.data = data
self.headers = headers and headers[:] or []
self.renderer = renderer
self.media_type = media_type
@property
def rendered_content(self):
self['Content-Type'] = self.renderer.media_type
if self.data is None:
return self.renderer.render()
return self.renderer.render(self.data, self.media_type)
@property
def status_text(self):
"""
Returns reason text corresponding to our HTTP response status code.
Provided for convenience.
"""
return STATUS_CODE_TEXT.get(self.status_code, '')

20
rest_framework/reverse.py Normal file
View File

@ -0,0 +1,20 @@
"""
Provide reverse functions that return fully qualified URLs
"""
from django.core.urlresolvers import reverse as django_reverse
from django.utils.functional import lazy
def reverse(viewname, *args, **kwargs):
"""
Same as `django.core.urlresolvers.reverse`, but optionally takes a request
and returns a fully qualified URL, using the request to get the base URL.
"""
request = kwargs.pop('request', None)
url = django_reverse(viewname, *args, **kwargs)
if request:
return request.build_absolute_uri(url)
return url
reverse_lazy = lazy(reverse, str)

View File

View File

@ -0,0 +1,66 @@
#!/usr/bin/env python
"""
Useful tool to run the test suite for rest_framework and generate a coverage report.
"""
# http://ericholscher.com/blog/2009/jun/29/enable-setuppy-test-your-django-apps/
# http://www.travisswicegood.com/2010/01/17/django-virtualenv-pip-and-fabric/
# http://code.djangoproject.com/svn/django/trunk/tests/runtests.py
import os
import sys
os.environ['DJANGO_SETTINGS_MODULE'] = 'rest_framework.runtests.settings'
from coverage import coverage
def main():
"""Run the tests for rest_framework and generate a coverage report."""
cov = coverage()
cov.erase()
cov.start()
from django.conf import settings
from django.test.utils import get_runner
TestRunner = get_runner(settings)
if hasattr(TestRunner, 'func_name'):
# Pre 1.2 test runners were just functions,
# and did not support the 'failfast' option.
import warnings
warnings.warn(
'Function-based test runners are deprecated. Test runners should be classes with a run_tests() method.',
DeprecationWarning
)
failures = TestRunner(['rest_framework'])
else:
test_runner = TestRunner()
failures = test_runner.run_tests(['rest_framework'])
cov.stop()
# Discover the list of all modules that we should test coverage for
import rest_framework
project_dir = os.path.dirname(rest_framework.__file__)
cov_files = []
for (path, dirs, files) in os.walk(project_dir):
# Drop tests and runtests directories from the test coverage report
if os.path.basename(path) == 'tests' or os.path.basename(path) == 'runtests':
continue
# Drop the compat module from coverage, since we're not interested in the coverage
# of a module which is specifically for resolving environment dependant imports.
# (Because we'll end up getting different coverage reports for it for each environment)
if 'compat.py' in files:
files.remove('compat.py')
cov_files.extend([os.path.join(path, file) for file in files if file.endswith('.py')])
cov.report(cov_files)
if '--html' in sys.argv:
cov.html_report(cov_files, directory='coverage')
sys.exit(failures)
if __name__ == '__main__':
main()

View File

@ -0,0 +1,40 @@
#!/usr/bin/env python
# http://ericholscher.com/blog/2009/jun/29/enable-setuppy-test-your-django-apps/
# http://www.travisswicegood.com/2010/01/17/django-virtualenv-pip-and-fabric/
# http://code.djangoproject.com/svn/django/trunk/tests/runtests.py
import os
import sys
os.environ['DJANGO_SETTINGS_MODULE'] = 'rest_framework.runtests.settings'
from django.conf import settings
from django.test.utils import get_runner
def usage():
return """
Usage: python runtests.py [UnitTestClass].[method]
You can pass the Class name of the `UnitTestClass` you want to test.
Append a method name if you only want to test a specific method of that class.
"""
def main():
TestRunner = get_runner(settings)
test_runner = TestRunner()
if len(sys.argv) == 2:
test_case = '.' + sys.argv[1]
elif len(sys.argv) == 1:
test_case = ''
else:
print usage()
sys.exit(1)
failures = test_runner.run_tests(['rest_framework' + test_case])
sys.exit(failures)
if __name__ == '__main__':
main()

View File

@ -0,0 +1,118 @@
# Django settings for testproject project.
DEBUG = True
TEMPLATE_DEBUG = DEBUG
DEBUG_PROPAGATE_EXCEPTIONS = True
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 = ''
# 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',
'rest_framework',
'rest_framework.authtoken',
)
STATIC_URL = '/static/'
import django
if django.VERSION < (1, 3):
INSTALLED_APPS += ('staticfiles',)
# OAuth support is optional, so we only test oauth if it's installed.
try:
import oauth_provider
except ImportError:
pass
else:
INSTALLED_APPS += ('oauth_provider',)
# If we're running on the Jenkins server we want to archive the coverage reports as XML.
import os
if os.environ.get('HUDSON_URL', None):
TEST_RUNNER = 'xmlrunner.extra.djangotestrunner.XMLTestRunner'
TEST_OUTPUT_VERBOSE = True
TEST_OUTPUT_DESCRIPTIONS = True
TEST_OUTPUT_DIR = 'xmlrunner'

View File

@ -0,0 +1,7 @@
"""
Blank URLConf just to keep runtests.py happy.
"""
from django.conf.urls.defaults import *
urlpatterns = patterns('',
)

View File

@ -0,0 +1,348 @@
from decimal import Decimal
from django.core.serializers.base import DeserializedObject
from django.utils.datastructures import SortedDict
import copy
import datetime
import types
from rest_framework.fields import *
class DictWithMetadata(dict):
"""
A dict-like object, that can have additional properties attached.
"""
pass
class SortedDictWithMetadata(SortedDict, DictWithMetadata):
"""
A sorted dict-like object, that can have additional properties attached.
"""
pass
class RecursionOccured(BaseException):
pass
def _is_protected_type(obj):
"""
True if the object is a native datatype that does not need to
be serialized further.
"""
return isinstance(obj, (
types.NoneType,
int, long,
datetime.datetime, datetime.date, datetime.time,
float, Decimal,
basestring)
)
def _get_declared_fields(bases, attrs):
"""
Create a list of serializer field instances from the passed in 'attrs',
plus any fields on the base classes (in 'bases').
Note that all fields from the base classes are used.
"""
fields = [(field_name, attrs.pop(field_name))
for field_name, obj in attrs.items()
if isinstance(obj, Field)]
fields.sort(key=lambda x: x[1].creation_counter)
# If this class is subclassing another Serializer, add that Serializer's
# fields. Note that we loop over the bases in *reverse*. This is necessary
# in order to the correct order of fields.
for base in bases[::-1]:
if hasattr(base, 'base_fields'):
fields = base.base_fields.items() + fields
return SortedDict(fields)
class SerializerMetaclass(type):
def __new__(cls, name, bases, attrs):
attrs['base_fields'] = _get_declared_fields(bases, attrs)
return super(SerializerMetaclass, cls).__new__(cls, name, bases, attrs)
class SerializerOptions(object):
"""
Meta class options for ModelSerializer
"""
def __init__(self, meta):
self.nested = getattr(meta, 'nested', False)
self.fields = getattr(meta, 'fields', ())
self.exclude = getattr(meta, 'exclude', ())
class BaseSerializer(Field):
class Meta(object):
pass
_options_class = SerializerOptions
_dict_class = SortedDictWithMetadata # Set to unsorted dict for backwards compatability with unsorted implementations.
def __init__(self, data=None, instance=None, context=None, **kwargs):
super(BaseSerializer, self).__init__(**kwargs)
self.fields = copy.deepcopy(self.base_fields)
self.opts = self._options_class(self.Meta)
self.parent = None
self.root = None
self.stack = []
self.context = context or {}
self.init_data = data
self.instance = instance
self._data = None
self._errors = None
#####
# Methods to determine which fields to use when (de)serializing objects.
def default_fields(self, serialize, obj=None, data=None, nested=False):
"""
Return the complete set of default fields for the object, as a dict.
"""
return {}
def get_fields(self, serialize, obj=None, data=None, nested=False):
"""
Returns the complete set of fields for the object as a dict.
This will be the set of any explicitly declared fields,
plus the set of fields returned by get_default_fields().
"""
ret = SortedDict()
# Get the explicitly declared fields
for key, field in self.fields.items():
ret[key] = field
# Determine if the declared field corrosponds to a model field.
try:
if key == 'pk':
model_field = obj._meta.pk
else:
model_field = obj._meta.get_field_by_name(key)[0]
except:
model_field = None
# Set up the field
field.initialize(parent=self, model_field=model_field)
# Add in the default fields
fields = self.default_fields(serialize, obj, data, nested)
for key, val in fields.items():
if key not in ret:
ret[key] = val
# If 'fields' is specified, use those fields, in that order.
if self.opts.fields:
new = SortedDict()
for key in self.opts.fields:
new[key] = ret[key]
ret = new
# Remove anything in 'exclude'
if self.opts.exclude:
for key in self.opts.exclude:
ret.pop(key, None)
return ret
#####
# Field methods - used when the serializer class is itself used as a field.
def initialize(self, parent, model_field=None):
"""
Same behaviour as usual Field, except that we need to keep track
of state so that we can deal with handling maximum depth and recursion.
"""
super(BaseSerializer, self).initialize(parent, model_field)
self.stack = parent.stack[:]
if parent.opts.nested and not isinstance(parent.opts.nested, bool):
self.opts.nested = parent.opts.nested - 1
else:
self.opts.nested = parent.opts.nested
#####
# Methods to convert or revert from objects <--> primative representations.
def get_field_key(self, field_name):
"""
Return the key that should be used for a given field.
"""
return field_name
def convert_object(self, obj):
"""
Core of serialization.
Convert an object into a dictionary of serialized field values.
"""
if obj in self.stack and not self.source == '*':
raise RecursionOccured()
self.stack.append(obj)
ret = self._dict_class()
ret.fields = {}
fields = self.get_fields(serialize=True, obj=obj, nested=self.opts.nested)
for field_name, field in fields.items():
key = self.get_field_key(field_name)
try:
value = field.field_to_native(obj, field_name)
except RecursionOccured:
field = self.get_fields(serialize=True, obj=obj, nested=False)[field_name]
value = field.field_to_native(obj, field_name)
ret[key] = value
ret.fields[key] = field
return ret
def restore_fields(self, data):
"""
Core of deserialization, together with `restore_object`.
Converts a dictionary of data into a dictionary of deserialized fields.
"""
fields = self.get_fields(serialize=False, data=data, nested=self.opts.nested)
reverted_data = {}
for field_name, field in fields.items():
try:
field.field_from_native(data, field_name, reverted_data)
except ValidationError as err:
self._errors[field_name] = list(err.messages)
return reverted_data
def restore_object(self, attrs, instance=None):
"""
Deserialize a dictionary of attributes into an object instance.
You should override this method to control how deserialized objects
are instantiated.
"""
if instance is not None:
instance.update(attrs)
return instance
return attrs
def to_native(self, obj):
"""
Serialize objects -> primatives.
"""
if isinstance(obj, dict):
return dict([(key, self.to_native(val))
for (key, val) in obj.items()])
elif hasattr(obj, '__iter__'):
return (self.to_native(item) for item in obj)
return self.convert_object(obj)
def from_native(self, data):
"""
Deserialize primatives -> objects.
"""
if hasattr(data, '__iter__') and not isinstance(data, dict):
# TODO: error data when deserializing lists
return (self.from_native(item) for item in data)
self._errors = {}
attrs = self.restore_fields(data)
if not self._errors:
return self.restore_object(attrs, instance=getattr(self, 'instance', None))
@property
def errors(self):
"""
Run deserialization and return error data,
setting self.object if no errors occured.
"""
if self._errors is None:
obj = self.from_native(self.init_data)
if not self._errors:
self.object = obj
return self._errors
def is_valid(self):
return not self.errors
@property
def data(self):
if self._data is None:
self._data = self.to_native(self.instance)
return self._data
class Serializer(BaseSerializer):
__metaclass__ = SerializerMetaclass
class ModelSerializerOptions(SerializerOptions):
"""
Meta class options for ModelSerializer
"""
def __init__(self, meta):
super(ModelSerializerOptions, self).__init__(meta)
self.model = getattr(meta, 'model', None)
class ModelSerializer(RelatedField, Serializer):
"""
A serializer that deals with model instances and querysets.
"""
_options_class = ModelSerializerOptions
def default_fields(self, serialize, obj=None, data=None, nested=False):
"""
Return all the fields that should be serialized for the model.
"""
if serialize:
cls = obj.__class__
else:
cls = self.opts.model
opts = cls._meta.concrete_model._meta
pk_field = opts.pk
while pk_field.rel:
pk_field = pk_field.rel.to._meta.pk
fields = [pk_field]
fields += [field for field in opts.fields if field.serialize]
fields += [field for field in opts.many_to_many if field.serialize]
ret = SortedDict()
for model_field in fields:
if model_field.rel and nested:
field = self.get_nested_field(model_field)
elif model_field.rel:
field = self.get_related_field(model_field)
else:
field = self.get_field(model_field)
field.initialize(parent=self, model_field=model_field)
ret[model_field.name] = field
return ret
def get_nested_field(self, model_field):
"""
Creates a default instance of a nested relational field.
"""
return ModelSerializer()
def get_related_field(self, model_field):
"""
Creates a default instance of a flat relational field.
"""
return PrimaryKeyRelatedField()
def get_field(self, model_field):
"""
Creates a default instance of a basic field.
"""
return Field()
def restore_object(self, attrs, instance=None):
"""
Restore the model instance.
"""
m2m_data = {}
for field in self.opts.model._meta.many_to_many:
if field.name in attrs:
m2m_data[field.name] = attrs.pop(field.name)
return DeserializedObject(self.opts.model(**attrs), m2m_data)

125
rest_framework/settings.py Normal file
View File

@ -0,0 +1,125 @@
"""
Settings for REST framework are all namespaced in the REST_FRAMEWORK setting.
For example your project's `settings.py` file might look like this:
REST_FRAMEWORK = {
'DEFAULT_RENDERERS': (
'rest_framework.renderers.JSONRenderer',
'rest_framework.renderers.YAMLRenderer',
)
'DEFAULT_PARSERS': (
'rest_framework.parsers.JSONParser',
'rest_framework.parsers.YAMLParser',
)
}
This module provides the `api_setting` object, that is used to access
REST framework settings, checking for user settings first, then falling
back to the defaults.
"""
from django.conf import settings
from django.utils import importlib
DEFAULTS = {
'DEFAULT_RENDERERS': (
'rest_framework.renderers.JSONRenderer',
'rest_framework.renderers.JSONPRenderer',
'rest_framework.renderers.DocumentingHTMLRenderer',
'rest_framework.renderers.DocumentingPlainTextRenderer',
),
'DEFAULT_PARSERS': (
'rest_framework.parsers.JSONParser',
'rest_framework.parsers.FormParser'
),
'DEFAULT_AUTHENTICATION': (
'rest_framework.authentication.SessionAuthentication',
'rest_framework.authentication.UserBasicAuthentication'
),
'DEFAULT_PERMISSIONS': (),
'DEFAULT_THROTTLES': (),
'DEFAULT_CONTENT_NEGOTIATION': 'rest_framework.negotiation.DefaultContentNegotiation',
'UNAUTHENTICATED_USER': 'django.contrib.auth.models.AnonymousUser',
'UNAUTHENTICATED_TOKEN': None,
'FORM_METHOD_OVERRIDE': '_method',
'FORM_CONTENT_OVERRIDE': '_content',
'FORM_CONTENTTYPE_OVERRIDE': '_content_type',
'URL_ACCEPT_OVERRIDE': '_accept',
'URL_FORMAT_OVERRIDE': 'format',
'FORMAT_SUFFIX_KWARG': 'format'
}
# List of settings that may be in string import notation.
IMPORT_STRINGS = (
'DEFAULT_RENDERERS',
'DEFAULT_PARSERS',
'DEFAULT_AUTHENTICATION',
'DEFAULT_PERMISSIONS',
'DEFAULT_THROTTLES',
'DEFAULT_CONTENT_NEGOTIATION',
'UNAUTHENTICATED_USER',
'UNAUTHENTICATED_TOKEN',
)
def perform_import(val, setting):
"""
If the given setting is a string import notation,
then perform the necessary import or imports.
"""
if val is None or not setting in IMPORT_STRINGS:
return val
if isinstance(val, basestring):
return import_from_string(val, setting)
elif isinstance(val, (list, tuple)):
return [import_from_string(item, setting) for item in val]
return val
def import_from_string(val, setting):
"""
Attempt to import a class from a string representation.
"""
try:
# Nod to tastypie's use of importlib.
parts = val.split('.')
module_path, class_name = '.'.join(parts[:-1]), parts[-1]
module = importlib.import_module(module_path)
return getattr(module, class_name)
except:
msg = "Could not import '%s' for API setting '%s'" % (val, setting)
raise ImportError(msg)
class APISettings(object):
"""
A settings object, that allows API settings to be accessed as properties.
For example:
from rest_framework.settings import api_settings
print api_settings.DEFAULT_RENDERERS
Any setting with string import paths will be automatically resolved
and return the class, rather than the string literal.
"""
def __getattr__(self, attr):
if attr not in DEFAULTS.keys():
raise AttributeError("Invalid API setting: '%s'" % attr)
try:
# Check if present in user settings
val = perform_import(settings.REST_FRAMEWORK[attr], attr)
except (AttributeError, KeyError):
# Fall back to defaults
val = perform_import(DEFAULTS[attr], attr)
# Cache the result
setattr(self, attr, val)
return val
api_settings = APISettings()

View File

@ -0,0 +1,22 @@
/*
This CSS file contains some tweaks specific to the included Bootstrap theme.
It's separate from `style.css` so that it can be easily overridden by replacing
a single block in the template.
*/
.form-actions {
background: transparent;
border-top-color: transparent;
padding-top: 0;
}
.navbar-inverse .brand a {
color: #999;
}
.navbar-inverse .brand:hover a {
color: white;
text-decoration: none;
}

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,30 @@
.com { color: #93a1a1; }
.lit { color: #195f91; }
.pun, .opn, .clo { color: #93a1a1; }
.fun { color: #dc322f; }
.str, .atv { color: #D14; }
.kwd, .prettyprint .tag { color: #1e347b; }
.typ, .atn, .dec, .var { color: teal; }
.pln { color: #48484c; }
.prettyprint {
padding: 8px;
background-color: #f7f7f9;
border: 1px solid #e1e1e8;
}
.prettyprint.linenums {
-webkit-box-shadow: inset 40px 0 0 #fbfbfc, inset 41px 0 0 #ececf0;
-moz-box-shadow: inset 40px 0 0 #fbfbfc, inset 41px 0 0 #ececf0;
box-shadow: inset 40px 0 0 #fbfbfc, inset 41px 0 0 #ececf0;
}
/* Specify class=linenums on a pre to get line numbering */
ol.linenums {
margin: 0 0 0 33px; /* IE indents via margin-left */
}
ol.linenums li {
padding-left: 12px;
color: #bebec5;
line-height: 20px;
text-shadow: 0 1px 0 #fff;
}

View File

@ -0,0 +1,74 @@
body {
padding-top: 0;
padding-bottom: 1em;
}
/* The navbar is fixed at >= 980px wide, so add padding to the body to prevent
content running up underneath it. */
@media (min-width:980px) {
body {
padding-top: 60px;
}
}
h1 {
font-weight: 500;
}
h2, h3 {
font-weight: 300;
}
.resource-description, .response-info {
margin-bottom: 2em;
}
#footer {
border-top: 1px solid #eee;
margin-top: 2em;
padding-top: 1em;
text-align: right;
}
.version:before {
content: "v";
opacity: 0.6;
padding-right: 0.25em;
}
.format-option {
font-family: Menlo, Consolas, "Andale Mono", "Lucida Console", monospace;
}
#options-form {
position: relative;
}
/* To allow tooltips to work on disabled elements */
.disabled-tooltip-shield {
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
}
#options-form {
margin-right: 1em;
}
.errorlist {
margin-top: 0.5em;
}
pre {
overflow: auto;
word-wrap: normal;
white-space: pre;
font-size: 12px;
}
.page-header {
border-bottom: none;
padding-bottom: 0px;
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,28 @@
var q=null;window.PR_SHOULD_USE_CONTINUATION=!0;
(function(){function L(a){function m(a){var f=a.charCodeAt(0);if(f!==92)return f;var b=a.charAt(1);return(f=r[b])?f:"0"<=b&&b<="7"?parseInt(a.substring(1),8):b==="u"||b==="x"?parseInt(a.substring(2),16):a.charCodeAt(1)}function e(a){if(a<32)return(a<16?"\\x0":"\\x")+a.toString(16);a=String.fromCharCode(a);if(a==="\\"||a==="-"||a==="["||a==="]")a="\\"+a;return a}function h(a){for(var f=a.substring(1,a.length-1).match(/\\u[\dA-Fa-f]{4}|\\x[\dA-Fa-f]{2}|\\[0-3][0-7]{0,2}|\\[0-7]{1,2}|\\[\S\s]|[^\\]/g),a=
[],b=[],o=f[0]==="^",c=o?1:0,i=f.length;c<i;++c){var j=f[c];if(/\\[bdsw]/i.test(j))a.push(j);else{var j=m(j),d;c+2<i&&"-"===f[c+1]?(d=m(f[c+2]),c+=2):d=j;b.push([j,d]);d<65||j>122||(d<65||j>90||b.push([Math.max(65,j)|32,Math.min(d,90)|32]),d<97||j>122||b.push([Math.max(97,j)&-33,Math.min(d,122)&-33]))}}b.sort(function(a,f){return a[0]-f[0]||f[1]-a[1]});f=[];j=[NaN,NaN];for(c=0;c<b.length;++c)i=b[c],i[0]<=j[1]+1?j[1]=Math.max(j[1],i[1]):f.push(j=i);b=["["];o&&b.push("^");b.push.apply(b,a);for(c=0;c<
f.length;++c)i=f[c],b.push(e(i[0])),i[1]>i[0]&&(i[1]+1>i[0]&&b.push("-"),b.push(e(i[1])));b.push("]");return b.join("")}function y(a){for(var f=a.source.match(/\[(?:[^\\\]]|\\[\S\s])*]|\\u[\dA-Fa-f]{4}|\\x[\dA-Fa-f]{2}|\\\d+|\\[^\dux]|\(\?[!:=]|[()^]|[^()[\\^]+/g),b=f.length,d=[],c=0,i=0;c<b;++c){var j=f[c];j==="("?++i:"\\"===j.charAt(0)&&(j=+j.substring(1))&&j<=i&&(d[j]=-1)}for(c=1;c<d.length;++c)-1===d[c]&&(d[c]=++t);for(i=c=0;c<b;++c)j=f[c],j==="("?(++i,d[i]===void 0&&(f[c]="(?:")):"\\"===j.charAt(0)&&
(j=+j.substring(1))&&j<=i&&(f[c]="\\"+d[i]);for(i=c=0;c<b;++c)"^"===f[c]&&"^"!==f[c+1]&&(f[c]="");if(a.ignoreCase&&s)for(c=0;c<b;++c)j=f[c],a=j.charAt(0),j.length>=2&&a==="["?f[c]=h(j):a!=="\\"&&(f[c]=j.replace(/[A-Za-z]/g,function(a){a=a.charCodeAt(0);return"["+String.fromCharCode(a&-33,a|32)+"]"}));return f.join("")}for(var t=0,s=!1,l=!1,p=0,d=a.length;p<d;++p){var g=a[p];if(g.ignoreCase)l=!0;else if(/[a-z]/i.test(g.source.replace(/\\u[\da-f]{4}|\\x[\da-f]{2}|\\[^UXux]/gi,""))){s=!0;l=!1;break}}for(var r=
{b:8,t:9,n:10,v:11,f:12,r:13},n=[],p=0,d=a.length;p<d;++p){g=a[p];if(g.global||g.multiline)throw Error(""+g);n.push("(?:"+y(g)+")")}return RegExp(n.join("|"),l?"gi":"g")}function M(a){function m(a){switch(a.nodeType){case 1:if(e.test(a.className))break;for(var g=a.firstChild;g;g=g.nextSibling)m(g);g=a.nodeName;if("BR"===g||"LI"===g)h[s]="\n",t[s<<1]=y++,t[s++<<1|1]=a;break;case 3:case 4:g=a.nodeValue,g.length&&(g=p?g.replace(/\r\n?/g,"\n"):g.replace(/[\t\n\r ]+/g," "),h[s]=g,t[s<<1]=y,y+=g.length,
t[s++<<1|1]=a)}}var e=/(?:^|\s)nocode(?:\s|$)/,h=[],y=0,t=[],s=0,l;a.currentStyle?l=a.currentStyle.whiteSpace:window.getComputedStyle&&(l=document.defaultView.getComputedStyle(a,q).getPropertyValue("white-space"));var p=l&&"pre"===l.substring(0,3);m(a);return{a:h.join("").replace(/\n$/,""),c:t}}function B(a,m,e,h){m&&(a={a:m,d:a},e(a),h.push.apply(h,a.e))}function x(a,m){function e(a){for(var l=a.d,p=[l,"pln"],d=0,g=a.a.match(y)||[],r={},n=0,z=g.length;n<z;++n){var f=g[n],b=r[f],o=void 0,c;if(typeof b===
"string")c=!1;else{var i=h[f.charAt(0)];if(i)o=f.match(i[1]),b=i[0];else{for(c=0;c<t;++c)if(i=m[c],o=f.match(i[1])){b=i[0];break}o||(b="pln")}if((c=b.length>=5&&"lang-"===b.substring(0,5))&&!(o&&typeof o[1]==="string"))c=!1,b="src";c||(r[f]=b)}i=d;d+=f.length;if(c){c=o[1];var j=f.indexOf(c),k=j+c.length;o[2]&&(k=f.length-o[2].length,j=k-c.length);b=b.substring(5);B(l+i,f.substring(0,j),e,p);B(l+i+j,c,C(b,c),p);B(l+i+k,f.substring(k),e,p)}else p.push(l+i,b)}a.e=p}var h={},y;(function(){for(var e=a.concat(m),
l=[],p={},d=0,g=e.length;d<g;++d){var r=e[d],n=r[3];if(n)for(var k=n.length;--k>=0;)h[n.charAt(k)]=r;r=r[1];n=""+r;p.hasOwnProperty(n)||(l.push(r),p[n]=q)}l.push(/[\S\s]/);y=L(l)})();var t=m.length;return e}function u(a){var m=[],e=[];a.tripleQuotedStrings?m.push(["str",/^(?:'''(?:[^'\\]|\\[\S\s]|''?(?=[^']))*(?:'''|$)|"""(?:[^"\\]|\\[\S\s]|""?(?=[^"]))*(?:"""|$)|'(?:[^'\\]|\\[\S\s])*(?:'|$)|"(?:[^"\\]|\\[\S\s])*(?:"|$))/,q,"'\""]):a.multiLineStrings?m.push(["str",/^(?:'(?:[^'\\]|\\[\S\s])*(?:'|$)|"(?:[^"\\]|\\[\S\s])*(?:"|$)|`(?:[^\\`]|\\[\S\s])*(?:`|$))/,
q,"'\"`"]):m.push(["str",/^(?:'(?:[^\n\r'\\]|\\.)*(?:'|$)|"(?:[^\n\r"\\]|\\.)*(?:"|$))/,q,"\"'"]);a.verbatimStrings&&e.push(["str",/^@"(?:[^"]|"")*(?:"|$)/,q]);var h=a.hashComments;h&&(a.cStyleComments?(h>1?m.push(["com",/^#(?:##(?:[^#]|#(?!##))*(?:###|$)|.*)/,q,"#"]):m.push(["com",/^#(?:(?:define|elif|else|endif|error|ifdef|include|ifndef|line|pragma|undef|warning)\b|[^\n\r]*)/,q,"#"]),e.push(["str",/^<(?:(?:(?:\.\.\/)*|\/?)(?:[\w-]+(?:\/[\w-]+)+)?[\w-]+\.h|[a-z]\w*)>/,q])):m.push(["com",/^#[^\n\r]*/,
q,"#"]));a.cStyleComments&&(e.push(["com",/^\/\/[^\n\r]*/,q]),e.push(["com",/^\/\*[\S\s]*?(?:\*\/|$)/,q]));a.regexLiterals&&e.push(["lang-regex",/^(?:^^\.?|[!+-]|!=|!==|#|%|%=|&|&&|&&=|&=|\(|\*|\*=|\+=|,|-=|->|\/|\/=|:|::|;|<|<<|<<=|<=|=|==|===|>|>=|>>|>>=|>>>|>>>=|[?@[^]|\^=|\^\^|\^\^=|{|\||\|=|\|\||\|\|=|~|break|case|continue|delete|do|else|finally|instanceof|return|throw|try|typeof)\s*(\/(?=[^*/])(?:[^/[\\]|\\[\S\s]|\[(?:[^\\\]]|\\[\S\s])*(?:]|$))+\/)/]);(h=a.types)&&e.push(["typ",h]);a=(""+a.keywords).replace(/^ | $/g,
"");a.length&&e.push(["kwd",RegExp("^(?:"+a.replace(/[\s,]+/g,"|")+")\\b"),q]);m.push(["pln",/^\s+/,q," \r\n\t\xa0"]);e.push(["lit",/^@[$_a-z][\w$@]*/i,q],["typ",/^(?:[@_]?[A-Z]+[a-z][\w$@]*|\w+_t\b)/,q],["pln",/^[$_a-z][\w$@]*/i,q],["lit",/^(?:0x[\da-f]+|(?:\d(?:_\d+)*\d*(?:\.\d*)?|\.\d\+)(?:e[+-]?\d+)?)[a-z]*/i,q,"0123456789"],["pln",/^\\[\S\s]?/,q],["pun",/^.[^\s\w"-$'./@\\`]*/,q]);return x(m,e)}function D(a,m){function e(a){switch(a.nodeType){case 1:if(k.test(a.className))break;if("BR"===a.nodeName)h(a),
a.parentNode&&a.parentNode.removeChild(a);else for(a=a.firstChild;a;a=a.nextSibling)e(a);break;case 3:case 4:if(p){var b=a.nodeValue,d=b.match(t);if(d){var c=b.substring(0,d.index);a.nodeValue=c;(b=b.substring(d.index+d[0].length))&&a.parentNode.insertBefore(s.createTextNode(b),a.nextSibling);h(a);c||a.parentNode.removeChild(a)}}}}function h(a){function b(a,d){var e=d?a.cloneNode(!1):a,f=a.parentNode;if(f){var f=b(f,1),g=a.nextSibling;f.appendChild(e);for(var h=g;h;h=g)g=h.nextSibling,f.appendChild(h)}return e}
for(;!a.nextSibling;)if(a=a.parentNode,!a)return;for(var a=b(a.nextSibling,0),e;(e=a.parentNode)&&e.nodeType===1;)a=e;d.push(a)}var k=/(?:^|\s)nocode(?:\s|$)/,t=/\r\n?|\n/,s=a.ownerDocument,l;a.currentStyle?l=a.currentStyle.whiteSpace:window.getComputedStyle&&(l=s.defaultView.getComputedStyle(a,q).getPropertyValue("white-space"));var p=l&&"pre"===l.substring(0,3);for(l=s.createElement("LI");a.firstChild;)l.appendChild(a.firstChild);for(var d=[l],g=0;g<d.length;++g)e(d[g]);m===(m|0)&&d[0].setAttribute("value",
m);var r=s.createElement("OL");r.className="linenums";for(var n=Math.max(0,m-1|0)||0,g=0,z=d.length;g<z;++g)l=d[g],l.className="L"+(g+n)%10,l.firstChild||l.appendChild(s.createTextNode("\xa0")),r.appendChild(l);a.appendChild(r)}function k(a,m){for(var e=m.length;--e>=0;){var h=m[e];A.hasOwnProperty(h)?window.console&&console.warn("cannot override language handler %s",h):A[h]=a}}function C(a,m){if(!a||!A.hasOwnProperty(a))a=/^\s*</.test(m)?"default-markup":"default-code";return A[a]}function E(a){var m=
a.g;try{var e=M(a.h),h=e.a;a.a=h;a.c=e.c;a.d=0;C(m,h)(a);var k=/\bMSIE\b/.test(navigator.userAgent),m=/\n/g,t=a.a,s=t.length,e=0,l=a.c,p=l.length,h=0,d=a.e,g=d.length,a=0;d[g]=s;var r,n;for(n=r=0;n<g;)d[n]!==d[n+2]?(d[r++]=d[n++],d[r++]=d[n++]):n+=2;g=r;for(n=r=0;n<g;){for(var z=d[n],f=d[n+1],b=n+2;b+2<=g&&d[b+1]===f;)b+=2;d[r++]=z;d[r++]=f;n=b}for(d.length=r;h<p;){var o=l[h+2]||s,c=d[a+2]||s,b=Math.min(o,c),i=l[h+1],j;if(i.nodeType!==1&&(j=t.substring(e,b))){k&&(j=j.replace(m,"\r"));i.nodeValue=
j;var u=i.ownerDocument,v=u.createElement("SPAN");v.className=d[a+1];var x=i.parentNode;x.replaceChild(v,i);v.appendChild(i);e<o&&(l[h+1]=i=u.createTextNode(t.substring(b,o)),x.insertBefore(i,v.nextSibling))}e=b;e>=o&&(h+=2);e>=c&&(a+=2)}}catch(w){"console"in window&&console.log(w&&w.stack?w.stack:w)}}var v=["break,continue,do,else,for,if,return,while"],w=[[v,"auto,case,char,const,default,double,enum,extern,float,goto,int,long,register,short,signed,sizeof,static,struct,switch,typedef,union,unsigned,void,volatile"],
"catch,class,delete,false,import,new,operator,private,protected,public,this,throw,true,try,typeof"],F=[w,"alignof,align_union,asm,axiom,bool,concept,concept_map,const_cast,constexpr,decltype,dynamic_cast,explicit,export,friend,inline,late_check,mutable,namespace,nullptr,reinterpret_cast,static_assert,static_cast,template,typeid,typename,using,virtual,where"],G=[w,"abstract,boolean,byte,extends,final,finally,implements,import,instanceof,null,native,package,strictfp,super,synchronized,throws,transient"],
H=[G,"as,base,by,checked,decimal,delegate,descending,dynamic,event,fixed,foreach,from,group,implicit,in,interface,internal,into,is,lock,object,out,override,orderby,params,partial,readonly,ref,sbyte,sealed,stackalloc,string,select,uint,ulong,unchecked,unsafe,ushort,var"],w=[w,"debugger,eval,export,function,get,null,set,undefined,var,with,Infinity,NaN"],I=[v,"and,as,assert,class,def,del,elif,except,exec,finally,from,global,import,in,is,lambda,nonlocal,not,or,pass,print,raise,try,with,yield,False,True,None"],
J=[v,"alias,and,begin,case,class,def,defined,elsif,end,ensure,false,in,module,next,nil,not,or,redo,rescue,retry,self,super,then,true,undef,unless,until,when,yield,BEGIN,END"],v=[v,"case,done,elif,esac,eval,fi,function,in,local,set,then,until"],K=/^(DIR|FILE|vector|(de|priority_)?queue|list|stack|(const_)?iterator|(multi)?(set|map)|bitset|u?(int|float)\d*)/,N=/\S/,O=u({keywords:[F,H,w,"caller,delete,die,do,dump,elsif,eval,exit,foreach,for,goto,if,import,last,local,my,next,no,our,print,package,redo,require,sub,undef,unless,until,use,wantarray,while,BEGIN,END"+
I,J,v],hashComments:!0,cStyleComments:!0,multiLineStrings:!0,regexLiterals:!0}),A={};k(O,["default-code"]);k(x([],[["pln",/^[^<?]+/],["dec",/^<!\w[^>]*(?:>|$)/],["com",/^<\!--[\S\s]*?(?:--\>|$)/],["lang-",/^<\?([\S\s]+?)(?:\?>|$)/],["lang-",/^<%([\S\s]+?)(?:%>|$)/],["pun",/^(?:<[%?]|[%?]>)/],["lang-",/^<xmp\b[^>]*>([\S\s]+?)<\/xmp\b[^>]*>/i],["lang-js",/^<script\b[^>]*>([\S\s]*?)(<\/script\b[^>]*>)/i],["lang-css",/^<style\b[^>]*>([\S\s]*?)(<\/style\b[^>]*>)/i],["lang-in.tag",/^(<\/?[a-z][^<>]*>)/i]]),
["default-markup","htm","html","mxml","xhtml","xml","xsl"]);k(x([["pln",/^\s+/,q," \t\r\n"],["atv",/^(?:"[^"]*"?|'[^']*'?)/,q,"\"'"]],[["tag",/^^<\/?[a-z](?:[\w-.:]*\w)?|\/?>$/i],["atn",/^(?!style[\s=]|on)[a-z](?:[\w:-]*\w)?/i],["lang-uq.val",/^=\s*([^\s"'>]*(?:[^\s"'/>]|\/(?=\s)))/],["pun",/^[/<->]+/],["lang-js",/^on\w+\s*=\s*"([^"]+)"/i],["lang-js",/^on\w+\s*=\s*'([^']+)'/i],["lang-js",/^on\w+\s*=\s*([^\s"'>]+)/i],["lang-css",/^style\s*=\s*"([^"]+)"/i],["lang-css",/^style\s*=\s*'([^']+)'/i],["lang-css",
/^style\s*=\s*([^\s"'>]+)/i]]),["in.tag"]);k(x([],[["atv",/^[\S\s]+/]]),["uq.val"]);k(u({keywords:F,hashComments:!0,cStyleComments:!0,types:K}),["c","cc","cpp","cxx","cyc","m"]);k(u({keywords:"null,true,false"}),["json"]);k(u({keywords:H,hashComments:!0,cStyleComments:!0,verbatimStrings:!0,types:K}),["cs"]);k(u({keywords:G,cStyleComments:!0}),["java"]);k(u({keywords:v,hashComments:!0,multiLineStrings:!0}),["bsh","csh","sh"]);k(u({keywords:I,hashComments:!0,multiLineStrings:!0,tripleQuotedStrings:!0}),
["cv","py"]);k(u({keywords:"caller,delete,die,do,dump,elsif,eval,exit,foreach,for,goto,if,import,last,local,my,next,no,our,print,package,redo,require,sub,undef,unless,until,use,wantarray,while,BEGIN,END",hashComments:!0,multiLineStrings:!0,regexLiterals:!0}),["perl","pl","pm"]);k(u({keywords:J,hashComments:!0,multiLineStrings:!0,regexLiterals:!0}),["rb"]);k(u({keywords:w,cStyleComments:!0,regexLiterals:!0}),["js"]);k(u({keywords:"all,and,by,catch,class,else,extends,false,finally,for,if,in,is,isnt,loop,new,no,not,null,of,off,on,or,return,super,then,true,try,unless,until,when,while,yes",
hashComments:3,cStyleComments:!0,multilineStrings:!0,tripleQuotedStrings:!0,regexLiterals:!0}),["coffee"]);k(x([],[["str",/^[\S\s]+/]]),["regex"]);window.prettyPrintOne=function(a,m,e){var h=document.createElement("PRE");h.innerHTML=a;e&&D(h,e);E({g:m,i:e,h:h});return h.innerHTML};window.prettyPrint=function(a){function m(){for(var e=window.PR_SHOULD_USE_CONTINUATION?l.now()+250:Infinity;p<h.length&&l.now()<e;p++){var n=h[p],k=n.className;if(k.indexOf("prettyprint")>=0){var k=k.match(g),f,b;if(b=
!k){b=n;for(var o=void 0,c=b.firstChild;c;c=c.nextSibling)var i=c.nodeType,o=i===1?o?b:c:i===3?N.test(c.nodeValue)?b:o:o;b=(f=o===b?void 0:o)&&"CODE"===f.tagName}b&&(k=f.className.match(g));k&&(k=k[1]);b=!1;for(o=n.parentNode;o;o=o.parentNode)if((o.tagName==="pre"||o.tagName==="code"||o.tagName==="xmp")&&o.className&&o.className.indexOf("prettyprint")>=0){b=!0;break}b||((b=(b=n.className.match(/\blinenums\b(?::(\d+))?/))?b[1]&&b[1].length?+b[1]:!0:!1)&&D(n,b),d={g:k,h:n,i:b},E(d))}}p<h.length?setTimeout(m,
250):a&&a()}for(var e=[document.getElementsByTagName("pre"),document.getElementsByTagName("code"),document.getElementsByTagName("xmp")],h=[],k=0;k<e.length;++k)for(var t=0,s=e[k].length;t<s;++t)h.push(e[k][t]);var e=q,l=Date;l.now||(l={now:function(){return+new Date}});var p=0,d,g=/\blang(?:uage)?-([\w.]+)(?!\S)/;m()};window.PR={createSimpleLexer:x,registerLangHandler:k,sourceDecorator:u,PR_ATTRIB_NAME:"atn",PR_ATTRIB_VALUE:"atv",PR_COMMENT:"com",PR_DECLARATION:"dec",PR_KEYWORD:"kwd",PR_LITERAL:"lit",
PR_NOCODE:"nocode",PR_PLAIN:"pln",PR_PUNCTUATION:"pun",PR_SOURCE:"src",PR_STRING:"str",PR_TAG:"tag",PR_TYPE:"typ"}})();

View File

@ -0,0 +1,22 @@
/*
This CSS file contains some tweaks specific to the included Bootstrap theme.
It's separate from `style.css` so that it can be easily overridden by replacing
a single block in the template.
*/
.form-actions {
background: transparent;
border-top-color: transparent;
padding-top: 0;
}
.navbar-inverse .brand a {
color: #999;
}
.navbar-inverse .brand:hover a {
color: white;
text-decoration: none;
}

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,30 @@
.com { color: #93a1a1; }
.lit { color: #195f91; }
.pun, .opn, .clo { color: #93a1a1; }
.fun { color: #dc322f; }
.str, .atv { color: #D14; }
.kwd, .prettyprint .tag { color: #1e347b; }
.typ, .atn, .dec, .var { color: teal; }
.pln { color: #48484c; }
.prettyprint {
padding: 8px;
background-color: #f7f7f9;
border: 1px solid #e1e1e8;
}
.prettyprint.linenums {
-webkit-box-shadow: inset 40px 0 0 #fbfbfc, inset 41px 0 0 #ececf0;
-moz-box-shadow: inset 40px 0 0 #fbfbfc, inset 41px 0 0 #ececf0;
box-shadow: inset 40px 0 0 #fbfbfc, inset 41px 0 0 #ececf0;
}
/* Specify class=linenums on a pre to get line numbering */
ol.linenums {
margin: 0 0 0 33px; /* IE indents via margin-left */
}
ol.linenums li {
padding-left: 12px;
color: #bebec5;
line-height: 20px;
text-shadow: 0 1px 0 #fff;
}

View File

@ -0,0 +1,74 @@
body {
padding-top: 0;
padding-bottom: 1em;
}
/* The navbar is fixed at >= 980px wide, so add padding to the body to prevent
content running up underneath it. */
@media (min-width:980px) {
body {
padding-top: 60px;
}
}
h1 {
font-weight: 500;
}
h2, h3 {
font-weight: 300;
}
.resource-description, .response-info {
margin-bottom: 2em;
}
#footer {
border-top: 1px solid #eee;
margin-top: 2em;
padding-top: 1em;
text-align: right;
}
.version:before {
content: "v";
opacity: 0.6;
padding-right: 0.25em;
}
.format-option {
font-family: Menlo, Consolas, "Andale Mono", "Lucida Console", monospace;
}
#options-form {
position: relative;
}
/* To allow tooltips to work on disabled elements */
.disabled-tooltip-shield {
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
}
#options-form {
margin-right: 1em;
}
.errorlist {
margin-top: 0.5em;
}
pre {
overflow: auto;
word-wrap: normal;
white-space: pre;
font-size: 12px;
}
.page-header {
border-bottom: none;
padding-bottom: 0px;
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,28 @@
var q=null;window.PR_SHOULD_USE_CONTINUATION=!0;
(function(){function L(a){function m(a){var f=a.charCodeAt(0);if(f!==92)return f;var b=a.charAt(1);return(f=r[b])?f:"0"<=b&&b<="7"?parseInt(a.substring(1),8):b==="u"||b==="x"?parseInt(a.substring(2),16):a.charCodeAt(1)}function e(a){if(a<32)return(a<16?"\\x0":"\\x")+a.toString(16);a=String.fromCharCode(a);if(a==="\\"||a==="-"||a==="["||a==="]")a="\\"+a;return a}function h(a){for(var f=a.substring(1,a.length-1).match(/\\u[\dA-Fa-f]{4}|\\x[\dA-Fa-f]{2}|\\[0-3][0-7]{0,2}|\\[0-7]{1,2}|\\[\S\s]|[^\\]/g),a=
[],b=[],o=f[0]==="^",c=o?1:0,i=f.length;c<i;++c){var j=f[c];if(/\\[bdsw]/i.test(j))a.push(j);else{var j=m(j),d;c+2<i&&"-"===f[c+1]?(d=m(f[c+2]),c+=2):d=j;b.push([j,d]);d<65||j>122||(d<65||j>90||b.push([Math.max(65,j)|32,Math.min(d,90)|32]),d<97||j>122||b.push([Math.max(97,j)&-33,Math.min(d,122)&-33]))}}b.sort(function(a,f){return a[0]-f[0]||f[1]-a[1]});f=[];j=[NaN,NaN];for(c=0;c<b.length;++c)i=b[c],i[0]<=j[1]+1?j[1]=Math.max(j[1],i[1]):f.push(j=i);b=["["];o&&b.push("^");b.push.apply(b,a);for(c=0;c<
f.length;++c)i=f[c],b.push(e(i[0])),i[1]>i[0]&&(i[1]+1>i[0]&&b.push("-"),b.push(e(i[1])));b.push("]");return b.join("")}function y(a){for(var f=a.source.match(/\[(?:[^\\\]]|\\[\S\s])*]|\\u[\dA-Fa-f]{4}|\\x[\dA-Fa-f]{2}|\\\d+|\\[^\dux]|\(\?[!:=]|[()^]|[^()[\\^]+/g),b=f.length,d=[],c=0,i=0;c<b;++c){var j=f[c];j==="("?++i:"\\"===j.charAt(0)&&(j=+j.substring(1))&&j<=i&&(d[j]=-1)}for(c=1;c<d.length;++c)-1===d[c]&&(d[c]=++t);for(i=c=0;c<b;++c)j=f[c],j==="("?(++i,d[i]===void 0&&(f[c]="(?:")):"\\"===j.charAt(0)&&
(j=+j.substring(1))&&j<=i&&(f[c]="\\"+d[i]);for(i=c=0;c<b;++c)"^"===f[c]&&"^"!==f[c+1]&&(f[c]="");if(a.ignoreCase&&s)for(c=0;c<b;++c)j=f[c],a=j.charAt(0),j.length>=2&&a==="["?f[c]=h(j):a!=="\\"&&(f[c]=j.replace(/[A-Za-z]/g,function(a){a=a.charCodeAt(0);return"["+String.fromCharCode(a&-33,a|32)+"]"}));return f.join("")}for(var t=0,s=!1,l=!1,p=0,d=a.length;p<d;++p){var g=a[p];if(g.ignoreCase)l=!0;else if(/[a-z]/i.test(g.source.replace(/\\u[\da-f]{4}|\\x[\da-f]{2}|\\[^UXux]/gi,""))){s=!0;l=!1;break}}for(var r=
{b:8,t:9,n:10,v:11,f:12,r:13},n=[],p=0,d=a.length;p<d;++p){g=a[p];if(g.global||g.multiline)throw Error(""+g);n.push("(?:"+y(g)+")")}return RegExp(n.join("|"),l?"gi":"g")}function M(a){function m(a){switch(a.nodeType){case 1:if(e.test(a.className))break;for(var g=a.firstChild;g;g=g.nextSibling)m(g);g=a.nodeName;if("BR"===g||"LI"===g)h[s]="\n",t[s<<1]=y++,t[s++<<1|1]=a;break;case 3:case 4:g=a.nodeValue,g.length&&(g=p?g.replace(/\r\n?/g,"\n"):g.replace(/[\t\n\r ]+/g," "),h[s]=g,t[s<<1]=y,y+=g.length,
t[s++<<1|1]=a)}}var e=/(?:^|\s)nocode(?:\s|$)/,h=[],y=0,t=[],s=0,l;a.currentStyle?l=a.currentStyle.whiteSpace:window.getComputedStyle&&(l=document.defaultView.getComputedStyle(a,q).getPropertyValue("white-space"));var p=l&&"pre"===l.substring(0,3);m(a);return{a:h.join("").replace(/\n$/,""),c:t}}function B(a,m,e,h){m&&(a={a:m,d:a},e(a),h.push.apply(h,a.e))}function x(a,m){function e(a){for(var l=a.d,p=[l,"pln"],d=0,g=a.a.match(y)||[],r={},n=0,z=g.length;n<z;++n){var f=g[n],b=r[f],o=void 0,c;if(typeof b===
"string")c=!1;else{var i=h[f.charAt(0)];if(i)o=f.match(i[1]),b=i[0];else{for(c=0;c<t;++c)if(i=m[c],o=f.match(i[1])){b=i[0];break}o||(b="pln")}if((c=b.length>=5&&"lang-"===b.substring(0,5))&&!(o&&typeof o[1]==="string"))c=!1,b="src";c||(r[f]=b)}i=d;d+=f.length;if(c){c=o[1];var j=f.indexOf(c),k=j+c.length;o[2]&&(k=f.length-o[2].length,j=k-c.length);b=b.substring(5);B(l+i,f.substring(0,j),e,p);B(l+i+j,c,C(b,c),p);B(l+i+k,f.substring(k),e,p)}else p.push(l+i,b)}a.e=p}var h={},y;(function(){for(var e=a.concat(m),
l=[],p={},d=0,g=e.length;d<g;++d){var r=e[d],n=r[3];if(n)for(var k=n.length;--k>=0;)h[n.charAt(k)]=r;r=r[1];n=""+r;p.hasOwnProperty(n)||(l.push(r),p[n]=q)}l.push(/[\S\s]/);y=L(l)})();var t=m.length;return e}function u(a){var m=[],e=[];a.tripleQuotedStrings?m.push(["str",/^(?:'''(?:[^'\\]|\\[\S\s]|''?(?=[^']))*(?:'''|$)|"""(?:[^"\\]|\\[\S\s]|""?(?=[^"]))*(?:"""|$)|'(?:[^'\\]|\\[\S\s])*(?:'|$)|"(?:[^"\\]|\\[\S\s])*(?:"|$))/,q,"'\""]):a.multiLineStrings?m.push(["str",/^(?:'(?:[^'\\]|\\[\S\s])*(?:'|$)|"(?:[^"\\]|\\[\S\s])*(?:"|$)|`(?:[^\\`]|\\[\S\s])*(?:`|$))/,
q,"'\"`"]):m.push(["str",/^(?:'(?:[^\n\r'\\]|\\.)*(?:'|$)|"(?:[^\n\r"\\]|\\.)*(?:"|$))/,q,"\"'"]);a.verbatimStrings&&e.push(["str",/^@"(?:[^"]|"")*(?:"|$)/,q]);var h=a.hashComments;h&&(a.cStyleComments?(h>1?m.push(["com",/^#(?:##(?:[^#]|#(?!##))*(?:###|$)|.*)/,q,"#"]):m.push(["com",/^#(?:(?:define|elif|else|endif|error|ifdef|include|ifndef|line|pragma|undef|warning)\b|[^\n\r]*)/,q,"#"]),e.push(["str",/^<(?:(?:(?:\.\.\/)*|\/?)(?:[\w-]+(?:\/[\w-]+)+)?[\w-]+\.h|[a-z]\w*)>/,q])):m.push(["com",/^#[^\n\r]*/,
q,"#"]));a.cStyleComments&&(e.push(["com",/^\/\/[^\n\r]*/,q]),e.push(["com",/^\/\*[\S\s]*?(?:\*\/|$)/,q]));a.regexLiterals&&e.push(["lang-regex",/^(?:^^\.?|[!+-]|!=|!==|#|%|%=|&|&&|&&=|&=|\(|\*|\*=|\+=|,|-=|->|\/|\/=|:|::|;|<|<<|<<=|<=|=|==|===|>|>=|>>|>>=|>>>|>>>=|[?@[^]|\^=|\^\^|\^\^=|{|\||\|=|\|\||\|\|=|~|break|case|continue|delete|do|else|finally|instanceof|return|throw|try|typeof)\s*(\/(?=[^*/])(?:[^/[\\]|\\[\S\s]|\[(?:[^\\\]]|\\[\S\s])*(?:]|$))+\/)/]);(h=a.types)&&e.push(["typ",h]);a=(""+a.keywords).replace(/^ | $/g,
"");a.length&&e.push(["kwd",RegExp("^(?:"+a.replace(/[\s,]+/g,"|")+")\\b"),q]);m.push(["pln",/^\s+/,q," \r\n\t\xa0"]);e.push(["lit",/^@[$_a-z][\w$@]*/i,q],["typ",/^(?:[@_]?[A-Z]+[a-z][\w$@]*|\w+_t\b)/,q],["pln",/^[$_a-z][\w$@]*/i,q],["lit",/^(?:0x[\da-f]+|(?:\d(?:_\d+)*\d*(?:\.\d*)?|\.\d\+)(?:e[+-]?\d+)?)[a-z]*/i,q,"0123456789"],["pln",/^\\[\S\s]?/,q],["pun",/^.[^\s\w"-$'./@\\`]*/,q]);return x(m,e)}function D(a,m){function e(a){switch(a.nodeType){case 1:if(k.test(a.className))break;if("BR"===a.nodeName)h(a),
a.parentNode&&a.parentNode.removeChild(a);else for(a=a.firstChild;a;a=a.nextSibling)e(a);break;case 3:case 4:if(p){var b=a.nodeValue,d=b.match(t);if(d){var c=b.substring(0,d.index);a.nodeValue=c;(b=b.substring(d.index+d[0].length))&&a.parentNode.insertBefore(s.createTextNode(b),a.nextSibling);h(a);c||a.parentNode.removeChild(a)}}}}function h(a){function b(a,d){var e=d?a.cloneNode(!1):a,f=a.parentNode;if(f){var f=b(f,1),g=a.nextSibling;f.appendChild(e);for(var h=g;h;h=g)g=h.nextSibling,f.appendChild(h)}return e}
for(;!a.nextSibling;)if(a=a.parentNode,!a)return;for(var a=b(a.nextSibling,0),e;(e=a.parentNode)&&e.nodeType===1;)a=e;d.push(a)}var k=/(?:^|\s)nocode(?:\s|$)/,t=/\r\n?|\n/,s=a.ownerDocument,l;a.currentStyle?l=a.currentStyle.whiteSpace:window.getComputedStyle&&(l=s.defaultView.getComputedStyle(a,q).getPropertyValue("white-space"));var p=l&&"pre"===l.substring(0,3);for(l=s.createElement("LI");a.firstChild;)l.appendChild(a.firstChild);for(var d=[l],g=0;g<d.length;++g)e(d[g]);m===(m|0)&&d[0].setAttribute("value",
m);var r=s.createElement("OL");r.className="linenums";for(var n=Math.max(0,m-1|0)||0,g=0,z=d.length;g<z;++g)l=d[g],l.className="L"+(g+n)%10,l.firstChild||l.appendChild(s.createTextNode("\xa0")),r.appendChild(l);a.appendChild(r)}function k(a,m){for(var e=m.length;--e>=0;){var h=m[e];A.hasOwnProperty(h)?window.console&&console.warn("cannot override language handler %s",h):A[h]=a}}function C(a,m){if(!a||!A.hasOwnProperty(a))a=/^\s*</.test(m)?"default-markup":"default-code";return A[a]}function E(a){var m=
a.g;try{var e=M(a.h),h=e.a;a.a=h;a.c=e.c;a.d=0;C(m,h)(a);var k=/\bMSIE\b/.test(navigator.userAgent),m=/\n/g,t=a.a,s=t.length,e=0,l=a.c,p=l.length,h=0,d=a.e,g=d.length,a=0;d[g]=s;var r,n;for(n=r=0;n<g;)d[n]!==d[n+2]?(d[r++]=d[n++],d[r++]=d[n++]):n+=2;g=r;for(n=r=0;n<g;){for(var z=d[n],f=d[n+1],b=n+2;b+2<=g&&d[b+1]===f;)b+=2;d[r++]=z;d[r++]=f;n=b}for(d.length=r;h<p;){var o=l[h+2]||s,c=d[a+2]||s,b=Math.min(o,c),i=l[h+1],j;if(i.nodeType!==1&&(j=t.substring(e,b))){k&&(j=j.replace(m,"\r"));i.nodeValue=
j;var u=i.ownerDocument,v=u.createElement("SPAN");v.className=d[a+1];var x=i.parentNode;x.replaceChild(v,i);v.appendChild(i);e<o&&(l[h+1]=i=u.createTextNode(t.substring(b,o)),x.insertBefore(i,v.nextSibling))}e=b;e>=o&&(h+=2);e>=c&&(a+=2)}}catch(w){"console"in window&&console.log(w&&w.stack?w.stack:w)}}var v=["break,continue,do,else,for,if,return,while"],w=[[v,"auto,case,char,const,default,double,enum,extern,float,goto,int,long,register,short,signed,sizeof,static,struct,switch,typedef,union,unsigned,void,volatile"],
"catch,class,delete,false,import,new,operator,private,protected,public,this,throw,true,try,typeof"],F=[w,"alignof,align_union,asm,axiom,bool,concept,concept_map,const_cast,constexpr,decltype,dynamic_cast,explicit,export,friend,inline,late_check,mutable,namespace,nullptr,reinterpret_cast,static_assert,static_cast,template,typeid,typename,using,virtual,where"],G=[w,"abstract,boolean,byte,extends,final,finally,implements,import,instanceof,null,native,package,strictfp,super,synchronized,throws,transient"],
H=[G,"as,base,by,checked,decimal,delegate,descending,dynamic,event,fixed,foreach,from,group,implicit,in,interface,internal,into,is,lock,object,out,override,orderby,params,partial,readonly,ref,sbyte,sealed,stackalloc,string,select,uint,ulong,unchecked,unsafe,ushort,var"],w=[w,"debugger,eval,export,function,get,null,set,undefined,var,with,Infinity,NaN"],I=[v,"and,as,assert,class,def,del,elif,except,exec,finally,from,global,import,in,is,lambda,nonlocal,not,or,pass,print,raise,try,with,yield,False,True,None"],
J=[v,"alias,and,begin,case,class,def,defined,elsif,end,ensure,false,in,module,next,nil,not,or,redo,rescue,retry,self,super,then,true,undef,unless,until,when,yield,BEGIN,END"],v=[v,"case,done,elif,esac,eval,fi,function,in,local,set,then,until"],K=/^(DIR|FILE|vector|(de|priority_)?queue|list|stack|(const_)?iterator|(multi)?(set|map)|bitset|u?(int|float)\d*)/,N=/\S/,O=u({keywords:[F,H,w,"caller,delete,die,do,dump,elsif,eval,exit,foreach,for,goto,if,import,last,local,my,next,no,our,print,package,redo,require,sub,undef,unless,until,use,wantarray,while,BEGIN,END"+
I,J,v],hashComments:!0,cStyleComments:!0,multiLineStrings:!0,regexLiterals:!0}),A={};k(O,["default-code"]);k(x([],[["pln",/^[^<?]+/],["dec",/^<!\w[^>]*(?:>|$)/],["com",/^<\!--[\S\s]*?(?:--\>|$)/],["lang-",/^<\?([\S\s]+?)(?:\?>|$)/],["lang-",/^<%([\S\s]+?)(?:%>|$)/],["pun",/^(?:<[%?]|[%?]>)/],["lang-",/^<xmp\b[^>]*>([\S\s]+?)<\/xmp\b[^>]*>/i],["lang-js",/^<script\b[^>]*>([\S\s]*?)(<\/script\b[^>]*>)/i],["lang-css",/^<style\b[^>]*>([\S\s]*?)(<\/style\b[^>]*>)/i],["lang-in.tag",/^(<\/?[a-z][^<>]*>)/i]]),
["default-markup","htm","html","mxml","xhtml","xml","xsl"]);k(x([["pln",/^\s+/,q," \t\r\n"],["atv",/^(?:"[^"]*"?|'[^']*'?)/,q,"\"'"]],[["tag",/^^<\/?[a-z](?:[\w-.:]*\w)?|\/?>$/i],["atn",/^(?!style[\s=]|on)[a-z](?:[\w:-]*\w)?/i],["lang-uq.val",/^=\s*([^\s"'>]*(?:[^\s"'/>]|\/(?=\s)))/],["pun",/^[/<->]+/],["lang-js",/^on\w+\s*=\s*"([^"]+)"/i],["lang-js",/^on\w+\s*=\s*'([^']+)'/i],["lang-js",/^on\w+\s*=\s*([^\s"'>]+)/i],["lang-css",/^style\s*=\s*"([^"]+)"/i],["lang-css",/^style\s*=\s*'([^']+)'/i],["lang-css",
/^style\s*=\s*([^\s"'>]+)/i]]),["in.tag"]);k(x([],[["atv",/^[\S\s]+/]]),["uq.val"]);k(u({keywords:F,hashComments:!0,cStyleComments:!0,types:K}),["c","cc","cpp","cxx","cyc","m"]);k(u({keywords:"null,true,false"}),["json"]);k(u({keywords:H,hashComments:!0,cStyleComments:!0,verbatimStrings:!0,types:K}),["cs"]);k(u({keywords:G,cStyleComments:!0}),["java"]);k(u({keywords:v,hashComments:!0,multiLineStrings:!0}),["bsh","csh","sh"]);k(u({keywords:I,hashComments:!0,multiLineStrings:!0,tripleQuotedStrings:!0}),
["cv","py"]);k(u({keywords:"caller,delete,die,do,dump,elsif,eval,exit,foreach,for,goto,if,import,last,local,my,next,no,our,print,package,redo,require,sub,undef,unless,until,use,wantarray,while,BEGIN,END",hashComments:!0,multiLineStrings:!0,regexLiterals:!0}),["perl","pl","pm"]);k(u({keywords:J,hashComments:!0,multiLineStrings:!0,regexLiterals:!0}),["rb"]);k(u({keywords:w,cStyleComments:!0,regexLiterals:!0}),["js"]);k(u({keywords:"all,and,by,catch,class,else,extends,false,finally,for,if,in,is,isnt,loop,new,no,not,null,of,off,on,or,return,super,then,true,try,unless,until,when,while,yes",
hashComments:3,cStyleComments:!0,multilineStrings:!0,tripleQuotedStrings:!0,regexLiterals:!0}),["coffee"]);k(x([],[["str",/^[\S\s]+/]]),["regex"]);window.prettyPrintOne=function(a,m,e){var h=document.createElement("PRE");h.innerHTML=a;e&&D(h,e);E({g:m,i:e,h:h});return h.innerHTML};window.prettyPrint=function(a){function m(){for(var e=window.PR_SHOULD_USE_CONTINUATION?l.now()+250:Infinity;p<h.length&&l.now()<e;p++){var n=h[p],k=n.className;if(k.indexOf("prettyprint")>=0){var k=k.match(g),f,b;if(b=
!k){b=n;for(var o=void 0,c=b.firstChild;c;c=c.nextSibling)var i=c.nodeType,o=i===1?o?b:c:i===3?N.test(c.nodeValue)?b:o:o;b=(f=o===b?void 0:o)&&"CODE"===f.tagName}b&&(k=f.className.match(g));k&&(k=k[1]);b=!1;for(o=n.parentNode;o;o=o.parentNode)if((o.tagName==="pre"||o.tagName==="code"||o.tagName==="xmp")&&o.className&&o.className.indexOf("prettyprint")>=0){b=!0;break}b||((b=(b=n.className.match(/\blinenums\b(?::(\d+))?/))?b[1]&&b[1].length?+b[1]:!0:!1)&&D(n,b),d={g:k,h:n,i:b},E(d))}}p<h.length?setTimeout(m,
250):a&&a()}for(var e=[document.getElementsByTagName("pre"),document.getElementsByTagName("code"),document.getElementsByTagName("xmp")],h=[],k=0;k<e.length;++k)for(var t=0,s=e[k].length;t<s;++t)h.push(e[k][t]);var e=q,l=Date;l.now||(l={now:function(){return+new Date}});var p=0,d,g=/\blang(?:uage)?-([\w.]+)(?!\S)/;m()};window.PR={createSimpleLexer:x,registerLangHandler:k,sourceDecorator:u,PR_ATTRIB_NAME:"atn",PR_ATTRIB_VALUE:"atv",PR_COMMENT:"com",PR_DECLARATION:"dec",PR_KEYWORD:"kwd",PR_LITERAL:"lit",
PR_NOCODE:"nocode",PR_PLAIN:"pln",PR_PUNCTUATION:"pun",PR_SOURCE:"src",PR_STRING:"str",PR_TAG:"tag",PR_TYPE:"typ"}})();

52
rest_framework/status.py Normal file
View File

@ -0,0 +1,52 @@
"""
Descriptive HTTP status codes, for code readability.
See RFC 2616 - http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html
And RFC 6585 - http://tools.ietf.org/html/rfc6585
"""
HTTP_100_CONTINUE = 100
HTTP_101_SWITCHING_PROTOCOLS = 101
HTTP_200_OK = 200
HTTP_201_CREATED = 201
HTTP_202_ACCEPTED = 202
HTTP_203_NON_AUTHORITATIVE_INFORMATION = 203
HTTP_204_NO_CONTENT = 204
HTTP_205_RESET_CONTENT = 205
HTTP_206_PARTIAL_CONTENT = 206
HTTP_300_MULTIPLE_CHOICES = 300
HTTP_301_MOVED_PERMANENTLY = 301
HTTP_302_FOUND = 302
HTTP_303_SEE_OTHER = 303
HTTP_304_NOT_MODIFIED = 304
HTTP_305_USE_PROXY = 305
HTTP_306_RESERVED = 306
HTTP_307_TEMPORARY_REDIRECT = 307
HTTP_400_BAD_REQUEST = 400
HTTP_401_UNAUTHORIZED = 401
HTTP_402_PAYMENT_REQUIRED = 402
HTTP_403_FORBIDDEN = 403
HTTP_404_NOT_FOUND = 404
HTTP_405_METHOD_NOT_ALLOWED = 405
HTTP_406_NOT_ACCEPTABLE = 406
HTTP_407_PROXY_AUTHENTICATION_REQUIRED = 407
HTTP_408_REQUEST_TIMEOUT = 408
HTTP_409_CONFLICT = 409
HTTP_410_GONE = 410
HTTP_411_LENGTH_REQUIRED = 411
HTTP_412_PRECONDITION_FAILED = 412
HTTP_413_REQUEST_ENTITY_TOO_LARGE = 413
HTTP_414_REQUEST_URI_TOO_LONG = 414
HTTP_415_UNSUPPORTED_MEDIA_TYPE = 415
HTTP_416_REQUESTED_RANGE_NOT_SATISFIABLE = 416
HTTP_417_EXPECTATION_FAILED = 417
HTTP_428_PRECONDITION_REQUIRED = 428
HTTP_429_TOO_MANY_REQUESTS = 429
HTTP_431_REQUEST_HEADER_FIELDS_TOO_LARGE = 431
HTTP_500_INTERNAL_SERVER_ERROR = 500
HTTP_501_NOT_IMPLEMENTED = 501
HTTP_502_BAD_GATEWAY = 502
HTTP_503_SERVICE_UNAVAILABLE = 503
HTTP_504_GATEWAY_TIMEOUT = 504
HTTP_505_HTTP_VERSION_NOT_SUPPORTED = 505
HTTP_511_NETWORD_AUTHENTICATION_REQUIRED = 511

View File

@ -0,0 +1,3 @@
{% extends "rest_framework/base.html" %}
{# Override this template in your own templates directory to customize #}

View File

@ -0,0 +1,8 @@
{% autoescape off %}{{ name }}
{{ description }}
HTTP {{ response.status_code }} {{ response.status_text }}
{% for key, val in response.headers.items %}{{ key }}: {{ val }}
{% endfor %}
{{ content }}{% endautoescape %}

View File

@ -0,0 +1,214 @@
{% load url from future %}
{% load urlize_quoted_links %}
{% load add_query_param %}
{% load add_class %}
{% load optional_login %}
{% load static %}
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
{% block bootstrap_theme %}
<link rel="stylesheet" type="text/css" href="{% get_static_prefix %}rest_framework/css/bootstrap.min.css"/>
<link rel="stylesheet" type="text/css" href="{% get_static_prefix %}rest_framework/css/bootstrap-tweaks.css"/>
{% endblock %}
<link rel="stylesheet" type="text/css" href='{% get_static_prefix %}rest_framework/css/prettify.css'/>
<link rel="stylesheet" type="text/css" href='{% get_static_prefix %}rest_framework/css/style.css'/>
{% block extrastyle %}{% endblock %}
<title>{% block title %}Django REST framework - {{ name }}{% endblock %}</title>
{% block extrahead %}{% endblock %}
{% block blockbots %}<meta name="robots" content="NONE,NOARCHIVE" />{% endblock %}
</head>
<body class="{% block bodyclass %}{% endblock %} container">
<div class="navbar navbar-fixed-top {% block bootstrap_navbar_variant %}navbar-inverse{% endblock %}">
<div class="navbar-inner">
<div class="container">
<span class="brand" href="/">
{% block branding %}<a href='http://django-rest-framework.org'>Django REST framework <span class="version">{{ version }}</span></a>{% endblock %}
</span>
<ul class="nav pull-right">
{% block userlinks %}
{% if user.is_active %}
<li class="dropdown">
<a href="#" class="dropdown-toggle" data-toggle="dropdown">
Welcome, {{ user }}
<b class="caret"></b>
</a>
<ul class="dropdown-menu">
<li>{% optional_logout %}</li>
</ul>
</li>
{% else %}
<li>{% optional_login %}</li>
{% endif %}
{% endblock %}
</ul>
</div>
</div>
</div>
{% block global_heading %}{% endblock %}
{% block breadcrumbs %}
<ul class="breadcrumb">
{% for breadcrumb_name, breadcrumb_url in breadcrumblist %}
<li>
<a href="{{ breadcrumb_url }}" {% if forloop.last %}class="active"{% endif %}>{{ breadcrumb_name }}</a> {% if not forloop.last %}<span class="divider">&rsaquo;</span>{% endif %}
</li>
{% endfor %}
</ul>
{% endblock %}
<!-- Content -->
<div id="content">
{% if 'GET' in allowed_methods %}
<form id="get-form" class="pull-right">
<fieldset>
<div class="btn-group format-selection">
<a class="btn btn-primary js-tooltip" href='{{ request.get_full_path }}' rel="nofollow" title="Do a GET request on the {{ name }} resource">GET</a>
<button class="btn btn-primary dropdown-toggle js-tooltip" data-toggle="dropdown" title="Specify a format for the GET request">
<span class="caret"></span>
</button>
<ul class="dropdown-menu">
{% for format in available_formats %}
{% with FORMAT_PARAM|add:"="|add:format as param %}
<li>
<a class="js-tooltip format-option" href='{{ request.get_full_path|add_query_param:param }}' rel="nofollow" title="Do a GET request on the {{ name }} resource with the format set to `{{ format }}`">{{ format }}</a>
</li>
{% endwith %}
{% endfor %}
</ul>
</div>
</fieldset>
</form>
{% endif %}
{% if api_settings.FORM_METHOD_OVERRIDE %}
<form id="options-form" action="{{ request.get_full_path }}" method="post" class="pull-right">
{% csrf_token %}
<input type="hidden" name="{{ api_settings.FORM_METHOD_OVERRIDE }}" value="OPTIONS" />
<button class="btn btn-info js-tooltip" {% if 'OPTIONS' in allowed_methods %} title="Do an OPTIONS request on the {{ name }} resource"{% else %} disabled{% endif %}>OPTIONS</button>
{% if not 'OPTIONS' in allowed_methods %}
<div class="js-tooltip disabled-tooltip-shield" title="OPTIONS request not allowed for resource {{ name }}"></div>
{% endif %}
</form>
{% endif %}
<div class="content-main">
<div class="page-header"><h1>{{ name }}</h1></div>
<p class="resource-description">{{ description }}</p>
<div class="request-info">
<pre class="prettyprint"><b>{{ request.method }}</b> {{ request.get_full_path }}</pre>
<div>
<div class="response-info">
<pre class="prettyprint"><div class="meta nocode"><b>HTTP {{ response.status_code }} {{ response.status_text }}</b>{% autoescape off %}
{% for key, val in response.items %}<b>{{ key }}:</b> <span class="lit">{{ val|urlize_quoted_links }}</span>
{% endfor %}
</div>{{ content|urlize_quoted_links }}</pre>{% endautoescape %}
</div>
{% if response.status_code != 403 %}
{% if 'POST' in allowed_methods %}
<form action="{{ request.get_full_path }}" method="POST" {% if post_form.is_multipart %}enctype="multipart/form-data"{% endif %} class="form-horizontal">
<fieldset>
<h2>POST: {{ name }}</h2>
{% csrf_token %}
{{ post_form.non_field_errors }}
{% for field in post_form %}
<div class="control-group {% if field.errors %}error{% endif %}">
{{ field.label_tag|add_class:"control-label" }}
<div class="controls">
{{ field }}
<span class="help-inline">{{ field.help_text }}</span>
{{ field.errors|add_class:"help-block" }}
</div>
</div>
{% endfor %}
<div class="form-actions">
<button class="btn btn-primary" title="Do a POST request on the {{ name }} resource">POST</button>
</div>
</fieldset>
</form>
{% endif %}
{% if 'PUT' in allowed_methods and api_settings.FORM_METHOD_OVERRIDE %}
<form action="{{ request.get_full_path }}" method="POST" {% if put_form.is_multipart %}enctype="multipart/form-data"{% endif %} class="form-horizontal">
<fieldset>
<h2>PUT: {{ name }}</h2>
<input type="hidden" name="{{ api_settings.FORM_METHOD_OVERRIDE }}" value="PUT" />
{% csrf_token %}
{{ put_form.non_field_errors }}
{% for field in put_form %}
<div class="control-group {% if field.errors %}error{% endif %}">
{{ field.label_tag|add_class:"control-label" }}
<div class="controls">
{{ field }}
<span class='help-inline'>{{ field.help_text }}</span>
{{ field.errors|add_class:"help-block" }}
</div>
</div>
{% endfor %}
<div class="form-actions">
<button class="btn btn-primary js-tooltip" title="Do a PUT request on the {{ name }} resource">PUT</button>
</div>
</fieldset>
</form>
{% endif %}
{% if 'DELETE' in allowed_methods and api_settings.FORM_METHOD_OVERRIDE %}
<form action="{{ request.get_full_path }}" method="POST" class="form-horizontal">
<fieldset>
<h2>DELETE: {{ name }}</h2>
{% csrf_token %}
<input type="hidden" name="{{ api_settings.FORM_METHOD_OVERRIDE }}" value="DELETE" />
<div class="form-actions">
<button class="btn btn-danger js-tooltip" title="Do a DELETE request on the {{ name }} resource">DELETE</button>
</div>
</fieldset>
</form>
{% endif %}
{% endif %}
</div>
<!-- END content-main -->
</div>
<!-- END Content -->
<div id="footer">
{% block footer %}
<a class="powered-by" href='http://django-rest-framework.org'>Django REST framework</a> <span class="version">{{ version }}</span>
{% endblock %}
</div>
</div>
<script src="{% get_static_prefix %}rest_framework/js/jquery-1.8.1-min.js"></script>
<script src="{% get_static_prefix %}rest_framework/js/bootstrap.min.js"></script>
<script src="{% get_static_prefix %}rest_framework/js/prettify-min.js"></script>
<script>
prettyPrint();
$('.js-tooltip').tooltip({
delay: 1000
});
</script>
{% block extrabody %}{% endblock %}
</body>
</html>

View File

@ -0,0 +1,45 @@
{% load url from future %}
{% load static %}
<html>
<head>
<link rel="stylesheet" type="text/css" href='{% get_static_prefix %}rest_framework/css/style.css'/>
</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 'rest_framework:login' %}" id="login-form">
{% csrf_token %}
<div class="form-row">
<label for="id_username">Username:</label> {{ form.username }}
</div>
<div class="form-row">
<label for="id_password">Password:</label> {{ form.password }}
<input type="hidden" name="next" value="{{ next }}" />
</div>
<div class="form-row">
<label>&nbsp;</label><input type="submit" value="Log in">
</div>
</form>
<script type="text/javascript">
document.getElementById('id_username').focus()
</script>
</div>
<br class="clear">
</div>
<div id="footer"></div>
</div>
</body>
</html>

View File

View File

@ -0,0 +1,40 @@
"""
From http://stackoverflow.com/questions/4124220/django-adding-css-classes-when-rendering-form-fields-in-a-template
The add_class filter allows for inserting classes into template variables that
contain HTML tags, useful for modifying forms without needing to change the
Form objects.
To use:
{{ field.label_tag|add_class:"control-label" }}
will insert the class `controls-label` into the label tag generated by a form.
In the case of Django REST Framework, the filter is used to add Bootstrap-specific
classes to the forms, while still allowing non-Bootstrap customization of the
browsable API.
"""
import re
from django.utils.safestring import mark_safe
from django import template
register = template.Library()
class_re = re.compile(r'(?<=class=["\'])(.*)(?=["\'])')
@register.filter
def add_class(value, css_class):
string = unicode(value)
match = class_re.search(string)
if match:
m = re.search(r'^%s$|^%s\s|\s%s\s|\s%s$' % (css_class, css_class,
css_class, css_class),
match.group(1))
print match.group(1)
if not m:
return mark_safe(class_re.sub(match.group(1) + " " + css_class,
string))
else:
return mark_safe(string.replace('>', ' class="%s">' % css_class, 1))
return value

View File

@ -0,0 +1,20 @@
from django.http import QueryDict
from django.template import Library
from urlparse import urlparse, urlunparse
register = Library()
def replace_query_param(url, key, val):
(scheme, netloc, path, params, query, fragment) = urlparse(url)
query_dict = QueryDict(query).copy()
query_dict[key] = val
query = query_dict.urlencode()
return urlunparse((scheme, netloc, path, params, query, fragment))
def add_query_param(url, param):
key, val = param.split('=')
return replace_query_param(url, key, val)
register.filter('add_query_param', add_query_param)

View File

@ -0,0 +1,32 @@
"""
Tags to optionally include the login and logout links, depending on if the
login and logout views are in the urlconf.
"""
from django import template
from django.core.urlresolvers import reverse, NoReverseMatch
register = template.Library()
@register.simple_tag(takes_context=True)
def optional_login(context):
try:
login_url = reverse('rest_framework:login')
except NoReverseMatch:
return ''
request = context['request']
snippet = "<a href='%s?next=%s'>Log in</a>" % (login_url, request.path)
return snippet
@register.simple_tag(takes_context=True)
def optional_logout(context):
try:
logout_url = reverse('rest_framework:logout')
except NoReverseMatch:
return ''
request = context['request']
snippet = "<a href='%s?next=%s'>Log out</a>" % (logout_url, request.path)
return snippet

View File

@ -0,0 +1,102 @@
"""
Adds the custom filter 'urlize_quoted_links'
This is identical to the built-in filter 'urlize' with the exception that
single and double quotes are permitted as leading or trailing punctuation.
Almost all of this code is copied verbatim from django.utils.html
LEADING_PUNCTUATION and TRAILING_PUNCTUATION have been modified
"""
import re
import string
from django.utils.safestring import SafeData, mark_safe
from django.utils.encoding import force_unicode
from django.utils.html import escape
from django import template
# Configuration for urlize() function.
LEADING_PUNCTUATION = ['(', '<', '&lt;', '"', "'"]
TRAILING_PUNCTUATION = ['.', ',', ')', '>', '\n', '&gt;', '"', "'"]
# List of possible strings used for bullets in bulleted lists.
DOTS = ['&middot;', '*', '\xe2\x80\xa2', '&#149;', '&bull;', '&#8226;']
unencoded_ampersands_re = re.compile(r'&(?!(\w+|#\d+);)')
word_split_re = re.compile(r'(\s+)')
punctuation_re = re.compile('^(?P<lead>(?:%s)*)(?P<middle>.*?)(?P<trail>(?:%s)*)$' % \
('|'.join([re.escape(x) for x in LEADING_PUNCTUATION]),
'|'.join([re.escape(x) for x in TRAILING_PUNCTUATION])))
simple_email_re = re.compile(r'^\S+@[a-zA-Z0-9._-]+\.[a-zA-Z0-9._-]+$')
link_target_attribute_re = re.compile(r'(<a [^>]*?)target=[^\s>]+')
html_gunk_re = re.compile(r'(?:<br clear="all">|<i><\/i>|<b><\/b>|<em><\/em>|<strong><\/strong>|<\/?smallcaps>|<\/?uppercase>)', re.IGNORECASE)
hard_coded_bullets_re = re.compile(r'((?:<p>(?:%s).*?[a-zA-Z].*?</p>\s*)+)' % '|'.join([re.escape(x) for x in DOTS]), re.DOTALL)
trailing_empty_content_re = re.compile(r'(?:<p>(?:&nbsp;|\s|<br \/>)*?</p>\s*)+\Z')
def urlize_quoted_links(text, trim_url_limit=None, nofollow=True, autoescape=True):
"""
Converts any URLs in text into clickable links.
Works on http://, https://, www. links and links ending in .org, .net or
.com. Links can have trailing punctuation (periods, commas, close-parens)
and leading punctuation (opening parens) and it'll still do the right
thing.
If trim_url_limit is not None, the URLs in link text longer than this limit
will truncated to trim_url_limit-3 characters and appended with an elipsis.
If nofollow is True, the URLs in link text will get a rel="nofollow"
attribute.
If autoescape is True, the link text and URLs will get autoescaped.
"""
trim_url = lambda x, limit=trim_url_limit: limit is not None and (len(x) > limit and ('%s...' % x[:max(0, limit - 3)])) or x
safe_input = isinstance(text, SafeData)
words = word_split_re.split(force_unicode(text))
nofollow_attr = nofollow and ' rel="nofollow"' or ''
for i, word in enumerate(words):
match = None
if '.' in word or '@' in word or ':' in word:
match = punctuation_re.match(word)
if match:
lead, middle, trail = match.groups()
# Make URL we want to point to.
url = None
if middle.startswith('http://') or middle.startswith('https://'):
url = middle
elif middle.startswith('www.') or ('@' not in middle and \
middle and middle[0] in string.ascii_letters + string.digits and \
(middle.endswith('.org') or middle.endswith('.net') or middle.endswith('.com'))):
url = 'http://%s' % middle
elif '@' in middle and not ':' in middle and simple_email_re.match(middle):
url = 'mailto:%s' % middle
nofollow_attr = ''
# Make link.
if url:
trimmed = trim_url(middle)
if autoescape and not safe_input:
lead, trail = escape(lead), escape(trail)
url, trimmed = escape(url), escape(trimmed)
middle = '<a href="%s"%s>%s</a>' % (url, nofollow_attr, trimmed)
words[i] = mark_safe('%s%s%s' % (lead, middle, trail))
else:
if safe_input:
words[i] = mark_safe(word)
elif autoescape:
words[i] = escape(word)
elif safe_input:
words[i] = mark_safe(word)
elif autoescape:
words[i] = escape(word)
return u''.join(words)
#urlize_quoted_links.needs_autoescape = True
urlize_quoted_links.is_safe = True
# Register urlize_quoted_links as a custom filter
# http://docs.djangoproject.com/en/dev/howto/custom-template-tags/
register = template.Library()
register.filter(urlize_quoted_links)

View File

@ -0,0 +1,12 @@
"""Force import of all modules in this package in order to get the standard test runner to pick up the tests. Yowzers."""
import os
modules = [filename.rsplit('.', 1)[0]
for filename in os.listdir(os.path.dirname(__file__))
if filename.endswith('.py') and not filename.startswith('_')]
__test__ = dict()
for module in modules:
exec("from rest_framework.tests.%s import __doc__ as module_doc" % module)
exec("from rest_framework.tests.%s import *" % module)
__test__[module] = module_doc or ""

View File

@ -0,0 +1,153 @@
from django.conf.urls.defaults import patterns
from django.contrib.auth.models import User
from django.test import Client, TestCase
from django.utils import simplejson as json
from django.http import HttpResponse
from rest_framework.views import APIView
from rest_framework import permissions
from rest_framework.authtoken.models import Token
from rest_framework.authentication import TokenAuthentication
import base64
class MockView(APIView):
permission_classes = (permissions.IsAuthenticated,)
def post(self, request):
return HttpResponse({'a': 1, 'b': 2, 'c': 3})
def put(self, request):
return HttpResponse({'a': 1, 'b': 2, 'c': 3})
MockView.authentication_classes += (TokenAuthentication,)
urlpatterns = patterns('',
(r'^$', MockView.as_view()),
)
class BasicAuthTests(TestCase):
"""Basic authentication"""
urls = 'rest_framework.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 = 'rest_framework.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_put_form_session_auth_passing(self):
"""
Ensure PUTting 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.put('/', {'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)
class TokenAuthTests(TestCase):
"""Token authentication"""
urls = 'rest_framework.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)
self.key = 'abcd1234'
self.token = Token.objects.create(key=self.key, user=self.user)
def test_post_form_passing_token_auth(self):
"""Ensure POSTing json over token auth with correct credentials passes and does not require CSRF"""
auth = "Token " + self.key
response = self.csrf_client.post('/', {'example': 'example'}, HTTP_AUTHORIZATION=auth)
self.assertEqual(response.status_code, 200)
def test_post_json_passing_token_auth(self):
"""Ensure POSTing form over token auth with correct credentials passes and does not require CSRF"""
auth = "Token " + self.key
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_token_auth(self):
"""Ensure POSTing form over token auth without correct credentials fails"""
response = self.csrf_client.post('/', {'example': 'example'})
self.assertEqual(response.status_code, 403)
def test_post_json_failing_token_auth(self):
"""Ensure POSTing json over token auth without correct credentials fails"""
response = self.csrf_client.post('/', json.dumps({'example': 'example'}), 'application/json')
self.assertEqual(response.status_code, 403)
def test_token_has_auto_assigned_key_if_none_provided(self):
"""Ensure creating a token with no key will auto-assign a key"""
token = Token.objects.create(user=self.user)
self.assertTrue(bool(token.key))

View File

@ -0,0 +1,72 @@
from django.conf.urls.defaults import patterns, url
from django.test import TestCase
from rest_framework.utils.breadcrumbs import get_breadcrumbs
from rest_framework.views import APIView
class Root(APIView):
pass
class ResourceRoot(APIView):
pass
class ResourceInstance(APIView):
pass
class NestedResourceRoot(APIView):
pass
class NestedResourceInstance(APIView):
pass
urlpatterns = patterns('',
url(r'^$', Root.as_view()),
url(r'^resource/$', ResourceRoot.as_view()),
url(r'^resource/(?P<key>[0-9]+)$', ResourceInstance.as_view()),
url(r'^resource/(?P<key>[0-9]+)/$', NestedResourceRoot.as_view()),
url(r'^resource/(?P<key>[0-9]+)/(?P<other>[A-Za-z]+)$', NestedResourceInstance.as_view()),
)
class BreadcrumbTests(TestCase):
"""Tests the breadcrumb functionality used by the HTML renderer."""
urls = 'rest_framework.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', '/')])

View File

@ -0,0 +1,113 @@
from django.test import TestCase
from rest_framework.views import APIView
from rest_framework.compat import apply_markdown
# 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)
# We support markdown < 2.1 and markdown >= 2.1
MARKED_DOWN_lt_21 = """<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>"""
MARKED_DOWN_gte_21 = """<h2 id="an-example-docstring">an example docstring</h2>
<ul>
<li>list</li>
<li>list</li>
</ul>
<h3 id="another-header">another header</h3>
<pre><code>code block
</code></pre>
<p>indented</p>
<h2 id="hash-style-header">hash style header</h2>"""
class TestViewNamesAndDescriptions(TestCase):
def test_resource_name_uses_classname_by_default(self):
"""Ensure Resource names are based on the classname by default."""
class MockView(APIView):
pass
self.assertEquals(MockView().get_name(), 'Mock')
def test_resource_name_can_be_set_explicitly(self):
"""Ensure Resource names can be set using the 'get_name' method."""
example = 'Some Other Name'
class MockView(APIView):
def get_name(self):
return example
self.assertEquals(MockView().get_name(), example)
def test_resource_description_uses_docstring_by_default(self):
"""Ensure Resource names are based on the docstring by default."""
class MockView(APIView):
"""an example docstring
====================
* list
* list
another header
--------------
code block
indented
# hash style header #"""
self.assertEquals(MockView().get_description(), DESCRIPTION)
def test_resource_description_can_be_set_explicitly(self):
"""Ensure Resource descriptions can be set using the 'get_description' method."""
example = 'Some other description'
class MockView(APIView):
"""docstring"""
def get_description(self):
return example
self.assertEquals(MockView().get_description(), 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 'get_description' method."""
example = 'Some other description'
class MockView(APIView):
def get_description(self):
return example
self.assertEquals(MockView().get_description(), 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 MockView(APIView):
pass
self.assertEquals(MockView().get_description(), '')
def test_markdown(self):
"""Ensure markdown to HTML works as expected"""
if apply_markdown:
gte_21_match = apply_markdown(DESCRIPTION) == MARKED_DOWN_gte_21
lt_21_match = apply_markdown(DESCRIPTION) == MARKED_DOWN_lt_21
self.assertTrue(gte_21_match or lt_21_match)

View File

@ -0,0 +1,34 @@
# from django.test import TestCase
# from django import forms
# from rest_framework.compat import RequestFactory
# from rest_framework.views import View
# from rest_framework.response import Response
# import StringIO
# class UploadFilesTests(TestCase):
# """Check uploading of files"""
# def setUp(self):
# self.factory = RequestFactory()
# def test_upload_file(self):
# class FileForm(forms.Form):
# file = forms.FileField()
# class MockView(View):
# permissions = ()
# form = FileForm
# def post(self, request, *args, **kwargs):
# return Response({'FILE_NAME': self.CONTENT['file'].name,
# 'FILE_CONTENT': self.CONTENT['file'].read()})
# file = StringIO.StringIO('stuff')
# file.name = 'stuff.txt'
# request = self.factory.post('/', {'file': file})
# view = MockView.as_view()
# response = view(request)
# self.assertEquals(response.raw_content, {"FILE_CONTENT": "stuff", "FILE_NAME": "stuff.txt"})

View File

View File

@ -0,0 +1,285 @@
# """Tests for the mixin module"""
# from django.test import TestCase
# from rest_framework import status
# from rest_framework.compat import RequestFactory
# from django.contrib.auth.models import Group, User
# from rest_framework.mixins import CreateModelMixin, PaginatorMixin, ReadModelMixin
# from rest_framework.resources import ModelResource
# from rest_framework.response import Response, ImmediateResponse
# from rest_framework.tests.models import CustomUser
# from rest_framework.tests.testcases import TestModelsTestCase
# from rest_framework.views import View
# class TestModelRead(TestModelsTestCase):
# """Tests on ReadModelMixin"""
# def setUp(self):
# super(TestModelRead, self).setUp()
# self.req = RequestFactory()
# def test_read(self):
# Group.objects.create(name='other group')
# group = Group.objects.create(name='my group')
# class GroupResource(ModelResource):
# model = Group
# request = self.req.get('/groups')
# mixin = ReadModelMixin()
# mixin.resource = GroupResource
# response = mixin.get(request, id=group.id)
# self.assertEquals(group.name, response.raw_content.name)
# def test_read_404(self):
# class GroupResource(ModelResource):
# model = Group
# request = self.req.get('/groups')
# mixin = ReadModelMixin()
# mixin.resource = GroupResource
# self.assertRaises(ImmediateResponse, mixin.get, request, id=12345)
# class TestModelCreation(TestModelsTestCase):
# """Tests on CreateModelMixin"""
# def setUp(self):
# super(TestModelsTestCase, self).setUp()
# self.req = RequestFactory()
# def test_creation(self):
# self.assertEquals(0, Group.objects.count())
# class GroupResource(ModelResource):
# model = Group
# form_data = {'name': 'foo'}
# request = self.req.post('/groups', data=form_data)
# mixin = CreateModelMixin()
# mixin.resource = GroupResource
# mixin.CONTENT = form_data
# response = mixin.post(request)
# self.assertEquals(1, Group.objects.count())
# self.assertEquals('foo', response.raw_content.name)
# def test_creation_with_m2m_relation(self):
# class UserResource(ModelResource):
# model = User
# def url(self, instance):
# return "/users/%i" % instance.id
# group = Group(name='foo')
# group.save()
# form_data = {
# 'username': 'bar',
# 'password': 'baz',
# 'groups': [group.id]
# }
# request = self.req.post('/groups', data=form_data)
# cleaned_data = dict(form_data)
# cleaned_data['groups'] = [group]
# mixin = CreateModelMixin()
# mixin.resource = UserResource
# mixin.CONTENT = cleaned_data
# response = mixin.post(request)
# self.assertEquals(1, User.objects.count())
# self.assertEquals(1, response.raw_content.groups.count())
# self.assertEquals('foo', response.raw_content.groups.all()[0].name)
# def test_creation_with_m2m_relation_through(self):
# """
# Tests creation where the m2m relation uses a through table
# """
# class UserResource(ModelResource):
# model = CustomUser
# def url(self, instance):
# return "/customusers/%i" % instance.id
# form_data = {'username': 'bar0', 'groups': []}
# request = self.req.post('/groups', data=form_data)
# cleaned_data = dict(form_data)
# cleaned_data['groups'] = []
# mixin = CreateModelMixin()
# mixin.resource = UserResource
# mixin.CONTENT = cleaned_data
# response = mixin.post(request)
# self.assertEquals(1, CustomUser.objects.count())
# self.assertEquals(0, response.raw_content.groups.count())
# group = Group(name='foo1')
# group.save()
# form_data = {'username': 'bar1', 'groups': [group.id]}
# request = self.req.post('/groups', data=form_data)
# cleaned_data = dict(form_data)
# cleaned_data['groups'] = [group]
# mixin = CreateModelMixin()
# mixin.resource = UserResource
# mixin.CONTENT = cleaned_data
# response = mixin.post(request)
# self.assertEquals(2, CustomUser.objects.count())
# self.assertEquals(1, response.raw_content.groups.count())
# self.assertEquals('foo1', response.raw_content.groups.all()[0].name)
# group2 = Group(name='foo2')
# group2.save()
# form_data = {'username': 'bar2', 'groups': [group.id, group2.id]}
# request = self.req.post('/groups', data=form_data)
# cleaned_data = dict(form_data)
# cleaned_data['groups'] = [group, group2]
# mixin = CreateModelMixin()
# mixin.resource = UserResource
# mixin.CONTENT = cleaned_data
# response = mixin.post(request)
# self.assertEquals(3, CustomUser.objects.count())
# self.assertEquals(2, response.raw_content.groups.count())
# self.assertEquals('foo1', response.raw_content.groups.all()[0].name)
# self.assertEquals('foo2', response.raw_content.groups.all()[1].name)
# class MockPaginatorView(PaginatorMixin, View):
# total = 60
# def get(self, request):
# return Response(range(0, self.total))
# def post(self, request):
# return Response({'status': 'OK'}, status=status.HTTP_201_CREATED)
# class TestPagination(TestCase):
# def setUp(self):
# self.req = RequestFactory()
# def test_default_limit(self):
# """ Tests if pagination works without overwriting the limit """
# request = self.req.get('/paginator')
# response = MockPaginatorView.as_view()(request)
# content = response.raw_content
# self.assertEqual(response.status_code, status.HTTP_200_OK)
# self.assertEqual(MockPaginatorView.total, content['total'])
# self.assertEqual(MockPaginatorView.limit, content['per_page'])
# self.assertEqual(range(0, MockPaginatorView.limit), content['results'])
# def test_overwriting_limit(self):
# """ Tests if the limit can be overwritten """
# limit = 10
# request = self.req.get('/paginator')
# response = MockPaginatorView.as_view(limit=limit)(request)
# content = response.raw_content
# self.assertEqual(response.status_code, status.HTTP_200_OK)
# self.assertEqual(content['per_page'], limit)
# self.assertEqual(range(0, limit), content['results'])
# def test_limit_param(self):
# """ Tests if the client can set the limit """
# from math import ceil
# limit = 5
# num_pages = int(ceil(MockPaginatorView.total / float(limit)))
# request = self.req.get('/paginator/?limit=%d' % limit)
# response = MockPaginatorView.as_view()(request)
# content = response.raw_content
# self.assertEqual(response.status_code, status.HTTP_200_OK)
# self.assertEqual(MockPaginatorView.total, content['total'])
# self.assertEqual(limit, content['per_page'])
# self.assertEqual(num_pages, content['pages'])
# def test_exceeding_limit(self):
# """ Makes sure the client cannot exceed the default limit """
# from math import ceil
# limit = MockPaginatorView.limit + 10
# num_pages = int(ceil(MockPaginatorView.total / float(limit)))
# request = self.req.get('/paginator/?limit=%d' % limit)
# response = MockPaginatorView.as_view()(request)
# content = response.raw_content
# self.assertEqual(response.status_code, status.HTTP_200_OK)
# self.assertEqual(MockPaginatorView.total, content['total'])
# self.assertNotEqual(limit, content['per_page'])
# self.assertNotEqual(num_pages, content['pages'])
# self.assertEqual(MockPaginatorView.limit, content['per_page'])
# def test_only_works_for_get(self):
# """ Pagination should only work for GET requests """
# request = self.req.post('/paginator', data={'content': 'spam'})
# response = MockPaginatorView.as_view()(request)
# content = response.raw_content
# self.assertEqual(response.status_code, status.HTTP_201_CREATED)
# self.assertEqual(None, content.get('per_page'))
# self.assertEqual('OK', content['status'])
# def test_non_int_page(self):
# """ Tests that it can handle invalid values """
# request = self.req.get('/paginator/?page=spam')
# response = MockPaginatorView.as_view()(request)
# self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)
# def test_page_range(self):
# """ Tests that the page range is handle correctly """
# request = self.req.get('/paginator/?page=0')
# response = MockPaginatorView.as_view()(request)
# content = response.raw_content
# self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)
# request = self.req.get('/paginator/')
# response = MockPaginatorView.as_view()(request)
# content = response.raw_content
# self.assertEqual(response.status_code, status.HTTP_200_OK)
# self.assertEqual(range(0, MockPaginatorView.limit), content['results'])
# num_pages = content['pages']
# request = self.req.get('/paginator/?page=%d' % num_pages)
# response = MockPaginatorView.as_view()(request)
# content = response.raw_content
# self.assertEqual(response.status_code, status.HTTP_200_OK)
# self.assertEqual(range(MockPaginatorView.limit*(num_pages-1), MockPaginatorView.total), content['results'])
# request = self.req.get('/paginator/?page=%d' % (num_pages + 1,))
# response = MockPaginatorView.as_view()(request)
# content = response.raw_content
# self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)
# def test_existing_query_parameters_are_preserved(self):
# """ Tests that existing query parameters are preserved when
# generating next/previous page links """
# request = self.req.get('/paginator/?foo=bar&another=something')
# response = MockPaginatorView.as_view()(request)
# content = response.raw_content
# self.assertEqual(response.status_code, status.HTTP_200_OK)
# self.assertTrue('foo=bar' in content['next'])
# self.assertTrue('another=something' in content['next'])
# self.assertTrue('page=2' in content['next'])
# def test_duplicate_parameters_are_not_created(self):
# """ Regression: ensure duplicate "page" parameters are not added to
# paginated URLs. So page 1 should contain ?page=2, not ?page=1&page=2 """
# request = self.req.get('/paginator/?page=1')
# response = MockPaginatorView.as_view()(request)
# content = response.raw_content
# self.assertTrue('page=2' in content['next'])
# self.assertFalse('page=1' in content['next'])

View File

@ -0,0 +1,28 @@
from django.db import models
from django.contrib.auth.models import Group
class CustomUser(models.Model):
"""
A custom user model, which uses a 'through' table for the foreign key
"""
username = models.CharField(max_length=255, unique=True)
groups = models.ManyToManyField(
to=Group, blank=True, null=True, through='UserGroupMap'
)
@models.permalink
def get_absolute_url(self):
return ('custom_user', (), {
'pk': self.id
})
class UserGroupMap(models.Model):
user = models.ForeignKey(to=CustomUser)
group = models.ForeignKey(to=Group)
@models.permalink
def get_absolute_url(self):
return ('user_group_map', (), {
'pk': self.id
})

View File

@ -0,0 +1,90 @@
# from django.conf.urls.defaults import patterns, url
# from django.forms import ModelForm
# from django.contrib.auth.models import Group, User
# from rest_framework.resources import ModelResource
# from rest_framework.views import ListOrCreateModelView, InstanceModelView
# from rest_framework.tests.models import CustomUser
# from rest_framework.tests.testcases import TestModelsTestCase
# class GroupResource(ModelResource):
# model = Group
# class UserForm(ModelForm):
# class Meta:
# model = User
# exclude = ('last_login', 'date_joined')
# class UserResource(ModelResource):
# model = User
# form = UserForm
# class CustomUserResource(ModelResource):
# model = CustomUser
# urlpatterns = patterns('',
# url(r'^users/$', ListOrCreateModelView.as_view(resource=UserResource), name='users'),
# url(r'^users/(?P<id>[0-9]+)/$', InstanceModelView.as_view(resource=UserResource)),
# url(r'^customusers/$', ListOrCreateModelView.as_view(resource=CustomUserResource), name='customusers'),
# url(r'^customusers/(?P<id>[0-9]+)/$', InstanceModelView.as_view(resource=CustomUserResource)),
# url(r'^groups/$', ListOrCreateModelView.as_view(resource=GroupResource), name='groups'),
# url(r'^groups/(?P<id>[0-9]+)/$', InstanceModelView.as_view(resource=GroupResource)),
# )
# class ModelViewTests(TestModelsTestCase):
# """Test the model views rest_framework provides"""
# urls = 'rest_framework.tests.modelviews'
# def test_creation(self):
# """Ensure that a model object can be created"""
# self.assertEqual(0, Group.objects.count())
# response = self.client.post('/groups/', {'name': 'foo'})
# self.assertEqual(response.status_code, 201)
# self.assertEqual(1, Group.objects.count())
# self.assertEqual('foo', Group.objects.all()[0].name)
# def test_creation_with_m2m_relation(self):
# """Ensure that a model object with a m2m relation can be created"""
# group = Group(name='foo')
# group.save()
# self.assertEqual(0, User.objects.count())
# response = self.client.post('/users/', {'username': 'bar', 'password': 'baz', 'groups': [group.id]})
# self.assertEqual(response.status_code, 201)
# self.assertEqual(1, User.objects.count())
# user = User.objects.all()[0]
# self.assertEqual('bar', user.username)
# self.assertEqual('baz', user.password)
# self.assertEqual(1, user.groups.count())
# group = user.groups.all()[0]
# self.assertEqual('foo', group.name)
# def test_creation_with_m2m_relation_through(self):
# """
# Ensure that a model object with a m2m relation can be created where that
# relation uses a through table
# """
# group = Group(name='foo')
# group.save()
# self.assertEqual(0, User.objects.count())
# response = self.client.post('/customusers/', {'username': 'bar', 'groups': [group.id]})
# self.assertEqual(response.status_code, 201)
# self.assertEqual(1, CustomUser.objects.count())
# user = CustomUser.objects.all()[0]
# self.assertEqual('bar', user.username)
# self.assertEqual(1, user.groups.count())
# group = user.groups.all()[0]
# self.assertEqual('foo', group.name)

View File

@ -0,0 +1,211 @@
import time
from django.conf.urls.defaults import patterns, url, include
from django.contrib.auth.models import User
from django.test import Client, TestCase
from rest_framework.views import APIView
# Since oauth2 / django-oauth-plus are optional dependancies, we don't want to
# always run these tests.
# Unfortunatly we can't skip tests easily until 2.7, se we'll just do this for now.
try:
import oauth2 as oauth
from oauth_provider.decorators import oauth_required
from oauth_provider.models import Resource, Consumer, Token
except ImportError:
pass
else:
# Alrighty, we're good to go here.
class ClientView(APIView):
def get(self, request):
return {'resource': 'Protected!'}
urlpatterns = patterns('',
url(r'^$', oauth_required(ClientView.as_view())),
url(r'^oauth/', include('oauth_provider.urls')),
url(r'^restframework/', include('rest_framework.urls', namespace='rest_framework')),
)
class OAuthTests(TestCase):
"""
OAuth authentication:
* the user would like to access his API data from a third-party website
* the third-party website proposes a link to get that API data
* the user is redirected to the API and must log in if not authenticated
* the API displays a webpage to confirm that the user trusts the third-party website
* if confirmed, the user is redirected to the third-party website through the callback view
* the third-party website is able to retrieve data from the API
"""
urls = 'rest_framework.tests.oauthentication'
def setUp(self):
self.client = Client()
self.username = 'john'
self.email = 'lennon@thebeatles.com'
self.password = 'password'
self.user = User.objects.create_user(self.username, self.email, self.password)
# OAuth requirements
self.resource = Resource(name='data', url='/')
self.resource.save()
self.CONSUMER_KEY = 'dpf43f3p2l4k3l03'
self.CONSUMER_SECRET = 'kd94hf93k423kf44'
self.consumer = Consumer(key=self.CONSUMER_KEY, secret=self.CONSUMER_SECRET,
name='api.example.com', user=self.user)
self.consumer.save()
def test_oauth_invalid_and_anonymous_access(self):
"""
Verify that the resource is protected and the OAuth authorization view
require the user to be logged in.
"""
response = self.client.get('/')
self.assertEqual(response.content, 'Invalid request parameters.')
self.assertEqual(response.status_code, 401)
response = self.client.get('/oauth/authorize/', follow=True)
self.assertRedirects(response, '/accounts/login/?next=/oauth/authorize/')
def test_oauth_authorize_access(self):
"""
Verify that once logged in, the user can access the authorization page
but can't display the page because the request token is not specified.
"""
self.client.login(username=self.username, password=self.password)
response = self.client.get('/oauth/authorize/', follow=True)
self.assertEqual(response.content, 'No request token specified.')
def _create_request_token_parameters(self):
"""
A shortcut to create request's token parameters.
"""
return {
'oauth_consumer_key': self.CONSUMER_KEY,
'oauth_signature_method': 'PLAINTEXT',
'oauth_signature': '%s&' % self.CONSUMER_SECRET,
'oauth_timestamp': str(int(time.time())),
'oauth_nonce': 'requestnonce',
'oauth_version': '1.0',
'oauth_callback': 'http://api.example.com/request_token_ready',
'scope': 'data',
}
def test_oauth_request_token_retrieval(self):
"""
Verify that the request token can be retrieved by the server.
"""
response = self.client.get("/oauth/request_token/",
self._create_request_token_parameters())
self.assertEqual(response.status_code, 200)
token = list(Token.objects.all())[-1]
self.failIf(token.key not in response.content)
self.failIf(token.secret not in response.content)
def test_oauth_user_request_authorization(self):
"""
Verify that the user can access the authorization page once logged in
and the request token has been retrieved.
"""
# Setup
response = self.client.get("/oauth/request_token/",
self._create_request_token_parameters())
token = list(Token.objects.all())[-1]
# Starting the test here
self.client.login(username=self.username, password=self.password)
parameters = {'oauth_token': token.key}
response = self.client.get("/oauth/authorize/", parameters)
self.assertEqual(response.status_code, 200)
self.failIf(not response.content.startswith('Fake authorize view for api.example.com with params: oauth_token='))
self.assertEqual(token.is_approved, 0)
parameters['authorize_access'] = 1 # fake authorization by the user
response = self.client.post("/oauth/authorize/", parameters)
self.assertEqual(response.status_code, 302)
self.failIf(not response['Location'].startswith('http://api.example.com/request_token_ready?oauth_verifier='))
token = Token.objects.get(key=token.key)
self.failIf(token.key not in response['Location'])
self.assertEqual(token.is_approved, 1)
def _create_access_token_parameters(self, token):
"""
A shortcut to create access' token parameters.
"""
return {
'oauth_consumer_key': self.CONSUMER_KEY,
'oauth_token': token.key,
'oauth_signature_method': 'PLAINTEXT',
'oauth_signature': '%s&%s' % (self.CONSUMER_SECRET, token.secret),
'oauth_timestamp': str(int(time.time())),
'oauth_nonce': 'accessnonce',
'oauth_version': '1.0',
'oauth_verifier': token.verifier,
'scope': 'data',
}
def test_oauth_access_token_retrieval(self):
"""
Verify that the request token can be retrieved by the server.
"""
# Setup
response = self.client.get("/oauth/request_token/",
self._create_request_token_parameters())
token = list(Token.objects.all())[-1]
self.client.login(username=self.username, password=self.password)
parameters = {'oauth_token': token.key,}
response = self.client.get("/oauth/authorize/", parameters)
parameters['authorize_access'] = 1 # fake authorization by the user
response = self.client.post("/oauth/authorize/", parameters)
token = Token.objects.get(key=token.key)
# Starting the test here
response = self.client.get("/oauth/access_token/", self._create_access_token_parameters(token))
self.assertEqual(response.status_code, 200)
self.failIf(not response.content.startswith('oauth_token_secret='))
access_token = list(Token.objects.filter(token_type=Token.ACCESS))[-1]
self.failIf(access_token.key not in response.content)
self.failIf(access_token.secret not in response.content)
self.assertEqual(access_token.user.username, 'john')
def _create_access_parameters(self, access_token):
"""
A shortcut to create access' parameters.
"""
parameters = {
'oauth_consumer_key': self.CONSUMER_KEY,
'oauth_token': access_token.key,
'oauth_signature_method': 'HMAC-SHA1',
'oauth_timestamp': str(int(time.time())),
'oauth_nonce': 'accessresourcenonce',
'oauth_version': '1.0',
}
oauth_request = oauth.Request.from_token_and_callback(access_token,
http_url='http://testserver/', parameters=parameters)
signature_method = oauth.SignatureMethod_HMAC_SHA1()
signature = signature_method.sign(oauth_request, self.consumer, access_token)
parameters['oauth_signature'] = signature
return parameters
def test_oauth_protected_resource_access(self):
"""
Verify that the request token can be retrieved by the server.
"""
# Setup
response = self.client.get("/oauth/request_token/",
self._create_request_token_parameters())
token = list(Token.objects.all())[-1]
self.client.login(username=self.username, password=self.password)
parameters = {'oauth_token': token.key,}
response = self.client.get("/oauth/authorize/", parameters)
parameters['authorize_access'] = 1 # fake authorization by the user
response = self.client.post("/oauth/authorize/", parameters)
token = Token.objects.get(key=token.key)
response = self.client.get("/oauth/access_token/", self._create_access_token_parameters(token))
access_token = list(Token.objects.filter(token_type=Token.ACCESS))[-1]
# Starting the test here
response = self.client.get("/", self._create_access_token_parameters(access_token))
self.assertEqual(response.status_code, 200)
self.assertEqual(response.content, '{"resource": "Protected!"}')

View File

@ -0,0 +1,11 @@
"""Tests for the rest_framework package setup."""
from django.test import TestCase
import rest_framework
class TestVersion(TestCase):
"""Simple sanity test to check the VERSION exists"""
def test_version(self):
"""Ensure the VERSION exists."""
rest_framework.VERSION

View File

@ -0,0 +1,212 @@
# """
# ..
# >>> from rest_framework.parsers import FormParser
# >>> from rest_framework.compat import RequestFactory
# >>> from rest_framework.views import View
# >>> from StringIO import StringIO
# >>> from urllib import urlencode
# >>> req = RequestFactory().get('/')
# >>> some_view = View()
# >>> some_view.request = req # Make as if this request had been dispatched
#
# FormParser
# ============
#
# Data flatening
# ----------------
#
# Here is some example data, which would eventually be sent along with a post request :
#
# >>> inpt = urlencode([
# ... ('key1', 'bla1'),
# ... ('key2', 'blo1'), ('key2', 'blo2'),
# ... ])
#
# Default behaviour for :class:`parsers.FormParser`, is to return a single value for each parameter :
#
# >>> (data, files) = FormParser(some_view).parse(StringIO(inpt))
# >>> data == {'key1': 'bla1', 'key2': 'blo1'}
# True
#
# However, you can customize this behaviour by subclassing :class:`parsers.FormParser`, and overriding :meth:`parsers.FormParser.is_a_list` :
#
# >>> class MyFormParser(FormParser):
# ...
# ... def is_a_list(self, key, val_list):
# ... return len(val_list) > 1
#
# This new parser only flattens the lists of parameters that contain a single value.
#
# >>> (data, files) = MyFormParser(some_view).parse(StringIO(inpt))
# >>> data == {'key1': 'bla1', 'key2': ['blo1', 'blo2']}
# True
#
# .. note:: The same functionality is available for :class:`parsers.MultiPartParser`.
#
# Submitting an empty list
# --------------------------
#
# When submitting an empty select multiple, like this one ::
#
# <select multiple="multiple" name="key2"></select>
#
# The browsers usually strip the parameter completely. A hack to avoid this, and therefore being able to submit an empty select multiple, is to submit a value that tells the server that the list is empty ::
#
# <select multiple="multiple" name="key2"><option value="_empty"></select>
#
# :class:`parsers.FormParser` provides the server-side implementation for this hack. Considering the following posted data :
#
# >>> inpt = urlencode([
# ... ('key1', 'blo1'), ('key1', '_empty'),
# ... ('key2', '_empty'),
# ... ])
#
# :class:`parsers.FormParser` strips the values ``_empty`` from all the lists.
#
# >>> (data, files) = MyFormParser(some_view).parse(StringIO(inpt))
# >>> data == {'key1': 'blo1'}
# True
#
# Oh ... but wait a second, the parameter ``key2`` isn't even supposed to be a list, so the parser just stripped it.
#
# >>> class MyFormParser(FormParser):
# ...
# ... def is_a_list(self, key, val_list):
# ... return key == 'key2'
# ...
# >>> (data, files) = MyFormParser(some_view).parse(StringIO(inpt))
# >>> data == {'key1': 'blo1', 'key2': []}
# True
#
# Better like that. Note that you can configure something else than ``_empty`` for the empty value by setting :attr:`parsers.FormParser.EMPTY_VALUE`.
# """
# import httplib, mimetypes
# from tempfile import TemporaryFile
# from django.test import TestCase
# from rest_framework.compat import RequestFactory
# from rest_framework.parsers import MultiPartParser
# from rest_framework.views import View
# from StringIO import StringIO
#
# def encode_multipart_formdata(fields, files):
# """For testing multipart parser.
# fields is a sequence of (name, value) elements for regular form fields.
# files is a sequence of (name, filename, value) elements for data to be uploaded as files
# Return (content_type, body)."""
# BOUNDARY = '----------ThIs_Is_tHe_bouNdaRY_$'
# CRLF = '\r\n'
# L = []
# for (key, value) in fields:
# L.append('--' + BOUNDARY)
# L.append('Content-Disposition: form-data; name="%s"' % key)
# L.append('')
# L.append(value)
# for (key, filename, value) in files:
# L.append('--' + BOUNDARY)
# L.append('Content-Disposition: form-data; name="%s"; filename="%s"' % (key, filename))
# L.append('Content-Type: %s' % get_content_type(filename))
# L.append('')
# L.append(value)
# L.append('--' + BOUNDARY + '--')
# L.append('')
# body = CRLF.join(L)
# content_type = 'multipart/form-data; boundary=%s' % BOUNDARY
# return content_type, body
#
# def get_content_type(filename):
# return mimetypes.guess_type(filename)[0] or 'application/octet-stream'
#
#class TestMultiPartParser(TestCase):
# def setUp(self):
# self.req = RequestFactory()
# self.content_type, self.body = encode_multipart_formdata([('key1', 'val1'), ('key1', 'val2')],
# [('file1', 'pic.jpg', 'blablabla'), ('file1', 't.txt', 'blobloblo')])
#
# def test_multipartparser(self):
# """Ensure that MultiPartParser can parse multipart/form-data that contains a mix of several files and parameters."""
# post_req = RequestFactory().post('/', self.body, content_type=self.content_type)
# view = View()
# view.request = post_req
# (data, files) = MultiPartParser(view).parse(StringIO(self.body))
# self.assertEqual(data['key1'], 'val1')
# self.assertEqual(files['file1'].read(), 'blablabla')
from StringIO import StringIO
from django import forms
from django.test import TestCase
from rest_framework.parsers import FormParser
from rest_framework.parsers import XMLParser
import datetime
class Form(forms.Form):
field1 = forms.CharField(max_length=3)
field2 = forms.CharField()
class TestFormParser(TestCase):
def setUp(self):
self.string = "field1=abc&field2=defghijk"
def test_parse(self):
""" Make sure the `QueryDict` works OK """
parser = FormParser()
stream = StringIO(self.string)
data = parser.parse(stream)
self.assertEqual(Form(data).is_valid(), True)
class TestXMLParser(TestCase):
def setUp(self):
self._input = StringIO(
'<?xml version="1.0" encoding="utf-8"?>'
'<root>'
'<field_a>121.0</field_a>'
'<field_b>dasd</field_b>'
'<field_c></field_c>'
'<field_d>2011-12-25 12:45:00</field_d>'
'</root>'
)
self._data = {
'field_a': 121,
'field_b': 'dasd',
'field_c': None,
'field_d': datetime.datetime(2011, 12, 25, 12, 45, 00)
}
self._complex_data_input = StringIO(
'<?xml version="1.0" encoding="utf-8"?>'
'<root>'
'<creation_date>2011-12-25 12:45:00</creation_date>'
'<sub_data_list>'
'<list-item><sub_id>1</sub_id><sub_name>first</sub_name></list-item>'
'<list-item><sub_id>2</sub_id><sub_name>second</sub_name></list-item>'
'</sub_data_list>'
'<name>name</name>'
'</root>'
)
self._complex_data = {
"creation_date": datetime.datetime(2011, 12, 25, 12, 45, 00),
"name": "name",
"sub_data_list": [
{
"sub_id": 1,
"sub_name": "first"
},
{
"sub_id": 2,
"sub_name": "second"
}
]
}
def test_parse(self):
parser = XMLParser()
data = parser.parse(self._input)
self.assertEqual(data, self._data)
def test_complex_data_parse(self):
parser = XMLParser()
data = parser.parse(self._complex_data_input)
self.assertEqual(data, self._complex_data)

View File

@ -0,0 +1,375 @@
import re
from django.conf.urls.defaults import patterns, url, include
from django.test import TestCase
from rest_framework import status
from rest_framework.response import Response
from rest_framework.views import APIView
from rest_framework.renderers import BaseRenderer, JSONRenderer, YAMLRenderer, \
XMLRenderer, JSONPRenderer, DocumentingHTMLRenderer
from rest_framework.parsers import YAMLParser, XMLParser
from StringIO import StringIO
import datetime
from decimal import Decimal
DUMMYSTATUS = status.HTTP_200_OK
DUMMYCONTENT = 'dummycontent'
RENDERER_A_SERIALIZER = lambda x: 'Renderer A: %s' % x
RENDERER_B_SERIALIZER = lambda x: 'Renderer B: %s' % x
expected_results = [
((elem for elem in [1, 2, 3]), JSONRenderer, '[1, 2, 3]') # Generator
]
class BasicRendererTests(TestCase):
def test_expected_results(self):
for value, renderer_cls, expected in expected_results:
output = renderer_cls().render(value)
self.assertEquals(output, expected)
class RendererA(BaseRenderer):
media_type = 'mock/renderera'
format = "formata"
def render(self, obj=None, media_type=None):
return RENDERER_A_SERIALIZER(obj)
class RendererB(BaseRenderer):
media_type = 'mock/rendererb'
format = "formatb"
def render(self, obj=None, media_type=None):
return RENDERER_B_SERIALIZER(obj)
class MockView(APIView):
renderer_classes = (RendererA, RendererB)
def get(self, request, **kwargs):
response = Response(DUMMYCONTENT, status=DUMMYSTATUS)
return response
class MockGETView(APIView):
def get(self, request, **kwargs):
return Response({'foo': ['bar', 'baz']})
class HTMLView(APIView):
renderer_classes = (DocumentingHTMLRenderer, )
def get(self, request, **kwargs):
return Response('text')
class HTMLView1(APIView):
renderer_classes = (DocumentingHTMLRenderer, JSONRenderer)
def get(self, request, **kwargs):
return Response('text')
urlpatterns = patterns('',
url(r'^.*\.(?P<format>.+)$', MockView.as_view(renderer_classes=[RendererA, RendererB])),
url(r'^$', MockView.as_view(renderer_classes=[RendererA, RendererB])),
url(r'^jsonp/jsonrenderer$', MockGETView.as_view(renderer_classes=[JSONRenderer, JSONPRenderer])),
url(r'^jsonp/nojsonrenderer$', MockGETView.as_view(renderer_classes=[JSONPRenderer])),
url(r'^html$', HTMLView.as_view()),
url(r'^html1$', HTMLView1.as_view()),
url(r'^api', include('rest_framework.urls', namespace='rest_framework'))
)
class RendererEndToEndTests(TestCase):
"""
End-to-end testing of renderers using an RendererMixin on a generic view.
"""
urls = 'rest_framework.tests.renderers'
def test_default_renderer_serializes_content(self):
"""If the Accept header is not set the default renderer should serialize the response."""
resp = self.client.get('/')
self.assertEquals(resp['Content-Type'], RendererA.media_type)
self.assertEquals(resp.content, RENDERER_A_SERIALIZER(DUMMYCONTENT))
self.assertEquals(resp.status_code, DUMMYSTATUS)
def test_head_method_serializes_no_content(self):
"""No response must be included in HEAD requests."""
resp = self.client.head('/')
self.assertEquals(resp.status_code, DUMMYSTATUS)
self.assertEquals(resp['Content-Type'], RendererA.media_type)
self.assertEquals(resp.content, '')
def test_default_renderer_serializes_content_on_accept_any(self):
"""If the Accept header is set to */* the default renderer should serialize the response."""
resp = self.client.get('/', HTTP_ACCEPT='*/*')
self.assertEquals(resp['Content-Type'], RendererA.media_type)
self.assertEquals(resp.content, RENDERER_A_SERIALIZER(DUMMYCONTENT))
self.assertEquals(resp.status_code, DUMMYSTATUS)
def test_specified_renderer_serializes_content_default_case(self):
"""If the Accept header is set the specified renderer should serialize the response.
(In this case we check that works for the default renderer)"""
resp = self.client.get('/', HTTP_ACCEPT=RendererA.media_type)
self.assertEquals(resp['Content-Type'], RendererA.media_type)
self.assertEquals(resp.content, RENDERER_A_SERIALIZER(DUMMYCONTENT))
self.assertEquals(resp.status_code, DUMMYSTATUS)
def test_specified_renderer_serializes_content_non_default_case(self):
"""If the Accept header is set the specified renderer should serialize the response.
(In this case we check that works for a non-default renderer)"""
resp = self.client.get('/', HTTP_ACCEPT=RendererB.media_type)
self.assertEquals(resp['Content-Type'], RendererB.media_type)
self.assertEquals(resp.content, RENDERER_B_SERIALIZER(DUMMYCONTENT))
self.assertEquals(resp.status_code, DUMMYSTATUS)
def test_specified_renderer_serializes_content_on_accept_query(self):
"""The '_accept' query string should behave in the same way as the Accept header."""
resp = self.client.get('/?_accept=%s' % RendererB.media_type)
self.assertEquals(resp['Content-Type'], RendererB.media_type)
self.assertEquals(resp.content, RENDERER_B_SERIALIZER(DUMMYCONTENT))
self.assertEquals(resp.status_code, DUMMYSTATUS)
def test_unsatisfiable_accept_header_on_request_returns_406_status(self):
"""If the Accept header is unsatisfiable we should return a 406 Not Acceptable response."""
resp = self.client.get('/', HTTP_ACCEPT='foo/bar')
self.assertEquals(resp.status_code, status.HTTP_406_NOT_ACCEPTABLE)
def test_specified_renderer_serializes_content_on_format_query(self):
"""If a 'format' query is specified, the renderer with the matching
format attribute should serialize the response."""
resp = self.client.get('/?format=%s' % RendererB.format)
self.assertEquals(resp['Content-Type'], RendererB.media_type)
self.assertEquals(resp.content, RENDERER_B_SERIALIZER(DUMMYCONTENT))
self.assertEquals(resp.status_code, DUMMYSTATUS)
def test_specified_renderer_serializes_content_on_format_kwargs(self):
"""If a 'format' keyword arg is specified, the renderer with the matching
format attribute should serialize the response."""
resp = self.client.get('/something.formatb')
self.assertEquals(resp['Content-Type'], RendererB.media_type)
self.assertEquals(resp.content, RENDERER_B_SERIALIZER(DUMMYCONTENT))
self.assertEquals(resp.status_code, DUMMYSTATUS)
def test_specified_renderer_is_used_on_format_query_with_matching_accept(self):
"""If both a 'format' query and a matching Accept header specified,
the renderer with the matching format attribute should serialize the response."""
resp = self.client.get('/?format=%s' % RendererB.format,
HTTP_ACCEPT=RendererB.media_type)
self.assertEquals(resp['Content-Type'], RendererB.media_type)
self.assertEquals(resp.content, RENDERER_B_SERIALIZER(DUMMYCONTENT))
self.assertEquals(resp.status_code, DUMMYSTATUS)
_flat_repr = '{"foo": ["bar", "baz"]}'
_indented_repr = '{\n "foo": [\n "bar",\n "baz"\n ]\n}'
def strip_trailing_whitespace(content):
"""
Seems to be some inconsistencies re. trailing whitespace with
different versions of the json lib.
"""
return re.sub(' +\n', '\n', content)
class JSONRendererTests(TestCase):
"""
Tests specific to the JSON Renderer
"""
def test_without_content_type_args(self):
"""
Test basic JSON rendering.
"""
obj = {'foo': ['bar', 'baz']}
renderer = JSONRenderer(None)
content = renderer.render(obj, 'application/json')
# Fix failing test case which depends on version of JSON library.
self.assertEquals(content, _flat_repr)
def test_with_content_type_args(self):
"""
Test JSON rendering with additional content type arguments supplied.
"""
obj = {'foo': ['bar', 'baz']}
renderer = JSONRenderer(None)
content = renderer.render(obj, 'application/json; indent=2')
self.assertEquals(strip_trailing_whitespace(content), _indented_repr)
class JSONPRendererTests(TestCase):
"""
Tests specific to the JSONP Renderer
"""
urls = 'rest_framework.tests.renderers'
def test_without_callback_with_json_renderer(self):
"""
Test JSONP rendering with View JSON Renderer.
"""
resp = self.client.get('/jsonp/jsonrenderer',
HTTP_ACCEPT='application/javascript')
self.assertEquals(resp.status_code, 200)
self.assertEquals(resp['Content-Type'], 'application/javascript')
self.assertEquals(resp.content, 'callback(%s);' % _flat_repr)
def test_without_callback_without_json_renderer(self):
"""
Test JSONP rendering without View JSON Renderer.
"""
resp = self.client.get('/jsonp/nojsonrenderer',
HTTP_ACCEPT='application/javascript')
self.assertEquals(resp.status_code, 200)
self.assertEquals(resp['Content-Type'], 'application/javascript')
self.assertEquals(resp.content, 'callback(%s);' % _flat_repr)
def test_with_callback(self):
"""
Test JSONP rendering with callback function name.
"""
callback_func = 'myjsonpcallback'
resp = self.client.get('/jsonp/nojsonrenderer?callback=' + callback_func,
HTTP_ACCEPT='application/javascript')
self.assertEquals(resp.status_code, 200)
self.assertEquals(resp['Content-Type'], 'application/javascript')
self.assertEquals(resp.content, '%s(%s);' % (callback_func, _flat_repr))
if YAMLRenderer:
_yaml_repr = 'foo: [bar, baz]\n'
class YAMLRendererTests(TestCase):
"""
Tests specific to the JSON Renderer
"""
def test_render(self):
"""
Test basic YAML rendering.
"""
obj = {'foo': ['bar', 'baz']}
renderer = YAMLRenderer(None)
content = renderer.render(obj, 'application/yaml')
self.assertEquals(content, _yaml_repr)
def test_render_and_parse(self):
"""
Test rendering and then parsing returns the original object.
IE obj -> render -> parse -> obj.
"""
obj = {'foo': ['bar', 'baz']}
renderer = YAMLRenderer(None)
parser = YAMLParser()
content = renderer.render(obj, 'application/yaml')
data = parser.parse(StringIO(content))
self.assertEquals(obj, data)
class XMLRendererTestCase(TestCase):
"""
Tests specific to the XML Renderer
"""
_complex_data = {
"creation_date": datetime.datetime(2011, 12, 25, 12, 45, 00),
"name": "name",
"sub_data_list": [
{
"sub_id": 1,
"sub_name": "first"
},
{
"sub_id": 2,
"sub_name": "second"
}
]
}
def test_render_string(self):
"""
Test XML rendering.
"""
renderer = XMLRenderer(None)
content = renderer.render({'field': 'astring'}, 'application/xml')
self.assertXMLContains(content, '<field>astring</field>')
def test_render_integer(self):
"""
Test XML rendering.
"""
renderer = XMLRenderer(None)
content = renderer.render({'field': 111}, 'application/xml')
self.assertXMLContains(content, '<field>111</field>')
def test_render_datetime(self):
"""
Test XML rendering.
"""
renderer = XMLRenderer(None)
content = renderer.render({
'field': datetime.datetime(2011, 12, 25, 12, 45, 00)
}, 'application/xml')
self.assertXMLContains(content, '<field>2011-12-25 12:45:00</field>')
def test_render_float(self):
"""
Test XML rendering.
"""
renderer = XMLRenderer(None)
content = renderer.render({'field': 123.4}, 'application/xml')
self.assertXMLContains(content, '<field>123.4</field>')
def test_render_decimal(self):
"""
Test XML rendering.
"""
renderer = XMLRenderer(None)
content = renderer.render({'field': Decimal('111.2')}, 'application/xml')
self.assertXMLContains(content, '<field>111.2</field>')
def test_render_none(self):
"""
Test XML rendering.
"""
renderer = XMLRenderer(None)
content = renderer.render({'field': None}, 'application/xml')
self.assertXMLContains(content, '<field></field>')
def test_render_complex_data(self):
"""
Test XML rendering.
"""
renderer = XMLRenderer(None)
content = renderer.render(self._complex_data, 'application/xml')
self.assertXMLContains(content, '<sub_name>first</sub_name>')
self.assertXMLContains(content, '<sub_name>second</sub_name>')
def test_render_and_parse_complex_data(self):
"""
Test XML rendering.
"""
renderer = XMLRenderer(None)
content = StringIO(renderer.render(self._complex_data, 'application/xml'))
parser = XMLParser()
complex_data_out = parser.parse(content)
error_msg = "complex data differs!IN:\n %s \n\n OUT:\n %s" % (repr(self._complex_data), repr(complex_data_out))
self.assertEqual(self._complex_data, complex_data_out, error_msg)
def assertXMLContains(self, xml, string):
self.assertTrue(xml.startswith('<?xml version="1.0" encoding="utf-8"?>\n<root>'))
self.assertTrue(xml.endswith('</root>'))
self.assertTrue(string in xml, '%r not in %r' % (string, xml))

View File

@ -0,0 +1,252 @@
"""
Tests for content parsing, and form-overloaded content parsing.
"""
from django.conf.urls.defaults import patterns
from django.contrib.auth.models import User
from django.test import TestCase, Client
from rest_framework import status
from rest_framework.authentication import SessionAuthentication
from rest_framework.compat import RequestFactory
from rest_framework.parsers import (
FormParser,
MultiPartParser,
PlainTextParser,
)
from rest_framework.request import Request
from rest_framework.response import Response
from rest_framework.views import APIView
factory = RequestFactory()
class TestMethodOverloading(TestCase):
def test_method(self):
"""
Request methods should be same as underlying request.
"""
request = Request(factory.get('/'))
self.assertEqual(request.method, 'GET')
request = Request(factory.post('/'))
self.assertEqual(request.method, 'POST')
def test_overloaded_method(self):
"""
POST requests can be overloaded to another method by setting a
reserved form field
"""
request = Request(factory.post('/', {Request._METHOD_PARAM: 'DELETE'}))
self.assertEqual(request.method, 'DELETE')
class TestContentParsing(TestCase):
def test_standard_behaviour_determines_no_content_GET(self):
"""
Ensure request.DATA returns None for GET request with no content.
"""
request = Request(factory.get('/'))
self.assertEqual(request.DATA, None)
def test_standard_behaviour_determines_no_content_HEAD(self):
"""
Ensure request.DATA returns None for HEAD request.
"""
request = Request(factory.head('/'))
self.assertEqual(request.DATA, None)
def test_standard_behaviour_determines_form_content_POST(self):
"""
Ensure request.DATA returns content for POST request with form content.
"""
data = {'qwerty': 'uiop'}
request = Request(factory.post('/', data))
request.parser_classes = (FormParser, MultiPartParser)
self.assertEqual(request.DATA.items(), data.items())
def test_standard_behaviour_determines_non_form_content_POST(self):
"""
Ensure request.DATA returns content for POST request with
non-form content.
"""
content = 'qwerty'
content_type = 'text/plain'
request = Request(factory.post('/', content, content_type=content_type))
request.parser_classes = (PlainTextParser,)
self.assertEqual(request.DATA, content)
def test_standard_behaviour_determines_form_content_PUT(self):
"""
Ensure request.DATA returns content for PUT request with form content.
"""
data = {'qwerty': 'uiop'}
from django import VERSION
if VERSION >= (1, 5):
from django.test.client import MULTIPART_CONTENT, BOUNDARY, encode_multipart
request = Request(factory.put('/', encode_multipart(BOUNDARY, data),
content_type=MULTIPART_CONTENT))
else:
request = Request(factory.put('/', data))
request.parser_classes = (FormParser, MultiPartParser)
self.assertEqual(request.DATA.items(), data.items())
def test_standard_behaviour_determines_non_form_content_PUT(self):
"""
Ensure request.DATA returns content for PUT request with
non-form content.
"""
content = 'qwerty'
content_type = 'text/plain'
request = Request(factory.put('/', content, content_type=content_type))
request.parser_classes = (PlainTextParser, )
self.assertEqual(request.DATA, content)
def test_overloaded_behaviour_allows_content_tunnelling(self):
"""
Ensure request.DATA returns content for overloaded POST request.
"""
content = 'qwerty'
content_type = 'text/plain'
data = {
Request._CONTENT_PARAM: content,
Request._CONTENTTYPE_PARAM: content_type
}
request = Request(factory.post('/', data))
request.parser_classes = (PlainTextParser, )
self.assertEqual(request.DATA, content)
# def test_accessing_post_after_data_form(self):
# """
# Ensures request.POST can be accessed after request.DATA in
# form request.
# """
# data = {'qwerty': 'uiop'}
# request = factory.post('/', data=data)
# self.assertEqual(request.DATA.items(), data.items())
# self.assertEqual(request.POST.items(), data.items())
# def test_accessing_post_after_data_for_json(self):
# """
# Ensures request.POST can be accessed after request.DATA in
# json request.
# """
# data = {'qwerty': 'uiop'}
# content = json.dumps(data)
# content_type = 'application/json'
# parsers = (JSONParser, )
# request = factory.post('/', content, content_type=content_type,
# parsers=parsers)
# self.assertEqual(request.DATA.items(), data.items())
# self.assertEqual(request.POST.items(), [])
# def test_accessing_post_after_data_for_overloaded_json(self):
# """
# Ensures request.POST can be accessed after request.DATA in overloaded
# json request.
# """
# data = {'qwerty': 'uiop'}
# content = json.dumps(data)
# content_type = 'application/json'
# parsers = (JSONParser, )
# form_data = {Request._CONTENT_PARAM: content,
# Request._CONTENTTYPE_PARAM: content_type}
# request = factory.post('/', form_data, parsers=parsers)
# self.assertEqual(request.DATA.items(), data.items())
# self.assertEqual(request.POST.items(), form_data.items())
# def test_accessing_data_after_post_form(self):
# """
# Ensures request.DATA can be accessed after request.POST in
# form request.
# """
# data = {'qwerty': 'uiop'}
# parsers = (FormParser, MultiPartParser)
# request = factory.post('/', data, parsers=parsers)
# self.assertEqual(request.POST.items(), data.items())
# self.assertEqual(request.DATA.items(), data.items())
# def test_accessing_data_after_post_for_json(self):
# """
# Ensures request.DATA can be accessed after request.POST in
# json request.
# """
# data = {'qwerty': 'uiop'}
# content = json.dumps(data)
# content_type = 'application/json'
# parsers = (JSONParser, )
# request = factory.post('/', content, content_type=content_type,
# parsers=parsers)
# self.assertEqual(request.POST.items(), [])
# self.assertEqual(request.DATA.items(), data.items())
# def test_accessing_data_after_post_for_overloaded_json(self):
# """
# Ensures request.DATA can be accessed after request.POST in overloaded
# json request
# """
# data = {'qwerty': 'uiop'}
# content = json.dumps(data)
# content_type = 'application/json'
# parsers = (JSONParser, )
# form_data = {Request._CONTENT_PARAM: content,
# Request._CONTENTTYPE_PARAM: content_type}
# request = factory.post('/', form_data, parsers=parsers)
# self.assertEqual(request.POST.items(), form_data.items())
# self.assertEqual(request.DATA.items(), data.items())
class MockView(APIView):
authentication_classes = (SessionAuthentication,)
def post(self, request):
if request.POST.get('example') is not None:
return Response(status=status.HTTP_200_OK)
return Response(status=status.INTERNAL_SERVER_ERROR)
urlpatterns = patterns('',
(r'^$', MockView.as_view()),
)
class TestContentParsingWithAuthentication(TestCase):
urls = 'rest_framework.tests.request'
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_user_logged_in_authentication_has_POST_when_not_logged_in(self):
"""
Ensures request.POST exists after SessionAuthentication when user
doesn't log in.
"""
content = {'example': 'example'}
response = self.client.post('/', content)
self.assertEqual(status.HTTP_200_OK, response.status_code)
response = self.csrf_client.post('/', content)
self.assertEqual(status.HTTP_200_OK, response.status_code)
# def test_user_logged_in_authentication_has_post_when_logged_in(self):
# """Ensures request.POST exists after UserLoggedInAuthentication when user does log in"""
# self.client.login(username='john', password='password')
# self.csrf_client.login(username='john', password='password')
# content = {'example': 'example'}
# response = self.client.post('/', content)
# self.assertEqual(status.OK, response.status_code, "POST data is malformed")
# response = self.csrf_client.post('/', content)
# self.assertEqual(status.OK, response.status_code, "POST data is malformed")

View File

@ -0,0 +1,177 @@
import unittest
from django.conf.urls.defaults import patterns, url, include
from django.test import TestCase
from rest_framework.response import Response
from rest_framework.views import APIView
from rest_framework import status
from rest_framework.renderers import (
BaseRenderer,
JSONRenderer,
DocumentingHTMLRenderer
)
class MockPickleRenderer(BaseRenderer):
media_type = 'application/pickle'
class MockJsonRenderer(BaseRenderer):
media_type = 'application/json'
DUMMYSTATUS = status.HTTP_200_OK
DUMMYCONTENT = 'dummycontent'
RENDERER_A_SERIALIZER = lambda x: 'Renderer A: %s' % x
RENDERER_B_SERIALIZER = lambda x: 'Renderer B: %s' % x
class RendererA(BaseRenderer):
media_type = 'mock/renderera'
format = "formata"
def render(self, obj=None, media_type=None):
return RENDERER_A_SERIALIZER(obj)
class RendererB(BaseRenderer):
media_type = 'mock/rendererb'
format = "formatb"
def render(self, obj=None, media_type=None):
return RENDERER_B_SERIALIZER(obj)
class MockView(APIView):
renderer_classes = (RendererA, RendererB)
def get(self, request, **kwargs):
return Response(DUMMYCONTENT, status=DUMMYSTATUS)
class HTMLView(APIView):
renderer_classes = (DocumentingHTMLRenderer, )
def get(self, request, **kwargs):
return Response('text')
class HTMLView1(APIView):
renderer_classes = (DocumentingHTMLRenderer, JSONRenderer)
def get(self, request, **kwargs):
return Response('text')
urlpatterns = patterns('',
url(r'^.*\.(?P<format>.+)$', MockView.as_view(renderer_classes=[RendererA, RendererB])),
url(r'^$', MockView.as_view(renderer_classes=[RendererA, RendererB])),
url(r'^html$', HTMLView.as_view()),
url(r'^html1$', HTMLView1.as_view()),
url(r'^restframework', include('rest_framework.urls', namespace='rest_framework'))
)
# TODO: Clean tests bellow - remove duplicates with above, better unit testing, ...
class RendererIntegrationTests(TestCase):
"""
End-to-end testing of renderers using an ResponseMixin on a generic view.
"""
urls = 'rest_framework.tests.response'
def test_default_renderer_serializes_content(self):
"""If the Accept header is not set the default renderer should serialize the response."""
resp = self.client.get('/')
self.assertEquals(resp['Content-Type'], RendererA.media_type)
self.assertEquals(resp.content, RENDERER_A_SERIALIZER(DUMMYCONTENT))
self.assertEquals(resp.status_code, DUMMYSTATUS)
def test_head_method_serializes_no_content(self):
"""No response must be included in HEAD requests."""
resp = self.client.head('/')
self.assertEquals(resp.status_code, DUMMYSTATUS)
self.assertEquals(resp['Content-Type'], RendererA.media_type)
self.assertEquals(resp.content, '')
def test_default_renderer_serializes_content_on_accept_any(self):
"""If the Accept header is set to */* the default renderer should serialize the response."""
resp = self.client.get('/', HTTP_ACCEPT='*/*')
self.assertEquals(resp['Content-Type'], RendererA.media_type)
self.assertEquals(resp.content, RENDERER_A_SERIALIZER(DUMMYCONTENT))
self.assertEquals(resp.status_code, DUMMYSTATUS)
def test_specified_renderer_serializes_content_default_case(self):
"""If the Accept header is set the specified renderer should serialize the response.
(In this case we check that works for the default renderer)"""
resp = self.client.get('/', HTTP_ACCEPT=RendererA.media_type)
self.assertEquals(resp['Content-Type'], RendererA.media_type)
self.assertEquals(resp.content, RENDERER_A_SERIALIZER(DUMMYCONTENT))
self.assertEquals(resp.status_code, DUMMYSTATUS)
def test_specified_renderer_serializes_content_non_default_case(self):
"""If the Accept header is set the specified renderer should serialize the response.
(In this case we check that works for a non-default renderer)"""
resp = self.client.get('/', HTTP_ACCEPT=RendererB.media_type)
self.assertEquals(resp['Content-Type'], RendererB.media_type)
self.assertEquals(resp.content, RENDERER_B_SERIALIZER(DUMMYCONTENT))
self.assertEquals(resp.status_code, DUMMYSTATUS)
def test_specified_renderer_serializes_content_on_accept_query(self):
"""The '_accept' query string should behave in the same way as the Accept header."""
resp = self.client.get('/?_accept=%s' % RendererB.media_type)
self.assertEquals(resp['Content-Type'], RendererB.media_type)
self.assertEquals(resp.content, RENDERER_B_SERIALIZER(DUMMYCONTENT))
self.assertEquals(resp.status_code, DUMMYSTATUS)
@unittest.skip('can\'t pass because view is a simple Django view and response is an ImmediateResponse')
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, status.HTTP_406_NOT_ACCEPTABLE)
def test_specified_renderer_serializes_content_on_format_query(self):
"""If a 'format' query is specified, the renderer with the matching
format attribute should serialize the response."""
resp = self.client.get('/?format=%s' % RendererB.format)
self.assertEquals(resp['Content-Type'], RendererB.media_type)
self.assertEquals(resp.content, RENDERER_B_SERIALIZER(DUMMYCONTENT))
self.assertEquals(resp.status_code, DUMMYSTATUS)
def test_specified_renderer_serializes_content_on_format_kwargs(self):
"""If a 'format' keyword arg is specified, the renderer with the matching
format attribute should serialize the response."""
resp = self.client.get('/something.formatb')
self.assertEquals(resp['Content-Type'], RendererB.media_type)
self.assertEquals(resp.content, RENDERER_B_SERIALIZER(DUMMYCONTENT))
self.assertEquals(resp.status_code, DUMMYSTATUS)
def test_specified_renderer_is_used_on_format_query_with_matching_accept(self):
"""If both a 'format' query and a matching Accept header specified,
the renderer with the matching format attribute should serialize the response."""
resp = self.client.get('/?format=%s' % RendererB.format,
HTTP_ACCEPT=RendererB.media_type)
self.assertEquals(resp['Content-Type'], RendererB.media_type)
self.assertEquals(resp.content, RENDERER_B_SERIALIZER(DUMMYCONTENT))
self.assertEquals(resp.status_code, DUMMYSTATUS)
class Issue122Tests(TestCase):
"""
Tests that covers #122.
"""
urls = 'rest_framework.tests.response'
def test_only_html_renderer(self):
"""
Test if no infinite recursion occurs.
"""
self.client.get('/html')
def test_html_renderer_is_first(self):
"""
Test if no infinite recursion occurs.
"""
self.client.get('/html1')

View File

@ -0,0 +1,35 @@
from django.conf.urls.defaults import patterns, url
from django.test import TestCase
from django.utils import simplejson as json
from rest_framework.renderers import JSONRenderer
from rest_framework.reverse import reverse
from rest_framework.views import APIView
from rest_framework.response import Response
class MyView(APIView):
"""
Mock resource which simply returns a URL, so that we can ensure
that reversed URLs are fully qualified.
"""
renderers = (JSONRenderer, )
def get(self, request):
return Response(reverse('myview', request=request))
urlpatterns = patterns('',
url(r'^myview$', MyView.as_view(), name='myview'),
)
class ReverseTests(TestCase):
"""
Tests for fully qualifed URLs when using `reverse`.
"""
urls = 'rest_framework.tests.reverse'
def test_reversed_urls_are_fully_qualified(self):
response = self.client.get('/myview')
self.assertEqual(json.loads(response.content), 'http://testserver/myview')

View File

@ -0,0 +1,117 @@
import datetime
from django.test import TestCase
from rest_framework import serializers
class Comment(object):
def __init__(self, email, content, created):
self.email = email
self.content = content
self.created = created or datetime.datetime.now()
def __eq__(self, other):
return all([getattr(self, attr) == getattr(other, attr)
for attr in ('email', 'content', 'created')])
class CommentSerializer(serializers.Serializer):
email = serializers.EmailField()
content = serializers.CharField(max_length=1000)
created = serializers.DateTimeField()
def restore_object(self, data, instance=None):
if instance is None:
return Comment(**data)
for key, val in data.items():
setattr(instance, key, val)
return instance
class BasicTests(TestCase):
def setUp(self):
self.comment = Comment(
'tom@example.com',
'Happy new year!',
datetime.datetime(2012, 1, 1)
)
self.data = {
'email': 'tom@example.com',
'content': 'Happy new year!',
'created': datetime.datetime(2012, 1, 1)
}
def test_empty(self):
serializer = CommentSerializer()
expected = {
'email': '',
'content': '',
'created': None
}
self.assertEquals(serializer.data, expected)
def test_serialization(self):
serializer = CommentSerializer(instance=self.comment)
expected = self.data
self.assertEquals(serializer.data, expected)
def test_deserialization_for_create(self):
serializer = CommentSerializer(self.data)
expected = self.comment
self.assertEquals(serializer.is_valid(), True)
self.assertEquals(serializer.object, expected)
self.assertFalse(serializer.object is expected)
def test_deserialization_for_update(self):
serializer = CommentSerializer(self.data, instance=self.comment)
expected = self.comment
self.assertEquals(serializer.is_valid(), True)
self.assertEquals(serializer.object, expected)
self.assertTrue(serializer.object is expected)
class ValidationTests(TestCase):
def setUp(self):
self.comment = Comment(
'tom@example.com',
'Happy new year!',
datetime.datetime(2012, 1, 1)
)
self.data = {
'email': 'tom@example.com',
'content': 'x' * 1001,
'created': datetime.datetime(2012, 1, 1)
}
def test_deserialization_for_create(self):
serializer = CommentSerializer(self.data)
self.assertEquals(serializer.is_valid(), False)
self.assertEquals(serializer.errors, {'content': [u'Ensure this value has at most 1000 characters (it has 1001).']})
def test_deserialization_for_update(self):
serializer = CommentSerializer(self.data, instance=self.comment)
self.assertEquals(serializer.is_valid(), False)
self.assertEquals(serializer.errors, {'content': [u'Ensure this value has at most 1000 characters (it has 1001).']})
class MetadataTests(TestCase):
# def setUp(self):
# self.comment = Comment(
# 'tomchristie',
# 'Happy new year!',
# datetime.datetime(2012, 1, 1)
# )
# self.data = {
# 'email': 'tomchristie',
# 'content': 'Happy new year!',
# 'created': datetime.datetime(2012, 1, 1)
# }
def test_empty(self):
serializer = CommentSerializer()
expected = {
'email': serializers.CharField,
'content': serializers.CharField,
'created': serializers.DateTimeField
}
for field_name, field in expected.items():
self.assertTrue(isinstance(serializer.data.fields[field_name], field))

View File

@ -0,0 +1,12 @@
"""Tests for the status module"""
from django.test import TestCase
from rest_framework import status
class TestStatus(TestCase):
"""Simple sanity test to check the status module"""
def test_status(self):
"""Ensure the status module is present and correct."""
self.assertEquals(200, status.HTTP_200_OK)
self.assertEquals(404, status.HTTP_404_NOT_FOUND)

View File

@ -0,0 +1,63 @@
# http://djangosnippets.org/snippets/1011/
from django.conf import settings
from django.core.management import call_command
from django.db.models import loading
from django.test import TestCase
NO_SETTING = ('!', None)
class TestSettingsManager(object):
"""
A class which can modify some Django settings temporarily for a
test and then revert them to their original values later.
Automatically handles resyncing the DB if INSTALLED_APPS is
modified.
"""
def __init__(self):
self._original_settings = {}
def set(self, **kwargs):
for k,v in kwargs.iteritems():
self._original_settings.setdefault(k, getattr(settings, k,
NO_SETTING))
setattr(settings, k, v)
if 'INSTALLED_APPS' in kwargs:
self.syncdb()
def syncdb(self):
loading.cache.loaded = False
call_command('syncdb', verbosity=0)
def revert(self):
for k,v in self._original_settings.iteritems():
if v == NO_SETTING:
delattr(settings, k)
else:
setattr(settings, k, v)
if 'INSTALLED_APPS' in self._original_settings:
self.syncdb()
self._original_settings = {}
class SettingsTestCase(TestCase):
"""
A subclass of the Django TestCase with a settings_manager
attribute which is an instance of TestSettingsManager.
Comes with a tearDown() method that calls
self.settings_manager.revert().
"""
def __init__(self, *args, **kwargs):
super(SettingsTestCase, self).__init__(*args, **kwargs)
self.settings_manager = TestSettingsManager()
def tearDown(self):
self.settings_manager.revert()
class TestModelsTestCase(SettingsTestCase):
def setUp(self, *args, **kwargs):
installed_apps = tuple(settings.INSTALLED_APPS) + ('rest_framework.tests',)
self.settings_manager.set(INSTALLED_APPS=installed_apps)

View File

@ -0,0 +1,144 @@
"""
Tests for the throttling implementations in the permissions module.
"""
from django.test import TestCase
from django.contrib.auth.models import User
from django.core.cache import cache
from rest_framework.compat import RequestFactory
from rest_framework.views import APIView
from rest_framework.throttling import UserRateThrottle
from rest_framework.response import Response
class User3SecRateThrottle(UserRateThrottle):
rate = '3/sec'
scope = 'seconds'
class User3MinRateThrottle(UserRateThrottle):
rate = '3/min'
scope = 'minutes'
class MockView(APIView):
throttle_classes = (User3SecRateThrottle,)
def get(self, request):
return Response('foo')
class MockView_MinuteThrottling(APIView):
throttle_classes = (User3MinRateThrottle,)
def get(self, request):
return Response('foo')
class ThrottlingTests(TestCase):
urls = 'rest_framework.tests.throttling'
def setUp(self):
"""
Reset the cache so that no throttles will be active
"""
cache.clear()
self.factory = RequestFactory()
def test_requests_are_throttled(self):
"""
Ensure request rate is limited
"""
request = self.factory.get('/')
for dummy in range(4):
response = MockView.as_view()(request)
self.assertEqual(429, response.status_code)
def set_throttle_timer(self, view, value):
"""
Explicitly set the timer, overriding time.time()
"""
view.throttle_classes[0].timer = lambda self: value
def test_request_throttling_expires(self):
"""
Ensure request rate is limited for a limited duration only
"""
self.set_throttle_timer(MockView, 0)
request = self.factory.get('/')
for dummy in range(4):
response = MockView.as_view()(request)
self.assertEqual(429, response.status_code)
# Advance the timer by one second
self.set_throttle_timer(MockView, 1)
response = MockView.as_view()(request)
self.assertEqual(200, response.status_code)
def ensure_is_throttled(self, view, expect):
request = self.factory.get('/')
request.user = User.objects.create(username='a')
for dummy in range(3):
view.as_view()(request)
request.user = User.objects.create(username='b')
response = view.as_view()(request)
self.assertEqual(expect, response.status_code)
def test_request_throttling_is_per_user(self):
"""
Ensure request rate is only limited per user, not globally for
PerUserThrottles
"""
self.ensure_is_throttled(MockView, 200)
def ensure_response_header_contains_proper_throttle_field(self, view, expected_headers):
"""
Ensure the response returns an X-Throttle field with status and next attributes
set properly.
"""
request = self.factory.get('/')
for timer, expect in expected_headers:
self.set_throttle_timer(view, timer)
response = view.as_view()(request)
if expect is not None:
self.assertEquals(response['X-Throttle-Wait-Seconds'], expect)
else:
self.assertFalse('X-Throttle-Wait-Seconds' in response.headers)
def test_seconds_fields(self):
"""
Ensure for second based throttles.
"""
self.ensure_response_header_contains_proper_throttle_field(MockView,
((0, None),
(0, None),
(0, None),
(0, '1')
))
def test_minutes_fields(self):
"""
Ensure for minute based throttles.
"""
self.ensure_response_header_contains_proper_throttle_field(MockView_MinuteThrottling,
((0, None),
(0, None),
(0, None),
(0, '60')
))
def test_next_rate_remains_constant_if_followed(self):
"""
If a client follows the recommended next request rate,
the throttling rate should stay constant.
"""
self.ensure_response_header_contains_proper_throttle_field(MockView_MinuteThrottling,
((0, None),
(20, None),
(40, None),
(60, None),
(80, None)
))

View File

@ -0,0 +1,329 @@
# from django import forms
# from django.db import models
# from django.test import TestCase
# from rest_framework.response import ImmediateResponse
# from rest_framework.views import View
# class TestDisabledValidations(TestCase):
# """Tests on FormValidator with validation disabled by setting form to None"""
# def test_disabled_form_validator_returns_content_unchanged(self):
# """If the view's form attribute is None then FormValidator(view).validate_request(content, None)
# should just return the content unmodified."""
# class DisabledFormResource(FormResource):
# form = None
# class MockView(View):
# resource = DisabledFormResource
# view = MockView()
# content = {'qwerty': 'uiop'}
# self.assertEqual(FormResource(view).validate_request(content, None), content)
# def test_disabled_form_validator_get_bound_form_returns_none(self):
# """If the view's form attribute is None on then
# FormValidator(view).get_bound_form(content) should just return None."""
# class DisabledFormResource(FormResource):
# form = None
# class MockView(View):
# resource = DisabledFormResource
# view = MockView()
# content = {'qwerty': 'uiop'}
# self.assertEqual(FormResource(view).get_bound_form(content), None)
# def test_disabled_model_form_validator_returns_content_unchanged(self):
# """If the view's form is None and does not have a Resource with a model set then
# ModelFormValidator(view).validate_request(content, None) should just return the content unmodified."""
# class DisabledModelFormView(View):
# resource = ModelResource
# view = DisabledModelFormView()
# content = {'qwerty': 'uiop'}
# self.assertEqual(ModelResource(view).get_bound_form(content), None)
# 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 DisabledModelFormView(View):
# resource = ModelResource
# view = DisabledModelFormView()
# content = {'qwerty': 'uiop'}
# self.assertEqual(ModelResource(view).get_bound_form(content), None)
# 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_non_field_error_returns_appropriate_message(self):
# """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)
# ERROR_TEXT = 'You may not supply both field1 and field2'
# def clean(self):
# if 'field1' in self.cleaned_data and 'field2' in self.cleaned_data:
# raise forms.ValidationError(self.ERROR_TEXT)
# return self.cleaned_data
# class MockResource(FormResource):
# form = MockForm
# class MockView(View):
# pass
# view = MockView()
# content = {'field1': 'example1', 'field2': 'example2'}
# try:
# MockResource(view).validate_request(content, None)
# except ImmediateResponse, exc:
# response = exc.response
# self.assertEqual(response.raw_content, {'errors': [MockForm.ERROR_TEXT]})
# else:
# self.fail('ImmediateResponse was not raised')
# 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.
# (ModelFormValidator should behave as FormValidator if a form is set rather than relying on the default ModelForm)"""
# def setUp(self):
# class MockForm(forms.Form):
# qwerty = forms.CharField(required=True)
# class MockFormResource(FormResource):
# form = MockForm
# class MockModelResource(ModelResource):
# form = MockForm
# class MockFormView(View):
# resource = MockFormResource
# class MockModelFormView(View):
# resource = MockModelResource
# self.MockFormResource = MockFormResource
# self.MockModelResource = MockModelResource
# self.MockFormView = MockFormView
# self.MockModelFormView = MockModelFormView
# 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_request(content, None), content)
# def validation_failure_raises_response_exception(self, validator):
# """If form validation fails a ResourceException 400 (Bad Request) should be raised."""
# content = {}
# self.assertRaises(ImmediateResponse, validator.validate_request, content, None)
# 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(ImmediateResponse, validator.validate_request, content, None)
# 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, None, allowed_extra_fields=('extra',))
# def validation_allows_unknown_fields_if_explicitly_allowed(self, validator):
# """If we set ``unknown_form_fields`` on the form resource, then don't
# raise errors on unexpected request data"""
# content = {'qwerty': 'uiop', 'extra': 'extra'}
# validator.allow_unknown_form_fields = True
# self.assertEqual({'qwerty': u'uiop'},
# validator.validate_request(content, None),
# "Resource didn't accept unknown fields.")
# validator.allow_unknown_form_fields = False
# 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, None, 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_request(content, None)
# except ImmediateResponse, exc:
# response = exc.response
# self.assertEqual(response.raw_content, {'field_errors': {'qwerty': ['This field is required.']}})
# else:
# self.fail('ResourceException was not raised')
# 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_request(content, None)
# except ImmediateResponse, exc:
# response = exc.response
# self.assertEqual(response.raw_content, {'field_errors': {'qwerty': ['This field is required.']}})
# else:
# self.fail('ResourceException was not raised')
# 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_request(content, None)
# except ImmediateResponse, exc:
# response = exc.response
# self.assertEqual(response.raw_content, {'field_errors': {'extra': ['This field does not exist.']}})
# else:
# self.fail('ResourceException was not raised')
# 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_request(content, None)
# except ImmediateResponse, exc:
# response = exc.response
# self.assertEqual(response.raw_content, {'field_errors': {'qwerty': ['This field is required.'],
# 'extra': ['This field does not exist.']}})
# else:
# self.fail('ResourceException was not raised')
# # Tests on FormResource
# def test_form_validation_returns_content_unchanged_if_already_valid_and_clean(self):
# validator = self.MockFormResource(self.MockFormView())
# self.validation_returns_content_unchanged_if_already_valid_and_clean(validator)
# def test_form_validation_failure_raises_response_exception(self):
# validator = self.MockFormResource(self.MockFormView())
# self.validation_failure_raises_response_exception(validator)
# def test_validation_does_not_allow_extra_fields_by_default(self):
# validator = self.MockFormResource(self.MockFormView())
# self.validation_does_not_allow_extra_fields_by_default(validator)
# def test_validation_allows_extra_fields_if_explicitly_set(self):
# validator = self.MockFormResource(self.MockFormView())
# self.validation_allows_extra_fields_if_explicitly_set(validator)
# def test_validation_allows_unknown_fields_if_explicitly_allowed(self):
# validator = self.MockFormResource(self.MockFormView())
# self.validation_allows_unknown_fields_if_explicitly_allowed(validator)
# def test_validation_does_not_require_extra_fields_if_explicitly_set(self):
# validator = self.MockFormResource(self.MockFormView())
# self.validation_does_not_require_extra_fields_if_explicitly_set(validator)
# def test_validation_failed_due_to_no_content_returns_appropriate_message(self):
# validator = self.MockFormResource(self.MockFormView())
# self.validation_failed_due_to_no_content_returns_appropriate_message(validator)
# def test_validation_failed_due_to_field_error_returns_appropriate_message(self):
# validator = self.MockFormResource(self.MockFormView())
# self.validation_failed_due_to_field_error_returns_appropriate_message(validator)
# def test_validation_failed_due_to_invalid_field_returns_appropriate_message(self):
# validator = self.MockFormResource(self.MockFormView())
# self.validation_failed_due_to_invalid_field_returns_appropriate_message(validator)
# def test_validation_failed_due_to_multiple_errors_returns_appropriate_message(self):
# validator = self.MockFormResource(self.MockFormView())
# self.validation_failed_due_to_multiple_errors_returns_appropriate_message(validator)
# # Same tests on ModelResource
# def test_modelform_validation_returns_content_unchanged_if_already_valid_and_clean(self):
# validator = self.MockModelResource(self.MockModelFormView())
# self.validation_returns_content_unchanged_if_already_valid_and_clean(validator)
# def test_modelform_validation_failure_raises_response_exception(self):
# validator = self.MockModelResource(self.MockModelFormView())
# self.validation_failure_raises_response_exception(validator)
# def test_modelform_validation_does_not_allow_extra_fields_by_default(self):
# validator = self.MockModelResource(self.MockModelFormView())
# self.validation_does_not_allow_extra_fields_by_default(validator)
# def test_modelform_validation_allows_extra_fields_if_explicitly_set(self):
# validator = self.MockModelResource(self.MockModelFormView())
# self.validation_allows_extra_fields_if_explicitly_set(validator)
# def test_modelform_validation_does_not_require_extra_fields_if_explicitly_set(self):
# validator = self.MockModelResource(self.MockModelFormView())
# self.validation_does_not_require_extra_fields_if_explicitly_set(validator)
# def test_modelform_validation_failed_due_to_no_content_returns_appropriate_message(self):
# validator = self.MockModelResource(self.MockModelFormView())
# self.validation_failed_due_to_no_content_returns_appropriate_message(validator)
# def test_modelform_validation_failed_due_to_field_error_returns_appropriate_message(self):
# validator = self.MockModelResource(self.MockModelFormView())
# self.validation_failed_due_to_field_error_returns_appropriate_message(validator)
# def test_modelform_validation_failed_due_to_invalid_field_returns_appropriate_message(self):
# validator = self.MockModelResource(self.MockModelFormView())
# self.validation_failed_due_to_invalid_field_returns_appropriate_message(validator)
# def test_modelform_validation_failed_due_to_multiple_errors_returns_appropriate_message(self):
# validator = self.MockModelResource(self.MockModelFormView())
# self.validation_failed_due_to_multiple_errors_returns_appropriate_message(validator)
# 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 MockResource(ModelResource):
# model = MockModel
# class MockView(View):
# resource = MockResource
# self.validator = MockResource(MockView)
# 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.validator.validate_request(content, None), 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.validator.validate_request(content, None), 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(ImmediateResponse, self.validator.validate_request, content, None)
# 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(ImmediateResponse, self.validator.validate_request, content, None)
# 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.validator.validate_request(content, None)
# def test_model_form_validator_uses_model_forms(self):
# self.assertTrue(isinstance(self.validator.get_bound_form(), forms.ModelForm))

View File

@ -0,0 +1,128 @@
# from django.core.urlresolvers import reverse
# from django.conf.urls.defaults import patterns, url, include
# from django.http import HttpResponse
# from django.test import TestCase
# from django.utils import simplejson as json
# from rest_framework.views import View
# class MockView(View):
# """This is a basic mock view"""
# pass
# class MockViewFinal(View):
# """View with final() override"""
# def final(self, request, response, *args, **kwargs):
# return HttpResponse('{"test": "passed"}', content_type="application/json")
# # class ResourceMockView(View):
# # """This is a resource-based mock view"""
# # class MockForm(forms.Form):
# # foo = forms.BooleanField(required=False)
# # bar = forms.IntegerField(help_text='Must be an integer.')
# # baz = forms.CharField(max_length=32)
# # form = MockForm
# # class MockResource(ModelResource):
# # """This is a mock model-based resource"""
# # class MockResourceModel(models.Model):
# # foo = models.BooleanField()
# # bar = models.IntegerField(help_text='Must be an integer.')
# # baz = models.CharField(max_length=32, help_text='Free text. Max length 32 chars.')
# # model = MockResourceModel
# # fields = ('foo', 'bar', 'baz')
# urlpatterns = patterns('',
# url(r'^mock/$', MockView.as_view()),
# url(r'^mock/final/$', MockViewFinal.as_view()),
# # url(r'^resourcemock/$', ResourceMockView.as_view()),
# # url(r'^model/$', ListOrCreateModelView.as_view(resource=MockResource)),
# # url(r'^model/(?P<pk>[^/]+)/$', InstanceModelView.as_view(resource=MockResource)),
# url(r'^restframework/', include('rest_framework.urls', namespace='rest_framework')),
# )
# class BaseViewTests(TestCase):
# """Test the base view class of rest_framework"""
# urls = 'rest_framework.tests.views'
# def test_view_call_final(self):
# response = self.client.options('/mock/final/')
# self.assertEqual(response['Content-Type'].split(';')[0], "application/json")
# data = json.loads(response.content)
# self.assertEqual(data['test'], 'passed')
# def test_options_method_simple_view(self):
# response = self.client.options('/mock/')
# self._verify_options_response(response,
# name='Mock',
# description='This is a basic mock view')
# def test_options_method_resource_view(self):
# response = self.client.options('/resourcemock/')
# self._verify_options_response(response,
# name='Resource Mock',
# description='This is a resource-based mock view',
# fields={'foo': 'BooleanField',
# 'bar': 'IntegerField',
# 'baz': 'CharField',
# })
# def test_options_method_model_resource_list_view(self):
# response = self.client.options('/model/')
# self._verify_options_response(response,
# name='Mock List',
# description='This is a mock model-based resource',
# fields={'foo': 'BooleanField',
# 'bar': 'IntegerField',
# 'baz': 'CharField',
# })
# def test_options_method_model_resource_detail_view(self):
# response = self.client.options('/model/0/')
# self._verify_options_response(response,
# name='Mock Instance',
# description='This is a mock model-based resource',
# fields={'foo': 'BooleanField',
# 'bar': 'IntegerField',
# 'baz': 'CharField',
# })
# def _verify_options_response(self, response, name, description, fields=None, status=200,
# mime_type='application/json'):
# self.assertEqual(response.status_code, status)
# self.assertEqual(response['Content-Type'].split(';')[0], mime_type)
# data = json.loads(response.content)
# self.assertTrue('application/json' in data['renders'])
# self.assertEqual(name, data['name'])
# self.assertEqual(description, data['description'])
# if fields is None:
# self.assertFalse(hasattr(data, 'fields'))
# else:
# self.assertEqual(data['fields'], fields)
# class ExtraViewsTests(TestCase):
# """Test the extra views rest_framework provides"""
# urls = 'rest_framework.tests.views'
# def test_login_view(self):
# """Ensure the login view exists"""
# response = self.client.get(reverse('rest_framework: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(reverse('rest_framework:logout'))
# self.assertEqual(response.status_code, 200)
# self.assertEqual(response['Content-Type'].split(';')[0], 'text/html')

View File

@ -0,0 +1,217 @@
from django.core.cache import cache
from rest_framework.settings import api_settings
import time
class BaseThrottle(object):
"""
Rate throttling of requests.
"""
def __init__(self, view=None):
"""
All throttles hold a reference to the instantiating view.
"""
self.view = view
def allow_request(self, request):
"""
Return `True` if the request should be allowed, `False` otherwise.
"""
raise NotImplementedError('.allow_request() must be overridden')
def wait(self):
"""
Optionally, return a recommeded number of seconds to wait before
the next request.
"""
return None
class SimpleRateThottle(BaseThrottle):
"""
A simple cache implementation, that only requires `.get_cache_key()`
to be overridden.
The rate (requests / seconds) is set by a :attr:`throttle` attribute
on the :class:`.View` class. The attribute is a string of the form 'number of
requests/period'.
Period should be one of: ('s', 'sec', 'm', 'min', 'h', 'hour', 'd', 'day')
Previous request information used for throttling is stored in the cache.
"""
timer = time.time
settings = api_settings
cache_format = 'throtte_%(scope)s_%(ident)s'
scope = None
def __init__(self, view):
super(SimpleRateThottle, self).__init__(view)
rate = self.get_rate_description()
self.num_requests, self.duration = self.parse_rate_description(rate)
def get_cache_key(self, request):
"""
Should return a unique cache-key which can be used for throttling.
Must be overridden.
May return `None` if the request should not be throttled.
"""
raise NotImplementedError('.get_cache_key() must be overridden')
def get_rate_description(self):
"""
Determine the string representation of the allowed request rate.
"""
try:
return self.rate
except AttributeError:
return self.settings.DEFAULT_THROTTLE_RATES.get(self.scope)
def parse_rate_description(self, rate):
"""
Given the request rate string, return a two tuple of:
<allowed number of requests>, <period of time in seconds>
"""
assert rate, "No throttle rate set for '%s'" % self.__class__.__name__
num, period = rate.split('/')
num_requests = int(num)
duration = {'s': 1, 'm': 60, 'h': 3600, 'd': 86400}[period[0]]
return (num_requests, duration)
def allow_request(self, request):
"""
Implement the check to see if the request should be throttled.
On success calls `throttle_success`.
On failure calls `throttle_failure`.
"""
self.key = self.get_cache_key(request)
self.history = cache.get(self.key, [])
self.now = self.timer()
# Drop any requests from the history which have now passed the
# throttle duration
while self.history and self.history[-1] <= self.now - self.duration:
self.history.pop()
if len(self.history) >= self.num_requests:
return self.throttle_failure()
return self.throttle_success()
def throttle_success(self):
"""
Inserts the current request's timestamp along with the key
into the cache.
"""
self.history.insert(0, self.now)
cache.set(self.key, self.history, self.duration)
return True
def throttle_failure(self):
"""
Called when a request to the API has failed due to throttling.
"""
return False
def wait(self):
"""
Returns the recommended next request time in seconds.
"""
if self.history:
remaining_duration = self.duration - (self.now - self.history[-1])
else:
remaining_duration = self.duration
available_requests = self.num_requests - len(self.history) + 1
return remaining_duration / float(available_requests)
class AnonRateThrottle(SimpleRateThottle):
"""
Limits the rate of API calls that may be made by a anonymous users.
The IP address of the request will be used as the unqiue cache key.
"""
scope = 'anon'
def get_cache_key(self, request):
if request.user.is_authenticated():
return None # Only throttle unauthenticated requests.
ident = request.META.get('REMOTE_ADDR', None)
return self.cache_format % {
'scope': self.scope,
'ident': ident
}
class UserRateThrottle(SimpleRateThottle):
"""
Limits the rate of API calls that may be made by a given user.
The user id will be used as a unique cache key if the user is
authenticated. For anonymous requests, the IP address of the request will
be used.
"""
scope = 'user'
def get_cache_key(self, request):
if request.user.is_authenticated():
ident = request.user.id
else:
ident = request.META.get('REMOTE_ADDR', None)
return self.cache_format % {
'scope': self.scope,
'ident': ident
}
class ScopedRateThrottle(SimpleRateThottle):
"""
Limits the rate of API calls by different amounts for various parts of
the API. Any view that has the `throttle_scope` property set will be
throttled. The unique cache key will be generated by concatenating the
user id of the request, and the scope of the view being accessed.
"""
scope_attr = 'throttle_scope'
def __init__(self, view):
"""
Scope is determined from the view being accessed.
"""
self.scope = getattr(self.view, self.scope_attr, None)
super(ScopedRateThrottle, self).__init__(view)
def parse_rate_description(self, rate):
"""
Subclassed so that we don't fail if `view.throttle_scope` is not set.
"""
if not rate:
return (None, None)
return super(ScopedRateThrottle, self).parse_rate_description(rate)
def get_cache_key(self, request):
"""
If `view.throttle_scope` is not set, don't apply this throttle.
Otherwise generate the unique cache key by concatenating the user id
with the '.throttle_scope` property of the view.
"""
if not self.scope:
return None # Only throttle views if `.throttle_scope` is set.
if request.user.is_authenticated():
ident = request.user.id
else:
ident = request.META.get('REMOTE_ADDR', None)
return self.cache_format % {
'scope': self.scope,
'ident': ident
}

View File

@ -0,0 +1,24 @@
from django.conf.urls.defaults import url
from rest_framework.settings import api_settings
def format_suffix_patterns(urlpatterns, suffix_required=False, suffix_kwarg=None):
"""
Supplement existing urlpatterns with corrosponding patterns that also
include a '.format' suffix. Retains urlpattern ordering.
"""
suffix_kwarg = suffix_kwarg or api_settings.FORMAT_SUFFIX_KWARG
suffix_pattern = '.(?P<%s>[a-z]+)$' % suffix_kwarg
ret = []
for urlpattern in urlpatterns:
# Form our complementing '.format' urlpattern
regex = urlpattern.regex.pattern.rstrip('$') + suffix_pattern
view = urlpattern._callback or urlpattern._callback_str
kwargs = urlpattern.default_args
name = urlpattern.name
# Add in both the existing and the new urlpattern
if not suffix_required:
ret.append(urlpattern)
ret.append(url(regex, view, kwargs, name))
return ret

23
rest_framework/urls.py Normal file
View File

@ -0,0 +1,23 @@
"""
Login and logout views for the browseable API.
Add these to your root URLconf if you're using the browseable API and
your API requires authentication.
The urls must be namespaced as 'rest_framework', and you should make sure
your authentication settings include `SessionAuthentication`.
urlpatterns = patterns('',
...
url(r'^auth', include('rest_framework.urls', namespace='rest_framework'))
)
"""
from django.conf.urls.defaults import patterns, url
template_name = {'template_name': 'rest_framework/login.html'}
urlpatterns = patterns('django.contrib.auth.views',
url(r'^login/$', 'login', template_name, name='login'),
url(r'^logout/$', 'logout', template_name, name='logout'),
)

View File

@ -0,0 +1,101 @@
from django.utils.encoding import smart_unicode
from django.utils.xmlutils import SimplerXMLGenerator
from rest_framework.compat import StringIO
import re
import xml.etree.ElementTree as ET
# From xml2dict
class XML2Dict(object):
def __init__(self):
pass
def _parse_node(self, node):
node_tree = {}
# Save attrs and text, hope there will not be a child with same name
if node.text:
node_tree = node.text
for (k, v) in node.attrib.items():
k, v = self._namespace_split(k, v)
node_tree[k] = v
#Save childrens
for child in node.getchildren():
tag, tree = self._namespace_split(child.tag, self._parse_node(child))
if tag not in node_tree: # the first time, so store it in dict
node_tree[tag] = tree
continue
old = node_tree[tag]
if not isinstance(old, list):
node_tree.pop(tag)
node_tree[tag] = [old] # multi times, so change old dict to a list
node_tree[tag].append(tree) # add the new one
return node_tree
def _namespace_split(self, tag, value):
"""
Split the tag '{http://cs.sfsu.edu/csc867/myscheduler}patients'
ns = http://cs.sfsu.edu/csc867/myscheduler
name = patients
"""
result = re.compile("\{(.*)\}(.*)").search(tag)
if result:
value.namespace, tag = result.groups()
return (tag, value)
def parse(self, file):
"""parse a xml file to a dict"""
f = open(file, 'r')
return self.fromstring(f.read())
def fromstring(self, s):
"""parse a string"""
t = ET.fromstring(s)
unused_root_tag, root_tree = self._namespace_split(t.tag, self._parse_node(t))
return root_tree
def xml2dict(input):
return XML2Dict().fromstring(input)
# Piston:
class XMLRenderer():
def _to_xml(self, xml, data):
if isinstance(data, (list, tuple)):
for item in data:
xml.startElement("list-item", {})
self._to_xml(xml, item)
xml.endElement("list-item")
elif isinstance(data, dict):
for key, value in data.iteritems():
xml.startElement(key, {})
self._to_xml(xml, value)
xml.endElement(key)
elif data is None:
# Don't output any value
pass
else:
xml.characters(smart_unicode(data))
def dict2xml(self, data):
stream = StringIO.StringIO()
xml = SimplerXMLGenerator(stream, "utf-8")
xml.startDocument()
xml.startElement("root", {})
self._to_xml(xml, data)
xml.endElement("root")
xml.endDocument()
return stream.getvalue()
def dict2xml(input):
return XMLRenderer().dict2xml(input)

Some files were not shown because too many files have changed in this diff Show More