mirror of
https://github.com/encode/django-rest-framework.git
synced 2024-11-21 17:16:47 +03:00
Add official support for Django 5.1 (#9514)
* Add official support for Django 5.1 Following the supported Python versions: https://docs.djangoproject.com/en/stable/faq/install/ * Add tests to cover compat with Django's 5.1 LoginRequiredMiddleware * First pass to create DRF's LoginRequiredMiddleware * Attempt to fix the tests * Revert custom middleware implementation * Disable LoginRequiredMiddleware on DRF views * Document how to integrate DRF with LoginRequiredMiddleware * Move login required tests under a separate test case * Revert redundant change * Disable LoginRequiredMiddleware on ViewSets * Add some integrations tests to cover various view types
This commit is contained in:
parent
125ad42eb3
commit
2ede857de0
|
@ -55,7 +55,7 @@ Some reasons you might want to use REST framework:
|
|||
# Requirements
|
||||
|
||||
* Python 3.8+
|
||||
* Django 5.0, 4.2
|
||||
* Django 4.2, 5.0, 5.1
|
||||
|
||||
We **highly recommend** and only officially support the latest patch release of
|
||||
each Python and Django series.
|
||||
|
|
|
@ -90,6 +90,12 @@ The kind of response that will be used depends on the authentication scheme. Al
|
|||
|
||||
Note that when a request may successfully authenticate, but still be denied permission to perform the request, in which case a `403 Permission Denied` response will always be used, regardless of the authentication scheme.
|
||||
|
||||
## Django 5.1+ `LoginRequiredMiddleware`
|
||||
|
||||
If you're running Django 5.1+ and use the [`LoginRequiredMiddleware`][login-required-middleware], please note that all views from DRF are opted-out of this middleware. This is because the authentication in DRF is based authentication and permissions classes, which may be determined after the middleware has been applied. Additionally, when the request is not authenticated, the middleware redirects the user to the login page, which is not suitable for API requests, where it's preferable to return a 401 status code.
|
||||
|
||||
REST framework offers an equivalent mechanism for DRF views via the global settings, `DEFAULT_AUTHENTICATION_CLASSES` and `DEFAULT_PERMISSION_CLASSES`. They should be changed accordingly if you need to enforce that API requests are logged in.
|
||||
|
||||
## Apache mod_wsgi specific configuration
|
||||
|
||||
Note that if deploying to [Apache using mod_wsgi][mod_wsgi_official], the authorization header is not passed through to a WSGI application by default, as it is assumed that authentication will be handled by Apache, rather than at an application level.
|
||||
|
@ -484,3 +490,4 @@ More information can be found in the [Documentation](https://django-rest-durin.r
|
|||
[drfpasswordless]: https://github.com/aaronn/django-rest-framework-passwordless
|
||||
[django-rest-authemail]: https://github.com/celiao/django-rest-authemail
|
||||
[django-rest-durin]: https://github.com/eshaan7/django-rest-durin
|
||||
[login-required-middleware]: https://docs.djangoproject.com/en/stable/ref/middleware/#django.contrib.auth.middleware.LoginRequiredMiddleware
|
|
@ -87,7 +87,7 @@ continued development by **[signing up for a paid plan][funding]**.
|
|||
|
||||
REST framework requires the following:
|
||||
|
||||
* Django (4.2, 5.0)
|
||||
* Django (4.2, 5.0, 5.1)
|
||||
* Python (3.8, 3.9, 3.10, 3.11, 3.12)
|
||||
|
||||
We **highly recommend** and only officially support the latest patch release of
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
"""
|
||||
Provides an APIView class that is the base of all views in REST framework.
|
||||
"""
|
||||
from django import VERSION as DJANGO_VERSION
|
||||
from django.conf import settings
|
||||
from django.core.exceptions import PermissionDenied
|
||||
from django.db import connections, models
|
||||
|
@ -139,6 +140,11 @@ class APIView(View):
|
|||
view.cls = cls
|
||||
view.initkwargs = initkwargs
|
||||
|
||||
# Exempt all DRF views from Django's LoginRequiredMiddleware. Users should set
|
||||
# DEFAULT_PERMISSION_CLASSES to 'rest_framework.permissions.IsAuthenticated' instead
|
||||
if DJANGO_VERSION >= (5, 1):
|
||||
view.login_required = False
|
||||
|
||||
# Note: session based authentication is explicitly CSRF validated,
|
||||
# all other authentication is CSRF exempt.
|
||||
return csrf_exempt(view)
|
||||
|
|
|
@ -19,6 +19,7 @@ automatically.
|
|||
from functools import update_wrapper
|
||||
from inspect import getmembers
|
||||
|
||||
from django import VERSION as DJANGO_VERSION
|
||||
from django.urls import NoReverseMatch
|
||||
from django.utils.decorators import classonlymethod
|
||||
from django.views.decorators.csrf import csrf_exempt
|
||||
|
@ -136,6 +137,12 @@ class ViewSetMixin:
|
|||
view.cls = cls
|
||||
view.initkwargs = initkwargs
|
||||
view.actions = actions
|
||||
|
||||
# Exempt from Django's LoginRequiredMiddleware. Users should set
|
||||
# DEFAULT_PERMISSION_CLASSES to 'rest_framework.permissions.IsAuthenticated' instead
|
||||
if DJANGO_VERSION >= (5, 1):
|
||||
view.login_required = False
|
||||
|
||||
return csrf_exempt(view)
|
||||
|
||||
def initialize_request(self, request, *args, **kwargs):
|
||||
|
|
1
setup.py
1
setup.py
|
@ -91,6 +91,7 @@ setup(
|
|||
'Framework :: Django',
|
||||
'Framework :: Django :: 4.2',
|
||||
'Framework :: Django :: 5.0',
|
||||
'Framework :: Django :: 5.1',
|
||||
'Intended Audience :: Developers',
|
||||
'License :: OSI Approved :: BSD License',
|
||||
'Operating System :: OS Independent',
|
||||
|
|
|
@ -1,14 +1,21 @@
|
|||
import unittest
|
||||
|
||||
import django
|
||||
from django.contrib.auth.models import User
|
||||
from django.http import HttpRequest
|
||||
from django.test import override_settings
|
||||
from django.urls import path
|
||||
from django.urls import include, path
|
||||
|
||||
from rest_framework import status
|
||||
from rest_framework.authentication import TokenAuthentication
|
||||
from rest_framework.authtoken.models import Token
|
||||
from rest_framework.decorators import action, api_view
|
||||
from rest_framework.request import is_form_media_type
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.routers import SimpleRouter
|
||||
from rest_framework.test import APITestCase
|
||||
from rest_framework.views import APIView
|
||||
from rest_framework.viewsets import GenericViewSet
|
||||
|
||||
|
||||
class PostView(APIView):
|
||||
|
@ -16,9 +23,39 @@ class PostView(APIView):
|
|||
return Response(data=request.data, status=200)
|
||||
|
||||
|
||||
class GetAPIView(APIView):
|
||||
def get(self, request):
|
||||
return Response(data="OK", status=200)
|
||||
|
||||
|
||||
@api_view(['GET'])
|
||||
def get_func_view(request):
|
||||
return Response(data="OK", status=200)
|
||||
|
||||
|
||||
class ListViewSet(GenericViewSet):
|
||||
|
||||
def list(self, request, *args, **kwargs):
|
||||
response = Response()
|
||||
response.view = self
|
||||
return response
|
||||
|
||||
@action(detail=False, url_path='list-action')
|
||||
def list_action(self, request, *args, **kwargs):
|
||||
response = Response()
|
||||
response.view = self
|
||||
return response
|
||||
|
||||
|
||||
router = SimpleRouter()
|
||||
router.register(r'view-set', ListViewSet, basename='view_set')
|
||||
|
||||
urlpatterns = [
|
||||
path('auth', APIView.as_view(authentication_classes=(TokenAuthentication,))),
|
||||
path('post', PostView.as_view()),
|
||||
path('get', GetAPIView.as_view()),
|
||||
path('get-func', get_func_view),
|
||||
path('api/', include(router.urls)),
|
||||
]
|
||||
|
||||
|
||||
|
@ -74,3 +111,38 @@ class TestMiddleware(APITestCase):
|
|||
|
||||
response = self.client.post('/post', {'foo': 'bar'}, format='json')
|
||||
assert response.status_code == 200
|
||||
|
||||
|
||||
@unittest.skipUnless(django.VERSION >= (5, 1), 'Only for Django 5.1+')
|
||||
@override_settings(
|
||||
ROOT_URLCONF='tests.test_middleware',
|
||||
MIDDLEWARE=(
|
||||
# Needed for AuthenticationMiddleware
|
||||
'django.contrib.sessions.middleware.SessionMiddleware',
|
||||
# Needed for LoginRequiredMiddleware
|
||||
'django.contrib.auth.middleware.AuthenticationMiddleware',
|
||||
'django.contrib.auth.middleware.LoginRequiredMiddleware',
|
||||
),
|
||||
)
|
||||
class TestLoginRequiredMiddlewareCompat(APITestCase):
|
||||
"""
|
||||
Django's 5.1+ LoginRequiredMiddleware should NOT apply to DRF views.
|
||||
|
||||
Instead, users should put IsAuthenticated in their
|
||||
DEFAULT_PERMISSION_CLASSES setting.
|
||||
"""
|
||||
def test_class_based_view(self):
|
||||
response = self.client.get('/get')
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
|
||||
def test_function_based_view(self):
|
||||
response = self.client.get('/get-func')
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
|
||||
def test_viewset_list(self):
|
||||
response = self.client.get('/api/view-set/')
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
|
||||
def test_viewset_list_action(self):
|
||||
response = self.client.get('/api/view-set/list-action/')
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
import copy
|
||||
import unittest
|
||||
|
||||
from django import VERSION as DJANGO_VERSION
|
||||
from django.test import TestCase
|
||||
|
||||
from rest_framework import status
|
||||
|
@ -136,3 +138,13 @@ class TestCustomSettings(TestCase):
|
|||
response = self.view(request)
|
||||
assert response.status_code == 400
|
||||
assert response.data == {'error': 'SyntaxError'}
|
||||
|
||||
|
||||
@unittest.skipUnless(DJANGO_VERSION >= (5, 1), 'Only for Django 5.1+')
|
||||
class TestLoginRequiredMiddlewareCompat(TestCase):
|
||||
def test_class_based_view_opted_out(self):
|
||||
class_based_view = BasicView.as_view()
|
||||
assert class_based_view.login_required is False
|
||||
|
||||
def test_function_based_view_opted_out(self):
|
||||
assert basic_view.login_required is False
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
import unittest
|
||||
from functools import wraps
|
||||
|
||||
import pytest
|
||||
from django import VERSION as DJANGO_VERSION
|
||||
from django.db import models
|
||||
from django.test import TestCase, override_settings
|
||||
from django.urls import include, path
|
||||
|
@ -196,6 +198,11 @@ class InitializeViewSetsTestCase(TestCase):
|
|||
assert get.view.action == 'list_action'
|
||||
assert head.view.action == 'list_action'
|
||||
|
||||
@unittest.skipUnless(DJANGO_VERSION >= (5, 1), 'Only for Django 5.1+')
|
||||
def test_login_required_middleware_compat(self):
|
||||
view = ActionViewSet.as_view(actions={'get': 'list'})
|
||||
assert view.login_required is False
|
||||
|
||||
|
||||
class GetExtraActionsTests(TestCase):
|
||||
|
||||
|
|
13
tox.ini
13
tox.ini
|
@ -1,9 +1,9 @@
|
|||
[tox]
|
||||
envlist =
|
||||
{py38,py39}-{django42}
|
||||
{py310}-{django42,django50,djangomain}
|
||||
{py311}-{django42,django50,djangomain}
|
||||
{py312}-{django42,django50,djangomain}
|
||||
{py310}-{django42,django50,django51,djangomain}
|
||||
{py311}-{django42,django50,django51,djangomain}
|
||||
{py312}-{django42,django50,django51,djangomain}
|
||||
base
|
||||
dist
|
||||
docs
|
||||
|
@ -17,6 +17,7 @@ setenv =
|
|||
deps =
|
||||
django42: Django>=4.2,<5.0
|
||||
django50: Django>=5.0,<5.1
|
||||
django51: Django>=5.1,<5.2
|
||||
djangomain: https://github.com/django/django/archive/main.tar.gz
|
||||
-rrequirements/requirements-testing.txt
|
||||
-rrequirements/requirements-optionals.txt
|
||||
|
@ -42,12 +43,6 @@ deps =
|
|||
-rrequirements/requirements-testing.txt
|
||||
-rrequirements/requirements-documentation.txt
|
||||
|
||||
[testenv:py38-djangomain]
|
||||
ignore_outcome = true
|
||||
|
||||
[testenv:py39-djangomain]
|
||||
ignore_outcome = true
|
||||
|
||||
[testenv:py310-djangomain]
|
||||
ignore_outcome = true
|
||||
|
||||
|
|
Loading…
Reference in New Issue
Block a user