mirror of
https://github.com/encode/django-rest-framework.git
synced 2025-08-03 20:10:10 +03:00
Merge remote-tracking branch 'reference/master' into feature/dotted_names_for_regular_fields
Conflicts: rest_framework/serializers.py
This commit is contained in:
commit
707f363bae
27
.travis.yml
27
.travis.yml
|
@ -5,22 +5,26 @@ python:
|
|||
- "2.7"
|
||||
- "3.2"
|
||||
- "3.3"
|
||||
- "3.4"
|
||||
|
||||
env:
|
||||
- DJANGO="django==1.6"
|
||||
- DJANGO="django==1.5.5"
|
||||
- DJANGO="django==1.4.10"
|
||||
- DJANGO="https://www.djangoproject.com/download/1.7b2/tarball/"
|
||||
- DJANGO="django==1.6.3"
|
||||
- DJANGO="django==1.5.6"
|
||||
- DJANGO="django==1.4.11"
|
||||
- DJANGO="django==1.3.7"
|
||||
|
||||
install:
|
||||
- pip install $DJANGO
|
||||
- pip install defusedxml==0.3
|
||||
- pip install defusedxml==0.3 Pillow==2.3.0
|
||||
- "if [[ ${TRAVIS_PYTHON_VERSION::1} != '3' ]]; then pip install oauth2==1.5.211; fi"
|
||||
- "if [[ ${TRAVIS_PYTHON_VERSION::1} != '3' ]]; then pip install django-oauth-plus==2.2.1; fi"
|
||||
- "if [[ ${TRAVIS_PYTHON_VERSION::1} != '3' ]]; then pip install django-oauth-plus==2.2.4; fi"
|
||||
- "if [[ ${TRAVIS_PYTHON_VERSION::1} != '3' ]]; then pip install django-oauth2-provider==0.2.4; fi"
|
||||
- "if [[ ${TRAVIS_PYTHON_VERSION::1} != '3' ]]; then pip install django-guardian==1.1.1; fi"
|
||||
- "if [[ ${DJANGO::11} == 'django==1.3' ]]; then pip install django-filter==0.5.4; fi"
|
||||
- "if [[ ${DJANGO::11} != 'django==1.3' ]]; then pip install django-filter==0.6; fi"
|
||||
- "if [[ ${DJANGO::11} != 'django==1.3' ]]; then pip install django-filter==0.7; fi"
|
||||
- "if [[ ${TRAVIS_PYTHON_VERSION::1} == '3' ]]; then pip install -e git+https://github.com/linovia/django-guardian.git@feature/django_1_7#egg=django-guardian-1.2.0; fi"
|
||||
- "if [[ ${DJANGO} == 'https://www.djangoproject.com/download/1.7b2/tarball/' ]]; then pip install -e git+https://github.com/linovia/django-guardian.git@feature/django_1_7#egg=django-guardian-1.2.0; fi"
|
||||
- export PYTHONPATH=.
|
||||
|
||||
script:
|
||||
|
@ -28,12 +32,17 @@ script:
|
|||
|
||||
matrix:
|
||||
exclude:
|
||||
- python: "2.6"
|
||||
env: DJANGO="https://www.djangoproject.com/download/1.7b2/tarball/"
|
||||
- python: "3.2"
|
||||
env: DJANGO="django==1.4.10"
|
||||
env: DJANGO="django==1.4.11"
|
||||
- python: "3.2"
|
||||
env: DJANGO="django==1.3.7"
|
||||
- python: "3.3"
|
||||
env: DJANGO="django==1.4.10"
|
||||
env: DJANGO="django==1.4.11"
|
||||
- python: "3.3"
|
||||
env: DJANGO="django==1.3.7"
|
||||
|
||||
- python: "3.4"
|
||||
env: DJANGO="django==1.4.11"
|
||||
- python: "3.4"
|
||||
env: DJANGO="django==1.3.7"
|
||||
|
|
|
@ -393,6 +393,14 @@ The [Django OAuth2 Consumer][doac] library from [Rediker Software][rediker] is a
|
|||
|
||||
JSON Web Token is a fairly new standard which can be used for token-based authentication. Unlike the built-in TokenAuthentication scheme, JWT Authentication doesn't need to use a database to validate a token. [Blimp][blimp] maintains the [djangorestframework-jwt][djangorestframework-jwt] package which provides a JWT Authentication class as well as a mechanism for clients to obtain a JWT given the username and password.
|
||||
|
||||
## Hawk HTTP Authentication
|
||||
|
||||
The [HawkREST][hawkrest] library builds on the [Mohawk][mohawk] library to let you work with [Hawk][hawk] signed requests and responses in your API. [Hawk][hawk] lets two parties securely communicate with each other using messages signed by a shared key. It is based on [HTTP MAC access authentication][mac] (which was based on parts of [OAuth 1.0][oauth-1.0a]).
|
||||
|
||||
## HTTP Signature Authentication
|
||||
|
||||
HTTP Signature (currently a [IETF draft][http-signature-ietf-draft]) provides a way to achieve origin authentication and message integrity for HTTP messages. Similar to [Amazon's HTTP Signature scheme][amazon-http-signature], used by many of its services, it permits stateless, per-request authentication. [Elvio Toccalino][etoccalino] maintains the [djangorestframework-httpsignature][djangorestframework-httpsignature] package which provides an easy to use HTTP Signature Authentication mechanism.
|
||||
|
||||
[cite]: http://jacobian.org/writing/rest-worst-practices/
|
||||
[http401]: http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.4.2
|
||||
[http403]: http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.4.4
|
||||
|
@ -419,3 +427,11 @@ JSON Web Token is a fairly new standard which can be used for token-based authen
|
|||
[doac-rest-framework]: https://github.com/Rediker-Software/doac/blob/master/docs/integrations.md#
|
||||
[blimp]: https://github.com/GetBlimp
|
||||
[djangorestframework-jwt]: https://github.com/GetBlimp/django-rest-framework-jwt
|
||||
[etoccalino]: https://github.com/etoccalino/
|
||||
[djangorestframework-httpsignature]: https://github.com/etoccalino/django-rest-framework-httpsignature
|
||||
[amazon-http-signature]: http://docs.aws.amazon.com/general/latest/gr/signature-version-4.html
|
||||
[http-signature-ietf-draft]: https://datatracker.ietf.org/doc/draft-cavage-http-signatures/
|
||||
[hawkrest]: http://hawkrest.readthedocs.org/en/latest/
|
||||
[hawk]: https://github.com/hueniverse/hawk
|
||||
[mohawk]: http://mohawk.readthedocs.org/en/latest/
|
||||
[mac]: http://tools.ietf.org/html/draft-hammer-oauth-v2-mac-token-05
|
||||
|
|
|
@ -18,7 +18,7 @@ The handled exceptions are:
|
|||
|
||||
In each case, REST framework will return a response with an appropriate status code and content-type. The body of the response will include any additional details regarding the nature of the error.
|
||||
|
||||
By default all error responses will include a key `details` in the body of the response, but other keys may also be included.
|
||||
By default all error responses will include a key `detail` in the body of the response, but other keys may also be included.
|
||||
|
||||
For example, the following request:
|
||||
|
||||
|
@ -86,7 +86,7 @@ Note that the exception handler will only be called for responses generated by r
|
|||
|
||||
The **base class** for all exceptions raised inside REST framework.
|
||||
|
||||
To provide a custom exception, subclass `APIException` and set the `.status_code` and `.detail` properties on the class.
|
||||
To provide a custom exception, subclass `APIException` and set the `.status_code` and `.default_detail` properties on the class.
|
||||
|
||||
For example, if your API relies on a third party service that may sometimes be unreachable, you might want to implement an exception for the "503 Service Unavailable" HTTP response code. You could do this like so:
|
||||
|
||||
|
@ -94,7 +94,7 @@ For example, if your API relies on a third party service that may sometimes be u
|
|||
|
||||
class ServiceUnavailable(APIException):
|
||||
status_code = 503
|
||||
detail = 'Service temporarily unavailable, try again later.'
|
||||
default_detail = 'Service temporarily unavailable, try again later.'
|
||||
|
||||
## ParseError
|
||||
|
||||
|
|
|
@ -104,6 +104,7 @@ A serializer definition that looked like this:
|
|||
expired = serializers.Field(source='has_expired')
|
||||
|
||||
class Meta:
|
||||
model = Account
|
||||
fields = ('url', 'owner', 'name', 'expired')
|
||||
|
||||
Would produce output similar to:
|
||||
|
@ -125,7 +126,7 @@ A field that supports both read and write operations. By itself `WritableField`
|
|||
|
||||
## ModelField
|
||||
|
||||
A generic field that can be tied to any arbitrary model field. The `ModelField` class delegates the task of serialization/deserialization to it's associated model field. This field can be used to create serializer fields for custom model fields, without having to create a new custom serializer field.
|
||||
A generic field that can be tied to any arbitrary model field. The `ModelField` class delegates the task of serialization/deserialization to its associated model field. This field can be used to create serializer fields for custom model fields, without having to create a new custom serializer field.
|
||||
|
||||
The `ModelField` class is generally intended for internal use, but can be used by your API if needed. In order to properly instantiate a `ModelField`, it must be passed a field that is attached to an instantiated model. For example: `ModelField(model_field=MyModel()._meta.get_field('custom_field'))`
|
||||
|
||||
|
@ -307,7 +308,7 @@ Django's regular [FILE_UPLOAD_HANDLERS] are used for handling uploaded files.
|
|||
|
||||
If you want to create a custom field, you'll probably want to override either one or both of the `.to_native()` and `.from_native()` methods. These two methods are used to convert between the initial datatype, and a primitive, serializable datatype. Primitive datatypes may be any of a number, string, date/time/datetime or None. They may also be any list or dictionary like object that only contains other primitive objects.
|
||||
|
||||
The `.to_native()` method is called to convert the initial datatype into a primitive, serializable datatype. The `from_native()` method is called to restore a primitive datatype into it's initial representation.
|
||||
The `.to_native()` method is called to convert the initial datatype into a primitive, serializable datatype. The `from_native()` method is called to restore a primitive datatype into its initial representation.
|
||||
|
||||
## Examples
|
||||
|
||||
|
@ -346,8 +347,17 @@ As an example, let's create a field that can be used represent the class name of
|
|||
"""
|
||||
return obj.__class__
|
||||
|
||||
# Third party packages
|
||||
|
||||
The following third party packages are also available.
|
||||
|
||||
## DRF Compound Fields
|
||||
|
||||
The [drf-compound-fields][drf-compound-fields] package provides "compound" serializer fields, such as lists of simple values, which can be described by other fields rather than serializers with the `many=True` option. Also provided are fields for typed dictionaries and values that can be either a specific type or a list of items of that type.
|
||||
|
||||
[cite]: https://docs.djangoproject.com/en/dev/ref/forms/api/#django.forms.Form.cleaned_data
|
||||
[FILE_UPLOAD_HANDLERS]: https://docs.djangoproject.com/en/dev/ref/settings/#std:setting-FILE_UPLOAD_HANDLERS
|
||||
[ecma262]: http://ecma-international.org/ecma-262/5.1/#sec-15.9.1.15
|
||||
[strftime]: http://docs.python.org/2/library/datetime.html#strftime-and-strptime-behavior
|
||||
[iso8601]: http://www.w3.org/TR/NOTE-datetime
|
||||
[drf-compound-fields]: http://drf-compound-fields.readthedocs.org
|
||||
|
|
|
@ -24,7 +24,7 @@ For example:
|
|||
from myapp.serializers import PurchaseSerializer
|
||||
from rest_framework import generics
|
||||
|
||||
class PurchaseList(generics.ListAPIView)
|
||||
class PurchaseList(generics.ListAPIView):
|
||||
serializer_class = PurchaseSerializer
|
||||
|
||||
def get_queryset(self):
|
||||
|
@ -46,7 +46,7 @@ For example if your URL config contained an entry like this:
|
|||
|
||||
You could then write a view that returned a purchase queryset filtered by the username portion of the URL:
|
||||
|
||||
class PurchaseList(generics.ListAPIView)
|
||||
class PurchaseList(generics.ListAPIView):
|
||||
serializer_class = PurchaseSerializer
|
||||
|
||||
def get_queryset(self):
|
||||
|
@ -63,7 +63,7 @@ A final example of filtering the initial queryset would be to determine the init
|
|||
|
||||
We can override `.get_queryset()` to deal with URLs such as `http://example.com/api/purchases?username=denvercoder9`, and filter the queryset only if the `username` parameter is included in the URL:
|
||||
|
||||
class PurchaseList(generics.ListAPIView)
|
||||
class PurchaseList(generics.ListAPIView):
|
||||
serializer_class = PurchaseSerializer
|
||||
|
||||
def get_queryset(self):
|
||||
|
@ -264,13 +264,17 @@ For example:
|
|||
|
||||
search_fields = ('=username', '=email')
|
||||
|
||||
By default, the search parameter is named `'search`', but this may be overridden with the `SEARCH_PARAM` setting.
|
||||
|
||||
For more details, see the [Django documentation][search-django-admin].
|
||||
|
||||
---
|
||||
|
||||
## OrderingFilter
|
||||
|
||||
The `OrderingFilter` class supports simple query parameter controlled ordering of results. To specify the result order, set a query parameter named `'ordering'` to the required field name. For example:
|
||||
The `OrderingFilter` class supports simple query parameter controlled ordering of results. By default, the query parameter is named `'ordering'`, but this may by overridden with the `ORDERING_PARAM` setting.
|
||||
|
||||
For example, to order users by username:
|
||||
|
||||
http://example.com/api/users?ordering=username
|
||||
|
||||
|
|
|
@ -70,7 +70,7 @@ The following attributes control the basic view behavior.
|
|||
|
||||
**Shortcuts**:
|
||||
|
||||
* `model` - This shortcut may be used instead of setting either (or both) of the `queryset`/`serializer_class` attributes, although using the explicit style is generally preferred. If used instead of `serializer_class`, then then `DEFAULT_MODEL_SERIALIZER_CLASS` setting will determine the base serializer class. Note that `model` is only ever used for generating a default queryset or serializer class - the `queryset` and `serializer_class` attributes are always preferred if provided.
|
||||
* `model` - This shortcut may be used instead of setting either (or both) of the `queryset`/`serializer_class` attributes, although using the explicit style is generally preferred. If used instead of `serializer_class`, then `DEFAULT_MODEL_SERIALIZER_CLASS` setting will determine the base serializer class. Note that `model` is only ever used for generating a default queryset or serializer class - the `queryset` and `serializer_class` attributes are always preferred if provided.
|
||||
|
||||
**Pagination**:
|
||||
|
||||
|
@ -119,7 +119,7 @@ For example:
|
|||
self.check_object_permissions(self.request, obj)
|
||||
return obj
|
||||
|
||||
Note that if your API doesn't include any object level permissions, you may optionally exclude the ``self.check_object_permissions, and simply return the object from the `get_object_or_404` lookup.
|
||||
Note that if your API doesn't include any object level permissions, you may optionally exclude the `self.check_object_permissions`, and simply return the object from the `get_object_or_404` lookup.
|
||||
|
||||
#### `get_filter_backends(self)`
|
||||
|
||||
|
|
|
@ -103,6 +103,7 @@ You can also set the pagination style on a per-view basis, using the `ListAPIVie
|
|||
max_paginate_by = 100
|
||||
|
||||
Note that using a `paginate_by` value of `None` will turn off pagination for the view.
|
||||
Note if you use the `PAGINATE_BY_PARAM` settings, you also have to set the `paginate_by_param` attribute in your view to `None` in order to turn off pagination for those requests that contain the `paginate_by_param` parameter.
|
||||
|
||||
For more complex requirements such as serialization that differs depending on the requested media type you can override the `.get_paginate_by()` and `.get_pagination_serializer_class()` methods.
|
||||
|
||||
|
@ -147,4 +148,14 @@ Alternatively, to set your custom pagination serializer on a per-view basis, use
|
|||
pagination_serializer_class = CustomPaginationSerializer
|
||||
paginate_by = 10
|
||||
|
||||
# Third party packages
|
||||
|
||||
The following third party packages are also available.
|
||||
|
||||
## DRF-extensions
|
||||
|
||||
The [`DRF-extensions` package][drf-extensions] includes a [`PaginateByMaxMixin` mixin class][paginate-by-max-mixin] that allows your API clients to specify `?page_size=max` to obtain the maximum allowed page size.
|
||||
|
||||
[cite]: https://docs.djangoproject.com/en/dev/topics/pagination/
|
||||
[drf-extensions]: http://chibisov.github.io/drf-extensions/docs/
|
||||
[paginate-by-max-mixin]: http://chibisov.github.io/drf-extensions/docs/#paginatebymaxmixin
|
||||
|
|
|
@ -56,7 +56,7 @@ You can also set the authentication policy on a per-view, or per-viewset basis,
|
|||
using the `APIView` class based views.
|
||||
|
||||
from rest_framework.permissions import IsAuthenticated
|
||||
from rest_framework.responses import Response
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.views import APIView
|
||||
|
||||
class ExampleView(APIView):
|
||||
|
|
|
@ -138,6 +138,26 @@ Renders the request data into `YAML`.
|
|||
|
||||
Requires the `pyyaml` package to be installed.
|
||||
|
||||
Note that non-ascii characters will be rendered using `\uXXXX` character escape. For example:
|
||||
|
||||
unicode black star: "\u2605"
|
||||
|
||||
**.media_type**: `application/yaml`
|
||||
|
||||
**.format**: `'.yaml'`
|
||||
|
||||
**.charset**: `utf-8`
|
||||
|
||||
## UnicodeYAMLRenderer
|
||||
|
||||
Renders the request data into `YAML`.
|
||||
|
||||
Requires the `pyyaml` package to be installed.
|
||||
|
||||
Note that non-ascii characters will not be character escaped. For example:
|
||||
|
||||
unicode black star: ★
|
||||
|
||||
**.media_type**: `application/yaml`
|
||||
|
||||
**.format**: `'.yaml'`
|
||||
|
|
|
@ -161,7 +161,7 @@ To do any other validation that requires access to multiple fields, add a method
|
|||
"""
|
||||
Check that the start is before the stop.
|
||||
"""
|
||||
if attrs['start'] < attrs['finish']:
|
||||
if attrs['start'] > attrs['finish']:
|
||||
raise serializers.ValidationError("finish must occur after start")
|
||||
return attrs
|
||||
|
||||
|
@ -383,14 +383,14 @@ You may wish to specify multiple fields as write-only. Instead of adding each f
|
|||
fields = ('email', 'username', 'password')
|
||||
write_only_fields = ('password',) # Note: Password field is write-only
|
||||
|
||||
def restore_object(self, attrs, instance=None):
|
||||
"""
|
||||
Instantiate a new User instance.
|
||||
"""
|
||||
assert instance is None, 'Cannot update users with CreateUserSerializer'
|
||||
user = User(email=attrs['email'], username=attrs['username'])
|
||||
user.set_password(attrs['password'])
|
||||
return user
|
||||
def restore_object(self, attrs, instance=None):
|
||||
"""
|
||||
Instantiate a new User instance.
|
||||
"""
|
||||
assert instance is None, 'Cannot update users with CreateUserSerializer'
|
||||
user = User(email=attrs['email'], username=attrs['username'])
|
||||
user.set_password(attrs['password'])
|
||||
return user
|
||||
|
||||
## Specifying fields explicitly
|
||||
|
||||
|
@ -464,7 +464,7 @@ For more specific requirements such as specifying a different lookup for each fi
|
|||
model = Account
|
||||
fields = ('url', 'account_name', 'users', 'created')
|
||||
|
||||
## Overiding the URL field behavior
|
||||
## Overriding the URL field behavior
|
||||
|
||||
The name of the URL field defaults to 'url'. You can override this globally, by using the `URL_FIELD_NAME` setting.
|
||||
|
||||
|
@ -478,7 +478,7 @@ You can also override this on a per-serializer basis by using the `url_field_nam
|
|||
|
||||
**Note**: The generic view implementations normally generate a `Location` header in response to successful `POST` requests. Serializers using `url_field_name` option will not have this header automatically included by the view. If you need to do so you will ned to also override the view's `get_success_headers()` method.
|
||||
|
||||
You can also overide the URL field's view name and lookup field without overriding the field explicitly, by using the `view_name` and `lookup_field` options, like so:
|
||||
You can also override the URL field's view name and lookup field without overriding the field explicitly, by using the `view_name` and `lookup_field` options, like so:
|
||||
|
||||
class AccountSerializer(serializers.HyperlinkedModelSerializer):
|
||||
class Meta:
|
||||
|
|
|
@ -158,6 +158,18 @@ A client request like the following would return a paginated list of up to 100 i
|
|||
|
||||
Default: `None`
|
||||
|
||||
### SEARCH_PARAM
|
||||
|
||||
The name of a query paramater, which can be used to specify the search term used by `SearchFilter`.
|
||||
|
||||
Default: `search`
|
||||
|
||||
#### ORDERING_PARAM
|
||||
|
||||
The name of a query paramater, which can be used to specify the ordering of results returned by `OrderingFilter`.
|
||||
|
||||
Default: `ordering`
|
||||
|
||||
---
|
||||
|
||||
## Authentication settings
|
||||
|
|
|
@ -218,12 +218,12 @@ You can use any of REST framework's test case classes as you would for the regul
|
|||
|
||||
When checking the validity of test responses it's often more convenient to inspect the data that the response was created with, rather than inspecting the fully rendered response.
|
||||
|
||||
For example, it's easier to inspect `request.data`:
|
||||
For example, it's easier to inspect `response.data`:
|
||||
|
||||
response = self.client.get('/users/4/')
|
||||
self.assertEqual(response.data, {'id': 4, 'username': 'lauren'})
|
||||
|
||||
Instead of inspecting the result of parsing `request.content`:
|
||||
Instead of inspecting the result of parsing `response.content`:
|
||||
|
||||
response = self.client.get('/users/4/')
|
||||
self.assertEqual(json.loads(response.content), {'id': 4, 'username': 'lauren'})
|
||||
|
|
|
@ -150,7 +150,7 @@ For example, given the following views...
|
|||
|
||||
REST_FRAMEWORK = {
|
||||
'DEFAULT_THROTTLE_CLASSES': (
|
||||
'rest_framework.throttling.ScopedRateThrottle'
|
||||
'rest_framework.throttling.ScopedRateThrottle',
|
||||
),
|
||||
'DEFAULT_THROTTLE_RATES': {
|
||||
'contacts': '1000/day',
|
||||
|
|
|
@ -225,7 +225,7 @@ To create a base viewset class that provides `create`, `list` and `retrieve` ope
|
|||
mixins.RetrieveModelMixin,
|
||||
viewsets.GenericViewSet):
|
||||
"""
|
||||
A viewset that provides `retrieve`, `update`, and `list` actions.
|
||||
A viewset that provides `retrieve`, `create`, and `list` actions.
|
||||
|
||||
To use it, override the class and set the `.queryset` and
|
||||
`.serializer_class` attributes.
|
||||
|
|
|
@ -60,14 +60,14 @@ To run the tests, clone the repository, and then:
|
|||
|
||||
# Setup the virtual environment
|
||||
virtualenv env
|
||||
env/bin/activate
|
||||
source env/bin/activate
|
||||
pip install -r requirements.txt
|
||||
pip install -r optionals.txt
|
||||
|
||||
# Run the tests
|
||||
rest_framework/runtests/runtests.py
|
||||
|
||||
You can also use the excellent `[tox][tox]` testing tool to run the tests against all supported versions of Python and Django. Install `tox` globally, and then simply run:
|
||||
You can also use the excellent [tox][tox] testing tool to run the tests against all supported versions of Python and Django. Install `tox` globally, and then simply run:
|
||||
|
||||
tox
|
||||
|
||||
|
|
|
@ -182,6 +182,7 @@ The following people have helped make REST framework great.
|
|||
* Ian Foote - [ian-foote]
|
||||
* Chuck Harmston - [chuckharmston]
|
||||
* Philip Forget - [philipforget]
|
||||
* Artem Mezhenin - [amezhenin]
|
||||
|
||||
Many thanks to everyone who's contributed to the project.
|
||||
|
||||
|
@ -400,3 +401,4 @@ You can also contact [@_tomchristie][twitter] directly on twitter.
|
|||
[ian-foote]: https://github.com/ian-foote
|
||||
[chuckharmston]: https://github.com/chuckharmston
|
||||
[philipforget]: https://github.com/philipforget
|
||||
[amezhenin]: https://github.com/amezhenin
|
||||
|
|
|
@ -40,6 +40,36 @@ You can determine your currently installed version using `pip freeze`:
|
|||
|
||||
## 2.3.x series
|
||||
|
||||
### 2.3.x
|
||||
|
||||
**Date**: April 2014
|
||||
|
||||
* Fix nested serializers linked through a backward foreign key relation
|
||||
* Fix bad links for the `BrowsableAPIRenderer` with `YAMLRenderer`
|
||||
* Add `UnicodeYAMLRenderer` that extends `YAMLRenderer` with unicode
|
||||
* Fix `parse_header` argument convertion
|
||||
* Fix mediatype detection under Python3
|
||||
* Web browseable API now offers blank option on dropdown when the field is not required
|
||||
* `APIException` representation improved for logging purposes
|
||||
* Allow source="*" within nested serializers
|
||||
* Better support for custom oauth2 provider backends
|
||||
* Fix field validation if it's optional and has no value
|
||||
* Add `SEARCH_PARAM` and `ORDERING_PARAM`
|
||||
* Fix `APIRequestFactory` to support arguments within the url string for GET
|
||||
* Allow three transport modes for access tokens when accessing a protected resource
|
||||
* Fix `Request`'s `QueryDict` encoding
|
||||
|
||||
### 2.3.13
|
||||
|
||||
**Date**: 6th March 2014
|
||||
|
||||
* Django 1.7 Support.
|
||||
* Fix `default` argument when used with serializer relation fields.
|
||||
* Display the media type of the content that is being displayed in the browsable API, rather than 'text/html'.
|
||||
* Bugfix for `urlize` template failure when URL regex is matched, but value does not `urlparse`.
|
||||
* Use `urandom` for token generation.
|
||||
* Only use `Vary: Accept` when more than one renderer exists.
|
||||
|
||||
### 2.3.12
|
||||
|
||||
**Date**: 15th January 2014
|
||||
|
@ -101,11 +131,11 @@ You can determine your currently installed version using `pip freeze`:
|
|||
* Bugfix: `client.force_authenticate(None)` should also clear session info if it exists.
|
||||
* Bugfix: Client sending empty string instead of file now clears `FileField`.
|
||||
* Bugfix: Empty values on ChoiceFields with `required=False` now consistently return `None`.
|
||||
* Bugfix: Clients setting `page=0` now simply returns the default page size, instead of disabling pagination. [*]
|
||||
* Bugfix: Clients setting `page_size=0` now simply returns the default page size, instead of disabling pagination. [*]
|
||||
|
||||
---
|
||||
|
||||
[*] Note that the change in `page=0` behaviour fixes what is considered to be a bug in how clients can effect the pagination size. However if you were relying on this behavior you will need to add the following mixin to your list views in order to preserve the existing behavior.
|
||||
[*] Note that the change in `page_size=0` behaviour fixes what is considered to be a bug in how clients can effect the pagination size. However if you were relying on this behavior you will need to add the following mixin to your list views in order to preserve the existing behavior.
|
||||
|
||||
class DisablePaginationMixin(object):
|
||||
def get_paginate_by(self, queryset=None):
|
||||
|
|
|
@ -104,7 +104,7 @@ Don't forget to sync the database for the first time.
|
|||
|
||||
## Creating a Serializer class
|
||||
|
||||
The first thing we need to get started on our Web API is provide a way of serializing and deserializing the snippet instances into representations such as `json`. We can do this by declaring serializers that work very similar to Django's forms. Create a file in the `snippets` directory named `serializers.py` and add the following.
|
||||
The first thing we need to get started on our Web API is to provide a way of serializing and deserializing the snippet instances into representations such as `json`. We can do this by declaring serializers that work very similar to Django's forms. Create a file in the `snippets` directory named `serializers.py` and add the following.
|
||||
|
||||
from django.forms import widgets
|
||||
from rest_framework import serializers
|
||||
|
@ -143,7 +143,7 @@ The first thing we need to get started on our Web API is provide a way of serial
|
|||
# Create new instance
|
||||
return Snippet(**attrs)
|
||||
|
||||
The first part of serializer class defines the fields that get serialized/deserialized. The `restore_object` method defines how fully fledged instances get created when deserializing data.
|
||||
The first part of the serializer class defines the fields that get serialized/deserialized. The `restore_object` method defines how fully fledged instances get created when deserializing data.
|
||||
|
||||
Notice that we can also use various attributes that would typically be used on form fields, such as `widget=widgets.Textarea`. These can be used to control how the serializer should render when displayed as an HTML form. This is particularly useful for controlling how the browsable API should be displayed, as we'll see later in the tutorial.
|
||||
|
||||
|
|
|
@ -44,11 +44,11 @@ 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.
|
||||
|
||||
rm tmp.db
|
||||
python ./manage.py syncdb
|
||||
python manage.py syncdb
|
||||
|
||||
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.
|
||||
|
||||
python ./manage.py createsuperuser
|
||||
python manage.py createsuperuser
|
||||
|
||||
## Adding endpoints for our User models
|
||||
|
||||
|
@ -129,7 +129,7 @@ Then, add the following property to **both** the `SnippetList` and `SnippetDetai
|
|||
|
||||
If you open a browser and navigate to the browsable API at the moment, you'll find that you're no longer able to create new code snippets. In order to do so we'd need to be able to login as a user.
|
||||
|
||||
We can add a login view for use with the browsable API, by editing our URLconf once more.
|
||||
We can add a login view for use with the browsable API, by editing the URLconf in our project-level urls.py file.
|
||||
|
||||
Add the following import at the top of the file:
|
||||
|
||||
|
|
|
@ -21,7 +21,7 @@ First of all let's refactor our `UserList` and `UserDetail` views into a single
|
|||
queryset = User.objects.all()
|
||||
serializer_class = UserSerializer
|
||||
|
||||
Here we've used `ReadOnlyModelViewSet` class to automatically provide the default 'read-only' operations. We're still setting the `queryset` and `serializer_class` attributes exactly as we did when we were using regular views, but we no longer need to provide the same information to two separate classes.
|
||||
Here we've used the `ReadOnlyModelViewSet` class to automatically provide the default 'read-only' operations. We're still setting the `queryset` and `serializer_class` attributes exactly as we did when we were using regular views, but we no longer need to provide the same information to two separate classes.
|
||||
|
||||
Next we're going to replace the `SnippetList`, `SnippetDetail` and `SnippetHighlight` view classes. We can remove the three views, and again replace them with a single class.
|
||||
|
||||
|
@ -85,7 +85,7 @@ In the `urls.py` file we bind our `ViewSet` classes into a set of concrete views
|
|||
|
||||
Notice how we're creating multiple views from each `ViewSet` class, by binding the http methods to the required action for each view.
|
||||
|
||||
Now that we've bound our resources into concrete views, that we can register the views with the URL conf as usual.
|
||||
Now that we've bound our resources into concrete views, we can register the views with the URL conf as usual.
|
||||
|
||||
urlpatterns = format_suffix_patterns(patterns('snippets.views',
|
||||
url(r'^$', 'api_root'),
|
||||
|
@ -138,7 +138,7 @@ You can review the final [tutorial code][repo] on GitHub, or try out a live exam
|
|||
|
||||
## Onwards and upwards
|
||||
|
||||
We've reached the end of our tutorial. If you want to get more involved in the REST framework project, here's a few places you can start:
|
||||
We've reached the end of our tutorial. If you want to get more involved in the REST framework project, here are a few places you can start:
|
||||
|
||||
* Contribute on [GitHub][github] by reviewing and submitting issues, and making pull requests.
|
||||
* Join the [REST framework discussion group][group], and help build the community.
|
||||
|
|
|
@ -5,3 +5,4 @@ django-filter>=0.5.4
|
|||
django-oauth-plus>=2.2.1
|
||||
oauth2>=1.5.211
|
||||
django-oauth2-provider>=0.2.4
|
||||
Pillow==2.3.0
|
||||
|
|
|
@ -8,10 +8,10 @@ ______ _____ _____ _____ __ _
|
|||
"""
|
||||
|
||||
__title__ = 'Django REST framework'
|
||||
__version__ = '2.3.12'
|
||||
__version__ = '2.3.13'
|
||||
__author__ = 'Tom Christie'
|
||||
__license__ = 'BSD 2-Clause'
|
||||
__copyright__ = 'Copyright 2011-2013 Tom Christie'
|
||||
__copyright__ = 'Copyright 2011-2014 Tom Christie'
|
||||
|
||||
# Version synonym
|
||||
VERSION = __version__
|
||||
|
|
|
@ -6,6 +6,7 @@ import base64
|
|||
|
||||
from django.contrib.auth import authenticate
|
||||
from django.core.exceptions import ImproperlyConfigured
|
||||
from django.conf import settings
|
||||
from rest_framework import exceptions, HTTP_HEADER_ENCODING
|
||||
from rest_framework.compat import CsrfViewMiddleware
|
||||
from rest_framework.compat import oauth, oauth_provider, oauth_provider_store
|
||||
|
@ -291,6 +292,7 @@ class OAuth2Authentication(BaseAuthentication):
|
|||
OAuth 2 authentication backend using `django-oauth2-provider`
|
||||
"""
|
||||
www_authenticate_realm = 'api'
|
||||
allow_query_params_token = settings.DEBUG
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(OAuth2Authentication, self).__init__(*args, **kwargs)
|
||||
|
@ -308,7 +310,13 @@ class OAuth2Authentication(BaseAuthentication):
|
|||
|
||||
auth = get_authorization_header(request).split()
|
||||
|
||||
if not auth or auth[0].lower() != b'bearer':
|
||||
if auth and auth[0].lower() == b'bearer':
|
||||
access_token = auth[1]
|
||||
elif 'access_token' in request.POST:
|
||||
access_token = request.POST['access_token']
|
||||
elif 'access_token' in request.GET and self.allow_query_params_token:
|
||||
access_token = request.GET['access_token']
|
||||
else:
|
||||
return None
|
||||
|
||||
if len(auth) == 1:
|
||||
|
@ -318,7 +326,7 @@ class OAuth2Authentication(BaseAuthentication):
|
|||
msg = 'Invalid bearer header. Token string should not contain spaces.'
|
||||
raise exceptions.AuthenticationFailed(msg)
|
||||
|
||||
return self.authenticate_credentials(request, auth[1])
|
||||
return self.authenticate_credentials(request, access_token)
|
||||
|
||||
def authenticate_credentials(self, request, access_token):
|
||||
"""
|
||||
|
@ -326,11 +334,11 @@ class OAuth2Authentication(BaseAuthentication):
|
|||
"""
|
||||
|
||||
try:
|
||||
token = oauth2_provider.models.AccessToken.objects.select_related('user')
|
||||
token = oauth2_provider.oauth2.models.AccessToken.objects.select_related('user')
|
||||
# provider_now switches to timezone aware datetime when
|
||||
# the oauth2_provider version supports to it.
|
||||
token = token.get(token=access_token, expires__gt=provider_now())
|
||||
except oauth2_provider.models.AccessToken.DoesNotExist:
|
||||
except oauth2_provider.oauth2.models.AccessToken.DoesNotExist:
|
||||
raise exceptions.AuthenticationFailed('Invalid token')
|
||||
|
||||
user = token.user
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import uuid
|
||||
import hmac
|
||||
import binascii
|
||||
import os
|
||||
from hashlib import sha1
|
||||
from django.conf import settings
|
||||
from django.db import models
|
||||
|
@ -34,8 +34,7 @@ class Token(models.Model):
|
|||
return super(Token, self).save(*args, **kwargs)
|
||||
|
||||
def generate_key(self):
|
||||
unique = uuid.uuid4()
|
||||
return hmac.new(unique.bytes, digestmod=sha1).hexdigest()
|
||||
return binascii.hexlify(os.urandom(20)).decode()
|
||||
|
||||
def __unicode__(self):
|
||||
return self.key
|
||||
|
|
|
@ -1,4 +1,6 @@
|
|||
from django.contrib.auth import authenticate
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
from rest_framework import serializers
|
||||
|
||||
|
||||
|
@ -15,10 +17,13 @@ class AuthTokenSerializer(serializers.Serializer):
|
|||
|
||||
if user:
|
||||
if not user.is_active:
|
||||
raise serializers.ValidationError('User account is disabled.')
|
||||
msg = _('User account is disabled.')
|
||||
raise serializers.ValidationError(msg)
|
||||
attrs['user'] = user
|
||||
return attrs
|
||||
else:
|
||||
raise serializers.ValidationError('Unable to login with provided credentials.')
|
||||
msg = _('Unable to login with provided credentials.')
|
||||
raise serializers.ValidationError(msg)
|
||||
else:
|
||||
raise serializers.ValidationError('Must include "username" and "password"')
|
||||
msg = _('Must include "username" and "password"')
|
||||
raise serializers.ValidationError(msg)
|
||||
|
|
|
@ -457,7 +457,7 @@ from django.test.client import RequestFactory as DjangoRequestFactory
|
|||
from django.test.client import FakePayload
|
||||
try:
|
||||
# In 1.5 the test client uses force_bytes
|
||||
from django.utils.encoding import force_bytes_or_smart_bytes
|
||||
from django.utils.encoding import force_bytes as force_bytes_or_smart_bytes
|
||||
except ImportError:
|
||||
# In 1.3 and 1.4 the test client just uses smart_str
|
||||
from django.utils.encoding import smart_str as force_bytes_or_smart_bytes
|
||||
|
@ -550,13 +550,10 @@ except (ImportError, ImproperlyConfigured):
|
|||
|
||||
# OAuth 2 support is optional
|
||||
try:
|
||||
import provider.oauth2 as oauth2_provider
|
||||
from provider.oauth2 import models as oauth2_provider_models
|
||||
from provider.oauth2 import forms as oauth2_provider_forms
|
||||
import provider as oauth2_provider
|
||||
from provider import scope as oauth2_provider_scope
|
||||
from provider import constants as oauth2_constants
|
||||
from provider import __version__ as provider_version
|
||||
if provider_version in ('0.2.3', '0.2.4'):
|
||||
if oauth2_provider.__version__ in ('0.2.3', '0.2.4'):
|
||||
# 0.2.3 and 0.2.4 are supported version that do not support
|
||||
# timezone aware datetimes
|
||||
import datetime
|
||||
|
@ -566,8 +563,6 @@ try:
|
|||
from django.utils.timezone import now as provider_now
|
||||
except ImportError:
|
||||
oauth2_provider = None
|
||||
oauth2_provider_models = None
|
||||
oauth2_provider_forms = None
|
||||
oauth2_provider_scope = None
|
||||
oauth2_constants = None
|
||||
provider_now = None
|
||||
|
@ -584,3 +579,23 @@ if six.PY3:
|
|||
else:
|
||||
def is_non_str_iterable(obj):
|
||||
return hasattr(obj, '__iter__')
|
||||
|
||||
|
||||
try:
|
||||
from django.utils.encoding import python_2_unicode_compatible
|
||||
except ImportError:
|
||||
def python_2_unicode_compatible(klass):
|
||||
"""
|
||||
A decorator that defines __unicode__ and __str__ methods under Python 2.
|
||||
Under Python 3 it does nothing.
|
||||
|
||||
To support Python 2 and 3 with a single code base, define a __str__ method
|
||||
returning text and apply this decorator to the class.
|
||||
"""
|
||||
if '__str__' not in klass.__dict__:
|
||||
raise ValueError("@python_2_unicode_compatible cannot be applied "
|
||||
"to %s because it doesn't define __str__()." %
|
||||
klass.__name__)
|
||||
klass.__unicode__ = klass.__str__
|
||||
klass.__str__ = lambda self: self.__unicode__().encode('utf-8')
|
||||
return klass
|
||||
|
|
|
@ -12,7 +12,7 @@ import math
|
|||
class APIException(Exception):
|
||||
"""
|
||||
Base class for REST framework exceptions.
|
||||
Subclasses should provide `.status_code` and `.detail` properties.
|
||||
Subclasses should provide `.status_code` and `.default_detail` properties.
|
||||
"""
|
||||
status_code = status.HTTP_500_INTERNAL_SERVER_ERROR
|
||||
default_detail = ''
|
||||
|
@ -20,6 +20,8 @@ class APIException(Exception):
|
|||
def __init__(self, detail=None):
|
||||
self.detail = detail or self.default_detail
|
||||
|
||||
def __str__(self):
|
||||
return self.detail
|
||||
|
||||
class ParseError(APIException):
|
||||
status_code = status.HTTP_400_BAD_REQUEST
|
||||
|
|
|
@ -62,7 +62,7 @@ def get_component(obj, attr_name):
|
|||
|
||||
def readable_datetime_formats(formats):
|
||||
format = ', '.join(formats).replace(ISO_8601,
|
||||
'YYYY-MM-DDThh:mm[:ss[.uuuuuu]][+HHMM|-HHMM|Z]')
|
||||
'YYYY-MM-DDThh:mm[:ss[.uuuuuu]][+HH:MM|-HH:MM|Z]')
|
||||
return humanize_strptime(format)
|
||||
|
||||
|
||||
|
@ -154,7 +154,12 @@ class Field(object):
|
|||
def widget_html(self):
|
||||
if not self.widget:
|
||||
return ''
|
||||
return self.widget.render(self._name, self._value)
|
||||
|
||||
attrs = {}
|
||||
if 'id' not in self.widget.attrs:
|
||||
attrs['id'] = self._name
|
||||
|
||||
return self.widget.render(self._name, self._value, attrs=attrs)
|
||||
|
||||
def label_tag(self):
|
||||
return '<label for="%s">%s:</label>' % (self._name, self.label)
|
||||
|
@ -164,7 +169,7 @@ class Field(object):
|
|||
Called to set up a field prior to field_to_native or field_from_native.
|
||||
|
||||
parent - The parent serializer.
|
||||
model_field - The model field this field corresponds to, if one exists.
|
||||
field_name - The name of the field being initialized.
|
||||
"""
|
||||
self.parent = parent
|
||||
self.root = parent.root or parent
|
||||
|
@ -290,7 +295,7 @@ class WritableField(Field):
|
|||
self.validators = self.default_validators + validators
|
||||
self.default = default if default is not None else self.default
|
||||
|
||||
# Widgets are ony used for HTML forms.
|
||||
# Widgets are only used for HTML forms.
|
||||
widget = widget or self.widget
|
||||
if isinstance(widget, type):
|
||||
widget = widget()
|
||||
|
@ -302,6 +307,11 @@ class WritableField(Field):
|
|||
result.validators = self.validators[:]
|
||||
return result
|
||||
|
||||
def get_default_value(self):
|
||||
if is_simple_callable(self.default):
|
||||
return self.default()
|
||||
return self.default
|
||||
|
||||
def validate(self, value):
|
||||
if value in validators.EMPTY_VALUES and self.required:
|
||||
raise ValidationError(self.error_messages['required'])
|
||||
|
@ -350,10 +360,7 @@ class WritableField(Field):
|
|||
except KeyError:
|
||||
if self.default is not None and not self.partial:
|
||||
# Note: partial updates shouldn't set defaults
|
||||
if is_simple_callable(self.default):
|
||||
native = self.default()
|
||||
else:
|
||||
native = self.default
|
||||
native = self.get_default_value()
|
||||
else:
|
||||
if self.required:
|
||||
raise ValidationError(self.error_messages['required'])
|
||||
|
@ -478,7 +485,8 @@ class URLField(CharField):
|
|||
type_label = 'url'
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
kwargs['validators'] = [validators.URLValidator()]
|
||||
if not 'validators' in kwargs:
|
||||
kwargs['validators'] = [validators.URLValidator()]
|
||||
super(URLField, self).__init__(**kwargs)
|
||||
|
||||
|
||||
|
|
|
@ -6,6 +6,7 @@ from __future__ import unicode_literals
|
|||
from django.core.exceptions import ImproperlyConfigured
|
||||
from django.db import models
|
||||
from rest_framework.compat import django_filters, six, guardian, get_model_name
|
||||
from rest_framework.settings import api_settings
|
||||
from functools import reduce
|
||||
import operator
|
||||
|
||||
|
@ -69,7 +70,8 @@ class DjangoFilterBackend(BaseFilterBackend):
|
|||
|
||||
|
||||
class SearchFilter(BaseFilterBackend):
|
||||
search_param = 'search' # The URL query parameter used for the search.
|
||||
# The URL query parameter used for the search.
|
||||
search_param = api_settings.SEARCH_PARAM
|
||||
|
||||
def get_search_terms(self, request):
|
||||
"""
|
||||
|
@ -107,7 +109,8 @@ class SearchFilter(BaseFilterBackend):
|
|||
|
||||
|
||||
class OrderingFilter(BaseFilterBackend):
|
||||
ordering_param = 'ordering' # The URL query parameter used for the ordering.
|
||||
# The URL query parameter used for the ordering.
|
||||
ordering_param = api_settings.ORDERING_PARAM
|
||||
ordering_fields = None
|
||||
|
||||
def get_ordering(self, request):
|
||||
|
|
|
@ -116,30 +116,27 @@ class UpdateModelMixin(object):
|
|||
partial = kwargs.pop('partial', False)
|
||||
self.object = self.get_object_or_none()
|
||||
|
||||
if self.object is None:
|
||||
created = True
|
||||
save_kwargs = {'force_insert': True}
|
||||
success_status_code = status.HTTP_201_CREATED
|
||||
else:
|
||||
created = False
|
||||
save_kwargs = {'force_update': True}
|
||||
success_status_code = status.HTTP_200_OK
|
||||
|
||||
serializer = self.get_serializer(self.object, data=request.DATA,
|
||||
files=request.FILES, partial=partial)
|
||||
|
||||
if serializer.is_valid():
|
||||
try:
|
||||
self.pre_save(serializer.object)
|
||||
except ValidationError as err:
|
||||
# full_clean on model instance may be called in pre_save, so we
|
||||
# have to handle eventual errors.
|
||||
return Response(err.message_dict, status=status.HTTP_400_BAD_REQUEST)
|
||||
self.object = serializer.save(**save_kwargs)
|
||||
self.post_save(self.object, created=created)
|
||||
return Response(serializer.data, status=success_status_code)
|
||||
if not serializer.is_valid():
|
||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||
try:
|
||||
self.pre_save(serializer.object)
|
||||
except ValidationError as err:
|
||||
# full_clean on model instance may be called in pre_save,
|
||||
# so we have to handle eventual errors.
|
||||
return Response(err.message_dict, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
if self.object is None:
|
||||
self.object = serializer.save(force_insert=True)
|
||||
self.post_save(self.object, created=True)
|
||||
return Response(serializer.data, status=status.HTTP_201_CREATED)
|
||||
|
||||
self.object = serializer.save(force_update=True)
|
||||
self.post_save(self.object, created=False)
|
||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||
|
||||
def partial_update(self, request, *args, **kwargs):
|
||||
kwargs['partial'] = True
|
||||
|
|
|
@ -10,7 +10,7 @@ from django.core.files.uploadhandler import StopFutureHandlers
|
|||
from django.http import QueryDict
|
||||
from django.http.multipartparser import MultiPartParser as DjangoMultiPartParser
|
||||
from django.http.multipartparser import MultiPartParserError, parse_header, ChunkIter
|
||||
from rest_framework.compat import etree, six, yaml
|
||||
from rest_framework.compat import etree, six, yaml, force_text
|
||||
from rest_framework.exceptions import ParseError
|
||||
from rest_framework import renderers
|
||||
import json
|
||||
|
@ -288,7 +288,7 @@ class FileUploadParser(BaseParser):
|
|||
|
||||
try:
|
||||
meta = parser_context['request'].META
|
||||
disposition = parse_header(meta['HTTP_CONTENT_DISPOSITION'])
|
||||
return disposition[1]['filename']
|
||||
disposition = parse_header(meta['HTTP_CONTENT_DISPOSITION'].encode('utf-8'))
|
||||
return force_text(disposition[1]['filename'])
|
||||
except (AttributeError, KeyError):
|
||||
pass
|
||||
|
|
|
@ -33,6 +33,7 @@ class RelatedField(WritableField):
|
|||
many_widget = widgets.SelectMultiple
|
||||
form_field_class = forms.ChoiceField
|
||||
many_form_field_class = forms.MultipleChoiceField
|
||||
null_values = (None, '', 'None')
|
||||
|
||||
cache_choices = False
|
||||
empty_label = None
|
||||
|
@ -58,6 +59,8 @@ class RelatedField(WritableField):
|
|||
super(RelatedField, self).__init__(*args, **kwargs)
|
||||
|
||||
if not self.required:
|
||||
# Accessed in ModelChoiceIterator django/forms/models.py:1034
|
||||
# If set adds empty choice.
|
||||
self.empty_label = BLANK_CHOICE_DASH[0][1]
|
||||
|
||||
self.queryset = queryset
|
||||
|
@ -118,6 +121,14 @@ class RelatedField(WritableField):
|
|||
|
||||
choices = property(_get_choices, _set_choices)
|
||||
|
||||
### Default value handling
|
||||
|
||||
def get_default_value(self):
|
||||
default = super(RelatedField, self).get_default_value()
|
||||
if self.many and default is None:
|
||||
return []
|
||||
return default
|
||||
|
||||
### Regular serializer stuff...
|
||||
|
||||
def field_to_native(self, obj, field_name):
|
||||
|
@ -166,11 +177,11 @@ class RelatedField(WritableField):
|
|||
except KeyError:
|
||||
if self.partial:
|
||||
return
|
||||
value = [] if self.many else None
|
||||
value = self.get_default_value()
|
||||
|
||||
if value in (None, '') and self.required:
|
||||
raise ValidationError(self.error_messages['required'])
|
||||
elif value in (None, ''):
|
||||
if value in self.null_values:
|
||||
if self.required:
|
||||
raise ValidationError(self.error_messages['required'])
|
||||
into[(self.source or field_name)] = None
|
||||
elif self.many:
|
||||
into[(self.source or field_name)] = [self.from_native(item) for item in value]
|
||||
|
|
|
@ -10,6 +10,7 @@ from __future__ import unicode_literals
|
|||
|
||||
import copy
|
||||
import json
|
||||
import django
|
||||
from django import forms
|
||||
from django.core.exceptions import ImproperlyConfigured
|
||||
from django.http.multipartparser import parse_header
|
||||
|
@ -145,7 +146,7 @@ class XMLRenderer(BaseRenderer):
|
|||
|
||||
def render(self, data, accepted_media_type=None, renderer_context=None):
|
||||
"""
|
||||
Renders *obj* into serialized XML.
|
||||
Renders `data` into serialized XML.
|
||||
"""
|
||||
if data is None:
|
||||
return ''
|
||||
|
@ -192,17 +193,26 @@ class YAMLRenderer(BaseRenderer):
|
|||
format = 'yaml'
|
||||
encoder = encoders.SafeDumper
|
||||
charset = 'utf-8'
|
||||
ensure_ascii = True
|
||||
|
||||
def render(self, data, accepted_media_type=None, renderer_context=None):
|
||||
"""
|
||||
Renders *obj* into serialized YAML.
|
||||
Renders `data` into serialized YAML.
|
||||
"""
|
||||
assert yaml, 'YAMLRenderer requires pyyaml to be installed'
|
||||
|
||||
if data is None:
|
||||
return ''
|
||||
|
||||
return yaml.dump(data, stream=None, encoding=self.charset, Dumper=self.encoder)
|
||||
return yaml.dump(data, stream=None, encoding=self.charset, Dumper=self.encoder, allow_unicode=not self.ensure_ascii)
|
||||
|
||||
|
||||
class UnicodeYAMLRenderer(YAMLRenderer):
|
||||
"""
|
||||
Renderer which serializes to YAML.
|
||||
Does *not* apply character escaping for non-ascii characters.
|
||||
"""
|
||||
ensure_ascii = False
|
||||
|
||||
|
||||
class TemplateHTMLRenderer(BaseRenderer):
|
||||
|
@ -543,6 +553,14 @@ class BrowsableAPIRenderer(BaseRenderer):
|
|||
raw_data_patch_form = self.get_raw_data_form(view, 'PATCH', request)
|
||||
raw_data_put_or_patch_form = raw_data_put_form or raw_data_patch_form
|
||||
|
||||
response_headers = dict(response.items())
|
||||
renderer_content_type = ''
|
||||
if renderer:
|
||||
renderer_content_type = '%s' % renderer.media_type
|
||||
if renderer.charset:
|
||||
renderer_content_type += ' ;%s' % renderer.charset
|
||||
response_headers['Content-Type'] = renderer_content_type
|
||||
|
||||
context = {
|
||||
'content': self.get_content(renderer, data, accepted_media_type, renderer_context),
|
||||
'view': view,
|
||||
|
@ -554,6 +572,7 @@ class BrowsableAPIRenderer(BaseRenderer):
|
|||
'breadcrumblist': self.get_breadcrumbs(request),
|
||||
'allowed_methods': view.allowed_methods,
|
||||
'available_formats': [renderer.format for renderer in view.renderer_classes],
|
||||
'response_headers': response_headers,
|
||||
|
||||
'put_form': self.get_rendered_html_form(view, 'PUT', request),
|
||||
'post_form': self.get_rendered_html_form(view, 'POST', request),
|
||||
|
@ -597,7 +616,7 @@ class MultiPartRenderer(BaseRenderer):
|
|||
media_type = 'multipart/form-data; boundary=BoUnDaRyStRiNg'
|
||||
format = 'multipart'
|
||||
charset = 'utf-8'
|
||||
BOUNDARY = 'BoUnDaRyStRiNg'
|
||||
BOUNDARY = 'BoUnDaRyStRiNg' if django.VERSION >= (1, 5) else b'BoUnDaRyStRiNg'
|
||||
|
||||
def render(self, data, accepted_media_type=None, renderer_context=None):
|
||||
return encode_multipart(self.BOUNDARY, data)
|
||||
|
|
|
@ -346,7 +346,7 @@ class Request(object):
|
|||
media_type = self.content_type
|
||||
|
||||
if stream is None or media_type is None:
|
||||
empty_data = QueryDict('', self._request._encoding)
|
||||
empty_data = QueryDict('', encoding=self._request._encoding)
|
||||
empty_files = MultiValueDict()
|
||||
return (empty_data, empty_files)
|
||||
|
||||
|
@ -362,7 +362,7 @@ class Request(object):
|
|||
# re-raise. Ensures we don't simply repeat the error when
|
||||
# attempting to render the browsable renderer response, or when
|
||||
# logging the request or similar.
|
||||
self._data = QueryDict('', self._request._encoding)
|
||||
self._data = QueryDict('', encoding=self._request._encoding)
|
||||
self._files = MultiValueDict()
|
||||
raise
|
||||
|
||||
|
|
|
@ -26,6 +26,10 @@ def usage():
|
|||
|
||||
|
||||
def main():
|
||||
try:
|
||||
django.setup()
|
||||
except AttributeError:
|
||||
pass
|
||||
TestRunner = get_runner(settings)
|
||||
|
||||
test_runner = TestRunner()
|
||||
|
|
|
@ -16,6 +16,7 @@ import datetime
|
|||
import inspect
|
||||
import types
|
||||
from decimal import Decimal
|
||||
from django.contrib.contenttypes.generic import GenericForeignKey
|
||||
from django.core.paginator import Page
|
||||
from django.db import models
|
||||
from django.forms import widgets
|
||||
|
@ -439,16 +440,6 @@ class BaseSerializer(WritableField):
|
|||
raise ValidationError(self.error_messages['required'])
|
||||
return
|
||||
|
||||
# Set the serializer object if it exists
|
||||
obj = get_component(self.parent.object, self.source or field_name) if self.parent.object else None
|
||||
|
||||
# If we have a model manager or similar object then we need
|
||||
# to iterate through each instance.
|
||||
if (self.many and
|
||||
not hasattr(obj, '__iter__') and
|
||||
is_simple_callable(getattr(obj, 'all', None))):
|
||||
obj = obj.all()
|
||||
|
||||
if self.source == '*':
|
||||
if value:
|
||||
reverted_data = self.restore_fields(value, {})
|
||||
|
@ -458,6 +449,16 @@ class BaseSerializer(WritableField):
|
|||
if value in (None, ''):
|
||||
into[(self.source or field_name)] = None
|
||||
else:
|
||||
# Set the serializer object if it exists
|
||||
obj = get_component(self.parent.object, self.source or field_name) if self.parent.object else None
|
||||
|
||||
# If we have a model manager or similar object then we need
|
||||
# to iterate through each instance.
|
||||
if (self.many and
|
||||
not hasattr(obj, '__iter__') and
|
||||
is_simple_callable(getattr(obj, 'all', None))):
|
||||
obj = obj.all()
|
||||
|
||||
kwargs = {
|
||||
'instance': obj,
|
||||
'data': value,
|
||||
|
@ -502,7 +503,7 @@ class BaseSerializer(WritableField):
|
|||
else:
|
||||
many = hasattr(data, '__iter__') and not isinstance(data, (Page, dict, six.text_type))
|
||||
if many:
|
||||
warnings.warn('Implict list/queryset serialization is deprecated. '
|
||||
warnings.warn('Implicit list/queryset serialization is deprecated. '
|
||||
'Use the `many=True` flag when instantiating the serializer.',
|
||||
DeprecationWarning, stacklevel=3)
|
||||
|
||||
|
@ -564,7 +565,7 @@ class BaseSerializer(WritableField):
|
|||
else:
|
||||
many = hasattr(obj, '__iter__') and not isinstance(obj, (Page, dict))
|
||||
if many:
|
||||
warnings.warn('Implict list/queryset serialization is deprecated. '
|
||||
warnings.warn('Implicit list/queryset serialization is deprecated. '
|
||||
'Use the `many=True` flag when instantiating the serializer.',
|
||||
DeprecationWarning, stacklevel=2)
|
||||
|
||||
|
@ -759,7 +760,10 @@ class ModelSerializer(Serializer):
|
|||
|
||||
ret[accessor_name] = field
|
||||
|
||||
# Add the `read_only` flag to any fields that have bee specified
|
||||
# Ensure that 'read_only_fields' is an iterable
|
||||
assert isinstance(self.opts.read_only_fields, (list, tuple)), '`read_only_fields` must be a list or tuple'
|
||||
|
||||
# Add the `read_only` flag to any fields that have been specified
|
||||
# in the `read_only_fields` option
|
||||
for field_name in self.opts.read_only_fields:
|
||||
assert field_name not in self.base_fields.keys(), (
|
||||
|
@ -773,6 +777,9 @@ class ModelSerializer(Serializer):
|
|||
(field_name, self.__class__.__name__))
|
||||
ret[field_name].read_only = True
|
||||
|
||||
# Ensure that 'write_only_fields' is an iterable
|
||||
assert isinstance(self.opts.write_only_fields, (list, tuple)), '`write_only_fields` must be a list or tuple'
|
||||
|
||||
for field_name in self.opts.write_only_fields:
|
||||
assert field_name not in self.base_fields.keys(), (
|
||||
"field '%s' on serializer '%s' specified in "
|
||||
|
@ -822,6 +829,19 @@ class ModelSerializer(Serializer):
|
|||
|
||||
if model_field:
|
||||
kwargs['required'] = not(model_field.null or model_field.blank)
|
||||
if model_field.help_text is not None:
|
||||
kwargs['help_text'] = model_field.help_text
|
||||
if model_field.verbose_name is not None:
|
||||
kwargs['label'] = model_field.verbose_name
|
||||
|
||||
if not model_field.editable:
|
||||
kwargs['read_only'] = True
|
||||
|
||||
if model_field.verbose_name is not None:
|
||||
kwargs['label'] = model_field.verbose_name
|
||||
|
||||
if model_field.help_text is not None:
|
||||
kwargs['help_text'] = model_field.help_text
|
||||
|
||||
return PrimaryKeyRelatedField(**kwargs)
|
||||
|
||||
|
@ -882,7 +902,7 @@ class ModelSerializer(Serializer):
|
|||
except KeyError:
|
||||
return ModelField(model_field=model_field, **kwargs)
|
||||
|
||||
def get_validation_exclusions(self):
|
||||
def get_validation_exclusions(self, instance=None):
|
||||
"""
|
||||
Return a list of field names to exclude from model validation.
|
||||
"""
|
||||
|
@ -894,6 +914,7 @@ class ModelSerializer(Serializer):
|
|||
field_name = field.source or field_name
|
||||
if field_name in exclusions \
|
||||
and not field.read_only \
|
||||
and (field.required or hasattr(instance, field_name)) \
|
||||
and not isinstance(field, Serializer):
|
||||
exclusions.remove(field_name)
|
||||
return exclusions
|
||||
|
@ -908,7 +929,7 @@ class ModelSerializer(Serializer):
|
|||
the full_clean validation checking.
|
||||
"""
|
||||
try:
|
||||
instance.full_clean(exclude=self.get_validation_exclusions())
|
||||
instance.full_clean(exclude=self.get_validation_exclusions(instance))
|
||||
except ValidationError as err:
|
||||
self._errors = err.message_dict
|
||||
return None
|
||||
|
@ -938,6 +959,8 @@ class ModelSerializer(Serializer):
|
|||
|
||||
# Forward m2m relations
|
||||
for field in meta.many_to_many + meta.virtual_fields:
|
||||
if isinstance(field, GenericForeignKey):
|
||||
continue
|
||||
if field.name in attrs:
|
||||
m2m_data[field.name] = attrs.pop(field.name)
|
||||
|
||||
|
@ -948,39 +971,34 @@ class ModelSerializer(Serializer):
|
|||
nested_forward_relations[field_name] = attrs[field_name]
|
||||
|
||||
# Update an existing instance...
|
||||
if instance is not None:
|
||||
for key, val in attrs.items():
|
||||
keys = key.split('.')
|
||||
if instance is None:
|
||||
instance = self.opts.model()
|
||||
|
||||
# Work on the current instance for this attribute
|
||||
attr_instance = instance
|
||||
for key, val in attrs.items():
|
||||
keys = key.split('.')
|
||||
|
||||
# Raise an error if we span more than one relation
|
||||
if len(keys) > 2:
|
||||
self._errors[key] = 'Can not span more than a relation.'
|
||||
# Work on the current instance for this attribute
|
||||
attr_instance = instance
|
||||
|
||||
# Raise an error if we span more than one relation
|
||||
if len(keys) > 2:
|
||||
self._errors[key] = 'Can not span more than a relation.'
|
||||
continue
|
||||
|
||||
# Mark the related instance as the one to save
|
||||
if len(keys) == 2:
|
||||
try:
|
||||
attr_instance = getattr(instance, keys[0])
|
||||
related_models_to_save[key] = attr_instance
|
||||
except AttributeError:
|
||||
self._errors[key] = self.error_messages['missing']
|
||||
continue
|
||||
|
||||
# Mark the related instance as the one to save
|
||||
if len(keys) == 2:
|
||||
try:
|
||||
attr_instance = getattr(instance, keys[0])
|
||||
related_models_to_save[key] = attr_instance
|
||||
except AttributeError:
|
||||
self._errors[key] = self.error_messages['missing']
|
||||
continue
|
||||
|
||||
# Assign the value
|
||||
try:
|
||||
setattr(attr_instance, keys[-1], val)
|
||||
except ValueError:
|
||||
self._errors[key] = self.error_messages['required']
|
||||
|
||||
# ...or create a new instance
|
||||
else:
|
||||
for key, value in ((k, v) for k, v in attrs.items() if '.' in k):
|
||||
self._errors[key] = 'You can not set dotted sources during creation.'
|
||||
del attrs[key]
|
||||
instance = self.opts.model(**attrs)
|
||||
# Assign the value
|
||||
try:
|
||||
setattr(attr_instance, keys[-1], val)
|
||||
except ValueError:
|
||||
self._errors[key] = self.error_messages['required']
|
||||
|
||||
# Any relations that cannot be set until we've
|
||||
# saved the model get hidden away on these
|
||||
|
@ -1110,6 +1128,10 @@ class HyperlinkedModelSerializer(ModelSerializer):
|
|||
|
||||
if model_field:
|
||||
kwargs['required'] = not(model_field.null or model_field.blank)
|
||||
if model_field.help_text is not None:
|
||||
kwargs['help_text'] = model_field.help_text
|
||||
if model_field.verbose_name is not None:
|
||||
kwargs['label'] = model_field.verbose_name
|
||||
|
||||
if self.opts.lookup_field:
|
||||
kwargs['lookup_field'] = self.opts.lookup_field
|
||||
|
|
|
@ -69,6 +69,10 @@ DEFAULTS = {
|
|||
'PAGINATE_BY_PARAM': None,
|
||||
'MAX_PAGINATE_BY': None,
|
||||
|
||||
# Filtering
|
||||
'SEARCH_PARAM': 'search',
|
||||
'ORDERING_PARAM': 'ordering',
|
||||
|
||||
# Authentication
|
||||
'UNAUTHENTICATED_USER': 'django.contrib.auth.models.AnonymousUser',
|
||||
'UNAUTHENTICATED_TOKEN': None,
|
||||
|
|
|
@ -118,7 +118,7 @@
|
|||
</div>
|
||||
<div class="response-info">
|
||||
<pre class="prettyprint"><div class="meta nocode"><b>HTTP {{ response.status_code }} {{ response.status_text }}</b>{% autoescape off %}
|
||||
{% for key, val in response.items %}<b>{{ key }}:</b> <span class="lit">{{ val|break_long_headers|urlize_quoted_links }}</span>
|
||||
{% for key, val in response_headers.items %}<b>{{ key }}:</b> <span class="lit">{{ val|break_long_headers|urlize_quoted_links }}</span>
|
||||
{% endfor %}
|
||||
</div>{{ content|urlize_quoted_links }}</pre>{% endautoescape %}
|
||||
</div>
|
||||
|
|
|
@ -6,7 +6,7 @@ from django.utils.encoding import iri_to_uri
|
|||
from django.utils.html import escape
|
||||
from django.utils.safestring import SafeData, mark_safe
|
||||
from rest_framework.compat import urlparse, force_text, six, smart_urlquote
|
||||
import re, string
|
||||
import re
|
||||
|
||||
register = template.Library()
|
||||
|
||||
|
@ -180,7 +180,7 @@ def add_class(value, css_class):
|
|||
|
||||
|
||||
# Bunch of stuff cloned from urlize
|
||||
TRAILING_PUNCTUATION = ['.', ',', ':', ';', '.)', '"', "'"]
|
||||
TRAILING_PUNCTUATION = ['.', ',', ':', ';', '.)', '"', "']", "'}", "'"]
|
||||
WRAPPING_PUNCTUATION = [('(', ')'), ('<', '>'), ('[', ']'), ('<', '>'),
|
||||
('"', '"'), ("'", "'")]
|
||||
word_split_re = re.compile(r'(\s+)')
|
||||
|
@ -189,6 +189,17 @@ simple_url_2_re = re.compile(r'^www\.|^(?!http)\w[^@]+\.(com|edu|gov|int|mil|net
|
|||
simple_email_re = re.compile(r'^\S+@\S+\.\S+$')
|
||||
|
||||
|
||||
def smart_urlquote_wrapper(matched_url):
|
||||
"""
|
||||
Simple wrapper for smart_urlquote. ValueError("Invalid IPv6 URL") can
|
||||
be raised here, see issue #1386
|
||||
"""
|
||||
try:
|
||||
return smart_urlquote(matched_url)
|
||||
except ValueError:
|
||||
return None
|
||||
|
||||
|
||||
@register.filter
|
||||
def urlize_quoted_links(text, trim_url_limit=None, nofollow=True, autoescape=True):
|
||||
"""
|
||||
|
@ -211,7 +222,6 @@ def urlize_quoted_links(text, trim_url_limit=None, nofollow=True, autoescape=Tru
|
|||
safe_input = isinstance(text, SafeData)
|
||||
words = word_split_re.split(force_text(text))
|
||||
for i, word in enumerate(words):
|
||||
match = None
|
||||
if '.' in word or '@' in word or ':' in word:
|
||||
# Deal with punctuation.
|
||||
lead, middle, trail = '', word, ''
|
||||
|
@ -233,9 +243,9 @@ def urlize_quoted_links(text, trim_url_limit=None, nofollow=True, autoescape=Tru
|
|||
url = None
|
||||
nofollow_attr = ' rel="nofollow"' if nofollow else ''
|
||||
if simple_url_re.match(middle):
|
||||
url = smart_urlquote(middle)
|
||||
url = smart_urlquote_wrapper(middle)
|
||||
elif simple_url_2_re.match(middle):
|
||||
url = smart_urlquote('http://%s' % middle)
|
||||
url = smart_urlquote_wrapper('http://%s' % middle)
|
||||
elif not ':' in middle and simple_email_re.match(middle):
|
||||
local, domain = middle.rsplit('@', 1)
|
||||
try:
|
||||
|
|
|
@ -8,6 +8,7 @@ from django.conf import settings
|
|||
from django.test.client import Client as DjangoClient
|
||||
from django.test.client import ClientHandler
|
||||
from django.test import testcases
|
||||
from django.utils.http import urlencode
|
||||
from rest_framework.settings import api_settings
|
||||
from rest_framework.compat import RequestFactory as DjangoRequestFactory
|
||||
from rest_framework.compat import force_bytes_or_smart_bytes, six
|
||||
|
@ -71,6 +72,17 @@ class APIRequestFactory(DjangoRequestFactory):
|
|||
|
||||
return ret, content_type
|
||||
|
||||
def get(self, path, data=None, **extra):
|
||||
r = {
|
||||
'QUERY_STRING': urlencode(data or {}, doseq=True),
|
||||
}
|
||||
# Fix to support old behavior where you have the arguments in the url
|
||||
# See #1461
|
||||
if not data and '?' in path:
|
||||
r['QUERY_STRING'] = path.split('?')[1]
|
||||
r.update(extra)
|
||||
return self.generic('GET', path, **r)
|
||||
|
||||
def post(self, path, data=None, format=None, content_type=None, **extra):
|
||||
data, content_type = self._encode_data(data, format, content_type)
|
||||
return self.generic('POST', path, data, content_type, **extra)
|
||||
|
|
|
@ -103,7 +103,7 @@ class BlogPostComment(RESTFrameworkModel):
|
|||
|
||||
class Album(RESTFrameworkModel):
|
||||
title = models.CharField(max_length=100, unique=True)
|
||||
|
||||
ref = models.CharField(max_length=10, unique=True, null=True, blank=True)
|
||||
|
||||
class Photo(RESTFrameworkModel):
|
||||
description = models.TextField()
|
||||
|
@ -143,14 +143,16 @@ class ForeignKeyTarget(RESTFrameworkModel):
|
|||
|
||||
class ForeignKeySource(RESTFrameworkModel):
|
||||
name = models.CharField(max_length=100)
|
||||
target = models.ForeignKey(ForeignKeyTarget, related_name='sources')
|
||||
target = models.ForeignKey(ForeignKeyTarget, related_name='sources',
|
||||
help_text='Target', verbose_name='Target')
|
||||
|
||||
|
||||
# Nullable ForeignKey
|
||||
class NullableForeignKeySource(RESTFrameworkModel):
|
||||
name = models.CharField(max_length=100)
|
||||
target = models.ForeignKey(ForeignKeyTarget, null=True, blank=True,
|
||||
related_name='nullable_sources')
|
||||
related_name='nullable_sources',
|
||||
verbose_name='Optional target object')
|
||||
|
||||
|
||||
# OneToOne
|
||||
|
@ -168,3 +170,10 @@ class NullableOneToOneSource(RESTFrameworkModel):
|
|||
class BasicModelSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = BasicModel
|
||||
|
||||
|
||||
# Models to test filters
|
||||
class FilterableItem(models.Model):
|
||||
text = models.CharField(max_length=100)
|
||||
decimal = models.DecimalField(max_digits=4, decimal_places=2)
|
||||
date = models.DateField()
|
||||
|
|
8
rest_framework/tests/serializers.py
Normal file
8
rest_framework/tests/serializers.py
Normal file
|
@ -0,0 +1,8 @@
|
|||
from rest_framework import serializers
|
||||
|
||||
from rest_framework.tests.models import NullableForeignKeySource
|
||||
|
||||
|
||||
class NullableFKSourceSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = NullableForeignKeySource
|
|
@ -3,6 +3,7 @@ from django.contrib.auth.models import User
|
|||
from django.http import HttpResponse
|
||||
from django.test import TestCase
|
||||
from django.utils import unittest
|
||||
from django.utils.http import urlencode
|
||||
from rest_framework import HTTP_HEADER_ENCODING
|
||||
from rest_framework import exceptions
|
||||
from rest_framework import permissions
|
||||
|
@ -18,8 +19,8 @@ from rest_framework.authentication import (
|
|||
OAuth2Authentication
|
||||
)
|
||||
from rest_framework.authtoken.models import Token
|
||||
from rest_framework.compat import patterns, url, include
|
||||
from rest_framework.compat import oauth2_provider, oauth2_provider_models, oauth2_provider_scope
|
||||
from rest_framework.compat import patterns, url, include, six
|
||||
from rest_framework.compat import oauth2_provider, oauth2_provider_scope
|
||||
from rest_framework.compat import oauth, oauth_provider
|
||||
from rest_framework.test import APIRequestFactory, APIClient
|
||||
from rest_framework.views import APIView
|
||||
|
@ -53,10 +54,14 @@ urlpatterns = patterns('',
|
|||
permission_classes=[permissions.TokenHasReadWriteScope]))
|
||||
)
|
||||
|
||||
class OAuth2AuthenticationDebug(OAuth2Authentication):
|
||||
allow_query_params_token = True
|
||||
|
||||
if oauth2_provider is not None:
|
||||
urlpatterns += patterns('',
|
||||
url(r'^oauth2/', include('provider.oauth2.urls', namespace='oauth2')),
|
||||
url(r'^oauth2-test/$', MockView.as_view(authentication_classes=[OAuth2Authentication])),
|
||||
url(r'^oauth2-test-debug/$', MockView.as_view(authentication_classes=[OAuth2AuthenticationDebug])),
|
||||
url(r'^oauth2-with-scope-test/$', MockView.as_view(authentication_classes=[OAuth2Authentication],
|
||||
permission_classes=[permissions.TokenHasReadWriteScope])),
|
||||
)
|
||||
|
@ -190,6 +195,12 @@ class TokenAuthTests(TestCase):
|
|||
token = Token.objects.create(user=self.user)
|
||||
self.assertTrue(bool(token.key))
|
||||
|
||||
def test_generate_key_returns_string(self):
|
||||
"""Ensure generate_key returns a string"""
|
||||
token = Token()
|
||||
key = token.generate_key()
|
||||
self.assertTrue(isinstance(key, six.string_types))
|
||||
|
||||
def test_token_login_json(self):
|
||||
"""Ensure token login view using JSON POST works."""
|
||||
client = APIClient(enforce_csrf_checks=True)
|
||||
|
@ -488,7 +499,7 @@ class OAuth2Tests(TestCase):
|
|||
self.ACCESS_TOKEN = "access_token"
|
||||
self.REFRESH_TOKEN = "refresh_token"
|
||||
|
||||
self.oauth2_client = oauth2_provider_models.Client.objects.create(
|
||||
self.oauth2_client = oauth2_provider.oauth2.models.Client.objects.create(
|
||||
client_id=self.CLIENT_ID,
|
||||
client_secret=self.CLIENT_SECRET,
|
||||
redirect_uri='',
|
||||
|
@ -497,12 +508,12 @@ class OAuth2Tests(TestCase):
|
|||
user=None,
|
||||
)
|
||||
|
||||
self.access_token = oauth2_provider_models.AccessToken.objects.create(
|
||||
self.access_token = oauth2_provider.oauth2.models.AccessToken.objects.create(
|
||||
token=self.ACCESS_TOKEN,
|
||||
client=self.oauth2_client,
|
||||
user=self.user,
|
||||
)
|
||||
self.refresh_token = oauth2_provider_models.RefreshToken.objects.create(
|
||||
self.refresh_token = oauth2_provider.oauth2.models.RefreshToken.objects.create(
|
||||
user=self.user,
|
||||
access_token=self.access_token,
|
||||
client=self.oauth2_client
|
||||
|
@ -545,6 +556,27 @@ class OAuth2Tests(TestCase):
|
|||
response = self.csrf_client.get('/oauth2-test/', HTTP_AUTHORIZATION=auth)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
@unittest.skipUnless(oauth2_provider, 'django-oauth2-provider not installed')
|
||||
def test_post_form_passing_auth_url_transport(self):
|
||||
"""Ensure GETing form over OAuth with correct client credentials in form data succeed"""
|
||||
response = self.csrf_client.post('/oauth2-test/',
|
||||
data={'access_token': self.access_token.token})
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
@unittest.skipUnless(oauth2_provider, 'django-oauth2-provider not installed')
|
||||
def test_get_form_passing_auth_url_transport(self):
|
||||
"""Ensure GETing form over OAuth with correct client credentials in query succeed when DEBUG is True"""
|
||||
query = urlencode({'access_token': self.access_token.token})
|
||||
response = self.csrf_client.get('/oauth2-test-debug/?%s' % query)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
@unittest.skipUnless(oauth2_provider, 'django-oauth2-provider not installed')
|
||||
def test_get_form_failing_auth_url_transport(self):
|
||||
"""Ensure GETing form over OAuth with correct client credentials in query fails when DEBUG is False"""
|
||||
query = urlencode({'access_token': self.access_token.token})
|
||||
response = self.csrf_client.get('/oauth2-test/?%s' % query)
|
||||
self.assertIn(response.status_code, (status.HTTP_401_UNAUTHORIZED, status.HTTP_403_FORBIDDEN))
|
||||
|
||||
@unittest.skipUnless(oauth2_provider, 'django-oauth2-provider not installed')
|
||||
def test_post_form_passing_auth(self):
|
||||
"""Ensure POSTing form over OAuth with correct credentials passes and does not require CSRF"""
|
||||
|
|
|
@ -4,6 +4,7 @@ General serializer field tests.
|
|||
from __future__ import unicode_literals
|
||||
|
||||
import datetime
|
||||
import re
|
||||
from decimal import Decimal
|
||||
from uuid import uuid4
|
||||
from django.core import validators
|
||||
|
@ -103,6 +104,16 @@ class BasicFieldTests(TestCase):
|
|||
keys = list(field.to_native(ret).keys())
|
||||
self.assertEqual(keys, ['c', 'b', 'a', 'z'])
|
||||
|
||||
def test_widget_html_attributes(self):
|
||||
"""
|
||||
Make sure widget_html() renders the correct attributes
|
||||
"""
|
||||
r = re.compile('(\S+)=["\']?((?:.(?!["\']?\s+(?:\S+)=|[>"\']))+.)["\']?')
|
||||
form = TimeFieldModelSerializer().data
|
||||
attributes = r.findall(form.fields['clock'].widget_html())
|
||||
self.assertIn(('name', 'clock'), attributes)
|
||||
self.assertIn(('id', 'clock'), attributes)
|
||||
|
||||
|
||||
class DateFieldTest(TestCase):
|
||||
"""
|
||||
|
@ -312,7 +323,7 @@ class DateTimeFieldTest(TestCase):
|
|||
f.from_native('04:61:59')
|
||||
except validators.ValidationError as e:
|
||||
self.assertEqual(e.messages, ["Datetime has wrong format. Use one of these formats instead: "
|
||||
"YYYY-MM-DDThh:mm[:ss[.uuuuuu]][+HHMM|-HHMM|Z]"])
|
||||
"YYYY-MM-DDThh:mm[:ss[.uuuuuu]][+HH:MM|-HH:MM|Z]"])
|
||||
else:
|
||||
self.fail("ValidationError was not properly raised")
|
||||
|
||||
|
@ -326,7 +337,7 @@ class DateTimeFieldTest(TestCase):
|
|||
f.from_native('04 -- 31')
|
||||
except validators.ValidationError as e:
|
||||
self.assertEqual(e.messages, ["Datetime has wrong format. Use one of these formats instead: "
|
||||
"YYYY-MM-DDThh:mm[:ss[.uuuuuu]][+HHMM|-HHMM|Z]"])
|
||||
"YYYY-MM-DDThh:mm[:ss[.uuuuuu]][+HH:MM|-HH:MM|Z]"])
|
||||
else:
|
||||
self.fail("ValidationError was not properly raised")
|
||||
|
||||
|
@ -860,7 +871,9 @@ class SlugFieldTests(TestCase):
|
|||
|
||||
class URLFieldTests(TestCase):
|
||||
"""
|
||||
Tests for URLField attribute values
|
||||
Tests for URLField attribute values.
|
||||
|
||||
(Includes test for #1210, checking that validators can be overridden.)
|
||||
"""
|
||||
|
||||
class URLFieldModel(RESTFrameworkModel):
|
||||
|
@ -902,6 +915,11 @@ class URLFieldTests(TestCase):
|
|||
self.assertEqual(getattr(serializer.fields['url_field'],
|
||||
'max_length'), 20)
|
||||
|
||||
def test_validators_can_be_overridden(self):
|
||||
url_field = serializers.URLField(validators=[])
|
||||
validators = url_field.validators
|
||||
self.assertEqual([], validators, 'Passing `validators` kwarg should have overridden default validators')
|
||||
|
||||
|
||||
class FieldMetadata(TestCase):
|
||||
def setUp(self):
|
||||
|
|
|
@ -7,18 +7,15 @@ from django.test import TestCase
|
|||
from django.utils import unittest
|
||||
from rest_framework import generics, serializers, status, filters
|
||||
from rest_framework.compat import django_filters, patterns, url
|
||||
from rest_framework.settings import api_settings
|
||||
from rest_framework.test import APIRequestFactory
|
||||
from rest_framework.tests.models import BasicModel
|
||||
from .models import FilterableItem
|
||||
from .utils import temporary_setting
|
||||
|
||||
factory = APIRequestFactory()
|
||||
|
||||
|
||||
class FilterableItem(models.Model):
|
||||
text = models.CharField(max_length=100)
|
||||
decimal = models.DecimalField(max_digits=4, decimal_places=2)
|
||||
date = models.DateField()
|
||||
|
||||
|
||||
if django_filters:
|
||||
# Basic filter on a list view.
|
||||
class FilterFieldsRootView(generics.ListCreateAPIView):
|
||||
|
@ -128,7 +125,7 @@ class IntegrationTestFiltering(CommonFilteringTestCase):
|
|||
|
||||
# Tests that the decimal filter works.
|
||||
search_decimal = Decimal('2.25')
|
||||
request = factory.get('/?decimal=%s' % search_decimal)
|
||||
request = factory.get('/', {'decimal': '%s' % search_decimal})
|
||||
response = view(request).render()
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
expected_data = [f for f in self.data if f['decimal'] == search_decimal]
|
||||
|
@ -136,7 +133,7 @@ class IntegrationTestFiltering(CommonFilteringTestCase):
|
|||
|
||||
# Tests that the date filter works.
|
||||
search_date = datetime.date(2012, 9, 22)
|
||||
request = factory.get('/?date=%s' % search_date) # search_date str: '2012-09-22'
|
||||
request = factory.get('/', {'date': '%s' % search_date}) # search_date str: '2012-09-22'
|
||||
response = view(request).render()
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
expected_data = [f for f in self.data if f['date'] == search_date]
|
||||
|
@ -151,7 +148,7 @@ class IntegrationTestFiltering(CommonFilteringTestCase):
|
|||
|
||||
# Tests that the decimal filter works.
|
||||
search_decimal = Decimal('2.25')
|
||||
request = factory.get('/?decimal=%s' % search_decimal)
|
||||
request = factory.get('/', {'decimal': '%s' % search_decimal})
|
||||
response = view(request).render()
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
expected_data = [f for f in self.data if f['decimal'] == search_decimal]
|
||||
|
@ -184,7 +181,7 @@ class IntegrationTestFiltering(CommonFilteringTestCase):
|
|||
|
||||
# Tests that the decimal filter set with 'lt' in the filter class works.
|
||||
search_decimal = Decimal('4.25')
|
||||
request = factory.get('/?decimal=%s' % search_decimal)
|
||||
request = factory.get('/', {'decimal': '%s' % search_decimal})
|
||||
response = view(request).render()
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
expected_data = [f for f in self.data if f['decimal'] < search_decimal]
|
||||
|
@ -192,7 +189,7 @@ class IntegrationTestFiltering(CommonFilteringTestCase):
|
|||
|
||||
# Tests that the date filter set with 'gt' in the filter class works.
|
||||
search_date = datetime.date(2012, 10, 2)
|
||||
request = factory.get('/?date=%s' % search_date) # search_date str: '2012-10-02'
|
||||
request = factory.get('/', {'date': '%s' % search_date}) # search_date str: '2012-10-02'
|
||||
response = view(request).render()
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
expected_data = [f for f in self.data if f['date'] > search_date]
|
||||
|
@ -200,7 +197,7 @@ class IntegrationTestFiltering(CommonFilteringTestCase):
|
|||
|
||||
# Tests that the text filter set with 'icontains' in the filter class works.
|
||||
search_text = 'ff'
|
||||
request = factory.get('/?text=%s' % search_text)
|
||||
request = factory.get('/', {'text': '%s' % search_text})
|
||||
response = view(request).render()
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
expected_data = [f for f in self.data if search_text in f['text'].lower()]
|
||||
|
@ -209,7 +206,10 @@ class IntegrationTestFiltering(CommonFilteringTestCase):
|
|||
# Tests that multiple filters works.
|
||||
search_decimal = Decimal('5.25')
|
||||
search_date = datetime.date(2012, 10, 2)
|
||||
request = factory.get('/?decimal=%s&date=%s' % (search_decimal, search_date))
|
||||
request = factory.get('/', {
|
||||
'decimal': '%s' % (search_decimal,),
|
||||
'date': '%s' % (search_date,)
|
||||
})
|
||||
response = view(request).render()
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
expected_data = [f for f in self.data if f['date'] > search_date and
|
||||
|
@ -234,7 +234,7 @@ class IntegrationTestFiltering(CommonFilteringTestCase):
|
|||
view = FilterFieldsRootView.as_view()
|
||||
|
||||
search_integer = 10
|
||||
request = factory.get('/?integer=%s' % search_integer)
|
||||
request = factory.get('/', {'integer': '%s' % search_integer})
|
||||
response = view(request).render()
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
|
||||
|
@ -265,14 +265,18 @@ class IntegrationTestDetailFiltering(CommonFilteringTestCase):
|
|||
# Tests that the decimal filter set that should fail.
|
||||
search_decimal = Decimal('4.25')
|
||||
high_item = self.objects.filter(decimal__gt=search_decimal)[0]
|
||||
response = self.client.get('{url}?decimal={param}'.format(url=self._get_url(high_item), param=search_decimal))
|
||||
response = self.client.get(
|
||||
'{url}'.format(url=self._get_url(high_item)),
|
||||
{'decimal': '{param}'.format(param=search_decimal)})
|
||||
self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)
|
||||
|
||||
# Tests that the decimal filter set that should succeed.
|
||||
search_decimal = Decimal('4.25')
|
||||
low_item = self.objects.filter(decimal__lt=search_decimal)[0]
|
||||
low_item_data = self._serialize_object(low_item)
|
||||
response = self.client.get('{url}?decimal={param}'.format(url=self._get_url(low_item), param=search_decimal))
|
||||
response = self.client.get(
|
||||
'{url}'.format(url=self._get_url(low_item)),
|
||||
{'decimal': '{param}'.format(param=search_decimal)})
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
self.assertEqual(response.data, low_item_data)
|
||||
|
||||
|
@ -281,7 +285,11 @@ class IntegrationTestDetailFiltering(CommonFilteringTestCase):
|
|||
search_date = datetime.date(2012, 10, 2)
|
||||
valid_item = self.objects.filter(decimal__lt=search_decimal, date__gt=search_date)[0]
|
||||
valid_item_data = self._serialize_object(valid_item)
|
||||
response = self.client.get('{url}?decimal={decimal}&date={date}'.format(url=self._get_url(valid_item), decimal=search_decimal, date=search_date))
|
||||
response = self.client.get(
|
||||
'{url}'.format(url=self._get_url(valid_item)), {
|
||||
'decimal': '{decimal}'.format(decimal=search_decimal),
|
||||
'date': '{date}'.format(date=search_date)
|
||||
})
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
self.assertEqual(response.data, valid_item_data)
|
||||
|
||||
|
@ -315,7 +323,7 @@ class SearchFilterTests(TestCase):
|
|||
search_fields = ('title', 'text')
|
||||
|
||||
view = SearchListView.as_view()
|
||||
request = factory.get('?search=b')
|
||||
request = factory.get('/', {'search': 'b'})
|
||||
response = view(request)
|
||||
self.assertEqual(
|
||||
response.data,
|
||||
|
@ -332,7 +340,7 @@ class SearchFilterTests(TestCase):
|
|||
search_fields = ('=title', 'text')
|
||||
|
||||
view = SearchListView.as_view()
|
||||
request = factory.get('?search=zzz')
|
||||
request = factory.get('/', {'search': 'zzz'})
|
||||
response = view(request)
|
||||
self.assertEqual(
|
||||
response.data,
|
||||
|
@ -348,7 +356,7 @@ class SearchFilterTests(TestCase):
|
|||
search_fields = ('title', '^text')
|
||||
|
||||
view = SearchListView.as_view()
|
||||
request = factory.get('?search=b')
|
||||
request = factory.get('/', {'search': 'b'})
|
||||
response = view(request)
|
||||
self.assertEqual(
|
||||
response.data,
|
||||
|
@ -357,6 +365,24 @@ class SearchFilterTests(TestCase):
|
|||
]
|
||||
)
|
||||
|
||||
def test_search_with_nonstandard_search_param(self):
|
||||
with temporary_setting('SEARCH_PARAM', 'query', module=filters):
|
||||
class SearchListView(generics.ListAPIView):
|
||||
model = SearchFilterModel
|
||||
filter_backends = (filters.SearchFilter,)
|
||||
search_fields = ('title', 'text')
|
||||
|
||||
view = SearchListView.as_view()
|
||||
request = factory.get('/', {'query': 'b'})
|
||||
response = view(request)
|
||||
self.assertEqual(
|
||||
response.data,
|
||||
[
|
||||
{'id': 1, 'title': 'z', 'text': 'abc'},
|
||||
{'id': 2, 'title': 'zz', 'text': 'bcd'}
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
class OrdringFilterModel(models.Model):
|
||||
title = models.CharField(max_length=20)
|
||||
|
@ -396,7 +422,7 @@ class OrderingFilterTests(TestCase):
|
|||
ordering_fields = ('text',)
|
||||
|
||||
view = OrderingListView.as_view()
|
||||
request = factory.get('?ordering=text')
|
||||
request = factory.get('/', {'ordering': 'text'})
|
||||
response = view(request)
|
||||
self.assertEqual(
|
||||
response.data,
|
||||
|
@ -415,7 +441,7 @@ class OrderingFilterTests(TestCase):
|
|||
ordering_fields = ('text',)
|
||||
|
||||
view = OrderingListView.as_view()
|
||||
request = factory.get('?ordering=-text')
|
||||
request = factory.get('/', {'ordering': '-text'})
|
||||
response = view(request)
|
||||
self.assertEqual(
|
||||
response.data,
|
||||
|
@ -434,7 +460,7 @@ class OrderingFilterTests(TestCase):
|
|||
ordering_fields = ('text',)
|
||||
|
||||
view = OrderingListView.as_view()
|
||||
request = factory.get('?ordering=foobar')
|
||||
request = factory.get('/', {'ordering': 'foobar'})
|
||||
response = view(request)
|
||||
self.assertEqual(
|
||||
response.data,
|
||||
|
@ -503,7 +529,7 @@ class OrderingFilterTests(TestCase):
|
|||
models.Count("relateds"))
|
||||
|
||||
view = OrderingListView.as_view()
|
||||
request = factory.get('?ordering=relateds__count')
|
||||
request = factory.get('/', {'ordering': 'relateds__count'})
|
||||
response = view(request)
|
||||
self.assertEqual(
|
||||
response.data,
|
||||
|
@ -514,6 +540,26 @@ class OrderingFilterTests(TestCase):
|
|||
]
|
||||
)
|
||||
|
||||
def test_ordering_with_nonstandard_ordering_param(self):
|
||||
with temporary_setting('ORDERING_PARAM', 'order', filters):
|
||||
class OrderingListView(generics.ListAPIView):
|
||||
model = OrdringFilterModel
|
||||
filter_backends = (filters.OrderingFilter,)
|
||||
ordering = ('title',)
|
||||
ordering_fields = ('text',)
|
||||
|
||||
view = OrderingListView.as_view()
|
||||
request = factory.get('/', {'order': 'text'})
|
||||
response = view(request)
|
||||
self.assertEqual(
|
||||
response.data,
|
||||
[
|
||||
{'id': 1, 'title': 'zyx', 'text': 'abc'},
|
||||
{'id': 2, 'title': 'yxw', 'text': 'bcd'},
|
||||
{'id': 3, 'title': 'xwv', 'text': 'cde'},
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
class SensitiveOrderingFilterModel(models.Model):
|
||||
username = models.CharField(max_length=20)
|
||||
|
@ -566,7 +612,7 @@ class SensitiveOrderingFilterTests(TestCase):
|
|||
serializer_class = serializer_cls
|
||||
|
||||
view = OrderingListView.as_view()
|
||||
request = factory.get('?ordering=-username')
|
||||
request = factory.get('/', {'ordering': '-username'})
|
||||
response = view(request)
|
||||
|
||||
if serializer_cls == SensitiveDataSerializer3:
|
||||
|
@ -596,7 +642,7 @@ class SensitiveOrderingFilterTests(TestCase):
|
|||
serializer_class = serializer_cls
|
||||
|
||||
view = OrderingListView.as_view()
|
||||
request = factory.get('?ordering=password')
|
||||
request = factory.get('/', {'ordering': 'password'})
|
||||
response = view(request)
|
||||
|
||||
if serializer_cls == SensitiveDataSerializer3:
|
||||
|
|
|
@ -4,8 +4,10 @@ from django.contrib.contenttypes.generic import GenericRelation, GenericForeignK
|
|||
from django.db import models
|
||||
from django.test import TestCase
|
||||
from rest_framework import serializers
|
||||
from rest_framework.compat import python_2_unicode_compatible
|
||||
|
||||
|
||||
@python_2_unicode_compatible
|
||||
class Tag(models.Model):
|
||||
"""
|
||||
Tags have a descriptive slug, and are attached to an arbitrary object.
|
||||
|
@ -15,10 +17,11 @@ class Tag(models.Model):
|
|||
object_id = models.PositiveIntegerField()
|
||||
tagged_item = GenericForeignKey('content_type', 'object_id')
|
||||
|
||||
def __unicode__(self):
|
||||
def __str__(self):
|
||||
return self.tag
|
||||
|
||||
|
||||
@python_2_unicode_compatible
|
||||
class Bookmark(models.Model):
|
||||
"""
|
||||
A URL bookmark that may have multiple tags attached.
|
||||
|
@ -26,10 +29,11 @@ class Bookmark(models.Model):
|
|||
url = models.URLField()
|
||||
tags = GenericRelation(Tag)
|
||||
|
||||
def __unicode__(self):
|
||||
def __str__(self):
|
||||
return 'Bookmark: %s' % self.url
|
||||
|
||||
|
||||
@python_2_unicode_compatible
|
||||
class Note(models.Model):
|
||||
"""
|
||||
A textual note that may have multiple tags attached.
|
||||
|
@ -37,7 +41,7 @@ class Note(models.Model):
|
|||
text = models.TextField()
|
||||
tags = GenericRelation(Tag)
|
||||
|
||||
def __unicode__(self):
|
||||
def __str__(self):
|
||||
return 'Note: %s' % self.text
|
||||
|
||||
|
||||
|
@ -127,3 +131,21 @@ class TestGenericRelations(TestCase):
|
|||
}
|
||||
]
|
||||
self.assertEqual(serializer.data, expected)
|
||||
|
||||
def test_restore_object_generic_fk(self):
|
||||
"""
|
||||
Ensure an object with a generic foreign key can be restored.
|
||||
"""
|
||||
|
||||
class TagSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = Tag
|
||||
exclude = ('content_type', 'object_id')
|
||||
|
||||
serializer = TagSerializer()
|
||||
|
||||
bookmark = Bookmark(url='http://example.com')
|
||||
attrs = {'tagged_item': bookmark, 'tag': 'example'}
|
||||
|
||||
tag = serializer.restore_object(attrs)
|
||||
self.assertEqual(tag.tagged_item, bookmark)
|
||||
|
|
|
@ -5,6 +5,7 @@ from django.test import TestCase
|
|||
from rest_framework import generics, renderers, serializers, status
|
||||
from rest_framework.test import APIRequestFactory
|
||||
from rest_framework.tests.models import BasicModel, Comment, SlugBasedModel
|
||||
from rest_framework.tests.models import ForeignKeySource, ForeignKeyTarget
|
||||
from rest_framework.compat import six
|
||||
|
||||
factory = APIRequestFactory()
|
||||
|
@ -28,6 +29,13 @@ class InstanceView(generics.RetrieveUpdateDestroyAPIView):
|
|||
return queryset.exclude(text='filtered out')
|
||||
|
||||
|
||||
class FKInstanceView(generics.RetrieveUpdateDestroyAPIView):
|
||||
"""
|
||||
FK: example description for OPTIONS.
|
||||
"""
|
||||
model = ForeignKeySource
|
||||
|
||||
|
||||
class SlugSerializer(serializers.ModelSerializer):
|
||||
slug = serializers.Field() # read only
|
||||
|
||||
|
@ -407,6 +415,72 @@ class TestInstanceView(TestCase):
|
|||
self.assertFalse(self.objects.filter(id=999).exists())
|
||||
|
||||
|
||||
class TestFKInstanceView(TestCase):
|
||||
def setUp(self):
|
||||
"""
|
||||
Create 3 BasicModel instances.
|
||||
"""
|
||||
items = ['foo', 'bar', 'baz']
|
||||
for item in items:
|
||||
t = ForeignKeyTarget(name=item)
|
||||
t.save()
|
||||
ForeignKeySource(name='source_' + item, target=t).save()
|
||||
|
||||
self.objects = ForeignKeySource.objects
|
||||
self.data = [
|
||||
{'id': obj.id, 'name': obj.name}
|
||||
for obj in self.objects.all()
|
||||
]
|
||||
self.view = FKInstanceView.as_view()
|
||||
|
||||
def test_options_root_view(self):
|
||||
"""
|
||||
OPTIONS requests to ListCreateAPIView should return metadata
|
||||
"""
|
||||
request = factory.options('/999')
|
||||
with self.assertNumQueries(1):
|
||||
response = self.view(request, pk=999).render()
|
||||
expected = {
|
||||
'name': 'Fk Instance',
|
||||
'description': 'FK: example description for OPTIONS.',
|
||||
'renders': [
|
||||
'application/json',
|
||||
'text/html'
|
||||
],
|
||||
'parses': [
|
||||
'application/json',
|
||||
'application/x-www-form-urlencoded',
|
||||
'multipart/form-data'
|
||||
],
|
||||
'actions': {
|
||||
'PUT': {
|
||||
'id': {
|
||||
'type': 'integer',
|
||||
'required': False,
|
||||
'read_only': True,
|
||||
'label': 'ID'
|
||||
},
|
||||
'name': {
|
||||
'type': 'string',
|
||||
'required': True,
|
||||
'read_only': False,
|
||||
'label': 'name',
|
||||
'max_length': 100
|
||||
},
|
||||
'target': {
|
||||
'type': 'field',
|
||||
'required': True,
|
||||
'read_only': False,
|
||||
'label': 'Target',
|
||||
'help_text': 'Target'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
self.assertEqual(response.data, expected)
|
||||
|
||||
|
||||
class TestOverriddenGetObject(TestCase):
|
||||
"""
|
||||
Test cases for a RetrieveUpdateDestroyAPIView that does NOT use the
|
||||
|
|
|
@ -50,7 +50,7 @@ class TemplateHTMLRendererTests(TestCase):
|
|||
"""
|
||||
self.get_template = django.template.loader.get_template
|
||||
|
||||
def get_template(template_name):
|
||||
def get_template(template_name, dirs=None):
|
||||
if template_name == 'example.html':
|
||||
return Template("example: {{ object }}")
|
||||
raise TemplateDoesNotExist(template_name)
|
||||
|
@ -108,11 +108,13 @@ class TemplateHTMLRendererExceptionTests(TestCase):
|
|||
def test_not_found_html_view_with_template(self):
|
||||
response = self.client.get('/not_found')
|
||||
self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)
|
||||
self.assertEqual(response.content, six.b("404: Not found"))
|
||||
self.assertTrue(response.content in (
|
||||
six.b("404: Not found"), six.b("404 Not Found")))
|
||||
self.assertEqual(response['Content-Type'], 'text/html; charset=utf-8')
|
||||
|
||||
def test_permission_denied_html_view_with_template(self):
|
||||
response = self.client.get('/permission_denied')
|
||||
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
|
||||
self.assertEqual(response.content, six.b("403: Permission denied"))
|
||||
self.assertTrue(response.content in (
|
||||
six.b("403: Permission denied"), six.b("403 Forbidden")))
|
||||
self.assertEqual(response['Content-Type'], 'text/html; charset=utf-8')
|
||||
|
|
30
rest_framework/tests/test_nullable_fields.py
Normal file
30
rest_framework/tests/test_nullable_fields.py
Normal file
|
@ -0,0 +1,30 @@
|
|||
from django.core.urlresolvers import reverse
|
||||
|
||||
from rest_framework.compat import patterns, url
|
||||
from rest_framework.test import APITestCase
|
||||
from rest_framework.tests.models import NullableForeignKeySource
|
||||
from rest_framework.tests.serializers import NullableFKSourceSerializer
|
||||
from rest_framework.tests.views import NullableFKSourceDetail
|
||||
|
||||
|
||||
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 = 'rest_framework.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)
|
|
@ -9,14 +9,18 @@ from rest_framework import generics, status, pagination, filters, serializers
|
|||
from rest_framework.compat import django_filters
|
||||
from rest_framework.test import APIRequestFactory
|
||||
from rest_framework.tests.models import BasicModel
|
||||
from .models import FilterableItem
|
||||
|
||||
factory = APIRequestFactory()
|
||||
|
||||
# Helper function to split arguments out of an url
|
||||
def split_arguments_from_url(url):
|
||||
if '?' not in url:
|
||||
return url
|
||||
|
||||
class FilterableItem(models.Model):
|
||||
text = models.CharField(max_length=100)
|
||||
decimal = models.DecimalField(max_digits=4, decimal_places=2)
|
||||
date = models.DateField()
|
||||
path, args = url.split('?')
|
||||
args = dict(r.split('=') for r in args.split('&'))
|
||||
return path, args
|
||||
|
||||
|
||||
class RootView(generics.ListCreateAPIView):
|
||||
|
@ -84,7 +88,7 @@ class IntegrationTestPagination(TestCase):
|
|||
self.assertNotEqual(response.data['next'], None)
|
||||
self.assertEqual(response.data['previous'], None)
|
||||
|
||||
request = factory.get(response.data['next'])
|
||||
request = factory.get(*split_arguments_from_url(response.data['next']))
|
||||
with self.assertNumQueries(2):
|
||||
response = self.view(request).render()
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
|
@ -93,7 +97,7 @@ class IntegrationTestPagination(TestCase):
|
|||
self.assertNotEqual(response.data['next'], None)
|
||||
self.assertNotEqual(response.data['previous'], None)
|
||||
|
||||
request = factory.get(response.data['next'])
|
||||
request = factory.get(*split_arguments_from_url(response.data['next']))
|
||||
with self.assertNumQueries(2):
|
||||
response = self.view(request).render()
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
|
@ -146,7 +150,7 @@ class IntegrationTestPaginationAndFiltering(TestCase):
|
|||
|
||||
EXPECTED_NUM_QUERIES = 2
|
||||
|
||||
request = factory.get('/?decimal=15.20')
|
||||
request = factory.get('/', {'decimal': '15.20'})
|
||||
with self.assertNumQueries(EXPECTED_NUM_QUERIES):
|
||||
response = view(request).render()
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
|
@ -155,7 +159,7 @@ class IntegrationTestPaginationAndFiltering(TestCase):
|
|||
self.assertNotEqual(response.data['next'], None)
|
||||
self.assertEqual(response.data['previous'], None)
|
||||
|
||||
request = factory.get(response.data['next'])
|
||||
request = factory.get(*split_arguments_from_url(response.data['next']))
|
||||
with self.assertNumQueries(EXPECTED_NUM_QUERIES):
|
||||
response = view(request).render()
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
|
@ -164,7 +168,7 @@ class IntegrationTestPaginationAndFiltering(TestCase):
|
|||
self.assertEqual(response.data['next'], None)
|
||||
self.assertNotEqual(response.data['previous'], None)
|
||||
|
||||
request = factory.get(response.data['previous'])
|
||||
request = factory.get(*split_arguments_from_url(response.data['previous']))
|
||||
with self.assertNumQueries(EXPECTED_NUM_QUERIES):
|
||||
response = view(request).render()
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
|
@ -191,7 +195,7 @@ class IntegrationTestPaginationAndFiltering(TestCase):
|
|||
|
||||
view = BasicFilterFieldsRootView.as_view()
|
||||
|
||||
request = factory.get('/?decimal=15.20')
|
||||
request = factory.get('/', {'decimal': '15.20'})
|
||||
with self.assertNumQueries(2):
|
||||
response = view(request).render()
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
|
@ -200,7 +204,7 @@ class IntegrationTestPaginationAndFiltering(TestCase):
|
|||
self.assertNotEqual(response.data['next'], None)
|
||||
self.assertEqual(response.data['previous'], None)
|
||||
|
||||
request = factory.get(response.data['next'])
|
||||
request = factory.get(*split_arguments_from_url(response.data['next']))
|
||||
with self.assertNumQueries(2):
|
||||
response = view(request).render()
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
|
@ -209,7 +213,7 @@ class IntegrationTestPaginationAndFiltering(TestCase):
|
|||
self.assertEqual(response.data['next'], None)
|
||||
self.assertNotEqual(response.data['previous'], None)
|
||||
|
||||
request = factory.get(response.data['previous'])
|
||||
request = factory.get(*split_arguments_from_url(response.data['previous']))
|
||||
with self.assertNumQueries(2):
|
||||
response = view(request).render()
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
|
@ -317,7 +321,7 @@ class TestCustomPaginateByParam(TestCase):
|
|||
"""
|
||||
If paginate_by_param is set, the new kwarg should limit per view requests.
|
||||
"""
|
||||
request = factory.get('/?page_size=5')
|
||||
request = factory.get('/', {'page_size': 5})
|
||||
response = self.view(request).render()
|
||||
self.assertEqual(response.data['count'], 13)
|
||||
self.assertEqual(response.data['results'], self.data[:5])
|
||||
|
@ -345,7 +349,7 @@ class TestMaxPaginateByParam(TestCase):
|
|||
"""
|
||||
If max_paginate_by is set, it should limit page size for the view.
|
||||
"""
|
||||
request = factory.get('/?page_size=10')
|
||||
request = factory.get('/', data={'page_size': 10})
|
||||
response = self.view(request).render()
|
||||
self.assertEqual(response.data['count'], 13)
|
||||
self.assertEqual(response.data['results'], self.data[:5])
|
||||
|
|
|
@ -96,7 +96,7 @@ class TestFileUploadParser(TestCase):
|
|||
request = MockRequest()
|
||||
request.upload_handlers = (MemoryFileUploadHandler(),)
|
||||
request.META = {
|
||||
'HTTP_CONTENT_DISPOSITION': 'Content-Disposition: inline; filename=file.txt'.encode('utf-8'),
|
||||
'HTTP_CONTENT_DISPOSITION': 'Content-Disposition: inline; filename=file.txt',
|
||||
'HTTP_CONTENT_LENGTH': 14,
|
||||
}
|
||||
self.parser_context = {'request': request, 'kwargs': {}}
|
||||
|
@ -112,4 +112,4 @@ class TestFileUploadParser(TestCase):
|
|||
def test_get_filename(self):
|
||||
parser = FileUploadParser()
|
||||
filename = parser.get_filename(self.stream, None, self.parser_context)
|
||||
self.assertEqual(filename, 'file.txt'.encode('utf-8'))
|
||||
self.assertEqual(filename, 'file.txt')
|
||||
|
|
|
@ -2,8 +2,10 @@
|
|||
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 rest_framework.tests.models import BlogPost
|
||||
|
||||
|
@ -118,3 +120,25 @@ class RelatedFieldSourceTests(TestCase):
|
|||
(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')
|
||||
|
||||
|
|
|
@ -3,9 +3,7 @@ from django.db import models
|
|||
from django.test import TestCase
|
||||
from rest_framework import serializers
|
||||
|
||||
|
||||
class OneToOneTarget(models.Model):
|
||||
name = models.CharField(max_length=100)
|
||||
from .models import OneToOneTarget
|
||||
|
||||
|
||||
class OneToOneSource(models.Model):
|
||||
|
|
|
@ -12,7 +12,7 @@ from rest_framework.compat import yaml, etree, patterns, url, include, six, Stri
|
|||
from rest_framework.response import Response
|
||||
from rest_framework.views import APIView
|
||||
from rest_framework.renderers import BaseRenderer, JSONRenderer, YAMLRenderer, \
|
||||
XMLRenderer, JSONPRenderer, BrowsableAPIRenderer, UnicodeJSONRenderer
|
||||
XMLRenderer, JSONPRenderer, BrowsableAPIRenderer, UnicodeJSONRenderer, UnicodeYAMLRenderer
|
||||
from rest_framework.parsers import YAMLParser, XMLParser
|
||||
from rest_framework.settings import api_settings
|
||||
from rest_framework.test import APIRequestFactory
|
||||
|
@ -256,6 +256,18 @@ class RendererEndToEndTests(TestCase):
|
|||
self.assertEqual(resp.get('Content-Type', None), None)
|
||||
self.assertEqual(resp.status_code, status.HTTP_204_NO_CONTENT)
|
||||
|
||||
def test_contains_headers_of_api_response(self):
|
||||
"""
|
||||
Issue #1437
|
||||
|
||||
Test we display the headers of the API response and not those from the
|
||||
HTML response
|
||||
"""
|
||||
resp = self.client.get('/html1')
|
||||
self.assertContains(resp, '>GET, HEAD, OPTIONS<')
|
||||
self.assertContains(resp, '>application/json<')
|
||||
self.assertNotContains(resp, '>text/html; charset=utf-8<')
|
||||
|
||||
|
||||
_flat_repr = '{"foo": ["bar", "baz"]}'
|
||||
_indented_repr = '{\n "foo": [\n "bar",\n "baz"\n ]\n}'
|
||||
|
@ -455,6 +467,17 @@ if yaml:
|
|||
self.assertTrue(string in content, '%r not in %r' % (string, content))
|
||||
|
||||
|
||||
class UnicodeYAMLRendererTests(TestCase):
|
||||
"""
|
||||
Tests specific for the Unicode YAML Renderer
|
||||
"""
|
||||
def test_proper_encoding(self):
|
||||
obj = {'countries': ['United Kingdom', 'France', 'España']}
|
||||
renderer = UnicodeYAMLRenderer()
|
||||
content = renderer.render(obj, 'application/yaml')
|
||||
self.assertEqual(content.strip(), 'countries: [United Kingdom, France, España]'.encode('utf-8'))
|
||||
|
||||
|
||||
class XMLRendererTestCase(TestCase):
|
||||
"""
|
||||
Tests specific to the XML Renderer
|
||||
|
@ -601,6 +624,10 @@ class CacheRenderTest(TestCase):
|
|||
method = getattr(self.client, http_method)
|
||||
resp = method(url)
|
||||
del resp.client, resp.request
|
||||
try:
|
||||
del resp.wsgi_request
|
||||
except AttributeError:
|
||||
pass
|
||||
return resp
|
||||
|
||||
def test_obj_pickling(self):
|
||||
|
|
|
@ -3,15 +3,42 @@ from __future__ import unicode_literals
|
|||
from django.db import models
|
||||
from django.db.models.fields import BLANK_CHOICE_DASH
|
||||
from django.test import TestCase
|
||||
from django.utils import unittest
|
||||
from django.utils.datastructures import MultiValueDict
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from rest_framework import serializers, fields, relations
|
||||
from rest_framework.tests.models import (HasPositiveIntegerAsChoice, Album, ActionItem, Anchor, BasicModel,
|
||||
BlankFieldModel, BlogPost, BlogPostComment, Book, CallableDefaultValueModel, DefaultValueModel,
|
||||
ManyToManyModel, Person, ReadOnlyManyToManyModel, Photo, RESTFrameworkModel)
|
||||
ManyToManyModel, Person, ReadOnlyManyToManyModel, Photo, RESTFrameworkModel,
|
||||
ForeignKeySource, ManyToManySource)
|
||||
from rest_framework.tests.models import BasicModelSerializer
|
||||
import datetime
|
||||
import pickle
|
||||
try:
|
||||
import PIL
|
||||
except:
|
||||
PIL = None
|
||||
|
||||
|
||||
if PIL is not None:
|
||||
class AMOAFModel(RESTFrameworkModel):
|
||||
char_field = models.CharField(max_length=1024, blank=True)
|
||||
comma_separated_integer_field = models.CommaSeparatedIntegerField(max_length=1024, blank=True)
|
||||
decimal_field = models.DecimalField(max_digits=64, decimal_places=32, blank=True)
|
||||
email_field = models.EmailField(max_length=1024, blank=True)
|
||||
file_field = models.FileField(upload_to='test', max_length=1024, blank=True)
|
||||
image_field = models.ImageField(upload_to='test', max_length=1024, blank=True)
|
||||
slug_field = models.SlugField(max_length=1024, blank=True)
|
||||
url_field = models.URLField(max_length=1024, blank=True)
|
||||
|
||||
class DVOAFModel(RESTFrameworkModel):
|
||||
positive_integer_field = models.PositiveIntegerField(blank=True)
|
||||
positive_small_integer_field = models.PositiveSmallIntegerField(blank=True)
|
||||
email_field = models.EmailField(blank=True)
|
||||
file_field = models.FileField(upload_to='test', blank=True)
|
||||
image_field = models.ImageField(upload_to='test', blank=True)
|
||||
slug_field = models.SlugField(blank=True)
|
||||
url_field = models.URLField(blank=True)
|
||||
|
||||
|
||||
class SubComment(object):
|
||||
|
@ -71,6 +98,15 @@ class ActionItemSerializer(serializers.ModelSerializer):
|
|||
class Meta:
|
||||
model = ActionItem
|
||||
|
||||
class ActionItemSerializerOptionalFields(serializers.ModelSerializer):
|
||||
"""
|
||||
Intended to test that fields with `required=False` are excluded from validation.
|
||||
"""
|
||||
title = serializers.CharField(required=False)
|
||||
|
||||
class Meta:
|
||||
model = ActionItem
|
||||
fields = ('title',)
|
||||
|
||||
class ActionItemSerializerCustomRestore(serializers.ModelSerializer):
|
||||
|
||||
|
@ -132,7 +168,7 @@ class AlbumsSerializer(serializers.ModelSerializer):
|
|||
|
||||
class Meta:
|
||||
model = Album
|
||||
fields = ['title'] # lists are also valid options
|
||||
fields = ['title', 'ref'] # lists are also valid options
|
||||
|
||||
|
||||
class PositiveIntegerAsChoiceSerializer(serializers.ModelSerializer):
|
||||
|
@ -141,6 +177,16 @@ class PositiveIntegerAsChoiceSerializer(serializers.ModelSerializer):
|
|||
fields = ['some_integer']
|
||||
|
||||
|
||||
class ForeignKeySourceSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = ForeignKeySource
|
||||
|
||||
|
||||
class HyperlinkedForeignKeySourceSerializer(serializers.HyperlinkedModelSerializer):
|
||||
class Meta:
|
||||
model = ForeignKeySource
|
||||
|
||||
|
||||
class BasicTests(TestCase):
|
||||
def setUp(self):
|
||||
self.comment = Comment(
|
||||
|
@ -288,7 +334,13 @@ class BasicTests(TestCase):
|
|||
serializer.save()
|
||||
self.assertIsNotNone(serializer.data.get('id',None), 'Model is saved. `id` should be set.')
|
||||
|
||||
|
||||
def test_fields_marked_as_not_required_are_excluded_from_validation(self):
|
||||
"""
|
||||
Check that fields with `required=False` are included in list of exclusions.
|
||||
"""
|
||||
serializer = ActionItemSerializerOptionalFields(self.actionitem)
|
||||
exclusions = serializer.get_validation_exclusions()
|
||||
self.assertTrue('title' in exclusions, '`title` field was marked `required=False` and should be excluded')
|
||||
|
||||
|
||||
class DictStyleSerializer(serializers.Serializer):
|
||||
|
@ -467,6 +519,32 @@ class ValidationTests(TestCase):
|
|||
)
|
||||
self.assertEqual(serializer.is_valid(), True)
|
||||
|
||||
def test_writable_star_source_on_nested_serializer_with_parent_object(self):
|
||||
class TitleSerializer(serializers.Serializer):
|
||||
title = serializers.WritableField(source='title')
|
||||
|
||||
class AlbumSerializer(serializers.ModelSerializer):
|
||||
nested = TitleSerializer(source='*')
|
||||
|
||||
class Meta:
|
||||
model = Album
|
||||
fields = ('nested',)
|
||||
|
||||
class PhotoSerializer(serializers.ModelSerializer):
|
||||
album = AlbumSerializer(source='album')
|
||||
|
||||
class Meta:
|
||||
model = Photo
|
||||
fields = ('album', )
|
||||
|
||||
photo = Photo(album=Album())
|
||||
|
||||
data = {'album': {'nested': {'title': 'test'}}}
|
||||
|
||||
serializer = PhotoSerializer(photo, data=data)
|
||||
self.assertEqual(serializer.is_valid(), True)
|
||||
self.assertEqual(serializer.data, data)
|
||||
|
||||
def test_writable_star_source_with_inner_source_fields(self):
|
||||
"""
|
||||
Tests that a serializer with source="*" correctly expands the
|
||||
|
@ -576,12 +654,15 @@ class ModelValidationTests(TestCase):
|
|||
"""
|
||||
Just check if serializers.ModelSerializer handles unique checks via .full_clean()
|
||||
"""
|
||||
serializer = AlbumsSerializer(data={'title': 'a'})
|
||||
serializer = AlbumsSerializer(data={'title': 'a', 'ref': '1'})
|
||||
serializer.is_valid()
|
||||
serializer.save()
|
||||
second_serializer = AlbumsSerializer(data={'title': 'a'})
|
||||
self.assertFalse(second_serializer.is_valid())
|
||||
self.assertEqual(second_serializer.errors, {'title': ['Album with this Title already exists.']})
|
||||
self.assertEqual(second_serializer.errors, {'title': ['Album with this Title already exists.'],})
|
||||
third_serializer = AlbumsSerializer(data=[{'title': 'b', 'ref': '1'}, {'title': 'c'}])
|
||||
self.assertFalse(third_serializer.is_valid())
|
||||
self.assertEqual(third_serializer.errors, [{'ref': ['Album with this Ref already exists.']}, {}])
|
||||
|
||||
def test_foreign_key_is_null_with_partial(self):
|
||||
"""
|
||||
|
@ -865,6 +946,58 @@ class DefaultValueTests(TestCase):
|
|||
self.assertEqual(instance.text, 'overridden')
|
||||
|
||||
|
||||
class WritableFieldDefaultValueTests(TestCase):
|
||||
|
||||
def setUp(self):
|
||||
self.expected = {'default': 'value'}
|
||||
self.create_field = fields.WritableField
|
||||
|
||||
def test_get_default_value_with_noncallable(self):
|
||||
field = self.create_field(default=self.expected)
|
||||
got = field.get_default_value()
|
||||
self.assertEqual(got, self.expected)
|
||||
|
||||
def test_get_default_value_with_callable(self):
|
||||
field = self.create_field(default=lambda : self.expected)
|
||||
got = field.get_default_value()
|
||||
self.assertEqual(got, self.expected)
|
||||
|
||||
def test_get_default_value_when_not_required(self):
|
||||
field = self.create_field(default=self.expected, required=False)
|
||||
got = field.get_default_value()
|
||||
self.assertEqual(got, self.expected)
|
||||
|
||||
def test_get_default_value_returns_None(self):
|
||||
field = self.create_field()
|
||||
got = field.get_default_value()
|
||||
self.assertIsNone(got)
|
||||
|
||||
def test_get_default_value_returns_non_True_values(self):
|
||||
values = [None, '', False, 0, [], (), {}] # values that assumed as 'False' in the 'if' clause
|
||||
for expected in values:
|
||||
field = self.create_field(default=expected)
|
||||
got = field.get_default_value()
|
||||
self.assertEqual(got, expected)
|
||||
|
||||
|
||||
class RelatedFieldDefaultValueTests(WritableFieldDefaultValueTests):
|
||||
|
||||
def setUp(self):
|
||||
self.expected = {'foo': 'bar'}
|
||||
self.create_field = relations.RelatedField
|
||||
|
||||
def test_get_default_value_returns_empty_list(self):
|
||||
field = self.create_field(many=True)
|
||||
got = field.get_default_value()
|
||||
self.assertListEqual(got, [])
|
||||
|
||||
def test_get_default_value_returns_expected(self):
|
||||
expected = [1, 2, 3]
|
||||
field = self.create_field(many=True, default=expected)
|
||||
got = field.get_default_value()
|
||||
self.assertListEqual(got, expected)
|
||||
|
||||
|
||||
class CallableDefaultValueTests(TestCase):
|
||||
def setUp(self):
|
||||
class CallableDefaultValueSerializer(serializers.ModelSerializer):
|
||||
|
@ -1478,18 +1611,23 @@ class ManyFieldHelpTextTest(TestCase):
|
|||
self.assertEqual('Some help text.', rel_field.help_text)
|
||||
|
||||
|
||||
class AttributeMappingOnAutogeneratedRelatedFields(TestCase):
|
||||
|
||||
def test_primary_key_related_field(self):
|
||||
serializer = ForeignKeySourceSerializer()
|
||||
self.assertEqual(serializer.fields['target'].help_text, 'Target')
|
||||
self.assertEqual(serializer.fields['target'].label, 'Target')
|
||||
|
||||
def test_hyperlinked_related_field(self):
|
||||
serializer = HyperlinkedForeignKeySourceSerializer()
|
||||
self.assertEqual(serializer.fields['target'].help_text, 'Target')
|
||||
self.assertEqual(serializer.fields['target'].label, 'Target')
|
||||
|
||||
|
||||
@unittest.skipUnless(PIL is not None, 'PIL is not installed')
|
||||
class AttributeMappingOnAutogeneratedFieldsTests(TestCase):
|
||||
|
||||
def setUp(self):
|
||||
class AMOAFModel(RESTFrameworkModel):
|
||||
char_field = models.CharField(max_length=1024, blank=True)
|
||||
comma_separated_integer_field = models.CommaSeparatedIntegerField(max_length=1024, blank=True)
|
||||
decimal_field = models.DecimalField(max_digits=64, decimal_places=32, blank=True)
|
||||
email_field = models.EmailField(max_length=1024, blank=True)
|
||||
file_field = models.FileField(max_length=1024, blank=True)
|
||||
image_field = models.ImageField(max_length=1024, blank=True)
|
||||
slug_field = models.SlugField(max_length=1024, blank=True)
|
||||
url_field = models.URLField(max_length=1024, blank=True)
|
||||
|
||||
class AMOAFSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
|
@ -1559,17 +1697,10 @@ class AttributeMappingOnAutogeneratedFieldsTests(TestCase):
|
|||
self.field_test('url_field')
|
||||
|
||||
|
||||
@unittest.skipUnless(PIL is not None, 'PIL is not installed')
|
||||
class DefaultValuesOnAutogeneratedFieldsTests(TestCase):
|
||||
|
||||
def setUp(self):
|
||||
class DVOAFModel(RESTFrameworkModel):
|
||||
positive_integer_field = models.PositiveIntegerField(blank=True)
|
||||
positive_small_integer_field = models.PositiveSmallIntegerField(blank=True)
|
||||
email_field = models.EmailField(blank=True)
|
||||
file_field = models.FileField(blank=True)
|
||||
image_field = models.ImageField(blank=True)
|
||||
slug_field = models.SlugField(blank=True)
|
||||
url_field = models.URLField(blank=True)
|
||||
|
||||
class DVOAFSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
from __future__ import unicode_literals
|
||||
from django.test import TestCase
|
||||
from rest_framework.test import APIRequestFactory
|
||||
from rest_framework.templatetags.rest_framework import add_query_param
|
||||
from rest_framework.templatetags.rest_framework import add_query_param, urlize_quoted_links
|
||||
|
||||
factory = APIRequestFactory()
|
||||
|
||||
|
@ -17,3 +17,35 @@ class TemplateTagTests(TestCase):
|
|||
json_url = add_query_param(request, "format", "json")
|
||||
self.assertIn("q=%E6%9F%A5%E8%AF%A2", json_url)
|
||||
self.assertIn("format=json", json_url)
|
||||
|
||||
|
||||
class Issue1386Tests(TestCase):
|
||||
"""
|
||||
Covers #1386
|
||||
"""
|
||||
|
||||
def test_issue_1386(self):
|
||||
"""
|
||||
Test function urlize_quoted_links with different args
|
||||
"""
|
||||
correct_urls = [
|
||||
"asdf.com",
|
||||
"asdf.net",
|
||||
"www.as_df.org",
|
||||
"as.d8f.ghj8.gov",
|
||||
]
|
||||
for i in correct_urls:
|
||||
res = urlize_quoted_links(i)
|
||||
self.assertNotEqual(res, i)
|
||||
self.assertIn(i, res)
|
||||
|
||||
incorrect_urls = [
|
||||
"mailto://asdf@fdf.com",
|
||||
"asdf.netnet",
|
||||
]
|
||||
for i in incorrect_urls:
|
||||
res = urlize_quoted_links(i)
|
||||
self.assertEqual(i, res)
|
||||
|
||||
# example from issue #1386, this shouldn't raise an exception
|
||||
_ = urlize_quoted_links("asdf:[/p]zxcv.com")
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
# -- coding: utf-8 --
|
||||
|
||||
from __future__ import unicode_literals
|
||||
from io import BytesIO
|
||||
|
||||
from django.contrib.auth.models import User
|
||||
from django.test import TestCase
|
||||
from rest_framework.compat import patterns, url
|
||||
|
@ -143,3 +145,20 @@ class TestAPIRequestFactory(TestCase):
|
|||
force_authenticate(request, user=user)
|
||||
response = view(request)
|
||||
self.assertEqual(response.data['user'], 'example')
|
||||
|
||||
def test_upload_file(self):
|
||||
# This is a 1x1 black png
|
||||
simple_png = BytesIO(b'\x89PNG\r\n\x1a\n\x00\x00\x00\rIHDR\x00\x00\x00\x01\x00\x00\x00\x01\x08\x06\x00\x00\x00\x1f\x15\xc4\x89\x00\x00\x00\rIDATx\x9cc````\x00\x00\x00\x05\x00\x01\xa5\xf6E@\x00\x00\x00\x00IEND\xaeB`\x82')
|
||||
simple_png.name = 'test.png'
|
||||
factory = APIRequestFactory()
|
||||
factory.post('/', data={'image': simple_png})
|
||||
|
||||
def test_request_factory_url_arguments(self):
|
||||
"""
|
||||
This is a non regression test against #1461
|
||||
"""
|
||||
factory = APIRequestFactory()
|
||||
request = factory.get('/view/?demo=test')
|
||||
self.assertEqual(dict(request.GET), {'demo': ['test']})
|
||||
request = factory.get('/view/', {'demo': 'test'})
|
||||
self.assertEqual(dict(request.GET), {'demo': ['test']})
|
||||
|
|
38
rest_framework/tests/test_urlizer.py
Normal file
38
rest_framework/tests/test_urlizer.py
Normal file
|
@ -0,0 +1,38 @@
|
|||
from __future__ import unicode_literals
|
||||
from django.test import TestCase
|
||||
from rest_framework.templatetags.rest_framework import urlize_quoted_links
|
||||
import sys
|
||||
|
||||
|
||||
class URLizerTests(TestCase):
|
||||
"""
|
||||
Test if both JSON and YAML URLs are transformed into links well
|
||||
"""
|
||||
def _urlize_dict_check(self, data):
|
||||
"""
|
||||
For all items in dict test assert that the value is urlized key
|
||||
"""
|
||||
for original, urlized in data.items():
|
||||
assert urlize_quoted_links(original, nofollow=False) == urlized
|
||||
|
||||
def test_json_with_url(self):
|
||||
"""
|
||||
Test if JSON URLs are transformed into links well
|
||||
"""
|
||||
data = {}
|
||||
data['"url": "http://api/users/1/", '] = \
|
||||
'"url": "<a href="http://api/users/1/">http://api/users/1/</a>", '
|
||||
data['"foo_set": [\n "http://api/foos/1/"\n], '] = \
|
||||
'"foo_set": [\n "<a href="http://api/foos/1/">http://api/foos/1/</a>"\n], '
|
||||
self._urlize_dict_check(data)
|
||||
|
||||
def test_yaml_with_url(self):
|
||||
"""
|
||||
Test if YAML URLs are transformed into links well
|
||||
"""
|
||||
data = {}
|
||||
data['''{users: 'http://api/users/'}'''] = \
|
||||
'''{users: '<a href="http://api/users/">http://api/users/</a>'}'''
|
||||
data['''foo_set: ['http://api/foos/1/']'''] = \
|
||||
'''foo_set: ['<a href="http://api/foos/1/">http://api/foos/1/</a>']'''
|
||||
self._urlize_dict_check(data)
|
|
@ -1,4 +1,5 @@
|
|||
from __future__ import unicode_literals
|
||||
from django.core.validators import MaxValueValidator
|
||||
from django.db import models
|
||||
from django.test import TestCase
|
||||
from rest_framework import generics, serializers, status
|
||||
|
@ -102,3 +103,46 @@ class TestAvoidValidation(TestCase):
|
|||
self.assertFalse(serializer.is_valid())
|
||||
self.assertDictEqual(serializer.errors,
|
||||
{'non_field_errors': ['Invalid data']})
|
||||
|
||||
|
||||
# regression tests for issue: 1493
|
||||
|
||||
class ValidationMaxValueValidatorModel(models.Model):
|
||||
number_value = models.PositiveIntegerField(validators=[MaxValueValidator(100)])
|
||||
|
||||
|
||||
class ValidationMaxValueValidatorModelSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = ValidationMaxValueValidatorModel
|
||||
|
||||
|
||||
class UpdateMaxValueValidationModel(generics.RetrieveUpdateDestroyAPIView):
|
||||
model = ValidationMaxValueValidatorModel
|
||||
serializer_class = ValidationMaxValueValidatorModelSerializer
|
||||
|
||||
|
||||
class TestMaxValueValidatorValidation(TestCase):
|
||||
|
||||
def test_max_value_validation_serializer_success(self):
|
||||
serializer = ValidationMaxValueValidatorModelSerializer(data={'number_value': 99})
|
||||
self.assertTrue(serializer.is_valid())
|
||||
|
||||
def test_max_value_validation_serializer_fails(self):
|
||||
serializer = ValidationMaxValueValidatorModelSerializer(data={'number_value': 101})
|
||||
self.assertFalse(serializer.is_valid())
|
||||
self.assertDictEqual({'number_value': ['Ensure this value is less than or equal to 100.']}, serializer.errors)
|
||||
|
||||
def test_max_value_validation_success(self):
|
||||
obj = ValidationMaxValueValidatorModel.objects.create(number_value=100)
|
||||
request = factory.patch('/{0}'.format(obj.pk), {'number_value': 98}, format='json')
|
||||
view = UpdateMaxValueValidationModel().as_view()
|
||||
response = view(request, pk=obj.pk).render()
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
|
||||
def test_max_value_validation_fail(self):
|
||||
obj = ValidationMaxValueValidatorModel.objects.create(number_value=100)
|
||||
request = factory.patch('/{0}'.format(obj.pk), {'number_value': 101}, format='json')
|
||||
view = UpdateMaxValueValidationModel().as_view()
|
||||
response = view(request, pk=obj.pk).render()
|
||||
self.assertEqual(response.content, b'{"number_value": ["Ensure this value is less than or equal to 100."]}')
|
||||
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
from __future__ import unicode_literals
|
||||
|
||||
import sys
|
||||
import copy
|
||||
from django.test import TestCase
|
||||
from rest_framework import status
|
||||
|
@ -11,6 +12,11 @@ from rest_framework.views import APIView
|
|||
|
||||
factory = APIRequestFactory()
|
||||
|
||||
if sys.version_info[:2] >= (3, 4):
|
||||
JSON_ERROR = 'JSON parse error - Expecting value:'
|
||||
else:
|
||||
JSON_ERROR = 'JSON parse error - No JSON object could be decoded'
|
||||
|
||||
|
||||
class BasicView(APIView):
|
||||
def get(self, request, *args, **kwargs):
|
||||
|
@ -48,7 +54,7 @@ def sanitise_json_error(error_dict):
|
|||
of json.
|
||||
"""
|
||||
ret = copy.copy(error_dict)
|
||||
chop = len('JSON parse error - No JSON object could be decoded')
|
||||
chop = len(JSON_ERROR)
|
||||
ret['detail'] = ret['detail'][:chop]
|
||||
return ret
|
||||
|
||||
|
@ -61,7 +67,7 @@ class ClassBasedViewIntegrationTests(TestCase):
|
|||
request = factory.post('/', 'f00bar', content_type='application/json')
|
||||
response = self.view(request)
|
||||
expected = {
|
||||
'detail': 'JSON parse error - No JSON object could be decoded'
|
||||
'detail': JSON_ERROR
|
||||
}
|
||||
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
|
||||
self.assertEqual(sanitise_json_error(response.data), expected)
|
||||
|
@ -76,7 +82,7 @@ class ClassBasedViewIntegrationTests(TestCase):
|
|||
request = factory.post('/', form_data)
|
||||
response = self.view(request)
|
||||
expected = {
|
||||
'detail': 'JSON parse error - No JSON object could be decoded'
|
||||
'detail': JSON_ERROR
|
||||
}
|
||||
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
|
||||
self.assertEqual(sanitise_json_error(response.data), expected)
|
||||
|
@ -90,7 +96,7 @@ class FunctionBasedViewIntegrationTests(TestCase):
|
|||
request = factory.post('/', 'f00bar', content_type='application/json')
|
||||
response = self.view(request)
|
||||
expected = {
|
||||
'detail': 'JSON parse error - No JSON object could be decoded'
|
||||
'detail': JSON_ERROR
|
||||
}
|
||||
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
|
||||
self.assertEqual(sanitise_json_error(response.data), expected)
|
||||
|
@ -105,7 +111,7 @@ class FunctionBasedViewIntegrationTests(TestCase):
|
|||
request = factory.post('/', form_data)
|
||||
response = self.view(request)
|
||||
expected = {
|
||||
'detail': 'JSON parse error - No JSON object could be decoded'
|
||||
'detail': JSON_ERROR
|
||||
}
|
||||
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
|
||||
self.assertEqual(sanitise_json_error(response.data), expected)
|
||||
|
|
25
rest_framework/tests/utils.py
Normal file
25
rest_framework/tests/utils.py
Normal file
|
@ -0,0 +1,25 @@
|
|||
from contextlib import contextmanager
|
||||
from rest_framework.compat import six
|
||||
from rest_framework.settings import api_settings
|
||||
|
||||
|
||||
@contextmanager
|
||||
def temporary_setting(setting, value, module=None):
|
||||
"""
|
||||
Temporarily change value of setting for test.
|
||||
|
||||
Optionally reload given module, useful when module uses value of setting on
|
||||
import.
|
||||
"""
|
||||
original_value = getattr(api_settings, setting)
|
||||
setattr(api_settings, setting, value)
|
||||
|
||||
if module is not None:
|
||||
six.moves.reload_module(module)
|
||||
|
||||
yield
|
||||
|
||||
setattr(api_settings, setting, original_value)
|
||||
|
||||
if module is not None:
|
||||
six.moves.reload_module(module)
|
8
rest_framework/tests/views.py
Normal file
8
rest_framework/tests/views.py
Normal file
|
@ -0,0 +1,8 @@
|
|||
from rest_framework import generics
|
||||
from rest_framework.tests.models import NullableForeignKeySource
|
||||
from rest_framework.tests.serializers import NullableFKSourceSerializer
|
||||
|
||||
|
||||
class NullableFKSourceDetail(generics.RetrieveUpdateDestroyAPIView):
|
||||
model = NullableForeignKeySource
|
||||
model_serializer_class = NullableFKSourceSerializer
|
|
@ -136,6 +136,8 @@ class SimpleRateThrottle(BaseThrottle):
|
|||
remaining_duration = self.duration
|
||||
|
||||
available_requests = self.num_requests - len(self.history) + 1
|
||||
if available_requests <= 0:
|
||||
return None
|
||||
|
||||
return remaining_duration / float(available_requests)
|
||||
|
||||
|
@ -155,6 +157,8 @@ class AnonRateThrottle(SimpleRateThrottle):
|
|||
ident = request.META.get('HTTP_X_FORWARDED_FOR')
|
||||
if ident is None:
|
||||
ident = request.META.get('REMOTE_ADDR')
|
||||
else:
|
||||
ident = ''.join(ident.split())
|
||||
|
||||
return self.cache_format % {
|
||||
'scope': self.scope,
|
||||
|
|
|
@ -74,7 +74,7 @@ class _MediaType(object):
|
|||
return 0
|
||||
elif self.sub_type == '*':
|
||||
return 1
|
||||
elif not self.params or self.params.keys() == ['q']:
|
||||
elif not self.params or list(self.params.keys()) == ['q']:
|
||||
return 2
|
||||
return 3
|
||||
|
||||
|
|
|
@ -112,12 +112,13 @@ class APIView(View):
|
|||
|
||||
@property
|
||||
def default_response_headers(self):
|
||||
# TODO: deprecate?
|
||||
# TODO: Only vary by accept if multiple renderers
|
||||
return {
|
||||
headers = {
|
||||
'Allow': ', '.join(self.allowed_methods),
|
||||
'Vary': 'Accept'
|
||||
}
|
||||
if len(self.renderer_classes) > 1:
|
||||
headers['Vary'] = 'Accept'
|
||||
return headers
|
||||
|
||||
|
||||
def http_method_not_allowed(self, request, *args, **kwargs):
|
||||
"""
|
||||
|
@ -130,7 +131,7 @@ class APIView(View):
|
|||
"""
|
||||
If request is not permitted, determine what kind of exception to raise.
|
||||
"""
|
||||
if not self.request.successful_authenticator:
|
||||
if not request.successful_authenticator:
|
||||
raise exceptions.NotAuthenticated()
|
||||
raise exceptions.PermissionDenied()
|
||||
|
||||
|
@ -294,7 +295,7 @@ class APIView(View):
|
|||
|
||||
# Dispatch methods
|
||||
|
||||
def initialize_request(self, request, *args, **kargs):
|
||||
def initialize_request(self, request, *args, **kwargs):
|
||||
"""
|
||||
Returns the initial request object.
|
||||
"""
|
||||
|
|
105
tox.ini
105
tox.ini
|
@ -1,93 +1,154 @@
|
|||
[tox]
|
||||
downloadcache = {toxworkdir}/cache/
|
||||
envlist = py3.3-django1.6,py3.2-django1.6,py2.7-django1.6,py2.6-django1.6,py3.3-django1.5,py3.2-django1.5,py2.7-django1.5,py2.6-django1.5,py2.7-django1.4,py2.6-django1.4,py2.7-django1.3,py2.6-django1.3
|
||||
envlist =
|
||||
py3.4-django1.7,py3.3-django1.7,py3.2-django1.7,py2.7-django1.7,
|
||||
py3.4-django1.6,py3.3-django1.6,py3.2-django1.6,py2.7-django1.6,py2.6-django1.6,
|
||||
py3.4-django1.5,py3.3-django1.5,py3.2-django1.5,py2.7-django1.5,py2.6-django1.5,
|
||||
py2.7-django1.4,py2.6-django1.4,
|
||||
py2.7-django1.3,py2.6-django1.3
|
||||
|
||||
[testenv]
|
||||
commands = {envpython} rest_framework/runtests/runtests.py
|
||||
|
||||
[testenv:py3.4-django1.7]
|
||||
basepython = python3.4
|
||||
deps = https://www.djangoproject.com/download/1.7b2/tarball/
|
||||
django-filter==0.7
|
||||
defusedxml==0.3
|
||||
Pillow==2.3.0
|
||||
|
||||
[testenv:py3.3-django1.7]
|
||||
basepython = python3.3
|
||||
deps = https://www.djangoproject.com/download/1.7b2/tarball/
|
||||
django-filter==0.7
|
||||
defusedxml==0.3
|
||||
Pillow==2.3.0
|
||||
|
||||
[testenv:py3.2-django1.7]
|
||||
basepython = python3.2
|
||||
deps = https://www.djangoproject.com/download/1.7b2/tarball/
|
||||
django-filter==0.7
|
||||
defusedxml==0.3
|
||||
Pillow==2.3.0
|
||||
|
||||
[testenv:py2.7-django1.7]
|
||||
basepython = python2.7
|
||||
deps = https://www.djangoproject.com/download/1.7b2/tarball/
|
||||
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.1.1
|
||||
Pillow==2.3.0
|
||||
|
||||
[testenv:py3.4-django1.6]
|
||||
basepython = python3.4
|
||||
deps = Django==1.6.3
|
||||
django-filter==0.7
|
||||
defusedxml==0.3
|
||||
Pillow==2.3.0
|
||||
|
||||
[testenv:py3.3-django1.6]
|
||||
basepython = python3.3
|
||||
deps = Django==1.6
|
||||
django-filter==0.6a1
|
||||
deps = Django==1.6.3
|
||||
django-filter==0.7
|
||||
defusedxml==0.3
|
||||
Pillow==2.3.0
|
||||
|
||||
[testenv:py3.2-django1.6]
|
||||
basepython = python3.2
|
||||
deps = Django==1.6
|
||||
django-filter==0.6a1
|
||||
deps = Django==1.6.3
|
||||
django-filter==0.7
|
||||
defusedxml==0.3
|
||||
Pillow==2.3.0
|
||||
|
||||
[testenv:py2.7-django1.6]
|
||||
basepython = python2.7
|
||||
deps = Django==1.6
|
||||
django-filter==0.6a1
|
||||
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.1.1
|
||||
Pillow==2.3.0
|
||||
|
||||
[testenv:py2.6-django1.6]
|
||||
basepython = python2.6
|
||||
deps = Django==1.6
|
||||
django-filter==0.6a1
|
||||
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.1.1
|
||||
Pillow==2.3.0
|
||||
|
||||
[testenv:py3.4-django1.5]
|
||||
basepython = python3.4
|
||||
deps = django==1.5.6
|
||||
django-filter==0.7
|
||||
defusedxml==0.3
|
||||
Pillow==2.3.0
|
||||
|
||||
[testenv:py3.3-django1.5]
|
||||
basepython = python3.3
|
||||
deps = django==1.5.5
|
||||
django-filter==0.6a1
|
||||
deps = django==1.5.6
|
||||
django-filter==0.7
|
||||
defusedxml==0.3
|
||||
Pillow==2.3.0
|
||||
|
||||
[testenv:py3.2-django1.5]
|
||||
basepython = python3.2
|
||||
deps = django==1.5.5
|
||||
django-filter==0.6a1
|
||||
deps = django==1.5.6
|
||||
django-filter==0.7
|
||||
defusedxml==0.3
|
||||
Pillow==2.3.0
|
||||
|
||||
[testenv:py2.7-django1.5]
|
||||
basepython = python2.7
|
||||
deps = django==1.5.5
|
||||
django-filter==0.6a1
|
||||
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.1.1
|
||||
Pillow==2.3.0
|
||||
|
||||
[testenv:py2.6-django1.5]
|
||||
basepython = python2.6
|
||||
deps = django==1.5.5
|
||||
django-filter==0.6a1
|
||||
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.1.1
|
||||
Pillow==2.3.0
|
||||
|
||||
[testenv:py2.7-django1.4]
|
||||
basepython = python2.7
|
||||
deps = django==1.4.10
|
||||
django-filter==0.6a1
|
||||
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.1.1
|
||||
Pillow==2.3.0
|
||||
|
||||
[testenv:py2.6-django1.4]
|
||||
basepython = python2.6
|
||||
deps = django==1.4.10
|
||||
django-filter==0.6a1
|
||||
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.1.1
|
||||
Pillow==2.3.0
|
||||
|
||||
[testenv:py2.7-django1.3]
|
||||
basepython = python2.7
|
||||
|
@ -98,6 +159,7 @@ deps = django==1.3.5
|
|||
oauth2==1.5.211
|
||||
django-oauth2-provider==0.2.3
|
||||
django-guardian==1.1.1
|
||||
Pillow==2.3.0
|
||||
|
||||
[testenv:py2.6-django1.3]
|
||||
basepython = python2.6
|
||||
|
@ -108,3 +170,4 @@ deps = django==1.3.5
|
|||
oauth2==1.5.211
|
||||
django-oauth2-provider==0.2.3
|
||||
django-guardian==1.1.1
|
||||
Pillow==2.3.0
|
||||
|
|
Loading…
Reference in New Issue
Block a user