mirror of
https://github.com/encode/django-rest-framework.git
synced 2024-11-25 11:04:02 +03:00
Change package name: djangorestframework -> rest_framework
This commit is contained in:
parent
a1bcfbfe92
commit
4b691c4027
2
.gitignore
vendored
2
.gitignore
vendored
|
@ -7,7 +7,7 @@ html/
|
||||||
coverage/
|
coverage/
|
||||||
build/
|
build/
|
||||||
dist/
|
dist/
|
||||||
djangorestframework.egg-info/
|
rest_framework.egg-info/
|
||||||
MANIFEST
|
MANIFEST
|
||||||
|
|
||||||
!.gitignore
|
!.gitignore
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
recursive-include djangorestframework/static *.ico *.txt *.css
|
recursive-include rest_framework/static *.ico *.txt *.css
|
||||||
recursive-include djangorestframework/templates *.txt *.html
|
recursive-include rest_framework/templates *.txt *.html
|
||||||
recursive-include examples .keep *.py *.txt
|
recursive-include examples .keep *.py *.txt
|
||||||
recursive-include docs *.py *.rst *.html *.txt
|
recursive-include docs *.py *.rst *.html *.txt
|
||||||
include AUTHORS LICENSE CHANGELOG.rst requirements.txt tox.ini
|
include AUTHORS LICENSE CHANGELOG.rst requirements.txt tox.ini
|
||||||
|
|
|
@ -28,7 +28,7 @@ For more information, check out [the documentation][docs], in particular, the tu
|
||||||
|
|
||||||
Install using `pip`...
|
Install using `pip`...
|
||||||
|
|
||||||
pip install djangorestframework
|
pip install rest_framework
|
||||||
|
|
||||||
...or clone the project from github.
|
...or clone the project from github.
|
||||||
|
|
||||||
|
@ -47,7 +47,7 @@ To build the docs.
|
||||||
|
|
||||||
To run the tests.
|
To run the tests.
|
||||||
|
|
||||||
./djangorestframework/runtests/runtests.py
|
./rest_framework/runtests/runtests.py
|
||||||
|
|
||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
|
|
@ -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.
|
The default authentication policy may be set globally, using the `DEFAULT_AUTHENTICATION` setting. For example.
|
||||||
|
|
||||||
API_SETTINGS = {
|
REST_FRAMEWORK = {
|
||||||
'DEFAULT_AUTHENTICATION': (
|
'DEFAULT_AUTHENTICATION': (
|
||||||
'djangorestframework.authentication.UserBasicAuthentication',
|
'rest_framework.authentication.UserBasicAuthentication',
|
||||||
'djangorestframework.authentication.SessionAuthentication',
|
'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.
|
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.
|
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=...)
|
token = Token.objects.create(user=...)
|
||||||
print token.key
|
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.
|
If successfully authenticated, `TokenAuthentication` provides the following credentials.
|
||||||
|
|
||||||
* `request.user` will be a `django.contrib.auth.models.User` instance.
|
* `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.
|
**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.
|
If successfully authenticated, `OAuthAuthentication` provides the following credentials.
|
||||||
|
|
||||||
* `request.user` will be a `django.contrib.auth.models.User` instance.
|
* `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
|
## SessionAuthentication
|
||||||
|
|
||||||
|
|
|
@ -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.
|
The default permission policy may be set globally, using the `DEFAULT_PERMISSIONS` setting. For example.
|
||||||
|
|
||||||
API_SETTINGS = {
|
REST_FRAMEWORK = {
|
||||||
'DEFAULT_PERMISSIONS': (
|
'DEFAULT_PERMISSIONS': (
|
||||||
'djangorestframework.permissions.IsAuthenticated',
|
'rest_framework.permissions.IsAuthenticated',
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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.
|
`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
|
## .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.
|
`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
|
[cite]: https://groups.google.com/d/topic/django-developers/dxI4qVzrBY4/discussion
|
|
@ -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.
|
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 rest_framework.utils import reverse
|
||||||
from djangorestframework.views import APIView
|
from rest_framework.views import APIView
|
||||||
|
|
||||||
class MyView(APIView):
|
class MyView(APIView):
|
||||||
def get(self, request):
|
def get(self, request):
|
||||||
|
|
|
@ -6,16 +6,16 @@
|
||||||
>
|
>
|
||||||
> — [The Zen of Python][cite]
|
> — [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:
|
For example your project's `settings.py` file might include something like this:
|
||||||
|
|
||||||
API_SETTINGS = {
|
REST_FRAMEWORK = {
|
||||||
'DEFAULT_RENDERERS': (
|
'DEFAULT_RENDERERS': (
|
||||||
'djangorestframework.renderers.YAMLRenderer',
|
'rest_framework.renderers.YAMLRenderer',
|
||||||
)
|
)
|
||||||
'DEFAULT_PARSERS': (
|
'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,
|
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.
|
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
|
print api_settings.DEFAULT_AUTHENTICATION
|
||||||
|
|
||||||
|
@ -37,9 +37,9 @@ A list or tuple of renderer classes, that determines the default set of renderer
|
||||||
Default:
|
Default:
|
||||||
|
|
||||||
(
|
(
|
||||||
'djangorestframework.renderers.JSONRenderer',
|
'rest_framework.renderers.JSONRenderer',
|
||||||
'djangorestframework.renderers.DocumentingHTMLRenderer'
|
'rest_framework.renderers.DocumentingHTMLRenderer'
|
||||||
'djangorestframework.renderers.TemplateHTMLRenderer'
|
'rest_framework.renderers.TemplateHTMLRenderer'
|
||||||
)
|
)
|
||||||
|
|
||||||
## DEFAULT_PARSERS
|
## DEFAULT_PARSERS
|
||||||
|
@ -49,8 +49,8 @@ A list or tuple of parser classes, that determines the default set of parsers us
|
||||||
Default:
|
Default:
|
||||||
|
|
||||||
(
|
(
|
||||||
'djangorestframework.parsers.JSONParser',
|
'rest_framework.parsers.JSONParser',
|
||||||
'djangorestframework.parsers.FormParser'
|
'rest_framework.parsers.FormParser'
|
||||||
)
|
)
|
||||||
|
|
||||||
## DEFAULT_AUTHENTICATION
|
## DEFAULT_AUTHENTICATION
|
||||||
|
@ -60,8 +60,8 @@ A list or tuple of authentication classes, that determines the default set of au
|
||||||
Default:
|
Default:
|
||||||
|
|
||||||
(
|
(
|
||||||
'djangorestframework.authentication.SessionAuthentication',
|
'rest_framework.authentication.SessionAuthentication',
|
||||||
'djangorestframework.authentication.UserBasicAuthentication'
|
'rest_framework.authentication.UserBasicAuthentication'
|
||||||
)
|
)
|
||||||
|
|
||||||
## DEFAULT_PERMISSIONS
|
## DEFAULT_PERMISSIONS
|
||||||
|
@ -80,13 +80,13 @@ Default: `()`
|
||||||
|
|
||||||
**TODO**
|
**TODO**
|
||||||
|
|
||||||
Default: `djangorestframework.serializers.ModelSerializer`
|
Default: `rest_framework.serializers.ModelSerializer`
|
||||||
|
|
||||||
## DEFAULT_PAGINATION_SERIALIZER
|
## DEFAULT_PAGINATION_SERIALIZER
|
||||||
|
|
||||||
**TODO**
|
**TODO**
|
||||||
|
|
||||||
Default: `djangorestframework.pagination.PaginationSerializer`
|
Default: `rest_framework.pagination.PaginationSerializer`
|
||||||
|
|
||||||
## FORMAT_SUFFIX_KWARG
|
## FORMAT_SUFFIX_KWARG
|
||||||
|
|
||||||
|
|
|
@ -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.
|
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):
|
def empty_view(self):
|
||||||
content = {'please move along': 'nothing to see here'}
|
content = {'please move along': 'nothing to see here'}
|
||||||
|
|
|
@ -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.
|
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': (
|
'DEFAULT_THROTTLES': (
|
||||||
'djangorestframework.throttles.AnonThrottle',
|
'rest_framework.throttles.AnonThrottle',
|
||||||
'djangorestframework.throttles.UserThrottle',
|
'rest_framework.throttles.UserThrottle',
|
||||||
)
|
)
|
||||||
'DEFAULT_THROTTLE_RATES': {
|
'DEFAULT_THROTTLE_RATES': {
|
||||||
'anon': '100/day',
|
'anon': '100/day',
|
||||||
|
@ -95,7 +95,7 @@ For example, multiple user throttle rates could be implemented by using the foll
|
||||||
|
|
||||||
...and the following settings.
|
...and the following settings.
|
||||||
|
|
||||||
API_SETTINGS = {
|
REST_FRAMEWORK = {
|
||||||
'DEFAULT_THROTTLES': (
|
'DEFAULT_THROTTLES': (
|
||||||
'example.throttles.BurstRateThrottle',
|
'example.throttles.BurstRateThrottle',
|
||||||
'example.throttles.SustainedRateThrottle',
|
'example.throttles.SustainedRateThrottle',
|
||||||
|
@ -130,9 +130,9 @@ For example, given the following views...
|
||||||
|
|
||||||
...and the following settings.
|
...and the following settings.
|
||||||
|
|
||||||
API_SETTINGS = {
|
REST_FRAMEWORK = {
|
||||||
'DEFAULT_THROTTLES': (
|
'DEFAULT_THROTTLES': (
|
||||||
'djangorestframework.throttles.ScopedRateThrottle',
|
'rest_framework.throttles.ScopedRateThrottle',
|
||||||
)
|
)
|
||||||
'DEFAULT_THROTTLE_RATES': {
|
'DEFAULT_THROTTLE_RATES': {
|
||||||
'contacts': '1000/day',
|
'contacts': '1000/day',
|
||||||
|
|
|
@ -40,20 +40,22 @@ Install using `pip`, including any optional packages you want...
|
||||||
pip install -r requirements.txt
|
pip install -r requirements.txt
|
||||||
pip install -r optionals.txt
|
pip install -r optionals.txt
|
||||||
|
|
||||||
Add `djangorestframework` to your `INSTALLED_APPS`.
|
Add `rest_framework` to your `INSTALLED_APPS`.
|
||||||
|
|
||||||
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.
|
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('',
|
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
|
## Quickstart
|
||||||
|
|
||||||
**TODO**
|
**TODO**
|
||||||
|
@ -110,7 +112,7 @@ Build the docs:
|
||||||
|
|
||||||
Run the tests:
|
Run the tests:
|
||||||
|
|
||||||
./djangorestframework/runtests/runtests.py
|
./rest_framework/runtests/runtests.py
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
|
|
|
@ -4,7 +4,7 @@ API may stand for Application *Programming* Interface, but humans have to be abl
|
||||||
|
|
||||||
## URLs
|
## 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
|
## Formats
|
||||||
|
@ -14,7 +14,7 @@ By default, the API will return the format specified by the headers, which in th
|
||||||
|
|
||||||
## Customizing
|
## 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.
|
The included browsable API template is built with [Bootstrap (2.1.1)][bootstrap], making it easy to customize the look-and-feel.
|
||||||
|
|
||||||
|
|
|
@ -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 = (
|
INSTALLED_APPS = (
|
||||||
...
|
...
|
||||||
'djangorestframework',
|
'rest_framework',
|
||||||
'blog'
|
'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.
|
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 blog import models
|
||||||
from djangorestframework import serializers
|
from rest_framework import serializers
|
||||||
|
|
||||||
|
|
||||||
class CommentSerializer(serializers.Serializer):
|
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.models import Comment
|
||||||
from blog.serializers import CommentSerializer
|
from blog.serializers import CommentSerializer
|
||||||
from djangorestframework.renderers import JSONRenderer
|
from rest_framework.renderers import JSONRenderer
|
||||||
from djangorestframework.parsers import JSONParser
|
from rest_framework.parsers import JSONParser
|
||||||
|
|
||||||
c1 = Comment(email='leila@example.com', content='nothing to say')
|
c1 = Comment(email='leila@example.com', content='nothing to say')
|
||||||
c2 = Comment(email='tom@example.com', content='foo bar')
|
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.models import Comment
|
||||||
from blog.serializers import CommentSerializer
|
from blog.serializers import CommentSerializer
|
||||||
from djangorestframework.renderers import JSONRenderer
|
from rest_framework.renderers import JSONRenderer
|
||||||
from djangorestframework.parsers import JSONParser
|
from rest_framework.parsers import JSONParser
|
||||||
from django.http import HttpResponse
|
from django.http import HttpResponse
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -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.models import Comment
|
||||||
from blog.serializers import CommentSerializer
|
from blog.serializers import CommentSerializer
|
||||||
from djangorestframework import status
|
from rest_framework import status
|
||||||
from djangorestframework.decorators import api_view
|
from rest_framework.decorators import api_view
|
||||||
from djangorestframework.response import Response
|
from rest_framework.response import Response
|
||||||
|
|
||||||
@api_view(['GET', 'POST'])
|
@api_view(['GET', 'POST'])
|
||||||
def comment_root(request):
|
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.
|
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 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',
|
urlpatterns = patterns('blogpost.views',
|
||||||
url(r'^$', 'comment_root'),
|
url(r'^$', 'comment_root'),
|
||||||
|
|
|
@ -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.models import Comment
|
||||||
from blog.serializers import CommentSerializer
|
from blog.serializers import CommentSerializer
|
||||||
from django.http import Http404
|
from django.http import Http404
|
||||||
from djangorestframework.views import APIView
|
from rest_framework.views import APIView
|
||||||
from djangorestframework.response import Response
|
from rest_framework.response import Response
|
||||||
from djangorestframework import status
|
from rest_framework import status
|
||||||
|
|
||||||
|
|
||||||
class CommentRoot(APIView):
|
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.
|
We'll also need to refactor our URLconf slightly now we're using class based views.
|
||||||
|
|
||||||
from django.conf.urls import patterns, url
|
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
|
from blogpost import views
|
||||||
|
|
||||||
urlpatterns = patterns('',
|
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.models import Comment
|
||||||
from blog.serializers import CommentSerializer
|
from blog.serializers import CommentSerializer
|
||||||
from djangorestframework import mixins
|
from rest_framework import mixins
|
||||||
from djangorestframework import generics
|
from rest_framework import generics
|
||||||
|
|
||||||
class CommentRoot(mixins.ListModelMixin,
|
class CommentRoot(mixins.ListModelMixin,
|
||||||
mixins.CreateModelMixin,
|
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.models import Comment
|
||||||
from blog.serializers import CommentSerializer
|
from blog.serializers import CommentSerializer
|
||||||
from djangorestframework import generics
|
from rest_framework import generics
|
||||||
|
|
||||||
|
|
||||||
class CommentRoot(generics.RootAPIView):
|
class CommentRoot(generics.RootAPIView):
|
||||||
|
|
|
@ -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.
|
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 blog import resources
|
||||||
from djangorestframework.routers import DefaultRouter
|
from rest_framework.routers import DefaultRouter
|
||||||
|
|
||||||
router = DefaultRouter()
|
router = DefaultRouter()
|
||||||
router.register(resources.BlogPostResource)
|
router.register(resources.BlogPostResource)
|
||||||
|
|
|
@ -24,7 +24,7 @@ else:
|
||||||
|
|
||||||
main_header = '<li class="main"><a href="#{{ anchor }}">{{ title }}</a></li>'
|
main_header = '<li class="main"><a href="#{{ anchor }}">{{ title }}</a></li>'
|
||||||
sub_header = '<li><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()
|
page = open(os.path.join(docs_dir, 'template.html'), 'r').read()
|
||||||
|
|
||||||
|
|
3
rest_framework/__init__.py
Normal file
3
rest_framework/__init__.py
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
__version__ = '2.0.0'
|
||||||
|
|
||||||
|
VERSION = __version__ # synonym
|
132
rest_framework/authentication.py
Normal file
132
rest_framework/authentication.py
Normal 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
|
0
rest_framework/authtoken/__init__.py
Normal file
0
rest_framework/authtoken/__init__.py
Normal file
72
rest_framework/authtoken/migrations/0001_initial.py
Normal file
72
rest_framework/authtoken/migrations/0001_initial.py
Normal 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']
|
0
rest_framework/authtoken/migrations/__init__.py
Normal file
0
rest_framework/authtoken/migrations/__init__.py
Normal file
23
rest_framework/authtoken/models.py
Normal file
23
rest_framework/authtoken/models.py
Normal 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()
|
0
rest_framework/authtoken/views.py
Normal file
0
rest_framework/authtoken/views.py
Normal file
480
rest_framework/compat.py
Normal file
480
rest_framework/compat.py
Normal 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
|
53
rest_framework/decorators.py
Normal file
53
rest_framework/decorators.py
Normal 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
|
79
rest_framework/exceptions.py
Normal file
79
rest_framework/exceptions.py
Normal 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
446
rest_framework/fields.py
Normal 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
113
rest_framework/generics.py
Normal 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
97
rest_framework/mixins.py
Normal 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
1
rest_framework/models.py
Normal file
|
@ -0,0 +1 @@
|
||||||
|
# Just to keep things like ./manage.py test happy
|
74
rest_framework/negotiation.py
Normal file
74
rest_framework/negotiation.py
Normal 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
260
rest_framework/parsers.py
Normal 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
|
116
rest_framework/permissions.py
Normal file
116
rest_framework/permissions.py
Normal 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
402
rest_framework/renderers.py
Normal 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
284
rest_framework/request.py
Normal 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)
|
87
rest_framework/resources.py
Normal file
87
rest_framework/resources.py
Normal 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)
|
39
rest_framework/response.py
Normal file
39
rest_framework/response.py
Normal 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
20
rest_framework/reverse.py
Normal 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)
|
0
rest_framework/runtests/__init__.py
Normal file
0
rest_framework/runtests/__init__.py
Normal file
66
rest_framework/runtests/runcoverage.py
Executable file
66
rest_framework/runtests/runcoverage.py
Executable 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()
|
40
rest_framework/runtests/runtests.py
Executable file
40
rest_framework/runtests/runtests.py
Executable 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()
|
118
rest_framework/runtests/settings.py
Normal file
118
rest_framework/runtests/settings.py
Normal 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'
|
7
rest_framework/runtests/urls.py
Normal file
7
rest_framework/runtests/urls.py
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
"""
|
||||||
|
Blank URLConf just to keep runtests.py happy.
|
||||||
|
"""
|
||||||
|
from django.conf.urls.defaults import *
|
||||||
|
|
||||||
|
urlpatterns = patterns('',
|
||||||
|
)
|
348
rest_framework/serializers.py
Normal file
348
rest_framework/serializers.py
Normal 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
125
rest_framework/settings.py
Normal 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()
|
22
rest_framework/static/djangorestframework/css/bootstrap-tweaks.css
vendored
Normal file
22
rest_framework/static/djangorestframework/css/bootstrap-tweaks.css
vendored
Normal 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;
|
||||||
|
}
|
841
rest_framework/static/djangorestframework/css/bootstrap.min.css
vendored
Normal file
841
rest_framework/static/djangorestframework/css/bootstrap.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
30
rest_framework/static/djangorestframework/css/prettify.css
Normal file
30
rest_framework/static/djangorestframework/css/prettify.css
Normal 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;
|
||||||
|
}
|
74
rest_framework/static/djangorestframework/css/style.css
Normal file
74
rest_framework/static/djangorestframework/css/style.css
Normal 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;
|
||||||
|
}
|
BIN
rest_framework/static/djangorestframework/img/glyphicons-halflings-white.png
Executable file
BIN
rest_framework/static/djangorestframework/img/glyphicons-halflings-white.png
Executable file
Binary file not shown.
After Width: | Height: | Size: 8.6 KiB |
BIN
rest_framework/static/djangorestframework/img/glyphicons-halflings.png
Executable file
BIN
rest_framework/static/djangorestframework/img/glyphicons-halflings.png
Executable file
Binary file not shown.
After Width: | Height: | Size: 12 KiB |
7
rest_framework/static/djangorestframework/js/bootstrap.min.js
vendored
Executable file
7
rest_framework/static/djangorestframework/js/bootstrap.min.js
vendored
Executable file
File diff suppressed because one or more lines are too long
2
rest_framework/static/djangorestframework/js/jquery-1.8.1-min.js
vendored
Normal file
2
rest_framework/static/djangorestframework/js/jquery-1.8.1-min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
28
rest_framework/static/djangorestframework/js/prettify-min.js
vendored
Normal file
28
rest_framework/static/djangorestframework/js/prettify-min.js
vendored
Normal 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"}})();
|
22
rest_framework/static/rest_framework/css/bootstrap-tweaks.css
vendored
Normal file
22
rest_framework/static/rest_framework/css/bootstrap-tweaks.css
vendored
Normal 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;
|
||||||
|
}
|
841
rest_framework/static/rest_framework/css/bootstrap.min.css
vendored
Normal file
841
rest_framework/static/rest_framework/css/bootstrap.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
30
rest_framework/static/rest_framework/css/prettify.css
Normal file
30
rest_framework/static/rest_framework/css/prettify.css
Normal 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;
|
||||||
|
}
|
74
rest_framework/static/rest_framework/css/style.css
Normal file
74
rest_framework/static/rest_framework/css/style.css
Normal 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;
|
||||||
|
}
|
BIN
rest_framework/static/rest_framework/img/glyphicons-halflings-white.png
Executable file
BIN
rest_framework/static/rest_framework/img/glyphicons-halflings-white.png
Executable file
Binary file not shown.
After Width: | Height: | Size: 8.6 KiB |
BIN
rest_framework/static/rest_framework/img/glyphicons-halflings.png
Executable file
BIN
rest_framework/static/rest_framework/img/glyphicons-halflings.png
Executable file
Binary file not shown.
After Width: | Height: | Size: 12 KiB |
7
rest_framework/static/rest_framework/js/bootstrap.min.js
vendored
Executable file
7
rest_framework/static/rest_framework/js/bootstrap.min.js
vendored
Executable file
File diff suppressed because one or more lines are too long
2
rest_framework/static/rest_framework/js/jquery-1.8.1-min.js
vendored
Normal file
2
rest_framework/static/rest_framework/js/jquery-1.8.1-min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
28
rest_framework/static/rest_framework/js/prettify-min.js
vendored
Normal file
28
rest_framework/static/rest_framework/js/prettify-min.js
vendored
Normal 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
52
rest_framework/status.py
Normal 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
|
3
rest_framework/templates/rest_framework/api.html
Normal file
3
rest_framework/templates/rest_framework/api.html
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
{% extends "rest_framework/base.html" %}
|
||||||
|
|
||||||
|
{# Override this template in your own templates directory to customize #}
|
8
rest_framework/templates/rest_framework/api.txt
Normal file
8
rest_framework/templates/rest_framework/api.txt
Normal 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 %}
|
214
rest_framework/templates/rest_framework/base.html
Normal file
214
rest_framework/templates/rest_framework/base.html
Normal 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">›</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>
|
45
rest_framework/templates/rest_framework/login.html
Normal file
45
rest_framework/templates/rest_framework/login.html
Normal 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> </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>
|
0
rest_framework/templatetags/__init__.py
Normal file
0
rest_framework/templatetags/__init__.py
Normal file
40
rest_framework/templatetags/add_class.py
Normal file
40
rest_framework/templatetags/add_class.py
Normal 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
|
20
rest_framework/templatetags/add_query_param.py
Normal file
20
rest_framework/templatetags/add_query_param.py
Normal 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)
|
32
rest_framework/templatetags/optional_login.py
Normal file
32
rest_framework/templatetags/optional_login.py
Normal 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
|
102
rest_framework/templatetags/urlize_quoted_links.py
Normal file
102
rest_framework/templatetags/urlize_quoted_links.py
Normal 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 = ['(', '<', '<', '"', "'"]
|
||||||
|
TRAILING_PUNCTUATION = ['.', ',', ')', '>', '\n', '>', '"', "'"]
|
||||||
|
|
||||||
|
# List of possible strings used for bullets in bulleted lists.
|
||||||
|
DOTS = ['·', '*', '\xe2\x80\xa2', '•', '•', '•']
|
||||||
|
|
||||||
|
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>(?: |\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)
|
12
rest_framework/tests/__init__.py
Normal file
12
rest_framework/tests/__init__.py
Normal 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 ""
|
153
rest_framework/tests/authentication.py
Normal file
153
rest_framework/tests/authentication.py
Normal 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))
|
72
rest_framework/tests/breadcrumbs.py
Normal file
72
rest_framework/tests/breadcrumbs.py
Normal 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', '/')])
|
113
rest_framework/tests/description.py
Normal file
113
rest_framework/tests/description.py
Normal 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)
|
34
rest_framework/tests/files.py
Normal file
34
rest_framework/tests/files.py
Normal 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"})
|
0
rest_framework/tests/methods.py
Normal file
0
rest_framework/tests/methods.py
Normal file
285
rest_framework/tests/mixins.py
Normal file
285
rest_framework/tests/mixins.py
Normal 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'])
|
28
rest_framework/tests/models.py
Normal file
28
rest_framework/tests/models.py
Normal 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
|
||||||
|
})
|
90
rest_framework/tests/modelviews.py
Normal file
90
rest_framework/tests/modelviews.py
Normal 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)
|
211
rest_framework/tests/oauthentication.py
Normal file
211
rest_framework/tests/oauthentication.py
Normal 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!"}')
|
11
rest_framework/tests/package.py
Normal file
11
rest_framework/tests/package.py
Normal 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
|
||||||
|
|
212
rest_framework/tests/parsers.py
Normal file
212
rest_framework/tests/parsers.py
Normal 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)
|
375
rest_framework/tests/renderers.py
Normal file
375
rest_framework/tests/renderers.py
Normal 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))
|
252
rest_framework/tests/request.py
Normal file
252
rest_framework/tests/request.py
Normal 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")
|
177
rest_framework/tests/response.py
Normal file
177
rest_framework/tests/response.py
Normal 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')
|
35
rest_framework/tests/reverse.py
Normal file
35
rest_framework/tests/reverse.py
Normal 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')
|
117
rest_framework/tests/serializer.py
Normal file
117
rest_framework/tests/serializer.py
Normal 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))
|
12
rest_framework/tests/status.py
Normal file
12
rest_framework/tests/status.py
Normal 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)
|
63
rest_framework/tests/testcases.py
Normal file
63
rest_framework/tests/testcases.py
Normal 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)
|
144
rest_framework/tests/throttling.py
Normal file
144
rest_framework/tests/throttling.py
Normal 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)
|
||||||
|
))
|
329
rest_framework/tests/validators.py
Normal file
329
rest_framework/tests/validators.py
Normal 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))
|
128
rest_framework/tests/views.py
Normal file
128
rest_framework/tests/views.py
Normal 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')
|
217
rest_framework/throttling.py
Normal file
217
rest_framework/throttling.py
Normal 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
|
||||||
|
}
|
24
rest_framework/urlpatterns.py
Normal file
24
rest_framework/urlpatterns.py
Normal 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
23
rest_framework/urls.py
Normal 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'),
|
||||||
|
)
|
101
rest_framework/utils/__init__.py
Normal file
101
rest_framework/utils/__init__.py
Normal 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
Loading…
Reference in New Issue
Block a user