diff --git a/docs/api-guide/reverse.md b/docs/api-guide/reverse.md index 71fb83f9e..35d88e2db 100644 --- a/docs/api-guide/reverse.md +++ b/docs/api-guide/reverse.md @@ -23,7 +23,7 @@ There's no requirement for you to use them, but if you do then the self-describi **Signature:** `reverse(viewname, *args, **kwargs)` -Has the same behavior as [`django.core.urlresolvers.reverse`][reverse], except that it returns a fully qualified URL, using the request to determine the host and port. +Has the same behavior as [`django.urls.reverse`][reverse], except that it returns a fully qualified URL, using the request to determine the host and port. You should **include the request as a keyword argument** to the function, for example: @@ -44,7 +44,7 @@ You should **include the request as a keyword argument** to the function, for ex **Signature:** `reverse_lazy(viewname, *args, **kwargs)` -Has the same behavior as [`django.core.urlresolvers.reverse_lazy`][reverse-lazy], except that it returns a fully qualified URL, using the request to determine the host and port. +Has the same behavior as [`django.urls.reverse_lazy`][reverse-lazy], except that it returns a fully qualified URL, using the request to determine the host and port. As with the `reverse` function, you should **include the request as a keyword argument** to the function, for example: diff --git a/docs/api-guide/testing.md b/docs/api-guide/testing.md index 69da7d105..18f9e19e9 100644 --- a/docs/api-guide/testing.md +++ b/docs/api-guide/testing.md @@ -197,7 +197,7 @@ REST framework includes the following test case classes, that mirror the existin You can use any of REST framework's test case classes as you would for the regular Django test case classes. The `self.client` attribute will be an `APIClient` instance. - from django.core.urlresolvers import reverse + from django.urls import reverse from rest_framework import status from rest_framework.test import APITestCase from myproject.apps.core.models import Account diff --git a/requirements/requirements-optionals.txt b/requirements/requirements-optionals.txt index afade0aa0..31f24f4b7 100644 --- a/requirements/requirements-optionals.txt +++ b/requirements/requirements-optionals.txt @@ -1,5 +1,5 @@ # Optional packages which may be used with REST framework. markdown==2.6.4 -django-guardian==1.4.3 -django-filter==0.13.0 -coreapi==2.0.0 +django-guardian==1.4.6 +django-filter==0.14.0 +coreapi==2.0.8 diff --git a/rest_framework/compat.py b/rest_framework/compat.py index bda346fa8..7ec39ba63 100644 --- a/rest_framework/compat.py +++ b/rest_framework/compat.py @@ -23,6 +23,16 @@ except ImportError: from django.utils import importlib # Will be removed in Django 1.9 +try: + from django.urls import ( + NoReverseMatch, RegexURLPattern, RegexURLResolver, ResolverMatch, Resolver404, get_script_prefix, reverse, reverse_lazy, resolve + ) +except ImportError: + from django.core.urlresolvers import ( # Will be removed in Django 2.0 + NoReverseMatch, RegexURLPattern, RegexURLResolver, ResolverMatch, Resolver404, get_script_prefix, reverse, reverse_lazy, resolve + ) + + try: import urlparse # Python 2.x except ImportError: @@ -128,6 +138,12 @@ def is_authenticated(user): return user.is_authenticated +def is_anonymous(user): + if django.VERSION < (1, 10): + return user.is_anonymous() + return user.is_anonymous + + def get_related_model(field): if django.VERSION < (1, 9): return _resolve_model(field.rel.to) @@ -207,8 +223,13 @@ try: if markdown.version <= '2.2': HEADERID_EXT_PATH = 'headerid' - else: + LEVEL_PARAM = 'level' + elif markdown.version < '2.6': HEADERID_EXT_PATH = 'markdown.extensions.headerid' + LEVEL_PARAM = 'level' + else: + HEADERID_EXT_PATH = 'markdown.extensions.toc' + LEVEL_PARAM = 'baselevel' def apply_markdown(text): """ @@ -218,7 +239,7 @@ try: extensions = [HEADERID_EXT_PATH] extension_configs = { HEADERID_EXT_PATH: { - 'level': '2' + LEVEL_PARAM: '2' } } md = markdown.Markdown( @@ -284,3 +305,11 @@ def template_render(template, context=None, request=None): # backends template, e.g. django.template.backends.django.Template else: return template.render(context, request=request) + + +def set_many(instance, field, value): + if django.VERSION < (1, 10): + setattr(instance, field, value) + else: + field = getattr(instance, field) + field.set(value) diff --git a/rest_framework/relations.py b/rest_framework/relations.py index 7fe22ec5b..c6eb92a24 100644 --- a/rest_framework/relations.py +++ b/rest_framework/relations.py @@ -4,9 +4,6 @@ from __future__ import unicode_literals from collections import OrderedDict from django.core.exceptions import ImproperlyConfigured, ObjectDoesNotExist -from django.core.urlresolvers import ( - NoReverseMatch, Resolver404, get_script_prefix, resolve -) from django.db.models import Manager from django.db.models.query import QuerySet from django.utils import six @@ -14,6 +11,9 @@ from django.utils.encoding import python_2_unicode_compatible, smart_text from django.utils.six.moves.urllib import parse as urlparse from django.utils.translation import ugettext_lazy as _ +from rest_framework.compat import ( + NoReverseMatch, Resolver404, get_script_prefix, resolve +) from rest_framework.fields import ( Field, empty, get_attribute, is_simple_callable, iter_options ) diff --git a/rest_framework/reverse.py b/rest_framework/reverse.py index 5a7ba09a8..fd418dcca 100644 --- a/rest_framework/reverse.py +++ b/rest_framework/reverse.py @@ -3,11 +3,11 @@ Provide urlresolver functions that return fully qualified URLs or view names """ from __future__ import unicode_literals -from django.core.urlresolvers import reverse as django_reverse -from django.core.urlresolvers import NoReverseMatch from django.utils import six from django.utils.functional import lazy +from rest_framework.compat import reverse as django_reverse +from rest_framework.compat import NoReverseMatch from rest_framework.settings import api_settings from rest_framework.utils.urls import replace_query_param @@ -54,7 +54,7 @@ def reverse(viewname, args=None, kwargs=None, request=None, format=None, **extra def _reverse(viewname, args=None, kwargs=None, request=None, format=None, **extra): """ - Same as `django.core.urlresolvers.reverse`, but optionally takes a request + Same as `django.urls.reverse`, but optionally takes a request and returns a fully qualified URL, using the request to get the base URL. """ if format is not None: diff --git a/rest_framework/routers.py b/rest_framework/routers.py index 64f110cd9..e8dc2c068 100644 --- a/rest_framework/routers.py +++ b/rest_framework/routers.py @@ -20,9 +20,9 @@ from collections import OrderedDict, namedtuple from django.conf.urls import url from django.core.exceptions import ImproperlyConfigured -from django.core.urlresolvers import NoReverseMatch from rest_framework import exceptions, renderers, views +from rest_framework.compat import NoReverseMatch from rest_framework.response import Response from rest_framework.reverse import reverse from rest_framework.schemas import SchemaGenerator diff --git a/rest_framework/schemas.py b/rest_framework/schemas.py index bf1e6dd4a..f2ec1f9e1 100644 --- a/rest_framework/schemas.py +++ b/rest_framework/schemas.py @@ -2,12 +2,13 @@ from importlib import import_module from django.conf import settings from django.contrib.admindocs.views import simplify_regex -from django.core.urlresolvers import RegexURLPattern, RegexURLResolver from django.utils import six from django.utils.encoding import force_text from rest_framework import exceptions, serializers -from rest_framework.compat import coreapi, uritemplate, urlparse +from rest_framework.compat import ( + RegexURLPattern, RegexURLResolver, coreapi, uritemplate, urlparse +) from rest_framework.request import clone_request from rest_framework.views import APIView diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index 4d1ed63ae..28f70bd40 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -23,7 +23,7 @@ from django.utils.functional import cached_property from django.utils.translation import ugettext_lazy as _ from rest_framework.compat import JSONField as ModelJSONField -from rest_framework.compat import postgres_fields, unicode_to_repr +from rest_framework.compat import postgres_fields, set_many, unicode_to_repr from rest_framework.utils import model_meta from rest_framework.utils.field_mapping import ( ClassLookupDict, get_field_kwargs, get_nested_relation_kwargs, @@ -892,19 +892,23 @@ class ModelSerializer(Serializer): # Save many-to-many relationships after the instance is created. if many_to_many: for field_name, value in many_to_many.items(): - setattr(instance, field_name, value) + set_many(instance, field_name, value) return instance def update(self, instance, validated_data): raise_errors_on_nested_writes('update', self, validated_data) + info = model_meta.get_field_info(instance) # Simply set each attribute on the instance, and then save it. # Note that unlike `.create()` we don't need to treat many-to-many # relationships as being a special case. During updates we already # have an instance pk for the relationships to be associated with. for attr, value in validated_data.items(): - setattr(instance, attr, value) + if attr in info.relations and info.relations[attr].to_many: + set_many(instance, attr, value) + else: + setattr(instance, attr, value) instance.save() return instance diff --git a/rest_framework/templatetags/rest_framework.py b/rest_framework/templatetags/rest_framework.py index 3bb85e472..c1c8a5396 100644 --- a/rest_framework/templatetags/rest_framework.py +++ b/rest_framework/templatetags/rest_framework.py @@ -3,14 +3,13 @@ from __future__ import absolute_import, unicode_literals import re from django import template -from django.core.urlresolvers import NoReverseMatch, reverse from django.template import loader from django.utils import six from django.utils.encoding import force_text, iri_to_uri from django.utils.html import escape, format_html, smart_urlquote from django.utils.safestring import SafeData, mark_safe -from rest_framework.compat import template_render +from rest_framework.compat import NoReverseMatch, reverse, template_render from rest_framework.renderers import HTMLFormRenderer from rest_framework.utils.urls import replace_query_param diff --git a/rest_framework/urlpatterns.py b/rest_framework/urlpatterns.py index 7a02bb0f0..4ea55300e 100644 --- a/rest_framework/urlpatterns.py +++ b/rest_framework/urlpatterns.py @@ -1,8 +1,8 @@ from __future__ import unicode_literals from django.conf.urls import include, url -from django.core.urlresolvers import RegexURLResolver +from rest_framework.compat import RegexURLResolver from rest_framework.settings import api_settings diff --git a/rest_framework/utils/breadcrumbs.py b/rest_framework/utils/breadcrumbs.py index 2e3ab9084..74f4f7840 100644 --- a/rest_framework/utils/breadcrumbs.py +++ b/rest_framework/utils/breadcrumbs.py @@ -1,6 +1,6 @@ from __future__ import unicode_literals -from django.core.urlresolvers import get_script_prefix, resolve +from rest_framework.compat import get_script_prefix, resolve def get_breadcrumbs(url, request=None): diff --git a/tests/browsable_api/test_form_rendering.py b/tests/browsable_api/test_form_rendering.py index 5a31ae0dd..8b79ab6ff 100644 --- a/tests/browsable_api/test_form_rendering.py +++ b/tests/browsable_api/test_form_rendering.py @@ -11,6 +11,7 @@ factory = APIRequestFactory() class BasicSerializer(serializers.ModelSerializer): class Meta: model = BasicModel + fields = '__all__' class ManyPostView(generics.GenericAPIView): diff --git a/tests/conftest.py b/tests/conftest.py index a5123b9d8..256678226 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,6 +1,13 @@ def pytest_configure(): from django.conf import settings + MIDDLEWARE = ( + 'django.middleware.common.CommonMiddleware', + 'django.contrib.sessions.middleware.SessionMiddleware', + 'django.contrib.auth.middleware.AuthenticationMiddleware', + 'django.contrib.messages.middleware.MessageMiddleware', + ) + settings.configure( DEBUG_PROPAGATE_EXCEPTIONS=True, DATABASES={ @@ -21,12 +28,8 @@ def pytest_configure(): 'APP_DIRS': True, }, ], - MIDDLEWARE_CLASSES=( - 'django.middleware.common.CommonMiddleware', - 'django.contrib.sessions.middleware.SessionMiddleware', - 'django.contrib.auth.middleware.AuthenticationMiddleware', - 'django.contrib.messages.middleware.MessageMiddleware', - ), + MIDDLEWARE=MIDDLEWARE, + MIDDLEWARE_CLASSES=MIDDLEWARE, INSTALLED_APPS=( 'django.contrib.auth', 'django.contrib.contenttypes', diff --git a/tests/test_atomic_requests.py b/tests/test_atomic_requests.py index 8342ad3af..09d7f2fb1 100644 --- a/tests/test_atomic_requests.py +++ b/tests/test_atomic_requests.py @@ -5,7 +5,7 @@ import unittest 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.test import TestCase, TransactionTestCase, override_settings from django.utils.decorators import method_decorator from rest_framework import status @@ -36,6 +36,20 @@ class APIExceptionView(APIView): raise APIException +class NonAtomicAPIExceptionView(APIView): + @method_decorator(transaction.non_atomic_requests) + def dispatch(self, *args, **kwargs): + return super(NonAtomicAPIExceptionView, self).dispatch(*args, **kwargs) + + def get(self, request, *args, **kwargs): + BasicModel.objects.all() + raise Http404 + +urlpatterns = ( + url(r'^$', NonAtomicAPIExceptionView.as_view()), +) + + @unittest.skipUnless( connection.features.uses_savepoints, "'atomic' requires transactions and savepoints." @@ -124,22 +138,8 @@ class DBTransactionAPIExceptionTests(TestCase): connection.features.uses_savepoints, "'atomic' requires transactions and savepoints." ) +@override_settings(ROOT_URLCONF='tests.test_atomic_requests') class NonAtomicDBTransactionAPIExceptionTests(TransactionTestCase): - @property - def urls(self): - class NonAtomicAPIExceptionView(APIView): - @method_decorator(transaction.non_atomic_requests) - def dispatch(self, *args, **kwargs): - return super(NonAtomicAPIExceptionView, self).dispatch(*args, **kwargs) - - def get(self, request, *args, **kwargs): - BasicModel.objects.all() - raise Http404 - - return ( - url(r'^$', NonAtomicAPIExceptionView.as_view()), - ) - def setUp(self): connections.databases['default']['ATOMIC_REQUESTS'] = True diff --git a/tests/test_authentication.py b/tests/test_authentication.py index 5ef620abe..6f17ea14f 100644 --- a/tests/test_authentication.py +++ b/tests/test_authentication.py @@ -20,6 +20,7 @@ from rest_framework.authentication import ( ) from rest_framework.authtoken.models import Token from rest_framework.authtoken.views import obtain_auth_token +from rest_framework.compat import is_authenticated from rest_framework.response import Response from rest_framework.test import APIClient, APIRequestFactory from rest_framework.views import APIView @@ -408,7 +409,7 @@ class FailingAuthAccessedInRenderer(TestCase): def render(self, data, media_type=None, renderer_context=None): request = renderer_context['request'] - if request.user.is_authenticated(): + if is_authenticated(request.user): return b'authenticated' return b'not authenticated' diff --git a/tests/test_filters.py b/tests/test_filters.py index 03d61fc37..c67412dd7 100644 --- a/tests/test_filters.py +++ b/tests/test_filters.py @@ -6,7 +6,6 @@ from decimal import Decimal from django.conf.urls import url from django.core.exceptions import ImproperlyConfigured -from django.core.urlresolvers import reverse from django.db import models from django.test import TestCase from django.test.utils import override_settings @@ -14,7 +13,7 @@ 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, reverse from rest_framework.test import APIRequestFactory from .models import BaseFilterableItem, BasicModel, FilterableItem @@ -77,6 +76,7 @@ if django_filters: class Meta: model = BaseFilterableItem + fields = '__all__' class BaseFilterableItemFilterRootView(generics.ListCreateAPIView): queryset = FilterableItem.objects.all() @@ -456,7 +456,7 @@ class AttributeModel(models.Model): class SearchFilterModelFk(models.Model): title = models.CharField(max_length=20) - attribute = models.ForeignKey(AttributeModel) + attribute = models.ForeignKey(AttributeModel, on_delete=models.CASCADE) class SearchFilterFkSerializer(serializers.ModelSerializer): diff --git a/tests/test_model_serializer.py b/tests/test_model_serializer.py index 01243ff6e..cd9b2dfc3 100644 --- a/tests/test_model_serializer.py +++ b/tests/test_model_serializer.py @@ -20,7 +20,7 @@ from django.test import TestCase from django.utils import six from rest_framework import serializers -from rest_framework.compat import unicode_repr +from rest_framework.compat import set_many, unicode_repr def dedent(blocktext): @@ -651,7 +651,7 @@ class TestIntegration(TestCase): foreign_key=self.foreign_key_target, one_to_one=self.one_to_one_target, ) - self.instance.many_to_many = self.many_to_many_targets + set_many(self.instance, 'many_to_many', self.many_to_many_targets) self.instance.save() def test_pk_retrival(self): @@ -962,7 +962,7 @@ class OneToOneTargetTestModel(models.Model): class OneToOneSourceTestModel(models.Model): - target = models.OneToOneField(OneToOneTargetTestModel, primary_key=True) + target = models.OneToOneField(OneToOneTargetTestModel, primary_key=True, on_delete=models.CASCADE) class TestModelFieldValues(TestCase): @@ -990,6 +990,7 @@ class TestUniquenessOverride(TestCase): class TestSerializer(serializers.ModelSerializer): class Meta: model = TestModel + fields = '__all__' extra_kwargs = {'field_1': {'required': False}} fields = TestSerializer().fields diff --git a/tests/test_permissions.py b/tests/test_permissions.py index 5cef22628..f8561e61d 100644 --- a/tests/test_permissions.py +++ b/tests/test_permissions.py @@ -4,7 +4,6 @@ import base64 import unittest 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 @@ -12,7 +11,7 @@ from rest_framework import ( HTTP_HEADER_ENCODING, authentication, generics, permissions, serializers, status ) -from rest_framework.compat import guardian +from rest_framework.compat import ResolverMatch, guardian, set_many from rest_framework.filters import DjangoObjectPermissionsFilter from rest_framework.routers import DefaultRouter from rest_framework.test import APIRequestFactory @@ -74,15 +73,15 @@ class ModelPermissionsIntegrationTests(TestCase): def setUp(self): User.objects.create_user('disallowed', 'disallowed@example.com', 'password') user = User.objects.create_user('permitted', 'permitted@example.com', 'password') - user.user_permissions = [ + set_many(user, 'user_permissions', [ Permission.objects.get(codename='add_basicmodel'), Permission.objects.get(codename='change_basicmodel'), Permission.objects.get(codename='delete_basicmodel') - ] + ]) user = User.objects.create_user('updateonly', 'updateonly@example.com', 'password') - user.user_permissions = [ + set_many(user, 'user_permissions', [ Permission.objects.get(codename='change_basicmodel'), - ] + ]) self.permitted_credentials = basic_auth_header('permitted', 'password') self.disallowed_credentials = basic_auth_header('disallowed', 'password') diff --git a/tests/test_request.py b/tests/test_request.py index dbfa695fd..32fbbc50b 100644 --- a/tests/test_request.py +++ b/tests/test_request.py @@ -13,6 +13,7 @@ from django.utils import six from rest_framework import status from rest_framework.authentication import SessionAuthentication +from rest_framework.compat import is_anonymous from rest_framework.parsers import BaseParser, FormParser, MultiPartParser from rest_framework.request import Request from rest_framework.response import Response @@ -169,9 +170,9 @@ class TestUserSetter(TestCase): def test_user_can_logout(self): self.request.user = self.user - self.assertFalse(self.request.user.is_anonymous()) + self.assertFalse(is_anonymous(self.request.user)) logout(self.request) - self.assertTrue(self.request.user.is_anonymous()) + self.assertTrue(is_anonymous(self.request.user)) def test_logged_in_user_is_set_on_wrapped_request(self): login(self.request, self.user) diff --git a/tests/test_reverse.py b/tests/test_reverse.py index 03d31f1f9..f30a8bf9a 100644 --- a/tests/test_reverse.py +++ b/tests/test_reverse.py @@ -1,9 +1,9 @@ from __future__ import unicode_literals from django.conf.urls import url -from django.core.urlresolvers import NoReverseMatch from django.test import TestCase, override_settings +from rest_framework.compat import NoReverseMatch from rest_framework.reverse import reverse from rest_framework.test import APIRequestFactory diff --git a/tests/test_schemas.py b/tests/test_schemas.py index 197e62eb0..dc01d8cd8 100644 --- a/tests/test_schemas.py +++ b/tests/test_schemas.py @@ -215,4 +215,4 @@ class TestSchemaGenerator(TestCase): } } ) - self.assertEquals(schema, expected) + self.assertEqual(schema, expected) diff --git a/tests/test_urlpatterns.py b/tests/test_urlpatterns.py index 78d37c1a8..33d367e1d 100644 --- a/tests/test_urlpatterns.py +++ b/tests/test_urlpatterns.py @@ -3,9 +3,9 @@ from __future__ import unicode_literals from collections import namedtuple from django.conf.urls import include, url -from django.core import urlresolvers from django.test import TestCase +from rest_framework.compat import RegexURLResolver, Resolver404 from rest_framework.test import APIRequestFactory from rest_framework.urlpatterns import format_suffix_patterns @@ -28,7 +28,7 @@ class FormatSuffixTests(TestCase): urlpatterns = format_suffix_patterns(urlpatterns) except Exception: self.fail("Failed to apply `format_suffix_patterns` on the supplied urlpatterns") - resolver = urlresolvers.RegexURLResolver(r'^/', urlpatterns) + resolver = RegexURLResolver(r'^/', urlpatterns) for test_path in test_paths: request = factory.get(test_path.path) try: @@ -43,7 +43,7 @@ class FormatSuffixTests(TestCase): urlpatterns = format_suffix_patterns([ url(r'^test/$', dummy_view), ]) - resolver = urlresolvers.RegexURLResolver(r'^/', urlpatterns) + resolver = RegexURLResolver(r'^/', urlpatterns) test_paths = [ (URLTestPath('/test.api', (), {'format': 'api'}), True), @@ -55,7 +55,7 @@ class FormatSuffixTests(TestCase): request = factory.get(test_path.path) try: callback, callback_args, callback_kwargs = resolver.resolve(request.path_info) - except urlresolvers.Resolver404: + except Resolver404: callback, callback_args, callback_kwargs = (None, None, None) if not expected_resolved: assert callback is None diff --git a/tests/utils.py b/tests/utils.py index 5b2d75864..52582f093 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -1,5 +1,5 @@ from django.core.exceptions import ObjectDoesNotExist -from django.core.urlresolvers import NoReverseMatch +from rest_framework.compat import NoReverseMatch class MockObject(object):