diff options
| author | Karol Majta | 2013-05-18 16:56:38 +0200 | 
|---|---|---|
| committer | Karol Majta | 2013-05-18 16:56:38 +0200 | 
| commit | 5bebd29f11dd9268b9a23c27cf58c8440664f5e9 (patch) | |
| tree | 66f75d176f8746432cf9083c0bdfe92a068e142f | |
| parent | ebe959b52a10a88975b15c69275b0ef5c50cb9fa (diff) | |
| parent | 3f47eb7a77fcc735782dd1bf8e8e053e26417ea1 (diff) | |
| download | django-rest-framework-5bebd29f11dd9268b9a23c27cf58c8440664f5e9.tar.bz2 | |
Merge branch 'master' of git://github.com/tomchristie/django-rest-framework
| -rw-r--r-- | .travis.yml | 17 | ||||
| -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 | 12 | ||||
| -rw-r--r-- | rest_framework/fields.py | 14 | ||||
| -rw-r--r-- | rest_framework/relations.py | 26 | ||||
| -rw-r--r-- | rest_framework/runtests/settings.py | 2 | ||||
| -rw-r--r-- | rest_framework/serializers.py | 72 | ||||
| -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 | 169 | ||||
| -rw-r--r-- | rest_framework/tests/relations.py | 55 | ||||
| -rw-r--r-- | rest_framework/tests/relations_hyperlink.py | 71 | ||||
| -rw-r--r-- | rest_framework/tests/relations_pk.py | 121 | ||||
| -rw-r--r-- | rest_framework/tests/serializer.py | 295 | 
18 files changed, 988 insertions, 217 deletions
| diff --git a/.travis.yml b/.travis.yml index 205feef9..3a7c2d7a 100644 --- a/.travis.yml +++ b/.travis.yml @@ -7,9 +7,9 @@ python:    - "3.3"  env: -  - DJANGO="django==1.5 --use-mirrors" -  - DJANGO="django==1.4.3 --use-mirrors" -  - DJANGO="django==1.3.5 --use-mirrors" +  - DJANGO="django==1.5.1 --use-mirrors" +  - DJANGO="django==1.4.5 --use-mirrors" +  - DJANGO="django==1.3.7 --use-mirrors"  install:    - pip install $DJANGO @@ -18,7 +18,7 @@ install:    - "if [[ ${TRAVIS_PYTHON_VERSION::1} != '3' ]]; then pip install django-oauth-plus==2.0 --use-mirrors; fi"    - "if [[ ${TRAVIS_PYTHON_VERSION::1} != '3' ]]; then pip install django-oauth2-provider==0.2.3 --use-mirrors; fi"    - "if [[ ${DJANGO::11} == 'django==1.3' ]]; then pip install django-filter==0.5.4 --use-mirrors; fi" -  - "if [[ ${DJANGO::11} != 'django==1.3' ]]; then pip install django-filter==0.6a1 --use-mirrors; fi" +  - "if [[ ${DJANGO::11} != 'django==1.3' ]]; then pip install django-filter==0.6 --use-mirrors; fi"    - export PYTHONPATH=.  script: @@ -27,10 +27,11 @@ script:  matrix:    exclude:      - python: "3.2" -      env: DJANGO="django==1.4.3 --use-mirrors" +      env: DJANGO="django==1.4.5 --use-mirrors"      - python: "3.2" -      env: DJANGO="django==1.3.5 --use-mirrors" +      env: DJANGO="django==1.3.7 --use-mirrors"      - python: "3.3" -      env: DJANGO="django==1.4.3 --use-mirrors" +      env: DJANGO="django==1.4.5 --use-mirrors"      - python: "3.3" -      env: DJANGO="django==1.3.5 --use-mirrors" +      env: DJANGO="django==1.3.7 --use-mirrors" + diff --git a/docs/api-guide/relations.md b/docs/api-guide/relations.md index 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 8151b4d3..d805c0c1 100644 --- a/docs/topics/credits.md +++ b/docs/topics/credits.md @@ -124,6 +124,12 @@ The following people have helped make REST framework great.  * Marlon Bailey - [avinash240]  * James Summerfield - [jsummerfield]  * Andy Freeland - [rouge8] +* Craig de Stigter - [craigds] +* Pablo Recio - [pyriku] +* Brian Zambrano - [brianz] +* Òscar Vilaplana - [grimborg] +* Ryan Kaskel - [ryankask] +* Andy McKay - [andymckay]  Many thanks to everyone who's contributed to the project. @@ -284,3 +290,9 @@ You can also contact [@_tomchristie][twitter] directly on twitter.  [avinash240]: https://github.com/avinash240  [jsummerfield]: https://github.com/jsummerfield  [rouge8]: https://github.com/rouge8 +[craigds]: https://github.com/craigds +[pyriku]: https://github.com/pyriku +[brianz]: https://github.com/brianz +[grimborg]: https://github.com/grimborg +[ryankask]: https://github.com/ryankask +[andymckay]: https://github.com/andymckay diff --git a/rest_framework/fields.py b/rest_framework/fields.py index c83ee5ec..fc14184c 100644 --- a/rest_framework/fields.py +++ b/rest_framework/fields.py @@ -15,10 +15,12 @@ import warnings  from django.core import validators  from django.core.exceptions import ValidationError  from django.conf import settings +from django.db.models.fields import BLANK_CHOICE_DASH  from django import forms  from django.forms import widgets  from django.utils.encoding import is_protected_type  from django.utils.translation import ugettext_lazy as _ +from django.utils.datastructures import SortedDict  from rest_framework import ISO_8601  from rest_framework.compat import timezone, parse_date, parse_datetime, parse_time @@ -50,7 +52,7 @@ def get_component(obj, attr_name):      return that attribute on the object.      """      if isinstance(obj, dict): -        val = obj[attr_name] +        val = obj.get(attr_name)      else:          val = getattr(obj, attr_name) @@ -170,7 +172,11 @@ class Field(object):          elif hasattr(value, '__iter__') and not isinstance(value, (dict, six.string_types)):              return [self.to_native(item) for item in value]          elif isinstance(value, dict): -            return dict(map(self.to_native, (k, v)) for k, v in value.items()) +            # Make sure we preserve field ordering, if it exists +            ret = SortedDict() +            for key, val in value.items(): +                ret[key] = self.to_native(val) +            return ret          return smart_text(value)      def attributes(self): @@ -377,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) @@ -386,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) @@ -402,6 +406,8 @@ class ChoiceField(WritableField):      def __init__(self, choices=(), *args, **kwargs):          super(ChoiceField, self).__init__(*args, **kwargs)          self.choices = choices +        if not self.required: +            self.choices = BLANK_CHOICE_DASH + self.choices      def _get_choices(self):          return self._choices diff --git a/rest_framework/relations.py b/rest_framework/relations.py index c4b790d4..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: @@ -221,12 +227,20 @@ class PrimaryKeyRelatedField(RelatedField):      def field_to_native(self, obj, field_name):          if self.many:              # To-many relationship -            try: + +            queryset = None +            if not self.source:                  # Prefer obj.serializable_value for performance reasons -                queryset = obj.serializable_value(self.source or field_name) -            except AttributeError: +                try: +                    queryset = obj.serializable_value(field_name) +                except AttributeError: +                    pass +            if queryset is None:                  # RelatedManager (reverse relationship) -                queryset = getattr(obj, self.source or field_name) +                source = self.source or field_name +                queryset = obj +                for component in source.split('.'): +                    queryset = get_component(queryset, component)              # Forward relationship              return [self.to_native(item.pk) for item in queryset.all()] @@ -434,7 +448,7 @@ class HyperlinkedRelatedField(RelatedField):              raise Exception('Writable related fields must include a `queryset` argument')          try: -            http_prefix = value.startswith('http:') or value.startswith('https:') +            http_prefix = value.startswith(('http:', 'https:'))          except AttributeError:              msg = self.error_messages['incorrect_type']              raise ValidationError(msg % type(value).__name__) diff --git a/rest_framework/runtests/settings.py b/rest_framework/runtests/settings.py index 9b519f27..9dd7b545 100644 --- a/rest_framework/runtests/settings.py +++ b/rest_framework/runtests/settings.py @@ -4,6 +4,8 @@ DEBUG = True  TEMPLATE_DEBUG = DEBUG  DEBUG_PROPAGATE_EXCEPTIONS = True +ALLOWED_HOSTS = ['*'] +  ADMINS = (      # ('Your Name', 'your_email@domain.com'),  ) diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index 7707de7a..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 @@ -705,15 +726,14 @@ class ModelSerializer(Serializer):          Creates a default instance of a basic non-relational field.          """          kwargs = {} -        has_default = model_field.has_default() -        if model_field.null or model_field.blank or has_default: +        if model_field.null or model_field.blank:              kwargs['required'] = False          if isinstance(model_field, models.AutoField) or not model_field.editable:              kwargs['read_only'] = True -        if has_default: +        if model_field.has_default():              kwargs['default'] = model_field.get_default()          if issubclass(model_field.__class__, models.TextField): @@ -724,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 3cdfa0f6..dad69975 100644 --- a/rest_framework/tests/fields.py +++ b/rest_framework/tests/fields.py @@ -2,15 +2,15 @@  General serializer field tests.  """  from __future__ import unicode_literals +from django.utils.datastructures import SortedDict  import datetime  from decimal import Decimal -  from django.db import models  from django.test import TestCase  from django.core import validators -  from rest_framework import serializers  from rest_framework.serializers import Serializer +from rest_framework.tests.models import RESTFrameworkModel  class TimestampedModel(models.Model): @@ -63,6 +63,20 @@ class BasicFieldTests(TestCase):          serializer = CharPrimaryKeyModelSerializer()          self.assertEqual(serializer.fields['id'].read_only, False) +    def test_dict_field_ordering(self): +        """ +        Field should preserve dictionary ordering, if it exists. +        See: https://github.com/tomchristie/django-rest-framework/issues/832 +        """ +        ret = SortedDict() +        ret['c'] = 1 +        ret['b'] = 1 +        ret['a'] = 1 +        ret['z'] = 1 +        field = serializers.Field() +        keys = list(field.to_native(ret).keys()) +        self.assertEqual(keys, ['c', 'b', 'a', 'z']) +  class DateFieldTest(TestCase):      """ @@ -645,4 +659,153 @@ class DecimalFieldTest(TestCase):          s = DecimalSerializer(data={'decimal_field': '12345.6'})          self.assertFalse(s.is_valid()) -        self.assertEqual(s.errors,  {'decimal_field': ['Ensure that there are no more than 4 digits in total.']})
\ No newline at end of file +        self.assertEqual(s.errors,  {'decimal_field': ['Ensure that there are no more than 4 digits in total.']}) + + +class ChoiceFieldTests(TestCase): +    """ +    Tests for the ChoiceField options generator +    """ + +    SAMPLE_CHOICES = [ +        ('red', 'Red'), +        ('green', 'Green'), +        ('blue', 'Blue'), +    ] + +    def test_choices_required(self): +        """ +        Make sure proper choices are rendered if field is required +        """ +        f = serializers.ChoiceField(required=True, choices=self.SAMPLE_CHOICES) +        self.assertEqual(f.choices, self.SAMPLE_CHOICES) + +    def test_choices_not_required(self): +        """ +        Make sure proper choices (plus blank) are rendered if the field isn't required +        """ +        f = serializers.ChoiceField(required=False, choices=self.SAMPLE_CHOICES) +        self.assertEqual(f.choices, models.fields.BLANK_CHOICE_DASH + self.SAMPLE_CHOICES) + + +class EmailFieldTests(TestCase): +    """ +    Tests for EmailField attribute values +    """ + +    class EmailFieldModel(RESTFrameworkModel): +        email_field = models.EmailField(blank=True) + +    class EmailFieldWithGivenMaxLengthModel(RESTFrameworkModel): +        email_field = models.EmailField(max_length=150, blank=True) + +    def test_default_model_value(self): +        class EmailFieldSerializer(serializers.ModelSerializer): +            class Meta: +                model = self.EmailFieldModel + +        serializer = EmailFieldSerializer(data={}) +        self.assertEqual(serializer.is_valid(), True) +        self.assertEqual(getattr(serializer.fields['email_field'], 'max_length'), 75) + +    def test_given_model_value(self): +        class EmailFieldSerializer(serializers.ModelSerializer): +            class Meta: +                model = self.EmailFieldWithGivenMaxLengthModel + +        serializer = EmailFieldSerializer(data={}) +        self.assertEqual(serializer.is_valid(), True) +        self.assertEqual(getattr(serializer.fields['email_field'], 'max_length'), 150) + +    def test_given_serializer_value(self): +        class EmailFieldSerializer(serializers.ModelSerializer): +            email_field = serializers.EmailField(source='email_field', max_length=20, required=False) + +            class Meta: +                model = self.EmailFieldModel + +        serializer = EmailFieldSerializer(data={}) +        self.assertEqual(serializer.is_valid(), True) +        self.assertEqual(getattr(serializer.fields['email_field'], 'max_length'), 20) + + +class SlugFieldTests(TestCase): +    """ +    Tests for SlugField attribute values +    """ + +    class SlugFieldModel(RESTFrameworkModel): +        slug_field = models.SlugField(blank=True) + +    class SlugFieldWithGivenMaxLengthModel(RESTFrameworkModel): +        slug_field = models.SlugField(max_length=84, blank=True) + +    def test_default_model_value(self): +        class SlugFieldSerializer(serializers.ModelSerializer): +            class Meta: +                model = self.SlugFieldModel + +        serializer = SlugFieldSerializer(data={}) +        self.assertEqual(serializer.is_valid(), True) +        self.assertEqual(getattr(serializer.fields['slug_field'], 'max_length'), 50) + +    def test_given_model_value(self): +        class SlugFieldSerializer(serializers.ModelSerializer): +            class Meta: +                model = self.SlugFieldWithGivenMaxLengthModel + +        serializer = SlugFieldSerializer(data={}) +        self.assertEqual(serializer.is_valid(), True) +        self.assertEqual(getattr(serializer.fields['slug_field'], 'max_length'), 84) + +    def test_given_serializer_value(self): +        class SlugFieldSerializer(serializers.ModelSerializer): +            slug_field = serializers.SlugField(source='slug_field', max_length=20, required=False) + +            class Meta: +                model = self.SlugFieldModel + +        serializer = SlugFieldSerializer(data={}) +        self.assertEqual(serializer.is_valid(), True) +        self.assertEqual(getattr(serializer.fields['slug_field'], 'max_length'), 20) + + +class URLFieldTests(TestCase): +    """ +    Tests for URLField attribute values +    """ + +    class URLFieldModel(RESTFrameworkModel): +        url_field = models.URLField(blank=True) + +    class URLFieldWithGivenMaxLengthModel(RESTFrameworkModel): +        url_field = models.URLField(max_length=128, blank=True) + +    def test_default_model_value(self): +        class URLFieldSerializer(serializers.ModelSerializer): +            class Meta: +                model = self.URLFieldModel + +        serializer = URLFieldSerializer(data={}) +        self.assertEqual(serializer.is_valid(), True) +        self.assertEqual(getattr(serializer.fields['url_field'], 'max_length'), 200) + +    def test_given_model_value(self): +        class URLFieldSerializer(serializers.ModelSerializer): +            class Meta: +                model = self.URLFieldWithGivenMaxLengthModel + +        serializer = URLFieldSerializer(data={}) +        self.assertEqual(serializer.is_valid(), True) +        self.assertEqual(getattr(serializer.fields['url_field'], 'max_length'), 128) + +    def test_given_serializer_value(self): +        class URLFieldSerializer(serializers.ModelSerializer): +            url_field = serializers.URLField(source='url_field', max_length=20, required=False) + +            class Meta: +                model = self.URLFieldWithGivenMaxLengthModel + +        serializer = URLFieldSerializer(data={}) +        self.assertEqual(serializer.is_valid(), True) +        self.assertEqual(getattr(serializer.fields['url_field'], 'max_length'), 20) diff --git a/rest_framework/tests/relations.py b/rest_framework/tests/relations.py index cbf93c65..d19219c9 100644 --- a/rest_framework/tests/relations.py +++ b/rest_framework/tests/relations.py @@ -5,6 +5,7 @@ from __future__ import unicode_literals  from django.db import models  from django.test import TestCase  from rest_framework import serializers +from rest_framework.tests.models import BlogPost  class NullModel(models.Model): @@ -33,7 +34,7 @@ class FieldTests(TestCase):          self.assertRaises(serializers.ValidationError, field.from_native, []) -class TestManyRelateMixin(TestCase): +class TestManyRelatedMixin(TestCase):      def test_missing_many_to_many_related_field(self):          '''          Regression test for #632 @@ -45,3 +46,55 @@ class TestManyRelateMixin(TestCase):          into = {}          field.field_from_native({}, None, 'field_name', into)          self.assertEqual(into['field_name'], []) + + +# Regression tests for #694 (`source` attribute on related fields) + +class RelatedFieldSourceTests(TestCase): +    def test_related_manager_source(self): +        """ +        Relational fields should be able to use manager-returning methods as their source. +        """ +        BlogPost.objects.create(title='blah') +        field = serializers.RelatedField(many=True, source='get_blogposts_manager') + +        class ClassWithManagerMethod(object): +            def get_blogposts_manager(self): +                return BlogPost.objects + +        obj = ClassWithManagerMethod() +        value = field.field_to_native(obj, 'field_name') +        self.assertEqual(value, ['BlogPost object']) + +    def test_related_queryset_source(self): +        """ +        Relational fields should be able to use queryset-returning methods as their source. +        """ +        BlogPost.objects.create(title='blah') +        field = serializers.RelatedField(many=True, source='get_blogposts_queryset') + +        class ClassWithQuerysetMethod(object): +            def get_blogposts_queryset(self): +                return BlogPost.objects.all() + +        obj = ClassWithQuerysetMethod() +        value = field.field_to_native(obj, 'field_name') +        self.assertEqual(value, ['BlogPost object']) + +    def test_dotted_source(self): +        """ +        Source argument should support dotted.source notation. +        """ +        BlogPost.objects.create(title='blah') +        field = serializers.RelatedField(many=True, source='a.b.c') + +        class ClassWithQuerysetMethod(object): +            a = { +                'b': { +                    'c': BlogPost.objects.all() +                } +            } + +        obj = ClassWithQuerysetMethod() +        value = field.field_to_native(obj, 'field_name') +        self.assertEqual(value, ['BlogPost object']) diff --git a/rest_framework/tests/relations_hyperlink.py b/rest_framework/tests/relations_hyperlink.py index b1eed9a7..b3efbf52 100644 --- a/rest_framework/tests/relations_hyperlink.py +++ b/rest_framework/tests/relations_hyperlink.py @@ -4,6 +4,7 @@ from django.test.client import RequestFactory  from rest_framework import serializers  from rest_framework.compat import patterns, url  from rest_framework.tests.models import ( +    BlogPost,      ManyToManyTarget, ManyToManySource, ForeignKeyTarget, ForeignKeySource,      NullableForeignKeySource, OneToOneTarget, NullableOneToOneSource  ) @@ -16,6 +17,7 @@ def dummy_view(request, pk):      pass  urlpatterns = patterns('', +    url(r'^dummyurl/(?P<pk>[0-9]+)/$', dummy_view, name='dummy-url'),      url(r'^manytomanysource/(?P<pk>[0-9]+)/$', dummy_view, name='manytomanysource-detail'),      url(r'^manytomanytarget/(?P<pk>[0-9]+)/$', dummy_view, name='manytomanytarget-detail'),      url(r'^foreignkeysource/(?P<pk>[0-9]+)/$', dummy_view, name='foreignkeysource-detail'), @@ -451,3 +453,72 @@ class HyperlinkedNullableOneToOneTests(TestCase):              {'url': 'http://testserver/onetoonetarget/2/', 'name': 'target-2', 'nullable_source': None},          ]          self.assertEqual(serializer.data, expected) + + +# Regression tests for #694 (`source` attribute on related fields) + +class HyperlinkedRelatedFieldSourceTests(TestCase): +    urls = 'rest_framework.tests.relations_hyperlink' + +    def test_related_manager_source(self): +        """ +        Relational fields should be able to use manager-returning methods as their source. +        """ +        BlogPost.objects.create(title='blah') +        field = serializers.HyperlinkedRelatedField( +            many=True, +            source='get_blogposts_manager', +            view_name='dummy-url', +        ) +        field.context = {'request': request} + +        class ClassWithManagerMethod(object): +            def get_blogposts_manager(self): +                return BlogPost.objects + +        obj = ClassWithManagerMethod() +        value = field.field_to_native(obj, 'field_name') +        self.assertEqual(value, ['http://testserver/dummyurl/1/']) + +    def test_related_queryset_source(self): +        """ +        Relational fields should be able to use queryset-returning methods as their source. +        """ +        BlogPost.objects.create(title='blah') +        field = serializers.HyperlinkedRelatedField( +            many=True, +            source='get_blogposts_queryset', +            view_name='dummy-url', +        ) +        field.context = {'request': request} + +        class ClassWithQuerysetMethod(object): +            def get_blogposts_queryset(self): +                return BlogPost.objects.all() + +        obj = ClassWithQuerysetMethod() +        value = field.field_to_native(obj, 'field_name') +        self.assertEqual(value, ['http://testserver/dummyurl/1/']) + +    def test_dotted_source(self): +        """ +        Source argument should support dotted.source notation. +        """ +        BlogPost.objects.create(title='blah') +        field = serializers.HyperlinkedRelatedField( +            many=True, +            source='a.b.c', +            view_name='dummy-url', +        ) +        field.context = {'request': request} + +        class ClassWithQuerysetMethod(object): +            a = { +                'b': { +                    'c': BlogPost.objects.all() +                } +            } + +        obj = ClassWithQuerysetMethod() +        value = field.field_to_native(obj, 'field_name') +        self.assertEqual(value, ['http://testserver/dummyurl/1/']) diff --git a/rest_framework/tests/relations_pk.py b/rest_framework/tests/relations_pk.py index 5ce8b567..e2a1b815 100644 --- a/rest_framework/tests/relations_pk.py +++ b/rest_framework/tests/relations_pk.py @@ -1,7 +1,11 @@  from __future__ import unicode_literals +from django.db import models  from django.test import TestCase  from rest_framework import serializers -from rest_framework.tests.models import ManyToManyTarget, ManyToManySource, ForeignKeyTarget, ForeignKeySource, NullableForeignKeySource, OneToOneTarget, NullableOneToOneSource +from rest_framework.tests.models import ( +    BlogPost, ManyToManyTarget, ManyToManySource, ForeignKeyTarget, ForeignKeySource, +    NullableForeignKeySource, OneToOneTarget, NullableOneToOneSource, +)  from rest_framework.compat import six @@ -124,6 +128,7 @@ class PKManyToManyTests(TestCase):          # Ensure source 4 is added, and everything else is as expected          queryset = ManyToManySource.objects.all()          serializer = ManyToManySourceSerializer(queryset, many=True) +        self.assertFalse(serializer.fields['targets'].read_only)          expected = [              {'id': 1, 'name': 'source-1', 'targets': [1]},              {'id': 2, 'name': 'source-2', 'targets': [1, 2]}, @@ -135,6 +140,7 @@ class PKManyToManyTests(TestCase):      def test_reverse_many_to_many_create(self):          data = {'id': 4, 'name': 'target-4', 'sources': [1, 3]}          serializer = ManyToManyTargetSerializer(data=data) +        self.assertFalse(serializer.fields['sources'].read_only)          self.assertTrue(serializer.is_valid())          obj = serializer.save()          self.assertEqual(serializer.data, data) @@ -421,3 +427,116 @@ class PKNullableOneToOneTests(TestCase):              {'id': 2, 'name': 'target-2', 'nullable_source': 1},          ]          self.assertEqual(serializer.data, expected) + + +# The below models and tests ensure that serializer fields corresponding +# to a ManyToManyField field with a user-specified ``through`` model are +# set to read only + + +class ManyToManyThroughTarget(models.Model): +    name = models.CharField(max_length=100) + + +class ManyToManyThrough(models.Model): +    source = models.ForeignKey('ManyToManyThroughSource') +    target = models.ForeignKey(ManyToManyThroughTarget) + + +class ManyToManyThroughSource(models.Model): +    name = models.CharField(max_length=100) +    targets = models.ManyToManyField(ManyToManyThroughTarget, +                                     related_name='sources', +                                     through='ManyToManyThrough') + + +class ManyToManyThroughTargetSerializer(serializers.ModelSerializer): +    class Meta: +        model = ManyToManyThroughTarget +        fields = ('id', 'name', 'sources') + + +class ManyToManyThroughSourceSerializer(serializers.ModelSerializer): +    class Meta: +        model = ManyToManyThroughSource +        fields = ('id', 'name', 'targets') + + +class PKManyToManyThroughTests(TestCase): +    def setUp(self): +        self.source = ManyToManyThroughSource.objects.create( +            name='through-source-1') +        self.target = ManyToManyThroughTarget.objects.create( +            name='through-target-1') + +    def test_many_to_many_create(self): +        data = {'id': 2, 'name': 'source-2', 'targets': [self.target.pk]} +        serializer = ManyToManyThroughSourceSerializer(data=data) +        self.assertTrue(serializer.fields['targets'].read_only) +        self.assertTrue(serializer.is_valid()) +        obj = serializer.save() +        self.assertEqual(obj.name, 'source-2') +        self.assertEqual(obj.targets.count(), 0) + +    def test_many_to_many_reverse_create(self): +        data = {'id': 2, 'name': 'target-2', 'sources': [self.source.pk]} +        serializer = ManyToManyThroughTargetSerializer(data=data) +        self.assertTrue(serializer.fields['sources'].read_only) +        self.assertTrue(serializer.is_valid()) +        serializer.save() +        obj = serializer.save() +        self.assertEqual(obj.name, 'target-2') +        self.assertEqual(obj.sources.count(), 0) + + +# Regression tests for #694 (`source` attribute on related fields) + + +class PrimaryKeyRelatedFieldSourceTests(TestCase): +    def test_related_manager_source(self): +        """ +        Relational fields should be able to use manager-returning methods as their source. +        """ +        BlogPost.objects.create(title='blah') +        field = serializers.PrimaryKeyRelatedField(many=True, source='get_blogposts_manager') + +        class ClassWithManagerMethod(object): +            def get_blogposts_manager(self): +                return BlogPost.objects + +        obj = ClassWithManagerMethod() +        value = field.field_to_native(obj, 'field_name') +        self.assertEqual(value, [1]) + +    def test_related_queryset_source(self): +        """ +        Relational fields should be able to use queryset-returning methods as their source. +        """ +        BlogPost.objects.create(title='blah') +        field = serializers.PrimaryKeyRelatedField(many=True, source='get_blogposts_queryset') + +        class ClassWithQuerysetMethod(object): +            def get_blogposts_queryset(self): +                return BlogPost.objects.all() + +        obj = ClassWithQuerysetMethod() +        value = field.field_to_native(obj, 'field_name') +        self.assertEqual(value, [1]) + +    def test_dotted_source(self): +        """ +        Source argument should support dotted.source notation. +        """ +        BlogPost.objects.create(title='blah') +        field = serializers.PrimaryKeyRelatedField(many=True, source='a.b.c') + +        class ClassWithQuerysetMethod(object): +            a = { +                'b': { +                    'c': BlogPost.objects.all() +                } +            } + +        obj = ClassWithQuerysetMethod() +        value = field.field_to_native(obj, 'field_name') +        self.assertEqual(value, [1]) diff --git a/rest_framework/tests/serializer.py b/rest_framework/tests/serializer.py index db3881f9..4f188c3e 100644 --- a/rest_framework/tests/serializer.py +++ b/rest_framework/tests/serializer.py @@ -1,10 +1,12 @@  from __future__ import unicode_literals +from django.db import models +from django.db.models.fields import BLANK_CHOICE_DASH  from django.utils.datastructures import MultiValueDict  from django.test import TestCase  from rest_framework import serializers  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 @@ -43,6 +45,17 @@ class CommentSerializer(serializers.Serializer):          return instance +class NamesSerializer(serializers.Serializer): +    first = serializers.CharField() +    last = serializers.CharField(required=False, default='') +    initials = serializers.CharField(required=False, default='') + + +class PersonIdentifierSerializer(serializers.Serializer): +    ssn = serializers.CharField() +    names = NamesSerializer(source='names', required=False) + +  class BookSerializer(serializers.ModelSerializer):      isbn = serializers.RegexField(regex=r'^[0-9]{13}$', error_messages={'invalid': 'isbn has to be exact 13 numbers'}) @@ -78,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. @@ -153,6 +177,42 @@ class BasicTests(TestCase):          self.assertFalse(serializer.object is expected)          self.assertEqual(serializer.data['sub_comment'], 'And Merry Christmas!') +    def test_create_nested(self): +        """Test a serializer with nested data.""" +        names = {'first': 'John', 'last': 'Doe', 'initials': 'jd'} +        data = {'ssn': '1234567890', 'names': names} +        serializer = PersonIdentifierSerializer(data=data) + +        self.assertEqual(serializer.is_valid(), True) +        self.assertEqual(serializer.object, data) +        self.assertFalse(serializer.object is data) +        self.assertEqual(serializer.data['names'], names) + +    def test_create_partial_nested(self): +        """Test a serializer with nested data which has missing fields.""" +        names = {'first': 'John'} +        data = {'ssn': '1234567890', 'names': names} +        serializer = PersonIdentifierSerializer(data=data) + +        expected_names = {'first': 'John', 'last': '', 'initials': ''} +        data['names'] = expected_names + +        self.assertEqual(serializer.is_valid(), True) +        self.assertEqual(serializer.object, data) +        self.assertFalse(serializer.object is expected_names) +        self.assertEqual(serializer.data['names'], expected_names) + +    def test_null_nested(self): +        """Test a serializer with a nonexistent nested field""" +        data = {'ssn': '1234567890'} +        serializer = PersonIdentifierSerializer(data=data) + +        self.assertEqual(serializer.is_valid(), True) +        self.assertEqual(serializer.object, data) +        self.assertFalse(serializer.object is data) +        expected = {'ssn': '1234567890', 'names': None} +        self.assertEqual(serializer.data, expected) +      def test_update(self):          serializer = CommentSerializer(self.comment, data=self.data)          expected = self.comment @@ -369,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): @@ -871,23 +942,6 @@ class RelatedTraversalTest(TestCase):          self.assertEqual(serializer.data, expected) -    def test_queryset_nested_traversal(self): -        """ -        Relational fields should be able to use methods as their source. -        """ -        BlogPost.objects.create(title='blah') - -        class QuerysetMethodSerializer(serializers.Serializer): -            blogposts = serializers.RelatedField(many=True, source='get_all_blogposts') - -        class ClassWithQuerysetMethod(object): -            def get_all_blogposts(self): -                return BlogPost.objects - -        obj = ClassWithQuerysetMethod() -        serializer = QuerysetMethodSerializer(obj) -        self.assertEqual(serializer.data, {'blogposts': ['BlogPost object']}) -  class SerializerMethodFieldTests(TestCase):      def setUp(self): @@ -1018,6 +1072,130 @@ class SerializerPickleTests(TestCase):          repr(pickle.loads(pickle.dumps(data, 0))) +# test for issue #725 +class SeveralChoicesModel(models.Model): +    color = models.CharField( +        max_length=10, +        choices=[('red', 'Red'), ('green', 'Green'), ('blue', 'Blue')], +        blank=False +    ) +    drink = models.CharField( +        max_length=10, +        choices=[('beer', 'Beer'), ('wine', 'Wine'), ('cider', 'Cider')], +        blank=False, +        default='beer' +    ) +    os = models.CharField( +        max_length=10, +        choices=[('linux', 'Linux'), ('osx', 'OSX'), ('windows', 'Windows')], +        blank=True +    ) +    music_genre = models.CharField( +        max_length=10, +        choices=[('rock', 'Rock'), ('metal', 'Metal'), ('grunge', 'Grunge')], +        blank=True, +        default='metal' +    ) + + +class SerializerChoiceFields(TestCase): + +    def setUp(self): +        super(SerializerChoiceFields, self).setUp() + +        class SeveralChoicesSerializer(serializers.ModelSerializer): +            class Meta: +                model = SeveralChoicesModel +                fields = ('color', 'drink', 'os', 'music_genre') + +        self.several_choices_serializer = SeveralChoicesSerializer + +    def test_choices_blank_false_not_default(self): +        serializer = self.several_choices_serializer() +        self.assertEqual( +            serializer.fields['color'].choices, +            [('red', 'Red'), ('green', 'Green'), ('blue', 'Blue')] +        ) + +    def test_choices_blank_false_with_default(self): +        serializer = self.several_choices_serializer() +        self.assertEqual( +            serializer.fields['drink'].choices, +            [('beer', 'Beer'), ('wine', 'Wine'), ('cider', 'Cider')] +        ) + +    def test_choices_blank_true_not_default(self): +        serializer = self.several_choices_serializer() +        self.assertEqual( +            serializer.fields['os'].choices, +            BLANK_CHOICE_DASH + [('linux', 'Linux'), ('osx', 'OSX'), ('windows', 'Windows')] +        ) + +    def test_choices_blank_true_with_default(self): +        serializer = self.several_choices_serializer() +        self.assertEqual( +            serializer.fields['music_genre'].choices, +            BLANK_CHOICE_DASH + [('rock', 'Rock'), ('metal', 'Metal'), ('grunge', 'Grunge')] +        ) + + +# Regression tests for #675 +class Ticket(models.Model): +    assigned = models.ForeignKey( +        Person, related_name='assigned_tickets') +    reviewer = models.ForeignKey( +        Person, blank=True, null=True, related_name='reviewed_tickets') + + +class SerializerRelatedChoicesTest(TestCase): + +    def setUp(self): +        super(SerializerRelatedChoicesTest, self).setUp() + +        class RelatedChoicesSerializer(serializers.ModelSerializer): +            class Meta: +                model = Ticket +                fields = ('assigned', 'reviewer') + +        self.related_fields_serializer = RelatedChoicesSerializer + +    def test_empty_queryset_required(self): +        serializer = self.related_fields_serializer() +        self.assertEqual(serializer.fields['assigned'].queryset.count(), 0) +        self.assertEqual( +            [x for x in serializer.fields['assigned'].widget.choices], +            [] +        ) + +    def test_empty_queryset_not_required(self): +        serializer = self.related_fields_serializer() +        self.assertEqual(serializer.fields['reviewer'].queryset.count(), 0) +        self.assertEqual( +            [x for x in serializer.fields['reviewer'].widget.choices], +            [('', '---------')] +        ) + +    def test_with_some_persons_required(self): +        Person.objects.create(name="Lionel Messi") +        Person.objects.create(name="Xavi Hernandez") +        serializer = self.related_fields_serializer() +        self.assertEqual(serializer.fields['assigned'].queryset.count(), 2) +        self.assertEqual( +            [x for x in serializer.fields['assigned'].widget.choices], +            [(1, 'Person object - 1'), (2, 'Person object - 2')] +        ) + +    def test_with_some_persons_not_required(self): +        Person.objects.create(name="Lionel Messi") +        Person.objects.create(name="Xavi Hernandez") +        serializer = self.related_fields_serializer() +        self.assertEqual(serializer.fields['reviewer'].queryset.count(), 2) +        self.assertEqual( +            [x for x in serializer.fields['reviewer'].widget.choices], +            [('', '---------'), (1, 'Person object - 1'), (2, 'Person object - 2')] +        ) + +  class DepthTest(TestCase):      def test_implicit_nesting(self): @@ -1143,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') | 
