From 48540f180a34345a6278e527cd4e494826f1b8f2 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Thu, 27 Aug 2015 17:11:53 +0100 Subject: [PATCH 01/27] unittest compat fallback --- rest_framework/compat.py | 8 ++++++++ tests/test_atomic_requests.py | 10 +++++----- tests/test_filters.py | 3 +-- tests/test_permissions.py | 3 +-- 4 files changed, 15 insertions(+), 9 deletions(-) diff --git a/rest_framework/compat.py b/rest_framework/compat.py index 2cff610882..164cf20031 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.test.utils import unittest + + # contrib.postgres only supported from 1.8 onwards. try: from django.contrib.postgres import fields as postgres_fields diff --git a/tests/test_atomic_requests.py b/tests/test_atomic_requests.py index d0d088f52e..8f68306638 100644 --- a/tests/test_atomic_requests.py +++ b/tests/test_atomic_requests.py @@ -5,9 +5,9 @@ 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,7 +35,7 @@ def post(self, request, *args, **kwargs): raise APIException -@skipUnless(connection.features.uses_savepoints, +@unittest.skipUnless(connection.features.uses_savepoints, "'atomic' requires transactions and savepoints.") class DBTransactionTests(TestCase): def setUp(self): @@ -55,7 +55,7 @@ def test_no_exception_conmmit_transaction(self): assert BasicModel.objects.count() == 1 -@skipUnless(connection.features.uses_savepoints, +@unittest.skipUnless(connection.features.uses_savepoints, "'atomic' requires transactions and savepoints.") class DBTransactionErrorTests(TestCase): def setUp(self): @@ -83,7 +83,7 @@ def test_generic_exception_delegate_transaction_management(self): assert BasicModel.objects.count() == 1 -@skipUnless(connection.features.uses_savepoints, +@unittest.skipUnless(connection.features.uses_savepoints, "'atomic' requires transactions and savepoints.") class DBTransactionAPIExceptionTests(TestCase): def setUp(self): @@ -113,7 +113,7 @@ def test_api_exception_rollback_transaction(self): assert BasicModel.objects.count() == 0 -@skipUnless(connection.features.uses_savepoints, +@unittest.skipUnless(connection.features.uses_savepoints, "'atomic' requires transactions and savepoints.") class NonAtomicDBTransactionAPIExceptionTests(TransactionTestCase): @property diff --git a/tests/test_filters.py b/tests/test_filters.py index 0610b08557..bce6e08faf 100644 --- a/tests/test_filters.py +++ b/tests/test_filters.py @@ -8,12 +8,11 @@ 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 diff --git a/tests/test_permissions.py b/tests/test_permissions.py index 3980200020..ffc262a412 100644 --- a/tests/test_permissions.py +++ b/tests/test_permissions.py @@ -6,13 +6,12 @@ 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 get_model_name, guardian, unittest from rest_framework.filters import DjangoObjectPermissionsFilter from rest_framework.routers import DefaultRouter from rest_framework.test import APIRequestFactory From f691006f2c79b408b819c716fba0a1bc4ed71fc1 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Thu, 27 Aug 2015 17:16:19 +0100 Subject: [PATCH 02/27] Resolve generic fields import --- tests/test_relations_generic.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_relations_generic.py b/tests/test_relations_generic.py index 962857365f..340d4d1d1f 100644 --- a/tests/test_relations_generic.py +++ b/tests/test_relations_generic.py @@ -1,6 +1,6 @@ from __future__ import unicode_literals -from django.contrib.contenttypes.generic import ( +from django.contrib.contenttypes.fields import ( GenericForeignKey, GenericRelation ) from django.contrib.contenttypes.models import ContentType From 4f2769746773f0a796206f4fcf01f180bdaff640 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Thu, 27 Aug 2015 17:28:12 +0100 Subject: [PATCH 03/27] Fix get_model import --- rest_framework/compat.py | 8 ++++++++ rest_framework/utils/model_meta.py | 4 ++-- tests/test_utils.py | 4 ++-- 3 files changed, 12 insertions(+), 4 deletions(-) diff --git a/rest_framework/compat.py b/rest_framework/compat.py index 164cf20031..607778a9ea 100644 --- a/rest_framework/compat.py +++ b/rest_framework/compat.py @@ -82,6 +82,14 @@ 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 + + # django-filter is optional try: import django_filters diff --git a/rest_framework/utils/model_meta.py b/rest_framework/utils/model_meta.py index 3bcd9049c4..9718647695 100644 --- a/rest_framework/utils/model_meta.py +++ b/rest_framework/utils/model_meta.py @@ -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 @@ -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)) diff --git a/tests/test_utils.py b/tests/test_utils.py index 062f78e11a..4c9fd03c85 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -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.""" From 654e0e45279b5023cea7574bda1ebd63dae96347 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Padilla?= Date: Thu, 27 Aug 2015 13:09:08 -0400 Subject: [PATCH 04/27] Update ModelSerializer fields behavior --- rest_framework/serializers.py | 21 +++++++++++++++++++-- tests/test_model_serializer.py | 15 +++++++++++++++ 2 files changed, 34 insertions(+), 2 deletions(-) diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index c7d4405c59..acaf3bef73 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 # -------------- @@ -943,13 +947,13 @@ 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__ ) - if exclude and not isinstance(exclude, (list, tuple)): + if exclude and exclude != ALL_FIELDS and not isinstance(exclude, (list, tuple)): raise TypeError( 'The `exclude` option must be a list or tuple. Got %s.' % type(exclude).__name__ @@ -962,6 +966,19 @@ 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 will be prohibited. " + "The {serializer_class} serializer needs updating.".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/tests/test_model_serializer.py b/tests/test_model_serializer.py index 777b956c4b..89557fa1dd 100644 --- a/tests/test_model_serializer.py +++ b/tests/test_model_serializer.py @@ -321,6 +321,21 @@ class Meta: 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+') From e70da5ac6b563cf82e8bc86f4a2f7103efc297c8 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Fri, 28 Aug 2015 10:03:08 +0100 Subject: [PATCH 05/27] Compat for GenericForeignKey, GenericRelation --- rest_framework/compat.py | 10 ++++++++++ tests/test_relations_generic.py | 4 +--- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/rest_framework/compat.py b/rest_framework/compat.py index 607778a9ea..fa42f95edb 100644 --- a/rest_framework/compat.py +++ b/rest_framework/compat.py @@ -90,6 +90,16 @@ def distinct(queryset, base): 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 diff --git a/tests/test_relations_generic.py b/tests/test_relations_generic.py index 340d4d1d1f..5cb2dfc05b 100644 --- a/tests/test_relations_generic.py +++ b/tests/test_relations_generic.py @@ -1,14 +1,12 @@ from __future__ import unicode_literals -from django.contrib.contenttypes.fields 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 From 25c4c7f9fd3a5d789ec772f341fb5e036fe073dd Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Fri, 28 Aug 2015 10:03:16 +0100 Subject: [PATCH 06/27] Pep8 fix --- tests/test_atomic_requests.py | 24 ++++++++++++++++-------- 1 file changed, 16 insertions(+), 8 deletions(-) diff --git a/tests/test_atomic_requests.py b/tests/test_atomic_requests.py index 8f68306638..a97efb69ce 100644 --- a/tests/test_atomic_requests.py +++ b/tests/test_atomic_requests.py @@ -35,8 +35,10 @@ def post(self, request, *args, **kwargs): raise APIException -@unittest.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 @@ def test_no_exception_conmmit_transaction(self): assert BasicModel.objects.count() == 1 -@unittest.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 @@ def test_generic_exception_delegate_transaction_management(self): assert BasicModel.objects.count() == 1 -@unittest.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 @@ def test_api_exception_rollback_transaction(self): assert BasicModel.objects.count() == 0 -@unittest.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): From 24a2c3f5c3349ec941790240c1f372a6de723e1b Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Fri, 28 Aug 2015 10:19:18 +0100 Subject: [PATCH 07/27] Resolve unittest compat --- rest_framework/compat.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rest_framework/compat.py b/rest_framework/compat.py index fa42f95edb..a512af771e 100644 --- a/rest_framework/compat.py +++ b/rest_framework/compat.py @@ -72,7 +72,7 @@ def distinct(queryset, base): import unittest unittest.skipUnless except (ImportError, AttributeError): - from django.test.utils import unittest + from django.utils import unittest # contrib.postgres only supported from 1.8 onwards. From a5ddd90df03fe04793e605d26d01f86391fa4771 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Fri, 28 Aug 2015 10:27:49 +0100 Subject: [PATCH 08/27] Log in and log out require escape and mark_safe --- rest_framework/templatetags/rest_framework.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) 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 = "
  • Log in
  • ".format(href=login_url, next=escape(request.path)) - return snippet + snippet = "
  • Log in
  • " + 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):
  • Log out
  • """ - - 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 From 6fa534f21475f4c9527b1181f8a427978ce1c085 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Fri, 28 Aug 2015 10:44:49 +0100 Subject: [PATCH 09/27] Fix urlpatterns in test --- tests/test_atomic_requests.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/tests/test_atomic_requests.py b/tests/test_atomic_requests.py index a97efb69ce..f24c7e6853 100644 --- a/tests/test_atomic_requests.py +++ b/tests/test_atomic_requests.py @@ -135,9 +135,8 @@ def get(self, request, *args, **kwargs): BasicModel.objects.all() raise Http404 - return patterns( - '', - url(r'^$', NonAtomicAPIExceptionView.as_view()) + return ( + url(r'^$', NonAtomicAPIExceptionView.as_view()), ) def setUp(self): From 7560e8381f90c646bdfeaf1ee491482f39766f53 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Fri, 28 Aug 2015 10:53:44 +0100 Subject: [PATCH 10/27] Drop unused patterns --- tests/test_atomic_requests.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_atomic_requests.py b/tests/test_atomic_requests.py index f24c7e6853..2d973cb8a9 100644 --- a/tests/test_atomic_requests.py +++ b/tests/test_atomic_requests.py @@ -1,6 +1,6 @@ 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 b51c1ff0b0535a135ba018064c99a5995a9d8893 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Fri, 28 Aug 2015 11:55:23 +0100 Subject: [PATCH 11/27] Django 1.9's test case HttpResponse.json() is not cachable. --- rest_framework/response.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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] From 8db6367188d27a41a42bf36eef12774e7cc59354 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Fri, 28 Aug 2015 12:08:32 +0100 Subject: [PATCH 12/27] Deal with 1.9's differing null behavior on reverse relationships and m2m --- rest_framework/utils/field_mapping.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/rest_framework/utils/field_mapping.py b/rest_framework/utils/field_mapping.py index f2598974ef..7805a2853e 100644 --- a/rest_framework/utils/field_mapping.py +++ b/rest_framework/utils/field_mapping.py @@ -233,9 +233,11 @@ def get_relation_kwargs(field_name, relation_info): # No further keyword arguments are valid. return kwargs - if model_field.has_default() or model_field.blank or model_field.null: + null = model_field.null and not to_many + + if model_field.has_default() or model_field.blank or null: kwargs['required'] = False - if model_field.null: + if null: kwargs['allow_null'] = True if model_field.validators: kwargs['validators'] = model_field.validators From f3ef13ab59c1ae9504e830ea3f45210fab5e973d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Padilla?= Date: Fri, 28 Aug 2015 08:05:20 -0400 Subject: [PATCH 13/27] Update to match docs on ModelForm fields --- docs/api-guide/serializers.md | 22 ++++++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/docs/api-guide/serializers.md b/docs/api-guide/serializers.md index abdb67afac..5138c6ce07 100644 --- a/docs/api-guide/serializers.md +++ b/docs/api-guide/serializers.md @@ -453,14 +453,31 @@ 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`. +It is strongly recommended that you explicitly set all fields that should be edited in the serializer using the `fields` attribute. Failure to do so can easily lead to security problems when a serializer unexpectedly allows a user to set certain fields, especially when new fields are added to a model. + +The alternative approach would be to include all fields automatically, or blacklist only some. This fundamental approach is known to be much less secure and has led to serious exploits on major websites (e.g. [GitHub][github-vuln-blog]). + +There are, however, two shortcuts available for cases where you can guarantee these security concerns do not apply to you: + +1. 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 = ('id', 'account_name', 'users', 'created') + fields = '__all__' + +2. Set the exclude attribute of the ModelForm’s inner Meta class to a list of fields to be excluded from the form. + +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` option will normally map to model fields on the model class. @@ -1035,6 +1052,7 @@ The [django-rest-framework-gis][django-rest-framework-gis] package provides a `G The [django-rest-framework-hstore][django-rest-framework-hstore] package provides an `HStoreSerializer` to support [django-hstore][django-hstore] `DictionaryField` model field and its `schema-mode` feature. [cite]: https://groups.google.com/d/topic/django-users/sVFaOfQi4wY/discussion +[github-vuln-blog]: https://github.com/blog/1068-public-key-security-vulnerability-and-mitigation [relations]: relations.md [model-managers]: https://docs.djangoproject.com/en/dev/topics/db/managers/ [encapsulation-blogpost]: http://www.dabapps.com/blog/django-models-and-encapsulation/ From 1fe8e9a0bf37e045457aa91a19748ea1a6479a1b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Padilla?= Date: Fri, 28 Aug 2015 08:11:07 -0400 Subject: [PATCH 14/27] Add note on deprecation path --- docs/api-guide/serializers.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/docs/api-guide/serializers.md b/docs/api-guide/serializers.md index 5138c6ce07..ed1ada92c5 100644 --- a/docs/api-guide/serializers.md +++ b/docs/api-guide/serializers.md @@ -483,6 +483,12 @@ The names in the `fields` option will normally map to model fields on the model Alternatively names in the `fields` options can map to properties or methods which take no arguments that exist on the model class. +--- + +**Note**: Before version 3.3, the `'__all__'` shortcut did not exist, but omitting the fields attribute had the same effect. Omitting both fields and exclude is now deprecated, but will continue to work as before until version 3.5 + +--- + ## Specifying nested serialization The default `ModelSerializer` uses primary keys for relationships, but you can also easily generate nested representations using the `depth` option: From 78632849cf7d6599883674bbb6baec87dab4fefd Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Fri, 28 Aug 2015 13:29:57 +0100 Subject: [PATCH 15/27] Comment against model_field.null 1.98 behavior --- rest_framework/utils/field_mapping.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/rest_framework/utils/field_mapping.py b/rest_framework/utils/field_mapping.py index 7805a2853e..62e4dbc127 100644 --- a/rest_framework/utils/field_mapping.py +++ b/rest_framework/utils/field_mapping.py @@ -233,6 +233,9 @@ def get_relation_kwargs(field_name, relation_info): # No further keyword arguments are valid. return kwargs + # Currently required for 1.9 master behavior, + # may be able to remove this with 1.9 alpha. + # See https://code.djangoproject.com/ticket/25320 null = model_field.null and not to_many if model_field.has_default() or model_field.blank or null: From 9dd1b2516be3b1993219ad93e5802f5b8d25e324 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Padilla?= Date: Fri, 28 Aug 2015 09:51:11 -0400 Subject: [PATCH 16/27] Update ModelSerializer fields docs --- docs/api-guide/serializers.md | 25 +++++++++++-------------- 1 file changed, 11 insertions(+), 14 deletions(-) diff --git a/docs/api-guide/serializers.md b/docs/api-guide/serializers.md index ed1ada92c5..cefcfa0972 100644 --- a/docs/api-guide/serializers.md +++ b/docs/api-guide/serializers.md @@ -432,6 +432,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. @@ -453,13 +454,16 @@ To do so, open the Django shell, using `python manage.py shell`, then import the ## Specifying which fields to include -It is strongly recommended that you explicitly set all fields that should be edited in the serializer using the `fields` attribute. Failure to do so can easily lead to security problems when a serializer unexpectedly allows a user to set certain fields, especially when new fields are added to a model. +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. -The alternative approach would be to include all fields automatically, or blacklist only some. This fundamental approach is known to be much less secure and has led to serious exploits on major websites (e.g. [GitHub][github-vuln-blog]). +For example: -There are, however, two shortcuts available for cases where you can guarantee these security concerns do not apply to you: + class AccountSerializer(serializers.ModelSerializer): + class Meta: + model = Account + fields = ('id', 'account_name', 'users', 'created') -1. Set the `fields` attribute to the special value `'__all__'` to indicate that all fields in the model should be used. +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: @@ -468,27 +472,21 @@ For example: model = Account fields = '__all__' -2. Set the exclude attribute of the ModelForm’s inner Meta class to a list of fields to be excluded from the form. +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' + 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` option will normally map to model fields on the model class. +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. ---- - -**Note**: Before version 3.3, the `'__all__'` shortcut did not exist, but omitting the fields attribute had the same effect. Omitting both fields and exclude is now deprecated, but will continue to work as before until version 3.5 - ---- - ## Specifying nested serialization The default `ModelSerializer` uses primary keys for relationships, but you can also easily generate nested representations using the `depth` option: @@ -1058,7 +1056,6 @@ The [django-rest-framework-gis][django-rest-framework-gis] package provides a `G The [django-rest-framework-hstore][django-rest-framework-hstore] package provides an `HStoreSerializer` to support [django-hstore][django-hstore] `DictionaryField` model field and its `schema-mode` feature. [cite]: https://groups.google.com/d/topic/django-users/sVFaOfQi4wY/discussion -[github-vuln-blog]: https://github.com/blog/1068-public-key-security-vulnerability-and-mitigation [relations]: relations.md [model-managers]: https://docs.djangoproject.com/en/dev/topics/db/managers/ [encapsulation-blogpost]: http://www.dabapps.com/blog/django-models-and-encapsulation/ From afd2a8f8f0186cfeb781c33922b27372f97f074b Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Thu, 3 Sep 2015 10:12:52 +0100 Subject: [PATCH 17/27] Adjust ModelField.null mappings now that Django-25320 is resolved --- rest_framework/utils/field_mapping.py | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/rest_framework/utils/field_mapping.py b/rest_framework/utils/field_mapping.py index 62e4dbc127..f2598974ef 100644 --- a/rest_framework/utils/field_mapping.py +++ b/rest_framework/utils/field_mapping.py @@ -233,14 +233,9 @@ def get_relation_kwargs(field_name, relation_info): # No further keyword arguments are valid. return kwargs - # Currently required for 1.9 master behavior, - # may be able to remove this with 1.9 alpha. - # See https://code.djangoproject.com/ticket/25320 - null = model_field.null and not to_many - - if model_field.has_default() or model_field.blank or null: + if model_field.has_default() or model_field.blank or model_field.null: kwargs['required'] = False - if null: + if model_field.null: kwargs['allow_null'] = True if model_field.validators: kwargs['validators'] = model_field.validators From 796baab86f81543019f5757eb0a3ec077dd14d49 Mon Sep 17 00:00:00 2001 From: Carlton Gibson Date: Mon, 21 Sep 2015 15:47:11 +0200 Subject: [PATCH 18/27] Drop Pythons 3.2, 3.3 for Django `master` No longer supported --- .travis.yml | 4 ---- tox.ini | 3 ++- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/.travis.yml b/.travis.yml index 87db5e0465..7513b4db24 100644 --- a/.travis.yml +++ b/.travis.yml @@ -24,16 +24,12 @@ env: - TOX_ENV=py27-django15 - TOX_ENV=py26-django15 - TOX_ENV=py27-djangomaster - - TOX_ENV=py32-djangomaster - - TOX_ENV=py33-djangomaster - TOX_ENV=py34-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 install: diff --git a/tox.ini b/tox.ini index ef505248bb..89d43771ad 100644 --- a/tox.ini +++ b/tox.ini @@ -5,7 +5,8 @@ addopts=--tb=short envlist = py27-{lint,docs}, {py26,py27,py32,py33,py34}-django{15,16}, - {py27,py32,py33,py34}-django{17,18,master} + {py27,py32,py33,py34}-django{17,18}, + {py27,py34}-django{master} [testenv] commands = ./runtests.py --fast {posargs} --coverage From 366dff4f26afe5139f1628052c84258f2bc2a388 Mon Sep 17 00:00:00 2001 From: Carlton Gibson Date: Mon, 21 Sep 2015 15:58:34 +0200 Subject: [PATCH 19/27] Test Python 3.5 against Django `master` --- .travis.yml | 2 ++ tox.ini | 10 +++++++++- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 7513b4db24..a714b80138 100644 --- a/.travis.yml +++ b/.travis.yml @@ -25,12 +25,14 @@ env: - TOX_ENV=py26-django15 - TOX_ENV=py27-djangomaster - TOX_ENV=py34-djangomaster + - TOX_ENV=py35-djangomaster matrix: fast_finish: true allow_failures: - env: TOX_ENV=py27-djangomaster - env: TOX_ENV=py34-djangomaster + - env: TOX_ENV=py35-djangomaster install: - pip install tox diff --git a/tox.ini b/tox.ini index 89d43771ad..924d11df59 100644 --- a/tox.ini +++ b/tox.ini @@ -6,9 +6,17 @@ envlist = py27-{lint,docs}, {py26,py27,py32,py33,py34}-django{15,16}, {py27,py32,py33,py34}-django{17,18}, - {py27,py34}-django{master} + {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 From 8edb9d3c1f6d4582f558fcc1d1024c35353ac9bf Mon Sep 17 00:00:00 2001 From: Carlton Gibson Date: Mon, 21 Sep 2015 16:08:56 +0200 Subject: [PATCH 20/27] Drop testing against Django 1.5 --- .travis.yml | 5 ----- tox.ini | 3 +-- 2 files changed, 1 insertion(+), 7 deletions(-) diff --git a/.travis.yml b/.travis.yml index a714b80138..4a7fdd42f2 100644 --- a/.travis.yml +++ b/.travis.yml @@ -18,11 +18,6 @@ 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=py34-djangomaster - TOX_ENV=py35-djangomaster diff --git a/tox.ini b/tox.ini index 924d11df59..135ca4407d 100644 --- a/tox.ini +++ b/tox.ini @@ -4,7 +4,7 @@ addopts=--tb=short [tox] envlist = py27-{lint,docs}, - {py26,py27,py32,py33,py34}-django{15,16}, + {py26,py27,py32,py33,py34}-django16, {py27,py32,py33,py34}-django{17,18}, {py27,py34,py35}-django{master} @@ -21,7 +21,6 @@ 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 From 9216dc9a254160a9903359fbf0db2b2c5826212c Mon Sep 17 00:00:00 2001 From: Carlton Gibson Date: Mon, 21 Sep 2015 16:20:07 +0200 Subject: [PATCH 21/27] Remove Django 1.5 EmailValidator fallback --- rest_framework/compat.py | 13 ------------- rest_framework/fields.py | 4 ++-- 2 files changed, 2 insertions(+), 15 deletions(-) diff --git a/rest_framework/compat.py b/rest_framework/compat.py index 4d3b29ddd1..fa737f42a3 100644 --- a/rest_framework/compat.py +++ b/rest_framework/compat.py @@ -183,19 +183,6 @@ def __init__(self, *args, **kwargs): 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 7c48c621ed..63e0b2dbf2 100644 --- a/rest_framework/fields.py +++ b/rest_framework/fields.py @@ -11,7 +11,7 @@ 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, ip_address_validators from django.forms import FilePathField as DjangoFilePathField from django.forms import ImageField as DjangoImageField from django.utils import six, timezone @@ -23,7 +23,7 @@ from rest_framework import ISO_8601 from rest_framework.compat import ( - EmailValidator, MaxLengthValidator, MaxValueValidator, MinLengthValidator, + MaxLengthValidator, MaxValueValidator, MinLengthValidator, MinValueValidator, OrderedDict, URLValidator, duration_string, parse_duration, unicode_repr, unicode_to_repr ) From e625cff8a5e4e4353d280fdd4c305dc6dbeed999 Mon Sep 17 00:00:00 2001 From: Carlton Gibson Date: Mon, 21 Sep 2015 16:22:46 +0200 Subject: [PATCH 22/27] Remove Django 1.5 URLValidator fallback --- rest_framework/compat.py | 13 ------------- rest_framework/fields.py | 4 ++-- 2 files changed, 2 insertions(+), 15 deletions(-) diff --git a/rest_framework/compat.py b/rest_framework/compat.py index fa737f42a3..c1074b7f8b 100644 --- a/rest_framework/compat.py +++ b/rest_framework/compat.py @@ -170,19 +170,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) - - # 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 63e0b2dbf2..c44440ecdf 100644 --- a/rest_framework/fields.py +++ b/rest_framework/fields.py @@ -11,7 +11,7 @@ from django.conf import settings from django.core.exceptions import ValidationError as DjangoValidationError from django.core.exceptions import ObjectDoesNotExist -from django.core.validators import EmailValidator, RegexValidator, ip_address_validators +from django.core.validators import EmailValidator, RegexValidator, ip_address_validators, URLValidator from django.forms import FilePathField as DjangoFilePathField from django.forms import ImageField as DjangoImageField from django.utils import six, timezone @@ -24,7 +24,7 @@ from rest_framework import ISO_8601 from rest_framework.compat import ( MaxLengthValidator, MaxValueValidator, MinLengthValidator, - MinValueValidator, OrderedDict, URLValidator, duration_string, + MinValueValidator, OrderedDict, duration_string, parse_duration, unicode_repr, unicode_to_repr ) from rest_framework.exceptions import ValidationError From 4a1ab3c18c7c85c9d962e7b6605e7d57423d29fe Mon Sep 17 00:00:00 2001 From: Carlton Gibson Date: Mon, 21 Sep 2015 16:29:33 +0200 Subject: [PATCH 23/27] Fix isort errors --- rest_framework/fields.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/rest_framework/fields.py b/rest_framework/fields.py index c44440ecdf..1e23b35f0a 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 EmailValidator, RegexValidator, ip_address_validators, URLValidator +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 @@ -24,8 +26,8 @@ from rest_framework import ISO_8601 from rest_framework.compat import ( MaxLengthValidator, MaxValueValidator, MinLengthValidator, - MinValueValidator, OrderedDict, duration_string, - parse_duration, unicode_repr, unicode_to_repr + MinValueValidator, OrderedDict, duration_string, parse_duration, + unicode_repr, unicode_to_repr ) from rest_framework.exceptions import ValidationError from rest_framework.settings import api_settings From 25de8c960fcbb79f6d1097da22cab2dfeefb4e6e Mon Sep 17 00:00:00 2001 From: Carlton Gibson Date: Mon, 21 Sep 2015 19:57:20 +0200 Subject: [PATCH 24/27] Remove Django 1.5 get_model_name fallback --- rest_framework/compat.py | 8 -------- rest_framework/filters.py | 6 ++---- rest_framework/permissions.py | 6 ++---- tests/test_permissions.py | 4 ++-- 4 files changed, 6 insertions(+), 18 deletions(-) diff --git a/rest_framework/compat.py b/rest_framework/compat.py index c1074b7f8b..b348f05a9c 100644 --- a/rest_framework/compat.py +++ b/rest_framework/compat.py @@ -127,14 +127,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 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/tests/test_permissions.py b/tests/test_permissions.py index ffc262a412..f0d77e957b 100644 --- a/tests/test_permissions.py +++ b/tests/test_permissions.py @@ -11,7 +11,7 @@ HTTP_HEADER_ENCODING, authentication, generics, permissions, serializers, status ) -from rest_framework.compat import get_model_name, guardian, unittest +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 @@ -278,7 +278,7 @@ def setUp(self): # 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 = { From 8ea1606abf5eee6a3e97c71b3cc93a81e362d782 Mon Sep 17 00:00:00 2001 From: Carlton Gibson Date: Mon, 21 Sep 2015 20:16:52 +0200 Subject: [PATCH 25/27] Remove Django 1.5 clean_manytomany_helptext fallback --- rest_framework/compat.py | 11 ----------- rest_framework/utils/field_mapping.py | 3 +-- 2 files changed, 1 insertion(+), 13 deletions(-) diff --git a/rest_framework/compat.py b/rest_framework/compat.py index b348f05a9c..fcca2dcbf8 100644 --- a/rest_framework/compat.py +++ b/rest_framework/compat.py @@ -106,17 +106,6 @@ def distinct(queryset, base): 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 diff --git a/rest_framework/utils/field_mapping.py b/rest_framework/utils/field_mapping.py index f2598974ef..a285536933 100644 --- a/rest_framework/utils/field_mapping.py +++ b/rest_framework/utils/field_mapping.py @@ -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 = ( @@ -222,7 +221,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: From 2aa33ed54460f016c157cbf13e7e122b7040704b Mon Sep 17 00:00:00 2001 From: Carlton Gibson Date: Mon, 21 Sep 2015 20:23:39 +0200 Subject: [PATCH 26/27] Adjust README and Release Notes --- README.md | 2 +- docs/topics/release-notes.md | 11 ++++++++++- 2 files changed, 11 insertions(+), 2 deletions(-) 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/topics/release-notes.md b/docs/topics/release-notes.md index 68803a5b14..be2586f02d 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.0.0 + +**Date**: NOT YET RELEASED + +* Removed support for Django Version 1.5 ([#3421][gh3421]) + ## 3.2.x series ### 3.2.3 @@ -514,5 +522,6 @@ For older release notes, [please see the version 2.x documentation][old-release- [gh3321]: https://github.com/tomchristie/django-rest-framework/issues/3321 - + +[gh3421]: https://github.com/tomchristie/django-rest-framework/pulls/3421 From 524a28c6f86c12bca302b50c1d7b7061034033f7 Mon Sep 17 00:00:00 2001 From: Carlton Gibson Date: Tue, 22 Sep 2015 15:14:25 +0200 Subject: [PATCH 27/27] Exclude Guardian testing against Django master --- tox.ini | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/tox.ini b/tox.ini index 135ca4407d..204a68457f 100644 --- a/tox.ini +++ b/tox.ini @@ -39,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 +