Merge remote-tracking branch 'reference/master' into bugfix/nested_through_many_to_many

This commit is contained in:
Xavier Ordoquy 2014-04-11 23:54:12 +02:00
commit 33e23353c1
27 changed files with 394 additions and 90 deletions

View File

@ -7,7 +7,7 @@ python:
- "3.3" - "3.3"
env: env:
- DJANGO="https://www.djangoproject.com/download/1.7a2/tarball/" - DJANGO="https://www.djangoproject.com/download/1.7b1/tarball/"
- DJANGO="django==1.6.2" - DJANGO="django==1.6.2"
- DJANGO="django==1.5.5" - DJANGO="django==1.5.5"
- DJANGO="django==1.4.10" - DJANGO="django==1.4.10"
@ -15,7 +15,7 @@ env:
install: install:
- pip install $DJANGO - pip install $DJANGO
- pip install defusedxml==0.3 Pillow - 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 oauth2==1.5.211; 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-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-oauth2-provider==0.2.4; fi"
@ -23,7 +23,7 @@ install:
- "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.5.4; fi"
- "if [[ ${DJANGO::11} != 'django==1.3' ]]; then pip install django-filter==0.7; 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 [[ ${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.7a2/tarball/' ]]; 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.7b1/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=. - export PYTHONPATH=.
script: script:
@ -32,7 +32,7 @@ script:
matrix: matrix:
exclude: exclude:
- python: "2.6" - python: "2.6"
env: DJANGO="https://www.djangoproject.com/download/1.7a2/tarball/" env: DJANGO="https://www.djangoproject.com/download/1.7b1/tarball/"
- python: "3.2" - python: "3.2"
env: DJANGO="django==1.4.10" env: DJANGO="django==1.4.10"
- python: "3.2" - python: "3.2"

View File

@ -2,7 +2,7 @@
# Serializer fields # Serializer fields
> Each field in a Form class is responsible not only for validating data, but also for "cleaning" it — normalizing it to a consistent format. > Each field in a Form class is responsible not only for validating data, but also for "cleaning" it — normalizing it to a consistent format.
> >
> — [Django documentation][cite] > — [Django documentation][cite]
@ -47,7 +47,7 @@ Defaults to `True`.
### `default` ### `default`
If set, this gives the default value that will be used for the field if no input value is supplied. If not set the default behavior is to not populate the attribute at all. If set, this gives the default value that will be used for the field if no input value is supplied. If not set the default behavior is to not populate the attribute at all.
May be set to a function or other callable, in which case the value will be evaluated each time it is used. May be set to a function or other callable, in which case the value will be evaluated each time it is used.
@ -92,7 +92,7 @@ For example, using the following model.
name = models.CharField(max_length=100) name = models.CharField(max_length=100)
created = models.DateTimeField(auto_now_add=True) created = models.DateTimeField(auto_now_add=True)
payment_expiry = models.DateTimeField() payment_expiry = models.DateTimeField()
def has_expired(self): def has_expired(self):
return now() > self.payment_expiry return now() > self.payment_expiry
@ -102,7 +102,7 @@ A serializer definition that looked like this:
class AccountSerializer(serializers.HyperlinkedModelSerializer): class AccountSerializer(serializers.HyperlinkedModelSerializer):
expired = serializers.Field(source='has_expired') expired = serializers.Field(source='has_expired')
class Meta: class Meta:
model = Account model = Account
fields = ('url', 'owner', 'name', 'expired') fields = ('url', 'owner', 'name', 'expired')
@ -112,7 +112,7 @@ Would produce output similar to:
{ {
'url': 'http://example.com/api/accounts/3/', 'url': 'http://example.com/api/accounts/3/',
'owner': 'http://example.com/api/users/12/', 'owner': 'http://example.com/api/users/12/',
'name': 'FooCorp business account', 'name': 'FooCorp business account',
'expired': True 'expired': True
} }
@ -224,7 +224,7 @@ In the case of JSON this means the default datetime representation uses the [ECM
**Signature:** `DateTimeField(format=None, input_formats=None)` **Signature:** `DateTimeField(format=None, input_formats=None)`
* `format` - A string representing the output format. If not specified, this defaults to `None`, which indicates that Python `datetime` objects should be returned by `to_native`. In this case the datetime encoding will be determined by the renderer. * `format` - A string representing the output format. If not specified, this defaults to `None`, which indicates that Python `datetime` objects should be returned by `to_native`. In this case the datetime encoding will be determined by the renderer.
* `input_formats` - A list of strings representing the input formats which may be used to parse the date. If not specified, the `DATETIME_INPUT_FORMATS` setting will be used, which defaults to `['iso-8601']`. * `input_formats` - A list of strings representing the input formats which may be used to parse the date. If not specified, the `DATETIME_INPUT_FORMATS` setting will be used, which defaults to `['iso-8601']`.
DateTime format strings may either be [Python strftime formats][strftime] which explicitly specify the format, or the special string `'iso-8601'`, which indicates that [ISO 8601][iso8601] style datetimes should be used. (eg `'2013-01-29T12:34:56.000000Z'`) DateTime format strings may either be [Python strftime formats][strftime] which explicitly specify the format, or the special string `'iso-8601'`, which indicates that [ISO 8601][iso8601] style datetimes should be used. (eg `'2013-01-29T12:34:56.000000Z'`)
@ -284,7 +284,7 @@ Corresponds to `django.forms.fields.FileField`.
**Signature:** `FileField(max_length=None, allow_empty_file=False)` **Signature:** `FileField(max_length=None, allow_empty_file=False)`
- `max_length` designates the maximum length for the file name. - `max_length` designates the maximum length for the file name.
- `allow_empty_file` designates if empty files are allowed. - `allow_empty_file` designates if empty files are allowed.
## ImageField ## ImageField
@ -329,12 +329,12 @@ Let's look at an example of serializing a class that represents an RGB color val
""" """
def to_native(self, obj): def to_native(self, obj):
return "rgb(%d, %d, %d)" % (obj.red, obj.green, obj.blue) return "rgb(%d, %d, %d)" % (obj.red, obj.green, obj.blue)
def from_native(self, data): def from_native(self, data):
data = data.strip('rgb(').rstrip(')') data = data.strip('rgb(').rstrip(')')
red, green, blue = [int(col) for col in data.split(',')] red, green, blue = [int(col) for col in data.split(',')]
return Color(red, green, blue) return Color(red, green, blue)
By default field values are treated as mapping to an attribute on the object. If you need to customize how the field value is accessed and set you need to override `.field_to_native()` and/or `.field_from_native()`. By default field values are treated as mapping to an attribute on the object. If you need to customize how the field value is accessed and set you need to override `.field_to_native()` and/or `.field_from_native()`.
@ -347,8 +347,17 @@ As an example, let's create a field that can be used represent the class name of
""" """
return obj.__class__ 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 [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 [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 [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 [strftime]: http://docs.python.org/2/library/datetime.html#strftime-and-strptime-behavior
[iso8601]: http://www.w3.org/TR/NOTE-datetime [iso8601]: http://www.w3.org/TR/NOTE-datetime
[drf-compound-fields]: http://drf-compound-fields.readthedocs.org

View File

@ -24,7 +24,7 @@ For example:
from myapp.serializers import PurchaseSerializer from myapp.serializers import PurchaseSerializer
from rest_framework import generics from rest_framework import generics
class PurchaseList(generics.ListAPIView) class PurchaseList(generics.ListAPIView):
serializer_class = PurchaseSerializer serializer_class = PurchaseSerializer
def get_queryset(self): 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: 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 serializer_class = PurchaseSerializer
def get_queryset(self): 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: 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 serializer_class = PurchaseSerializer
def get_queryset(self): def get_queryset(self):
@ -264,13 +264,17 @@ For example:
search_fields = ('=username', '=email') 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]. For more details, see the [Django documentation][search-django-admin].
--- ---
## OrderingFilter ## 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 http://example.com/api/users?ordering=username

View File

@ -158,6 +158,18 @@ A client request like the following would return a paginated list of up to 100 i
Default: `None` 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 ## Authentication settings

View File

@ -40,6 +40,17 @@ You can determine your currently installed version using `pip freeze`:
## 2.3.x series ## 2.3.x series
### 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 ### 2.3.12
**Date**: 15th January 2014 **Date**: 15th January 2014
@ -101,11 +112,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.force_authenticate(None)` should also clear session info if it exists.
* Bugfix: Client sending empty string instead of file now clears `FileField`. * Bugfix: Client sending empty string instead of file now clears `FileField`.
* Bugfix: Empty values on ChoiceFields with `required=False` now consistently return `None`. * 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): class DisablePaginationMixin(object):
def get_paginate_by(self, queryset=None): def get_paginate_by(self, queryset=None):

View File

@ -5,3 +5,4 @@ django-filter>=0.5.4
django-oauth-plus>=2.2.1 django-oauth-plus>=2.2.1
oauth2>=1.5.211 oauth2>=1.5.211
django-oauth2-provider>=0.2.4 django-oauth2-provider>=0.2.4
Pillow==2.3.0

View File

@ -8,10 +8,10 @@ ______ _____ _____ _____ __ _
""" """
__title__ = 'Django REST framework' __title__ = 'Django REST framework'
__version__ = '2.3.12' __version__ = '2.3.13'
__author__ = 'Tom Christie' __author__ = 'Tom Christie'
__license__ = 'BSD 2-Clause' __license__ = 'BSD 2-Clause'
__copyright__ = 'Copyright 2011-2013 Tom Christie' __copyright__ = 'Copyright 2011-2014 Tom Christie'
# Version synonym # Version synonym
VERSION = __version__ VERSION = __version__

View File

@ -6,6 +6,7 @@ import base64
from django.contrib.auth import authenticate from django.contrib.auth import authenticate
from django.core.exceptions import ImproperlyConfigured from django.core.exceptions import ImproperlyConfigured
from django.conf import settings
from rest_framework import exceptions, HTTP_HEADER_ENCODING from rest_framework import exceptions, HTTP_HEADER_ENCODING
from rest_framework.compat import CsrfViewMiddleware from rest_framework.compat import CsrfViewMiddleware
from rest_framework.compat import oauth, oauth_provider, oauth_provider_store 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` OAuth 2 authentication backend using `django-oauth2-provider`
""" """
www_authenticate_realm = 'api' www_authenticate_realm = 'api'
allow_query_params_token = settings.DEBUG
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super(OAuth2Authentication, self).__init__(*args, **kwargs) super(OAuth2Authentication, self).__init__(*args, **kwargs)
@ -308,7 +310,13 @@ class OAuth2Authentication(BaseAuthentication):
auth = get_authorization_header(request).split() 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 return None
if len(auth) == 1: if len(auth) == 1:
@ -318,7 +326,7 @@ class OAuth2Authentication(BaseAuthentication):
msg = 'Invalid bearer header. Token string should not contain spaces.' msg = 'Invalid bearer header. Token string should not contain spaces.'
raise exceptions.AuthenticationFailed(msg) 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): def authenticate_credentials(self, request, access_token):
""" """
@ -326,11 +334,11 @@ class OAuth2Authentication(BaseAuthentication):
""" """
try: 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 # provider_now switches to timezone aware datetime when
# the oauth2_provider version supports to it. # the oauth2_provider version supports to it.
token = token.get(token=access_token, expires__gt=provider_now()) 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') raise exceptions.AuthenticationFailed('Invalid token')
user = token.user user = token.user

View File

@ -550,13 +550,10 @@ except (ImportError, ImproperlyConfigured):
# OAuth 2 support is optional # OAuth 2 support is optional
try: try:
import provider.oauth2 as oauth2_provider import provider as oauth2_provider
from provider.oauth2 import models as oauth2_provider_models
from provider.oauth2 import forms as oauth2_provider_forms
from provider import scope as oauth2_provider_scope from provider import scope as oauth2_provider_scope
from provider import constants as oauth2_constants from provider import constants as oauth2_constants
from provider import __version__ as provider_version if oauth2_provider.__version__ in ('0.2.3', '0.2.4'):
if provider_version in ('0.2.3', '0.2.4'):
# 0.2.3 and 0.2.4 are supported version that do not support # 0.2.3 and 0.2.4 are supported version that do not support
# timezone aware datetimes # timezone aware datetimes
import datetime import datetime
@ -566,8 +563,6 @@ try:
from django.utils.timezone import now as provider_now from django.utils.timezone import now as provider_now
except ImportError: except ImportError:
oauth2_provider = None oauth2_provider = None
oauth2_provider_models = None
oauth2_provider_forms = None
oauth2_provider_scope = None oauth2_provider_scope = None
oauth2_constants = None oauth2_constants = None
provider_now = None provider_now = None

View File

@ -20,6 +20,8 @@ class APIException(Exception):
def __init__(self, detail=None): def __init__(self, detail=None):
self.detail = detail or self.default_detail self.detail = detail or self.default_detail
def __str__(self):
return self.detail
class ParseError(APIException): class ParseError(APIException):
status_code = status.HTTP_400_BAD_REQUEST status_code = status.HTTP_400_BAD_REQUEST

View File

@ -164,7 +164,7 @@ class Field(object):
Called to set up a field prior to field_to_native or field_from_native. Called to set up a field prior to field_to_native or field_from_native.
parent - The parent serializer. 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.parent = parent
self.root = parent.root or parent self.root = parent.root or parent

View File

@ -6,6 +6,7 @@ from __future__ import unicode_literals
from django.core.exceptions import ImproperlyConfigured from django.core.exceptions import ImproperlyConfigured
from django.db import models from django.db import models
from rest_framework.compat import django_filters, six, guardian, get_model_name from rest_framework.compat import django_filters, six, guardian, get_model_name
from rest_framework.settings import api_settings
from functools import reduce from functools import reduce
import operator import operator
@ -69,7 +70,8 @@ class DjangoFilterBackend(BaseFilterBackend):
class SearchFilter(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): def get_search_terms(self, request):
""" """
@ -107,7 +109,8 @@ class SearchFilter(BaseFilterBackend):
class OrderingFilter(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 ordering_fields = None
def get_ordering(self, request): def get_ordering(self, request):

View File

@ -59,6 +59,8 @@ class RelatedField(WritableField):
super(RelatedField, self).__init__(*args, **kwargs) super(RelatedField, self).__init__(*args, **kwargs)
if not self.required: 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.empty_label = BLANK_CHOICE_DASH[0][1]
self.queryset = queryset self.queryset = queryset

View File

@ -346,7 +346,7 @@ class Request(object):
media_type = self.content_type media_type = self.content_type
if stream is None or media_type is None: 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() empty_files = MultiValueDict()
return (empty_data, empty_files) return (empty_data, empty_files)
@ -362,7 +362,7 @@ class Request(object):
# re-raise. Ensures we don't simply repeat the error when # re-raise. Ensures we don't simply repeat the error when
# attempting to render the browsable renderer response, or when # attempting to render the browsable renderer response, or when
# logging the request or similar. # logging the request or similar.
self._data = QueryDict('', self._request._encoding) self._data = QueryDict('', encoding=self._request._encoding)
self._files = MultiValueDict() self._files = MultiValueDict()
raise raise

View File

@ -438,16 +438,6 @@ class BaseSerializer(WritableField):
raise ValidationError(self.error_messages['required']) raise ValidationError(self.error_messages['required'])
return 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 self.source == '*':
if value: if value:
reverted_data = self.restore_fields(value, {}) reverted_data = self.restore_fields(value, {})
@ -457,6 +447,16 @@ class BaseSerializer(WritableField):
if value in (None, ''): if value in (None, ''):
into[(self.source or field_name)] = None into[(self.source or field_name)] = None
else: 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 = { kwargs = {
'instance': obj, 'instance': obj,
'data': value, 'data': value,
@ -757,8 +757,11 @@ class ModelSerializer(Serializer):
field.read_only = True field.read_only = True
ret[accessor_name] = field ret[accessor_name] = field
# 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 bee specified # Add the `read_only` flag to any fields that have been specified
# in the `read_only_fields` option # in the `read_only_fields` option
for field_name in self.opts.read_only_fields: for field_name in self.opts.read_only_fields:
assert field_name not in self.base_fields.keys(), ( assert field_name not in self.base_fields.keys(), (
@ -771,7 +774,10 @@ class ModelSerializer(Serializer):
"on serializer '%s'." % "on serializer '%s'." %
(field_name, self.__class__.__name__)) (field_name, self.__class__.__name__))
ret[field_name].read_only = True 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: for field_name in self.opts.write_only_fields:
assert field_name not in self.base_fields.keys(), ( assert field_name not in self.base_fields.keys(), (
"field '%s' on serializer '%s' specified in " "field '%s' on serializer '%s' specified in "
@ -881,7 +887,7 @@ class ModelSerializer(Serializer):
except KeyError: except KeyError:
return ModelField(model_field=model_field, **kwargs) 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. Return a list of field names to exclude from model validation.
""" """
@ -893,7 +899,7 @@ class ModelSerializer(Serializer):
field_name = field.source or field_name field_name = field.source or field_name
if field_name in exclusions \ if field_name in exclusions \
and not field.read_only \ and not field.read_only \
and field.required \ and (field.required or hasattr(instance, field_name)) \
and not isinstance(field, Serializer): and not isinstance(field, Serializer):
exclusions.remove(field_name) exclusions.remove(field_name)
return exclusions return exclusions
@ -908,7 +914,7 @@ class ModelSerializer(Serializer):
the full_clean validation checking. the full_clean validation checking.
""" """
try: try:
instance.full_clean(exclude=self.get_validation_exclusions()) instance.full_clean(exclude=self.get_validation_exclusions(instance))
except ValidationError as err: except ValidationError as err:
self._errors = err.message_dict self._errors = err.message_dict
return None return None

View File

@ -69,6 +69,10 @@ DEFAULTS = {
'PAGINATE_BY_PARAM': None, 'PAGINATE_BY_PARAM': None,
'MAX_PAGINATE_BY': None, 'MAX_PAGINATE_BY': None,
# Filtering
'SEARCH_PARAM': 'search',
'ORDERING_PARAM': 'ordering',
# Authentication # Authentication
'UNAUTHENTICATED_USER': 'django.contrib.auth.models.AnonymousUser', 'UNAUTHENTICATED_USER': 'django.contrib.auth.models.AnonymousUser',
'UNAUTHENTICATED_TOKEN': None, 'UNAUTHENTICATED_TOKEN': None,

View File

@ -76,6 +76,10 @@ class APIRequestFactory(DjangoRequestFactory):
r = { r = {
'QUERY_STRING': urlencode(data or {}, doseq=True), '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) r.update(extra)
return self.generic('GET', path, **r) return self.generic('GET', path, **r)

View File

@ -103,7 +103,7 @@ class BlogPostComment(RESTFrameworkModel):
class Album(RESTFrameworkModel): class Album(RESTFrameworkModel):
title = models.CharField(max_length=100, unique=True) title = models.CharField(max_length=100, unique=True)
ref = models.CharField(max_length=10, unique=True, null=True, blank=True)
class Photo(RESTFrameworkModel): class Photo(RESTFrameworkModel):
description = models.TextField() description = models.TextField()

View File

@ -3,6 +3,7 @@ from django.contrib.auth.models import User
from django.http import HttpResponse from django.http import HttpResponse
from django.test import TestCase from django.test import TestCase
from django.utils import unittest from django.utils import unittest
from django.utils.http import urlencode
from rest_framework import HTTP_HEADER_ENCODING from rest_framework import HTTP_HEADER_ENCODING
from rest_framework import exceptions from rest_framework import exceptions
from rest_framework import permissions from rest_framework import permissions
@ -19,7 +20,7 @@ from rest_framework.authentication import (
) )
from rest_framework.authtoken.models import Token from rest_framework.authtoken.models import Token
from rest_framework.compat import patterns, url, include 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 oauth2_provider, oauth2_provider_scope
from rest_framework.compat import oauth, oauth_provider from rest_framework.compat import oauth, oauth_provider
from rest_framework.test import APIRequestFactory, APIClient from rest_framework.test import APIRequestFactory, APIClient
from rest_framework.views import APIView from rest_framework.views import APIView
@ -53,10 +54,14 @@ urlpatterns = patterns('',
permission_classes=[permissions.TokenHasReadWriteScope])) permission_classes=[permissions.TokenHasReadWriteScope]))
) )
class OAuth2AuthenticationDebug(OAuth2Authentication):
allow_query_params_token = True
if oauth2_provider is not None: if oauth2_provider is not None:
urlpatterns += patterns('', urlpatterns += patterns('',
url(r'^oauth2/', include('provider.oauth2.urls', namespace='oauth2')), url(r'^oauth2/', include('provider.oauth2.urls', namespace='oauth2')),
url(r'^oauth2-test/$', MockView.as_view(authentication_classes=[OAuth2Authentication])), 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], url(r'^oauth2-with-scope-test/$', MockView.as_view(authentication_classes=[OAuth2Authentication],
permission_classes=[permissions.TokenHasReadWriteScope])), permission_classes=[permissions.TokenHasReadWriteScope])),
) )
@ -488,7 +493,7 @@ class OAuth2Tests(TestCase):
self.ACCESS_TOKEN = "access_token" self.ACCESS_TOKEN = "access_token"
self.REFRESH_TOKEN = "refresh_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_id=self.CLIENT_ID,
client_secret=self.CLIENT_SECRET, client_secret=self.CLIENT_SECRET,
redirect_uri='', redirect_uri='',
@ -497,12 +502,12 @@ class OAuth2Tests(TestCase):
user=None, 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, token=self.ACCESS_TOKEN,
client=self.oauth2_client, client=self.oauth2_client,
user=self.user, 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, user=self.user,
access_token=self.access_token, access_token=self.access_token,
client=self.oauth2_client client=self.oauth2_client
@ -545,6 +550,27 @@ class OAuth2Tests(TestCase):
response = self.csrf_client.get('/oauth2-test/', HTTP_AUTHORIZATION=auth) response = self.csrf_client.get('/oauth2-test/', HTTP_AUTHORIZATION=auth)
self.assertEqual(response.status_code, 200) 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') @unittest.skipUnless(oauth2_provider, 'django-oauth2-provider not installed')
def test_post_form_passing_auth(self): def test_post_form_passing_auth(self):
"""Ensure POSTing form over OAuth with correct credentials passes and does not require CSRF""" """Ensure POSTing form over OAuth with correct credentials passes and does not require CSRF"""

View File

@ -7,9 +7,11 @@ from django.test import TestCase
from django.utils import unittest from django.utils import unittest
from rest_framework import generics, serializers, status, filters from rest_framework import generics, serializers, status, filters
from rest_framework.compat import django_filters, patterns, url 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.test import APIRequestFactory
from rest_framework.tests.models import BasicModel from rest_framework.tests.models import BasicModel
from .models import FilterableItem from .models import FilterableItem
from .utils import temporary_setting
factory = APIRequestFactory() factory = APIRequestFactory()
@ -363,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): class OrdringFilterModel(models.Model):
title = models.CharField(max_length=20) title = models.CharField(max_length=20)
@ -520,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): class SensitiveOrderingFilterModel(models.Model):
username = models.CharField(max_length=20) username = models.CharField(max_length=20)
@ -618,4 +658,4 @@ class SensitiveOrderingFilterTests(TestCase):
{'id': 2, username_field: 'userB'}, # PassC {'id': 2, username_field: 'userB'}, # PassC
{'id': 3, username_field: 'userC'}, # PassA {'id': 3, username_field: 'userC'}, # PassA
] ]
) )

View File

@ -2,8 +2,10 @@
General tests for relational fields. General tests for relational fields.
""" """
from __future__ import unicode_literals from __future__ import unicode_literals
from django import get_version
from django.db import models from django.db import models
from django.test import TestCase from django.test import TestCase
from django.utils import unittest
from rest_framework import serializers from rest_framework import serializers
from rest_framework.tests.models import BlogPost from rest_framework.tests.models import BlogPost
@ -118,3 +120,25 @@ class RelatedFieldSourceTests(TestCase):
(serializers.ModelSerializer,), attrs) (serializers.ModelSerializer,), attrs)
with self.assertRaises(AttributeError): with self.assertRaises(AttributeError):
TestSerializer(data={'name': 'foo'}) 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')

View File

@ -3,6 +3,7 @@ from __future__ import unicode_literals
from django.db import models from django.db import models
from django.db.models.fields import BLANK_CHOICE_DASH from django.db.models.fields import BLANK_CHOICE_DASH
from django.test import TestCase from django.test import TestCase
from django.utils import unittest
from django.utils.datastructures import MultiValueDict from django.utils.datastructures import MultiValueDict
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from rest_framework import serializers, fields, relations from rest_framework import serializers, fields, relations
@ -12,26 +13,31 @@ from rest_framework.tests.models import (HasPositiveIntegerAsChoice, Album, Acti
from rest_framework.tests.models import BasicModelSerializer from rest_framework.tests.models import BasicModelSerializer
import datetime import datetime
import pickle import pickle
try:
import PIL
except:
PIL = None
class AMOAFModel(RESTFrameworkModel): if PIL is not None:
char_field = models.CharField(max_length=1024, blank=True) class AMOAFModel(RESTFrameworkModel):
comma_separated_integer_field = models.CommaSeparatedIntegerField(max_length=1024, blank=True) char_field = models.CharField(max_length=1024, blank=True)
decimal_field = models.DecimalField(max_digits=64, decimal_places=32, blank=True) comma_separated_integer_field = models.CommaSeparatedIntegerField(max_length=1024, blank=True)
email_field = models.EmailField(max_length=1024, blank=True) decimal_field = models.DecimalField(max_digits=64, decimal_places=32, blank=True)
file_field = models.FileField(upload_to='test', max_length=1024, blank=True) email_field = models.EmailField(max_length=1024, blank=True)
image_field = models.ImageField(upload_to='test', max_length=1024, blank=True) file_field = models.FileField(upload_to='test', max_length=1024, blank=True)
slug_field = models.SlugField(max_length=1024, blank=True) image_field = models.ImageField(upload_to='test', max_length=1024, blank=True)
url_field = models.URLField(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): class DVOAFModel(RESTFrameworkModel):
positive_integer_field = models.PositiveIntegerField(blank=True) positive_integer_field = models.PositiveIntegerField(blank=True)
positive_small_integer_field = models.PositiveSmallIntegerField(blank=True) positive_small_integer_field = models.PositiveSmallIntegerField(blank=True)
email_field = models.EmailField(blank=True) email_field = models.EmailField(blank=True)
file_field = models.FileField(upload_to='test', blank=True) file_field = models.FileField(upload_to='test', blank=True)
image_field = models.ImageField(upload_to='test', blank=True) image_field = models.ImageField(upload_to='test', blank=True)
slug_field = models.SlugField(blank=True) slug_field = models.SlugField(blank=True)
url_field = models.URLField(blank=True) url_field = models.URLField(blank=True)
class SubComment(object): class SubComment(object):
@ -161,7 +167,7 @@ class AlbumsSerializer(serializers.ModelSerializer):
class Meta: class Meta:
model = Album model = Album
fields = ['title'] # lists are also valid options fields = ['title', 'ref'] # lists are also valid options
class PositiveIntegerAsChoiceSerializer(serializers.ModelSerializer): class PositiveIntegerAsChoiceSerializer(serializers.ModelSerializer):
@ -502,6 +508,32 @@ class ValidationTests(TestCase):
) )
self.assertEqual(serializer.is_valid(), True) 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): def test_writable_star_source_with_inner_source_fields(self):
""" """
Tests that a serializer with source="*" correctly expands the Tests that a serializer with source="*" correctly expands the
@ -611,12 +643,15 @@ class ModelValidationTests(TestCase):
""" """
Just check if serializers.ModelSerializer handles unique checks via .full_clean() 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.is_valid()
serializer.save() serializer.save()
second_serializer = AlbumsSerializer(data={'title': 'a'}) second_serializer = AlbumsSerializer(data={'title': 'a'})
self.assertFalse(second_serializer.is_valid()) 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): def test_foreign_key_is_null_with_partial(self):
""" """
@ -1565,6 +1600,7 @@ class ManyFieldHelpTextTest(TestCase):
self.assertEqual('Some help text.', rel_field.help_text) self.assertEqual('Some help text.', rel_field.help_text)
@unittest.skipUnless(PIL is not None, 'PIL is not installed')
class AttributeMappingOnAutogeneratedFieldsTests(TestCase): class AttributeMappingOnAutogeneratedFieldsTests(TestCase):
def setUp(self): def setUp(self):
@ -1637,6 +1673,7 @@ class AttributeMappingOnAutogeneratedFieldsTests(TestCase):
self.field_test('url_field') self.field_test('url_field')
@unittest.skipUnless(PIL is not None, 'PIL is not installed')
class DefaultValuesOnAutogeneratedFieldsTests(TestCase): class DefaultValuesOnAutogeneratedFieldsTests(TestCase):
def setUp(self): def setUp(self):

View File

@ -152,3 +152,13 @@ class TestAPIRequestFactory(TestCase):
simple_png.name = 'test.png' simple_png.name = 'test.png'
factory = APIRequestFactory() factory = APIRequestFactory()
factory.post('/', data={'image': simple_png}) 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']})

View File

@ -1,4 +1,5 @@
from __future__ import unicode_literals from __future__ import unicode_literals
from django.core.validators import MaxValueValidator
from django.db import models from django.db import models
from django.test import TestCase from django.test import TestCase
from rest_framework import generics, serializers, status from rest_framework import generics, serializers, status
@ -102,3 +103,46 @@ class TestAvoidValidation(TestCase):
self.assertFalse(serializer.is_valid()) self.assertFalse(serializer.is_valid())
self.assertDictEqual(serializer.errors, self.assertDictEqual(serializer.errors,
{'non_field_errors': ['Invalid data']}) {'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)

View 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)

View File

@ -74,7 +74,7 @@ class _MediaType(object):
return 0 return 0
elif self.sub_type == '*': elif self.sub_type == '*':
return 1 return 1
elif not self.params or self.params.keys() == ['q']: elif not self.params or list(self.params.keys()) == ['q']:
return 2 return 2
return 3 return 3

59
tox.ini
View File

@ -1,93 +1,128 @@
[tox] [tox]
downloadcache = {toxworkdir}/cache/ 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.3-django1.7,py3.2-django1.7,py2.7-django1.7,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
[testenv] [testenv]
commands = {envpython} rest_framework/runtests/runtests.py commands = {envpython} rest_framework/runtests/runtests.py
[testenv:py3.3-django1.7]
basepython = python3.3
deps = https://www.djangoproject.com/download/1.7b1/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.7b1/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.7b1/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.3-django1.6] [testenv:py3.3-django1.6]
basepython = python3.3 basepython = python3.3
deps = Django==1.6 deps = Django==1.6
django-filter==0.6a1 django-filter==0.7
defusedxml==0.3 defusedxml==0.3
Pillow==2.3.0
[testenv:py3.2-django1.6] [testenv:py3.2-django1.6]
basepython = python3.2 basepython = python3.2
deps = Django==1.6 deps = Django==1.6
django-filter==0.6a1 django-filter==0.7
defusedxml==0.3 defusedxml==0.3
Pillow==2.3.0
[testenv:py2.7-django1.6] [testenv:py2.7-django1.6]
basepython = python2.7 basepython = python2.7
deps = Django==1.6 deps = Django==1.6
django-filter==0.6a1 django-filter==0.7
defusedxml==0.3 defusedxml==0.3
django-oauth-plus==2.2.1 django-oauth-plus==2.2.1
oauth2==1.5.211 oauth2==1.5.211
django-oauth2-provider==0.2.4 django-oauth2-provider==0.2.4
django-guardian==1.1.1 django-guardian==1.1.1
Pillow==2.3.0
[testenv:py2.6-django1.6] [testenv:py2.6-django1.6]
basepython = python2.6 basepython = python2.6
deps = Django==1.6 deps = Django==1.6
django-filter==0.6a1 django-filter==0.7
defusedxml==0.3 defusedxml==0.3
django-oauth-plus==2.2.1 django-oauth-plus==2.2.1
oauth2==1.5.211 oauth2==1.5.211
django-oauth2-provider==0.2.4 django-oauth2-provider==0.2.4
django-guardian==1.1.1 django-guardian==1.1.1
Pillow==2.3.0
[testenv:py3.3-django1.5] [testenv:py3.3-django1.5]
basepython = python3.3 basepython = python3.3
deps = django==1.5.5 deps = django==1.5.5
django-filter==0.6a1 django-filter==0.7
defusedxml==0.3 defusedxml==0.3
Pillow==2.3.0
[testenv:py3.2-django1.5] [testenv:py3.2-django1.5]
basepython = python3.2 basepython = python3.2
deps = django==1.5.5 deps = django==1.5.5
django-filter==0.6a1 django-filter==0.7
defusedxml==0.3 defusedxml==0.3
Pillow==2.3.0
[testenv:py2.7-django1.5] [testenv:py2.7-django1.5]
basepython = python2.7 basepython = python2.7
deps = django==1.5.5 deps = django==1.5.5
django-filter==0.6a1 django-filter==0.7
defusedxml==0.3 defusedxml==0.3
django-oauth-plus==2.2.1 django-oauth-plus==2.2.1
oauth2==1.5.211 oauth2==1.5.211
django-oauth2-provider==0.2.3 django-oauth2-provider==0.2.3
django-guardian==1.1.1 django-guardian==1.1.1
Pillow==2.3.0
[testenv:py2.6-django1.5] [testenv:py2.6-django1.5]
basepython = python2.6 basepython = python2.6
deps = django==1.5.5 deps = django==1.5.5
django-filter==0.6a1 django-filter==0.7
defusedxml==0.3 defusedxml==0.3
django-oauth-plus==2.2.1 django-oauth-plus==2.2.1
oauth2==1.5.211 oauth2==1.5.211
django-oauth2-provider==0.2.3 django-oauth2-provider==0.2.3
django-guardian==1.1.1 django-guardian==1.1.1
Pillow==2.3.0
[testenv:py2.7-django1.4] [testenv:py2.7-django1.4]
basepython = python2.7 basepython = python2.7
deps = django==1.4.10 deps = django==1.4.10
django-filter==0.6a1 django-filter==0.7
defusedxml==0.3 defusedxml==0.3
django-oauth-plus==2.2.1 django-oauth-plus==2.2.1
oauth2==1.5.211 oauth2==1.5.211
django-oauth2-provider==0.2.3 django-oauth2-provider==0.2.3
django-guardian==1.1.1 django-guardian==1.1.1
Pillow==2.3.0
[testenv:py2.6-django1.4] [testenv:py2.6-django1.4]
basepython = python2.6 basepython = python2.6
deps = django==1.4.10 deps = django==1.4.10
django-filter==0.6a1 django-filter==0.7
defusedxml==0.3 defusedxml==0.3
django-oauth-plus==2.2.1 django-oauth-plus==2.2.1
oauth2==1.5.211 oauth2==1.5.211
django-oauth2-provider==0.2.3 django-oauth2-provider==0.2.3
django-guardian==1.1.1 django-guardian==1.1.1
Pillow==2.3.0
[testenv:py2.7-django1.3] [testenv:py2.7-django1.3]
basepython = python2.7 basepython = python2.7
@ -98,6 +133,7 @@ deps = django==1.3.5
oauth2==1.5.211 oauth2==1.5.211
django-oauth2-provider==0.2.3 django-oauth2-provider==0.2.3
django-guardian==1.1.1 django-guardian==1.1.1
Pillow==2.3.0
[testenv:py2.6-django1.3] [testenv:py2.6-django1.3]
basepython = python2.6 basepython = python2.6
@ -108,3 +144,4 @@ deps = django==1.3.5
oauth2==1.5.211 oauth2==1.5.211
django-oauth2-provider==0.2.3 django-oauth2-provider==0.2.3
django-guardian==1.1.1 django-guardian==1.1.1
Pillow==2.3.0