diff options
| author | Ryan Kaskel | 2013-05-18 16:26:17 +0100 |
|---|---|---|
| committer | Ryan Kaskel | 2013-05-18 16:26:17 +0100 |
| commit | 33f702d306093126b9b033bb2be0cf9391629c6b (patch) | |
| tree | 4b66faa5f6219fd9409bb0e6518f9d90172276ea | |
| parent | 10e451a85a034d1158148f54e98147e81454c2ed (diff) | |
| parent | 3f47eb7a77fcc735782dd1bf8e8e053e26417ea1 (diff) | |
| download | django-rest-framework-33f702d306093126b9b033bb2be0cf9391629c6b.tar.bz2 | |
Merge latest changes from master.
| -rw-r--r-- | docs/api-guide/relations.md | 9 | ||||
| -rw-r--r-- | docs/api-guide/renderers.md | 2 | ||||
| -rw-r--r-- | docs/topics/browsable-api.md | 11 | ||||
| -rw-r--r-- | docs/topics/credits.md | 6 | ||||
| -rw-r--r-- | rest_framework/fields.py | 2 | ||||
| -rw-r--r-- | rest_framework/relations.py | 10 | ||||
| -rw-r--r-- | rest_framework/serializers.py | 67 | ||||
| -rw-r--r-- | rest_framework/static/rest_framework/css/bootstrap-tweaks.css | 161 | ||||
| -rw-r--r-- | rest_framework/static/rest_framework/css/default.css | 149 | ||||
| -rw-r--r-- | rest_framework/templates/rest_framework/base.html | 13 | ||||
| -rw-r--r-- | rest_framework/templates/rest_framework/login_base.html | 6 | ||||
| -rw-r--r-- | rest_framework/tests/fields.py | 124 | ||||
| -rw-r--r-- | rest_framework/tests/relations_pk.py | 64 | ||||
| -rw-r--r-- | rest_framework/tests/serializer.py | 162 |
14 files changed, 608 insertions, 178 deletions
diff --git a/docs/api-guide/relations.md b/docs/api-guide/relations.md index 155c89de..99fe1083 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 ed733c65..b9a9fd7a 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 8ee01824..65f76abc 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 %} + <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. diff --git a/docs/topics/credits.md b/docs/topics/credits.md index 5998b4ca..d805c0c1 100644 --- a/docs/topics/credits.md +++ b/docs/topics/credits.md @@ -127,6 +127,9 @@ The following people have helped make REST framework great. * 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. @@ -290,3 +293,6 @@ You can also contact [@_tomchristie][twitter] directly on twitter. [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 c3ac6281..d5cf30e4 100644 --- a/rest_framework/fields.py +++ b/rest_framework/fields.py @@ -394,7 +394,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) @@ -403,7 +402,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) diff --git a/rest_framework/relations.py b/rest_framework/relations.py index 884b954c..c4271e33 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: @@ -442,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/serializers.py b/rest_framework/serializers.py index 500bb306..ff5eb873 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 @@ -723,6 +744,22 @@ class ModelSerializer(Serializer): kwargs['choices'] = model_field.flatchoices 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: 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 c650ef2e..9b520156 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 d806267b..0261a303 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 4410f285..9d939e73 100644 --- a/rest_framework/templates/rest_framework/base.html +++ b/rest_framework/templates/rest_framework/base.html @@ -13,8 +13,10 @@ <title>{% block title %}Django REST framework{% endblock %}</title> {% block style %} - {% block bootstrap_theme %}<link rel="stylesheet" type="text/css" href="{% static "rest_framework/css/bootstrap.min.css" %}"/>{% endblock %} - <link rel="stylesheet" type="text/css" href="{% static "rest_framework/css/bootstrap-tweaks.css" %}"/> + {% 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" %}"/> + {% 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/default.css" %}"/> {% endblock %} @@ -30,8 +32,8 @@ <div class="navbar {% block bootstrap_navbar_variant %}navbar-inverse{% endblock %}"> <div class="navbar-inner"> <div class="container-fluid"> - <span class="brand" href="/"> - {% block branding %}<a href='http://django-rest-framework.org'>Django REST framework <span class="version">{{ version }}</span></a>{% endblock %} + <span href="/"> + {% block branding %}<a class='brand' href='http://django-rest-framework.org'>Django REST framework <span class="version">{{ version }}</span></a>{% endblock %} </span> <ul class="nav pull-right"> {% block userlinks %} @@ -109,8 +111,7 @@ <div class="content-main"> <div class="page-header"><h1>{{ name }}</h1></div> {{ description }} - - <div class="request-info"> + <div class="request-info" style="clear: both" > <pre class="prettyprint"><b>{{ request.method }}</b> {{ request.get_full_path }}</pre> </div> <div class="response-info"> diff --git a/rest_framework/templates/rest_framework/login_base.html b/rest_framework/templates/rest_framework/login_base.html index a3e73b6b..be9a0072 100644 --- a/rest_framework/templates/rest_framework/login_base.html +++ b/rest_framework/templates/rest_framework/login_base.html @@ -4,8 +4,10 @@ <head> {% block style %} - {% block bootstrap_theme %}<link rel="stylesheet" type="text/css" href="{% static "rest_framework/css/bootstrap.min.css" %}"/>{% endblock %} - <link rel="stylesheet" type="text/css" href="{% static "rest_framework/css/bootstrap-tweaks.css" %}"/> + {% 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" %}"/> + {% endblock %} <link rel="stylesheet" type="text/css" href="{% static "rest_framework/css/default.css" %}"/> {% endblock %} </head> diff --git a/rest_framework/tests/fields.py b/rest_framework/tests/fields.py index 6b1cdfc7..dad69975 100644 --- a/rest_framework/tests/fields.py +++ b/rest_framework/tests/fields.py @@ -10,6 +10,7 @@ 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): @@ -685,3 +686,126 @@ class ChoiceFieldTests(TestCase): """ 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_pk.py b/rest_framework/tests/relations_pk.py index 0f8c5247..e2a1b815 100644 --- a/rest_framework/tests/relations_pk.py +++ b/rest_framework/tests/relations_pk.py @@ -1,4 +1,5 @@ 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 ( @@ -127,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]}, @@ -138,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) @@ -426,8 +429,69 @@ class PKNullableOneToOneTests(TestCase): 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): """ diff --git a/rest_framework/tests/serializer.py b/rest_framework/tests/serializer.py index e999b624..759017f4 100644 --- a/rest_framework/tests/serializer.py +++ b/rest_framework/tests/serializer.py @@ -7,7 +7,7 @@ from django.utils.translation import ugettext_lazy as _ from rest_framework import serializers 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) import datetime import pickle @@ -92,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. @@ -419,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): @@ -1118,6 +1140,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): def test_implicit_nesting(self): @@ -1271,3 +1350,84 @@ class LazyStringsTestCase(TestCase): serializer = LazyStringSerializer(self.model) self.assertEqual(type(serializer.data['lazystring']), type('lazystring')) + + +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') |
