mirror of
https://github.com/encode/django-rest-framework.git
synced 2025-02-03 05:04:31 +03:00
Merge branch 'master' of git://github.com/tomchristie/django-rest-framework
This commit is contained in:
commit
5bebd29f11
17
.travis.yml
17
.travis.yml
|
@ -7,9 +7,9 @@ python:
|
||||||
- "3.3"
|
- "3.3"
|
||||||
|
|
||||||
env:
|
env:
|
||||||
- DJANGO="django==1.5 --use-mirrors"
|
- DJANGO="django==1.5.1 --use-mirrors"
|
||||||
- DJANGO="django==1.4.3 --use-mirrors"
|
- DJANGO="django==1.4.5 --use-mirrors"
|
||||||
- DJANGO="django==1.3.5 --use-mirrors"
|
- DJANGO="django==1.3.7 --use-mirrors"
|
||||||
|
|
||||||
install:
|
install:
|
||||||
- pip install $DJANGO
|
- pip install $DJANGO
|
||||||
|
@ -18,7 +18,7 @@ install:
|
||||||
- "if [[ ${TRAVIS_PYTHON_VERSION::1} != '3' ]]; then pip install django-oauth-plus==2.0 --use-mirrors; fi"
|
- "if [[ ${TRAVIS_PYTHON_VERSION::1} != '3' ]]; then pip install django-oauth-plus==2.0 --use-mirrors; fi"
|
||||||
- "if [[ ${TRAVIS_PYTHON_VERSION::1} != '3' ]]; then pip install django-oauth2-provider==0.2.3 --use-mirrors; fi"
|
- "if [[ ${TRAVIS_PYTHON_VERSION::1} != '3' ]]; then pip install django-oauth2-provider==0.2.3 --use-mirrors; fi"
|
||||||
- "if [[ ${DJANGO::11} == 'django==1.3' ]]; then pip install django-filter==0.5.4 --use-mirrors; fi"
|
- "if [[ ${DJANGO::11} == 'django==1.3' ]]; then pip install django-filter==0.5.4 --use-mirrors; fi"
|
||||||
- "if [[ ${DJANGO::11} != 'django==1.3' ]]; then pip install django-filter==0.6a1 --use-mirrors; fi"
|
- "if [[ ${DJANGO::11} != 'django==1.3' ]]; then pip install django-filter==0.6 --use-mirrors; fi"
|
||||||
- export PYTHONPATH=.
|
- export PYTHONPATH=.
|
||||||
|
|
||||||
script:
|
script:
|
||||||
|
@ -27,10 +27,11 @@ script:
|
||||||
matrix:
|
matrix:
|
||||||
exclude:
|
exclude:
|
||||||
- python: "3.2"
|
- python: "3.2"
|
||||||
env: DJANGO="django==1.4.3 --use-mirrors"
|
env: DJANGO="django==1.4.5 --use-mirrors"
|
||||||
- python: "3.2"
|
- python: "3.2"
|
||||||
env: DJANGO="django==1.3.5 --use-mirrors"
|
env: DJANGO="django==1.3.7 --use-mirrors"
|
||||||
- python: "3.3"
|
- python: "3.3"
|
||||||
env: DJANGO="django==1.4.3 --use-mirrors"
|
env: DJANGO="django==1.4.5 --use-mirrors"
|
||||||
- python: "3.3"
|
- python: "3.3"
|
||||||
env: DJANGO="django==1.3.5 --use-mirrors"
|
env: DJANGO="django==1.3.7 --use-mirrors"
|
||||||
|
|
||||||
|
|
|
@ -381,6 +381,15 @@ Note that reverse generic keys, expressed using the `GenericRelation` field, can
|
||||||
|
|
||||||
For more information see [the Django documentation on generic relations][generic-relations].
|
For more information see [the Django documentation on generic relations][generic-relations].
|
||||||
|
|
||||||
|
## ManyToManyFields with a Through Model
|
||||||
|
|
||||||
|
By default, relational fields that target a ``ManyToManyField`` with a
|
||||||
|
``through`` model specified are set to read-only.
|
||||||
|
|
||||||
|
If you exlicitly specify a relational field pointing to a
|
||||||
|
``ManyToManyField`` with a through model, be sure to set ``read_only``
|
||||||
|
to ``True``.
|
||||||
|
|
||||||
## Advanced Hyperlinked fields
|
## Advanced Hyperlinked fields
|
||||||
|
|
||||||
If you have very specific requirements for the style of your hyperlinked relationships you can override `HyperlinkedRelatedField`.
|
If you have very specific requirements for the style of your hyperlinked relationships you can override `HyperlinkedRelatedField`.
|
||||||
|
|
|
@ -274,6 +274,8 @@ Exceptions raised and handled by an HTML renderer will attempt to render using o
|
||||||
|
|
||||||
Templates will render with a `RequestContext` which includes the `status_code` and `details` keys.
|
Templates will render with a `RequestContext` which includes the `status_code` and `details` keys.
|
||||||
|
|
||||||
|
**Note**: If `DEBUG=True`, Django's standard traceback error page will be displayed instead of rendering the HTTP status code and text.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
# Third party packages
|
# Third party packages
|
||||||
|
|
|
@ -35,6 +35,17 @@ A suitable replacement theme can be generated using Bootstrap's [Customize Tool]
|
||||||
|
|
||||||
You can also change the navbar variant, which by default is `navbar-inverse`, using the `bootstrap_navbar_variant` block. The empty `{% block bootstrap_navbar_variant %}{% endblock %}` will use the original Bootstrap navbar style.
|
You can also change the navbar variant, which by default is `navbar-inverse`, using the `bootstrap_navbar_variant` block. The empty `{% block bootstrap_navbar_variant %}{% endblock %}` will use the original Bootstrap navbar style.
|
||||||
|
|
||||||
|
Full Example
|
||||||
|
|
||||||
|
{% extends "rest_framework/base.html" %}
|
||||||
|
|
||||||
|
{% block bootstrap_theme %}
|
||||||
|
<link rel="stylesheet" href="/path/to/yourtheme/bootstrap.min.css' type="text/css">
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block bootstrap_navbar_variant %}{% endblock %}
|
||||||
|
|
||||||
|
|
||||||
For more specific CSS tweaks, use the `style` block instead.
|
For more specific CSS tweaks, use the `style` block instead.
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -124,6 +124,12 @@ The following people have helped make REST framework great.
|
||||||
* Marlon Bailey - [avinash240]
|
* Marlon Bailey - [avinash240]
|
||||||
* James Summerfield - [jsummerfield]
|
* James Summerfield - [jsummerfield]
|
||||||
* Andy Freeland - [rouge8]
|
* Andy Freeland - [rouge8]
|
||||||
|
* Craig de Stigter - [craigds]
|
||||||
|
* Pablo Recio - [pyriku]
|
||||||
|
* Brian Zambrano - [brianz]
|
||||||
|
* Òscar Vilaplana - [grimborg]
|
||||||
|
* Ryan Kaskel - [ryankask]
|
||||||
|
* Andy McKay - [andymckay]
|
||||||
|
|
||||||
Many thanks to everyone who's contributed to the project.
|
Many thanks to everyone who's contributed to the project.
|
||||||
|
|
||||||
|
@ -284,3 +290,9 @@ You can also contact [@_tomchristie][twitter] directly on twitter.
|
||||||
[avinash240]: https://github.com/avinash240
|
[avinash240]: https://github.com/avinash240
|
||||||
[jsummerfield]: https://github.com/jsummerfield
|
[jsummerfield]: https://github.com/jsummerfield
|
||||||
[rouge8]: https://github.com/rouge8
|
[rouge8]: https://github.com/rouge8
|
||||||
|
[craigds]: https://github.com/craigds
|
||||||
|
[pyriku]: https://github.com/pyriku
|
||||||
|
[brianz]: https://github.com/brianz
|
||||||
|
[grimborg]: https://github.com/grimborg
|
||||||
|
[ryankask]: https://github.com/ryankask
|
||||||
|
[andymckay]: https://github.com/andymckay
|
||||||
|
|
|
@ -15,10 +15,12 @@ import warnings
|
||||||
from django.core import validators
|
from django.core import validators
|
||||||
from django.core.exceptions import ValidationError
|
from django.core.exceptions import ValidationError
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
|
from django.db.models.fields import BLANK_CHOICE_DASH
|
||||||
from django import forms
|
from django import forms
|
||||||
from django.forms import widgets
|
from django.forms import widgets
|
||||||
from django.utils.encoding import is_protected_type
|
from django.utils.encoding import is_protected_type
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import ugettext_lazy as _
|
||||||
|
from django.utils.datastructures import SortedDict
|
||||||
|
|
||||||
from rest_framework import ISO_8601
|
from rest_framework import ISO_8601
|
||||||
from rest_framework.compat import timezone, parse_date, parse_datetime, parse_time
|
from rest_framework.compat import timezone, parse_date, parse_datetime, parse_time
|
||||||
|
@ -50,7 +52,7 @@ def get_component(obj, attr_name):
|
||||||
return that attribute on the object.
|
return that attribute on the object.
|
||||||
"""
|
"""
|
||||||
if isinstance(obj, dict):
|
if isinstance(obj, dict):
|
||||||
val = obj[attr_name]
|
val = obj.get(attr_name)
|
||||||
else:
|
else:
|
||||||
val = getattr(obj, attr_name)
|
val = getattr(obj, attr_name)
|
||||||
|
|
||||||
|
@ -170,7 +172,11 @@ class Field(object):
|
||||||
elif hasattr(value, '__iter__') and not isinstance(value, (dict, six.string_types)):
|
elif hasattr(value, '__iter__') and not isinstance(value, (dict, six.string_types)):
|
||||||
return [self.to_native(item) for item in value]
|
return [self.to_native(item) for item in value]
|
||||||
elif isinstance(value, dict):
|
elif isinstance(value, dict):
|
||||||
return dict(map(self.to_native, (k, v)) for k, v in value.items())
|
# Make sure we preserve field ordering, if it exists
|
||||||
|
ret = SortedDict()
|
||||||
|
for key, val in value.items():
|
||||||
|
ret[key] = self.to_native(val)
|
||||||
|
return ret
|
||||||
return smart_text(value)
|
return smart_text(value)
|
||||||
|
|
||||||
def attributes(self):
|
def attributes(self):
|
||||||
|
@ -377,7 +383,6 @@ class URLField(CharField):
|
||||||
type_name = 'URLField'
|
type_name = 'URLField'
|
||||||
|
|
||||||
def __init__(self, **kwargs):
|
def __init__(self, **kwargs):
|
||||||
kwargs['max_length'] = kwargs.get('max_length', 200)
|
|
||||||
kwargs['validators'] = [validators.URLValidator()]
|
kwargs['validators'] = [validators.URLValidator()]
|
||||||
super(URLField, self).__init__(**kwargs)
|
super(URLField, self).__init__(**kwargs)
|
||||||
|
|
||||||
|
@ -386,7 +391,6 @@ class SlugField(CharField):
|
||||||
type_name = 'SlugField'
|
type_name = 'SlugField'
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
kwargs['max_length'] = kwargs.get('max_length', 50)
|
|
||||||
super(SlugField, self).__init__(*args, **kwargs)
|
super(SlugField, self).__init__(*args, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
@ -402,6 +406,8 @@ class ChoiceField(WritableField):
|
||||||
def __init__(self, choices=(), *args, **kwargs):
|
def __init__(self, choices=(), *args, **kwargs):
|
||||||
super(ChoiceField, self).__init__(*args, **kwargs)
|
super(ChoiceField, self).__init__(*args, **kwargs)
|
||||||
self.choices = choices
|
self.choices = choices
|
||||||
|
if not self.required:
|
||||||
|
self.choices = BLANK_CHOICE_DASH + self.choices
|
||||||
|
|
||||||
def _get_choices(self):
|
def _get_choices(self):
|
||||||
return self._choices
|
return self._choices
|
||||||
|
|
|
@ -8,6 +8,7 @@ from __future__ import unicode_literals
|
||||||
from django.core.exceptions import ObjectDoesNotExist, ValidationError
|
from django.core.exceptions import ObjectDoesNotExist, ValidationError
|
||||||
from django.core.urlresolvers import resolve, get_script_prefix, NoReverseMatch
|
from django.core.urlresolvers import resolve, get_script_prefix, NoReverseMatch
|
||||||
from django import forms
|
from django import forms
|
||||||
|
from django.db.models.fields import BLANK_CHOICE_DASH
|
||||||
from django.forms import widgets
|
from django.forms import widgets
|
||||||
from django.forms.models import ModelChoiceIterator
|
from django.forms.models import ModelChoiceIterator
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import ugettext_lazy as _
|
||||||
|
@ -47,7 +48,7 @@ class RelatedField(WritableField):
|
||||||
DeprecationWarning, stacklevel=2)
|
DeprecationWarning, stacklevel=2)
|
||||||
kwargs['required'] = not kwargs.pop('null')
|
kwargs['required'] = not kwargs.pop('null')
|
||||||
|
|
||||||
self.queryset = kwargs.pop('queryset', None)
|
queryset = kwargs.pop('queryset', None)
|
||||||
self.many = kwargs.pop('many', self.many)
|
self.many = kwargs.pop('many', self.many)
|
||||||
if self.many:
|
if self.many:
|
||||||
self.widget = self.many_widget
|
self.widget = self.many_widget
|
||||||
|
@ -56,6 +57,11 @@ class RelatedField(WritableField):
|
||||||
kwargs['read_only'] = kwargs.pop('read_only', self.read_only)
|
kwargs['read_only'] = kwargs.pop('read_only', self.read_only)
|
||||||
super(RelatedField, self).__init__(*args, **kwargs)
|
super(RelatedField, self).__init__(*args, **kwargs)
|
||||||
|
|
||||||
|
if not self.required:
|
||||||
|
self.empty_label = BLANK_CHOICE_DASH[0][1]
|
||||||
|
|
||||||
|
self.queryset = queryset
|
||||||
|
|
||||||
def initialize(self, parent, field_name):
|
def initialize(self, parent, field_name):
|
||||||
super(RelatedField, self).initialize(parent, field_name)
|
super(RelatedField, self).initialize(parent, field_name)
|
||||||
if self.queryset is None and not self.read_only:
|
if self.queryset is None and not self.read_only:
|
||||||
|
@ -221,12 +227,20 @@ class PrimaryKeyRelatedField(RelatedField):
|
||||||
def field_to_native(self, obj, field_name):
|
def field_to_native(self, obj, field_name):
|
||||||
if self.many:
|
if self.many:
|
||||||
# To-many relationship
|
# To-many relationship
|
||||||
try:
|
|
||||||
|
queryset = None
|
||||||
|
if not self.source:
|
||||||
# Prefer obj.serializable_value for performance reasons
|
# Prefer obj.serializable_value for performance reasons
|
||||||
queryset = obj.serializable_value(self.source or field_name)
|
try:
|
||||||
except AttributeError:
|
queryset = obj.serializable_value(field_name)
|
||||||
|
except AttributeError:
|
||||||
|
pass
|
||||||
|
if queryset is None:
|
||||||
# RelatedManager (reverse relationship)
|
# RelatedManager (reverse relationship)
|
||||||
queryset = getattr(obj, self.source or field_name)
|
source = self.source or field_name
|
||||||
|
queryset = obj
|
||||||
|
for component in source.split('.'):
|
||||||
|
queryset = get_component(queryset, component)
|
||||||
|
|
||||||
# Forward relationship
|
# Forward relationship
|
||||||
return [self.to_native(item.pk) for item in queryset.all()]
|
return [self.to_native(item.pk) for item in queryset.all()]
|
||||||
|
@ -434,7 +448,7 @@ class HyperlinkedRelatedField(RelatedField):
|
||||||
raise Exception('Writable related fields must include a `queryset` argument')
|
raise Exception('Writable related fields must include a `queryset` argument')
|
||||||
|
|
||||||
try:
|
try:
|
||||||
http_prefix = value.startswith('http:') or value.startswith('https:')
|
http_prefix = value.startswith(('http:', 'https:'))
|
||||||
except AttributeError:
|
except AttributeError:
|
||||||
msg = self.error_messages['incorrect_type']
|
msg = self.error_messages['incorrect_type']
|
||||||
raise ValidationError(msg % type(value).__name__)
|
raise ValidationError(msg % type(value).__name__)
|
||||||
|
|
|
@ -4,6 +4,8 @@ DEBUG = True
|
||||||
TEMPLATE_DEBUG = DEBUG
|
TEMPLATE_DEBUG = DEBUG
|
||||||
DEBUG_PROPAGATE_EXCEPTIONS = True
|
DEBUG_PROPAGATE_EXCEPTIONS = True
|
||||||
|
|
||||||
|
ALLOWED_HOSTS = ['*']
|
||||||
|
|
||||||
ADMINS = (
|
ADMINS = (
|
||||||
# ('Your Name', 'your_email@domain.com'),
|
# ('Your Name', 'your_email@domain.com'),
|
||||||
)
|
)
|
||||||
|
|
|
@ -378,23 +378,27 @@ class BaseSerializer(WritableField):
|
||||||
# Set the serializer object if it exists
|
# Set the serializer object if it exists
|
||||||
obj = getattr(self.parent.object, field_name) if self.parent.object else None
|
obj = getattr(self.parent.object, field_name) if self.parent.object else None
|
||||||
|
|
||||||
if value in (None, ''):
|
if self.source == '*':
|
||||||
into[(self.source or field_name)] = None
|
if value:
|
||||||
|
into.update(value)
|
||||||
else:
|
else:
|
||||||
kwargs = {
|
if value in (None, ''):
|
||||||
'instance': obj,
|
into[(self.source or field_name)] = None
|
||||||
'data': value,
|
|
||||||
'context': self.context,
|
|
||||||
'partial': self.partial,
|
|
||||||
'many': self.many
|
|
||||||
}
|
|
||||||
serializer = self.__class__(**kwargs)
|
|
||||||
|
|
||||||
if serializer.is_valid():
|
|
||||||
into[self.source or field_name] = serializer.object
|
|
||||||
else:
|
else:
|
||||||
# Propagate errors up to our parent
|
kwargs = {
|
||||||
raise NestedValidationError(serializer.errors)
|
'instance': obj,
|
||||||
|
'data': value,
|
||||||
|
'context': self.context,
|
||||||
|
'partial': self.partial,
|
||||||
|
'many': self.many
|
||||||
|
}
|
||||||
|
serializer = self.__class__(**kwargs)
|
||||||
|
|
||||||
|
if serializer.is_valid():
|
||||||
|
into[self.source or field_name] = serializer.object
|
||||||
|
else:
|
||||||
|
# Propagate errors up to our parent
|
||||||
|
raise NestedValidationError(serializer.errors)
|
||||||
|
|
||||||
def get_identity(self, data):
|
def get_identity(self, data):
|
||||||
"""
|
"""
|
||||||
|
@ -587,11 +591,16 @@ class ModelSerializer(Serializer):
|
||||||
forward_rels += [field for field in opts.many_to_many if field.serialize]
|
forward_rels += [field for field in opts.many_to_many if field.serialize]
|
||||||
|
|
||||||
for model_field in forward_rels:
|
for model_field in forward_rels:
|
||||||
|
has_through_model = False
|
||||||
|
|
||||||
if model_field.rel:
|
if model_field.rel:
|
||||||
to_many = isinstance(model_field,
|
to_many = isinstance(model_field,
|
||||||
models.fields.related.ManyToManyField)
|
models.fields.related.ManyToManyField)
|
||||||
related_model = model_field.rel.to
|
related_model = model_field.rel.to
|
||||||
|
|
||||||
|
if to_many and not model_field.rel.through._meta.auto_created:
|
||||||
|
has_through_model = True
|
||||||
|
|
||||||
if model_field.rel and nested:
|
if model_field.rel and nested:
|
||||||
if len(inspect.getargspec(self.get_nested_field).args) == 2:
|
if len(inspect.getargspec(self.get_nested_field).args) == 2:
|
||||||
warnings.warn(
|
warnings.warn(
|
||||||
|
@ -620,6 +629,9 @@ class ModelSerializer(Serializer):
|
||||||
field = self.get_field(model_field)
|
field = self.get_field(model_field)
|
||||||
|
|
||||||
if field:
|
if field:
|
||||||
|
if has_through_model:
|
||||||
|
field.read_only = True
|
||||||
|
|
||||||
ret[model_field.name] = field
|
ret[model_field.name] = field
|
||||||
|
|
||||||
# Deal with reverse relationships
|
# Deal with reverse relationships
|
||||||
|
@ -637,6 +649,12 @@ class ModelSerializer(Serializer):
|
||||||
continue
|
continue
|
||||||
related_model = relation.model
|
related_model = relation.model
|
||||||
to_many = relation.field.rel.multiple
|
to_many = relation.field.rel.multiple
|
||||||
|
has_through_model = False
|
||||||
|
is_m2m = isinstance(relation.field,
|
||||||
|
models.fields.related.ManyToManyField)
|
||||||
|
|
||||||
|
if is_m2m and not relation.field.rel.through._meta.auto_created:
|
||||||
|
has_through_model = True
|
||||||
|
|
||||||
if nested:
|
if nested:
|
||||||
field = self.get_nested_field(None, related_model, to_many)
|
field = self.get_nested_field(None, related_model, to_many)
|
||||||
|
@ -644,6 +662,9 @@ class ModelSerializer(Serializer):
|
||||||
field = self.get_related_field(None, related_model, to_many)
|
field = self.get_related_field(None, related_model, to_many)
|
||||||
|
|
||||||
if field:
|
if field:
|
||||||
|
if has_through_model:
|
||||||
|
field.read_only = True
|
||||||
|
|
||||||
ret[accessor_name] = field
|
ret[accessor_name] = field
|
||||||
|
|
||||||
# Add the `read_only` flag to any fields that have bee specified
|
# Add the `read_only` flag to any fields that have bee specified
|
||||||
|
@ -705,15 +726,14 @@ class ModelSerializer(Serializer):
|
||||||
Creates a default instance of a basic non-relational field.
|
Creates a default instance of a basic non-relational field.
|
||||||
"""
|
"""
|
||||||
kwargs = {}
|
kwargs = {}
|
||||||
has_default = model_field.has_default()
|
|
||||||
|
|
||||||
if model_field.null or model_field.blank or has_default:
|
if model_field.null or model_field.blank:
|
||||||
kwargs['required'] = False
|
kwargs['required'] = False
|
||||||
|
|
||||||
if isinstance(model_field, models.AutoField) or not model_field.editable:
|
if isinstance(model_field, models.AutoField) or not model_field.editable:
|
||||||
kwargs['read_only'] = True
|
kwargs['read_only'] = True
|
||||||
|
|
||||||
if has_default:
|
if model_field.has_default():
|
||||||
kwargs['default'] = model_field.get_default()
|
kwargs['default'] = model_field.get_default()
|
||||||
|
|
||||||
if issubclass(model_field.__class__, models.TextField):
|
if issubclass(model_field.__class__, models.TextField):
|
||||||
|
@ -724,6 +744,22 @@ class ModelSerializer(Serializer):
|
||||||
kwargs['choices'] = model_field.flatchoices
|
kwargs['choices'] = model_field.flatchoices
|
||||||
return ChoiceField(**kwargs)
|
return ChoiceField(**kwargs)
|
||||||
|
|
||||||
|
attribute_dict = {
|
||||||
|
models.CharField: ['max_length'],
|
||||||
|
models.CommaSeparatedIntegerField: ['max_length'],
|
||||||
|
models.DecimalField: ['max_digits', 'decimal_places'],
|
||||||
|
models.EmailField: ['max_length'],
|
||||||
|
models.FileField: ['max_length'],
|
||||||
|
models.ImageField: ['max_length'],
|
||||||
|
models.SlugField: ['max_length'],
|
||||||
|
models.URLField: ['max_length'],
|
||||||
|
}
|
||||||
|
|
||||||
|
if model_field.__class__ in attribute_dict:
|
||||||
|
attributes = attribute_dict[model_field.__class__]
|
||||||
|
for attribute in attributes:
|
||||||
|
kwargs.update({attribute: getattr(model_field, attribute)})
|
||||||
|
|
||||||
try:
|
try:
|
||||||
return self.field_mapping[model_field.__class__](**kwargs)
|
return self.field_mapping[model_field.__class__](**kwargs)
|
||||||
except KeyError:
|
except KeyError:
|
||||||
|
|
|
@ -19,4 +19,163 @@ a single block in the template.
|
||||||
.navbar-inverse .brand:hover a {
|
.navbar-inverse .brand:hover a {
|
||||||
color: white;
|
color: white;
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* custom navigation styles */
|
||||||
|
.wrapper .navbar{
|
||||||
|
width: 100%;
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.navbar .navbar-inner{
|
||||||
|
background: #2C2C2C;
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
border-top: 5px solid #A30000;
|
||||||
|
border-radius: 0px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.navbar .navbar-inner .nav li, .navbar .navbar-inner .nav li a, .navbar .navbar-inner .brand:hover{
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-list > .active > a, .nav-list > .active > a:hover {
|
||||||
|
background: #2c2c2c;
|
||||||
|
}
|
||||||
|
|
||||||
|
.navbar .navbar-inner .dropdown-menu li a, .navbar .navbar-inner .dropdown-menu li{
|
||||||
|
color: #A30000;
|
||||||
|
}
|
||||||
|
.navbar .navbar-inner .dropdown-menu li a:hover{
|
||||||
|
background: #eeeeee;
|
||||||
|
color: #c20000;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*=== dabapps bootstrap styles ====*/
|
||||||
|
|
||||||
|
html{
|
||||||
|
width:100%;
|
||||||
|
background: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
body, .navbar .navbar-inner .container-fluid {
|
||||||
|
max-width: 1150px;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
body{
|
||||||
|
background: url("../img/grid.png") repeat-x;
|
||||||
|
background-attachment: fixed;
|
||||||
|
}
|
||||||
|
|
||||||
|
#content{
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* sticky footer and footer */
|
||||||
|
html, body {
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
.wrapper {
|
||||||
|
min-height: 100%;
|
||||||
|
height: auto !important;
|
||||||
|
height: 100%;
|
||||||
|
margin: 0 auto -60px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-switcher {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.well {
|
||||||
|
-webkit-box-shadow: none;
|
||||||
|
-moz-box-shadow: none;
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.well .form-actions {
|
||||||
|
padding-bottom: 0;
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.well form {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-tabs {
|
||||||
|
border: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-tabs > li {
|
||||||
|
float: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-tabs li a {
|
||||||
|
margin-right: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-tabs > .active > a {
|
||||||
|
background: #f5f5f5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-tabs > .active > a:hover {
|
||||||
|
background: #f5f5f5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tabbable.first-tab-active .tab-content
|
||||||
|
{
|
||||||
|
border-top-right-radius: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
#footer, #push {
|
||||||
|
height: 60px; /* .push must be the same height as .footer */
|
||||||
|
}
|
||||||
|
|
||||||
|
#footer{
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
#footer p {
|
||||||
|
text-align: center;
|
||||||
|
color: gray;
|
||||||
|
border-top: 1px solid #DDD;
|
||||||
|
padding-top: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#footer a {
|
||||||
|
color: gray;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
#footer a:hover {
|
||||||
|
color: gray;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-header {
|
||||||
|
border-bottom: none;
|
||||||
|
padding-bottom: 0px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* custom general page styles */
|
||||||
|
.hero-unit h2, .hero-unit h1{
|
||||||
|
color: #A30000;
|
||||||
|
}
|
||||||
|
|
||||||
|
body a, body a{
|
||||||
|
color: #A30000;
|
||||||
|
}
|
||||||
|
|
||||||
|
body a:hover{
|
||||||
|
color: #c20000;
|
||||||
|
}
|
||||||
|
|
||||||
|
#content a span{
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
.request-info {
|
||||||
|
clear:both;
|
||||||
|
}
|
||||||
|
|
|
@ -69,152 +69,3 @@ pre {
|
||||||
margin-bottom: 20px;
|
margin-bottom: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/*=== dabapps bootstrap styles ====*/
|
|
||||||
|
|
||||||
html{
|
|
||||||
width:100%;
|
|
||||||
background: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
body, .navbar .navbar-inner .container-fluid {
|
|
||||||
max-width: 1150px;
|
|
||||||
margin: 0 auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
body{
|
|
||||||
background: url("../img/grid.png") repeat-x;
|
|
||||||
background-attachment: fixed;
|
|
||||||
}
|
|
||||||
|
|
||||||
#content{
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
/* custom navigation styles */
|
|
||||||
.wrapper .navbar{
|
|
||||||
width: 100%;
|
|
||||||
position: absolute;
|
|
||||||
left: 0;
|
|
||||||
top: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.navbar .navbar-inner{
|
|
||||||
background: #2C2C2C;
|
|
||||||
color: white;
|
|
||||||
border: none;
|
|
||||||
border-top: 5px solid #A30000;
|
|
||||||
border-radius: 0px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.navbar .navbar-inner .nav li, .navbar .navbar-inner .nav li a, .navbar .navbar-inner .brand{
|
|
||||||
color: white;
|
|
||||||
}
|
|
||||||
|
|
||||||
.nav-list > .active > a, .nav-list > .active > a:hover {
|
|
||||||
background: #2c2c2c;
|
|
||||||
}
|
|
||||||
|
|
||||||
.navbar .navbar-inner .dropdown-menu li a, .navbar .navbar-inner .dropdown-menu li{
|
|
||||||
color: #A30000;
|
|
||||||
}
|
|
||||||
.navbar .navbar-inner .dropdown-menu li a:hover{
|
|
||||||
background: #eeeeee;
|
|
||||||
color: #c20000;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* custom general page styles */
|
|
||||||
.hero-unit h2, .hero-unit h1{
|
|
||||||
color: #A30000;
|
|
||||||
}
|
|
||||||
|
|
||||||
body a, body a{
|
|
||||||
color: #A30000;
|
|
||||||
}
|
|
||||||
|
|
||||||
body a:hover{
|
|
||||||
color: #c20000;
|
|
||||||
}
|
|
||||||
|
|
||||||
#content a span{
|
|
||||||
text-decoration: underline;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* sticky footer and footer */
|
|
||||||
html, body {
|
|
||||||
height: 100%;
|
|
||||||
}
|
|
||||||
.wrapper {
|
|
||||||
min-height: 100%;
|
|
||||||
height: auto !important;
|
|
||||||
height: 100%;
|
|
||||||
margin: 0 auto -60px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.form-switcher {
|
|
||||||
margin-bottom: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.well {
|
|
||||||
-webkit-box-shadow: none;
|
|
||||||
-moz-box-shadow: none;
|
|
||||||
box-shadow: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.well .form-actions {
|
|
||||||
padding-bottom: 0;
|
|
||||||
margin-bottom: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.well form {
|
|
||||||
margin-bottom: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.nav-tabs {
|
|
||||||
border: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.nav-tabs > li {
|
|
||||||
float: right;
|
|
||||||
}
|
|
||||||
|
|
||||||
.nav-tabs li a {
|
|
||||||
margin-right: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.nav-tabs > .active > a {
|
|
||||||
background: #f5f5f5;
|
|
||||||
}
|
|
||||||
|
|
||||||
.nav-tabs > .active > a:hover {
|
|
||||||
background: #f5f5f5;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tabbable.first-tab-active .tab-content
|
|
||||||
{
|
|
||||||
border-top-right-radius: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
#footer, #push {
|
|
||||||
height: 60px; /* .push must be the same height as .footer */
|
|
||||||
}
|
|
||||||
|
|
||||||
#footer{
|
|
||||||
text-align: right;
|
|
||||||
}
|
|
||||||
|
|
||||||
#footer p {
|
|
||||||
text-align: center;
|
|
||||||
color: gray;
|
|
||||||
border-top: 1px solid #DDD;
|
|
||||||
padding-top: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
#footer a {
|
|
||||||
color: gray;
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
|
||||||
|
|
||||||
#footer a:hover {
|
|
||||||
color: gray;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
|
@ -13,8 +13,10 @@
|
||||||
<title>{% block title %}Django REST framework{% endblock %}</title>
|
<title>{% block title %}Django REST framework{% endblock %}</title>
|
||||||
|
|
||||||
{% block style %}
|
{% block style %}
|
||||||
{% block bootstrap_theme %}<link rel="stylesheet" type="text/css" href="{% static "rest_framework/css/bootstrap.min.css" %}"/>{% endblock %}
|
{% block bootstrap_theme %}
|
||||||
<link rel="stylesheet" type="text/css" href="{% static "rest_framework/css/bootstrap-tweaks.css" %}"/>
|
<link rel="stylesheet" type="text/css" href="{% static "rest_framework/css/bootstrap.min.css" %}"/>
|
||||||
|
<link rel="stylesheet" type="text/css" href="{% static "rest_framework/css/bootstrap-tweaks.css" %}"/>
|
||||||
|
{% endblock %}
|
||||||
<link rel="stylesheet" type="text/css" href="{% static "rest_framework/css/prettify.css" %}"/>
|
<link rel="stylesheet" type="text/css" href="{% static "rest_framework/css/prettify.css" %}"/>
|
||||||
<link rel="stylesheet" type="text/css" href="{% static "rest_framework/css/default.css" %}"/>
|
<link rel="stylesheet" type="text/css" href="{% static "rest_framework/css/default.css" %}"/>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
@ -30,8 +32,8 @@
|
||||||
<div class="navbar {% block bootstrap_navbar_variant %}navbar-inverse{% endblock %}">
|
<div class="navbar {% block bootstrap_navbar_variant %}navbar-inverse{% endblock %}">
|
||||||
<div class="navbar-inner">
|
<div class="navbar-inner">
|
||||||
<div class="container-fluid">
|
<div class="container-fluid">
|
||||||
<span class="brand" href="/">
|
<span href="/">
|
||||||
{% block branding %}<a href='http://django-rest-framework.org'>Django REST framework <span class="version">{{ version }}</span></a>{% endblock %}
|
{% block branding %}<a class='brand' href='http://django-rest-framework.org'>Django REST framework <span class="version">{{ version }}</span></a>{% endblock %}
|
||||||
</span>
|
</span>
|
||||||
<ul class="nav pull-right">
|
<ul class="nav pull-right">
|
||||||
{% block userlinks %}
|
{% block userlinks %}
|
||||||
|
@ -109,8 +111,7 @@
|
||||||
<div class="content-main">
|
<div class="content-main">
|
||||||
<div class="page-header"><h1>{{ name }}</h1></div>
|
<div class="page-header"><h1>{{ name }}</h1></div>
|
||||||
{{ description }}
|
{{ description }}
|
||||||
|
<div class="request-info" style="clear: both" >
|
||||||
<div class="request-info">
|
|
||||||
<pre class="prettyprint"><b>{{ request.method }}</b> {{ request.get_full_path }}</pre>
|
<pre class="prettyprint"><b>{{ request.method }}</b> {{ request.get_full_path }}</pre>
|
||||||
</div>
|
</div>
|
||||||
<div class="response-info">
|
<div class="response-info">
|
||||||
|
|
|
@ -4,8 +4,10 @@
|
||||||
|
|
||||||
<head>
|
<head>
|
||||||
{% block style %}
|
{% block style %}
|
||||||
{% block bootstrap_theme %}<link rel="stylesheet" type="text/css" href="{% static "rest_framework/css/bootstrap.min.css" %}"/>{% endblock %}
|
{% block bootstrap_theme %}
|
||||||
<link rel="stylesheet" type="text/css" href="{% static "rest_framework/css/bootstrap-tweaks.css" %}"/>
|
<link rel="stylesheet" type="text/css" href="{% static "rest_framework/css/bootstrap.min.css" %}"/>
|
||||||
|
<link rel="stylesheet" type="text/css" href="{% static "rest_framework/css/bootstrap-tweaks.css" %}"/>
|
||||||
|
{% endblock %}
|
||||||
<link rel="stylesheet" type="text/css" href="{% static "rest_framework/css/default.css" %}"/>
|
<link rel="stylesheet" type="text/css" href="{% static "rest_framework/css/default.css" %}"/>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
</head>
|
</head>
|
||||||
|
|
|
@ -2,15 +2,15 @@
|
||||||
General serializer field tests.
|
General serializer field tests.
|
||||||
"""
|
"""
|
||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
|
from django.utils.datastructures import SortedDict
|
||||||
import datetime
|
import datetime
|
||||||
from decimal import Decimal
|
from decimal import Decimal
|
||||||
|
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.test import TestCase
|
from django.test import TestCase
|
||||||
from django.core import validators
|
from django.core import validators
|
||||||
|
|
||||||
from rest_framework import serializers
|
from rest_framework import serializers
|
||||||
from rest_framework.serializers import Serializer
|
from rest_framework.serializers import Serializer
|
||||||
|
from rest_framework.tests.models import RESTFrameworkModel
|
||||||
|
|
||||||
|
|
||||||
class TimestampedModel(models.Model):
|
class TimestampedModel(models.Model):
|
||||||
|
@ -63,6 +63,20 @@ class BasicFieldTests(TestCase):
|
||||||
serializer = CharPrimaryKeyModelSerializer()
|
serializer = CharPrimaryKeyModelSerializer()
|
||||||
self.assertEqual(serializer.fields['id'].read_only, False)
|
self.assertEqual(serializer.fields['id'].read_only, False)
|
||||||
|
|
||||||
|
def test_dict_field_ordering(self):
|
||||||
|
"""
|
||||||
|
Field should preserve dictionary ordering, if it exists.
|
||||||
|
See: https://github.com/tomchristie/django-rest-framework/issues/832
|
||||||
|
"""
|
||||||
|
ret = SortedDict()
|
||||||
|
ret['c'] = 1
|
||||||
|
ret['b'] = 1
|
||||||
|
ret['a'] = 1
|
||||||
|
ret['z'] = 1
|
||||||
|
field = serializers.Field()
|
||||||
|
keys = list(field.to_native(ret).keys())
|
||||||
|
self.assertEqual(keys, ['c', 'b', 'a', 'z'])
|
||||||
|
|
||||||
|
|
||||||
class DateFieldTest(TestCase):
|
class DateFieldTest(TestCase):
|
||||||
"""
|
"""
|
||||||
|
@ -645,4 +659,153 @@ class DecimalFieldTest(TestCase):
|
||||||
s = DecimalSerializer(data={'decimal_field': '12345.6'})
|
s = DecimalSerializer(data={'decimal_field': '12345.6'})
|
||||||
|
|
||||||
self.assertFalse(s.is_valid())
|
self.assertFalse(s.is_valid())
|
||||||
self.assertEqual(s.errors, {'decimal_field': ['Ensure that there are no more than 4 digits in total.']})
|
self.assertEqual(s.errors, {'decimal_field': ['Ensure that there are no more than 4 digits in total.']})
|
||||||
|
|
||||||
|
|
||||||
|
class ChoiceFieldTests(TestCase):
|
||||||
|
"""
|
||||||
|
Tests for the ChoiceField options generator
|
||||||
|
"""
|
||||||
|
|
||||||
|
SAMPLE_CHOICES = [
|
||||||
|
('red', 'Red'),
|
||||||
|
('green', 'Green'),
|
||||||
|
('blue', 'Blue'),
|
||||||
|
]
|
||||||
|
|
||||||
|
def test_choices_required(self):
|
||||||
|
"""
|
||||||
|
Make sure proper choices are rendered if field is required
|
||||||
|
"""
|
||||||
|
f = serializers.ChoiceField(required=True, choices=self.SAMPLE_CHOICES)
|
||||||
|
self.assertEqual(f.choices, self.SAMPLE_CHOICES)
|
||||||
|
|
||||||
|
def test_choices_not_required(self):
|
||||||
|
"""
|
||||||
|
Make sure proper choices (plus blank) are rendered if the field isn't required
|
||||||
|
"""
|
||||||
|
f = serializers.ChoiceField(required=False, choices=self.SAMPLE_CHOICES)
|
||||||
|
self.assertEqual(f.choices, models.fields.BLANK_CHOICE_DASH + self.SAMPLE_CHOICES)
|
||||||
|
|
||||||
|
|
||||||
|
class EmailFieldTests(TestCase):
|
||||||
|
"""
|
||||||
|
Tests for EmailField attribute values
|
||||||
|
"""
|
||||||
|
|
||||||
|
class EmailFieldModel(RESTFrameworkModel):
|
||||||
|
email_field = models.EmailField(blank=True)
|
||||||
|
|
||||||
|
class EmailFieldWithGivenMaxLengthModel(RESTFrameworkModel):
|
||||||
|
email_field = models.EmailField(max_length=150, blank=True)
|
||||||
|
|
||||||
|
def test_default_model_value(self):
|
||||||
|
class EmailFieldSerializer(serializers.ModelSerializer):
|
||||||
|
class Meta:
|
||||||
|
model = self.EmailFieldModel
|
||||||
|
|
||||||
|
serializer = EmailFieldSerializer(data={})
|
||||||
|
self.assertEqual(serializer.is_valid(), True)
|
||||||
|
self.assertEqual(getattr(serializer.fields['email_field'], 'max_length'), 75)
|
||||||
|
|
||||||
|
def test_given_model_value(self):
|
||||||
|
class EmailFieldSerializer(serializers.ModelSerializer):
|
||||||
|
class Meta:
|
||||||
|
model = self.EmailFieldWithGivenMaxLengthModel
|
||||||
|
|
||||||
|
serializer = EmailFieldSerializer(data={})
|
||||||
|
self.assertEqual(serializer.is_valid(), True)
|
||||||
|
self.assertEqual(getattr(serializer.fields['email_field'], 'max_length'), 150)
|
||||||
|
|
||||||
|
def test_given_serializer_value(self):
|
||||||
|
class EmailFieldSerializer(serializers.ModelSerializer):
|
||||||
|
email_field = serializers.EmailField(source='email_field', max_length=20, required=False)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = self.EmailFieldModel
|
||||||
|
|
||||||
|
serializer = EmailFieldSerializer(data={})
|
||||||
|
self.assertEqual(serializer.is_valid(), True)
|
||||||
|
self.assertEqual(getattr(serializer.fields['email_field'], 'max_length'), 20)
|
||||||
|
|
||||||
|
|
||||||
|
class SlugFieldTests(TestCase):
|
||||||
|
"""
|
||||||
|
Tests for SlugField attribute values
|
||||||
|
"""
|
||||||
|
|
||||||
|
class SlugFieldModel(RESTFrameworkModel):
|
||||||
|
slug_field = models.SlugField(blank=True)
|
||||||
|
|
||||||
|
class SlugFieldWithGivenMaxLengthModel(RESTFrameworkModel):
|
||||||
|
slug_field = models.SlugField(max_length=84, blank=True)
|
||||||
|
|
||||||
|
def test_default_model_value(self):
|
||||||
|
class SlugFieldSerializer(serializers.ModelSerializer):
|
||||||
|
class Meta:
|
||||||
|
model = self.SlugFieldModel
|
||||||
|
|
||||||
|
serializer = SlugFieldSerializer(data={})
|
||||||
|
self.assertEqual(serializer.is_valid(), True)
|
||||||
|
self.assertEqual(getattr(serializer.fields['slug_field'], 'max_length'), 50)
|
||||||
|
|
||||||
|
def test_given_model_value(self):
|
||||||
|
class SlugFieldSerializer(serializers.ModelSerializer):
|
||||||
|
class Meta:
|
||||||
|
model = self.SlugFieldWithGivenMaxLengthModel
|
||||||
|
|
||||||
|
serializer = SlugFieldSerializer(data={})
|
||||||
|
self.assertEqual(serializer.is_valid(), True)
|
||||||
|
self.assertEqual(getattr(serializer.fields['slug_field'], 'max_length'), 84)
|
||||||
|
|
||||||
|
def test_given_serializer_value(self):
|
||||||
|
class SlugFieldSerializer(serializers.ModelSerializer):
|
||||||
|
slug_field = serializers.SlugField(source='slug_field', max_length=20, required=False)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = self.SlugFieldModel
|
||||||
|
|
||||||
|
serializer = SlugFieldSerializer(data={})
|
||||||
|
self.assertEqual(serializer.is_valid(), True)
|
||||||
|
self.assertEqual(getattr(serializer.fields['slug_field'], 'max_length'), 20)
|
||||||
|
|
||||||
|
|
||||||
|
class URLFieldTests(TestCase):
|
||||||
|
"""
|
||||||
|
Tests for URLField attribute values
|
||||||
|
"""
|
||||||
|
|
||||||
|
class URLFieldModel(RESTFrameworkModel):
|
||||||
|
url_field = models.URLField(blank=True)
|
||||||
|
|
||||||
|
class URLFieldWithGivenMaxLengthModel(RESTFrameworkModel):
|
||||||
|
url_field = models.URLField(max_length=128, blank=True)
|
||||||
|
|
||||||
|
def test_default_model_value(self):
|
||||||
|
class URLFieldSerializer(serializers.ModelSerializer):
|
||||||
|
class Meta:
|
||||||
|
model = self.URLFieldModel
|
||||||
|
|
||||||
|
serializer = URLFieldSerializer(data={})
|
||||||
|
self.assertEqual(serializer.is_valid(), True)
|
||||||
|
self.assertEqual(getattr(serializer.fields['url_field'], 'max_length'), 200)
|
||||||
|
|
||||||
|
def test_given_model_value(self):
|
||||||
|
class URLFieldSerializer(serializers.ModelSerializer):
|
||||||
|
class Meta:
|
||||||
|
model = self.URLFieldWithGivenMaxLengthModel
|
||||||
|
|
||||||
|
serializer = URLFieldSerializer(data={})
|
||||||
|
self.assertEqual(serializer.is_valid(), True)
|
||||||
|
self.assertEqual(getattr(serializer.fields['url_field'], 'max_length'), 128)
|
||||||
|
|
||||||
|
def test_given_serializer_value(self):
|
||||||
|
class URLFieldSerializer(serializers.ModelSerializer):
|
||||||
|
url_field = serializers.URLField(source='url_field', max_length=20, required=False)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = self.URLFieldWithGivenMaxLengthModel
|
||||||
|
|
||||||
|
serializer = URLFieldSerializer(data={})
|
||||||
|
self.assertEqual(serializer.is_valid(), True)
|
||||||
|
self.assertEqual(getattr(serializer.fields['url_field'], 'max_length'), 20)
|
||||||
|
|
|
@ -5,6 +5,7 @@ from __future__ import unicode_literals
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.test import TestCase
|
from django.test import TestCase
|
||||||
from rest_framework import serializers
|
from rest_framework import serializers
|
||||||
|
from rest_framework.tests.models import BlogPost
|
||||||
|
|
||||||
|
|
||||||
class NullModel(models.Model):
|
class NullModel(models.Model):
|
||||||
|
@ -33,7 +34,7 @@ class FieldTests(TestCase):
|
||||||
self.assertRaises(serializers.ValidationError, field.from_native, [])
|
self.assertRaises(serializers.ValidationError, field.from_native, [])
|
||||||
|
|
||||||
|
|
||||||
class TestManyRelateMixin(TestCase):
|
class TestManyRelatedMixin(TestCase):
|
||||||
def test_missing_many_to_many_related_field(self):
|
def test_missing_many_to_many_related_field(self):
|
||||||
'''
|
'''
|
||||||
Regression test for #632
|
Regression test for #632
|
||||||
|
@ -45,3 +46,55 @@ class TestManyRelateMixin(TestCase):
|
||||||
into = {}
|
into = {}
|
||||||
field.field_from_native({}, None, 'field_name', into)
|
field.field_from_native({}, None, 'field_name', into)
|
||||||
self.assertEqual(into['field_name'], [])
|
self.assertEqual(into['field_name'], [])
|
||||||
|
|
||||||
|
|
||||||
|
# Regression tests for #694 (`source` attribute on related fields)
|
||||||
|
|
||||||
|
class RelatedFieldSourceTests(TestCase):
|
||||||
|
def test_related_manager_source(self):
|
||||||
|
"""
|
||||||
|
Relational fields should be able to use manager-returning methods as their source.
|
||||||
|
"""
|
||||||
|
BlogPost.objects.create(title='blah')
|
||||||
|
field = serializers.RelatedField(many=True, source='get_blogposts_manager')
|
||||||
|
|
||||||
|
class ClassWithManagerMethod(object):
|
||||||
|
def get_blogposts_manager(self):
|
||||||
|
return BlogPost.objects
|
||||||
|
|
||||||
|
obj = ClassWithManagerMethod()
|
||||||
|
value = field.field_to_native(obj, 'field_name')
|
||||||
|
self.assertEqual(value, ['BlogPost object'])
|
||||||
|
|
||||||
|
def test_related_queryset_source(self):
|
||||||
|
"""
|
||||||
|
Relational fields should be able to use queryset-returning methods as their source.
|
||||||
|
"""
|
||||||
|
BlogPost.objects.create(title='blah')
|
||||||
|
field = serializers.RelatedField(many=True, source='get_blogposts_queryset')
|
||||||
|
|
||||||
|
class ClassWithQuerysetMethod(object):
|
||||||
|
def get_blogposts_queryset(self):
|
||||||
|
return BlogPost.objects.all()
|
||||||
|
|
||||||
|
obj = ClassWithQuerysetMethod()
|
||||||
|
value = field.field_to_native(obj, 'field_name')
|
||||||
|
self.assertEqual(value, ['BlogPost object'])
|
||||||
|
|
||||||
|
def test_dotted_source(self):
|
||||||
|
"""
|
||||||
|
Source argument should support dotted.source notation.
|
||||||
|
"""
|
||||||
|
BlogPost.objects.create(title='blah')
|
||||||
|
field = serializers.RelatedField(many=True, source='a.b.c')
|
||||||
|
|
||||||
|
class ClassWithQuerysetMethod(object):
|
||||||
|
a = {
|
||||||
|
'b': {
|
||||||
|
'c': BlogPost.objects.all()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
obj = ClassWithQuerysetMethod()
|
||||||
|
value = field.field_to_native(obj, 'field_name')
|
||||||
|
self.assertEqual(value, ['BlogPost object'])
|
||||||
|
|
|
@ -4,6 +4,7 @@ from django.test.client import RequestFactory
|
||||||
from rest_framework import serializers
|
from rest_framework import serializers
|
||||||
from rest_framework.compat import patterns, url
|
from rest_framework.compat import patterns, url
|
||||||
from rest_framework.tests.models import (
|
from rest_framework.tests.models import (
|
||||||
|
BlogPost,
|
||||||
ManyToManyTarget, ManyToManySource, ForeignKeyTarget, ForeignKeySource,
|
ManyToManyTarget, ManyToManySource, ForeignKeyTarget, ForeignKeySource,
|
||||||
NullableForeignKeySource, OneToOneTarget, NullableOneToOneSource
|
NullableForeignKeySource, OneToOneTarget, NullableOneToOneSource
|
||||||
)
|
)
|
||||||
|
@ -16,6 +17,7 @@ def dummy_view(request, pk):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
urlpatterns = patterns('',
|
urlpatterns = patterns('',
|
||||||
|
url(r'^dummyurl/(?P<pk>[0-9]+)/$', dummy_view, name='dummy-url'),
|
||||||
url(r'^manytomanysource/(?P<pk>[0-9]+)/$', dummy_view, name='manytomanysource-detail'),
|
url(r'^manytomanysource/(?P<pk>[0-9]+)/$', dummy_view, name='manytomanysource-detail'),
|
||||||
url(r'^manytomanytarget/(?P<pk>[0-9]+)/$', dummy_view, name='manytomanytarget-detail'),
|
url(r'^manytomanytarget/(?P<pk>[0-9]+)/$', dummy_view, name='manytomanytarget-detail'),
|
||||||
url(r'^foreignkeysource/(?P<pk>[0-9]+)/$', dummy_view, name='foreignkeysource-detail'),
|
url(r'^foreignkeysource/(?P<pk>[0-9]+)/$', dummy_view, name='foreignkeysource-detail'),
|
||||||
|
@ -451,3 +453,72 @@ class HyperlinkedNullableOneToOneTests(TestCase):
|
||||||
{'url': 'http://testserver/onetoonetarget/2/', 'name': 'target-2', 'nullable_source': None},
|
{'url': 'http://testserver/onetoonetarget/2/', 'name': 'target-2', 'nullable_source': None},
|
||||||
]
|
]
|
||||||
self.assertEqual(serializer.data, expected)
|
self.assertEqual(serializer.data, expected)
|
||||||
|
|
||||||
|
|
||||||
|
# Regression tests for #694 (`source` attribute on related fields)
|
||||||
|
|
||||||
|
class HyperlinkedRelatedFieldSourceTests(TestCase):
|
||||||
|
urls = 'rest_framework.tests.relations_hyperlink'
|
||||||
|
|
||||||
|
def test_related_manager_source(self):
|
||||||
|
"""
|
||||||
|
Relational fields should be able to use manager-returning methods as their source.
|
||||||
|
"""
|
||||||
|
BlogPost.objects.create(title='blah')
|
||||||
|
field = serializers.HyperlinkedRelatedField(
|
||||||
|
many=True,
|
||||||
|
source='get_blogposts_manager',
|
||||||
|
view_name='dummy-url',
|
||||||
|
)
|
||||||
|
field.context = {'request': request}
|
||||||
|
|
||||||
|
class ClassWithManagerMethod(object):
|
||||||
|
def get_blogposts_manager(self):
|
||||||
|
return BlogPost.objects
|
||||||
|
|
||||||
|
obj = ClassWithManagerMethod()
|
||||||
|
value = field.field_to_native(obj, 'field_name')
|
||||||
|
self.assertEqual(value, ['http://testserver/dummyurl/1/'])
|
||||||
|
|
||||||
|
def test_related_queryset_source(self):
|
||||||
|
"""
|
||||||
|
Relational fields should be able to use queryset-returning methods as their source.
|
||||||
|
"""
|
||||||
|
BlogPost.objects.create(title='blah')
|
||||||
|
field = serializers.HyperlinkedRelatedField(
|
||||||
|
many=True,
|
||||||
|
source='get_blogposts_queryset',
|
||||||
|
view_name='dummy-url',
|
||||||
|
)
|
||||||
|
field.context = {'request': request}
|
||||||
|
|
||||||
|
class ClassWithQuerysetMethod(object):
|
||||||
|
def get_blogposts_queryset(self):
|
||||||
|
return BlogPost.objects.all()
|
||||||
|
|
||||||
|
obj = ClassWithQuerysetMethod()
|
||||||
|
value = field.field_to_native(obj, 'field_name')
|
||||||
|
self.assertEqual(value, ['http://testserver/dummyurl/1/'])
|
||||||
|
|
||||||
|
def test_dotted_source(self):
|
||||||
|
"""
|
||||||
|
Source argument should support dotted.source notation.
|
||||||
|
"""
|
||||||
|
BlogPost.objects.create(title='blah')
|
||||||
|
field = serializers.HyperlinkedRelatedField(
|
||||||
|
many=True,
|
||||||
|
source='a.b.c',
|
||||||
|
view_name='dummy-url',
|
||||||
|
)
|
||||||
|
field.context = {'request': request}
|
||||||
|
|
||||||
|
class ClassWithQuerysetMethod(object):
|
||||||
|
a = {
|
||||||
|
'b': {
|
||||||
|
'c': BlogPost.objects.all()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
obj = ClassWithQuerysetMethod()
|
||||||
|
value = field.field_to_native(obj, 'field_name')
|
||||||
|
self.assertEqual(value, ['http://testserver/dummyurl/1/'])
|
||||||
|
|
|
@ -1,7 +1,11 @@
|
||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
|
from django.db import models
|
||||||
from django.test import TestCase
|
from django.test import TestCase
|
||||||
from rest_framework import serializers
|
from rest_framework import serializers
|
||||||
from rest_framework.tests.models import ManyToManyTarget, ManyToManySource, ForeignKeyTarget, ForeignKeySource, NullableForeignKeySource, OneToOneTarget, NullableOneToOneSource
|
from rest_framework.tests.models import (
|
||||||
|
BlogPost, ManyToManyTarget, ManyToManySource, ForeignKeyTarget, ForeignKeySource,
|
||||||
|
NullableForeignKeySource, OneToOneTarget, NullableOneToOneSource,
|
||||||
|
)
|
||||||
from rest_framework.compat import six
|
from rest_framework.compat import six
|
||||||
|
|
||||||
|
|
||||||
|
@ -124,6 +128,7 @@ class PKManyToManyTests(TestCase):
|
||||||
# Ensure source 4 is added, and everything else is as expected
|
# Ensure source 4 is added, and everything else is as expected
|
||||||
queryset = ManyToManySource.objects.all()
|
queryset = ManyToManySource.objects.all()
|
||||||
serializer = ManyToManySourceSerializer(queryset, many=True)
|
serializer = ManyToManySourceSerializer(queryset, many=True)
|
||||||
|
self.assertFalse(serializer.fields['targets'].read_only)
|
||||||
expected = [
|
expected = [
|
||||||
{'id': 1, 'name': 'source-1', 'targets': [1]},
|
{'id': 1, 'name': 'source-1', 'targets': [1]},
|
||||||
{'id': 2, 'name': 'source-2', 'targets': [1, 2]},
|
{'id': 2, 'name': 'source-2', 'targets': [1, 2]},
|
||||||
|
@ -135,6 +140,7 @@ class PKManyToManyTests(TestCase):
|
||||||
def test_reverse_many_to_many_create(self):
|
def test_reverse_many_to_many_create(self):
|
||||||
data = {'id': 4, 'name': 'target-4', 'sources': [1, 3]}
|
data = {'id': 4, 'name': 'target-4', 'sources': [1, 3]}
|
||||||
serializer = ManyToManyTargetSerializer(data=data)
|
serializer = ManyToManyTargetSerializer(data=data)
|
||||||
|
self.assertFalse(serializer.fields['sources'].read_only)
|
||||||
self.assertTrue(serializer.is_valid())
|
self.assertTrue(serializer.is_valid())
|
||||||
obj = serializer.save()
|
obj = serializer.save()
|
||||||
self.assertEqual(serializer.data, data)
|
self.assertEqual(serializer.data, data)
|
||||||
|
@ -421,3 +427,116 @@ class PKNullableOneToOneTests(TestCase):
|
||||||
{'id': 2, 'name': 'target-2', 'nullable_source': 1},
|
{'id': 2, 'name': 'target-2', 'nullable_source': 1},
|
||||||
]
|
]
|
||||||
self.assertEqual(serializer.data, expected)
|
self.assertEqual(serializer.data, expected)
|
||||||
|
|
||||||
|
|
||||||
|
# The below models and tests ensure that serializer fields corresponding
|
||||||
|
# to a ManyToManyField field with a user-specified ``through`` model are
|
||||||
|
# set to read only
|
||||||
|
|
||||||
|
|
||||||
|
class ManyToManyThroughTarget(models.Model):
|
||||||
|
name = models.CharField(max_length=100)
|
||||||
|
|
||||||
|
|
||||||
|
class ManyToManyThrough(models.Model):
|
||||||
|
source = models.ForeignKey('ManyToManyThroughSource')
|
||||||
|
target = models.ForeignKey(ManyToManyThroughTarget)
|
||||||
|
|
||||||
|
|
||||||
|
class ManyToManyThroughSource(models.Model):
|
||||||
|
name = models.CharField(max_length=100)
|
||||||
|
targets = models.ManyToManyField(ManyToManyThroughTarget,
|
||||||
|
related_name='sources',
|
||||||
|
through='ManyToManyThrough')
|
||||||
|
|
||||||
|
|
||||||
|
class ManyToManyThroughTargetSerializer(serializers.ModelSerializer):
|
||||||
|
class Meta:
|
||||||
|
model = ManyToManyThroughTarget
|
||||||
|
fields = ('id', 'name', 'sources')
|
||||||
|
|
||||||
|
|
||||||
|
class ManyToManyThroughSourceSerializer(serializers.ModelSerializer):
|
||||||
|
class Meta:
|
||||||
|
model = ManyToManyThroughSource
|
||||||
|
fields = ('id', 'name', 'targets')
|
||||||
|
|
||||||
|
|
||||||
|
class PKManyToManyThroughTests(TestCase):
|
||||||
|
def setUp(self):
|
||||||
|
self.source = ManyToManyThroughSource.objects.create(
|
||||||
|
name='through-source-1')
|
||||||
|
self.target = ManyToManyThroughTarget.objects.create(
|
||||||
|
name='through-target-1')
|
||||||
|
|
||||||
|
def test_many_to_many_create(self):
|
||||||
|
data = {'id': 2, 'name': 'source-2', 'targets': [self.target.pk]}
|
||||||
|
serializer = ManyToManyThroughSourceSerializer(data=data)
|
||||||
|
self.assertTrue(serializer.fields['targets'].read_only)
|
||||||
|
self.assertTrue(serializer.is_valid())
|
||||||
|
obj = serializer.save()
|
||||||
|
self.assertEqual(obj.name, 'source-2')
|
||||||
|
self.assertEqual(obj.targets.count(), 0)
|
||||||
|
|
||||||
|
def test_many_to_many_reverse_create(self):
|
||||||
|
data = {'id': 2, 'name': 'target-2', 'sources': [self.source.pk]}
|
||||||
|
serializer = ManyToManyThroughTargetSerializer(data=data)
|
||||||
|
self.assertTrue(serializer.fields['sources'].read_only)
|
||||||
|
self.assertTrue(serializer.is_valid())
|
||||||
|
serializer.save()
|
||||||
|
obj = serializer.save()
|
||||||
|
self.assertEqual(obj.name, 'target-2')
|
||||||
|
self.assertEqual(obj.sources.count(), 0)
|
||||||
|
|
||||||
|
|
||||||
|
# Regression tests for #694 (`source` attribute on related fields)
|
||||||
|
|
||||||
|
|
||||||
|
class PrimaryKeyRelatedFieldSourceTests(TestCase):
|
||||||
|
def test_related_manager_source(self):
|
||||||
|
"""
|
||||||
|
Relational fields should be able to use manager-returning methods as their source.
|
||||||
|
"""
|
||||||
|
BlogPost.objects.create(title='blah')
|
||||||
|
field = serializers.PrimaryKeyRelatedField(many=True, source='get_blogposts_manager')
|
||||||
|
|
||||||
|
class ClassWithManagerMethod(object):
|
||||||
|
def get_blogposts_manager(self):
|
||||||
|
return BlogPost.objects
|
||||||
|
|
||||||
|
obj = ClassWithManagerMethod()
|
||||||
|
value = field.field_to_native(obj, 'field_name')
|
||||||
|
self.assertEqual(value, [1])
|
||||||
|
|
||||||
|
def test_related_queryset_source(self):
|
||||||
|
"""
|
||||||
|
Relational fields should be able to use queryset-returning methods as their source.
|
||||||
|
"""
|
||||||
|
BlogPost.objects.create(title='blah')
|
||||||
|
field = serializers.PrimaryKeyRelatedField(many=True, source='get_blogposts_queryset')
|
||||||
|
|
||||||
|
class ClassWithQuerysetMethod(object):
|
||||||
|
def get_blogposts_queryset(self):
|
||||||
|
return BlogPost.objects.all()
|
||||||
|
|
||||||
|
obj = ClassWithQuerysetMethod()
|
||||||
|
value = field.field_to_native(obj, 'field_name')
|
||||||
|
self.assertEqual(value, [1])
|
||||||
|
|
||||||
|
def test_dotted_source(self):
|
||||||
|
"""
|
||||||
|
Source argument should support dotted.source notation.
|
||||||
|
"""
|
||||||
|
BlogPost.objects.create(title='blah')
|
||||||
|
field = serializers.PrimaryKeyRelatedField(many=True, source='a.b.c')
|
||||||
|
|
||||||
|
class ClassWithQuerysetMethod(object):
|
||||||
|
a = {
|
||||||
|
'b': {
|
||||||
|
'c': BlogPost.objects.all()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
obj = ClassWithQuerysetMethod()
|
||||||
|
value = field.field_to_native(obj, 'field_name')
|
||||||
|
self.assertEqual(value, [1])
|
||||||
|
|
|
@ -1,10 +1,12 @@
|
||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
|
from django.db import models
|
||||||
|
from django.db.models.fields import BLANK_CHOICE_DASH
|
||||||
from django.utils.datastructures import MultiValueDict
|
from django.utils.datastructures import MultiValueDict
|
||||||
from django.test import TestCase
|
from django.test import TestCase
|
||||||
from rest_framework import serializers
|
from rest_framework import serializers
|
||||||
from rest_framework.tests.models import (HasPositiveIntegerAsChoice, Album, ActionItem, Anchor, BasicModel,
|
from rest_framework.tests.models import (HasPositiveIntegerAsChoice, Album, ActionItem, Anchor, BasicModel,
|
||||||
BlankFieldModel, BlogPost, BlogPostComment, Book, CallableDefaultValueModel, DefaultValueModel,
|
BlankFieldModel, BlogPost, BlogPostComment, Book, CallableDefaultValueModel, DefaultValueModel,
|
||||||
ManyToManyModel, Person, ReadOnlyManyToManyModel, Photo)
|
ManyToManyModel, Person, ReadOnlyManyToManyModel, Photo, RESTFrameworkModel)
|
||||||
import datetime
|
import datetime
|
||||||
import pickle
|
import pickle
|
||||||
|
|
||||||
|
@ -43,6 +45,17 @@ class CommentSerializer(serializers.Serializer):
|
||||||
return instance
|
return instance
|
||||||
|
|
||||||
|
|
||||||
|
class NamesSerializer(serializers.Serializer):
|
||||||
|
first = serializers.CharField()
|
||||||
|
last = serializers.CharField(required=False, default='')
|
||||||
|
initials = serializers.CharField(required=False, default='')
|
||||||
|
|
||||||
|
|
||||||
|
class PersonIdentifierSerializer(serializers.Serializer):
|
||||||
|
ssn = serializers.CharField()
|
||||||
|
names = NamesSerializer(source='names', required=False)
|
||||||
|
|
||||||
|
|
||||||
class BookSerializer(serializers.ModelSerializer):
|
class BookSerializer(serializers.ModelSerializer):
|
||||||
isbn = serializers.RegexField(regex=r'^[0-9]{13}$', error_messages={'invalid': 'isbn has to be exact 13 numbers'})
|
isbn = serializers.RegexField(regex=r'^[0-9]{13}$', error_messages={'invalid': 'isbn has to be exact 13 numbers'})
|
||||||
|
|
||||||
|
@ -78,6 +91,17 @@ class PersonSerializer(serializers.ModelSerializer):
|
||||||
read_only_fields = ('age',)
|
read_only_fields = ('age',)
|
||||||
|
|
||||||
|
|
||||||
|
class NestedSerializer(serializers.Serializer):
|
||||||
|
info = serializers.Field()
|
||||||
|
|
||||||
|
|
||||||
|
class ModelSerializerWithNestedSerializer(serializers.ModelSerializer):
|
||||||
|
nested = NestedSerializer(source='*')
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = Person
|
||||||
|
|
||||||
|
|
||||||
class PersonSerializerInvalidReadOnly(serializers.ModelSerializer):
|
class PersonSerializerInvalidReadOnly(serializers.ModelSerializer):
|
||||||
"""
|
"""
|
||||||
Testing for #652.
|
Testing for #652.
|
||||||
|
@ -153,6 +177,42 @@ class BasicTests(TestCase):
|
||||||
self.assertFalse(serializer.object is expected)
|
self.assertFalse(serializer.object is expected)
|
||||||
self.assertEqual(serializer.data['sub_comment'], 'And Merry Christmas!')
|
self.assertEqual(serializer.data['sub_comment'], 'And Merry Christmas!')
|
||||||
|
|
||||||
|
def test_create_nested(self):
|
||||||
|
"""Test a serializer with nested data."""
|
||||||
|
names = {'first': 'John', 'last': 'Doe', 'initials': 'jd'}
|
||||||
|
data = {'ssn': '1234567890', 'names': names}
|
||||||
|
serializer = PersonIdentifierSerializer(data=data)
|
||||||
|
|
||||||
|
self.assertEqual(serializer.is_valid(), True)
|
||||||
|
self.assertEqual(serializer.object, data)
|
||||||
|
self.assertFalse(serializer.object is data)
|
||||||
|
self.assertEqual(serializer.data['names'], names)
|
||||||
|
|
||||||
|
def test_create_partial_nested(self):
|
||||||
|
"""Test a serializer with nested data which has missing fields."""
|
||||||
|
names = {'first': 'John'}
|
||||||
|
data = {'ssn': '1234567890', 'names': names}
|
||||||
|
serializer = PersonIdentifierSerializer(data=data)
|
||||||
|
|
||||||
|
expected_names = {'first': 'John', 'last': '', 'initials': ''}
|
||||||
|
data['names'] = expected_names
|
||||||
|
|
||||||
|
self.assertEqual(serializer.is_valid(), True)
|
||||||
|
self.assertEqual(serializer.object, data)
|
||||||
|
self.assertFalse(serializer.object is expected_names)
|
||||||
|
self.assertEqual(serializer.data['names'], expected_names)
|
||||||
|
|
||||||
|
def test_null_nested(self):
|
||||||
|
"""Test a serializer with a nonexistent nested field"""
|
||||||
|
data = {'ssn': '1234567890'}
|
||||||
|
serializer = PersonIdentifierSerializer(data=data)
|
||||||
|
|
||||||
|
self.assertEqual(serializer.is_valid(), True)
|
||||||
|
self.assertEqual(serializer.object, data)
|
||||||
|
self.assertFalse(serializer.object is data)
|
||||||
|
expected = {'ssn': '1234567890', 'names': None}
|
||||||
|
self.assertEqual(serializer.data, expected)
|
||||||
|
|
||||||
def test_update(self):
|
def test_update(self):
|
||||||
serializer = CommentSerializer(self.comment, data=self.data)
|
serializer = CommentSerializer(self.comment, data=self.data)
|
||||||
expected = self.comment
|
expected = self.comment
|
||||||
|
@ -369,6 +429,17 @@ class ValidationTests(TestCase):
|
||||||
except:
|
except:
|
||||||
self.fail('Wrong exception type thrown.')
|
self.fail('Wrong exception type thrown.')
|
||||||
|
|
||||||
|
def test_writable_star_source_on_nested_serializer(self):
|
||||||
|
"""
|
||||||
|
Assert that a nested serializer instantiated with source='*' correctly
|
||||||
|
expands the data into the outer serializer.
|
||||||
|
"""
|
||||||
|
serializer = ModelSerializerWithNestedSerializer(data={
|
||||||
|
'name': 'marko',
|
||||||
|
'nested': {'info': 'hi'}},
|
||||||
|
)
|
||||||
|
self.assertEqual(serializer.is_valid(), True)
|
||||||
|
|
||||||
|
|
||||||
class CustomValidationTests(TestCase):
|
class CustomValidationTests(TestCase):
|
||||||
class CommentSerializerWithFieldValidator(CommentSerializer):
|
class CommentSerializerWithFieldValidator(CommentSerializer):
|
||||||
|
@ -871,23 +942,6 @@ class RelatedTraversalTest(TestCase):
|
||||||
|
|
||||||
self.assertEqual(serializer.data, expected)
|
self.assertEqual(serializer.data, expected)
|
||||||
|
|
||||||
def test_queryset_nested_traversal(self):
|
|
||||||
"""
|
|
||||||
Relational fields should be able to use methods as their source.
|
|
||||||
"""
|
|
||||||
BlogPost.objects.create(title='blah')
|
|
||||||
|
|
||||||
class QuerysetMethodSerializer(serializers.Serializer):
|
|
||||||
blogposts = serializers.RelatedField(many=True, source='get_all_blogposts')
|
|
||||||
|
|
||||||
class ClassWithQuerysetMethod(object):
|
|
||||||
def get_all_blogposts(self):
|
|
||||||
return BlogPost.objects
|
|
||||||
|
|
||||||
obj = ClassWithQuerysetMethod()
|
|
||||||
serializer = QuerysetMethodSerializer(obj)
|
|
||||||
self.assertEqual(serializer.data, {'blogposts': ['BlogPost object']})
|
|
||||||
|
|
||||||
|
|
||||||
class SerializerMethodFieldTests(TestCase):
|
class SerializerMethodFieldTests(TestCase):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
|
@ -1018,6 +1072,130 @@ class SerializerPickleTests(TestCase):
|
||||||
repr(pickle.loads(pickle.dumps(data, 0)))
|
repr(pickle.loads(pickle.dumps(data, 0)))
|
||||||
|
|
||||||
|
|
||||||
|
# test for issue #725
|
||||||
|
class SeveralChoicesModel(models.Model):
|
||||||
|
color = models.CharField(
|
||||||
|
max_length=10,
|
||||||
|
choices=[('red', 'Red'), ('green', 'Green'), ('blue', 'Blue')],
|
||||||
|
blank=False
|
||||||
|
)
|
||||||
|
drink = models.CharField(
|
||||||
|
max_length=10,
|
||||||
|
choices=[('beer', 'Beer'), ('wine', 'Wine'), ('cider', 'Cider')],
|
||||||
|
blank=False,
|
||||||
|
default='beer'
|
||||||
|
)
|
||||||
|
os = models.CharField(
|
||||||
|
max_length=10,
|
||||||
|
choices=[('linux', 'Linux'), ('osx', 'OSX'), ('windows', 'Windows')],
|
||||||
|
blank=True
|
||||||
|
)
|
||||||
|
music_genre = models.CharField(
|
||||||
|
max_length=10,
|
||||||
|
choices=[('rock', 'Rock'), ('metal', 'Metal'), ('grunge', 'Grunge')],
|
||||||
|
blank=True,
|
||||||
|
default='metal'
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class SerializerChoiceFields(TestCase):
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
super(SerializerChoiceFields, self).setUp()
|
||||||
|
|
||||||
|
class SeveralChoicesSerializer(serializers.ModelSerializer):
|
||||||
|
class Meta:
|
||||||
|
model = SeveralChoicesModel
|
||||||
|
fields = ('color', 'drink', 'os', 'music_genre')
|
||||||
|
|
||||||
|
self.several_choices_serializer = SeveralChoicesSerializer
|
||||||
|
|
||||||
|
def test_choices_blank_false_not_default(self):
|
||||||
|
serializer = self.several_choices_serializer()
|
||||||
|
self.assertEqual(
|
||||||
|
serializer.fields['color'].choices,
|
||||||
|
[('red', 'Red'), ('green', 'Green'), ('blue', 'Blue')]
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_choices_blank_false_with_default(self):
|
||||||
|
serializer = self.several_choices_serializer()
|
||||||
|
self.assertEqual(
|
||||||
|
serializer.fields['drink'].choices,
|
||||||
|
[('beer', 'Beer'), ('wine', 'Wine'), ('cider', 'Cider')]
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_choices_blank_true_not_default(self):
|
||||||
|
serializer = self.several_choices_serializer()
|
||||||
|
self.assertEqual(
|
||||||
|
serializer.fields['os'].choices,
|
||||||
|
BLANK_CHOICE_DASH + [('linux', 'Linux'), ('osx', 'OSX'), ('windows', 'Windows')]
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_choices_blank_true_with_default(self):
|
||||||
|
serializer = self.several_choices_serializer()
|
||||||
|
self.assertEqual(
|
||||||
|
serializer.fields['music_genre'].choices,
|
||||||
|
BLANK_CHOICE_DASH + [('rock', 'Rock'), ('metal', 'Metal'), ('grunge', 'Grunge')]
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# Regression tests for #675
|
||||||
|
class Ticket(models.Model):
|
||||||
|
assigned = models.ForeignKey(
|
||||||
|
Person, related_name='assigned_tickets')
|
||||||
|
reviewer = models.ForeignKey(
|
||||||
|
Person, blank=True, null=True, related_name='reviewed_tickets')
|
||||||
|
|
||||||
|
|
||||||
|
class SerializerRelatedChoicesTest(TestCase):
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
super(SerializerRelatedChoicesTest, self).setUp()
|
||||||
|
|
||||||
|
class RelatedChoicesSerializer(serializers.ModelSerializer):
|
||||||
|
class Meta:
|
||||||
|
model = Ticket
|
||||||
|
fields = ('assigned', 'reviewer')
|
||||||
|
|
||||||
|
self.related_fields_serializer = RelatedChoicesSerializer
|
||||||
|
|
||||||
|
def test_empty_queryset_required(self):
|
||||||
|
serializer = self.related_fields_serializer()
|
||||||
|
self.assertEqual(serializer.fields['assigned'].queryset.count(), 0)
|
||||||
|
self.assertEqual(
|
||||||
|
[x for x in serializer.fields['assigned'].widget.choices],
|
||||||
|
[]
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_empty_queryset_not_required(self):
|
||||||
|
serializer = self.related_fields_serializer()
|
||||||
|
self.assertEqual(serializer.fields['reviewer'].queryset.count(), 0)
|
||||||
|
self.assertEqual(
|
||||||
|
[x for x in serializer.fields['reviewer'].widget.choices],
|
||||||
|
[('', '---------')]
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_with_some_persons_required(self):
|
||||||
|
Person.objects.create(name="Lionel Messi")
|
||||||
|
Person.objects.create(name="Xavi Hernandez")
|
||||||
|
serializer = self.related_fields_serializer()
|
||||||
|
self.assertEqual(serializer.fields['assigned'].queryset.count(), 2)
|
||||||
|
self.assertEqual(
|
||||||
|
[x for x in serializer.fields['assigned'].widget.choices],
|
||||||
|
[(1, 'Person object - 1'), (2, 'Person object - 2')]
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_with_some_persons_not_required(self):
|
||||||
|
Person.objects.create(name="Lionel Messi")
|
||||||
|
Person.objects.create(name="Xavi Hernandez")
|
||||||
|
serializer = self.related_fields_serializer()
|
||||||
|
self.assertEqual(serializer.fields['reviewer'].queryset.count(), 2)
|
||||||
|
self.assertEqual(
|
||||||
|
[x for x in serializer.fields['reviewer'].widget.choices],
|
||||||
|
[('', '---------'), (1, 'Person object - 1'), (2, 'Person object - 2')]
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class DepthTest(TestCase):
|
class DepthTest(TestCase):
|
||||||
def test_implicit_nesting(self):
|
def test_implicit_nesting(self):
|
||||||
|
|
||||||
|
@ -1143,3 +1321,84 @@ class DeserializeListTestCase(TestCase):
|
||||||
self.assertFalse(serializer.is_valid())
|
self.assertFalse(serializer.is_valid())
|
||||||
expected = [{}, {'email': ['This field is required.']}, {}]
|
expected = [{}, {'email': ['This field is required.']}, {}]
|
||||||
self.assertEqual(serializer.errors, expected)
|
self.assertEqual(serializer.errors, expected)
|
||||||
|
|
||||||
|
|
||||||
|
class AttributeMappingOnAutogeneratedFieldsTests(TestCase):
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
class AMOAFModel(RESTFrameworkModel):
|
||||||
|
char_field = models.CharField(max_length=1024, blank=True)
|
||||||
|
comma_separated_integer_field = models.CommaSeparatedIntegerField(max_length=1024, blank=True)
|
||||||
|
decimal_field = models.DecimalField(max_digits=64, decimal_places=32, blank=True)
|
||||||
|
email_field = models.EmailField(max_length=1024, blank=True)
|
||||||
|
file_field = models.FileField(max_length=1024, blank=True)
|
||||||
|
image_field = models.ImageField(max_length=1024, blank=True)
|
||||||
|
slug_field = models.SlugField(max_length=1024, blank=True)
|
||||||
|
url_field = models.URLField(max_length=1024, blank=True)
|
||||||
|
|
||||||
|
class AMOAFSerializer(serializers.ModelSerializer):
|
||||||
|
class Meta:
|
||||||
|
model = AMOAFModel
|
||||||
|
|
||||||
|
self.serializer_class = AMOAFSerializer
|
||||||
|
self.fields_attributes = {
|
||||||
|
'char_field': [
|
||||||
|
('max_length', 1024),
|
||||||
|
],
|
||||||
|
'comma_separated_integer_field': [
|
||||||
|
('max_length', 1024),
|
||||||
|
],
|
||||||
|
'decimal_field': [
|
||||||
|
('max_digits', 64),
|
||||||
|
('decimal_places', 32),
|
||||||
|
],
|
||||||
|
'email_field': [
|
||||||
|
('max_length', 1024),
|
||||||
|
],
|
||||||
|
'file_field': [
|
||||||
|
('max_length', 1024),
|
||||||
|
],
|
||||||
|
'image_field': [
|
||||||
|
('max_length', 1024),
|
||||||
|
],
|
||||||
|
'slug_field': [
|
||||||
|
('max_length', 1024),
|
||||||
|
],
|
||||||
|
'url_field': [
|
||||||
|
('max_length', 1024),
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
def field_test(self, field):
|
||||||
|
serializer = self.serializer_class(data={})
|
||||||
|
self.assertEqual(serializer.is_valid(), True)
|
||||||
|
|
||||||
|
for attribute in self.fields_attributes[field]:
|
||||||
|
self.assertEqual(
|
||||||
|
getattr(serializer.fields[field], attribute[0]),
|
||||||
|
attribute[1]
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_char_field(self):
|
||||||
|
self.field_test('char_field')
|
||||||
|
|
||||||
|
def test_comma_separated_integer_field(self):
|
||||||
|
self.field_test('comma_separated_integer_field')
|
||||||
|
|
||||||
|
def test_decimal_field(self):
|
||||||
|
self.field_test('decimal_field')
|
||||||
|
|
||||||
|
def test_email_field(self):
|
||||||
|
self.field_test('email_field')
|
||||||
|
|
||||||
|
def test_file_field(self):
|
||||||
|
self.field_test('file_field')
|
||||||
|
|
||||||
|
def test_image_field(self):
|
||||||
|
self.field_test('image_field')
|
||||||
|
|
||||||
|
def test_slug_field(self):
|
||||||
|
self.field_test('slug_field')
|
||||||
|
|
||||||
|
def test_url_field(self):
|
||||||
|
self.field_test('url_field')
|
||||||
|
|
Loading…
Reference in New Issue
Block a user