mirror of
https://github.com/encode/django-rest-framework.git
synced 2025-08-03 12:00:12 +03:00
Merge remote-tracking branch 'reference/master' into bugfix/nested_through_many_to_many
This commit is contained in:
commit
33e23353c1
|
@ -7,7 +7,7 @@ python:
|
|||
- "3.3"
|
||||
|
||||
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.5.5"
|
||||
- DJANGO="django==1.4.10"
|
||||
|
@ -15,7 +15,7 @@ env:
|
|||
|
||||
install:
|
||||
- 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 django-oauth-plus==2.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.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.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=.
|
||||
|
||||
script:
|
||||
|
@ -32,7 +32,7 @@ script:
|
|||
matrix:
|
||||
exclude:
|
||||
- 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"
|
||||
env: DJANGO="django==1.4.10"
|
||||
- python: "3.2"
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
# 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]
|
||||
|
||||
|
@ -47,7 +47,7 @@ Defaults to `True`.
|
|||
|
||||
### `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.
|
||||
|
||||
|
@ -92,7 +92,7 @@ For example, using the following model.
|
|||
name = models.CharField(max_length=100)
|
||||
created = models.DateTimeField(auto_now_add=True)
|
||||
payment_expiry = models.DateTimeField()
|
||||
|
||||
|
||||
def has_expired(self):
|
||||
return now() > self.payment_expiry
|
||||
|
||||
|
@ -102,7 +102,7 @@ A serializer definition that looked like this:
|
|||
|
||||
class AccountSerializer(serializers.HyperlinkedModelSerializer):
|
||||
expired = serializers.Field(source='has_expired')
|
||||
|
||||
|
||||
class Meta:
|
||||
model = Account
|
||||
fields = ('url', 'owner', 'name', 'expired')
|
||||
|
@ -112,7 +112,7 @@ Would produce output similar to:
|
|||
{
|
||||
'url': 'http://example.com/api/accounts/3/',
|
||||
'owner': 'http://example.com/api/users/12/',
|
||||
'name': 'FooCorp business account',
|
||||
'name': 'FooCorp business account',
|
||||
'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)`
|
||||
|
||||
* `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']`.
|
||||
|
||||
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)`
|
||||
|
||||
- `max_length` designates the maximum length for the file name.
|
||||
|
||||
|
||||
- `allow_empty_file` designates if empty files are allowed.
|
||||
|
||||
## 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):
|
||||
return "rgb(%d, %d, %d)" % (obj.red, obj.green, obj.blue)
|
||||
|
||||
|
||||
def from_native(self, data):
|
||||
data = data.strip('rgb(').rstrip(')')
|
||||
red, green, blue = [int(col) for col in data.split(',')]
|
||||
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()`.
|
||||
|
||||
|
@ -347,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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -40,6 +40,17 @@ You can determine your currently installed version using `pip freeze`:
|
|||
|
||||
## 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
|
||||
|
||||
**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 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):
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -164,7 +164,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
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -59,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
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -438,16 +438,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, {})
|
||||
|
@ -457,6 +447,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,
|
||||
|
@ -757,8 +757,11 @@ class ModelSerializer(Serializer):
|
|||
field.read_only = True
|
||||
|
||||
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
|
||||
for field_name in self.opts.read_only_fields:
|
||||
assert field_name not in self.base_fields.keys(), (
|
||||
|
@ -771,7 +774,10 @@ class ModelSerializer(Serializer):
|
|||
"on serializer '%s'." %
|
||||
(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 "
|
||||
|
@ -881,7 +887,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.
|
||||
"""
|
||||
|
@ -893,7 +899,7 @@ class ModelSerializer(Serializer):
|
|||
field_name = field.source or field_name
|
||||
if field_name in exclusions \
|
||||
and not field.read_only \
|
||||
and field.required \
|
||||
and (field.required or hasattr(instance, field_name)) \
|
||||
and not isinstance(field, Serializer):
|
||||
exclusions.remove(field_name)
|
||||
return exclusions
|
||||
|
@ -908,7 +914,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
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -76,6 +76,10 @@ class APIRequestFactory(DjangoRequestFactory):
|
|||
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)
|
||||
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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
|
||||
|
@ -19,7 +20,7 @@ from rest_framework.authentication import (
|
|||
)
|
||||
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 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])),
|
||||
)
|
||||
|
@ -488,7 +493,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 +502,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 +550,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"""
|
||||
|
|
|
@ -7,9 +7,11 @@ 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()
|
||||
|
||||
|
@ -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):
|
||||
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):
|
||||
username = models.CharField(max_length=20)
|
||||
|
@ -618,4 +658,4 @@ class SensitiveOrderingFilterTests(TestCase):
|
|||
{'id': 2, username_field: 'userB'}, # PassC
|
||||
{'id': 3, username_field: 'userC'}, # PassA
|
||||
]
|
||||
)
|
||||
)
|
||||
|
|
|
@ -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,6 +3,7 @@ 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
|
||||
|
@ -12,26 +13,31 @@ from rest_framework.tests.models import (HasPositiveIntegerAsChoice, Album, Acti
|
|||
from rest_framework.tests.models import BasicModelSerializer
|
||||
import datetime
|
||||
import pickle
|
||||
try:
|
||||
import PIL
|
||||
except:
|
||||
PIL = 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)
|
||||
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 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):
|
||||
|
@ -161,7 +167,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):
|
||||
|
@ -502,6 +508,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
|
||||
|
@ -611,12 +643,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):
|
||||
"""
|
||||
|
@ -1565,6 +1600,7 @@ class ManyFieldHelpTextTest(TestCase):
|
|||
self.assertEqual('Some help text.', rel_field.help_text)
|
||||
|
||||
|
||||
@unittest.skipUnless(PIL is not None, 'PIL is not installed')
|
||||
class AttributeMappingOnAutogeneratedFieldsTests(TestCase):
|
||||
|
||||
def setUp(self):
|
||||
|
@ -1637,6 +1673,7 @@ class AttributeMappingOnAutogeneratedFieldsTests(TestCase):
|
|||
self.field_test('url_field')
|
||||
|
||||
|
||||
@unittest.skipUnless(PIL is not None, 'PIL is not installed')
|
||||
class DefaultValuesOnAutogeneratedFieldsTests(TestCase):
|
||||
|
||||
def setUp(self):
|
||||
|
|
|
@ -152,3 +152,13 @@ class TestAPIRequestFactory(TestCase):
|
|||
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']})
|
||||
|
|
|
@ -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)
|
||||
|
|
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)
|
|
@ -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
|
||||
|
||||
|
|
59
tox.ini
59
tox.ini
|
@ -1,93 +1,128 @@
|
|||
[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.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]
|
||||
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]
|
||||
basepython = python3.3
|
||||
deps = Django==1.6
|
||||
django-filter==0.6a1
|
||||
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
|
||||
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
|
||||
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
|
||||
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.5]
|
||||
basepython = python3.3
|
||||
deps = django==1.5.5
|
||||
django-filter==0.6a1
|
||||
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
|
||||
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
|
||||
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
|
||||
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
|
||||
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
|
||||
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 +133,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 +144,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