diff --git a/.travis.yml b/.travis.yml
index 205feef92..3a7c2d7ad 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -7,9 +7,9 @@ python:
- "3.3"
env:
- - DJANGO="django==1.5 --use-mirrors"
- - DJANGO="django==1.4.3 --use-mirrors"
- - DJANGO="django==1.3.5 --use-mirrors"
+ - DJANGO="django==1.5.1 --use-mirrors"
+ - DJANGO="django==1.4.5 --use-mirrors"
+ - DJANGO="django==1.3.7 --use-mirrors"
install:
- 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-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.6a1 --use-mirrors; fi"
+ - "if [[ ${DJANGO::11} != 'django==1.3' ]]; then pip install django-filter==0.6 --use-mirrors; fi"
- export PYTHONPATH=.
script:
@@ -27,10 +27,11 @@ script:
matrix:
exclude:
- python: "3.2"
- env: DJANGO="django==1.4.3 --use-mirrors"
+ env: DJANGO="django==1.4.5 --use-mirrors"
- python: "3.2"
- env: DJANGO="django==1.3.5 --use-mirrors"
+ env: DJANGO="django==1.3.7 --use-mirrors"
- python: "3.3"
- env: DJANGO="django==1.4.3 --use-mirrors"
+ env: DJANGO="django==1.4.5 --use-mirrors"
- python: "3.3"
- env: DJANGO="django==1.3.5 --use-mirrors"
+ env: DJANGO="django==1.3.7 --use-mirrors"
+
diff --git a/docs/api-guide/relations.md b/docs/api-guide/relations.md
index 155c89de3..99fe10834 100644
--- a/docs/api-guide/relations.md
+++ b/docs/api-guide/relations.md
@@ -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].
+## 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
If you have very specific requirements for the style of your hyperlinked relationships you can override `HyperlinkedRelatedField`.
diff --git a/docs/api-guide/renderers.md b/docs/api-guide/renderers.md
index ed733c653..b9a9fd7a3 100644
--- a/docs/api-guide/renderers.md
+++ b/docs/api-guide/renderers.md
@@ -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.
+**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
diff --git a/docs/topics/browsable-api.md b/docs/topics/browsable-api.md
index 8ee018249..65f76abcf 100644
--- a/docs/topics/browsable-api.md
+++ b/docs/topics/browsable-api.md
@@ -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.
+Full Example
+
+ {% extends "rest_framework/base.html" %}
+
+ {% block bootstrap_theme %}
+
+ {% endblock %}
+
+ {% block bootstrap_navbar_variant %}{% endblock %}
+
+
For more specific CSS tweaks, use the `style` block instead.
diff --git a/docs/topics/credits.md b/docs/topics/credits.md
index 8151b4d3a..d805c0c1d 100644
--- a/docs/topics/credits.md
+++ b/docs/topics/credits.md
@@ -124,6 +124,12 @@ The following people have helped make REST framework great.
* Marlon Bailey - [avinash240]
* James Summerfield - [jsummerfield]
* 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.
@@ -284,3 +290,9 @@ You can also contact [@_tomchristie][twitter] directly on twitter.
[avinash240]: https://github.com/avinash240
[jsummerfield]: https://github.com/jsummerfield
[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
diff --git a/rest_framework/fields.py b/rest_framework/fields.py
index e4da14566..46fb31528 100644
--- a/rest_framework/fields.py
+++ b/rest_framework/fields.py
@@ -15,10 +15,12 @@ import warnings
from django.core import validators
from django.core.exceptions import ValidationError
from django.conf import settings
+from django.db.models.fields import BLANK_CHOICE_DASH
from django import forms
from django.forms import widgets
from django.utils.encoding import is_protected_type
from django.utils.translation import ugettext_lazy as _
+from django.utils.datastructures import SortedDict
from rest_framework import ISO_8601
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.
"""
if isinstance(obj, dict):
- val = obj[attr_name]
+ val = obj.get(attr_name)
else:
val = getattr(obj, attr_name)
@@ -176,7 +178,11 @@ class Field(object):
elif hasattr(value, '__iter__') and not isinstance(value, (dict, six.string_types)):
return [self.to_native(item) for item in value]
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)
def attributes(self):
@@ -384,7 +390,6 @@ class URLField(CharField):
type_name = 'URLField'
def __init__(self, **kwargs):
- kwargs['max_length'] = kwargs.get('max_length', 200)
kwargs['validators'] = [validators.URLValidator()]
super(URLField, self).__init__(**kwargs)
@@ -393,7 +398,6 @@ class SlugField(CharField):
type_name = 'SlugField'
def __init__(self, *args, **kwargs):
- kwargs['max_length'] = kwargs.get('max_length', 50)
super(SlugField, self).__init__(*args, **kwargs)
@@ -409,6 +413,8 @@ class ChoiceField(WritableField):
def __init__(self, choices=(), *args, **kwargs):
super(ChoiceField, self).__init__(*args, **kwargs)
self.choices = choices
+ if not self.required:
+ self.choices = BLANK_CHOICE_DASH + self.choices
def _get_choices(self):
return self._choices
diff --git a/rest_framework/relations.py b/rest_framework/relations.py
index c4b790d47..c4271e337 100644
--- a/rest_framework/relations.py
+++ b/rest_framework/relations.py
@@ -8,6 +8,7 @@ from __future__ import unicode_literals
from django.core.exceptions import ObjectDoesNotExist, ValidationError
from django.core.urlresolvers import resolve, get_script_prefix, NoReverseMatch
from django import forms
+from django.db.models.fields import BLANK_CHOICE_DASH
from django.forms import widgets
from django.forms.models import ModelChoiceIterator
from django.utils.translation import ugettext_lazy as _
@@ -47,7 +48,7 @@ class RelatedField(WritableField):
DeprecationWarning, stacklevel=2)
kwargs['required'] = not kwargs.pop('null')
- self.queryset = kwargs.pop('queryset', None)
+ queryset = kwargs.pop('queryset', None)
self.many = kwargs.pop('many', self.many)
if self.many:
self.widget = self.many_widget
@@ -56,6 +57,11 @@ class RelatedField(WritableField):
kwargs['read_only'] = kwargs.pop('read_only', self.read_only)
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):
super(RelatedField, self).initialize(parent, field_name)
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):
if self.many:
# To-many relationship
- try:
+
+ queryset = None
+ if not self.source:
# Prefer obj.serializable_value for performance reasons
- queryset = obj.serializable_value(self.source or field_name)
- except AttributeError:
+ try:
+ queryset = obj.serializable_value(field_name)
+ except AttributeError:
+ pass
+ if queryset is None:
# 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
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')
try:
- http_prefix = value.startswith('http:') or value.startswith('https:')
+ http_prefix = value.startswith(('http:', 'https:'))
except AttributeError:
msg = self.error_messages['incorrect_type']
raise ValidationError(msg % type(value).__name__)
diff --git a/rest_framework/runtests/settings.py b/rest_framework/runtests/settings.py
index 9b519f271..9dd7b545e 100644
--- a/rest_framework/runtests/settings.py
+++ b/rest_framework/runtests/settings.py
@@ -4,6 +4,8 @@ DEBUG = True
TEMPLATE_DEBUG = DEBUG
DEBUG_PROPAGATE_EXCEPTIONS = True
+ALLOWED_HOSTS = ['*']
+
ADMINS = (
# ('Your Name', 'your_email@domain.com'),
)
diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py
index 942ab3994..425dd18cc 100644
--- a/rest_framework/serializers.py
+++ b/rest_framework/serializers.py
@@ -378,23 +378,27 @@ class BaseSerializer(WritableField):
# Set the serializer object if it exists
obj = getattr(self.parent.object, field_name) if self.parent.object else None
- if value in (None, ''):
- into[(self.source or field_name)] = None
+ if self.source == '*':
+ if value:
+ into.update(value)
else:
- kwargs = {
- '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
+ if value in (None, ''):
+ into[(self.source or field_name)] = None
else:
- # Propagate errors up to our parent
- raise NestedValidationError(serializer.errors)
+ kwargs = {
+ '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):
"""
@@ -587,11 +591,16 @@ class ModelSerializer(Serializer):
forward_rels += [field for field in opts.many_to_many if field.serialize]
for model_field in forward_rels:
+ has_through_model = False
+
if model_field.rel:
to_many = isinstance(model_field,
models.fields.related.ManyToManyField)
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 len(inspect.getargspec(self.get_nested_field).args) == 2:
warnings.warn(
@@ -620,6 +629,9 @@ class ModelSerializer(Serializer):
field = self.get_field(model_field)
if field:
+ if has_through_model:
+ field.read_only = True
+
ret[model_field.name] = field
# Deal with reverse relationships
@@ -637,6 +649,12 @@ class ModelSerializer(Serializer):
continue
related_model = relation.model
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:
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)
if field:
+ if has_through_model:
+ field.read_only = True
+
ret[accessor_name] = field
# 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.
"""
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
if isinstance(model_field, models.AutoField) or not model_field.editable:
kwargs['read_only'] = True
- if has_default:
+ if model_field.has_default():
kwargs['default'] = model_field.get_default()
if issubclass(model_field.__class__, models.TextField):
@@ -730,6 +750,22 @@ class ModelSerializer(Serializer):
if model_field.help_text is not None:
kwargs['help_text'] = model_field.help_text
+ 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:
return self.field_mapping[model_field.__class__](**kwargs)
except KeyError:
diff --git a/rest_framework/static/rest_framework/css/bootstrap-tweaks.css b/rest_framework/static/rest_framework/css/bootstrap-tweaks.css
index c650ef2e9..9b5201564 100644
--- a/rest_framework/static/rest_framework/css/bootstrap-tweaks.css
+++ b/rest_framework/static/rest_framework/css/bootstrap-tweaks.css
@@ -19,4 +19,163 @@ a single block in the template.
.navbar-inverse .brand:hover a {
color: white;
text-decoration: none;
-}
\ No newline at end of file
+}
+
+/* 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;
+}
diff --git a/rest_framework/static/rest_framework/css/default.css b/rest_framework/static/rest_framework/css/default.css
index d806267bc..0261a3038 100644
--- a/rest_framework/static/rest_framework/css/default.css
+++ b/rest_framework/static/rest_framework/css/default.css
@@ -69,152 +69,3 @@ pre {
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;
-}
-
diff --git a/rest_framework/templates/rest_framework/base.html b/rest_framework/templates/rest_framework/base.html
index 4410f285f..9d939e738 100644
--- a/rest_framework/templates/rest_framework/base.html
+++ b/rest_framework/templates/rest_framework/base.html
@@ -13,8 +13,10 @@
{% block title %}Django REST framework{% endblock %}
{% block style %}
- {% block bootstrap_theme %}{% endblock %}
-
+ {% block bootstrap_theme %}
+
+
+ {% endblock %}
{% endblock %}
@@ -30,8 +32,8 @@
-
- {% block branding %}Django REST framework {{ version }}{% endblock %}
+
+ {% block branding %}Django REST framework {{ version }}{% endblock %}
{% block userlinks %}
@@ -109,8 +111,7 @@
{{ description }}
-
-
+
{{ request.method }} {{ request.get_full_path }}
diff --git a/rest_framework/templates/rest_framework/login_base.html b/rest_framework/templates/rest_framework/login_base.html
index a3e73b6b6..be9a0072a 100644
--- a/rest_framework/templates/rest_framework/login_base.html
+++ b/rest_framework/templates/rest_framework/login_base.html
@@ -4,8 +4,10 @@
{% block style %}
- {% block bootstrap_theme %}
{% endblock %}
-
+ {% block bootstrap_theme %}
+
+
+ {% endblock %}
{% endblock %}
diff --git a/rest_framework/tests/fields.py b/rest_framework/tests/fields.py
index 3cdfa0f62..dad69975f 100644
--- a/rest_framework/tests/fields.py
+++ b/rest_framework/tests/fields.py
@@ -2,15 +2,15 @@
General serializer field tests.
"""
from __future__ import unicode_literals
+from django.utils.datastructures import SortedDict
import datetime
from decimal import Decimal
-
from django.db import models
from django.test import TestCase
from django.core import validators
-
from rest_framework import serializers
from rest_framework.serializers import Serializer
+from rest_framework.tests.models import RESTFrameworkModel
class TimestampedModel(models.Model):
@@ -63,6 +63,20 @@ class BasicFieldTests(TestCase):
serializer = CharPrimaryKeyModelSerializer()
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):
"""
@@ -645,4 +659,153 @@ class DecimalFieldTest(TestCase):
s = DecimalSerializer(data={'decimal_field': '12345.6'})
self.assertFalse(s.is_valid())
- self.assertEqual(s.errors, {'decimal_field': ['Ensure that there are no more than 4 digits in total.']})
\ No newline at end of file
+ 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)
diff --git a/rest_framework/tests/relations.py b/rest_framework/tests/relations.py
index cbf93c65e..d19219c90 100644
--- a/rest_framework/tests/relations.py
+++ b/rest_framework/tests/relations.py
@@ -5,6 +5,7 @@ from __future__ import unicode_literals
from django.db import models
from django.test import TestCase
from rest_framework import serializers
+from rest_framework.tests.models import BlogPost
class NullModel(models.Model):
@@ -33,7 +34,7 @@ class FieldTests(TestCase):
self.assertRaises(serializers.ValidationError, field.from_native, [])
-class TestManyRelateMixin(TestCase):
+class TestManyRelatedMixin(TestCase):
def test_missing_many_to_many_related_field(self):
'''
Regression test for #632
@@ -45,3 +46,55 @@ class TestManyRelateMixin(TestCase):
into = {}
field.field_from_native({}, None, 'field_name', into)
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'])
diff --git a/rest_framework/tests/relations_hyperlink.py b/rest_framework/tests/relations_hyperlink.py
index b1eed9a76..b3efbf524 100644
--- a/rest_framework/tests/relations_hyperlink.py
+++ b/rest_framework/tests/relations_hyperlink.py
@@ -4,6 +4,7 @@ from django.test.client import RequestFactory
from rest_framework import serializers
from rest_framework.compat import patterns, url
from rest_framework.tests.models import (
+ BlogPost,
ManyToManyTarget, ManyToManySource, ForeignKeyTarget, ForeignKeySource,
NullableForeignKeySource, OneToOneTarget, NullableOneToOneSource
)
@@ -16,6 +17,7 @@ def dummy_view(request, pk):
pass
urlpatterns = patterns('',
+ url(r'^dummyurl/(?P
[0-9]+)/$', dummy_view, name='dummy-url'),
url(r'^manytomanysource/(?P[0-9]+)/$', dummy_view, name='manytomanysource-detail'),
url(r'^manytomanytarget/(?P[0-9]+)/$', dummy_view, name='manytomanytarget-detail'),
url(r'^foreignkeysource/(?P[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},
]
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/'])
diff --git a/rest_framework/tests/relations_pk.py b/rest_framework/tests/relations_pk.py
index 5ce8b5671..e2a1b8152 100644
--- a/rest_framework/tests/relations_pk.py
+++ b/rest_framework/tests/relations_pk.py
@@ -1,7 +1,11 @@
from __future__ import unicode_literals
+from django.db import models
from django.test import TestCase
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
@@ -124,6 +128,7 @@ class PKManyToManyTests(TestCase):
# Ensure source 4 is added, and everything else is as expected
queryset = ManyToManySource.objects.all()
serializer = ManyToManySourceSerializer(queryset, many=True)
+ self.assertFalse(serializer.fields['targets'].read_only)
expected = [
{'id': 1, 'name': 'source-1', 'targets': [1]},
{'id': 2, 'name': 'source-2', 'targets': [1, 2]},
@@ -135,6 +140,7 @@ class PKManyToManyTests(TestCase):
def test_reverse_many_to_many_create(self):
data = {'id': 4, 'name': 'target-4', 'sources': [1, 3]}
serializer = ManyToManyTargetSerializer(data=data)
+ self.assertFalse(serializer.fields['sources'].read_only)
self.assertTrue(serializer.is_valid())
obj = serializer.save()
self.assertEqual(serializer.data, data)
@@ -421,3 +427,116 @@ class PKNullableOneToOneTests(TestCase):
{'id': 2, 'name': 'target-2', 'nullable_source': 1},
]
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])
diff --git a/rest_framework/tests/serializer.py b/rest_framework/tests/serializer.py
index 59859ad7a..5685c0615 100644
--- a/rest_framework/tests/serializer.py
+++ b/rest_framework/tests/serializer.py
@@ -1,10 +1,12 @@
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.test import TestCase
from rest_framework import serializers, fields, relations
from rest_framework.tests.models import (HasPositiveIntegerAsChoice, Album, ActionItem, Anchor, BasicModel,
BlankFieldModel, BlogPost, BlogPostComment, Book, CallableDefaultValueModel, DefaultValueModel,
- ManyToManyModel, Person, ReadOnlyManyToManyModel, Photo)
+ ManyToManyModel, Person, ReadOnlyManyToManyModel, Photo, RESTFrameworkModel)
from rest_framework.tests.models import BasicModelSerializer
import datetime
import pickle
@@ -44,6 +46,17 @@ class CommentSerializer(serializers.Serializer):
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):
isbn = serializers.RegexField(regex=r'^[0-9]{13}$', error_messages={'invalid': 'isbn has to be exact 13 numbers'})
@@ -79,6 +92,17 @@ class PersonSerializer(serializers.ModelSerializer):
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):
"""
Testing for #652.
@@ -154,6 +178,42 @@ class BasicTests(TestCase):
self.assertFalse(serializer.object is expected)
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):
serializer = CommentSerializer(self.comment, data=self.data)
expected = self.comment
@@ -370,6 +430,17 @@ class ValidationTests(TestCase):
except:
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 CommentSerializerWithFieldValidator(CommentSerializer):
@@ -872,23 +943,6 @@ class RelatedTraversalTest(TestCase):
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):
def setUp(self):
@@ -1019,6 +1073,130 @@ class SerializerPickleTests(TestCase):
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):
def test_implicit_nesting(self):
@@ -1169,3 +1347,84 @@ class FieldLabelTest(TestCase):
self.assertEquals(u'Help', fields.CharField(label='Label', help_text='Help').help_text)
self.assertEquals(u'Label', relations.ManyHyperlinkedRelatedField(view_name='fake', label='Label', help_text='Help').label)
+
+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')
+