Commit 81870b6e by BrickXu

Merge pull request #2 from tomchristie/master

Merge upstream
parents 71654967 d1fe61ce
...@@ -3,7 +3,7 @@ ...@@ -3,7 +3,7 @@
*~ *~
.* .*
html/ site/
htmlcov/ htmlcov/
coverage/ coverage/
build/ build/
......
language: python language: python
python: 2.7
sudo: false sudo: false
env: env:
- TOX_ENV=flake8 - TOX_ENV=py27-flake8
- TOX_ENV=py3.4-django1.7 - TOX_ENV=py27-docs
- TOX_ENV=py3.3-django1.7 - TOX_ENV=py34-django17
- TOX_ENV=py3.2-django1.7 - TOX_ENV=py33-django17
- TOX_ENV=py2.7-django1.7 - TOX_ENV=py32-django17
- TOX_ENV=py3.4-django1.6 - TOX_ENV=py27-django17
- TOX_ENV=py3.3-django1.6 - TOX_ENV=py34-django16
- TOX_ENV=py3.2-django1.6 - TOX_ENV=py33-django16
- TOX_ENV=py2.7-django1.6 - TOX_ENV=py32-django16
- TOX_ENV=py2.6-django1.6 - TOX_ENV=py27-django16
- TOX_ENV=py3.4-django1.5 - TOX_ENV=py26-django16
- TOX_ENV=py3.3-django1.5 - TOX_ENV=py34-django15
- TOX_ENV=py3.2-django1.5 - TOX_ENV=py33-django15
- TOX_ENV=py2.7-django1.5 - TOX_ENV=py32-django15
- TOX_ENV=py2.6-django1.5 - TOX_ENV=py27-django15
- TOX_ENV=py2.7-django1.4 - TOX_ENV=py26-django15
- TOX_ENV=py2.6-django1.4 - TOX_ENV=py27-django14
- TOX_ENV=py3.4-djangomaster - TOX_ENV=py26-django14
- TOX_ENV=py3.3-djangomaster - TOX_ENV=py34-djangomaster
- TOX_ENV=py2.7-djangomaster - TOX_ENV=py33-djangomaster
- TOX_ENV=py32-djangomaster
- TOX_ENV=py27-djangomaster
matrix: matrix:
fast_finish: true fast_finish: true
allow_failures: allow_failures:
- env: TOX_ENV=py3.4-djangomaster - env: TOX_ENV=py34-djangomaster
- env: TOX_ENV=py3.3-djangomaster - env: TOX_ENV=py33-djangomaster
- env: TOX_ENV=py2.7-djangomaster - env: TOX_ENV=py32-djangomaster
- env: TOX_ENV=py27-djangomaster
install: install:
- "pip install tox --download-cache $HOME/.pip-cache" - pip install tox
script: script:
- tox -e $TOX_ENV - tox -e $TOX_ENV
...@@ -101,15 +101,15 @@ There are many great markdown editors that make working with the documentation r ...@@ -101,15 +101,15 @@ There are many great markdown editors that make working with the documentation r
## Building the documentation ## Building the documentation
To build the documentation, simply run the `mkdocs.py` script. To build the documentation, install MkDocs with `pip install mkdocs` and then run the following command.
./mkdocs.py mkdocs build
This will build the html output into the `html` directory. This will build the html output into the `html` directory.
You can build the documentation and open a preview in a browser window by using the `-p` flag. You can build the documentation and open a preview in a browser window by using the `serve` command.
./mkdocs.py -p mkdocs serve
## Language style ## Language style
......
www.django-rest-framework.org
<a class="github" href="authentication.py"></a> source: authentication.py
# Authentication # Authentication
......
<a class="github" href="negotiation.py"></a> source: negotiation.py
# Content negotiation # Content negotiation
......
<a class="github" href="exceptions.py"></a> source: exceptions.py
# Exceptions # Exceptions
...@@ -100,7 +100,7 @@ For example, if your API relies on a third party service that may sometimes be u ...@@ -100,7 +100,7 @@ For example, if your API relies on a third party service that may sometimes be u
**Signature:** `ParseError(detail=None)` **Signature:** `ParseError(detail=None)`
Raised if the request contains malformed data when accessing `request.DATA` or `request.FILES`. Raised if the request contains malformed data when accessing `request.data`.
By default this exception results in a response with the HTTP status code "400 Bad Request". By default this exception results in a response with the HTTP status code "400 Bad Request".
...@@ -140,7 +140,7 @@ By default this exception results in a response with the HTTP status code "405 M ...@@ -140,7 +140,7 @@ By default this exception results in a response with the HTTP status code "405 M
**Signature:** `UnsupportedMediaType(media_type, detail=None)` **Signature:** `UnsupportedMediaType(media_type, detail=None)`
Raised if there are no parsers that can handle the content type of the request data when accessing `request.DATA` or `request.FILES`. Raised if there are no parsers that can handle the content type of the request data when accessing `request.data`.
By default this exception results in a response with the HTTP status code "415 Unsupported Media Type". By default this exception results in a response with the HTTP status code "415 Unsupported Media Type".
...@@ -152,5 +152,23 @@ Raised when an incoming request fails the throttling checks. ...@@ -152,5 +152,23 @@ Raised when an incoming request fails the throttling checks.
By default this exception results in a response with the HTTP status code "429 Too Many Requests". By default this exception results in a response with the HTTP status code "429 Too Many Requests".
## ValidationError
**Signature:** `ValidationError(detail)`
The `ValidationError` exception is slightly different from the other `APIException` classes:
* The `detail` argument is mandatory, not optional.
* The `detail` argument may be a list or dictionary of error details, and may also be a nested data structure.
* By convention you should import the serializers module and use a fully qualified `ValidationError` style, in order to differentiate it from Django's built-in validation error. For example. `raise serializers.ValidationError('This field must be an integer value.')`
The `ValidationError` class should be used for serializer and field validation, and by validator classes. It is also raised when calling `serializer.is_valid` with the `raise_exception` keyword argument:
serializer.is_valid(raise_exception=True)
The generic views use the `raise_exception=True` flag, which means that you can override the style of validation error responses globally in your API. To do so, use a custom exception handler, as described above.
By default this exception results in a response with the HTTP status code "400 Bad Request".
[cite]: http://www.doughellmann.com/articles/how-tos/python-exception-handling/index.html [cite]: http://www.doughellmann.com/articles/how-tos/python-exception-handling/index.html
[authentication]: authentication.md [authentication]: authentication.md
<a class="github" href="filters.py"></a> source: filters.py
# Filtering # Filtering
......
<a class="github" href="urlpatterns.py"></a> source: urlpatterns.py
# Format suffixes # Format suffixes
......
<a class="github" href="mixins.py"></a> source: mixins.py
<a class="github" href="generics.py"></a> generics.py
---
**Note**: This is the documentation for the **version 3.0** of REST framework. Documentation for [version 2.4](http://tomchristie.github.io/rest-framework-2-docs/) is also available.
---
# Generic views # Generic views
...@@ -7,7 +13,7 @@ ...@@ -7,7 +13,7 @@
> >
> &mdash; [Django Documentation][cite] > &mdash; [Django Documentation][cite]
One of the key benefits of class based views is the way they allow you to compose bits of reusable behaviour. REST framework takes advantage of this by providing a number of pre-built views that provide for commonly used patterns. One of the key benefits of class based views is the way they allow you to compose bits of reusable behavior. REST framework takes advantage of this by providing a number of pre-built views that provide for commonly used patterns.
The generic views provided by REST framework allow you to quickly build API views that map closely to your database models. The generic views provided by REST framework allow you to quickly build API views that map closely to your database models.
...@@ -171,24 +177,26 @@ For example: ...@@ -171,24 +177,26 @@ For example:
return 20 return 20
return 100 return 100
**Save / deletion hooks**: **Save and deletion hooks**:
The following methods are provided as placeholder interfaces. They contain empty implementations and are not called directly by `GenericAPIView`, but they are overridden and used by some of the mixin classes. The following methods are provided by the mixin classes, and provide easy overriding of the object save or deletion behavior.
* `pre_save(self, obj)` - A hook that is called before saving an object. * `perform_create(self, serializer)` - Called by `CreateModelMixin` when saving a new object instance.
* `post_save(self, obj, created=False)` - A hook that is called after saving an object. * `perform_update(self, serializer)` - Called by `UpdateModelMixin` when saving an existing object instance.
* `pre_delete(self, obj)` - A hook that is called before deleting an object. * `perform_destroy(self, instance)` - Called by `DestroyModelMixin` when deleting an object instance.
* `post_delete(self, obj)` - A hook that is called after deleting an object.
The `pre_save` method in particular is a useful hook for setting attributes that are implicit in the request, but are not part of the request data. For instance, you might set an attribute on the object based on the request user, or based on a URL keyword argument. These hooks are particularly useful for setting attributes that are implicit in the request, but are not part of the request data. For instance, you might set an attribute on the object based on the request user, or based on a URL keyword argument.
def pre_save(self, obj): def perform_create(self, serializer):
""" serializer.save(user=self.request.user)
Set the object's owner, based on the incoming request.
""" These override points are also particularly useful for adding behavior that occurs before or after saving an object, such as emailing a confirmation, or logging the update.
obj.owner = self.request.user
def perform_update(self, serializer):
instance = serializer.save()
send_email_confirmation(user=self.request.user, modified=instance)
Remember that the `pre_save()` method is not called by `GenericAPIView` itself, but it is called by `create()` and `update()` methods on the `CreateModelMixin` and `UpdateModelMixin` classes. **Note**: These methods replace the old-style version 2.x `pre_save`, `post_save`, `pre_delete` and `post_delete` methods, which are no longer available.
**Other methods**: **Other methods**:
...@@ -352,7 +360,7 @@ You can then simply apply this mixin to a view or viewset anytime you need to ap ...@@ -352,7 +360,7 @@ You can then simply apply this mixin to a view or viewset anytime you need to ap
serializer_class = UserSerializer serializer_class = UserSerializer
lookup_fields = ('account', 'username') lookup_fields = ('account', 'username')
Using custom mixins is a good option if you have custom behavior that needs to be used Using custom mixins is a good option if you have custom behavior that needs to be used.
## Creating custom base classes ## Creating custom base classes
......
<a class="github" href="metadata.py"></a> <a class="github" href="metadata.py"></a>
---
**Note**: This is the documentation for the **version 3.0** of REST framework. Documentation for [version 2.4](http://tomchristie.github.io/rest-framework-2-docs/) is also available.
---
# Metadata # Metadata
> [The `OPTIONS`] method allows a client to determine the options and/or requirements associated with a resource, or the capabilities of a server, without implying a resource action or initiating a resource retrieval. > [The `OPTIONS`] method allows a client to determine the options and/or requirements associated with a resource, or the capabilities of a server, without implying a resource action or initiating a resource retrieval.
......
<a class="github" href="pagination.py"></a> source: pagination.py
# Pagination # Pagination
......
<a class="github" href="parsers.py"></a> source: parsers.py
# Parsers # Parsers
...@@ -12,7 +12,7 @@ REST framework includes a number of built in Parser classes, that allow you to a ...@@ -12,7 +12,7 @@ REST framework includes a number of built in Parser classes, that allow you to a
## How the parser is determined ## How the parser is determined
The set of valid parsers for a view is always defined as a list of classes. When either `request.DATA` or `request.FILES` is accessed, REST framework will examine the `Content-Type` header on the incoming request, and determine which parser to use to parse the request content. The set of valid parsers for a view is always defined as a list of classes. When `request.data` is accessed, REST framework will examine the `Content-Type` header on the incoming request, and determine which parser to use to parse the request content.
--- ---
...@@ -48,7 +48,7 @@ using the `APIView` class based views. ...@@ -48,7 +48,7 @@ using the `APIView` class based views.
parser_classes = (YAMLParser,) parser_classes = (YAMLParser,)
def post(self, request, format=None): def post(self, request, format=None):
return Response({'received data': request.DATA}) return Response({'received data': request.data})
Or, if you're using the `@api_view` decorator with function based views. Or, if you're using the `@api_view` decorator with function based views.
...@@ -58,7 +58,7 @@ Or, if you're using the `@api_view` decorator with function based views. ...@@ -58,7 +58,7 @@ Or, if you're using the `@api_view` decorator with function based views.
""" """
A view that can accept POST requests with YAML content. A view that can accept POST requests with YAML content.
""" """
return Response({'received data': request.DATA}) return Response({'received data': request.data})
--- ---
...@@ -92,7 +92,7 @@ Requires the `defusedxml` package to be installed. ...@@ -92,7 +92,7 @@ Requires the `defusedxml` package to be installed.
## FormParser ## FormParser
Parses HTML form content. `request.DATA` will be populated with a `QueryDict` of data, `request.FILES` will be populated with an empty `QueryDict` of data. Parses HTML form content. `request.data` will be populated with a `QueryDict` of data.
You will typically want to use both `FormParser` and `MultiPartParser` together in order to fully support HTML form data. You will typically want to use both `FormParser` and `MultiPartParser` together in order to fully support HTML form data.
...@@ -100,7 +100,7 @@ You will typically want to use both `FormParser` and `MultiPartParser` together ...@@ -100,7 +100,7 @@ You will typically want to use both `FormParser` and `MultiPartParser` together
## MultiPartParser ## MultiPartParser
Parses multipart HTML form content, which supports file uploads. Both `request.DATA` and `request.FILES` will be populated with a `QueryDict`. Parses multipart HTML form content, which supports file uploads. Both `request.data` will be populated with a `QueryDict`.
You will typically want to use both `FormParser` and `MultiPartParser` together in order to fully support HTML form data. You will typically want to use both `FormParser` and `MultiPartParser` together in order to fully support HTML form data.
...@@ -108,7 +108,7 @@ You will typically want to use both `FormParser` and `MultiPartParser` together ...@@ -108,7 +108,7 @@ You will typically want to use both `FormParser` and `MultiPartParser` together
## FileUploadParser ## FileUploadParser
Parses raw file upload content. The `request.DATA` property will be an empty `QueryDict`, and `request.FILES` will be a dictionary with a single key `'file'` containing the uploaded file. Parses raw file upload content. The `request.data` property will be a dictionary with a single key `'file'` containing the uploaded file.
If the view used with `FileUploadParser` is called with a `filename` URL keyword argument, then that argument will be used as the filename. If it is called without a `filename` URL keyword argument, then the client must set the filename in the `Content-Disposition` HTTP header. For example `Content-Disposition: attachment; filename=upload.jpg`. If the view used with `FileUploadParser` is called with a `filename` URL keyword argument, then that argument will be used as the filename. If it is called without a `filename` URL keyword argument, then the client must set the filename in the `Content-Disposition` HTTP header. For example `Content-Disposition: attachment; filename=upload.jpg`.
...@@ -126,7 +126,7 @@ If the view used with `FileUploadParser` is called with a `filename` URL keyword ...@@ -126,7 +126,7 @@ If the view used with `FileUploadParser` is called with a `filename` URL keyword
parser_classes = (FileUploadParser,) parser_classes = (FileUploadParser,)
def put(self, request, filename, format=None): def put(self, request, filename, format=None):
file_obj = request.FILES['file'] file_obj = request.data['file']
# ... # ...
# do some staff with uploaded file # do some staff with uploaded file
# ... # ...
...@@ -139,7 +139,7 @@ If the view used with `FileUploadParser` is called with a `filename` URL keyword ...@@ -139,7 +139,7 @@ If the view used with `FileUploadParser` is called with a `filename` URL keyword
To implement a custom parser, you should override `BaseParser`, set the `.media_type` property, and implement the `.parse(self, stream, media_type, parser_context)` method. To implement a custom parser, you should override `BaseParser`, set the `.media_type` property, and implement the `.parse(self, stream, media_type, parser_context)` method.
The method should return the data that will be used to populate the `request.DATA` property. The method should return the data that will be used to populate the `request.data` property.
The arguments passed to `.parse()` are: The arguments passed to `.parse()` are:
...@@ -161,7 +161,7 @@ By default this will include the following keys: `view`, `request`, `args`, `kwa ...@@ -161,7 +161,7 @@ By default this will include the following keys: `view`, `request`, `args`, `kwa
## Example ## Example
The following is an example plaintext parser that will populate the `request.DATA` property with a string representing the body of the request. The following is an example plaintext parser that will populate the `request.data` property with a string representing the body of the request.
class PlainTextParser(BaseParser): class PlainTextParser(BaseParser):
""" """
......
<a class="github" href="permissions.py"></a> source: permissions.py
# Permissions # Permissions
......
<a class="github" href="renderers.py"></a> source: renderers.py
# Renderers # Renderers
......
<a class="github" href="request.py"></a> source: request.py
---
**Note**: This is the documentation for the **version 3.0** of REST framework. Documentation for [version 2.4](http://tomchristie.github.io/rest-framework-2-docs/) is also available.
---
# Requests # Requests
...@@ -14,26 +20,29 @@ REST framework's `Request` class extends the standard `HttpRequest`, adding supp ...@@ -14,26 +20,29 @@ REST framework's `Request` class extends the standard `HttpRequest`, adding supp
REST framework's Request objects provide flexible request parsing that allows you to treat requests with JSON data or other media types in the same way that you would normally deal with form data. REST framework's Request objects provide flexible request parsing that allows you to treat requests with JSON data or other media types in the same way that you would normally deal with form data.
## .DATA ## .data
`request.DATA` returns the parsed content of the request body. This is similar to the standard `request.POST` attribute except that: `request.data` returns the parsed content of the request body. This is similar to the standard `request.POST` and `request.FILES` attributes except that:
* It includes all parsed content, including *file and non-file* inputs.
* It supports parsing the content of HTTP methods other than `POST`, meaning that you can access the content of `PUT` and `PATCH` requests. * It supports parsing the content of HTTP methods other than `POST`, meaning that you can access the content of `PUT` and `PATCH` requests.
* It supports REST framework's flexible request parsing, rather than just supporting form data. For example you can handle incoming JSON data in the same way that you handle incoming form data. * It supports REST framework's flexible request parsing, rather than just supporting form data. For example you can handle incoming JSON data in the same way that you handle incoming form data.
For more details see the [parsers documentation]. For more details see the [parsers documentation].
## .FILES ## .query_params
`request.FILES` returns any uploaded files that may be present in the content of the request body. This is the same as the standard `HttpRequest` behavior, except that the same flexible request parsing is used for `request.DATA`. `request.query_params` is a more correctly named synonym for `request.GET`.
For more details see the [parsers documentation]. For clarity inside your code, we recommend using `request.query_params` instead of the Django's standard `request.GET`. Doing so will help keep your codebase more correct and obvious - any HTTP method type may include query parameters, not just `GET` requests.
## .QUERY_PARAMS ## .DATA and .FILES
`request.QUERY_PARAMS` is a more correctly named synonym for `request.GET`. The old-style version 2.x `request.data` and `request.FILES` attributes are still available, but are now pending deprecation in favor of the unified `request.data` attribute.
## .QUERY_PARAMS
For clarity inside your code, we recommend using `request.QUERY_PARAMS` instead of the usual `request.GET`, as *any* HTTP method type may include query parameters. The old-style version 2.x `request.QUERY_PARAMS` attribute is still available, but is now pending deprecation in favor of the more pythonic `request.query_params`.
## .parsers ## .parsers
...@@ -43,7 +52,7 @@ You won't typically need to access this property. ...@@ -43,7 +52,7 @@ You won't typically need to access this property.
--- ---
**Note:** If a client sends malformed content, then accessing `request.DATA` or `request.FILES` may raise a `ParseError`. By default REST framework's `APIView` class or `@api_view` decorator will catch the error and return a `400 Bad Request` response. **Note:** If a client sends malformed content, then accessing `request.data` may raise a `ParseError`. By default REST framework's `APIView` class or `@api_view` decorator will catch the error and return a `400 Bad Request` response.
If a client sends a request with a content-type that cannot be parsed then a `UnsupportedMediaType` exception will be raised, which by default will be caught and return a `415 Unsupported Media Type` response. If a client sends a request with a content-type that cannot be parsed then a `UnsupportedMediaType` exception will be raised, which by default will be caught and return a `415 Unsupported Media Type` response.
......
<a class="github" href="response.py"></a> source: response.py
# Responses # Responses
......
<a class="github" href="reverse.py"></a> source: reverse.py
# Returning URLs # Returning URLs
......
<a class="github" href="routers.py"></a> source: routers.py
# Routers # Routers
......
<a class="github" href="settings.py"></a> source: settings.py
# Settings # Settings
...@@ -51,7 +51,7 @@ Default: ...@@ -51,7 +51,7 @@ Default:
#### DEFAULT_PARSER_CLASSES #### DEFAULT_PARSER_CLASSES
A list or tuple of parser classes, that determines the default set of parsers used when accessing the `request.DATA` property. A list or tuple of parser classes, that determines the default set of parsers used when accessing the `request.data` property.
Default: Default:
......
<a class="github" href="status.py"></a> source: status.py
# Status Codes # Status Codes
......
<a class="github" href="test.py"></a> source: test.py
# Testing # Testing
......
<a class="github" href="throttling.py"></a> source: throttling.py
# Throttling # Throttling
......
<a class="github" href="validators.py"></a> source: validators.py
---
**Note**: This is the documentation for the **version 3.0** of REST framework. Documentation for [version 2.4](http://tomchristie.github.io/rest-framework-2-docs/) is also available.
---
# Validators # Validators
......
<a class="github" href="decorators.py"></a> <a class="github" href="views.py"></a> source: decorators.py
views.py
# Class Based Views # Class Based Views
...@@ -126,19 +127,26 @@ REST framework also allows you to work with regular function based views. It pr ...@@ -126,19 +127,26 @@ REST framework also allows you to work with regular function based views. It pr
## @api_view() ## @api_view()
**Signature:** `@api_view(http_method_names)` **Signature:** `@api_view(http_method_names=['GET'])`
The core of this functionality is the `api_view` decorator, which takes a list of HTTP methods that your view should respond to. For example, this is how you would write a very simple view that just manually returns some data: The core of this functionality is the `api_view` decorator, which takes a list of HTTP methods that your view should respond to. For example, this is how you would write a very simple view that just manually returns some data:
from rest_framework.decorators import api_view from rest_framework.decorators import api_view
@api_view(['GET']) @api_view()
def hello_world(request): def hello_world(request):
return Response({"message": "Hello, world!"}) return Response({"message": "Hello, world!"})
This view will use the default renderers, parsers, authentication classes etc specified in the [settings]. This view will use the default renderers, parsers, authentication classes etc specified in the [settings].
By default only `GET` methods will be accepted. Other methods will respond with "405 Method Not Allowed". To alter this behavior, specify which methods the view allows, like so:
@api_view(['GET', 'POST'])
def hello_world(request):
if request.method == 'POST':
return Response({"message": "Got some data!", "data": request.data})
return Response({"message": "Hello, world!"})
## API policy decorators ## API policy decorators
To override the default settings, REST framework provides a set of additional decorators which can be added to your views. These must come *after* (below) the `@api_view` decorator. For example, to create a view that uses a [throttle][throttling] to ensure it can only be called once per day by a particular user, use the `@throttle_classes` decorator, passing a list of throttle classes: To override the default settings, REST framework provides a set of additional decorators which can be added to your views. These must come *after* (below) the `@api_view` decorator. For example, to create a view that uses a [throttle][throttling] to ensure it can only be called once per day by a particular user, use the `@throttle_classes` decorator, passing a list of throttle classes:
......
<a class="github" href="viewsets.py"></a> source: viewsets.py
# ViewSets # ViewSets
...@@ -124,7 +124,7 @@ For example: ...@@ -124,7 +124,7 @@ For example:
@detail_route(methods=['post']) @detail_route(methods=['post'])
def set_password(self, request, pk=None): def set_password(self, request, pk=None):
user = self.get_object() user = self.get_object()
serializer = PasswordSerializer(data=request.DATA) serializer = PasswordSerializer(data=request.data)
if serializer.is_valid(): if serializer.is_valid():
user.set_password(serializer.data['password']) user.set_password(serializer.data['password'])
user.save() user.save()
......
...@@ -192,6 +192,18 @@ body{ ...@@ -192,6 +192,18 @@ body{
.navbar .navbar-inner .dropdown-menu li a, .navbar .navbar-inner .dropdown-menu li{ .navbar .navbar-inner .dropdown-menu li a, .navbar .navbar-inner .dropdown-menu li{
color: #A30000; color: #A30000;
} }
.dropdown-menu .active > a,
.dropdown-menu .active > a:hover {
background-image: none;
}
.navbar-inverse .nav .dropdown .active > a,
.navbar-inverse .nav .dropdown .active > a:hover,
.navbar-inverse .nav .dropdown .active > a:focus {
background-color: #eeeeee;
}
.navbar .navbar-inner .dropdown-menu li a:hover{ .navbar .navbar-inner .dropdown-menu li a:hover{
background: #eeeeee; background: #eeeeee;
color: #c20000; color: #c20000;
......
...@@ -9,7 +9,9 @@ ...@@ -9,7 +9,9 @@
--- ---
**Note**: The incoming 3.0 version has now been merged to the `master` branch on GitHub. For the source of the currently available PyPI version, please see the `2.4.4` tag. **Note**: This is the documentation for the **version 3.0** of REST framework. Documentation for [version 2.4](http://tomchristie.github.io/rest-framework-2-docs/) is also available.
For more details see the [3.0 release notes][3.0-announcement].
--- ---
...@@ -26,9 +28,6 @@ ...@@ -26,9 +28,6 @@
<img alt="Django REST Framework" title="Logo by Jake 'Sid' Smith" src="img/logo.png" width="600px" style="display: block; margin: 0 auto 0 auto"> <img alt="Django REST Framework" title="Logo by Jake 'Sid' Smith" src="img/logo.png" width="600px" style="display: block; margin: 0 auto 0 auto">
</p> </p>
<!--
# Django REST framework
-->
Django REST framework is a powerful and flexible toolkit that makes it easy to build Web APIs. Django REST framework is a powerful and flexible toolkit that makes it easy to build Web APIs.
...@@ -204,6 +203,7 @@ General guides to using REST framework. ...@@ -204,6 +203,7 @@ General guides to using REST framework.
* [2.2 Announcement][2.2-announcement] * [2.2 Announcement][2.2-announcement]
* [2.3 Announcement][2.3-announcement] * [2.3 Announcement][2.3-announcement]
* [2.4 Announcement][2.4-announcement] * [2.4 Announcement][2.4-announcement]
* [3.0 Announcement][3.0-announcement]
* [Kickstarter Announcement][kickstarter-announcement] * [Kickstarter Announcement][kickstarter-announcement]
* [Release Notes][release-notes] * [Release Notes][release-notes]
* [Credits][credits] * [Credits][credits]
...@@ -297,7 +297,7 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ...@@ -297,7 +297,7 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
[serializers]: api-guide/serializers.md [serializers]: api-guide/serializers.md
[fields]: api-guide/fields.md [fields]: api-guide/fields.md
[relations]: api-guide/relations.md [relations]: api-guide/relations.md
[validation]: api-guide/validation.md [validators]: api-guide/validators.md
[authentication]: api-guide/authentication.md [authentication]: api-guide/authentication.md
[permissions]: api-guide/permissions.md [permissions]: api-guide/permissions.md
[throttling]: api-guide/throttling.md [throttling]: api-guide/throttling.md
...@@ -322,6 +322,7 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ...@@ -322,6 +322,7 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
[2.2-announcement]: topics/2.2-announcement.md [2.2-announcement]: topics/2.2-announcement.md
[2.3-announcement]: topics/2.3-announcement.md [2.3-announcement]: topics/2.3-announcement.md
[2.4-announcement]: topics/2.4-announcement.md [2.4-announcement]: topics/2.4-announcement.md
[3.0-announcement]: topics/3.0-announcement.md
[kickstarter-announcement]: topics/kickstarter-announcement.md [kickstarter-announcement]: topics/kickstarter-announcement.md
[release-notes]: topics/release-notes.md [release-notes]: topics/release-notes.md
[credits]: topics/credits.md [credits]: topics/credits.md
......
...@@ -130,34 +130,24 @@ You can override the `BrowsableAPIRenderer.get_context()` method to customise th ...@@ -130,34 +130,24 @@ You can override the `BrowsableAPIRenderer.get_context()` method to customise th
For more advanced customization, such as not having a Bootstrap basis or tighter integration with the rest of your site, you can simply choose not to have `api.html` extend `base.html`. Then the page content and capabilities are entirely up to you. For more advanced customization, such as not having a Bootstrap basis or tighter integration with the rest of your site, you can simply choose not to have `api.html` extend `base.html`. Then the page content and capabilities are entirely up to you.
#### Autocompletion #### Handling `ChoiceField` with large numbers of items.
When a `ChoiceField` has too many items, rendering the widget containing all the options can become very slow, and cause the browsable API rendering to perform poorly. One solution is to replace the selector by an autocomplete widget, that only loads and renders a subset of the available options as needed. When a relationship or `ChoiceField` has too many items, rendering the widget containing all the options can become very slow, and cause the browsable API rendering to perform poorly.
There are [a variety of packages for autocomplete widgets][autocomplete-packages], such as [django-autocomplete-light][django-autocomplete-light]. To setup `django-autocomplete-light`, follow the [installation documentation][django-autocomplete-light-install], add the the following to the `api.html` template: The simplest option in this case is to replace the select input with a standard text input. For example:
{% block script %} author = serializers.HyperlinkedRelatedField(
{{ block.super }} queryset=User.objects.all(),
{% include 'autocomplete_light/static.html' %} style={'base_template': 'input.html'}
{% endblock %}
You can now add the `autocomplete_light.ChoiceWidget` widget to the serializer field.
import autocomplete_light
class BookSerializer(serializers.ModelSerializer):
author = serializers.ChoiceField(
widget=autocomplete_light.ChoiceWidget('AuthorAutocomplete')
) )
class Meta: #### Autocomplete
model = Book
--- An alternative, but more complex option would be to replace the input with an autocomplete widget, that only loads and renders a subset of the available options as needed. If you need to do this you'll need to do some work to build a custom autocomplete HTML template yourself.
![Autocomplete][autocomplete-image] There are [a variety of packages for autocomplete widgets][autocomplete-packages], such as [django-autocomplete-light][django-autocomplete-light], that you may want to refer to. Note that you will not be able to simply include these components as standard widgets, but will need to write the HTML template explicitly. This is because REST framework 3.0 no longer supports the `widget` keyword argument since it now uses templated HTML generation.
*Screenshot of the autocomplete-light widget* Better support for autocomplete inputs is planned in future versions.
--- ---
...@@ -175,4 +165,3 @@ You can now add the `autocomplete_light.ChoiceWidget` widget to the serializer f ...@@ -175,4 +165,3 @@ You can now add the `autocomplete_light.ChoiceWidget` widget to the serializer f
[autocomplete-packages]: https://www.djangopackages.com/grids/g/auto-complete/ [autocomplete-packages]: https://www.djangopackages.com/grids/g/auto-complete/
[django-autocomplete-light]: https://github.com/yourlabs/django-autocomplete-light [django-autocomplete-light]: https://github.com/yourlabs/django-autocomplete-light
[django-autocomplete-light-install]: http://django-autocomplete-light.readthedocs.org/en/latest/#install [django-autocomplete-light-install]: http://django-autocomplete-light.readthedocs.org/en/latest/#install
[autocomplete-image]: ../img/autocomplete.png
...@@ -135,15 +135,15 @@ There are many great Markdown editors that make working with the documentation r ...@@ -135,15 +135,15 @@ There are many great Markdown editors that make working with the documentation r
## Building the documentation ## Building the documentation
To build the documentation, simply run the `mkdocs.py` script. To build the documentation, install MkDocs with `pip install mkdocs` and then run the following command.
./mkdocs.py mkdocs build
This will build the html output into the `html` directory. This will build the documentation into the `site` directory.
You can build the documentation and open a preview in a browser window by using the `-p` flag. You can build the documentation and open a preview in a browser window by using the `serve` command.
./mkdocs.py -p mkdocs serve
## Language style ## Language style
...@@ -152,7 +152,6 @@ Documentation should be in American English. The tone of the documentation is v ...@@ -152,7 +152,6 @@ Documentation should be in American English. The tone of the documentation is v
Some other tips: Some other tips:
* Keep paragraphs reasonably short. * Keep paragraphs reasonably short.
* Use double spacing after the end of sentences.
* Don't use abbreviations such as 'e.g.' but instead use the long form, such as 'For example'. * Don't use abbreviations such as 'e.g.' but instead use the long form, such as 'For example'.
## Markdown style ## Markdown style
...@@ -198,21 +197,6 @@ If you want to draw attention to a note or warning, use a pair of enclosing line ...@@ -198,21 +197,6 @@ If you want to draw attention to a note or warning, use a pair of enclosing line
--- ---
# Third party packages
New features to REST framework are generally recommended to be implemented as third party libraries that are developed outside of the core framework. Ideally third party libraries should be properly documented and packaged, and made available on PyPI.
## Getting started
If you have some functionality that you would like to implement as a third party package it's worth contacting the [discussion group][google-group] as others may be willing to get involved. We strongly encourage third party package development and will always try to prioritize time spent helping their development, documentation and packaging.
We recommend the [`django-reusable-app`][django-reusable-app] template as a good resource for getting up and running with implementing a third party Django package.
## Linking to your package
Once your package is decently documented and available on PyPI open a pull request or issue, and we'll add a link to it from the main REST framework documentation. You can add your package under **Third party packages** of the API Guide section that best applies, like [Authentication][authentication] or [Permissions][permissions]. You can also link your package under the [Third Party Resources][third-party-resources] section.
We also suggest adding it to the [REST Framework][rest-framework-grid] grid on Django Packages.
[cite]: http://www.w3.org/People/Berners-Lee/FAQ.html [cite]: http://www.w3.org/People/Berners-Lee/FAQ.html
[code-of-conduct]: https://www.djangoproject.com/conduct/ [code-of-conduct]: https://www.djangoproject.com/conduct/
...@@ -226,8 +210,3 @@ We also suggest adding it to the [REST Framework][rest-framework-grid] grid on D ...@@ -226,8 +210,3 @@ We also suggest adding it to the [REST Framework][rest-framework-grid] grid on D
[markdown]: http://daringfireball.net/projects/markdown/basics [markdown]: http://daringfireball.net/projects/markdown/basics
[docs]: https://github.com/tomchristie/django-rest-framework/tree/master/docs [docs]: https://github.com/tomchristie/django-rest-framework/tree/master/docs
[mou]: http://mouapp.com/ [mou]: http://mouapp.com/
[django-reusable-app]: https://github.com/dabapps/django-reusable-app
[authentication]: ../api-guide/authentication.md
[permissions]: ../api-guide/permissions.md
[third-party-resources]: third-party-resources.md
[rest-framework-grid]: https://www.djangopackages.com/grids/g/django-rest-framework/
...@@ -442,7 +442,7 @@ The security vulnerabilities only affect APIs which use the `XMLParser` class, b ...@@ -442,7 +442,7 @@ The security vulnerabilities only affect APIs which use the `XMLParser` class, b
* Bugfix: Validation errors instead of exceptions when related fields receive incorrect types. * Bugfix: Validation errors instead of exceptions when related fields receive incorrect types.
* Bugfix: Handle ObjectDoesNotExist exception when serializing null reverse one-to-one * Bugfix: Handle ObjectDoesNotExist exception when serializing null reverse one-to-one
**Note**: Prior to 2.1.16, The Decimals would render in JSON using floating point if `simplejson` was installed, but otherwise render using string notation. Now that use of `simplejson` has been deprecated, Decimals will consistently render using string notation. See [#582] for more details. **Note**: Prior to 2.1.16, The Decimals would render in JSON using floating point if `simplejson` was installed, but otherwise render using string notation. Now that use of `simplejson` has been deprecated, Decimals will consistently render using string notation. See [ticket 582](ticket-582) for more details.
### 2.1.15 ### 2.1.15
...@@ -614,122 +614,7 @@ This change will not affect user code, so long as it's following the recommended ...@@ -614,122 +614,7 @@ This change will not affect user code, so long as it's following the recommended
* **Fix all of the things.** (Well, almost.) * **Fix all of the things.** (Well, almost.)
* For more information please see the [2.0 announcement][announcement]. * For more information please see the [2.0 announcement][announcement].
--- For older release notes, [please see the GitHub repo](old-release-notes).
## 0.4.x series
### 0.4.0
* Supports Django 1.5.
* Fixes issues with 'HEAD' method.
* Allow views to specify template used by TemplateRenderer
* More consistent error responses
* Some serializer fixes
* Fix internet explorer ajax behavior
* Minor xml and yaml fixes
* Improve setup (e.g. use staticfiles, not the defunct ADMIN_MEDIA_PREFIX)
* Sensible absolute URL generation, not using hacky set_script_prefix
---
## 0.3.x series
### 0.3.3
* Added DjangoModelPermissions class to support `django.contrib.auth` style permissions.
* Use `staticfiles` for css files.
- Easier to override. Won't conflict with customized admin styles (e.g. grappelli)
* Templates are now nicely namespaced.
- Allows easier overriding.
* Drop implied 'pk' filter if last arg in urlconf is unnamed.
- Too magical. Explicit is better than implicit.
* Saner template variable auto-escaping.
* Tidier setup.py
* Updated for URLObject 2.0
* Bugfixes:
- Bug with PerUserThrottling when user contains unicode chars.
### 0.3.2
* Bugfixes:
* Fix 403 for POST and PUT from the UI with UserLoggedInAuthentication (#115)
* serialize_model method in serializer.py may cause wrong value (#73)
* Fix Error when clicking OPTIONS button (#146)
* And many other fixes
* Remove short status codes
- Zen of Python: "There should be one-- and preferably only one --obvious way to do it."
* get_name, get_description become methods on the view - makes them overridable.
* Improved model mixin API - Hooks for build_query, get_instance_data, get_model, get_queryset, get_ordering
### 0.3.1
* [not documented]
### 0.3.0
* JSONP Support
* Bugfixes, including support for latest markdown release
---
## 0.2.x series
### 0.2.4
* Fix broken IsAdminUser permission.
* OPTIONS support.
* XMLParser.
* Drop mentions of Blog, BitBucket.
### 0.2.3
* Fix some throttling bugs.
* ``X-Throttle`` header on throttling.
* Support for nesting resources on related models.
### 0.2.2
* Throttling support complete.
### 0.2.1
* Couple of simple bugfixes over 0.2.0
### 0.2.0
* Big refactoring changes since 0.1.0, ask on the discussion group if anything isn't clear.
The public API has been massively cleaned up. Expect it to be fairly stable from here on in.
* ``Resource`` becomes decoupled into ``View`` and ``Resource``, your views should now inherit from ``View``, not ``Resource``.
* The handler functions on views ``.get() .put() .post()`` etc, no longer have the ``content`` and ``auth`` args.
Use ``self.CONTENT`` inside a view to access the deserialized, validated content.
Use ``self.user`` inside a view to access the authenticated user.
* ``allowed_methods`` and ``anon_allowed_methods`` are now defunct. if a method is defined, it's available.
The ``permissions`` attribute on a ``View`` is now used to provide generic permissions checking.
Use permission classes such as ``FullAnonAccess``, ``IsAuthenticated`` or ``IsUserOrIsAnonReadOnly`` to set the permissions.
* The ``authenticators`` class becomes ``authentication``. Class names change to ``Authentication``.
* The ``emitters`` class becomes ``renderers``. Class names change to ``Renderers``.
* ``ResponseException`` becomes ``ErrorResponse``.
* The mixin classes have been nicely refactored, the basic mixins are now ``RequestMixin``, ``ResponseMixin``, ``AuthMixin``, and ``ResourceMixin``
You can reuse these mixin classes individually without using the ``View`` class.
---
## 0.1.x series
### 0.1.1
* Final build before pulling in all the refactoring changes for 0.2, in case anyone needs to hang on to 0.1.
### 0.1.0
* Initial release.
[cite]: http://www.catb.org/~esr/writings/cathedral-bazaar/cathedral-bazaar/ar01s04.html [cite]: http://www.catb.org/~esr/writings/cathedral-bazaar/cathedral-bazaar/ar01s04.html
[deprecation-policy]: #deprecation-policy [deprecation-policy]: #deprecation-policy
...@@ -742,5 +627,6 @@ This change will not affect user code, so long as it's following the recommended ...@@ -742,5 +627,6 @@ This change will not affect user code, so long as it's following the recommended
[staticfiles13]: https://docs.djangoproject.com/en/1.3/howto/static-files/#with-a-template-tag [staticfiles13]: https://docs.djangoproject.com/en/1.3/howto/static-files/#with-a-template-tag
[2.1.0-notes]: https://groups.google.com/d/topic/django-rest-framework/Vv2M0CMY9bg/discussion [2.1.0-notes]: https://groups.google.com/d/topic/django-rest-framework/Vv2M0CMY9bg/discussion
[announcement]: rest-framework-2-announcement.md [announcement]: rest-framework-2-announcement.md
[#582]: https://github.com/tomchristie/django-rest-framework/issues/582 [ticket-582]: https://github.com/tomchristie/django-rest-framework/issues/582
[rfc-6266]: http://tools.ietf.org/html/rfc6266#section-4.3 [rfc-6266]: http://tools.ietf.org/html/rfc6266#section-4.3
[old-release-notes]: https://github.com/tomchristie/django-rest-framework/blob/2.4.4/docs/topics/release-notes.md#04x-series
...@@ -4,9 +4,9 @@ ...@@ -4,9 +4,9 @@
> >
> &mdash; Mike Amundsen, [REST fest 2012 keynote][cite]. > &mdash; Mike Amundsen, [REST fest 2012 keynote][cite].
First off, the disclaimer. The name "Django REST framework" was chosen simply to sure the project would be easily found by developers. Throughout the documentation we try to use the more simple and technically correct terminology of "Web APIs". First off, the disclaimer. The name "Django REST framework" was decided back in early 2011 and was chosen simply to sure the project would be easily found by developers. Throughout the documentation we try to use the more simple and technically correct terminology of "Web APIs".
If you are serious about designing a Hypermedia APIs, you should look to resources outside of this documentation to help inform your design choices. If you are serious about designing a Hypermedia API, you should look to resources outside of this documentation to help inform your design choices.
The following fall into the "required reading" category. The following fall into the "required reading" category.
......
...@@ -206,7 +206,7 @@ Open the file `snippets/serializers.py` again, and edit the `SnippetSerializer` ...@@ -206,7 +206,7 @@ Open the file `snippets/serializers.py` again, and edit the `SnippetSerializer`
model = Snippet model = Snippet
fields = ('id', 'title', 'code', 'linenos', 'language', 'style') fields = ('id', 'title', 'code', 'linenos', 'language', 'style')
Once nice property that serializers have is that you can inspect all the fields an serializer instance, by printing it's representation. Open the Django shell with `python manange.py shell`, then try the following: One nice property that serializers have is that you can inspect all the fields in a serializer instance, by printing it's representation. Open the Django shell with `python manange.py shell`, then try the following:
>>> from snippets.serializers import SnippetSerializer >>> from snippets.serializers import SnippetSerializer
>>> serializer = SnippetSerializer() >>> serializer = SnippetSerializer()
...@@ -219,7 +219,7 @@ Once nice property that serializers have is that you can inspect all the fields ...@@ -219,7 +219,7 @@ Once nice property that serializers have is that you can inspect all the fields
language = ChoiceField(choices=[('Clipper', 'FoxPro'), ('Cucumber', 'Gherkin'), ('RobotFramework', 'RobotFramework'), ('abap', 'ABAP'), ('ada', 'Ada')... language = ChoiceField(choices=[('Clipper', 'FoxPro'), ('Cucumber', 'Gherkin'), ('RobotFramework', 'RobotFramework'), ('abap', 'ABAP'), ('ada', 'Ada')...
style = ChoiceField(choices=[('autumn', 'autumn'), ('borland', 'borland'), ('bw', 'bw'), ('colorful', 'colorful')... style = ChoiceField(choices=[('autumn', 'autumn'), ('borland', 'borland'), ('bw', 'bw'), ('colorful', 'colorful')...
It's important to remember that `ModelSerializer` classes don't do anything particularly magically, they are simply a shortcut to creating a serializer class with: It's important to remember that `ModelSerializer` classes don't do anything particularly magical, they are simply a shortcut for creating serializer classes:
* An automatically determined set of fields. * An automatically determined set of fields.
* Simple default implementations for the `create()` and `update()` methods. * Simple default implementations for the `create()` and `update()` methods.
......
...@@ -5,10 +5,10 @@ Let's introduce a couple of essential building blocks. ...@@ -5,10 +5,10 @@ Let's introduce a couple of essential building blocks.
## Request objects ## Request objects
REST framework introduces a `Request` object that extends the regular `HttpRequest`, and provides more flexible request parsing. The core functionality of the `Request` object is the `request.DATA` attribute, which is similar to `request.POST`, but more useful for working with Web APIs. REST framework introduces a `Request` object that extends the regular `HttpRequest`, and provides more flexible request parsing. The core functionality of the `Request` object is the `request.data` attribute, which is similar to `request.POST`, but more useful for working with Web APIs.
request.POST # Only handles form data. Only works for 'POST' method. request.POST # Only handles form data. Only works for 'POST' method.
request.DATA # Handles arbitrary data. Works for 'POST', 'PUT' and 'PATCH' methods. request.data # Handles arbitrary data. Works for 'POST', 'PUT' and 'PATCH' methods.
## Response objects ## Response objects
...@@ -29,7 +29,7 @@ REST framework provides two wrappers you can use to write API views. ...@@ -29,7 +29,7 @@ REST framework provides two wrappers you can use to write API views.
These wrappers provide a few bits of functionality such as making sure you receive `Request` instances in your view, and adding context to `Response` objects so that content negotiation can be performed. These wrappers provide a few bits of functionality such as making sure you receive `Request` instances in your view, and adding context to `Response` objects so that content negotiation can be performed.
The wrappers also provide behaviour such as returning `405 Method Not Allowed` responses when appropriate, and handling any `ParseError` exception that occurs when accessing `request.DATA` with malformed input. The wrappers also provide behaviour such as returning `405 Method Not Allowed` responses when appropriate, and handling any `ParseError` exception that occurs when accessing `request.data` with malformed input.
## Pulling it all together ## Pulling it all together
...@@ -55,7 +55,7 @@ We don't need our `JSONResponse` class in `views.py` anymore, so go ahead and de ...@@ -55,7 +55,7 @@ We don't need our `JSONResponse` class in `views.py` anymore, so go ahead and de
return Response(serializer.data) return Response(serializer.data)
elif request.method == 'POST': elif request.method == 'POST':
serializer = SnippetSerializer(data=request.DATA) serializer = SnippetSerializer(data=request.data)
if serializer.is_valid(): if serializer.is_valid():
serializer.save() serializer.save()
return Response(serializer.data, status=status.HTTP_201_CREATED) return Response(serializer.data, status=status.HTTP_201_CREATED)
...@@ -80,7 +80,7 @@ Here is the view for an individual snippet, in the `views.py` module. ...@@ -80,7 +80,7 @@ Here is the view for an individual snippet, in the `views.py` module.
return Response(serializer.data) return Response(serializer.data)
elif request.method == 'PUT': elif request.method == 'PUT':
serializer = SnippetSerializer(snippet, data=request.DATA) serializer = SnippetSerializer(snippet, data=request.data)
if serializer.is_valid(): if serializer.is_valid():
serializer.save() serializer.save()
return Response(serializer.data) return Response(serializer.data)
...@@ -92,7 +92,7 @@ Here is the view for an individual snippet, in the `views.py` module. ...@@ -92,7 +92,7 @@ Here is the view for an individual snippet, in the `views.py` module.
This should all feel very familiar - it is not a lot different from working with regular Django views. This should all feel very familiar - it is not a lot different from working with regular Django views.
Notice that we're no longer explicitly tying our requests or responses to a given content type. `request.DATA` can handle incoming `json` requests, but it can also handle `yaml` and other formats. Similarly we're returning response objects with data, but allowing REST framework to render the response into the correct content type for us. Notice that we're no longer explicitly tying our requests or responses to a given content type. `request.data` can handle incoming `json` requests, but it can also handle `yaml` and other formats. Similarly we're returning response objects with data, but allowing REST framework to render the response into the correct content type for us.
## Adding optional format suffixes to our URLs ## Adding optional format suffixes to our URLs
......
...@@ -24,7 +24,7 @@ We'll start by rewriting the root view as a class based view. All this involves ...@@ -24,7 +24,7 @@ We'll start by rewriting the root view as a class based view. All this involves
return Response(serializer.data) return Response(serializer.data)
def post(self, request, format=None): def post(self, request, format=None):
serializer = SnippetSerializer(data=request.DATA) serializer = SnippetSerializer(data=request.data)
if serializer.is_valid(): if serializer.is_valid():
serializer.save() serializer.save()
return Response(serializer.data, status=status.HTTP_201_CREATED) return Response(serializer.data, status=status.HTTP_201_CREATED)
...@@ -49,7 +49,7 @@ So far, so good. It looks pretty similar to the previous case, but we've got be ...@@ -49,7 +49,7 @@ So far, so good. It looks pretty similar to the previous case, but we've got be
def put(self, request, pk, format=None): def put(self, request, pk, format=None):
snippet = self.get_object(pk) snippet = self.get_object(pk)
serializer = SnippetSerializer(snippet, data=request.DATA) serializer = SnippetSerializer(snippet, data=request.data)
if serializer.is_valid(): if serializer.is_valid():
serializer.save() serializer.save()
return Response(serializer.data) return Response(serializer.data)
......
<!DOCTYPE html> <!DOCTYPE html>
<html lang="en"> <html lang="en">
<head><meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<meta charset="utf-8"> <meta charset="utf-8">
<title>Django REST framework - 404 - Page not found</title> <title>Django REST framework - 404 - Page not found</title>
<link href="http://www.django-rest-framework.org/img/favicon.ico" rel="icon" type="image/x-icon"> <link href="http://www.django-rest-framework.org/img/favicon.ico" rel="icon" type="image/x-icon">
<link rel="canonical" href="http://www.django-rest-framework.org/404"/> <link rel="canonical" href="http://www.django-rest-framework.org/404" />
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="description" content="Django, API, REST, 404 - Page not found"> <meta name="description" content="Django, API, REST, 404 - Page not found">
<meta name="author" content="Tom Christie"> <meta name="author" content="Tom Christie">
...@@ -21,20 +23,22 @@ ...@@ -21,20 +23,22 @@
<![endif]--> <![endif]-->
<script type="text/javascript"> <script type="text/javascript">
var _gaq = _gaq || []; var _gaq = _gaq || [];
_gaq.push(['_setAccount', 'UA-18852272-2']); _gaq.push(['_setAccount', 'UA-18852272-2']);
_gaq.push(['_trackPageview']); _gaq.push(['_trackPageview']);
(function() { (function() {
var ga = document.createElement('script'); ga.type = 'text/javascript'; ga.async = true; var ga = document.createElement('script');
ga.type = 'text/javascript';
ga.async = true;
ga.src = ('https:' == document.location.protocol ? 'https://ssl' : 'http://www') + '.google-analytics.com/ga.js'; ga.src = ('https:' == document.location.protocol ? 'https://ssl' : 'http://www') + '.google-analytics.com/ga.js';
var s = document.getElementsByTagName('script')[0]; s.parentNode.insertBefore(ga, s); var s = document.getElementsByTagName('script')[0];
s.parentNode.insertBefore(ga, s);
})(); })();
</script> </script>
</head> </head>
<body onload="prettyPrint()" class="404-page">
<body onload="prettyPrint()" class="404-page">
<div class="wrapper"> <div class="wrapper">
...@@ -121,16 +125,16 @@ ...@@ -121,16 +125,16 @@
</li> </li>
--> -->
</ul> </ul>
</div><!--/.nav-collapse --> </div>
<!--/.nav-collapse -->
</div> </div>
</div> </div>
</div> </div>
<div class="body-content"> <div class="body-content">
<div class="container-fluid"> <div class="container-fluid">
<!-- Search Modal -->
<!-- Search Modal --> <div id="searchModal" class="modal hide fade" tabindex="-1" role="dialog" aria-labelledby="myModalLabel" aria-hidden="true">
<div id="searchModal" class="modal hide fade" tabindex="-1" role="dialog" aria-labelledby="myModalLabel" aria-hidden="true">
<div class="modal-header"> <div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-hidden="true">&times;</button> <button type="button" class="close" data-dismiss="modal" aria-hidden="true">&times;</button>
<h3 id="myModalLabel">Documentation search</h3> <h3 id="myModalLabel">Documentation search</h3>
...@@ -154,23 +158,30 @@ ...@@ -154,23 +158,30 @@
<div class="modal-footer"> <div class="modal-footer">
<button class="btn" data-dismiss="modal" aria-hidden="true">Close</button> <button class="btn" data-dismiss="modal" aria-hidden="true">Close</button>
</div> </div>
</div> </div>
<div class="row-fluid"> <div class="row-fluid">
<div id="main-content" class="span12"> <div id="main-content" class="span12">
<h1 id="404-page-not-found" style="text-align: center">404</h1> <h1 id="404-page-not-found" style="text-align: center">404</h1>
<p style="text-align: center"><strong>Page not found</strong></p> <p style="text-align: center"><strong>Page not found</strong>
</p>
<p style="text-align: center">Try the <a href="http://www.django-rest-framework.org/">homepage</a>, or <a href="#searchModal" data-toggle="modal">search the documentation</a>.</p> <p style="text-align: center">Try the <a href="http://www.django-rest-framework.org/">homepage</a>, or <a href="#searchModal" data-toggle="modal">search the documentation</a>.</p>
</div><!--/span--> </div>
</div><!--/row--> <!--/span-->
</div><!--/.fluid-container--> </div>
</div><!--/.body content--> <!--/row-->
</div>
<!--/.fluid-container-->
</div>
<!--/.body content-->
<div id="push"></div> <div id="push"></div>
</div><!--/.wrapper --> </div>
<!--/.wrapper -->
<footer class="span12"> <footer class="span12">
<p>Sponsored by <a href="http://dabapps.com/">DabApps</a>.</a></p> <p>Sponsored by <a href="http://dabapps.com/">DabApps</a>.</a>
</p>
</footer> </footer>
<!-- Le javascript <!-- Le javascript
...@@ -181,7 +192,9 @@ ...@@ -181,7 +192,9 @@
<script src="http://www.django-rest-framework.org/js/bootstrap-2.1.1-min.js"></script> <script src="http://www.django-rest-framework.org/js/bootstrap-2.1.1-min.js"></script>
<script> <script>
//$('.side-nav').scrollspy() //$('.side-nav').scrollspy()
var shiftWindow = function() { scrollBy(0, -50) }; var shiftWindow = function() {
scrollBy(0, -50)
};
if (location.hash) shiftWindow(); if (location.hash) shiftWindow();
window.addEventListener("hashchange", shiftWindow); window.addEventListener("hashchange", shiftWindow);
...@@ -192,10 +205,12 @@ ...@@ -192,10 +205,12 @@
// Dynamically force sidenav to no higher than browser window // Dynamically force sidenav to no higher than browser window
$('.side-nav').css('max-height', window.innerHeight - 130); $('.side-nav').css('max-height', window.innerHeight - 130);
$(function(){ $(function() {
$(window).resize(function(){ $(window).resize(function() {
$('.side-nav').css('max-height', window.innerHeight - 130); $('.side-nav').css('max-height', window.innerHeight - 130);
}); });
}); });
</script> </script>
</body></html> </body>
</html>
<!DOCTYPE html>
<html lang="en">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<meta charset="utf-8">
<title>{{ page_title }}</title>
<link href="{{ base_url }}/img/favicon.ico" rel="icon" type="image/x-icon">
<link rel="canonical" href="{{ canonical_url }}" />
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="description" content="Django, API, REST, {{ current_page.title }}">
<meta name="author" content="Tom Christie">
<!-- Le styles -->
<link href="{{ base_url }}/css/prettify.css" rel="stylesheet">
<link href="{{ base_url }}/css/bootstrap.css" rel="stylesheet">
<link href="{{ base_url }}/css/bootstrap-responsive.css" rel="stylesheet">
<link href="{{ base_url }}/css/default.css" rel="stylesheet">
<!-- Le HTML5 shim, for IE6-8 support of HTML5 elements -->
<!--[if lt IE 9]>
<script src="http://html5shim.googlecode.com/svn/trunk/html5.js"></script>
<![endif]-->
<script type="text/javascript">
var _gaq = _gaq || [];
_gaq.push(['_setAccount', 'UA-18852272-2']);
_gaq.push(['_trackPageview']);
(function() {
var ga = document.createElement('script');
ga.type = 'text/javascript';
ga.async = true;
ga.src = ('https:' == document.location.protocol ? 'https://ssl' : 'http://www') + '.google-analytics.com/ga.js';
var s = document.getElementsByTagName('script')[0];
s.parentNode.insertBefore(ga, s);
})();
</script>
<style>
span.fusion-wrap a {
display: block;
margin-top: 10px;
color: black;
}
a.fusion-poweredby {
display: block;
margin-top: 10px;
}
@media (max-width: 767px) {
div.promo {
display: none;
}
}
</style>
</head>
<body onload="prettyPrint()" class="{% if current_page.is_homepage %}index{% endif %}-page">
<div class="wrapper">
{% include "nav.html" %}
<div class="body-content">
<div class="container-fluid">
<!-- Search Modal -->
<div id="searchModal" class="modal hide fade" tabindex="-1" role="dialog" aria-labelledby="myModalLabel" aria-hidden="true">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-hidden="true">&times;</button>
<h3 id="myModalLabel">Documentation search</h3>
</div>
<div class="modal-body">
<!-- Custom google search -->
<script>
(function() {
var cx = '015016005043623903336:rxraeohqk6w';
var gcse = document.createElement('script');
gcse.type = 'text/javascript';
gcse.async = true;
gcse.src = (document.location.protocol == 'https:' ? 'https:' : 'http:') +
'//www.google.com/cse/cse.js?cx=' + cx;
var s = document.getElementsByTagName('script')[0];
s.parentNode.insertBefore(gcse, s);
})();
</script>
<gcse:search></gcse:search>
</div>
<div class="modal-footer">
<button class="btn" data-dismiss="modal" aria-hidden="true">Close</button>
</div>
</div>
<div class="row-fluid">
<div class="span3">
<!-- TODO
<p style="margin-top: -12px">
<a class="btn btn-mini btn-primary" style="width: 60px">&laquo; previous</a>
<a class="btn btn-mini btn-primary" style="float: right; margin-right: 8px; width: 60px;">next &raquo;</a>
</p>
-->
<div id="table-of-contents">
<ul class="nav nav-list side-nav well sidebar-nav-fixed">
{% if current_page.is_homepage %}
<li class="main">
<a href="#">Django REST framework</a>
</li>
{% endif %}
{% for toc_item in toc %}
<li class="{% if not current_page.is_homepage %}main{% endif %}">
<a href="{{ toc_item.url }}">{{ toc_item.title }}</a>
</li>
{% for toc_item in toc_item.children %}
<li>
<a href="{{ toc_item.url }}">{{ toc_item.title }}</a>
</li>
{% endfor %}
{% endfor %}
{% if current_page.is_homepage %}
<div class="promo">
<hr/>
<script type="text/javascript" src="//cdn.fusionads.net/fusion.js?zoneid=1332&serve=C6SDP2Y&placement=djangorestframework" id="_fusionads_js"></script>
</div>
{% endif %}
</ul>
</div>
</div>
<div id="main-content" class="span9">
{% if meta.source %}
{% for filename in meta.source %}
<a class="github" href="https://github.com/tomchristie/django-rest-framework/tree/master/rest_framework/{{ filename }}">
<span class="label label-info">{{ filename }}</span>
</a>
{% endfor %}
{% endif %}
{{ content }}
</div>
<!--/span-->
</div>
<!--/row-->
</div>
<!--/.fluid-container-->
</div>
<!--/.body content-->
<div id="push"></div>
</div>
<!--/.wrapper -->
<footer class="span12">
<p>Documentation built with <a href="http://www.mkdocs.org/">MkDocs</a>.</a>
</p>
</footer>
<!-- Le javascript
================================================== -->
<!-- Placed at the end of the document so the pages load faster -->
<script src="{{ base_url }}/js/jquery-1.8.1-min.js"></script>
<script src="{{ base_url }}/js/prettify-1.0.js"></script>
<script src="{{ base_url }}/js/bootstrap-2.1.1-min.js"></script>
<script>
//$('.side-nav').scrollspy()
var shiftWindow = function() {
scrollBy(0, -50)
};
if (location.hash) shiftWindow();
window.addEventListener("hashchange", shiftWindow);
$('.dropdown-menu').on('click touchstart', function(event) {
event.stopPropagation();
});
// Dynamically force sidenav to no higher than browser window
$('.side-nav').css('max-height', window.innerHeight - 130);
$(function() {
$(window).resize(function() {
$('.side-nav').css('max-height', window.innerHeight - 130);
});
});
</script>
</body>
</html>
<div class="navbar navbar-inverse navbar-fixed-top">
<div class="navbar-inner">
<div class="container-fluid">
<a class="repo-link btn btn-primary btn-small" href="https://github.com/tomchristie/django-rest-framework/tree/master">GitHub</a>
<a class="repo-link btn btn-inverse btn-small {% if not next_page %}disabled{% endif %}" rel="prev" {% if next_page %}href="{{ next_page.url }}"{% endif %}>
Next <i class="icon-arrow-right icon-white"></i>
</a>
<a class="repo-link btn btn-inverse btn-small {% if not previous_page %}disabled{% endif %}" rel="next" {% if previous_page %}href="{{ previous_page.url }}"{% endif %}>
<i class="icon-arrow-left icon-white"></i> Previous
</a>
<a class="repo-link btn btn-inverse btn-small" href="#searchModal" data-toggle="modal"><i class="icon-search icon-white"></i> Search</a>
<a class="btn btn-navbar" data-toggle="collapse" data-target=".nav-collapse">
<span class="icon-bar"></span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
</a>
<a class="brand" href="http://www.django-rest-framework.org">Django REST framework</a>
<div class="nav-collapse collapse">
{% if include_nav %}
<!-- Main navigation -->
<ul class="nav navbar-nav">
<li {% if current_page.is_homepage %}class="active"{% endif %}><a href="/">Home</a></li>
{% for nav_item in nav %} {% if nav_item.children %}
<li class="dropdown{% if nav_item.active %} active{% endif %}">
<a href="#" class="dropdown-toggle" data-toggle="dropdown">{{ nav_item.title }} <b class="caret"></b></a>
<ul class="dropdown-menu">
{% for nav_item in nav_item.children %}
<li {% if nav_item.active %}class="active" {% endif %}>
<a href="{{ nav_item.url }}">{{ nav_item.title }}</a>
</li>
{% endfor %}
</ul>
</li>
{% else %}
<li {% if nav_item.active %}class="active" {% endif %}>
<a href="{{ nav_item.url }}">{{ nav_item.title }}</a>
</li>
{% endif %} {% endfor %}
</ul>
{% endif %}
</div>
<!--/.nav-collapse -->
</div>
</div>
</div>
#!/usr/bin/env python
import markdown
import os
import re
import shutil
import sys
root_dir = os.path.abspath(os.path.dirname(__file__))
docs_dir = os.path.join(root_dir, 'docs')
html_dir = os.path.join(root_dir, 'html')
local = not '--deploy' in sys.argv
preview = '-p' in sys.argv
if local:
base_url = 'file://%s/' % os.path.normpath(os.path.join(os.getcwd(), html_dir))
suffix = '.html'
index = 'index.html'
else:
base_url = 'http://www.django-rest-framework.org'
suffix = ''
index = ''
main_header = '<li class="main"><a href="#{{ anchor }}">{{ title }}</a></li>'
sub_header = '<li><a href="#{{ anchor }}">{{ title }}</a></li>'
code_label = r'<a class="github" href="https://github.com/tomchristie/django-rest-framework/tree/master/rest_framework/\1"><span class="label label-info">\1</span></a>'
page = open(os.path.join(docs_dir, 'template.html'), 'r').read()
# Copy static files
# for static in ['css', 'js', 'img']:
# source = os.path.join(docs_dir, 'static', static)
# target = os.path.join(html_dir, static)
# if os.path.exists(target):
# shutil.rmtree(target)
# shutil.copytree(source, target)
# Hacky, but what the hell, it'll do the job
path_list = [
'index.md',
'tutorial/quickstart.md',
'tutorial/1-serialization.md',
'tutorial/2-requests-and-responses.md',
'tutorial/3-class-based-views.md',
'tutorial/4-authentication-and-permissions.md',
'tutorial/5-relationships-and-hyperlinked-apis.md',
'tutorial/6-viewsets-and-routers.md',
'api-guide/requests.md',
'api-guide/responses.md',
'api-guide/views.md',
'api-guide/generic-views.md',
'api-guide/viewsets.md',
'api-guide/routers.md',
'api-guide/parsers.md',
'api-guide/renderers.md',
'api-guide/serializers.md',
'api-guide/fields.md',
'api-guide/relations.md',
'api-guide/validators.md',
'api-guide/authentication.md',
'api-guide/permissions.md',
'api-guide/throttling.md',
'api-guide/filtering.md',
'api-guide/pagination.md',
'api-guide/content-negotiation.md',
'api-guide/format-suffixes.md',
'api-guide/reverse.md',
'api-guide/exceptions.md',
'api-guide/status-codes.md',
'api-guide/testing.md',
'api-guide/settings.md',
'topics/documenting-your-api.md',
'topics/ajax-csrf-cors.md',
'topics/browser-enhancements.md',
'topics/browsable-api.md',
'topics/rest-hypermedia-hateoas.md',
'topics/third-party-resources.md',
'topics/contributing.md',
'topics/rest-framework-2-announcement.md',
'topics/2.2-announcement.md',
'topics/2.3-announcement.md',
'topics/2.4-announcement.md',
'topics/release-notes.md',
'topics/credits.md',
]
prev_url_map = {}
next_url_map = {}
for idx in range(len(path_list)):
path = path_list[idx]
rel = '../' * path.count('/')
if idx == 1 and not local:
# Link back to '/', not '/index'
prev_url_map[path] = '/'
elif idx > 0:
prev_url_map[path] = rel + path_list[idx - 1][:-3] + suffix
if idx < len(path_list) - 1:
next_url_map[path] = rel + path_list[idx + 1][:-3] + suffix
for (dirpath, dirnames, filenames) in os.walk(docs_dir):
relative_dir = dirpath.replace(docs_dir, '').lstrip(os.path.sep)
build_dir = os.path.join(html_dir, relative_dir)
if not os.path.exists(build_dir):
os.makedirs(build_dir)
for filename in filenames:
path = os.path.join(dirpath, filename)
relative_path = os.path.join(relative_dir, filename)
if not filename.endswith('.md'):
if relative_dir:
output_path = os.path.join(build_dir, filename)
shutil.copy(path, output_path)
continue
output_path = os.path.join(build_dir, filename[:-3] + '.html')
toc = ''
text = open(path, 'r').read().decode('utf-8')
main_title = None
description = 'Django, API, REST'
for line in text.splitlines():
if line.startswith('# '):
title = line[2:].strip()
template = main_header
description = description + ', ' + title
elif line.startswith('## '):
title = line[3:].strip()
template = sub_header
else:
continue
if not main_title:
main_title = title
anchor = title.lower().replace(' ', '-').replace(':-', '-').replace("'", '').replace('?', '').replace('.', '')
template = template.replace('{{ title }}', title)
template = template.replace('{{ anchor }}', anchor)
toc += template + '\n'
if filename == 'index.md':
main_title = 'Django REST framework - Web APIs for Django'
else:
main_title = main_title + ' - Django REST framework'
if relative_path == 'index.md':
canonical_url = base_url
else:
canonical_url = base_url + '/' + relative_path[:-3] + suffix
prev_url = prev_url_map.get(relative_path)
next_url = next_url_map.get(relative_path)
content = markdown.markdown(text, ['headerid'])
output = page.replace('{{ content }}', content).replace('{{ toc }}', toc).replace('{{ base_url }}', base_url).replace('{{ suffix }}', suffix).replace('{{ index }}', index)
output = output.replace('{{ title }}', main_title)
output = output.replace('{{ description }}', description)
output = output.replace('{{ page_id }}', filename[:-3])
output = output.replace('{{ canonical_url }}', canonical_url)
if filename =='index.md':
output = output.replace('{{ ad_block }}', """<hr/>
<script type="text/javascript" src="//cdn.fusionads.net/fusion.js?zoneid=1332&serve=C6SDP2Y&placement=djangorestframework" id="_fusionads_js"></script>""")
else:
output = output.replace('{{ ad_block }}', '')
if prev_url:
output = output.replace('{{ prev_url }}', prev_url)
output = output.replace('{{ prev_url_disabled }}', '')
else:
output = output.replace('{{ prev_url }}', '#')
output = output.replace('{{ prev_url_disabled }}', 'disabled')
if next_url:
output = output.replace('{{ next_url }}', next_url)
output = output.replace('{{ next_url_disabled }}', '')
else:
output = output.replace('{{ next_url }}', '#')
output = output.replace('{{ next_url_disabled }}', 'disabled')
output = re.sub(r'a href="([^"]*)\.md"', r'a href="\1%s"' % suffix, output)
output = re.sub(r'<pre><code>:::bash', r'<pre class="prettyprint lang-bsh">', output)
output = re.sub(r'<pre>', r'<pre class="prettyprint lang-py">', output)
output = re.sub(r'<a class="github" href="([^"]*)"></a>', code_label, output)
open(output_path, 'w').write(output.encode('utf-8'))
if preview:
import subprocess
url = 'html/index.html'
try:
subprocess.Popen(["open", url]) # Mac
except OSError:
subprocess.Popen(["xdg-open", url]) # Linux
except:
os.startfile(url) # Windows
site_name: Django REST framework
site_url: http://www.django-rest-framework.org/
site_description: Django REST framework - Web APIs for Django
repo_url: https://github.com/tomchristie/django-rest-framework
theme_dir: docs_theme
pages:
- ['index.md', 'Home']
- ['tutorial/quickstart.md', 'Tutorial', 'Quickstart']
- ['tutorial/1-serialization.md', 'Tutorial', '1 - Serialization']
- ['tutorial/2-requests-and-responses.md', 'Tutorial', '2 - Requests and responses']
- ['tutorial/3-class-based-views.md', 'Tutorial', '3 - Class based views']
- ['tutorial/4-authentication-and-permissions.md', 'Tutorial', '4 - Authentication and permissions']
- ['tutorial/5-relationships-and-hyperlinked-apis.md', 'Tutorial', '5 - Relationships and hyperlinked APIs']
- ['tutorial/6-viewsets-and-routers.md', 'Tutorial', '6 - Viewsets and routers']
- ['api-guide/requests.md', 'API Guide', 'Requests']
- ['api-guide/responses.md', 'API Guide', 'Responses']
- ['api-guide/views.md', 'API Guide', 'Views']
- ['api-guide/generic-views.md', 'API Guide', 'Generic views']
- ['api-guide/viewsets.md', 'API Guide', 'Viewsets']
- ['api-guide/routers.md', 'API Guide', 'Routers']
- ['api-guide/parsers.md', 'API Guide', 'Parsers']
- ['api-guide/renderers.md', 'API Guide', 'Renderers']
- ['api-guide/serializers.md', 'API Guide', 'Serializers']
- ['api-guide/fields.md', 'API Guide', 'Serializer fields']
- ['api-guide/relations.md', 'API Guide', 'Serializer relations']
- ['api-guide/validators.md', 'API Guide', 'Validators']
- ['api-guide/authentication.md', 'API Guide', 'Authentication']
- ['api-guide/permissions.md', 'API Guide', 'Permissions']
- ['api-guide/throttling.md', 'API Guide', 'Throttling']
- ['api-guide/filtering.md', 'API Guide', 'Filtering']
- ['api-guide/pagination.md', 'API Guide', 'Pagination']
- ['api-guide/content-negotiation.md', 'API Guide', 'Content negotiation']
- ['api-guide/format-suffixes.md', 'API Guide', 'Format suffixes']
- ['api-guide/reverse.md', 'API Guide', 'Returning URLs']
- ['api-guide/exceptions.md', 'API Guide', 'Exceptions']
- ['api-guide/status-codes.md', 'API Guide', 'Status codes']
- ['api-guide/testing.md', 'API Guide', 'Testing']
- ['api-guide/settings.md', 'API Guide', 'Settings']
- ['topics/documenting-your-api.md', 'Topics', 'Documenting your API']
- ['topics/ajax-csrf-cors.md', 'Topics', 'AJAX, CSRF & CORS']
- ['topics/browser-enhancements.md', 'Topics',]
- ['topics/browsable-api.md', 'Topics', 'The Browsable API']
- ['topics/rest-hypermedia-hateoas.md', 'Topics', 'REST, Hypermedia & HATEOAS']
- ['topics/third-party-resources.md', 'Topics', 'Third Party Resources']
- ['topics/contributing.md', 'Topics', 'Contributing to REST framework']
- ['topics/rest-framework-2-announcement.md', 'Topics', '2.0 Announcement']
- ['topics/2.2-announcement.md', 'Topics', '2.2 Announcement']
- ['topics/2.3-announcement.md', 'Topics', '2.3 Announcement']
- ['topics/2.4-announcement.md', 'Topics', '2.4 Announcement']
- ['topics/3.0-announcement.md', 'Topics', '3.0 Announcement']
- ['topics/kickstarter-announcement.md', 'Topics', 'Kickstarter Announcement']
- ['topics/release-notes.md', 'Topics', 'Release Notes']
- ['topics/credits.md', 'Topics', 'Credits']
from rest_framework.views import APIView from rest_framework.views import APIView
from rest_framework import status
from rest_framework import parsers from rest_framework import parsers
from rest_framework import renderers from rest_framework import renderers
from rest_framework.response import Response from rest_framework.response import Response
...@@ -12,16 +11,13 @@ class ObtainAuthToken(APIView): ...@@ -12,16 +11,13 @@ class ObtainAuthToken(APIView):
permission_classes = () permission_classes = ()
parser_classes = (parsers.FormParser, parsers.MultiPartParser, parsers.JSONParser,) parser_classes = (parsers.FormParser, parsers.MultiPartParser, parsers.JSONParser,)
renderer_classes = (renderers.JSONRenderer,) renderer_classes = (renderers.JSONRenderer,)
serializer_class = AuthTokenSerializer
model = Token
def post(self, request): def post(self, request):
serializer = self.serializer_class(data=request.data) serializer = AuthTokenSerializer(data=request.data)
if serializer.is_valid(): serializer.is_valid(raise_exception=True)
user = serializer.validated_data['user'] user = serializer.validated_data['user']
token, created = Token.objects.get_or_create(user=user) token, created = Token.objects.get_or_create(user=user)
return Response({'token': token.key}) return Response({'token': token.key})
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
obtain_auth_token = ObtainAuthToken.as_view() obtain_auth_token = ObtainAuthToken.as_view()
...@@ -12,12 +12,14 @@ from rest_framework.views import APIView ...@@ -12,12 +12,14 @@ from rest_framework.views import APIView
import types import types
def api_view(http_method_names): def api_view(http_method_names=None):
""" """
Decorator that converts a function-based view into an APIView subclass. Decorator that converts a function-based view into an APIView subclass.
Takes a list of allowed methods for the view as an argument. Takes a list of allowed methods for the view as an argument.
""" """
if http_method_names is None:
http_method_names = ['GET']
def decorator(func): def decorator(func):
......
...@@ -69,7 +69,7 @@ def get_attribute(instance, attrs): ...@@ -69,7 +69,7 @@ def get_attribute(instance, attrs):
except (KeyError, TypeError, AttributeError): except (KeyError, TypeError, AttributeError):
raise exc raise exc
if is_simple_callable(instance): if is_simple_callable(instance):
return instance() instance = instance()
return instance return instance
...@@ -262,7 +262,11 @@ class Field(object): ...@@ -262,7 +262,11 @@ class Field(object):
if html.is_html_input(dictionary): if html.is_html_input(dictionary):
# HTML forms will represent empty fields as '', and cannot # HTML forms will represent empty fields as '', and cannot
# represent None or False values directly. # represent None or False values directly.
ret = dictionary.get(self.field_name, '') if self.field_name not in dictionary:
if getattr(self.root, 'partial', False):
return empty
return self.default_empty_html
ret = dictionary[self.field_name]
return self.default_empty_html if (ret == '') else ret return self.default_empty_html if (ret == '') else ret
return dictionary.get(self.field_name, empty) return dictionary.get(self.field_name, empty)
...@@ -317,7 +321,6 @@ class Field(object): ...@@ -317,7 +321,6 @@ class Field(object):
value = self.to_internal_value(data) value = self.to_internal_value(data)
self.run_validators(value) self.run_validators(value)
self.validate(value)
return value return value
def run_validators(self, value): def run_validators(self, value):
...@@ -344,9 +347,6 @@ class Field(object): ...@@ -344,9 +347,6 @@ class Field(object):
if errors: if errors:
raise ValidationError(errors) raise ValidationError(errors)
def validate(self, value):
pass
def to_internal_value(self, data): def to_internal_value(self, data):
""" """
Transform the *incoming* primitive data into a native value. Transform the *incoming* primitive data into a native value.
......
...@@ -49,6 +49,21 @@ class RelatedField(Field): ...@@ -49,6 +49,21 @@ class RelatedField(Field):
@classmethod @classmethod
def many_init(cls, *args, **kwargs): def many_init(cls, *args, **kwargs):
"""
This method handles creating a parent `ManyRelatedField` instance
when the `many=True` keyword argument is passed.
Typically you won't need to override this method.
Note that we're over-cautious in passing most arguments to both parent
and child classes in order to try to cover the general case. If you're
overriding this method you'll probably want something much simpler, eg:
@classmethod
def many_init(cls, *args, **kwargs):
kwargs['child'] = cls()
return CustomManyRelatedField(*args, **kwargs)
"""
list_kwargs = {'child_relation': cls(*args, **kwargs)} list_kwargs = {'child_relation': cls(*args, **kwargs)}
for key in kwargs.keys(): for key in kwargs.keys():
if key in MANY_RELATION_KWARGS: if key in MANY_RELATION_KWARGS:
...@@ -306,7 +321,9 @@ class ManyRelatedField(Field): ...@@ -306,7 +321,9 @@ class ManyRelatedField(Field):
The `ManyRelatedField` class is responsible for handling iterating through The `ManyRelatedField` class is responsible for handling iterating through
the values and passing each one to the child relationship. the values and passing each one to the child relationship.
You shouldn't need to be using this class directly yourself. This class is treated as private API.
You shouldn't generally need to be using this class directly yourself,
and should instead simply set 'many=True' on the relationship.
""" """
initial = [] initial = []
default_empty_html = [] default_empty_html = []
......
...@@ -310,7 +310,7 @@ class Request(object): ...@@ -310,7 +310,7 @@ class Request(object):
def _load_data_and_files(self): def _load_data_and_files(self):
""" """
Parses the request content into self.DATA and self.FILES. Parses the request content into `self.data`.
""" """
if not _hasattr(self, '_content_type'): if not _hasattr(self, '_content_type'):
self._load_method_and_content_type() self._load_method_and_content_type()
......
...@@ -5,7 +5,6 @@ it is initialized with unrendered data, instead of a pre-rendered string. ...@@ -5,7 +5,6 @@ it is initialized with unrendered data, instead of a pre-rendered string.
The appropriate renderer is called during Django's template response rendering. The appropriate renderer is called during Django's template response rendering.
""" """
from __future__ import unicode_literals from __future__ import unicode_literals
import django
from django.core.handlers.wsgi import STATUS_CODE_TEXT from django.core.handlers.wsgi import STATUS_CODE_TEXT
from django.template.response import SimpleTemplateResponse from django.template.response import SimpleTemplateResponse
from django.utils import six from django.utils import six
...@@ -16,9 +15,6 @@ class Response(SimpleTemplateResponse): ...@@ -16,9 +15,6 @@ class Response(SimpleTemplateResponse):
An HttpResponse that allows its data to be rendered into An HttpResponse that allows its data to be rendered into
arbitrary media types. arbitrary media types.
""" """
# TODO: remove that once Django 1.3 isn't supported
if django.VERSION >= (1, 4):
rendering_attrs = SimpleTemplateResponse.rendering_attrs + ['_closable_objects']
def __init__(self, data=None, status=None, def __init__(self, data=None, status=None,
template_name=None, headers=None, template_name=None, headers=None,
......
...@@ -11,6 +11,7 @@ python primitives. ...@@ -11,6 +11,7 @@ python primitives.
response content is handled by parsers and renderers. response content is handled by parsers and renderers.
""" """
from django.core.exceptions import ImproperlyConfigured from django.core.exceptions import ImproperlyConfigured
from django.core.exceptions import ValidationError as DjangoValidationError
from django.db import models from django.db import models
from django.db.models.fields import FieldDoesNotExist from django.db.models.fields import FieldDoesNotExist
from django.utils import six from django.utils import six
...@@ -86,6 +87,15 @@ class BaseSerializer(Field): ...@@ -86,6 +87,15 @@ class BaseSerializer(Field):
class when `many=True` is used. You can customize it if you need to class when `many=True` is used. You can customize it if you need to
control which keyword arguments are passed to the parent, and control which keyword arguments are passed to the parent, and
which are passed to the child. which are passed to the child.
Note that we're over-cautious in passing most arguments to both parent
and child classes in order to try to cover the general case. If you're
overriding this method you'll probably want something much simpler, eg:
@classmethod
def many_init(cls, *args, **kwargs):
kwargs['child'] = cls()
return CustomListSerializer(*args, **kwargs)
""" """
child_serializer = cls(*args, **kwargs) child_serializer = cls(*args, **kwargs)
list_kwargs = {'child': child_serializer} list_kwargs = {'child': child_serializer}
...@@ -93,7 +103,9 @@ class BaseSerializer(Field): ...@@ -93,7 +103,9 @@ class BaseSerializer(Field):
(key, value) for key, value in kwargs.items() (key, value) for key, value in kwargs.items()
if key in LIST_SERIALIZER_KWARGS if key in LIST_SERIALIZER_KWARGS
])) ]))
return ListSerializer(*args, **list_kwargs) meta = getattr(cls, 'Meta', None)
list_serializer_class = getattr(meta, 'list_serializer_class', ListSerializer)
return list_serializer_class(*args, **list_kwargs)
def to_internal_value(self, data): def to_internal_value(self, data):
raise NotImplementedError('`to_internal_value()` must be implemented.') raise NotImplementedError('`to_internal_value()` must be implemented.')
...@@ -319,6 +331,14 @@ class Serializer(BaseSerializer): ...@@ -319,6 +331,14 @@ class Serializer(BaseSerializer):
raise ValidationError({ raise ValidationError({
api_settings.NON_FIELD_ERRORS_KEY: [exc.detail] api_settings.NON_FIELD_ERRORS_KEY: [exc.detail]
}) })
except DjangoValidationError as exc:
# Normally you should raise `serializers.ValidationError`
# inside your codebase, but we handle Django's validation
# exception class as well for simpler compat.
# Eg. Calling Model.clean() explictily inside Serializer.validate()
raise ValidationError({
api_settings.NON_FIELD_ERRORS_KEY: list(exc.messages)
})
return value return value
...@@ -342,6 +362,8 @@ class Serializer(BaseSerializer): ...@@ -342,6 +362,8 @@ class Serializer(BaseSerializer):
validated_value = validate_method(validated_value) validated_value = validate_method(validated_value)
except ValidationError as exc: except ValidationError as exc:
errors[field.field_name] = exc.detail errors[field.field_name] = exc.detail
except DjangoValidationError as exc:
errors[field.field_name] = list(exc.messages)
except SkipField: except SkipField:
pass pass
else: else:
...@@ -362,14 +384,9 @@ class Serializer(BaseSerializer): ...@@ -362,14 +384,9 @@ class Serializer(BaseSerializer):
for field in fields: for field in fields:
attribute = field.get_attribute(instance) attribute = field.get_attribute(instance)
if attribute is None: if attribute is None:
value = None ret[field.field_name] = None
else: else:
value = field.to_representation(attribute) ret[field.field_name] = field.to_representation(attribute)
transform_method = getattr(self, 'transform_' + field.field_name, None)
if transform_method is not None:
value = transform_method(value)
ret[field.field_name] = value
return ret return ret
...@@ -548,6 +565,14 @@ class ModelSerializer(Serializer): ...@@ -548,6 +565,14 @@ class ModelSerializer(Serializer):
* A set of default fields are automatically populated. * A set of default fields are automatically populated.
* A set of default validators are automatically populated. * A set of default validators are automatically populated.
* Default `.create()` and `.update()` implementations are provided. * Default `.create()` and `.update()` implementations are provided.
The process of automatically determining a set of serializer fields
based on the model fields is reasonably complex, but you almost certainly
don't need to dig into the implemention.
If the `ModelSerializer` class *doesn't* generate the set of fields that
you need you should either declare the extra/differing fields explicitly on
the serializer class, or simply use a `Serializer` class.
""" """
_field_mapping = ClassLookupDict({ _field_mapping = ClassLookupDict({
models.AutoField: IntegerField, models.AutoField: IntegerField,
...@@ -576,6 +601,26 @@ class ModelSerializer(Serializer): ...@@ -576,6 +601,26 @@ class ModelSerializer(Serializer):
_related_class = PrimaryKeyRelatedField _related_class = PrimaryKeyRelatedField
def create(self, validated_attrs): def create(self, validated_attrs):
"""
We have a bit of extra checking around this in order to provide
descriptive messages when something goes wrong, but this method is
essentially just:
return ExampleModel.objects.create(**validated_attrs)
If there are many to many fields present on the instance then they
cannot be set until the model is instantiated, in which case the
implementation is like so:
example_relationship = validated_attrs.pop('example_relationship')
instance = ExampleModel.objects.create(**validated_attrs)
instance.example_relationship = example_relationship
return instance
The default implementation also does not handle nested relationships.
If you want to support writable nested relationships you'll need
to write an explicit `.create()` method.
"""
# Check that the user isn't trying to handle a writable nested field. # Check that the user isn't trying to handle a writable nested field.
# If we don't do this explicitly they'd likely get a confusing # If we don't do this explicitly they'd likely get a confusing
# error at the point of calling `Model.objects.create()`. # error at the point of calling `Model.objects.create()`.
...@@ -626,14 +671,19 @@ class ModelSerializer(Serializer): ...@@ -626,14 +671,19 @@ class ModelSerializer(Serializer):
return instance return instance
def get_validators(self): def get_validators(self):
# If the validators have been declared explicitly then use that.
validators = getattr(getattr(self, 'Meta', None), 'validators', None)
if validators is not None:
return validators
# Determine the default set of validators.
validators = []
model_class = self.Meta.model
field_names = set([ field_names = set([
field.source for field in self.fields.values() field.source for field in self.fields.values()
if (field.source != '*') and ('.' not in field.source) if (field.source != '*') and ('.' not in field.source)
]) ])
validators = getattr(getattr(self, 'Meta', None), 'validators', [])
model_class = self.Meta.model
# Note that we make sure to check `unique_together` both on the # Note that we make sure to check `unique_together` both on the
# base model class, but also on any parent classes. # base model class, but also on any parent classes.
for parent_class in [model_class] + list(model_class._meta.parents.keys()): for parent_class in [model_class] + list(model_class._meta.parents.keys()):
......
...@@ -6,6 +6,7 @@ relationships and their associated metadata. ...@@ -6,6 +6,7 @@ relationships and their associated metadata.
Usage: `get_field_info(model)` returns a `FieldInfo` instance. Usage: `get_field_info(model)` returns a `FieldInfo` instance.
""" """
from collections import namedtuple from collections import namedtuple
from django.core.exceptions import ImproperlyConfigured
from django.db import models from django.db import models
from django.utils import six from django.utils import six
from rest_framework.compat import OrderedDict from rest_framework.compat import OrderedDict
...@@ -43,7 +44,11 @@ def _resolve_model(obj): ...@@ -43,7 +44,11 @@ def _resolve_model(obj):
""" """
if isinstance(obj, six.string_types) and len(obj.split('.')) == 2: if isinstance(obj, six.string_types) and len(obj.split('.')) == 2:
app_name, model_name = obj.split('.') app_name, model_name = obj.split('.')
return models.get_model(app_name, model_name) resolved_model = models.get_model(app_name, model_name)
if resolved_model is None:
msg = "Django did not return a model for {0}.{1}"
raise ImproperlyConfigured(msg.format(app_name, model_name))
return resolved_model
elif inspect.isclass(obj) and issubclass(obj, models.Model): elif inspect.isclass(obj) and issubclass(obj, models.Model):
return obj return obj
raise ValueError("{0} is not a Django model".format(obj)) raise ValueError("{0} is not a Django model".format(obj))
......
...@@ -142,7 +142,7 @@ class SessionAuthTests(TestCase): ...@@ -142,7 +142,7 @@ class SessionAuthTests(TestCase):
cf. [#1810](https://github.com/tomchristie/django-rest-framework/pull/1810) cf. [#1810](https://github.com/tomchristie/django-rest-framework/pull/1810)
""" """
response = self.csrf_client.get('/auth/login/') response = self.csrf_client.get('/auth/login/')
self.assertContains(response, '<Label class="span4">Username:</label>') self.assertContains(response, '<label class="span4">Username:</label>')
def test_post_form_session_auth_failing_csrf(self): def test_post_form_session_auth_failing_csrf(self):
""" """
......
# from __future__ import unicode_literals
# from django.test import TestCase
# from django.utils import six
# from rest_framework import serializers
# from rest_framework.compat import BytesIO
# import datetime
# class UploadedFile(object):
# def __init__(self, file=None, created=None):
# self.file = file
# self.created = created or datetime.datetime.now()
# class UploadedFileSerializer(serializers.Serializer):
# file = serializers.FileField(required=False)
# created = serializers.DateTimeField()
# def restore_object(self, attrs, instance=None):
# if instance:
# instance.file = attrs['file']
# instance.created = attrs['created']
# return instance
# return UploadedFile(**attrs)
# class FileSerializerTests(TestCase):
# def test_create(self):
# now = datetime.datetime.now()
# file = BytesIO(six.b('stuff'))
# file.name = 'stuff.txt'
# file.size = len(file.getvalue())
# serializer = UploadedFileSerializer(data={'created': now}, files={'file': file})
# uploaded_file = UploadedFile(file=file, created=now)
# self.assertTrue(serializer.is_valid())
# self.assertEqual(serializer.object.created, uploaded_file.created)
# self.assertEqual(serializer.object.file, uploaded_file.file)
# self.assertFalse(serializer.object is uploaded_file)
# def test_creation_failure(self):
# """
# Passing files=None should result in an ValidationError
# Regression test for:
# https://github.com/tomchristie/django-rest-framework/issues/542
# """
# now = datetime.datetime.now()
# serializer = UploadedFileSerializer(data={'created': now})
# self.assertTrue(serializer.is_valid())
# self.assertEqual(serializer.object.created, now)
# self.assertIsNone(serializer.object.file)
# def test_remove_with_empty_string(self):
# """
# Passing empty string as data should cause file to be removed
# Test for:
# https://github.com/tomchristie/django-rest-framework/issues/937
# """
# now = datetime.datetime.now()
# file = BytesIO(six.b('stuff'))
# file.name = 'stuff.txt'
# file.size = len(file.getvalue())
# uploaded_file = UploadedFile(file=file, created=now)
# serializer = UploadedFileSerializer(instance=uploaded_file, data={'created': now, 'file': ''})
# self.assertTrue(serializer.is_valid())
# self.assertEqual(serializer.object.created, uploaded_file.created)
# self.assertIsNone(serializer.object.file)
# def test_validation_error_with_non_file(self):
# """
# Passing non-files should raise a validation error.
# """
# now = datetime.datetime.now()
# errmsg = 'No file was submitted. Check the encoding type on the form.'
# serializer = UploadedFileSerializer(data={'created': now, 'file': 'abc'})
# self.assertFalse(serializer.is_valid())
# self.assertEqual(serializer.errors, {'file': [errmsg]})
# def test_validation_with_no_data(self):
# """
# Validation should still function when no data dictionary is provided.
# """
# uploaded_file = BytesIO(six.b('stuff'))
# uploaded_file.name = 'stuff.txt'
# uploaded_file.size = len(uploaded_file.getvalue())
# serializer = UploadedFileSerializer(files={'file': uploaded_file})
# self.assertFalse(serializer.is_valid())
# from django.core.urlresolvers import reverse
# from django.conf.urls import patterns, url
# from rest_framework import serializers, generics
# from rest_framework.test import APITestCase
# from tests.models import NullableForeignKeySource
# class NullableFKSourceSerializer(serializers.ModelSerializer):
# class Meta:
# model = NullableForeignKeySource
# class NullableFKSourceDetail(generics.RetrieveUpdateDestroyAPIView):
# queryset = NullableForeignKeySource.objects.all()
# serializer_class = NullableFKSourceSerializer
# urlpatterns = patterns(
# '',
# url(r'^objects/(?P<pk>\d+)/$', NullableFKSourceDetail.as_view(), name='object-detail'),
# )
# class NullableForeignKeyTests(APITestCase):
# """
# DRF should be able to handle nullable foreign keys when a test
# Client POST/PUT request is made with its own serialized object.
# """
# urls = 'tests.test_nullable_fields'
# def test_updating_object_with_null_fk(self):
# obj = NullableForeignKeySource(name='example', target=None)
# obj.save()
# serialized_data = NullableFKSourceSerializer(obj).data
# response = self.client.put(reverse('object-detail', args=[obj.pk]), serialized_data)
# self.assertEqual(response.data, serialized_data)
...@@ -134,155 +134,3 @@ class TestSlugRelatedField(APISimpleTestCase): ...@@ -134,155 +134,3 @@ class TestSlugRelatedField(APISimpleTestCase):
def test_representation(self): def test_representation(self):
representation = self.field.to_representation(self.instance) representation = self.field.to_representation(self.instance)
assert representation == self.instance.name assert representation == self.instance.name
# Older tests, for review...
# """
# General tests for relational fields.
# """
# from __future__ import unicode_literals
# from django import get_version
# from django.db import models
# from django.test import TestCase
# from django.utils import unittest
# from rest_framework import serializers
# from tests.models import BlogPost
# class NullModel(models.Model):
# pass
# class FieldTests(TestCase):
# def test_pk_related_field_with_empty_string(self):
# """
# Regression test for #446
# https://github.com/tomchristie/django-rest-framework/issues/446
# """
# field = serializers.PrimaryKeyRelatedField(queryset=NullModel.objects.all())
# self.assertRaises(serializers.ValidationError, field.to_primitive, '')
# self.assertRaises(serializers.ValidationError, field.to_primitive, [])
# def test_hyperlinked_related_field_with_empty_string(self):
# field = serializers.HyperlinkedRelatedField(queryset=NullModel.objects.all(), view_name='')
# self.assertRaises(serializers.ValidationError, field.to_primitive, '')
# self.assertRaises(serializers.ValidationError, field.to_primitive, [])
# def test_slug_related_field_with_empty_string(self):
# field = serializers.SlugRelatedField(queryset=NullModel.objects.all(), slug_field='pk')
# self.assertRaises(serializers.ValidationError, field.to_primitive, '')
# self.assertRaises(serializers.ValidationError, field.to_primitive, [])
# class TestManyRelatedMixin(TestCase):
# def test_missing_many_to_many_related_field(self):
# '''
# Regression test for #632
# https://github.com/tomchristie/django-rest-framework/pull/632
# '''
# field = serializers.RelatedField(many=True, read_only=False)
# into = {}
# field.field_from_native({}, None, 'field_name', into)
# self.assertEqual(into['field_name'], [])
# # Regression tests for #694 (`source` attribute on related fields)
# class RelatedFieldSourceTests(TestCase):
# def test_related_manager_source(self):
# """
# Relational fields should be able to use manager-returning methods as their source.
# """
# BlogPost.objects.create(title='blah')
# field = serializers.RelatedField(many=True, source='get_blogposts_manager')
# class ClassWithManagerMethod(object):
# def get_blogposts_manager(self):
# return BlogPost.objects
# obj = ClassWithManagerMethod()
# value = field.field_to_native(obj, 'field_name')
# self.assertEqual(value, ['BlogPost object'])
# def test_related_queryset_source(self):
# """
# Relational fields should be able to use queryset-returning methods as their source.
# """
# BlogPost.objects.create(title='blah')
# field = serializers.RelatedField(many=True, source='get_blogposts_queryset')
# class ClassWithQuerysetMethod(object):
# def get_blogposts_queryset(self):
# return BlogPost.objects.all()
# obj = ClassWithQuerysetMethod()
# value = field.field_to_native(obj, 'field_name')
# self.assertEqual(value, ['BlogPost object'])
# def test_dotted_source(self):
# """
# Source argument should support dotted.source notation.
# """
# BlogPost.objects.create(title='blah')
# field = serializers.RelatedField(many=True, source='a.b.c')
# class ClassWithQuerysetMethod(object):
# a = {
# 'b': {
# 'c': BlogPost.objects.all()
# }
# }
# obj = ClassWithQuerysetMethod()
# value = field.field_to_native(obj, 'field_name')
# self.assertEqual(value, ['BlogPost object'])
# # Regression for #1129
# def test_exception_for_incorect_fk(self):
# """
# Check that the exception message are correct if the source field
# doesn't exist.
# """
# from tests.models import ManyToManySource
# class Meta:
# model = ManyToManySource
# attrs = {
# 'name': serializers.SlugRelatedField(
# slug_field='name', source='banzai'),
# 'Meta': Meta,
# }
# TestSerializer = type(
# str('TestSerializer'),
# (serializers.ModelSerializer,),
# attrs
# )
# with self.assertRaises(AttributeError):
# TestSerializer(data={'name': 'foo'})
# @unittest.skipIf(get_version() < '1.6.0', 'Upstream behaviour changed in v1.6')
# class RelatedFieldChoicesTests(TestCase):
# """
# Tests for #1408 "Web browseable API doesn't have blank option on drop down list box"
# https://github.com/tomchristie/django-rest-framework/issues/1408
# """
# def test_blank_option_is_added_to_choice_if_required_equals_false(self):
# """
# """
# post = BlogPost(title="Checking blank option is added")
# post.save()
# queryset = BlogPost.objects.all()
# field = serializers.RelatedField(required=False, queryset=queryset)
# choice_count = BlogPost.objects.count()
# widget_count = len(field.widget.choices)
# self.assertEqual(widget_count, choice_count + 1, 'BLANK_CHOICE_DASH option should have been added')
...@@ -411,30 +411,6 @@ class HyperlinkedNullableForeignKeyTests(TestCase): ...@@ -411,30 +411,6 @@ class HyperlinkedNullableForeignKeyTests(TestCase):
] ]
self.assertEqual(serializer.data, expected) self.assertEqual(serializer.data, expected)
# # reverse foreign keys MUST be read_only
# # In the general case they do not provide .remove() or .clear()
# # and cannot be arbitrarily set.
# def test_reverse_foreign_key_update(self):
# data = {'id': 1, 'name': 'target-1', 'sources': [1]}
# instance = ForeignKeyTarget.objects.get(pk=1)
# serializer = ForeignKeyTargetSerializer(instance, data=data)
# print serializer.is_valid()
# print serializer.errors
# print serializer
# self.assertTrue(serializer.is_valid())
# serializer.save()
# self.assertEqual(serializer.data, data)
# # Ensure target 1 is updated, and everything else is as expected
# queryset = ForeignKeyTarget.objects.all()
# serializer = ForeignKeyTargetSerializer(queryset, many=True)
# expected = [
# {'id': 1, 'name': 'target-1', 'sources': [1]},
# {'id': 2, 'name': 'target-2', 'sources': []},
# ]
# self.assertEqual(serializer.data, expected)
class HyperlinkedNullableOneToOneTests(TestCase): class HyperlinkedNullableOneToOneTests(TestCase):
urls = 'tests.test_relations_hyperlink' urls = 'tests.test_relations_hyperlink'
...@@ -455,72 +431,3 @@ class HyperlinkedNullableOneToOneTests(TestCase): ...@@ -455,72 +431,3 @@ class HyperlinkedNullableOneToOneTests(TestCase):
{'url': 'http://testserver/onetoonetarget/2/', 'name': 'target-2', 'nullable_source': None}, {'url': 'http://testserver/onetoonetarget/2/', 'name': 'target-2', 'nullable_source': None},
] ]
self.assertEqual(serializer.data, expected) self.assertEqual(serializer.data, expected)
# # Regression tests for #694 (`source` attribute on related fields)
# class HyperlinkedRelatedFieldSourceTests(TestCase):
# urls = 'tests.test_relations_hyperlink'
# def test_related_manager_source(self):
# """
# Relational fields should be able to use manager-returning methods as their source.
# """
# BlogPost.objects.create(title='blah')
# field = serializers.HyperlinkedRelatedField(
# many=True,
# source='get_blogposts_manager',
# view_name='dummy-url',
# )
# field.context = {'request': request}
# class ClassWithManagerMethod(object):
# def get_blogposts_manager(self):
# return BlogPost.objects
# obj = ClassWithManagerMethod()
# value = field.field_to_native(obj, 'field_name')
# self.assertEqual(value, ['http://testserver/dummyurl/1/'])
# def test_related_queryset_source(self):
# """
# Relational fields should be able to use queryset-returning methods as their source.
# """
# BlogPost.objects.create(title='blah')
# field = serializers.HyperlinkedRelatedField(
# many=True,
# source='get_blogposts_queryset',
# view_name='dummy-url',
# )
# field.context = {'request': request}
# class ClassWithQuerysetMethod(object):
# def get_blogposts_queryset(self):
# return BlogPost.objects.all()
# obj = ClassWithQuerysetMethod()
# value = field.field_to_native(obj, 'field_name')
# self.assertEqual(value, ['http://testserver/dummyurl/1/'])
# def test_dotted_source(self):
# """
# Source argument should support dotted.source notation.
# """
# BlogPost.objects.create(title='blah')
# field = serializers.HyperlinkedRelatedField(
# many=True,
# source='a.b.c',
# view_name='dummy-url',
# )
# field.context = {'request': request}
# class ClassWithQuerysetMethod(object):
# a = {
# 'b': {
# 'c': BlogPost.objects.all()
# }
# }
# obj = ClassWithQuerysetMethod()
# value = field.field_to_native(obj, 'field_name')
# self.assertEqual(value, ['http://testserver/dummyurl/1/'])
...@@ -398,27 +398,6 @@ class PKNullableForeignKeyTests(TestCase): ...@@ -398,27 +398,6 @@ class PKNullableForeignKeyTests(TestCase):
] ]
self.assertEqual(serializer.data, expected) self.assertEqual(serializer.data, expected)
# reverse foreign keys MUST be read_only
# In the general case they do not provide .remove() or .clear()
# and cannot be arbitrarily set.
# def test_reverse_foreign_key_update(self):
# data = {'id': 1, 'name': 'target-1', 'sources': [1]}
# instance = ForeignKeyTarget.objects.get(pk=1)
# serializer = ForeignKeyTargetSerializer(instance, data=data)
# self.assertTrue(serializer.is_valid())
# self.assertEqual(serializer.data, data)
# serializer.save()
# # Ensure target 1 is updated, and everything else is as expected
# queryset = ForeignKeyTarget.objects.all()
# serializer = ForeignKeyTargetSerializer(queryset, many=True)
# expected = [
# {'id': 1, 'name': 'target-1', 'sources': [1]},
# {'id': 2, 'name': 'target-2', 'sources': []},
# ]
# self.assertEqual(serializer.data, expected)
class PKNullableOneToOneTests(TestCase): class PKNullableOneToOneTests(TestCase):
def setUp(self): def setUp(self):
...@@ -437,113 +416,3 @@ class PKNullableOneToOneTests(TestCase): ...@@ -437,113 +416,3 @@ class PKNullableOneToOneTests(TestCase):
{'id': 2, 'name': 'target-2', 'nullable_source': 1}, {'id': 2, 'name': 'target-2', 'nullable_source': 1},
] ]
self.assertEqual(serializer.data, expected) self.assertEqual(serializer.data, expected)
# The below models and tests ensure that serializer fields corresponding
# to a ManyToManyField field with a user-specified ``through`` model are
# set to read only
# class ManyToManyThroughTarget(models.Model):
# name = models.CharField(max_length=100)
# class ManyToManyThrough(models.Model):
# source = models.ForeignKey('ManyToManyThroughSource')
# target = models.ForeignKey(ManyToManyThroughTarget)
# class ManyToManyThroughSource(models.Model):
# name = models.CharField(max_length=100)
# targets = models.ManyToManyField(ManyToManyThroughTarget,
# related_name='sources',
# through='ManyToManyThrough')
# class ManyToManyThroughTargetSerializer(serializers.ModelSerializer):
# class Meta:
# model = ManyToManyThroughTarget
# fields = ('id', 'name', 'sources')
# class ManyToManyThroughSourceSerializer(serializers.ModelSerializer):
# class Meta:
# model = ManyToManyThroughSource
# fields = ('id', 'name', 'targets')
# class PKManyToManyThroughTests(TestCase):
# def setUp(self):
# self.source = ManyToManyThroughSource.objects.create(
# name='through-source-1')
# self.target = ManyToManyThroughTarget.objects.create(
# name='through-target-1')
# def test_many_to_many_create(self):
# data = {'id': 2, 'name': 'source-2', 'targets': [self.target.pk]}
# serializer = ManyToManyThroughSourceSerializer(data=data)
# self.assertTrue(serializer.is_valid())
# obj = serializer.save()
# self.assertEqual(obj.name, 'source-2')
# self.assertEqual(obj.targets.count(), 0)
# def test_many_to_many_reverse_create(self):
# data = {'id': 2, 'name': 'target-2', 'sources': [self.source.pk]}
# serializer = ManyToManyThroughTargetSerializer(data=data)
# self.assertTrue(serializer.is_valid())
# obj = serializer.save()
# self.assertEqual(obj.name, 'target-2')
# self.assertEqual(obj.sources.count(), 0)
# # Regression tests for #694 (`source` attribute on related fields)
# class PrimaryKeyRelatedFieldSourceTests(TestCase):
# def test_related_manager_source(self):
# """
# Relational fields should be able to use manager-returning methods as their source.
# """
# BlogPost.objects.create(title='blah')
# field = serializers.PrimaryKeyRelatedField(many=True, source='get_blogposts_manager')
# class ClassWithManagerMethod(object):
# def get_blogposts_manager(self):
# return BlogPost.objects
# obj = ClassWithManagerMethod()
# value = field.field_to_native(obj, 'field_name')
# self.assertEqual(value, [1])
# def test_related_queryset_source(self):
# """
# Relational fields should be able to use queryset-returning methods as their source.
# """
# BlogPost.objects.create(title='blah')
# field = serializers.PrimaryKeyRelatedField(many=True, source='get_blogposts_queryset')
# class ClassWithQuerysetMethod(object):
# def get_blogposts_queryset(self):
# return BlogPost.objects.all()
# obj = ClassWithQuerysetMethod()
# value = field.field_to_native(obj, 'field_name')
# self.assertEqual(value, [1])
# def test_dotted_source(self):
# """
# Source argument should support dotted.source notation.
# """
# BlogPost.objects.create(title='blah')
# field = serializers.PrimaryKeyRelatedField(many=True, source='a.b.c')
# class ClassWithQuerysetMethod(object):
# a = {
# 'b': {
# 'c': BlogPost.objects.all()
# }
# }
# obj = ClassWithQuerysetMethod()
# value = field.field_to_native(obj, 'field_name')
# self.assertEqual(value, [1])
...@@ -9,7 +9,7 @@ from django.test import TestCase ...@@ -9,7 +9,7 @@ from django.test import TestCase
from django.utils import six, unittest from django.utils import six, unittest
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from rest_framework import status, permissions from rest_framework import status, permissions
from rest_framework.compat import yaml, etree, StringIO from rest_framework.compat import yaml, etree, StringIO, BytesIO
from rest_framework.response import Response from rest_framework.response import Response
from rest_framework.views import APIView from rest_framework.views import APIView
from rest_framework.renderers import BaseRenderer, JSONRenderer, YAMLRenderer, \ from rest_framework.renderers import BaseRenderer, JSONRenderer, YAMLRenderer, \
...@@ -467,7 +467,7 @@ if yaml: ...@@ -467,7 +467,7 @@ if yaml:
obj = {'foo': ['bar', 'baz']} obj = {'foo': ['bar', 'baz']}
renderer = YAMLRenderer() renderer = YAMLRenderer()
content = renderer.render(obj, 'application/yaml') content = renderer.render(obj, 'application/yaml')
self.assertEqual(content, _yaml_repr) self.assertEqual(content.decode('utf-8'), _yaml_repr)
def test_render_and_parse(self): def test_render_and_parse(self):
""" """
...@@ -480,7 +480,7 @@ if yaml: ...@@ -480,7 +480,7 @@ if yaml:
parser = YAMLParser() parser = YAMLParser()
content = renderer.render(obj, 'application/yaml') content = renderer.render(obj, 'application/yaml')
data = parser.parse(StringIO(content)) data = parser.parse(BytesIO(content))
self.assertEqual(obj, data) self.assertEqual(obj, data)
def test_render_decimal(self): def test_render_decimal(self):
...@@ -489,7 +489,7 @@ if yaml: ...@@ -489,7 +489,7 @@ if yaml:
""" """
renderer = YAMLRenderer() renderer = YAMLRenderer()
content = renderer.render({'field': Decimal('111.2')}, 'application/yaml') content = renderer.render({'field': Decimal('111.2')}, 'application/yaml')
self.assertYAMLContains(content, "field: '111.2'") self.assertYAMLContains(content.decode('utf-8'), "field: '111.2'")
def assertYAMLContains(self, content, string): def assertYAMLContains(self, content, string):
self.assertTrue(string in content, '%r not in %r' % (string, content)) self.assertTrue(string in content, '%r not in %r' % (string, content))
...@@ -646,6 +646,7 @@ class CacheRenderTest(TestCase): ...@@ -646,6 +646,7 @@ class CacheRenderTest(TestCase):
""" """
method = getattr(self.client, http_method) method = getattr(self.client, http_method)
resp = method(url) resp = method(url)
resp._closable_objects = []
del resp.client, resp.request del resp.client, resp.request
try: try:
del resp.wsgi_request del resp.wsgi_request
......
...@@ -179,89 +179,6 @@ class TestContentParsing(TestCase): ...@@ -179,89 +179,6 @@ class TestContentParsing(TestCase):
self.assertEqual(request._data, Empty) self.assertEqual(request._data, Empty)
self.assertEqual(request._files, Empty) self.assertEqual(request._files, Empty)
# 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): class MockView(APIView):
authentication_classes = (SessionAuthentication,) authentication_classes = (SessionAuthentication,)
...@@ -301,18 +218,6 @@ class TestContentParsingWithAuthentication(TestCase): ...@@ -301,18 +218,6 @@ class TestContentParsingWithAuthentication(TestCase):
response = self.csrf_client.post('/', content) response = self.csrf_client.post('/', content)
self.assertEqual(status.HTTP_200_OK, response.status_code) 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")
class TestUserSetter(TestCase): class TestUserSetter(TestCase):
......
...@@ -121,161 +121,3 @@ class BulkCreateSerializerTests(TestCase): ...@@ -121,161 +121,3 @@ class BulkCreateSerializerTests(TestCase):
expected_errors = {'non_field_errors': ['Expected a list of items but got type `dict`.']} expected_errors = {'non_field_errors': ['Expected a list of items but got type `dict`.']}
self.assertEqual(serializer.errors, expected_errors) self.assertEqual(serializer.errors, expected_errors)
# class BulkUpdateSerializerTests(TestCase):
# """
# Updating multiple instances using serializers.
# """
# def setUp(self):
# class Book(object):
# """
# A data type that can be persisted to a mock storage backend
# with `.save()` and `.delete()`.
# """
# object_map = {}
# def __init__(self, id, title, author):
# self.id = id
# self.title = title
# self.author = author
# def save(self):
# Book.object_map[self.id] = self
# def delete(self):
# del Book.object_map[self.id]
# class BookSerializer(serializers.Serializer):
# id = serializers.IntegerField()
# title = serializers.CharField(max_length=100)
# author = serializers.CharField(max_length=100)
# def restore_object(self, attrs, instance=None):
# if instance:
# instance.id = attrs['id']
# instance.title = attrs['title']
# instance.author = attrs['author']
# return instance
# return Book(**attrs)
# self.Book = Book
# self.BookSerializer = BookSerializer
# data = [
# {
# 'id': 0,
# 'title': 'The electric kool-aid acid test',
# 'author': 'Tom Wolfe'
# }, {
# 'id': 1,
# 'title': 'If this is a man',
# 'author': 'Primo Levi'
# }, {
# 'id': 2,
# 'title': 'The wind-up bird chronicle',
# 'author': 'Haruki Murakami'
# }
# ]
# for item in data:
# book = Book(item['id'], item['title'], item['author'])
# book.save()
# def books(self):
# """
# Return all the objects in the mock storage backend.
# """
# return self.Book.object_map.values()
# def test_bulk_update_success(self):
# """
# Correct bulk update serialization should return the input data.
# """
# data = [
# {
# 'id': 0,
# 'title': 'The electric kool-aid acid test',
# 'author': 'Tom Wolfe'
# }, {
# 'id': 2,
# 'title': 'Kafka on the shore',
# 'author': 'Haruki Murakami'
# }
# ]
# serializer = self.BookSerializer(self.books(), data=data, many=True, allow_add_remove=True)
# self.assertEqual(serializer.is_valid(), True)
# self.assertEqual(serializer.data, data)
# serializer.save()
# new_data = self.BookSerializer(self.books(), many=True).data
# self.assertEqual(data, new_data)
# def test_bulk_update_and_create(self):
# """
# Bulk update serialization may also include created items.
# """
# data = [
# {
# 'id': 0,
# 'title': 'The electric kool-aid acid test',
# 'author': 'Tom Wolfe'
# }, {
# 'id': 3,
# 'title': 'Kafka on the shore',
# 'author': 'Haruki Murakami'
# }
# ]
# serializer = self.BookSerializer(self.books(), data=data, many=True, allow_add_remove=True)
# self.assertEqual(serializer.is_valid(), True)
# self.assertEqual(serializer.data, data)
# serializer.save()
# new_data = self.BookSerializer(self.books(), many=True).data
# self.assertEqual(data, new_data)
# def test_bulk_update_invalid_create(self):
# """
# Bulk update serialization without allow_add_remove may not create items.
# """
# data = [
# {
# 'id': 0,
# 'title': 'The electric kool-aid acid test',
# 'author': 'Tom Wolfe'
# }, {
# 'id': 3,
# 'title': 'Kafka on the shore',
# 'author': 'Haruki Murakami'
# }
# ]
# expected_errors = [
# {},
# {'non_field_errors': ['Cannot create a new item, only existing items may be updated.']}
# ]
# serializer = self.BookSerializer(self.books(), data=data, many=True)
# self.assertEqual(serializer.is_valid(), False)
# self.assertEqual(serializer.errors, expected_errors)
# def test_bulk_update_error(self):
# """
# Incorrect bulk update serialization should return error data.
# """
# data = [
# {
# 'id': 0,
# 'title': 'The electric kool-aid acid test',
# 'author': 'Tom Wolfe'
# }, {
# 'id': 'foo',
# 'title': 'Kafka on the shore',
# 'author': 'Haruki Murakami'
# }
# ]
# expected_errors = [
# {},
# {'id': ['Enter a whole number.']}
# ]
# serializer = self.BookSerializer(self.books(), data=data, many=True, allow_add_remove=True)
# self.assertEqual(serializer.is_valid(), False)
# self.assertEqual(serializer.errors, expected_errors)
# from django.test import TestCase
# from rest_framework import serializers
# class EmptySerializerTestCase(TestCase):
# def test_empty_serializer(self):
# class FooBarSerializer(serializers.Serializer):
# foo = serializers.IntegerField()
# bar = serializers.SerializerMethodField()
# def get_bar(self, obj):
# return 'bar'
# serializer = FooBarSerializer()
# self.assertEquals(serializer.data, {'foo': 0})
from __future__ import unicode_literals from __future__ import unicode_literals
from django.core.exceptions import ImproperlyConfigured
from django.conf.urls import patterns, url from django.conf.urls import patterns, url
from django.test import TestCase from django.test import TestCase
from django.utils import six from django.utils import six
...@@ -7,6 +8,8 @@ from rest_framework.utils.breadcrumbs import get_breadcrumbs ...@@ -7,6 +8,8 @@ from rest_framework.utils.breadcrumbs import get_breadcrumbs
from rest_framework.views import APIView from rest_framework.views import APIView
from tests.models import BasicModel from tests.models import BasicModel
import rest_framework.utils.model_meta
class Root(APIView): class Root(APIView):
pass pass
...@@ -130,3 +133,34 @@ class ResolveModelTests(TestCase): ...@@ -130,3 +133,34 @@ class ResolveModelTests(TestCase):
def test_resolve_improper_string_representation(self): def test_resolve_improper_string_representation(self):
with self.assertRaises(ValueError): with self.assertRaises(ValueError):
_resolve_model('BasicModel') _resolve_model('BasicModel')
class ResolveModelWithPatchedDjangoTests(TestCase):
"""
Test coverage for when Django's `get_model` returns `None`.
Under certain circumstances Django may return `None` with `get_model`:
http://git.io/get-model-source
It usually happens with circular imports so it is important that DRF
excepts early, otherwise fault happens downstream and is much more
difficult to debug.
"""
def setUp(self):
"""Monkeypatch get_model."""
self.get_model = rest_framework.utils.model_meta.models.get_model
def get_model(app_label, model_name):
return None
rest_framework.utils.model_meta.models.get_model = get_model
def tearDown(self):
"""Revert monkeypatching."""
rest_framework.utils.model_meta.models.get_model = self.get_model
def test_blows_up_if_model_does_not_resolve(self):
with self.assertRaises(ImproperlyConfigured):
_resolve_model('tests.BasicModel')
...@@ -165,17 +165,6 @@ class TestChoiceFieldChoicesValidate(TestCase): ...@@ -165,17 +165,6 @@ class TestChoiceFieldChoicesValidate(TestCase):
except serializers.ValidationError: except serializers.ValidationError:
self.fail("Value %s does not validate" % str(value)) self.fail("Value %s does not validate" % str(value))
# def test_nested_choices(self):
# """
# Make sure a nested value for choices works as expected.
# """
# f = serializers.ChoiceField(choices=self.CHOICES_NESTED)
# value = self.CHOICES_NESTED[0][1][0][0]
# try:
# f.to_native(value)
# except ValidationError:
# self.fail("Value %s does not validate" % str(value))
class RegexSerializer(serializers.Serializer): class RegexSerializer(serializers.Serializer):
pin = serializers.CharField( pin = serializers.CharField(
......
[tox] [tox]
downloadcache = {toxworkdir}/cache/
envlist = envlist =
flake8, py27-flake8,
py3.4-django1.7,py3.3-django1.7,py3.2-django1.7,py2.7-django1.7, {py26,py27}-django14,
py3.4-django1.6,py3.3-django1.6,py3.2-django1.6,py2.7-django1.6,py2.6-django1.6, {py26,py27,py32,py33,py34}-django{15,16},
py3.4-django1.5,py3.3-django1.5,py3.2-django1.5,py2.7-django1.5,py2.6-django1.5, {py27,py32,py33,py34}-django{17,master}
py2.7-django1.4,py2.6-django1.4,
py3.4-djangomaster,py3.3-djangomaster,py2.7-djangomaster
[testenv] [testenv]
commands = ./runtests.py --fast commands = ./runtests.py --fast
setenv = setenv =
PYTHONDONTWRITEBYTECODE=1 PYTHONDONTWRITEBYTECODE=1
deps =
[testenv:flake8] django14: Django==1.4.16
basepython = python2.7 django15: Django==1.5.11
deps = pytest==2.5.2 django16: Django==1.6.8
django17: Django==1.7.1
djangomaster: https://github.com/django/django/zipball/master
{py26,py27}-django{14,15,16,17}: django-guardian==1.2.3
{py26,py27}-django{14,15,16}: oauth2==1.5.211
{py26,py27}-django{14,15,16}: django-oauth-plus==2.2.1
{py26,py27}-django{14,15}: django-oauth2-provider==0.2.3
{py26,py27}-django16: django-oauth2-provider==0.2.4
pytest-django==2.6.1
django-filter==0.7
defusedxml==0.3
markdown>=2.1.0
PyYAML>=3.10
[testenv:py27-flake8]
deps =
pytest==2.5.2
flake8==2.2.2 flake8==2.2.2
commands = ./runtests.py --lintonly commands = ./runtests.py --lintonly
[testenv:py3.4-django1.7] [testenv:py27-docs]
basepython = python3.4 deps =
deps = Django==1.7 mkdocs>=0.11.1
django-filter==0.7 commands = mkdocs build
defusedxml==0.3
pytest-django==2.6.1
[testenv:py3.3-django1.7]
basepython = python3.3
deps = Django==1.7
django-filter==0.7
defusedxml==0.3
pytest-django==2.6.1
[testenv:py3.2-django1.7]
basepython = python3.2
deps = Django==1.7
django-filter==0.7
defusedxml==0.3
pytest-django==2.6.1
[testenv:py2.7-django1.7]
basepython = python2.7
deps = Django==1.7
django-filter==0.7
defusedxml==0.3
# django-oauth-plus==2.2.1
# oauth2==1.5.211
# django-oauth2-provider==0.2.4
django-guardian==1.2.3
pytest-django==2.6.1
[testenv:py3.4-django1.6]
basepython = python3.4
deps = Django==1.6.3
django-filter==0.7
defusedxml==0.3
pytest-django==2.6.1
[testenv:py3.3-django1.6]
basepython = python3.3
deps = Django==1.6.3
django-filter==0.7
defusedxml==0.3
pytest-django==2.6.1
[testenv:py3.2-django1.6]
basepython = python3.2
deps = Django==1.6.3
django-filter==0.7
defusedxml==0.3
pytest-django==2.6.1
[testenv:py2.7-django1.6]
basepython = python2.7
deps = Django==1.6.3
django-filter==0.7
defusedxml==0.3
django-oauth-plus==2.2.1
oauth2==1.5.211
django-oauth2-provider==0.2.4
django-guardian==1.2.3
pytest-django==2.6.1
[testenv:py2.6-django1.6]
basepython = python2.6
deps = Django==1.6.3
django-filter==0.7
defusedxml==0.3
django-oauth-plus==2.2.1
oauth2==1.5.211
django-oauth2-provider==0.2.4
django-guardian==1.2.3
pytest-django==2.6.1
[testenv:py3.4-django1.5]
basepython = python3.4
deps = django==1.5.6
django-filter==0.7
defusedxml==0.3
pytest-django==2.6.1
[testenv:py3.3-django1.5]
basepython = python3.3
deps = django==1.5.6
django-filter==0.7
defusedxml==0.3
pytest-django==2.6.1
[testenv:py3.2-django1.5]
basepython = python3.2
deps = django==1.5.6
django-filter==0.7
defusedxml==0.3
pytest-django==2.6.1
[testenv:py2.7-django1.5]
basepython = python2.7
deps = django==1.5.6
django-filter==0.7
defusedxml==0.3
django-oauth-plus==2.2.1
oauth2==1.5.211
django-oauth2-provider==0.2.3
django-guardian==1.2.3
pytest-django==2.6.1
[testenv:py2.6-django1.5]
basepython = python2.6
deps = django==1.5.6
django-filter==0.7
defusedxml==0.3
django-oauth-plus==2.2.1
oauth2==1.5.211
django-oauth2-provider==0.2.3
django-guardian==1.2.3
pytest-django==2.6.1
[testenv:py2.7-django1.4]
basepython = python2.7
deps = django==1.4.11
django-filter==0.7
defusedxml==0.3
django-oauth-plus==2.2.1
oauth2==1.5.211
django-oauth2-provider==0.2.3
django-guardian==1.2.3
pytest-django==2.6.1
[testenv:py2.6-django1.4]
basepython = python2.6
deps = django==1.4.11
django-filter==0.7
defusedxml==0.3
django-oauth-plus==2.2.1
oauth2==1.5.211
django-oauth2-provider==0.2.3
django-guardian==1.2.3
pytest-django==2.6.1
[testenv:py3.4-djangomaster]
basepython = python3.4
deps = https://github.com/django/django/zipball/master
django-filter==0.7
defusedxml==0.3
pytest-django==2.6.1
[testenv:py3.3-djangomaster]
basepython = python3.3
deps = https://github.com/django/django/zipball/master
django-filter==0.7
defusedxml==0.3
pytest-django==2.6.1
[testenv:py2.7-djangomaster]
basepython = python3.2
deps = https://github.com/django/django/zipball/master
django-filter==0.7
defusedxml==0.3
pytest-django==2.6.1
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment