Merge pull request #3342 from tomchristie/version-3.3

Compat updates for version 3.3
This commit is contained in:
Tom Christie 2015-09-28 11:06:42 +01:00
commit 4762a73e34
20 changed files with 177 additions and 107 deletions

View File

@ -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

View File

@ -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

View File

@ -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.

View File

@ -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
<!-- 3.3.0 -->
[gh3421]: https://github.com/tomchristie/django-rest-framework/pulls/3421

View File

@ -67,6 +67,14 @@ except ImportError:
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 @@ except ImportError:
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 @@ except ImportError:
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 @@ else:
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']

View File

@ -11,7 +11,9 @@ import uuid
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 django.utils.translation import ugettext_lazy as _
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

View File

@ -11,9 +11,7 @@ from django.core.exceptions import ImproperlyConfigured
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 @@ class DjangoObjectPermissionsFilter(BaseFilterBackend):
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):

View File

@ -5,8 +5,6 @@ from __future__ import unicode_literals
from django.http import Http404
from rest_framework.compat import get_model_name
SAFE_METHODS = ('GET', 'HEAD', 'OPTIONS')
@ -104,7 +102,7 @@ class DjangoModelPermissions(BasePermission):
"""
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]]

View File

@ -98,7 +98,7 @@ class Response(SimpleTemplateResponse):
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]

View File

@ -12,6 +12,8 @@ response content is handled by parsers and renderers.
"""
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 @@ LIST_SERIALIZER_KWARGS = (
'instance', 'data', 'partial', 'context', 'allow_null'
)
ALL_FIELDS = '__all__'
# BaseSerializer
# --------------
@ -957,10 +961,10 @@ class ModelSerializer(Serializer):
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 @@ class ModelSerializer(Serializer):
)
)
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.

View File

@ -41,8 +41,9 @@ def optional_login(request):
except NoReverseMatch:
return ''
snippet = "<li><a href='{href}?next={next}'>Log in</a></li>".format(href=login_url, next=escape(request.path))
return snippet
snippet = "<li><a href='{href}?next={next}'>Log in</a></li>"
snippet = snippet.format(href=login_url, next=escape(request.path))
return mark_safe(snippet)
@register.simple_tag
@ -64,8 +65,8 @@ def optional_logout(request, user):
<li><a href='{href}?next={next}'>Log out</a></li>
</ul>
</li>"""
return snippet.format(user=user, href=logout_url, next=escape(request.path))
snippet = snippet.format(user=escape(user), href=logout_url, next=escape(request.path))
return mark_safe(snippet)
@register.simple_tag

View File

@ -8,7 +8,6 @@ from django.core import validators
from django.db import models
from django.utils.text import capfirst
from rest_framework.compat import clean_manytomany_helptext
from rest_framework.validators import UniqueValidator
NUMERIC_FIELD_TYPES = (
@ -230,7 +229,7 @@ def get_relation_kwargs(field_name, relation_info):
if model_field:
if model_field.verbose_name and needs_label(model_field, field_name):
kwargs['label'] = capfirst(model_field.verbose_name)
help_text = clean_manytomany_helptext(model_field.help_text)
help_text = model_field.help_text
if help_text:
kwargs['help_text'] = help_text
if not model_field.editable:

View File

@ -12,7 +12,7 @@ from django.core.exceptions import ImproperlyConfigured
from django.db import models
from django.utils import six
from rest_framework.compat import OrderedDict
from rest_framework.compat import OrderedDict, get_model
FieldInfo = namedtuple('FieldResult', [
'pk', # Model field instance
@ -45,7 +45,7 @@ def _resolve_model(obj):
"""
if isinstance(obj, six.string_types) and len(obj.split('.')) == 2:
app_name, model_name = obj.split('.')
resolved_model = models.get_model(app_name, model_name)
resolved_model = get_model(app_name, model_name)
if resolved_model is None:
msg = "Django did not return a model for {0}.{1}"
raise ImproperlyConfigured(msg.format(app_name, model_name))

View File

@ -1,13 +1,13 @@
from __future__ import unicode_literals
from django.conf.urls import patterns, url
from django.conf.urls import url
from django.db import connection, connections, transaction
from django.http import Http404
from django.test import TestCase, TransactionTestCase
from django.utils.decorators import method_decorator
from django.utils.unittest import skipUnless
from rest_framework import status
from rest_framework.compat import unittest
from rest_framework.exceptions import APIException
from rest_framework.response import Response
from rest_framework.test import APIRequestFactory
@ -35,8 +35,10 @@ class APIExceptionView(APIView):
raise APIException
@skipUnless(connection.features.uses_savepoints,
"'atomic' requires transactions and savepoints.")
@unittest.skipUnless(
connection.features.uses_savepoints,
"'atomic' requires transactions and savepoints."
)
class DBTransactionTests(TestCase):
def setUp(self):
self.view = BasicView.as_view()
@ -55,8 +57,10 @@ class DBTransactionTests(TestCase):
assert BasicModel.objects.count() == 1
@skipUnless(connection.features.uses_savepoints,
"'atomic' requires transactions and savepoints.")
@unittest.skipUnless(
connection.features.uses_savepoints,
"'atomic' requires transactions and savepoints."
)
class DBTransactionErrorTests(TestCase):
def setUp(self):
self.view = ErrorView.as_view()
@ -83,8 +87,10 @@ class DBTransactionErrorTests(TestCase):
assert BasicModel.objects.count() == 1
@skipUnless(connection.features.uses_savepoints,
"'atomic' requires transactions and savepoints.")
@unittest.skipUnless(
connection.features.uses_savepoints,
"'atomic' requires transactions and savepoints."
)
class DBTransactionAPIExceptionTests(TestCase):
def setUp(self):
self.view = APIExceptionView.as_view()
@ -113,8 +119,10 @@ class DBTransactionAPIExceptionTests(TestCase):
assert BasicModel.objects.count() == 0
@skipUnless(connection.features.uses_savepoints,
"'atomic' requires transactions and savepoints.")
@unittest.skipUnless(
connection.features.uses_savepoints,
"'atomic' requires transactions and savepoints."
)
class NonAtomicDBTransactionAPIExceptionTests(TransactionTestCase):
@property
def urls(self):
@ -127,9 +135,8 @@ class NonAtomicDBTransactionAPIExceptionTests(TransactionTestCase):
BasicModel.objects.all()
raise Http404
return patterns(
'',
url(r'^$', NonAtomicAPIExceptionView.as_view())
return (
url(r'^$', NonAtomicAPIExceptionView.as_view()),
)
def setUp(self):

View File

@ -8,12 +8,11 @@ from django.core.urlresolvers import reverse
from django.db import models
from django.test import TestCase
from django.test.utils import override_settings
from django.utils import unittest
from django.utils.dateparse import parse_date
from django.utils.six.moves import reload_module
from rest_framework import filters, generics, serializers, status
from rest_framework.compat import django_filters
from rest_framework.compat import django_filters, unittest
from rest_framework.test import APIRequestFactory
from .models import BaseFilterableItem, BasicModel, FilterableItem

View File

@ -321,6 +321,21 @@ class TestRegularFieldMappings(TestCase):
ExampleSerializer()
def test_fields_and_exclude_behavior(self):
class ImplicitFieldsSerializer(serializers.ModelSerializer):
class Meta:
model = RegularFieldsModel
class ExplicitFieldsSerializer(serializers.ModelSerializer):
class Meta:
model = RegularFieldsModel
fields = '__all__'
implicit = ImplicitFieldsSerializer()
explicit = ExplicitFieldsSerializer()
assert implicit.data == explicit.data
@pytest.mark.skipif(django.VERSION < (1, 8),
reason='DurationField is only available for django1.8+')

View File

@ -6,13 +6,12 @@ from django.contrib.auth.models import Group, Permission, User
from django.core.urlresolvers import ResolverMatch
from django.db import models
from django.test import TestCase
from django.utils import unittest
from rest_framework import (
HTTP_HEADER_ENCODING, authentication, generics, permissions, serializers,
status
)
from rest_framework.compat import get_model_name, guardian
from rest_framework.compat import guardian, unittest
from rest_framework.filters import DjangoObjectPermissionsFilter
from rest_framework.routers import DefaultRouter
from rest_framework.test import APIRequestFactory
@ -279,7 +278,7 @@ class ObjectPermissionsIntegrationTests(TestCase):
# give everyone model level permissions, as we are not testing those
everyone = Group.objects.create(name='everyone')
model_name = get_model_name(BasicPermModel)
model_name = BasicPermModel._meta.model_name
app_label = BasicPermModel._meta.app_label
f = '{0}_{1}'.format
perms = {

View File

@ -1,14 +1,12 @@
from __future__ import unicode_literals
from django.contrib.contenttypes.generic import (
GenericForeignKey, GenericRelation
)
from django.contrib.contenttypes.models import ContentType
from django.db import models
from django.test import TestCase
from django.utils.encoding import python_2_unicode_compatible
from rest_framework import serializers
from rest_framework.compat import GenericForeignKey, GenericRelation
@python_2_unicode_compatible

View File

@ -150,12 +150,12 @@ class ResolveModelWithPatchedDjangoTests(TestCase):
def setUp(self):
"""Monkeypatch get_model."""
self.get_model = rest_framework.utils.model_meta.models.get_model
self.get_model = rest_framework.utils.model_meta.get_model
def get_model(app_label, model_name):
return None
rest_framework.utils.model_meta.models.get_model = get_model
rest_framework.utils.model_meta.get_model = get_model
def tearDown(self):
"""Revert monkeypatching."""

36
tox.ini
View File

@ -4,15 +4,23 @@ addopts=--tb=short
[tox]
envlist =
py27-{lint,docs},
{py26,py27,py32,py33,py34}-django{15,16},
{py27,py32,py33,py34}-django{17,18,master}
{py26,py27,py32,py33,py34}-django16,
{py27,py32,py33,py34}-django{17,18},
{py27,py34,py35}-django{master}
[testenv]
basepython =
py26: python2.6
py27: python2.7
py32: python3.2
py33: python3.3
py34: python3.4
py35: python3.5
commands = ./runtests.py --fast {posargs} --coverage
setenv =
PYTHONDONTWRITEBYTECODE=1
deps =
django15: Django==1.5.6 # Should track minimum supported
django16: Django==1.6.3 # Should track minimum supported
django17: Django==1.7.10 # Should track maximum supported
django18: Django==1.8.4 # Should track maximum supported
@ -31,3 +39,25 @@ commands = mkdocs build
deps =
-rrequirements/requirements-testing.txt
-rrequirements/requirements-documentation.txt
# Specify explicitly to exclude Django Guardian against Django master (various Pythons)
[testenv:py27-djangomaster]
deps =
https://github.com/django/django/archive/master.tar.gz
-rrequirements/requirements-testing.txt
markdown==2.5.2
django-filter==0.10.0
[testenv:py34-djangomaster]
deps =
https://github.com/django/django/archive/master.tar.gz
-rrequirements/requirements-testing.txt
markdown==2.5.2
django-filter==0.10.0
[testenv:py35-djangomaster]
deps =
https://github.com/django/django/archive/master.tar.gz
-rrequirements/requirements-testing.txt
markdown==2.5.2
django-filter==0.10.0