Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Compat updates for version 3.3 #3342

Merged
merged 32 commits into from
Sep 28, 2015
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
48540f1
unittest compat fallback
tomchristie Aug 27, 2015
f691006
Resolve generic fields import
tomchristie Aug 27, 2015
4f27697
Fix get_model import
tomchristie Aug 27, 2015
654e0e4
Update ModelSerializer fields behavior
jpadilla Aug 27, 2015
e70da5a
Compat for GenericForeignKey, GenericRelation
tomchristie Aug 28, 2015
25c4c7f
Pep8 fix
tomchristie Aug 28, 2015
24a2c3f
Resolve unittest compat
tomchristie Aug 28, 2015
a5ddd90
Log in and log out require escape and mark_safe
tomchristie Aug 28, 2015
6fa534f
Fix urlpatterns in test
tomchristie Aug 28, 2015
7560e83
Drop unused patterns
tomchristie Aug 28, 2015
b51c1ff
Django 1.9's test case HttpResponse.json() is not cachable.
tomchristie Aug 28, 2015
8db6367
Deal with 1.9's differing null behavior on reverse relationships and m2m
tomchristie Aug 28, 2015
f3ef13a
Update to match docs on ModelForm fields
jpadilla Aug 28, 2015
1fe8e9a
Add note on deprecation path
jpadilla Aug 28, 2015
7863284
Comment against model_field.null 1.98 behavior
tomchristie Aug 28, 2015
9dd1b25
Update ModelSerializer fields docs
jpadilla Aug 28, 2015
f87573f
Merge pull request #3345 from jpadilla/fields
tomchristie Aug 28, 2015
afd2a8f
Adjust ModelField.null mappings now that Django-25320 is resolved
tomchristie Sep 3, 2015
a3067be
Merge branch 'master' into version-3.3
tomchristie Sep 17, 2015
796baab
Drop Pythons 3.2, 3.3 for Django `master`
Sep 21, 2015
366dff4
Test Python 3.5 against Django `master`
Sep 21, 2015
8edb9d3
Drop testing against Django 1.5
Sep 21, 2015
9216dc9
Remove Django 1.5 EmailValidator fallback
Sep 21, 2015
e625cff
Remove Django 1.5 URLValidator fallback
Sep 21, 2015
4a1ab3c
Fix isort errors
Sep 21, 2015
25de8c9
Remove Django 1.5 get_model_name fallback
Sep 21, 2015
8ea1606
Remove Django 1.5 clean_manytomany_helptext fallback
Sep 21, 2015
2aa33ed
Adjust README and Release Notes
Sep 21, 2015
5b2b675
Merge pull request #3421 from carltongibson/supported-versions-2
tomchristie Sep 22, 2015
524a28c
Exclude Guardian testing against Django master
Sep 22, 2015
eccf7cc
Merge pull request #3427 from carltongibson/exlude-guardian
tomchristie Sep 22, 2015
ca8313a
Merge branch 'master' into version-3.3
tomchristie Sep 28, 2015
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 2 additions & 9 deletions .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
25 changes: 23 additions & 2 deletions docs/api-guide/serializers.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand All @@ -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:

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

Expand Down
11 changes: 11 additions & 0 deletions docs/topics/release-notes.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
71 changes: 26 additions & 45 deletions rest_framework/compat.py
Original file line number Diff line number Diff line change
Expand Up @@ -67,30 +67,45 @@ 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
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
Expand All @@ -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
Expand Down Expand Up @@ -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']
Expand Down
10 changes: 6 additions & 4 deletions rest_framework/fields.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down
6 changes: 2 additions & 4 deletions rest_framework/filters.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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):
Expand Down
6 changes: 2 additions & 4 deletions rest_framework/permissions.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,6 @@

from django.http import Http404

from rest_framework.compat import get_model_name

SAFE_METHODS = ('GET', 'HEAD', 'OPTIONS')


Expand Down Expand Up @@ -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]]

Expand Down Expand Up @@ -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]]

Expand Down
2 changes: 1 addition & 1 deletion rest_framework/response.py
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down
24 changes: 21 additions & 3 deletions rest_framework/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -51,6 +53,8 @@
'instance', 'data', 'partial', 'context', 'allow_null'
)

ALL_FIELDS = '__all__'


# BaseSerializer
# --------------
Expand Down Expand Up @@ -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)):
Expand All @@ -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.
Expand Down
9 changes: 5 additions & 4 deletions rest_framework/templatetags/rest_framework.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down
3 changes: 1 addition & 2 deletions rest_framework/utils/field_mapping.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@
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 = (
Expand Down Expand Up @@ -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:
Expand Down
4 changes: 2 additions & 2 deletions rest_framework/utils/model_meta.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
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
Expand Down Expand Up @@ -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))
Expand Down
Loading