Merge branch 'master' of git://github.com/tomchristie/django-rest-framework

This commit is contained in:
Karol Majta 2013-05-18 16:56:38 +02:00
commit 5bebd29f11
18 changed files with 988 additions and 217 deletions

View File

@ -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"

View File

@ -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`.

View File

@ -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

View File

@ -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.

View File

@ -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

View File

@ -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

View File

@ -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__)

View File

@ -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'),
) )

View File

@ -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:

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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">

View File

@ -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>

View File

@ -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)

View File

@ -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'])

View File

@ -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/'])

View File

@ -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])

View File

@ -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')