mirror of
https://github.com/encode/django-rest-framework.git
synced 2024-11-23 01:57:00 +03:00
Merge branch 'master' into modelserialization-charfield-with-null
This commit is contained in:
commit
3326ddc865
22
.travis.yml
22
.travis.yml
|
@ -5,12 +5,13 @@ python:
|
||||||
- "2.7"
|
- "2.7"
|
||||||
- "3.2"
|
- "3.2"
|
||||||
- "3.3"
|
- "3.3"
|
||||||
|
- "3.4"
|
||||||
|
|
||||||
env:
|
env:
|
||||||
- DJANGO="https://www.djangoproject.com/download/1.7b2/tarball/"
|
- DJANGO="https://www.djangoproject.com/download/1.7.b4/tarball/"
|
||||||
- DJANGO="django==1.6.3"
|
- DJANGO="django==1.6.5"
|
||||||
- DJANGO="django==1.5.6"
|
- DJANGO="django==1.5.8"
|
||||||
- DJANGO="django==1.4.11"
|
- DJANGO="django==1.4.13"
|
||||||
- DJANGO="django==1.3.7"
|
- DJANGO="django==1.3.7"
|
||||||
|
|
||||||
install:
|
install:
|
||||||
|
@ -23,7 +24,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.7b2/tarball/' ]]; then pip install -e git+https://github.com/linovia/django-guardian.git@feature/django_1_7#egg=django-guardian-1.2.0; fi"
|
- "if [[ ${DJANGO} == 'https://www.djangoproject.com/download/1.7.b4/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,13 +33,16 @@ script:
|
||||||
matrix:
|
matrix:
|
||||||
exclude:
|
exclude:
|
||||||
- python: "2.6"
|
- python: "2.6"
|
||||||
env: DJANGO="https://www.djangoproject.com/download/1.7b2/tarball/"
|
env: DJANGO="https://www.djangoproject.com/download/1.7.b4/tarball/"
|
||||||
- python: "3.2"
|
- python: "3.2"
|
||||||
env: DJANGO="django==1.4.11"
|
env: DJANGO="django==1.4.13"
|
||||||
- python: "3.2"
|
- python: "3.2"
|
||||||
env: DJANGO="django==1.3.7"
|
env: DJANGO="django==1.3.7"
|
||||||
- python: "3.3"
|
- python: "3.3"
|
||||||
env: DJANGO="django==1.4.11"
|
env: DJANGO="django==1.4.13"
|
||||||
- python: "3.3"
|
- python: "3.3"
|
||||||
env: DJANGO="django==1.3.7"
|
env: DJANGO="django==1.3.7"
|
||||||
|
- python: "3.4"
|
||||||
|
env: DJANGO="django==1.4.13"
|
||||||
|
- python: "3.4"
|
||||||
|
env: DJANGO="django==1.3.7"
|
||||||
|
|
|
@ -119,7 +119,7 @@ Unauthenticated responses that are denied permission will result in an `HTTP 401
|
||||||
|
|
||||||
This authentication scheme uses a simple token-based HTTP Authentication scheme. Token authentication is appropriate for client-server setups, such as native desktop and mobile clients.
|
This authentication scheme uses a simple token-based HTTP Authentication scheme. Token authentication is appropriate for client-server setups, such as native desktop and mobile clients.
|
||||||
|
|
||||||
To use the `TokenAuthentication` scheme, include `rest_framework.authtoken` in your `INSTALLED_APPS` setting:
|
To use the `TokenAuthentication` scheme you'll need to [configure the authentication classes](#setting-the-authentication-scheme) to include `TokenAuthentication`, and additionally include `rest_framework.authtoken` in your `INSTALLED_APPS` setting:
|
||||||
|
|
||||||
INSTALLED_APPS = (
|
INSTALLED_APPS = (
|
||||||
...
|
...
|
||||||
|
|
|
@ -184,7 +184,9 @@ Corresponds to `django.db.models.fields.SlugField`.
|
||||||
|
|
||||||
## ChoiceField
|
## ChoiceField
|
||||||
|
|
||||||
A field that can accept a value out of a limited set of choices.
|
A field that can accept a value out of a limited set of choices. Optionally takes a `blank_display_value` parameter that customizes the display value of an empty choice.
|
||||||
|
|
||||||
|
**Signature:** `ChoiceField(choices=(), blank_display_value=None)`
|
||||||
|
|
||||||
## EmailField
|
## EmailField
|
||||||
|
|
||||||
|
|
|
@ -187,7 +187,7 @@ Remember that the `pre_save()` method is not called by `GenericAPIView` itself,
|
||||||
You won't typically need to override the following methods, although you might need to call into them if you're writing custom views using `GenericAPIView`.
|
You won't typically need to override the following methods, although you might need to call into them if you're writing custom views using `GenericAPIView`.
|
||||||
|
|
||||||
* `get_serializer_context(self)` - Returns a dictionary containing any extra context that should be supplied to the serializer. Defaults to including `'request'`, `'view'` and `'format'` keys.
|
* `get_serializer_context(self)` - Returns a dictionary containing any extra context that should be supplied to the serializer. Defaults to including `'request'`, `'view'` and `'format'` keys.
|
||||||
* `get_serializer(self, instance=None, data=None, files=None, many=False, partial=False)` - Returns a serializer instance.
|
* `get_serializer(self, instance=None, data=None, files=None, many=False, partial=False, allow_add_remove=False)` - Returns a serializer instance.
|
||||||
* `get_pagination_serializer(self, page)` - Returns a serializer instance to use with paginated data.
|
* `get_pagination_serializer(self, page)` - Returns a serializer instance to use with paginated data.
|
||||||
* `paginate_queryset(self, queryset)` - Paginate a queryset if required, either returning a page object, or `None` if pagination is not configured for this view.
|
* `paginate_queryset(self, queryset)` - Paginate a queryset if required, either returning a page object, or `None` if pagination is not configured for this view.
|
||||||
* `filter_queryset(self, queryset)` - Given a queryset, filter it with whichever filter backends are in use, returning a new queryset.
|
* `filter_queryset(self, queryset)` - Given a queryset, filter it with whichever filter backends are in use, returning a new queryset.
|
||||||
|
|
|
@ -179,7 +179,16 @@ The [wq.db package][wq.db] provides an advanced [Router][wq.db-router] class (an
|
||||||
|
|
||||||
app.router.register_model(MyModel)
|
app.router.register_model(MyModel)
|
||||||
|
|
||||||
|
## DRF-extensions
|
||||||
|
|
||||||
|
The [`DRF-extensions` package][drf-extensions] provides [routers][drf-extensions-routers] for creating [nested viewsets][drf-extensions-nested-viewsets], [collection level controllers][drf-extensions-collection-level-controllers] with [customizable endpoint names][drf-extensions-customizable-endpoint-names].
|
||||||
|
|
||||||
[cite]: http://guides.rubyonrails.org/routing.html
|
[cite]: http://guides.rubyonrails.org/routing.html
|
||||||
[drf-nested-routers]: https://github.com/alanjds/drf-nested-routers
|
[drf-nested-routers]: https://github.com/alanjds/drf-nested-routers
|
||||||
[wq.db]: http://wq.io/wq.db
|
[wq.db]: http://wq.io/wq.db
|
||||||
[wq.db-router]: http://wq.io/docs/app.py
|
[wq.db-router]: http://wq.io/docs/app.py
|
||||||
|
[drf-extensions]: http://chibisov.github.io/drf-extensions/docs/
|
||||||
|
[drf-extensions-routers]: http://chibisov.github.io/drf-extensions/docs/#routers
|
||||||
|
[drf-extensions-nested-viewsets]: http://chibisov.github.io/drf-extensions/docs/#nested-routes
|
||||||
|
[drf-extensions-collection-level-controllers]: http://chibisov.github.io/drf-extensions/docs/#collection-level-controllers
|
||||||
|
[drf-extensions-customizable-endpoint-names]: http://chibisov.github.io/drf-extensions/docs/#controller-endpoint-name
|
|
@ -73,8 +73,8 @@ Sometimes when serializing objects, you may not want to represent everything exa
|
||||||
|
|
||||||
If you need to customize the serialized value of a particular field, you can do this by creating a `transform_<fieldname>` method. For example if you needed to render some markdown from a text field:
|
If you need to customize the serialized value of a particular field, you can do this by creating a `transform_<fieldname>` method. For example if you needed to render some markdown from a text field:
|
||||||
|
|
||||||
description = serializers.TextField()
|
description = serializers.CharField()
|
||||||
description_html = serializers.TextField(source='description', read_only=True)
|
description_html = serializers.CharField(source='description', read_only=True)
|
||||||
|
|
||||||
def transform_description_html(self, obj, value):
|
def transform_description_html(self, obj, value):
|
||||||
from django.contrib.markup.templatetags.markup import markdown
|
from django.contrib.markup.templatetags.markup import markdown
|
||||||
|
@ -464,7 +464,7 @@ For more specific requirements such as specifying a different lookup for each fi
|
||||||
model = Account
|
model = Account
|
||||||
fields = ('url', 'account_name', 'users', 'created')
|
fields = ('url', 'account_name', 'users', 'created')
|
||||||
|
|
||||||
## Overiding the URL field behavior
|
## Overriding the URL field behavior
|
||||||
|
|
||||||
The name of the URL field defaults to 'url'. You can override this globally, by using the `URL_FIELD_NAME` setting.
|
The name of the URL field defaults to 'url'. You can override this globally, by using the `URL_FIELD_NAME` setting.
|
||||||
|
|
||||||
|
@ -478,7 +478,7 @@ You can also override this on a per-serializer basis by using the `url_field_nam
|
||||||
|
|
||||||
**Note**: The generic view implementations normally generate a `Location` header in response to successful `POST` requests. Serializers using `url_field_name` option will not have this header automatically included by the view. If you need to do so you will ned to also override the view's `get_success_headers()` method.
|
**Note**: The generic view implementations normally generate a `Location` header in response to successful `POST` requests. Serializers using `url_field_name` option will not have this header automatically included by the view. If you need to do so you will ned to also override the view's `get_success_headers()` method.
|
||||||
|
|
||||||
You can also overide the URL field's view name and lookup field without overriding the field explicitly, by using the `view_name` and `lookup_field` options, like so:
|
You can also override the URL field's view name and lookup field without overriding the field explicitly, by using the `view_name` and `lookup_field` options, like so:
|
||||||
|
|
||||||
class AccountSerializer(serializers.HyperlinkedModelSerializer):
|
class AccountSerializer(serializers.HyperlinkedModelSerializer):
|
||||||
class Meta:
|
class Meta:
|
||||||
|
|
|
@ -137,7 +137,7 @@ The `@action` and `@link` decorators can additionally take extra arguments that
|
||||||
def set_password(self, request, pk=None):
|
def set_password(self, request, pk=None):
|
||||||
...
|
...
|
||||||
|
|
||||||
The `@action` decorator will route `POST` requests by default, but may also accept other HTTP methods, by using the `method` argument. For example:
|
The `@action` decorator will route `POST` requests by default, but may also accept other HTTP methods, by using the `methods` argument. For example:
|
||||||
|
|
||||||
@action(methods=['POST', 'DELETE'])
|
@action(methods=['POST', 'DELETE'])
|
||||||
def unset_password(self, request, pk=None):
|
def unset_password(self, request, pk=None):
|
||||||
|
|
|
@ -40,24 +40,28 @@ You can determine your currently installed version using `pip freeze`:
|
||||||
|
|
||||||
## 2.3.x series
|
## 2.3.x series
|
||||||
|
|
||||||
### 2.3.x
|
### 2.3.14
|
||||||
|
|
||||||
**Date**: April 2014
|
**Date**: 12th June 2014
|
||||||
|
|
||||||
* Fix nested serializers linked through a backward foreign key relation
|
* **Security fix**: Escape request path when it is include as part of the login and logout links in the browsable API.
|
||||||
* Fix bad links for the `BrowsableAPIRenderer` with `YAMLRenderer`
|
* `help_text` and `verbose_name` automatically set for related fields on `ModelSerializer`.
|
||||||
* Add `UnicodeYAMLRenderer` that extends `YAMLRenderer` with unicode
|
* Fix nested serializers linked through a backward foreign key relation.
|
||||||
* Fix `parse_header` argument convertion
|
* Fix bad links for the `BrowsableAPIRenderer` with `YAMLRenderer`.
|
||||||
* Fix mediatype detection under Python3
|
* Add `UnicodeYAMLRenderer` that extends `YAMLRenderer` with unicode.
|
||||||
* Web browseable API now offers blank option on dropdown when the field is not required
|
* Fix `parse_header` argument convertion.
|
||||||
* `APIException` representation improved for logging purposes
|
* Fix mediatype detection under Python 3.
|
||||||
* Allow source="*" within nested serializers
|
* Web browseable API now offers blank option on dropdown when the field is not required.
|
||||||
* Better support for custom oauth2 provider backends
|
* `APIException` representation improved for logging purposes.
|
||||||
* Fix field validation if it's optional and has no value
|
* Allow source="*" within nested serializers.
|
||||||
* Add `SEARCH_PARAM` and `ORDERING_PARAM`
|
* Better support for custom oauth2 provider backends.
|
||||||
* Fix `APIRequestFactory` to support arguments within the url string for GET
|
* Fix field validation if it's optional and has no value.
|
||||||
* Allow three transport modes for access tokens when accessing a protected resource
|
* Add `SEARCH_PARAM` and `ORDERING_PARAM`.
|
||||||
* Fix `Request`'s `QueryDict` encoding
|
* Fix `APIRequestFactory` to support arguments within the url string for GET.
|
||||||
|
* Allow three transport modes for access tokens when accessing a protected resource.
|
||||||
|
* Fix `QueryDict` encoding on request objects.
|
||||||
|
* Ensure throttle keys do not contain spaces, as those are invalid if using `memcached`.
|
||||||
|
* Support `blank_display_value` on `ChoiceField`.
|
||||||
|
|
||||||
### 2.3.13
|
### 2.3.13
|
||||||
|
|
||||||
|
|
|
@ -8,7 +8,7 @@ ______ _____ _____ _____ __ _
|
||||||
"""
|
"""
|
||||||
|
|
||||||
__title__ = 'Django REST framework'
|
__title__ = 'Django REST framework'
|
||||||
__version__ = '2.3.13'
|
__version__ = '2.3.14'
|
||||||
__author__ = 'Tom Christie'
|
__author__ = 'Tom Christie'
|
||||||
__license__ = 'BSD 2-Clause'
|
__license__ = 'BSD 2-Clause'
|
||||||
__copyright__ = 'Copyright 2011-2014 Tom Christie'
|
__copyright__ = 'Copyright 2011-2014 Tom Christie'
|
||||||
|
|
|
@ -51,6 +51,7 @@ except ImportError:
|
||||||
# guardian is optional
|
# guardian is optional
|
||||||
try:
|
try:
|
||||||
import guardian
|
import guardian
|
||||||
|
import guardian.shortcuts # Fixes #1624
|
||||||
except ImportError:
|
except ImportError:
|
||||||
guardian = None
|
guardian = None
|
||||||
|
|
||||||
|
|
|
@ -62,7 +62,7 @@ def get_component(obj, attr_name):
|
||||||
|
|
||||||
def readable_datetime_formats(formats):
|
def readable_datetime_formats(formats):
|
||||||
format = ', '.join(formats).replace(ISO_8601,
|
format = ', '.join(formats).replace(ISO_8601,
|
||||||
'YYYY-MM-DDThh:mm[:ss[.uuuuuu]][+HHMM|-HHMM|Z]')
|
'YYYY-MM-DDThh:mm[:ss[.uuuuuu]][+HH:MM|-HH:MM|Z]')
|
||||||
return humanize_strptime(format)
|
return humanize_strptime(format)
|
||||||
|
|
||||||
|
|
||||||
|
@ -154,7 +154,12 @@ class Field(object):
|
||||||
def widget_html(self):
|
def widget_html(self):
|
||||||
if not self.widget:
|
if not self.widget:
|
||||||
return ''
|
return ''
|
||||||
return self.widget.render(self._name, self._value)
|
|
||||||
|
attrs = {}
|
||||||
|
if 'id' not in self.widget.attrs:
|
||||||
|
attrs['id'] = self._name
|
||||||
|
|
||||||
|
return self.widget.render(self._name, self._value, attrs=attrs)
|
||||||
|
|
||||||
def label_tag(self):
|
def label_tag(self):
|
||||||
return '<label for="%s">%s:</label>' % (self._name, self.label)
|
return '<label for="%s">%s:</label>' % (self._name, self.label)
|
||||||
|
@ -182,7 +187,7 @@ class Field(object):
|
||||||
|
|
||||||
def field_to_native(self, obj, field_name):
|
def field_to_native(self, obj, field_name):
|
||||||
"""
|
"""
|
||||||
Given and object and a field name, returns the value that should be
|
Given an object and a field name, returns the value that should be
|
||||||
serialized for that field.
|
serialized for that field.
|
||||||
"""
|
"""
|
||||||
if obj is None:
|
if obj is None:
|
||||||
|
@ -505,7 +510,7 @@ class SlugField(CharField):
|
||||||
|
|
||||||
class ChoiceField(WritableField):
|
class ChoiceField(WritableField):
|
||||||
type_name = 'ChoiceField'
|
type_name = 'ChoiceField'
|
||||||
type_label = 'multiple choice'
|
type_label = 'choice'
|
||||||
form_field_class = forms.ChoiceField
|
form_field_class = forms.ChoiceField
|
||||||
widget = widgets.Select
|
widget = widgets.Select
|
||||||
default_error_messages = {
|
default_error_messages = {
|
||||||
|
@ -513,12 +518,16 @@ class ChoiceField(WritableField):
|
||||||
'the available choices.'),
|
'the available choices.'),
|
||||||
}
|
}
|
||||||
|
|
||||||
def __init__(self, choices=(), *args, **kwargs):
|
def __init__(self, choices=(), blank_display_value=None, *args, **kwargs):
|
||||||
self.empty = kwargs.pop('empty', '')
|
self.empty = kwargs.pop('empty', '')
|
||||||
super(ChoiceField, self).__init__(*args, **kwargs)
|
super(ChoiceField, self).__init__(*args, **kwargs)
|
||||||
self.choices = choices
|
self.choices = choices
|
||||||
if not self.required:
|
if not self.required:
|
||||||
self.choices = BLANK_CHOICE_DASH + self.choices
|
if blank_display_value is None:
|
||||||
|
blank_choice = BLANK_CHOICE_DASH
|
||||||
|
else:
|
||||||
|
blank_choice = [('', blank_display_value)]
|
||||||
|
self.choices = blank_choice + self.choices
|
||||||
|
|
||||||
def _get_choices(self):
|
def _get_choices(self):
|
||||||
return self._choices
|
return self._choices
|
||||||
|
@ -1022,9 +1031,9 @@ class SerializerMethodField(Field):
|
||||||
A field that gets its value by calling a method on the serializer it's attached to.
|
A field that gets its value by calling a method on the serializer it's attached to.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, method_name):
|
def __init__(self, method_name, *args, **kwargs):
|
||||||
self.method_name = method_name
|
self.method_name = method_name
|
||||||
super(SerializerMethodField, self).__init__()
|
super(SerializerMethodField, self).__init__(*args, **kwargs)
|
||||||
|
|
||||||
def field_to_native(self, obj, field_name):
|
def field_to_native(self, obj, field_name):
|
||||||
value = getattr(self.parent, self.method_name)(obj)
|
value = getattr(self.parent, self.method_name)(obj)
|
||||||
|
|
|
@ -90,8 +90,8 @@ class GenericAPIView(views.APIView):
|
||||||
'view': self
|
'view': self
|
||||||
}
|
}
|
||||||
|
|
||||||
def get_serializer(self, instance=None, data=None,
|
def get_serializer(self, instance=None, data=None, files=None, many=False,
|
||||||
files=None, many=False, partial=False):
|
partial=False, allow_add_remove=False):
|
||||||
"""
|
"""
|
||||||
Return the serializer instance that should be used for validating and
|
Return the serializer instance that should be used for validating and
|
||||||
deserializing input, and for serializing output.
|
deserializing input, and for serializing output.
|
||||||
|
@ -99,7 +99,9 @@ class GenericAPIView(views.APIView):
|
||||||
serializer_class = self.get_serializer_class()
|
serializer_class = self.get_serializer_class()
|
||||||
context = self.get_serializer_context()
|
context = self.get_serializer_context()
|
||||||
return serializer_class(instance, data=data, files=files,
|
return serializer_class(instance, data=data, files=files,
|
||||||
many=many, partial=partial, context=context)
|
many=many, partial=partial,
|
||||||
|
allow_add_remove=allow_add_remove,
|
||||||
|
context=context)
|
||||||
|
|
||||||
def get_pagination_serializer(self, page):
|
def get_pagination_serializer(self, page):
|
||||||
"""
|
"""
|
||||||
|
|
|
@ -21,6 +21,7 @@ from django.core.paginator import Page
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.forms import widgets
|
from django.forms import widgets
|
||||||
from django.utils.datastructures import SortedDict
|
from django.utils.datastructures import SortedDict
|
||||||
|
from django.core.exceptions import ObjectDoesNotExist
|
||||||
from rest_framework.compat import get_concrete_model, six
|
from rest_framework.compat import get_concrete_model, six
|
||||||
from rest_framework.settings import api_settings
|
from rest_framework.settings import api_settings
|
||||||
|
|
||||||
|
@ -32,8 +33,8 @@ from rest_framework.settings import api_settings
|
||||||
# This helps keep the separation between model fields, form fields, and
|
# This helps keep the separation between model fields, form fields, and
|
||||||
# serializer fields more explicit.
|
# serializer fields more explicit.
|
||||||
|
|
||||||
from rest_framework.relations import *
|
from rest_framework.relations import * # NOQA
|
||||||
from rest_framework.fields import *
|
from rest_framework.fields import * # NOQA
|
||||||
|
|
||||||
|
|
||||||
def _resolve_model(obj):
|
def _resolve_model(obj):
|
||||||
|
@ -48,7 +49,7 @@ def _resolve_model(obj):
|
||||||
String representations should have the format:
|
String representations should have the format:
|
||||||
'appname.ModelName'
|
'appname.ModelName'
|
||||||
"""
|
"""
|
||||||
if type(obj) == str and len(obj.split('.')) == 2:
|
if isinstance(obj, six.string_types) and len(obj.split('.')) == 2:
|
||||||
app_name, model_name = obj.split('.')
|
app_name, model_name = obj.split('.')
|
||||||
return models.get_model(app_name, model_name)
|
return models.get_model(app_name, model_name)
|
||||||
elif inspect.isclass(obj) and issubclass(obj, models.Model):
|
elif inspect.isclass(obj) and issubclass(obj, models.Model):
|
||||||
|
@ -344,7 +345,7 @@ class BaseSerializer(WritableField):
|
||||||
|
|
||||||
for field_name, field in self.fields.items():
|
for field_name, field in self.fields.items():
|
||||||
if field.read_only and obj is None:
|
if field.read_only and obj is None:
|
||||||
continue
|
continue
|
||||||
field.initialize(parent=self, field_name=field_name)
|
field.initialize(parent=self, field_name=field_name)
|
||||||
key = self.get_field_key(field_name)
|
key = self.get_field_key(field_name)
|
||||||
value = field.field_to_native(obj, field_name)
|
value = field.field_to_native(obj, field_name)
|
||||||
|
@ -758,9 +759,9 @@ 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
|
# 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'
|
assert isinstance(self.opts.read_only_fields, (list, tuple)), '`read_only_fields` must be a list or tuple'
|
||||||
|
|
||||||
# Add the `read_only` flag to any fields that have been specified
|
# Add the `read_only` flag to any fields that have been specified
|
||||||
# in the `read_only_fields` option
|
# in the `read_only_fields` option
|
||||||
|
@ -775,10 +776,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
|
# 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'
|
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 "
|
||||||
|
@ -789,7 +790,7 @@ class ModelSerializer(Serializer):
|
||||||
"Non-existant field '%s' specified in `write_only_fields` "
|
"Non-existant field '%s' specified in `write_only_fields` "
|
||||||
"on serializer '%s'." %
|
"on serializer '%s'." %
|
||||||
(field_name, self.__class__.__name__))
|
(field_name, self.__class__.__name__))
|
||||||
ret[field_name].write_only = True
|
ret[field_name].write_only = True
|
||||||
|
|
||||||
return ret
|
return ret
|
||||||
|
|
||||||
|
|
|
@ -122,7 +122,7 @@ def optional_login(request):
|
||||||
except NoReverseMatch:
|
except NoReverseMatch:
|
||||||
return ''
|
return ''
|
||||||
|
|
||||||
snippet = "<a href='%s?next=%s'>Log in</a>" % (login_url, request.path)
|
snippet = "<a href='%s?next=%s'>Log in</a>" % (login_url, escape(request.path))
|
||||||
return snippet
|
return snippet
|
||||||
|
|
||||||
|
|
||||||
|
@ -136,7 +136,7 @@ def optional_logout(request):
|
||||||
except NoReverseMatch:
|
except NoReverseMatch:
|
||||||
return ''
|
return ''
|
||||||
|
|
||||||
snippet = "<a href='%s?next=%s'>Log out</a>" % (logout_url, request.path)
|
snippet = "<a href='%s?next=%s'>Log out</a>" % (logout_url, escape(request.path))
|
||||||
return snippet
|
return snippet
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -36,7 +36,7 @@ class APIRequestFactory(DjangoRequestFactory):
|
||||||
"""
|
"""
|
||||||
|
|
||||||
if not data:
|
if not data:
|
||||||
return ('', None)
|
return ('', content_type)
|
||||||
|
|
||||||
assert format is None or content_type is None, (
|
assert format is None or content_type is None, (
|
||||||
'You may not set both `format` and `content_type`.'
|
'You may not set both `format` and `content_type`.'
|
||||||
|
|
|
@ -4,6 +4,7 @@ General serializer field tests.
|
||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
import datetime
|
import datetime
|
||||||
|
import re
|
||||||
from decimal import Decimal
|
from decimal import Decimal
|
||||||
from uuid import uuid4
|
from uuid import uuid4
|
||||||
from django.core import validators
|
from django.core import validators
|
||||||
|
@ -103,6 +104,16 @@ class BasicFieldTests(TestCase):
|
||||||
keys = list(field.to_native(ret).keys())
|
keys = list(field.to_native(ret).keys())
|
||||||
self.assertEqual(keys, ['c', 'b', 'a', 'z'])
|
self.assertEqual(keys, ['c', 'b', 'a', 'z'])
|
||||||
|
|
||||||
|
def test_widget_html_attributes(self):
|
||||||
|
"""
|
||||||
|
Make sure widget_html() renders the correct attributes
|
||||||
|
"""
|
||||||
|
r = re.compile('(\S+)=["\']?((?:.(?!["\']?\s+(?:\S+)=|[>"\']))+.)["\']?')
|
||||||
|
form = TimeFieldModelSerializer().data
|
||||||
|
attributes = r.findall(form.fields['clock'].widget_html())
|
||||||
|
self.assertIn(('name', 'clock'), attributes)
|
||||||
|
self.assertIn(('id', 'clock'), attributes)
|
||||||
|
|
||||||
|
|
||||||
class DateFieldTest(TestCase):
|
class DateFieldTest(TestCase):
|
||||||
"""
|
"""
|
||||||
|
@ -312,7 +323,7 @@ class DateTimeFieldTest(TestCase):
|
||||||
f.from_native('04:61:59')
|
f.from_native('04:61:59')
|
||||||
except validators.ValidationError as e:
|
except validators.ValidationError as e:
|
||||||
self.assertEqual(e.messages, ["Datetime has wrong format. Use one of these formats instead: "
|
self.assertEqual(e.messages, ["Datetime has wrong format. Use one of these formats instead: "
|
||||||
"YYYY-MM-DDThh:mm[:ss[.uuuuuu]][+HHMM|-HHMM|Z]"])
|
"YYYY-MM-DDThh:mm[:ss[.uuuuuu]][+HH:MM|-HH:MM|Z]"])
|
||||||
else:
|
else:
|
||||||
self.fail("ValidationError was not properly raised")
|
self.fail("ValidationError was not properly raised")
|
||||||
|
|
||||||
|
@ -326,7 +337,7 @@ class DateTimeFieldTest(TestCase):
|
||||||
f.from_native('04 -- 31')
|
f.from_native('04 -- 31')
|
||||||
except validators.ValidationError as e:
|
except validators.ValidationError as e:
|
||||||
self.assertEqual(e.messages, ["Datetime has wrong format. Use one of these formats instead: "
|
self.assertEqual(e.messages, ["Datetime has wrong format. Use one of these formats instead: "
|
||||||
"YYYY-MM-DDThh:mm[:ss[.uuuuuu]][+HHMM|-HHMM|Z]"])
|
"YYYY-MM-DDThh:mm[:ss[.uuuuuu]][+HH:MM|-HH:MM|Z]"])
|
||||||
else:
|
else:
|
||||||
self.fail("ValidationError was not properly raised")
|
self.fail("ValidationError was not properly raised")
|
||||||
|
|
||||||
|
@ -706,6 +717,15 @@ class ChoiceFieldTests(TestCase):
|
||||||
f = serializers.ChoiceField(required=False, choices=SAMPLE_CHOICES)
|
f = serializers.ChoiceField(required=False, choices=SAMPLE_CHOICES)
|
||||||
self.assertEqual(f.choices, models.fields.BLANK_CHOICE_DASH + SAMPLE_CHOICES)
|
self.assertEqual(f.choices, models.fields.BLANK_CHOICE_DASH + SAMPLE_CHOICES)
|
||||||
|
|
||||||
|
def test_blank_choice_display(self):
|
||||||
|
blank = 'No Preference'
|
||||||
|
f = serializers.ChoiceField(
|
||||||
|
required=False,
|
||||||
|
choices=SAMPLE_CHOICES,
|
||||||
|
blank_display_value=blank,
|
||||||
|
)
|
||||||
|
self.assertEqual(f.choices, [('', blank)] + SAMPLE_CHOICES)
|
||||||
|
|
||||||
def test_invalid_choice_model(self):
|
def test_invalid_choice_model(self):
|
||||||
s = ChoiceFieldModelSerializer(data={'choice': 'wrong_value'})
|
s = ChoiceFieldModelSerializer(data={'choice': 'wrong_value'})
|
||||||
self.assertFalse(s.is_valid())
|
self.assertFalse(s.is_valid())
|
||||||
|
|
|
@ -3,6 +3,7 @@ from django.test import TestCase
|
||||||
|
|
||||||
from rest_framework.serializers import _resolve_model
|
from rest_framework.serializers import _resolve_model
|
||||||
from rest_framework.tests.models import BasicModel
|
from rest_framework.tests.models import BasicModel
|
||||||
|
from rest_framework.compat import six
|
||||||
|
|
||||||
|
|
||||||
class ResolveModelTests(TestCase):
|
class ResolveModelTests(TestCase):
|
||||||
|
@ -19,6 +20,10 @@ class ResolveModelTests(TestCase):
|
||||||
resolved_model = _resolve_model('tests.BasicModel')
|
resolved_model = _resolve_model('tests.BasicModel')
|
||||||
self.assertEqual(resolved_model, BasicModel)
|
self.assertEqual(resolved_model, BasicModel)
|
||||||
|
|
||||||
|
def test_resolve_unicode_representation(self):
|
||||||
|
resolved_model = _resolve_model(six.text_type('tests.BasicModel'))
|
||||||
|
self.assertEqual(resolved_model, BasicModel)
|
||||||
|
|
||||||
def test_resolve_non_django_model(self):
|
def test_resolve_non_django_model(self):
|
||||||
with self.assertRaises(ValueError):
|
with self.assertRaises(ValueError):
|
||||||
_resolve_model(TestCase)
|
_resolve_model(TestCase)
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
import sys
|
||||||
import copy
|
import copy
|
||||||
from django.test import TestCase
|
from django.test import TestCase
|
||||||
from rest_framework import status
|
from rest_framework import status
|
||||||
|
@ -11,6 +12,11 @@ from rest_framework.views import APIView
|
||||||
|
|
||||||
factory = APIRequestFactory()
|
factory = APIRequestFactory()
|
||||||
|
|
||||||
|
if sys.version_info[:2] >= (3, 4):
|
||||||
|
JSON_ERROR = 'JSON parse error - Expecting value:'
|
||||||
|
else:
|
||||||
|
JSON_ERROR = 'JSON parse error - No JSON object could be decoded'
|
||||||
|
|
||||||
|
|
||||||
class BasicView(APIView):
|
class BasicView(APIView):
|
||||||
def get(self, request, *args, **kwargs):
|
def get(self, request, *args, **kwargs):
|
||||||
|
@ -48,7 +54,7 @@ def sanitise_json_error(error_dict):
|
||||||
of json.
|
of json.
|
||||||
"""
|
"""
|
||||||
ret = copy.copy(error_dict)
|
ret = copy.copy(error_dict)
|
||||||
chop = len('JSON parse error - No JSON object could be decoded')
|
chop = len(JSON_ERROR)
|
||||||
ret['detail'] = ret['detail'][:chop]
|
ret['detail'] = ret['detail'][:chop]
|
||||||
return ret
|
return ret
|
||||||
|
|
||||||
|
@ -61,7 +67,7 @@ class ClassBasedViewIntegrationTests(TestCase):
|
||||||
request = factory.post('/', 'f00bar', content_type='application/json')
|
request = factory.post('/', 'f00bar', content_type='application/json')
|
||||||
response = self.view(request)
|
response = self.view(request)
|
||||||
expected = {
|
expected = {
|
||||||
'detail': 'JSON parse error - No JSON object could be decoded'
|
'detail': JSON_ERROR
|
||||||
}
|
}
|
||||||
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
|
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
|
||||||
self.assertEqual(sanitise_json_error(response.data), expected)
|
self.assertEqual(sanitise_json_error(response.data), expected)
|
||||||
|
@ -76,7 +82,7 @@ class ClassBasedViewIntegrationTests(TestCase):
|
||||||
request = factory.post('/', form_data)
|
request = factory.post('/', form_data)
|
||||||
response = self.view(request)
|
response = self.view(request)
|
||||||
expected = {
|
expected = {
|
||||||
'detail': 'JSON parse error - No JSON object could be decoded'
|
'detail': JSON_ERROR
|
||||||
}
|
}
|
||||||
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
|
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
|
||||||
self.assertEqual(sanitise_json_error(response.data), expected)
|
self.assertEqual(sanitise_json_error(response.data), expected)
|
||||||
|
@ -90,7 +96,7 @@ class FunctionBasedViewIntegrationTests(TestCase):
|
||||||
request = factory.post('/', 'f00bar', content_type='application/json')
|
request = factory.post('/', 'f00bar', content_type='application/json')
|
||||||
response = self.view(request)
|
response = self.view(request)
|
||||||
expected = {
|
expected = {
|
||||||
'detail': 'JSON parse error - No JSON object could be decoded'
|
'detail': JSON_ERROR
|
||||||
}
|
}
|
||||||
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
|
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
|
||||||
self.assertEqual(sanitise_json_error(response.data), expected)
|
self.assertEqual(sanitise_json_error(response.data), expected)
|
||||||
|
@ -105,7 +111,7 @@ class FunctionBasedViewIntegrationTests(TestCase):
|
||||||
request = factory.post('/', form_data)
|
request = factory.post('/', form_data)
|
||||||
response = self.view(request)
|
response = self.view(request)
|
||||||
expected = {
|
expected = {
|
||||||
'detail': 'JSON parse error - No JSON object could be decoded'
|
'detail': JSON_ERROR
|
||||||
}
|
}
|
||||||
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
|
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
|
||||||
self.assertEqual(sanitise_json_error(response.data), expected)
|
self.assertEqual(sanitise_json_error(response.data), expected)
|
||||||
|
|
28
tox.ini
28
tox.ini
|
@ -1,10 +1,22 @@
|
||||||
[tox]
|
[tox]
|
||||||
downloadcache = {toxworkdir}/cache/
|
downloadcache = {toxworkdir}/cache/
|
||||||
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
|
envlist =
|
||||||
|
py3.4-django1.7,py3.3-django1.7,py3.2-django1.7,py2.7-django1.7,
|
||||||
|
py3.4-django1.6,py3.3-django1.6,py3.2-django1.6,py2.7-django1.6,py2.6-django1.6,
|
||||||
|
py3.4-django1.5,py3.3-django1.5,py3.2-django1.5,py2.7-django1.5,py2.6-django1.5,
|
||||||
|
py2.7-django1.4,py2.6-django1.4,
|
||||||
|
py2.7-django1.3,py2.6-django1.3
|
||||||
|
|
||||||
[testenv]
|
[testenv]
|
||||||
commands = {envpython} rest_framework/runtests/runtests.py
|
commands = {envpython} rest_framework/runtests/runtests.py
|
||||||
|
|
||||||
|
[testenv:py3.4-django1.7]
|
||||||
|
basepython = python3.4
|
||||||
|
deps = https://www.djangoproject.com/download/1.7b2/tarball/
|
||||||
|
django-filter==0.7
|
||||||
|
defusedxml==0.3
|
||||||
|
Pillow==2.3.0
|
||||||
|
|
||||||
[testenv:py3.3-django1.7]
|
[testenv:py3.3-django1.7]
|
||||||
basepython = python3.3
|
basepython = python3.3
|
||||||
deps = https://www.djangoproject.com/download/1.7b2/tarball/
|
deps = https://www.djangoproject.com/download/1.7b2/tarball/
|
||||||
|
@ -30,6 +42,13 @@ deps = https://www.djangoproject.com/download/1.7b2/tarball/
|
||||||
django-guardian==1.1.1
|
django-guardian==1.1.1
|
||||||
Pillow==2.3.0
|
Pillow==2.3.0
|
||||||
|
|
||||||
|
[testenv:py3.4-django1.6]
|
||||||
|
basepython = python3.4
|
||||||
|
deps = Django==1.6.3
|
||||||
|
django-filter==0.7
|
||||||
|
defusedxml==0.3
|
||||||
|
Pillow==2.3.0
|
||||||
|
|
||||||
[testenv:py3.3-django1.6]
|
[testenv:py3.3-django1.6]
|
||||||
basepython = python3.3
|
basepython = python3.3
|
||||||
deps = Django==1.6.3
|
deps = Django==1.6.3
|
||||||
|
@ -66,6 +85,13 @@ deps = Django==1.6.3
|
||||||
django-guardian==1.1.1
|
django-guardian==1.1.1
|
||||||
Pillow==2.3.0
|
Pillow==2.3.0
|
||||||
|
|
||||||
|
[testenv:py3.4-django1.5]
|
||||||
|
basepython = python3.4
|
||||||
|
deps = django==1.5.6
|
||||||
|
django-filter==0.7
|
||||||
|
defusedxml==0.3
|
||||||
|
Pillow==2.3.0
|
||||||
|
|
||||||
[testenv:py3.3-django1.5]
|
[testenv:py3.3-django1.5]
|
||||||
basepython = python3.3
|
basepython = python3.3
|
||||||
deps = django==1.5.6
|
deps = django==1.5.6
|
||||||
|
|
Loading…
Reference in New Issue
Block a user