Merge branch 'master' of git://github.com/tomchristie/django-rest-framework into issue-192-expose-fields-for-options

This commit is contained in:
Nikolaus Schlemm 2013-05-18 17:13:59 +02:00
commit b225b1d5c9
14 changed files with 608 additions and 178 deletions

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

@ -127,6 +127,9 @@ The following people have helped make REST framework great.
* Craig de Stigter - [craigds] * Craig de Stigter - [craigds]
* Pablo Recio - [pyriku] * Pablo Recio - [pyriku]
* Brian Zambrano - [brianz] * 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.
@ -290,3 +293,6 @@ You can also contact [@_tomchristie][twitter] directly on twitter.
[craigds]: https://github.com/craigds [craigds]: https://github.com/craigds
[pyriku]: https://github.com/pyriku [pyriku]: https://github.com/pyriku
[brianz]: https://github.com/brianz [brianz]: https://github.com/brianz
[grimborg]: https://github.com/grimborg
[ryankask]: https://github.com/ryankask
[andymckay]: https://github.com/andymckay

View File

@ -383,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)
@ -392,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)

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

@ -378,6 +378,10 @@ 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 self.source == '*':
if value:
into.update(value)
else:
if value in (None, ''): if value in (None, ''):
into[(self.source or field_name)] = None into[(self.source or field_name)] = None
else: else:
@ -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
@ -723,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

@ -20,3 +20,162 @@ a single block in the template.
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.min.css" %}"/>
<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-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.min.css" %}"/>
<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-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

@ -10,6 +10,7 @@ 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):
@ -685,3 +686,126 @@ class ChoiceFieldTests(TestCase):
""" """
f = serializers.ChoiceField(required=False, choices=self.SAMPLE_CHOICES) f = serializers.ChoiceField(required=False, choices=self.SAMPLE_CHOICES)
self.assertEqual(f.choices, models.fields.BLANK_CHOICE_DASH + 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

@ -1,4 +1,5 @@
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 ( from rest_framework.tests.models import (
@ -127,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]},
@ -138,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)
@ -426,8 +429,69 @@ class PKNullableOneToOneTests(TestCase):
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) # Regression tests for #694 (`source` attribute on related fields)
class PrimaryKeyRelatedFieldSourceTests(TestCase): class PrimaryKeyRelatedFieldSourceTests(TestCase):
def test_related_manager_source(self): def test_related_manager_source(self):
""" """

View File

@ -6,7 +6,7 @@ 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
@ -91,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.
@ -418,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):
@ -1117,6 +1139,63 @@ class SerializerChoiceFields(TestCase):
) )
# 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):
@ -1242,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')