diff options
| author | Nikolaus Schlemm | 2013-05-18 17:13:59 +0200 | 
|---|---|---|
| committer | Nikolaus Schlemm | 2013-05-18 17:13:59 +0200 | 
| commit | b225b1d5c9cd8e2bec289f5da795637385b2cbe6 (patch) | |
| tree | fca92570fd7a056ed150b188c552d01d4c27dea5 | |
| parent | a42afa04c38afe25c9032b8ce37b572678b02cf1 (diff) | |
| parent | 3f47eb7a77fcc735782dd1bf8e8e053e26417ea1 (diff) | |
| download | django-rest-framework-b225b1d5c9cd8e2bec289f5da795637385b2cbe6.tar.bz2 | |
Merge branch 'master' of git://github.com/tomchristie/django-rest-framework into issue-192-expose-fields-for-options
| -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 491aa7ed..fc14184c 100644 --- a/rest_framework/fields.py +++ b/rest_framework/fields.py @@ -383,7 +383,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) @@ -392,7 +391,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 d0a8570c..4f188c3e 100644 --- a/rest_framework/tests/serializer.py +++ b/rest_framework/tests/serializer.py @@ -6,7 +6,7 @@ from django.test import TestCase  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 @@ -91,6 +91,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. @@ -418,6 +429,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): @@ -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):      def test_implicit_nesting(self): @@ -1242,3 +1321,84 @@ class DeserializeListTestCase(TestCase):          self.assertFalse(serializer.is_valid())          expected = [{}, {'email': ['This field is required.']}, {}]          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') | 
