diff --git a/.travis.yml b/.travis.yml index 205feef92..3a7c2d7ad 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 155c89de3..99fe10834 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 ed733c653..b9a9fd7a3 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 8ee018249..65f76abcf 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 %} + + {% 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 8151b4d3a..d805c0c1d 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 e4da14566..46fb31528 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) @@ -176,7 +178,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): @@ -384,7 +390,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) @@ -393,7 +398,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) @@ -409,6 +413,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 c4b790d47..c4271e337 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 9b519f271..9dd7b545e 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 942ab3994..425dd18cc 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): @@ -730,6 +750,22 @@ class ModelSerializer(Serializer): if model_field.help_text is not None: kwargs['help_text'] = model_field.help_text + 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 c650ef2e9..9b5201564 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 d806267bc..0261a3038 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 4410f285f..9d939e738 100644 --- a/rest_framework/templates/rest_framework/base.html +++ b/rest_framework/templates/rest_framework/base.html @@ -13,8 +13,10 @@ {% block title %}Django REST framework{% endblock %} {% block style %} - {% block bootstrap_theme %}{% endblock %} - + {% block bootstrap_theme %} + + + {% endblock %} {% endblock %} @@ -30,8 +32,8 @@