Commit 3326ddc8 by Serhiy Voyt

Merge branch 'master' into modelserialization-charfield-with-null

parents 4e6a2134 636ae419
...@@ -5,12 +5,13 @@ python: ...@@ -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: ...@@ -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: ...@@ -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 ...@@ -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`. ...@@ -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, ...@@ -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 ...@@ -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
\ No newline at end of file
...@@ -73,8 +73,8 @@ Sometimes when serializing objects, you may not want to represent everything exa ...@@ -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 ...@@ -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 ...@@ -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 ...@@ -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`: ...@@ -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 @@ ______ _____ _____ _____ __ _ ...@@ -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: ...@@ -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): ...@@ -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): ...@@ -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): ...@@ -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): ...@@ -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): ...@@ -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): ...@@ -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): ...@@ -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): ...@@ -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 ...@@ -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 ...@@ -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): ...@@ -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): ...@@ -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): ...@@ -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): ...@@ -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): ...@@ -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): ...@@ -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): ...@@ -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): ...@@ -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. ...@@ -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): ...@@ -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): ...@@ -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): ...@@ -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): ...@@ -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 ...@@ -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): ...@@ -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)
......
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 ...@@ -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): ...@@ -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): ...@@ -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): ...@@ -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): ...@@ -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): ...@@ -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)
......
[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/ ...@@ -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 ...@@ -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
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment