diff --git a/.travis.yml b/.travis.yml index 87db5e0465..4a7fdd42f2 100644 --- a/.travis.yml +++ b/.travis.yml @@ -18,23 +18,16 @@ env: - TOX_ENV=py32-django16 - TOX_ENV=py27-django16 - TOX_ENV=py26-django16 - - TOX_ENV=py34-django15 - - TOX_ENV=py33-django15 - - TOX_ENV=py32-django15 - - TOX_ENV=py27-django15 - - TOX_ENV=py26-django15 - TOX_ENV=py27-djangomaster - - TOX_ENV=py32-djangomaster - - TOX_ENV=py33-djangomaster - TOX_ENV=py34-djangomaster + - TOX_ENV=py35-djangomaster matrix: fast_finish: true allow_failures: - env: TOX_ENV=py27-djangomaster - - env: TOX_ENV=py32-djangomaster - - env: TOX_ENV=py33-djangomaster - env: TOX_ENV=py34-djangomaster + - env: TOX_ENV=py35-djangomaster install: - pip install tox diff --git a/README.md b/README.md index b183e14df0..c2a686f4ce 100644 --- a/README.md +++ b/README.md @@ -37,7 +37,7 @@ There is a live example API for testing purposes, [available here][sandbox]. # Requirements * Python (2.6.5+, 2.7, 3.2, 3.3, 3.4) -* Django (1.5.6+, 1.6.3+, 1.7, 1.8) +* Django (1.6.3+, 1.7, 1.8) # Installation diff --git a/docs/api-guide/serializers.md b/docs/api-guide/serializers.md index ca540a13b5..63189b9663 100644 --- a/docs/api-guide/serializers.md +++ b/docs/api-guide/serializers.md @@ -438,6 +438,7 @@ Declaring a `ModelSerializer` looks like this: class AccountSerializer(serializers.ModelSerializer): class Meta: model = Account + fields = ('id', 'account_name', 'users', 'created') By default, all the model fields on the class will be mapped to a corresponding serializer fields. @@ -459,7 +460,7 @@ To do so, open the Django shell, using `python manage.py shell`, then import the ## Specifying which fields to include -If you only want a subset of the default fields to be used in a model serializer, you can do so using `fields` or `exclude` options, just as you would with a `ModelForm`. +If you only want a subset of the default fields to be used in a model serializer, you can do so using `fields` or `exclude` options, just as you would with a `ModelForm`. It is strongly recommended that you explicitly set all fields that should be serialized using the `fields` attribute. This will make it less likely to result in unintentionally exposing data when your models change. For example: @@ -468,7 +469,27 @@ For example: model = Account fields = ('id', 'account_name', 'users', 'created') -The names in the `fields` option will normally map to model fields on the model class. +You can also set the `fields` attribute to the special value `'__all__'` to indicate that all fields in the model should be used. + +For example: + + class AccountSerializer(serializers.ModelSerializer): + class Meta: + model = Account + fields = '__all__' + +You can set the `exclude` attribute of the to a list of fields to be excluded from the serializer. + +For example: + + class AccountSerializer(serializers.ModelSerializer): + class Meta: + model = Account + exclude = ('users',) + +In the example above, if the `Account` model had 3 fields `account_name`, `users`, and `created`, this will result in the fields `account_name` and `created` to be serialized. + +The names in the `fields` and `exclude` attributes will normally map to model fields on the model class. Alternatively names in the `fields` options can map to properties or methods which take no arguments that exist on the model class. diff --git a/docs/topics/release-notes.md b/docs/topics/release-notes.md index 260dc476c3..93778f74dd 100644 --- a/docs/topics/release-notes.md +++ b/docs/topics/release-notes.md @@ -38,6 +38,14 @@ You can determine your currently installed version using `pip freeze`: --- +## 3.3.x series + +### 3.3.0 + +**Date**: NOT YET RELEASED + +* Removed support for Django Version 1.5 ([#3421][gh3421]) + ## 3.2.x series ### 3.2.4 @@ -533,3 +541,6 @@ For older release notes, [please see the version 2.x documentation][old-release- [gh3361]: https://github.com/tomchristie/django-rest-framework/issues/3361 [gh3364]: https://github.com/tomchristie/django-rest-framework/issues/3364 [gh3415]: https://github.com/tomchristie/django-rest-framework/issues/3415 + + +[gh3421]: https://github.com/tomchristie/django-rest-framework/pulls/3421 diff --git a/rest_framework/compat.py b/rest_framework/compat.py index 460795f658..fcca2dcbf8 100644 --- a/rest_framework/compat.py +++ b/rest_framework/compat.py @@ -67,6 +67,14 @@ def distinct(queryset, base): from django.utils.datastructures import SortedDict as OrderedDict +# unittest.SkipUnless only available in Python 2.7. +try: + import unittest + unittest.skipUnless +except (ImportError, AttributeError): + from django.utils import unittest + + # contrib.postgres only supported from 1.8 onwards. try: from django.contrib.postgres import fields as postgres_fields @@ -74,23 +82,30 @@ def distinct(queryset, base): postgres_fields = None +# Apps only exists from 1.7 onwards. +try: + from django.apps import apps + get_model = apps.get_model +except ImportError: + from django.db.models import get_model + + +# Import path changes from 1.7 onwards. +try: + from django.contrib.contenttypes.fields import ( + GenericForeignKey, GenericRelation + ) +except ImportError: + from django.contrib.contenttypes.generic import ( + GenericForeignKey, GenericRelation + ) + # django-filter is optional try: import django_filters except ImportError: django_filters = None -if django.VERSION >= (1, 6): - def clean_manytomany_helptext(text): - return text -else: - # Up to version 1.5 many to many fields automatically suffix - # the `help_text` attribute with hardcoded text. - def clean_manytomany_helptext(text): - if text.endswith(' Hold down "Control", or "Command" on a Mac, to select more than one.'): - text = text[:-69] - return text - # Django-guardian is optional. Import only if guardian is in INSTALLED_APPS # Fixes (#1712). We keep the try/except for the test suite. guardian = None @@ -101,14 +116,6 @@ def clean_manytomany_helptext(text): pass -def get_model_name(model_cls): - try: - return model_cls._meta.model_name - except AttributeError: - # < 1.6 used module_name instead of model_name - return model_cls._meta.module_name - - # MinValueValidator, MaxValueValidator et al. only accept `message` in 1.8+ if django.VERSION >= (1, 8): from django.core.validators import MinValueValidator, MaxValueValidator @@ -144,32 +151,6 @@ def __init__(self, *args, **kwargs): super(MaxLengthValidator, self).__init__(*args, **kwargs) -# URLValidator only accepts `message` in 1.6+ -if django.VERSION >= (1, 6): - from django.core.validators import URLValidator -else: - from django.core.validators import URLValidator as DjangoURLValidator - - - class URLValidator(DjangoURLValidator): - def __init__(self, *args, **kwargs): - self.message = kwargs.pop('message', self.message) - super(URLValidator, self).__init__(*args, **kwargs) - - -# EmailValidator requires explicit regex prior to 1.6+ -if django.VERSION >= (1, 6): - from django.core.validators import EmailValidator -else: - from django.core.validators import EmailValidator as DjangoEmailValidator - from django.core.validators import email_re - - - class EmailValidator(DjangoEmailValidator): - def __init__(self, *args, **kwargs): - super(EmailValidator, self).__init__(email_re, *args, **kwargs) - - # PATCH method is not implemented by Django if 'patch' not in View.http_method_names: View.http_method_names = View.http_method_names + ['patch'] diff --git a/rest_framework/fields.py b/rest_framework/fields.py index 86e86f86ef..09284dd554 100644 --- a/rest_framework/fields.py +++ b/rest_framework/fields.py @@ -11,7 +11,9 @@ from django.conf import settings from django.core.exceptions import ValidationError as DjangoValidationError from django.core.exceptions import ObjectDoesNotExist -from django.core.validators import RegexValidator, ip_address_validators +from django.core.validators import ( + EmailValidator, RegexValidator, URLValidator, ip_address_validators +) from django.forms import FilePathField as DjangoFilePathField from django.forms import ImageField as DjangoImageField from django.utils import six, timezone @@ -23,9 +25,9 @@ from rest_framework import ISO_8601 from rest_framework.compat import ( - EmailValidator, MaxLengthValidator, MaxValueValidator, MinLengthValidator, - MinValueValidator, OrderedDict, URLValidator, duration_string, - parse_duration, unicode_repr, unicode_to_repr + MaxLengthValidator, MaxValueValidator, MinLengthValidator, + MinValueValidator, OrderedDict, duration_string, parse_duration, + unicode_repr, unicode_to_repr ) from rest_framework.exceptions import ValidationError from rest_framework.settings import api_settings diff --git a/rest_framework/filters.py b/rest_framework/filters.py index 90c19aba08..c1e0ae054a 100644 --- a/rest_framework/filters.py +++ b/rest_framework/filters.py @@ -11,9 +11,7 @@ from django.db import models from django.utils import six -from rest_framework.compat import ( - distinct, django_filters, get_model_name, guardian -) +from rest_framework.compat import distinct, django_filters, guardian from rest_framework.settings import api_settings FilterSet = django_filters and django_filters.FilterSet or None @@ -202,7 +200,7 @@ def filter_queryset(self, request, queryset, view): model_cls = queryset.model kwargs = { 'app_label': model_cls._meta.app_label, - 'model_name': get_model_name(model_cls) + 'model_name': model_cls._meta.model_name } permission = self.perm_format % kwargs if guardian.VERSION >= (1, 3): diff --git a/rest_framework/permissions.py b/rest_framework/permissions.py index 628538903a..292952cfab 100644 --- a/rest_framework/permissions.py +++ b/rest_framework/permissions.py @@ -5,8 +5,6 @@ from django.http import Http404 -from rest_framework.compat import get_model_name - SAFE_METHODS = ('GET', 'HEAD', 'OPTIONS') @@ -104,7 +102,7 @@ def get_required_permissions(self, method, model_cls): """ kwargs = { 'app_label': model_cls._meta.app_label, - 'model_name': get_model_name(model_cls) + 'model_name': model_cls._meta.model_name } return [perm % kwargs for perm in self.perms_map[method]] @@ -166,7 +164,7 @@ class DjangoObjectPermissions(DjangoModelPermissions): def get_required_object_permissions(self, method, model_cls): kwargs = { 'app_label': model_cls._meta.app_label, - 'model_name': get_model_name(model_cls) + 'model_name': model_cls._meta.model_name } return [perm % kwargs for perm in self.perms_map[method]] diff --git a/rest_framework/response.py b/rest_framework/response.py index 3f038e3b5f..0e97668eb4 100644 --- a/rest_framework/response.py +++ b/rest_framework/response.py @@ -98,7 +98,7 @@ def __getstate__(self): state = super(Response, self).__getstate__() for key in ( 'accepted_renderer', 'renderer_context', 'resolver_match', - 'client', 'request', 'wsgi_request' + 'client', 'request', 'json', 'wsgi_request' ): if key in state: del state[key] diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index fc1fe468d1..0b0b1aa623 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -12,6 +12,8 @@ """ from __future__ import unicode_literals +import warnings + from django.db import models from django.db.models.fields import Field as DjangoModelField from django.db.models.fields import FieldDoesNotExist @@ -51,6 +53,8 @@ 'instance', 'data', 'partial', 'context', 'allow_null' ) +ALL_FIELDS = '__all__' + # BaseSerializer # -------------- @@ -957,10 +961,10 @@ def get_field_names(self, declared_fields, info): fields = getattr(self.Meta, 'fields', None) exclude = getattr(self.Meta, 'exclude', None) - if fields and not isinstance(fields, (list, tuple)): + if fields and fields != ALL_FIELDS and not isinstance(fields, (list, tuple)): raise TypeError( - 'The `fields` option must be a list or tuple. Got %s.' % - type(fields).__name__ + 'The `fields` option must be a list or tuple or "__all__". ' + 'Got %s.' % type(fields).__name__ ) if exclude and not isinstance(exclude, (list, tuple)): @@ -976,6 +980,20 @@ def get_field_names(self, declared_fields, info): ) ) + if fields is None and exclude is None: + warnings.warn( + "Creating a ModelSerializer without either the 'fields' " + "attribute or the 'exclude' attribute is pending deprecation " + "since 3.3.0. Add an explicit fields = '__all__' to the " + "{serializer_class} serializer.".format( + serializer_class=self.__class__.__name__ + ), + PendingDeprecationWarning + ) + + if fields == ALL_FIELDS: + fields = None + if fields is not None: # Ensure that all declared fields have also been included in the # `Meta.fields` option. diff --git a/rest_framework/templatetags/rest_framework.py b/rest_framework/templatetags/rest_framework.py index 0069d9a5e2..08acecef77 100644 --- a/rest_framework/templatetags/rest_framework.py +++ b/rest_framework/templatetags/rest_framework.py @@ -41,8 +41,9 @@ def optional_login(request): except NoReverseMatch: return '' - snippet = "