Skip to content

Commit 2ede857

Browse files
authored
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
1 parent 125ad42 commit 2ede857

File tree

10 files changed

+119
-12
lines changed

10 files changed

+119
-12
lines changed

README.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ Some reasons you might want to use REST framework:
5555
# Requirements
5656

5757
* Python 3.8+
58-
* Django 5.0, 4.2
58+
* Django 4.2, 5.0, 5.1
5959

6060
We **highly recommend** and only officially support the latest patch release of
6161
each Python and Django series.

docs/api-guide/authentication.md

+7
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,12 @@ The kind of response that will be used depends on the authentication scheme. Al
9090

9191
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.
9292

93+
## Django 5.1+ `LoginRequiredMiddleware`
94+
95+
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.
96+
97+
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.
98+
9399
## Apache mod_wsgi specific configuration
94100

95101
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
484490
[drfpasswordless]: https://github.com/aaronn/django-rest-framework-passwordless
485491
[django-rest-authemail]: https://github.com/celiao/django-rest-authemail
486492
[django-rest-durin]: https://github.com/eshaan7/django-rest-durin
493+
[login-required-middleware]: https://docs.djangoproject.com/en/stable/ref/middleware/#django.contrib.auth.middleware.LoginRequiredMiddleware

docs/index.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,7 @@ continued development by **[signing up for a paid plan][funding]**.
8787

8888
REST framework requires the following:
8989

90-
* Django (4.2, 5.0)
90+
* Django (4.2, 5.0, 5.1)
9191
* Python (3.8, 3.9, 3.10, 3.11, 3.12)
9292

9393
We **highly recommend** and only officially support the latest patch release of

rest_framework/views.py

+6
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
"""
22
Provides an APIView class that is the base of all views in REST framework.
33
"""
4+
from django import VERSION as DJANGO_VERSION
45
from django.conf import settings
56
from django.core.exceptions import PermissionDenied
67
from django.db import connections, models
@@ -139,6 +140,11 @@ def force_evaluation():
139140
view.cls = cls
140141
view.initkwargs = initkwargs
141142

143+
# Exempt all DRF views from Django's LoginRequiredMiddleware. Users should set
144+
# DEFAULT_PERMISSION_CLASSES to 'rest_framework.permissions.IsAuthenticated' instead
145+
if DJANGO_VERSION >= (5, 1):
146+
view.login_required = False
147+
142148
# Note: session based authentication is explicitly CSRF validated,
143149
# all other authentication is CSRF exempt.
144150
return csrf_exempt(view)

rest_framework/viewsets.py

+7
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
from functools import update_wrapper
2020
from inspect import getmembers
2121

22+
from django import VERSION as DJANGO_VERSION
2223
from django.urls import NoReverseMatch
2324
from django.utils.decorators import classonlymethod
2425
from django.views.decorators.csrf import csrf_exempt
@@ -136,6 +137,12 @@ def view(request, *args, **kwargs):
136137
view.cls = cls
137138
view.initkwargs = initkwargs
138139
view.actions = actions
140+
141+
# Exempt from Django's LoginRequiredMiddleware. Users should set
142+
# DEFAULT_PERMISSION_CLASSES to 'rest_framework.permissions.IsAuthenticated' instead
143+
if DJANGO_VERSION >= (5, 1):
144+
view.login_required = False
145+
139146
return csrf_exempt(view)
140147

141148
def initialize_request(self, request, *args, **kwargs):

setup.py

+1
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,7 @@ def get_version(package):
9191
'Framework :: Django',
9292
'Framework :: Django :: 4.2',
9393
'Framework :: Django :: 5.0',
94+
'Framework :: Django :: 5.1',
9495
'Intended Audience :: Developers',
9596
'License :: OSI Approved :: BSD License',
9697
'Operating System :: OS Independent',

tests/test_middleware.py

+73-1
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,61 @@
1+
import unittest
2+
3+
import django
14
from django.contrib.auth.models import User
25
from django.http import HttpRequest
36
from django.test import override_settings
4-
from django.urls import path
7+
from django.urls import include, path
58

9+
from rest_framework import status
610
from rest_framework.authentication import TokenAuthentication
711
from rest_framework.authtoken.models import Token
12+
from rest_framework.decorators import action, api_view
813
from rest_framework.request import is_form_media_type
914
from rest_framework.response import Response
15+
from rest_framework.routers import SimpleRouter
1016
from rest_framework.test import APITestCase
1117
from rest_framework.views import APIView
18+
from rest_framework.viewsets import GenericViewSet
1219

1320

1421
class PostView(APIView):
1522
def post(self, request):
1623
return Response(data=request.data, status=200)
1724

1825

26+
class GetAPIView(APIView):
27+
def get(self, request):
28+
return Response(data="OK", status=200)
29+
30+
31+
@api_view(['GET'])
32+
def get_func_view(request):
33+
return Response(data="OK", status=200)
34+
35+
36+
class ListViewSet(GenericViewSet):
37+
38+
def list(self, request, *args, **kwargs):
39+
response = Response()
40+
response.view = self
41+
return response
42+
43+
@action(detail=False, url_path='list-action')
44+
def list_action(self, request, *args, **kwargs):
45+
response = Response()
46+
response.view = self
47+
return response
48+
49+
50+
router = SimpleRouter()
51+
router.register(r'view-set', ListViewSet, basename='view_set')
52+
1953
urlpatterns = [
2054
path('auth', APIView.as_view(authentication_classes=(TokenAuthentication,))),
2155
path('post', PostView.as_view()),
56+
path('get', GetAPIView.as_view()),
57+
path('get-func', get_func_view),
58+
path('api/', include(router.urls)),
2259
]
2360

2461

@@ -74,3 +111,38 @@ def test_middleware_can_access_request_post_when_processing_response(self):
74111

75112
response = self.client.post('/post', {'foo': 'bar'}, format='json')
76113
assert response.status_code == 200
114+
115+
116+
@unittest.skipUnless(django.VERSION >= (5, 1), 'Only for Django 5.1+')
117+
@override_settings(
118+
ROOT_URLCONF='tests.test_middleware',
119+
MIDDLEWARE=(
120+
# Needed for AuthenticationMiddleware
121+
'django.contrib.sessions.middleware.SessionMiddleware',
122+
# Needed for LoginRequiredMiddleware
123+
'django.contrib.auth.middleware.AuthenticationMiddleware',
124+
'django.contrib.auth.middleware.LoginRequiredMiddleware',
125+
),
126+
)
127+
class TestLoginRequiredMiddlewareCompat(APITestCase):
128+
"""
129+
Django's 5.1+ LoginRequiredMiddleware should NOT apply to DRF views.
130+
131+
Instead, users should put IsAuthenticated in their
132+
DEFAULT_PERMISSION_CLASSES setting.
133+
"""
134+
def test_class_based_view(self):
135+
response = self.client.get('/get')
136+
assert response.status_code == status.HTTP_200_OK
137+
138+
def test_function_based_view(self):
139+
response = self.client.get('/get-func')
140+
assert response.status_code == status.HTTP_200_OK
141+
142+
def test_viewset_list(self):
143+
response = self.client.get('/api/view-set/')
144+
assert response.status_code == status.HTTP_200_OK
145+
146+
def test_viewset_list_action(self):
147+
response = self.client.get('/api/view-set/list-action/')
148+
assert response.status_code == status.HTTP_200_OK

tests/test_views.py

+12
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
import copy
2+
import unittest
23

4+
from django import VERSION as DJANGO_VERSION
35
from django.test import TestCase
46

57
from rest_framework import status
@@ -136,3 +138,13 @@ def test_get_exception_handler(self):
136138
response = self.view(request)
137139
assert response.status_code == 400
138140
assert response.data == {'error': 'SyntaxError'}
141+
142+
143+
@unittest.skipUnless(DJANGO_VERSION >= (5, 1), 'Only for Django 5.1+')
144+
class TestLoginRequiredMiddlewareCompat(TestCase):
145+
def test_class_based_view_opted_out(self):
146+
class_based_view = BasicView.as_view()
147+
assert class_based_view.login_required is False
148+
149+
def test_function_based_view_opted_out(self):
150+
assert basic_view.login_required is False

tests/test_viewsets.py

+7
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
1+
import unittest
12
from functools import wraps
23

34
import pytest
5+
from django import VERSION as DJANGO_VERSION
46
from django.db import models
57
from django.test import TestCase, override_settings
68
from django.urls import include, path
@@ -196,6 +198,11 @@ def test_viewset_action_attr_for_extra_action(self):
196198
assert get.view.action == 'list_action'
197199
assert head.view.action == 'list_action'
198200

201+
@unittest.skipUnless(DJANGO_VERSION >= (5, 1), 'Only for Django 5.1+')
202+
def test_login_required_middleware_compat(self):
203+
view = ActionViewSet.as_view(actions={'get': 'list'})
204+
assert view.login_required is False
205+
199206

200207
class GetExtraActionsTests(TestCase):
201208

tox.ini

+4-9
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
[tox]
22
envlist =
33
{py38,py39}-{django42}
4-
{py310}-{django42,django50,djangomain}
5-
{py311}-{django42,django50,djangomain}
6-
{py312}-{django42,django50,djangomain}
4+
{py310}-{django42,django50,django51,djangomain}
5+
{py311}-{django42,django50,django51,djangomain}
6+
{py312}-{django42,django50,django51,djangomain}
77
base
88
dist
99
docs
@@ -17,6 +17,7 @@ setenv =
1717
deps =
1818
django42: Django>=4.2,<5.0
1919
django50: Django>=5.0,<5.1
20+
django51: Django>=5.1,<5.2
2021
djangomain: https://github.com/django/django/archive/main.tar.gz
2122
-rrequirements/requirements-testing.txt
2223
-rrequirements/requirements-optionals.txt
@@ -42,12 +43,6 @@ deps =
4243
-rrequirements/requirements-testing.txt
4344
-rrequirements/requirements-documentation.txt
4445

45-
[testenv:py38-djangomain]
46-
ignore_outcome = true
47-
48-
[testenv:py39-djangomain]
49-
ignore_outcome = true
50-
5146
[testenv:py310-djangomain]
5247
ignore_outcome = true
5348

0 commit comments

Comments
 (0)