Commit ef26f43d by Carlton Gibson

Merge branch 'master' of github.com:tomchristie/django-rest-framework

parents c50a42bd 72c4ec4e
...@@ -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
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:
...@@ -74,7 +74,7 @@ Default: ...@@ -74,7 +74,7 @@ Default:
#### DEFAULT_PERMISSION_CLASSES #### DEFAULT_PERMISSION_CLASSES
A list or tuple of permission classes, that determines the default set of permissions checked at the start of a view. A list or tuple of permission classes, that determines the default set of permissions checked at the start of a view. Permission must be granted by every class in the list.
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> <a class="github" href="validators.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.
---
# Validators # Validators
> Validators can be useful for re-using validation logic between different types of fields. > Validators can be useful for re-using validation logic between different types of fields.
......
<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)
......
...@@ -44,7 +44,9 @@ When that's all done we'll need to update our database tables. ...@@ -44,7 +44,9 @@ When that's all done we'll need to update our database tables.
Normally we'd create a database migration in order to do that, but for the purposes of this tutorial, let's just delete the database and start again. Normally we'd create a database migration in order to do that, but for the purposes of this tutorial, let's just delete the database and start again.
rm tmp.db rm tmp.db
python manage.py syncdb rm -r snippets/migrations
python manage.py makemigrations snippets
python manage.py migrate
You might also want to create a few different users, to use for testing the API. The quickest way to do this will be with the `createsuperuser` command. You might also want to create a few different users, to use for testing the API. The quickest way to do this will be with the `createsuperuser` command.
......
...@@ -6,7 +6,6 @@ At the moment relationships within our API are represented by using primary keys ...@@ -6,7 +6,6 @@ At the moment relationships within our API are represented by using primary keys
Right now we have endpoints for 'snippets' and 'users', but we don't have a single entry point to our API. To create one, we'll use a regular function-based view and the `@api_view` decorator we introduced earlier. In your `snippets/views.py` add: Right now we have endpoints for 'snippets' and 'users', but we don't have a single entry point to our API. To create one, we'll use a regular function-based view and the `@api_view` decorator we introduced earlier. In your `snippets/views.py` add:
from rest_framework import renderers
from rest_framework.decorators import api_view from rest_framework.decorators import api_view
from rest_framework.response import Response from rest_framework.response import Response
from rest_framework.reverse import reverse from rest_framework.reverse import reverse
......
...@@ -60,7 +60,7 @@ To see what's going on under the hood let's first explicitly create a set of vie ...@@ -60,7 +60,7 @@ To see what's going on under the hood let's first explicitly create a set of vie
In the `urls.py` file we bind our `ViewSet` classes into a set of concrete views. In the `urls.py` file we bind our `ViewSet` classes into a set of concrete views.
from snippets.views import SnippetViewSet, UserViewSet from snippets.views import SnippetViewSet, UserViewSet, api_root
from rest_framework import renderers from rest_framework import renderers
snippet_list = SnippetViewSet.as_view({ snippet_list = SnippetViewSet.as_view({
......
...@@ -19,7 +19,7 @@ Create a new Django project named `tutorial`, then start a new app called `quick ...@@ -19,7 +19,7 @@ Create a new Django project named `tutorial`, then start a new app called `quick
pip install djangorestframework pip install djangorestframework
# Set up a new project with a single application # Set up a new project with a single application
django-admin.py startproject tutorial . django-admin.py startproject tutorial
cd tutorial cd tutorial
django-admin.py startapp quickstart django-admin.py startapp quickstart
cd .. cd ..
......
<!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):
......
...@@ -5,20 +5,44 @@ In addition Django's built in 403 and 404 exceptions are handled. ...@@ -5,20 +5,44 @@ In addition Django's built in 403 and 404 exceptions are handled.
(`django.http.Http404` and `django.core.exceptions.PermissionDenied`) (`django.http.Http404` and `django.core.exceptions.PermissionDenied`)
""" """
from __future__ import unicode_literals from __future__ import unicode_literals
from django.utils.translation import ugettext_lazy as _
from django.utils.translation import ungettext_lazy
from rest_framework import status from rest_framework import status
from rest_framework.compat import force_text
import math import math
def _force_text_recursive(data):
"""
Descend into a nested data structure, forcing any
lazy translation strings into plain text.
"""
if isinstance(data, list):
return [
_force_text_recursive(item) for item in data
]
elif isinstance(data, dict):
return dict([
(key, _force_text_recursive(value))
for key, value in data.items()
])
return force_text(data)
class APIException(Exception): class APIException(Exception):
""" """
Base class for REST framework exceptions. Base class for REST framework exceptions.
Subclasses should provide `.status_code` and `.default_detail` properties. Subclasses should provide `.status_code` and `.default_detail` properties.
""" """
status_code = status.HTTP_500_INTERNAL_SERVER_ERROR status_code = status.HTTP_500_INTERNAL_SERVER_ERROR
default_detail = 'A server error occured' default_detail = _('A server error occured')
def __init__(self, detail=None): def __init__(self, detail=None):
self.detail = detail or self.default_detail if detail is not None:
self.detail = force_text(detail)
else:
self.detail = force_text(self.default_detail)
def __str__(self): def __str__(self):
return self.detail return self.detail
...@@ -39,7 +63,7 @@ class ValidationError(APIException): ...@@ -39,7 +63,7 @@ class ValidationError(APIException):
# The details should always be coerced to a list if not already. # The details should always be coerced to a list if not already.
if not isinstance(detail, dict) and not isinstance(detail, list): if not isinstance(detail, dict) and not isinstance(detail, list):
detail = [detail] detail = [detail]
self.detail = detail self.detail = _force_text_recursive(detail)
def __str__(self): def __str__(self):
return str(self.detail) return str(self.detail)
...@@ -47,59 +71,77 @@ class ValidationError(APIException): ...@@ -47,59 +71,77 @@ class ValidationError(APIException):
class ParseError(APIException): class ParseError(APIException):
status_code = status.HTTP_400_BAD_REQUEST status_code = status.HTTP_400_BAD_REQUEST
default_detail = 'Malformed request.' default_detail = _('Malformed request.')
class AuthenticationFailed(APIException): class AuthenticationFailed(APIException):
status_code = status.HTTP_401_UNAUTHORIZED status_code = status.HTTP_401_UNAUTHORIZED
default_detail = 'Incorrect authentication credentials.' default_detail = _('Incorrect authentication credentials.')
class NotAuthenticated(APIException): class NotAuthenticated(APIException):
status_code = status.HTTP_401_UNAUTHORIZED status_code = status.HTTP_401_UNAUTHORIZED
default_detail = 'Authentication credentials were not provided.' default_detail = _('Authentication credentials were not provided.')
class PermissionDenied(APIException): class PermissionDenied(APIException):
status_code = status.HTTP_403_FORBIDDEN status_code = status.HTTP_403_FORBIDDEN
default_detail = 'You do not have permission to perform this action.' default_detail = _('You do not have permission to perform this action.')
class MethodNotAllowed(APIException): class MethodNotAllowed(APIException):
status_code = status.HTTP_405_METHOD_NOT_ALLOWED status_code = status.HTTP_405_METHOD_NOT_ALLOWED
default_detail = "Method '%s' not allowed." default_detail = _("Method '%s' not allowed.")
def __init__(self, method, detail=None): def __init__(self, method, detail=None):
self.detail = detail or (self.default_detail % method) if detail is not None:
self.detail = force_text(detail)
else:
self.detail = force_text(self.default_detail) % method
class NotAcceptable(APIException): class NotAcceptable(APIException):
status_code = status.HTTP_406_NOT_ACCEPTABLE status_code = status.HTTP_406_NOT_ACCEPTABLE
default_detail = "Could not satisfy the request Accept header" default_detail = _('Could not satisfy the request Accept header')
def __init__(self, detail=None, available_renderers=None): def __init__(self, detail=None, available_renderers=None):
self.detail = detail or self.default_detail if detail is not None:
self.detail = force_text(detail)
else:
self.detail = force_text(self.default_detail)
self.available_renderers = available_renderers self.available_renderers = available_renderers
class UnsupportedMediaType(APIException): class UnsupportedMediaType(APIException):
status_code = status.HTTP_415_UNSUPPORTED_MEDIA_TYPE status_code = status.HTTP_415_UNSUPPORTED_MEDIA_TYPE
default_detail = "Unsupported media type '%s' in request." default_detail = _("Unsupported media type '%s' in request.")
def __init__(self, media_type, detail=None): def __init__(self, media_type, detail=None):
self.detail = detail or (self.default_detail % media_type) if detail is not None:
self.detail = force_text(detail)
else:
self.detail = force_text(self.default_detail) % media_type
class Throttled(APIException): class Throttled(APIException):
status_code = status.HTTP_429_TOO_MANY_REQUESTS status_code = status.HTTP_429_TOO_MANY_REQUESTS
default_detail = 'Request was throttled.' default_detail = _('Request was throttled.')
extra_detail = " Expected available in %d second%s." extra_detail = ungettext_lazy(
'Expected available in %(wait)d second.',
'Expected available in %(wait)d seconds.',
'wait'
)
def __init__(self, wait=None, detail=None): def __init__(self, wait=None, detail=None):
if detail is not None:
self.detail = force_text(detail)
else:
self.detail = force_text(self.default_detail)
if wait is None: if wait is None:
self.detail = detail or self.default_detail
self.wait = None self.wait = None
else: else:
format = (detail or self.default_detail) + self.extra_detail
self.detail = format % (wait, wait != 1 and 's' or '')
self.wait = math.ceil(wait) self.wait = math.ceil(wait)
self.detail += ' ' + force_text(
self.extra_detail % {'wait': self.wait}
)
...@@ -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
...@@ -181,6 +181,9 @@ class Field(object): ...@@ -181,6 +181,9 @@ class Field(object):
self.style = {} if style is None else style self.style = {} if style is None else style
self.allow_null = allow_null self.allow_null = allow_null
if allow_null and self.default_empty_html is empty:
self.default_empty_html = None
if validators is not None: if validators is not None:
self.validators = validators[:] self.validators = validators[:]
...@@ -259,7 +262,11 @@ class Field(object): ...@@ -259,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)
...@@ -314,7 +321,6 @@ class Field(object): ...@@ -314,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):
...@@ -341,9 +347,6 @@ class Field(object): ...@@ -341,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.
...@@ -495,6 +498,7 @@ class CharField(Field): ...@@ -495,6 +498,7 @@ class CharField(Field):
} }
initial = '' initial = ''
coerce_blank_to_null = False coerce_blank_to_null = False
default_empty_html = ''
def __init__(self, **kwargs): def __init__(self, **kwargs):
self.allow_blank = kwargs.pop('allow_blank', False) self.allow_blank = kwargs.pop('allow_blank', False)
...@@ -947,6 +951,8 @@ class ChoiceField(Field): ...@@ -947,6 +951,8 @@ class ChoiceField(Field):
self.fail('invalid_choice', input=data) self.fail('invalid_choice', input=data)
def to_representation(self, value): def to_representation(self, value):
if value in ('', None):
return value
return self.choice_strings_to_values[six.text_type(value)] return self.choice_strings_to_values[six.text_type(value)]
......
...@@ -121,7 +121,10 @@ class SimpleMetadata(BaseMetadata): ...@@ -121,7 +121,10 @@ class SimpleMetadata(BaseMetadata):
if hasattr(field, 'choices'): if hasattr(field, 'choices'):
field_info['choices'] = [ field_info['choices'] = [
{'value': choice_value, 'display_name': choice_name} {
'value': choice_value,
'display_name': force_text(choice_name, strings_only=True)
}
for choice_value, choice_name in field.choices.items() for choice_value, choice_name in field.choices.items()
] ]
......
...@@ -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 = []
......
...@@ -429,7 +429,10 @@ class HTMLFormRenderer(BaseRenderer): ...@@ -429,7 +429,10 @@ class HTMLFormRenderer(BaseRenderer):
style['base_template'] = self.base_template style['base_template'] = self.base_template
style['renderer'] = self style['renderer'] = self
if 'template' in style: # This API needs to be finessed and finalized for 3.1
if 'template' in renderer_context:
template_name = renderer_context['template']
elif 'template' in style:
template_name = style['template'] template_name = style['template']
else: else:
template_name = style['template_pack'].strip('/') + '/' + style['base_template'] template_name = style['template_pack'].strip('/') + '/' + style['base_template']
...@@ -522,7 +525,10 @@ class BrowsableAPIRenderer(BaseRenderer): ...@@ -522,7 +525,10 @@ class BrowsableAPIRenderer(BaseRenderer):
else: else:
instance = None instance = None
if request.method == method: # If this is valid serializer data, and the form is for the same
# HTTP method as was used in the request then use the existing
# serializer instance, rather than dynamically creating a new one.
if request.method == method and serializer is not None:
try: try:
data = request.data data = request.data
except ParseError: except ParseError:
...@@ -555,7 +561,14 @@ class BrowsableAPIRenderer(BaseRenderer): ...@@ -555,7 +561,14 @@ class BrowsableAPIRenderer(BaseRenderer):
if data is not None: if data is not None:
serializer.is_valid() serializer.is_valid()
form_renderer = self.form_renderer_class() form_renderer = self.form_renderer_class()
return form_renderer.render(serializer.data, self.accepted_media_type, self.renderer_context) return form_renderer.render(
serializer.data,
self.accepted_media_type,
dict(
list(self.renderer_context.items()) +
[('template', 'rest_framework/api_form.html')]
)
)
def get_raw_data_form(self, data, view, method, request): def get_raw_data_form(self, data, view, method, request):
""" """
......
...@@ -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,
......
...@@ -86,6 +86,15 @@ class BaseSerializer(Field): ...@@ -86,6 +86,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 +102,9 @@ class BaseSerializer(Field): ...@@ -93,7 +102,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.')
...@@ -362,14 +373,9 @@ class Serializer(BaseSerializer): ...@@ -362,14 +373,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
...@@ -720,49 +726,62 @@ class ModelSerializer(Serializer): ...@@ -720,49 +726,62 @@ class ModelSerializer(Serializer):
# Determine if we need any additional `HiddenField` or extra keyword # Determine if we need any additional `HiddenField` or extra keyword
# arguments to deal with `unique_for` dates that are required to # arguments to deal with `unique_for` dates that are required to
# be in the input data in order to validate it. # be in the input data in order to validate it.
unique_fields = {} hidden_fields = {}
unique_constraint_names = set()
for model_field_name, field_name in model_field_mapping.items(): for model_field_name, field_name in model_field_mapping.items():
try: try:
model_field = model._meta.get_field(model_field_name) model_field = model._meta.get_field(model_field_name)
except FieldDoesNotExist: except FieldDoesNotExist:
continue continue
# Deal with each of the `unique_for_*` cases. # Include each of the `unique_for_*` field names.
for date_field_name in ( unique_constraint_names |= set([
model_field.unique_for_date, model_field.unique_for_date,
model_field.unique_for_month, model_field.unique_for_month,
model_field.unique_for_year model_field.unique_for_year
): ])
if date_field_name is None:
continue unique_constraint_names -= set([None])
# Include each of the `unique_together` field names,
# so long as all the field names are included on the serializer.
for parent_class in [model] + list(model._meta.parents.keys()):
for unique_together_list in parent_class._meta.unique_together:
if set(fields).issuperset(set(unique_together_list)):
unique_constraint_names |= set(unique_together_list)
# Now we have all the field names that have uniqueness constraints
# applied, we can add the extra 'required=...' or 'default=...'
# arguments that are appropriate to these fields, or add a `HiddenField` for it.
for unique_constraint_name in unique_constraint_names:
# Get the model field that is refered too. # Get the model field that is refered too.
date_field = model._meta.get_field(date_field_name) unique_constraint_field = model._meta.get_field(unique_constraint_name)
if date_field.auto_now_add: if getattr(unique_constraint_field, 'auto_now_add', None):
default = CreateOnlyDefault(timezone.now) default = CreateOnlyDefault(timezone.now)
elif date_field.auto_now: elif getattr(unique_constraint_field, 'auto_now', None):
default = timezone.now default = timezone.now
elif date_field.has_default(): elif unique_constraint_field.has_default():
default = model_field.default default = unique_constraint_field.default
else: else:
default = empty default = empty
if date_field_name in model_field_mapping: if unique_constraint_name in model_field_mapping:
# The corresponding date field is present in the serializer # The corresponding field is present in the serializer
if date_field_name not in extra_kwargs: if unique_constraint_name not in extra_kwargs:
extra_kwargs[date_field_name] = {} extra_kwargs[unique_constraint_name] = {}
if default is empty: if default is empty:
if 'required' not in extra_kwargs[date_field_name]: if 'required' not in extra_kwargs[unique_constraint_name]:
extra_kwargs[date_field_name]['required'] = True extra_kwargs[unique_constraint_name]['required'] = True
else: else:
if 'default' not in extra_kwargs[date_field_name]: if 'default' not in extra_kwargs[unique_constraint_name]:
extra_kwargs[date_field_name]['default'] = default extra_kwargs[unique_constraint_name]['default'] = default
else: elif default is not empty:
# The corresponding date field is not present in the, # The corresponding field is not present in the,
# serializer. We have a default to use for the date, so # serializer. We have a default to use for it, so
# add in a hidden field that populates it. # add in a hidden field that populates it.
unique_fields[date_field_name] = HiddenField(default=default) hidden_fields[unique_constraint_name] = HiddenField(default=default)
# Now determine the fields that should be included on the serializer. # Now determine the fields that should be included on the serializer.
for field_name in fields: for field_name in fields:
...@@ -838,12 +857,16 @@ class ModelSerializer(Serializer): ...@@ -838,12 +857,16 @@ class ModelSerializer(Serializer):
'validators', 'queryset' 'validators', 'queryset'
]: ]:
kwargs.pop(attr, None) kwargs.pop(attr, None)
if extras.get('default') and kwargs.get('required') is False:
kwargs.pop('required')
kwargs.update(extras) kwargs.update(extras)
# Create the serializer field. # Create the serializer field.
ret[field_name] = field_cls(**kwargs) ret[field_name] = field_cls(**kwargs)
for field_name, field in unique_fields.items(): for field_name, field in hidden_fields.items():
ret[field_name] = field ret[field_name] = field
return ret return ret
......
{% load rest_framework %}
{% csrf_token %}
{% for field in form %}
{% if not field.read_only %}
{% render_field field style=style %}
{% endif %}
{% endfor %}
<!-- form.non_field_errors -->
...@@ -237,13 +237,6 @@ ...@@ -237,13 +237,6 @@
</div> </div>
<!-- END Content --> <!-- END Content -->
</div><!-- /.container --> </div><!-- /.container -->
<footer>
{% block footer %}
<p>Sponsored by <a href="http://dabapps.com/">DabApps</a>.</p>
{% endblock %}
</footer>
</div><!-- ./wrapper --> </div><!-- ./wrapper -->
{% block script %} {% block script %}
......
...@@ -5,9 +5,12 @@ ...@@ -5,9 +5,12 @@
<legend class="control-label col-sm-2 {% if style.hide_label %}sr-only{% endif %}" style="border-bottom: 0">{{ field.label }}</legend> <legend class="control-label col-sm-2 {% if style.hide_label %}sr-only{% endif %}" style="border-bottom: 0">{{ field.label }}</legend>
</div> </div>
{% endif %} {% endif %}
<!--
<ul> <ul>
{% for child in field.value %} {% for child in field.value %}
<li>TODO</li> <li>TODO</li>
{% endfor %} {% endfor %}
</ul> </ul>
-->
<p>Lists are not currently supported in HTML input.</p>
</fieldset> </fieldset>
...@@ -4,6 +4,9 @@ ...@@ -4,6 +4,9 @@
{% endif %} {% endif %}
<div class="col-sm-10"> <div class="col-sm-10">
<select class="form-control" name="{{ field.name }}"> <select class="form-control" name="{{ field.name }}">
{% if field.allow_null %}
<option value="" {% if not field.value %}selected{% endif %}>--------</option>
{% endif %}
{% for key, text in field.choices.items %} {% for key, text in field.choices.items %}
<option value="{{ key }}" {% if key == field.value %}selected{% endif %}>{{ text }}</option> <option value="{{ key }}" {% if key == field.value %}selected{% endif %}>{{ text }}</option>
{% endfor %} {% endfor %}
......
<span>Lists are not currently supported in HTML input.</span>
...@@ -3,6 +3,9 @@ ...@@ -3,6 +3,9 @@
<label class="sr-only">{{ field.label }}</label> <label class="sr-only">{{ field.label }}</label>
{% endif %} {% endif %}
<select class="form-control" name="{{ field.name }}"> <select class="form-control" name="{{ field.name }}">
{% if field.allow_null %}
<option value="" {% if not field.value %}selected{% endif %}>--------</option>
{% endif %}
{% for key, text in field.choices.items %} {% for key, text in field.choices.items %}
<option value="{{ key }}" {% if key == field.value %}selected{% endif %}>{{ text }}</option> <option value="{{ key }}" {% if key == field.value %}selected{% endif %}>{{ text }}</option>
{% endfor %} {% endfor %}
......
...@@ -22,7 +22,7 @@ ...@@ -22,7 +22,7 @@
<div id="div_id_username" <div id="div_id_username"
class="clearfix control-group {% if form.username.errors %}error{% endif %}"> class="clearfix control-group {% if form.username.errors %}error{% endif %}">
<div class="controls"> <div class="controls">
<Label class="span4">Username:</label> <label class="span4">Username:</label>
<input style="height: 25px" type="text" name="username" maxlength="100" <input style="height: 25px" type="text" name="username" maxlength="100"
autocapitalize="off" autocapitalize="off"
autocorrect="off" class="span12 textinput textInput" autocorrect="off" class="span12 textinput textInput"
...@@ -36,9 +36,10 @@ ...@@ -36,9 +36,10 @@
</div> </div>
</div> </div>
<div id="div_id_password" <div id="div_id_password"
class="clearfix control-group {% if form.password.errors %}error{% endif %}"> class="clearfix control-group {% if form.password.errors %}error{% endif %}"
style="margin-top: 10px">
<div class="controls"> <div class="controls">
<Label class="span4">Password:</label> <label class="span4">Password:</label>
<input style="height: 25px" type="password" name="password" maxlength="100" <input style="height: 25px" type="password" name="password" maxlength="100"
autocapitalize="off" autocorrect="off" class="span12 textinput textInput" autocapitalize="off" autocorrect="off" class="span12 textinput textInput"
id="id_password" required> id="id_password" required>
...@@ -55,7 +56,7 @@ ...@@ -55,7 +56,7 @@
<div class="well well-small text-error" style="border: none">{{ error }}</div> <div class="well well-small text-error" style="border: none">{{ error }}</div>
{% endfor %} {% endfor %}
{% endif %} {% endif %}
<div class="form-actions-no-box"> <div class="form-actions-no-box" style="margin-top: 20px">
<input type="submit" name="submit" value="Log in" class="btn btn-primary" id="submit-id-submit"> <input type="submit" name="submit" value="Log in" class="btn btn-primary" id="submit-id-submit">
</div> </div>
</form> </form>
......
...@@ -4,4 +4,5 @@ ...@@ -4,4 +4,5 @@
{% for field_item in field.value.field_items.values() %} {% for field_item in field.value.field_items.values() %}
{{ renderer.render_field(field_item, layout=layout) }} {{ renderer.render_field(field_item, layout=layout) }}
{% endfor %} --> {% endfor %} -->
<p>Lists are not currently supported in HTML input.</p>
</fieldset> </fieldset>
...@@ -3,6 +3,9 @@ ...@@ -3,6 +3,9 @@
<label {% if style.hide_label %}class="sr-only"{% endif %}>{{ field.label }}</label> <label {% if style.hide_label %}class="sr-only"{% endif %}>{{ field.label }}</label>
{% endif %} {% endif %}
<select class="form-control" name="{{ field.name }}"> <select class="form-control" name="{{ field.name }}">
{% if field.allow_null %}
<option value="" {% if not field.value %}selected{% endif %}>--------</option>
{% endif %}
{% for key, text in field.choices.items %} {% for key, text in field.choices.items %}
<option value="{{ key }}" {% if key == field.value %}selected{% endif %}>{{ text }}</option> <option value="{{ key }}" {% if key == field.value %}selected{% endif %}>{{ text }}</option>
{% endfor %} {% endfor %}
......
...@@ -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))
......
...@@ -93,6 +93,9 @@ class UniqueTogetherValidator: ...@@ -93,6 +93,9 @@ class UniqueTogetherValidator:
The `UniqueTogetherValidator` always forces an implied 'required' The `UniqueTogetherValidator` always forces an implied 'required'
state on the fields it applies to. state on the fields it applies to.
""" """
if self.instance is not None:
return
missing = dict([ missing = dict([
(field_name, self.missing_message) (field_name, self.missing_message)
for field_name in self.fields for field_name in self.fields
...@@ -105,8 +108,17 @@ class UniqueTogetherValidator: ...@@ -105,8 +108,17 @@ class UniqueTogetherValidator:
""" """
Filter the queryset to all instances matching the given attributes. Filter the queryset to all instances matching the given attributes.
""" """
# If this is an update, then any unprovided field should
# have it's value set based on the existing instance attribute.
if self.instance is not None:
for field_name in self.fields:
if field_name not in attrs:
attrs[field_name] = getattr(self.instance, field_name)
# Determine the filter keyword arguments and filter the queryset.
filter_kwargs = dict([ filter_kwargs = dict([
(field_name, attrs[field_name]) for field_name in self.fields (field_name, attrs[field_name])
for field_name in self.fields
]) ])
return queryset.filter(**filter_kwargs) return queryset.filter(**filter_kwargs)
......
...@@ -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):
""" """
......
...@@ -793,7 +793,8 @@ class TestChoiceField(FieldValues): ...@@ -793,7 +793,8 @@ class TestChoiceField(FieldValues):
'amazing': ['`amazing` is not a valid choice.'] 'amazing': ['`amazing` is not a valid choice.']
} }
outputs = { outputs = {
'good': 'good' 'good': 'good',
'': ''
} }
field = serializers.ChoiceField( field = serializers.ChoiceField(
choices=[ choices=[
......
# 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(
......
...@@ -88,8 +88,8 @@ class TestUniquenessTogetherValidation(TestCase): ...@@ -88,8 +88,8 @@ class TestUniquenessTogetherValidation(TestCase):
expected = dedent(""" expected = dedent("""
UniquenessTogetherSerializer(): UniquenessTogetherSerializer():
id = IntegerField(label='ID', read_only=True) id = IntegerField(label='ID', read_only=True)
race_name = CharField(max_length=100) race_name = CharField(max_length=100, required=True)
position = IntegerField() position = IntegerField(required=True)
class Meta: class Meta:
validators = [<UniqueTogetherValidator(queryset=UniquenessTogetherModel.objects.all(), fields=('race_name', 'position'))>] validators = [<UniqueTogetherValidator(queryset=UniquenessTogetherModel.objects.all(), fields=('race_name', 'position'))>]
""") """)
......
[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