From d7c8dcfc7ee33edff0383688f89e7c1f6b474f6d Mon Sep 17 00:00:00 2001 From: Alex Laird Date: Thu, 21 Mar 2024 07:46:06 -0500 Subject: [PATCH 01/64] Revert PR that adds autocomplete_fields to TokenAdmin, as this break some use cases. (#9301) --- rest_framework/authtoken/admin.py | 1 - 1 file changed, 1 deletion(-) diff --git a/rest_framework/authtoken/admin.py b/rest_framework/authtoken/admin.py index 163328eb0..eabb8fca8 100644 --- a/rest_framework/authtoken/admin.py +++ b/rest_framework/authtoken/admin.py @@ -28,7 +28,6 @@ class TokenAdmin(admin.ModelAdmin): search_help_text = _('Username') ordering = ('-created',) actions = None # Actions not compatible with mapped IDs. - autocomplete_fields = ("user",) def get_changelist(self, request, **kwargs): return TokenChangeList From 56a5b354d0f82ea7e0df5cc4aa5187fe8450cbcb Mon Sep 17 00:00:00 2001 From: Aristotelis Mikropoulos Date: Thu, 21 Mar 2024 16:10:10 +0200 Subject: [PATCH 02/64] Add drf-sendables to third-party-packages.md (#9261) --- docs/community/third-party-packages.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/community/third-party-packages.md b/docs/community/third-party-packages.md index 3a4ba5848..a92da82fc 100644 --- a/docs/community/third-party-packages.md +++ b/docs/community/third-party-packages.md @@ -125,6 +125,7 @@ To submit new content, [open an issue][drf-create-issue] or [create a pull reque ### Misc +* [drf-sendables][drf-sendables] - User messages for Django REST Framework * [cookiecutter-django-rest][cookiecutter-django-rest] - A cookiecutter template that takes care of the setup and configuration so you can focus on making your REST apis awesome. * [djangorestrelationalhyperlink][djangorestrelationalhyperlink] - A hyperlinked serializer that can can be used to alter relationships via hyperlinks, but otherwise like a hyperlink model serializer. * [django-rest-framework-proxy][django-rest-framework-proxy] - Proxy to redirect incoming request to another API server. @@ -157,6 +158,7 @@ To submit new content, [open an issue][drf-create-issue] or [create a pull reque * [drf-redesign][drf-redesign] - A project that gives a fresh look to the browse-able API using Bootstrap 5. * [drf-material][drf-material] - A project that gives a sleek and elegant look to the browsable API using Material Design. +[drf-sendables]: https://github.com/amikrop/drf-sendables [cite]: http://www.software-ecosystems.com/Software_Ecosystems/Ecosystems.html [cookiecutter]: https://github.com/jpadilla/cookiecutter-django-rest-framework [new-repo]: https://github.com/new From 0e4ed816279a8b9332544192ad90f7324f49cd62 Mon Sep 17 00:00:00 2001 From: Asif Saif Uddin Date: Thu, 21 Mar 2024 22:09:43 +0600 Subject: [PATCH 03/64] =?UTF-8?q?Revert=20"feat:=20Add=20some=20changes=20?= =?UTF-8?q?to=20ValidationError=20to=20support=20django=20style=20vad?= =?UTF-8?q?=E2=80=A6"=20(#9326)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This reverts commit 4abfa28e0879e2df45937ac8c7a9ffa161561955. Co-authored-by: Tom Christie --- rest_framework/exceptions.py | 19 ++------ tests/test_validation_error.py | 86 ---------------------------------- 2 files changed, 3 insertions(+), 102 deletions(-) diff --git a/rest_framework/exceptions.py b/rest_framework/exceptions.py index bc20fcaa3..09f111102 100644 --- a/rest_framework/exceptions.py +++ b/rest_framework/exceptions.py @@ -144,30 +144,17 @@ class ValidationError(APIException): status_code = status.HTTP_400_BAD_REQUEST default_detail = _('Invalid input.') default_code = 'invalid' - default_params = {} - def __init__(self, detail=None, code=None, params=None): + def __init__(self, detail=None, code=None): if detail is None: detail = self.default_detail if code is None: code = self.default_code - if params is None: - params = self.default_params # For validation failures, we may collect many errors together, # so the details should always be coerced to a list if not already. - if isinstance(detail, str): - detail = [detail % params] - elif isinstance(detail, ValidationError): - detail = detail.detail - elif isinstance(detail, (list, tuple)): - final_detail = [] - for detail_item in detail: - if isinstance(detail_item, ValidationError): - final_detail += detail_item.detail - else: - final_detail += [detail_item % params if isinstance(detail_item, str) else detail_item] - detail = final_detail + if isinstance(detail, tuple): + detail = list(detail) elif not isinstance(detail, dict) and not isinstance(detail, list): detail = [detail] diff --git a/tests/test_validation_error.py b/tests/test_validation_error.py index 7b8b3190f..341c4342a 100644 --- a/tests/test_validation_error.py +++ b/tests/test_validation_error.py @@ -109,89 +109,3 @@ class TestValidationErrorConvertsTuplesToLists(TestCase): assert len(error.detail) == 2 assert str(error.detail[0]) == 'message1' assert str(error.detail[1]) == 'message2' - - -class TestValidationErrorWithDjangoStyle(TestCase): - def test_validation_error_details(self): - error = ValidationError('Invalid value: %(value)s', params={'value': '42'}) - assert str(error.detail[0]) == 'Invalid value: 42' - - def test_validation_error_details_tuple(self): - error = ValidationError( - detail=('Invalid value: %(value1)s', 'Invalid value: %(value2)s'), - params={'value1': '42', 'value2': '43'}, - ) - assert isinstance(error.detail, list) - assert len(error.detail) == 2 - assert str(error.detail[0]) == 'Invalid value: 42' - assert str(error.detail[1]) == 'Invalid value: 43' - - def test_validation_error_details_list(self): - error = ValidationError( - detail=['Invalid value: %(value1)s', 'Invalid value: %(value2)s', ], - params={'value1': '42', 'value2': '43'} - ) - assert isinstance(error.detail, list) - assert len(error.detail) == 2 - assert str(error.detail[0]) == 'Invalid value: 42' - assert str(error.detail[1]) == 'Invalid value: 43' - - def test_validation_error_details_validation_errors(self): - error = ValidationError( - detail=ValidationError( - detail='Invalid value: %(value1)s', - params={'value1': '42'}, - ), - ) - assert isinstance(error.detail, list) - assert len(error.detail) == 1 - assert str(error.detail[0]) == 'Invalid value: 42' - - def test_validation_error_details_validation_errors_list(self): - error = ValidationError( - detail=[ - ValidationError( - detail='Invalid value: %(value1)s', - params={'value1': '42'}, - ), - ValidationError( - detail='Invalid value: %(value2)s', - params={'value2': '43'}, - ), - 'Invalid value: %(value3)s' - ], - params={'value3': '44'} - ) - assert isinstance(error.detail, list) - assert len(error.detail) == 3 - assert str(error.detail[0]) == 'Invalid value: 42' - assert str(error.detail[1]) == 'Invalid value: 43' - assert str(error.detail[2]) == 'Invalid value: 44' - - def test_validation_error_details_validation_errors_nested_list(self): - error = ValidationError( - detail=[ - ValidationError( - detail='Invalid value: %(value1)s', - params={'value1': '42'}, - ), - ValidationError( - detail=[ - 'Invalid value: %(value2)s', - ValidationError( - detail='Invalid value: %(value3)s', - params={'value3': '44'}, - ) - ], - params={'value2': '43'}, - ), - 'Invalid value: %(value4)s' - ], - params={'value4': '45'} - ) - assert isinstance(error.detail, list) - assert len(error.detail) == 4 - assert str(error.detail[0]) == 'Invalid value: 42' - assert str(error.detail[1]) == 'Invalid value: 43' - assert str(error.detail[2]) == 'Invalid value: 44' - assert str(error.detail[3]) == 'Invalid value: 45' From da78a147f2f3c5820396f9fcd9372b2f5b9bbd18 Mon Sep 17 00:00:00 2001 From: Asif Saif Uddin Date: Fri, 22 Mar 2024 04:23:30 +0600 Subject: [PATCH 04/64] Revert "Re-prefetch related objects after updating (#8043)" (#9327) This reverts commit 2b34aa42916882cba03cfea41a1f82b7ae18476a. --- rest_framework/mixins.py | 9 +--- tests/test_prefetch_related.py | 94 +++++++++++----------------------- 2 files changed, 32 insertions(+), 71 deletions(-) diff --git a/rest_framework/mixins.py b/rest_framework/mixins.py index 6ac6366c7..7fa8947cb 100644 --- a/rest_framework/mixins.py +++ b/rest_framework/mixins.py @@ -4,8 +4,6 @@ Basic building blocks for generic class based views. We don't bind behaviour to http method handlers yet, which allows mixin classes to be composed in interesting ways. """ -from django.db.models.query import prefetch_related_objects - from rest_framework import status from rest_framework.response import Response from rest_framework.settings import api_settings @@ -69,13 +67,10 @@ class UpdateModelMixin: serializer.is_valid(raise_exception=True) self.perform_update(serializer) - queryset = self.filter_queryset(self.get_queryset()) - if queryset._prefetch_related_lookups: + if getattr(instance, '_prefetched_objects_cache', None): # If 'prefetch_related' has been applied to a queryset, we need to - # forcibly invalidate the prefetch cache on the instance, - # and then re-prefetch related objects + # forcibly invalidate the prefetch cache on the instance. instance._prefetched_objects_cache = {} - prefetch_related_objects([instance], *queryset._prefetch_related_lookups) return Response(serializer.data) diff --git a/tests/test_prefetch_related.py b/tests/test_prefetch_related.py index 8e7bcf4ac..b07087c97 100644 --- a/tests/test_prefetch_related.py +++ b/tests/test_prefetch_related.py @@ -1,5 +1,4 @@ from django.contrib.auth.models import Group, User -from django.db.models.query import Prefetch from django.test import TestCase from rest_framework import generics, serializers @@ -9,84 +8,51 @@ factory = APIRequestFactory() class UserSerializer(serializers.ModelSerializer): - permissions = serializers.SerializerMethodField() - - def get_permissions(self, obj): - ret = [] - for g in obj.groups.all(): - ret.extend([p.pk for p in g.permissions.all()]) - return ret - class Meta: model = User - fields = ('id', 'username', 'email', 'groups', 'permissions') + fields = ('id', 'username', 'email', 'groups') -class UserRetrieveUpdate(generics.RetrieveUpdateAPIView): - queryset = User.objects.exclude(username='exclude').prefetch_related( - Prefetch('groups', queryset=Group.objects.exclude(name='exclude')), - 'groups__permissions', - ) - serializer_class = UserSerializer - - -class UserUpdateWithoutPrefetchRelated(generics.UpdateAPIView): - queryset = User.objects.exclude(username='exclude') +class UserUpdate(generics.UpdateAPIView): + queryset = User.objects.exclude(username='exclude').prefetch_related('groups') serializer_class = UserSerializer class TestPrefetchRelatedUpdates(TestCase): def setUp(self): self.user = User.objects.create(username='tom', email='tom@example.com') - self.groups = [Group.objects.create(name=f'group {i}') for i in range(10)] + self.groups = [Group.objects.create(name='a'), Group.objects.create(name='b')] self.user.groups.set(self.groups) - self.user.groups.add(Group.objects.create(name='exclude')) - self.expected = { - 'id': self.user.pk, - 'username': 'tom', - 'groups': [group.pk for group in self.groups], - 'email': 'tom@example.com', - 'permissions': [], - } - self.view = UserRetrieveUpdate.as_view() def test_prefetch_related_updates(self): - self.groups.append(Group.objects.create(name='c')) - request = factory.put( - '/', {'username': 'new', 'groups': [group.pk for group in self.groups]}, format='json' - ) - self.expected['username'] = 'new' - self.expected['groups'] = [group.pk for group in self.groups] - response = self.view(request, pk=self.user.pk) - assert User.objects.get(pk=self.user.pk).groups.count() == 12 - assert response.data == self.expected - # Update and fetch should get same result - request = factory.get('/') - response = self.view(request, pk=self.user.pk) - assert response.data == self.expected + view = UserUpdate.as_view() + pk = self.user.pk + groups_pk = self.groups[0].pk + request = factory.put('/', {'username': 'new', 'groups': [groups_pk]}, format='json') + response = view(request, pk=pk) + assert User.objects.get(pk=pk).groups.count() == 1 + expected = { + 'id': pk, + 'username': 'new', + 'groups': [1], + 'email': 'tom@example.com' + } + assert response.data == expected def test_prefetch_related_excluding_instance_from_original_queryset(self): """ Regression test for https://github.com/encode/django-rest-framework/issues/4661 """ - request = factory.put( - '/', {'username': 'exclude', 'groups': [self.groups[0].pk]}, format='json' - ) - response = self.view(request, pk=self.user.pk) - assert User.objects.get(pk=self.user.pk).groups.count() == 2 - self.expected['username'] = 'exclude' - self.expected['groups'] = [self.groups[0].pk] - assert response.data == self.expected - - def test_db_query_count(self): - request = factory.put( - '/', {'username': 'new'}, format='json' - ) - with self.assertNumQueries(7): - self.view(request, pk=self.user.pk) - - request = factory.put( - '/', {'username': 'new2'}, format='json' - ) - with self.assertNumQueries(16): - UserUpdateWithoutPrefetchRelated.as_view()(request, pk=self.user.pk) + view = UserUpdate.as_view() + pk = self.user.pk + groups_pk = self.groups[0].pk + request = factory.put('/', {'username': 'exclude', 'groups': [groups_pk]}, format='json') + response = view(request, pk=pk) + assert User.objects.get(pk=pk).groups.count() == 1 + expected = { + 'id': pk, + 'username': 'exclude', + 'groups': [1], + 'email': 'tom@example.com' + } + assert response.data == expected From a4d58077a0aca89b82f63ab33ebb36b16bf26d4a Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Thu, 21 Mar 2024 22:26:50 +0000 Subject: [PATCH 05/64] =?UTF-8?q?Revert=20"feat:=20Add=20some=20changes=20?= =?UTF-8?q?to=20ValidationError=20to=20support=20django=20style=20vad?= =?UTF-8?q?=E2=80=A6"=20(#9330)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This reverts commit 4abfa28e0879e2df45937ac8c7a9ffa161561955. From 4f10c4e43ee57f4a2e387e0c8d44d28d21a3621c Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Thu, 21 Mar 2024 22:45:12 +0000 Subject: [PATCH 06/64] =?UTF-8?q?Revert=20"Fix=20Respect=20`can=5Fread=5Fm?= =?UTF-8?q?odel`=20permission=20in=20DjangoModelPermissions=20(#8=E2=80=A6?= =?UTF-8?q?"=20(#9332)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This reverts commit 0618fa88e1a8c2cf8a2aab29ef6de66b49e5f7ed. --- docs/api-guide/permissions.md | 3 +-- rest_framework/permissions.py | 11 +++-------- tests/test_permissions.py | 21 +-------------------- 3 files changed, 5 insertions(+), 30 deletions(-) diff --git a/docs/api-guide/permissions.md b/docs/api-guide/permissions.md index 5e0b6a153..775888fb6 100644 --- a/docs/api-guide/permissions.md +++ b/docs/api-guide/permissions.md @@ -173,12 +173,11 @@ This permission is suitable if you want to your API to allow read permissions to This permission class ties into Django's standard `django.contrib.auth` [model permissions][contribauth]. This permission must only be applied to views that have a `.queryset` property or `get_queryset()` method. Authorization will only be granted if the user *is authenticated* and has the *relevant model permissions* assigned. The appropriate model is determined by checking `get_queryset().model` or `queryset.model`. -* `GET` requests require the user to have the `view` or `change` permission on the model * `POST` requests require the user to have the `add` permission on the model. * `PUT` and `PATCH` requests require the user to have the `change` permission on the model. * `DELETE` requests require the user to have the `delete` permission on the model. -The default behaviour can also be overridden to support custom model permissions. +The default behavior can also be overridden to support custom model permissions. For example, you might want to include a `view` model permission for `GET` requests. To use custom model permissions, override `DjangoModelPermissions` and set the `.perms_map` property. Refer to the source code for details. diff --git a/rest_framework/permissions.py b/rest_framework/permissions.py index 8fb4569cb..71de226f9 100644 --- a/rest_framework/permissions.py +++ b/rest_framework/permissions.py @@ -186,9 +186,9 @@ class DjangoModelPermissions(BasePermission): # Override this if you need to also provide 'view' permissions, # or if you want to provide custom permission codes. perms_map = { - 'GET': ['%(app_label)s.view_%(model_name)s'], + 'GET': [], 'OPTIONS': [], - 'HEAD': ['%(app_label)s.view_%(model_name)s'], + 'HEAD': [], 'POST': ['%(app_label)s.add_%(model_name)s'], 'PUT': ['%(app_label)s.change_%(model_name)s'], 'PATCH': ['%(app_label)s.change_%(model_name)s'], @@ -239,13 +239,8 @@ class DjangoModelPermissions(BasePermission): queryset = self._queryset(view) perms = self.get_required_permissions(request.method, queryset.model) - change_perm = self.get_required_permissions('PUT', queryset.model) - user = request.user - if request.method == 'GET': - return user.has_perms(perms) or user.has_perms(change_perm) - - return user.has_perms(perms) + return request.user.has_perms(perms) class DjangoModelPermissionsOrAnonReadOnly(DjangoModelPermissions): diff --git a/tests/test_permissions.py b/tests/test_permissions.py index 428480dc7..aefff981e 100644 --- a/tests/test_permissions.py +++ b/tests/test_permissions.py @@ -80,8 +80,7 @@ class ModelPermissionsIntegrationTests(TestCase): user.user_permissions.set([ Permission.objects.get(codename='add_basicmodel'), Permission.objects.get(codename='change_basicmodel'), - Permission.objects.get(codename='delete_basicmodel'), - Permission.objects.get(codename='view_basicmodel') + Permission.objects.get(codename='delete_basicmodel') ]) user = User.objects.create_user('updateonly', 'updateonly@example.com', 'password') @@ -140,15 +139,6 @@ class ModelPermissionsIntegrationTests(TestCase): response = get_queryset_list_view(request, pk=1) self.assertEqual(response.status_code, status.HTTP_201_CREATED) - def test_has_get_permissions(self): - request = factory.get('/', HTTP_AUTHORIZATION=self.permitted_credentials) - response = root_view(request) - self.assertEqual(response.status_code, status.HTTP_200_OK) - - request = factory.get('/1', HTTP_AUTHORIZATION=self.updateonly_credentials) - response = root_view(request, pk=1) - self.assertEqual(response.status_code, status.HTTP_200_OK) - def test_has_put_permissions(self): request = factory.put('/1', {'text': 'foobar'}, format='json', HTTP_AUTHORIZATION=self.permitted_credentials) @@ -166,15 +156,6 @@ class ModelPermissionsIntegrationTests(TestCase): response = root_view(request, pk=1) self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) - def test_does_not_have_get_permissions(self): - request = factory.get('/', HTTP_AUTHORIZATION=self.disallowed_credentials) - response = root_view(request) - self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) - - request = factory.get('/1', HTTP_AUTHORIZATION=self.disallowed_credentials) - response = root_view(request, pk=1) - self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) - def test_does_not_have_put_permissions(self): request = factory.put('/1', {'text': 'foobar'}, format='json', HTTP_AUTHORIZATION=self.disallowed_credentials) From 4ef3aaf0ad0e31efe13d3c503a7099915a7c8875 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Fri, 22 Mar 2024 08:40:34 +0000 Subject: [PATCH 07/64] Revert #9030 (#9333) * Revert #9030 * Fix test case --- docs/api-guide/fields.md | 8 -- rest_framework/metadata.py | 4 - rest_framework/utils/field_mapping.py | 4 - tests/test_metadata.py | 129 -------------------------- tests/test_model_serializer.py | 6 +- 5 files changed, 3 insertions(+), 148 deletions(-) diff --git a/docs/api-guide/fields.md b/docs/api-guide/fields.md index ea944e1ff..94b6e7c21 100644 --- a/docs/api-guide/fields.md +++ b/docs/api-guide/fields.md @@ -68,14 +68,6 @@ When serializing the instance, default will be used if the object attribute or d Note that setting a `default` value implies that the field is not required. Including both the `default` and `required` keyword arguments is invalid and will raise an error. -Notes regarding default value propagation from model to serializer: - -All the default values from model will pass as default to the serializer and the options method. - -If the default is callable then it will be propagated to & evaluated every time in the serializer but not in options method. - -If the value for given field is not given then default value will be present in the serializer and available in serializer's methods. Specified validation on given field will be evaluated on default value as that field will be present in the serializer. - ### `allow_null` Normally an error will be raised if `None` is passed to a serializer field. Set this keyword argument to `True` if `None` should be considered a valid value. diff --git a/rest_framework/metadata.py b/rest_framework/metadata.py index fd0f4e163..364ca5b14 100644 --- a/rest_framework/metadata.py +++ b/rest_framework/metadata.py @@ -11,7 +11,6 @@ from django.http import Http404 from django.utils.encoding import force_str from rest_framework import exceptions, serializers -from rest_framework.fields import empty from rest_framework.request import clone_request from rest_framework.utils.field_mapping import ClassLookupDict @@ -150,7 +149,4 @@ class SimpleMetadata(BaseMetadata): for choice_value, choice_name in field.choices.items() ] - if getattr(field, 'default', None) and field.default != empty and not callable(field.default): - field_info['default'] = field.default - return field_info diff --git a/rest_framework/utils/field_mapping.py b/rest_framework/utils/field_mapping.py index 30bb65e0c..fc63f96fe 100644 --- a/rest_framework/utils/field_mapping.py +++ b/rest_framework/utils/field_mapping.py @@ -9,7 +9,6 @@ from django.db import models from django.utils.text import capfirst from rest_framework.compat import postgres_fields -from rest_framework.fields import empty from rest_framework.validators import UniqueValidator NUMERIC_FIELD_TYPES = ( @@ -128,9 +127,6 @@ def get_field_kwargs(field_name, model_field): kwargs['read_only'] = True return kwargs - if model_field.default is not None and model_field.default != empty and not callable(model_field.default): - kwargs['default'] = model_field.default - if model_field.has_default() or model_field.blank or model_field.null: kwargs['required'] = False diff --git a/tests/test_metadata.py b/tests/test_metadata.py index 387b9ecde..1bdc8697c 100644 --- a/tests/test_metadata.py +++ b/tests/test_metadata.py @@ -184,135 +184,6 @@ class TestMetadata: assert response.status_code == status.HTTP_200_OK assert response.data == expected - def test_actions_with_default(self): - """ - On generic views OPTIONS should return an 'actions' key with metadata - on the fields with default that may be supplied to PUT and POST requests. - """ - class NestedField(serializers.Serializer): - a = serializers.IntegerField(default=2) - b = serializers.IntegerField() - - class ExampleSerializer(serializers.Serializer): - choice_field = serializers.ChoiceField(['red', 'green', 'blue'], default='red') - integer_field = serializers.IntegerField( - min_value=1, max_value=1000, default=1 - ) - char_field = serializers.CharField( - min_length=3, max_length=40, default="example" - ) - list_field = serializers.ListField( - child=serializers.ListField( - child=serializers.IntegerField(default=1) - ) - ) - nested_field = NestedField() - uuid_field = serializers.UUIDField(label="UUID field") - - class ExampleView(views.APIView): - """Example view.""" - def post(self, request): - pass - - def get_serializer(self): - return ExampleSerializer() - - view = ExampleView.as_view() - response = view(request=request) - expected = { - 'name': 'Example', - 'description': 'Example view.', - 'renders': [ - 'application/json', - 'text/html' - ], - 'parses': [ - 'application/json', - 'application/x-www-form-urlencoded', - 'multipart/form-data' - ], - 'actions': { - 'POST': { - 'choice_field': { - 'type': 'choice', - 'required': False, - 'read_only': False, - 'label': 'Choice field', - "choices": [ - {'value': 'red', 'display_name': 'red'}, - {'value': 'green', 'display_name': 'green'}, - {'value': 'blue', 'display_name': 'blue'} - ], - 'default': 'red' - }, - 'integer_field': { - 'type': 'integer', - 'required': False, - 'read_only': False, - 'label': 'Integer field', - 'min_value': 1, - 'max_value': 1000, - 'default': 1 - }, - 'char_field': { - 'type': 'string', - 'required': False, - 'read_only': False, - 'label': 'Char field', - 'min_length': 3, - 'max_length': 40, - 'default': 'example' - }, - 'list_field': { - 'type': 'list', - 'required': True, - 'read_only': False, - 'label': 'List field', - 'child': { - 'type': 'list', - 'required': True, - 'read_only': False, - 'child': { - 'type': 'integer', - 'required': False, - 'read_only': False, - 'default': 1 - } - } - }, - 'nested_field': { - 'type': 'nested object', - 'required': True, - 'read_only': False, - 'label': 'Nested field', - 'children': { - 'a': { - 'type': 'integer', - 'required': False, - 'read_only': False, - 'label': 'A', - 'default': 2 - }, - 'b': { - 'type': 'integer', - 'required': True, - 'read_only': False, - 'label': 'B' - } - } - }, - 'uuid_field': { - 'type': 'string', - 'required': True, - 'read_only': False, - 'label': 'UUID field' - } - } - } - } - assert response.status_code == status.HTTP_200_OK - assert response.data == expected - def test_global_permissions(self): """ If a user does not have global permissions on an action, then any diff --git a/tests/test_model_serializer.py b/tests/test_model_serializer.py index 5b6551a98..d69f1652d 100644 --- a/tests/test_model_serializer.py +++ b/tests/test_model_serializer.py @@ -174,7 +174,7 @@ class TestRegularFieldMappings(TestCase): TestSerializer\(\): auto_field = IntegerField\(read_only=True\) big_integer_field = IntegerField\(.*\) - boolean_field = BooleanField\(default=False, required=False\) + boolean_field = BooleanField\(required=False\) char_field = CharField\(max_length=100\) comma_separated_integer_field = CharField\(max_length=100, validators=\[\]\) date_field = DateField\(\) @@ -183,7 +183,7 @@ class TestRegularFieldMappings(TestCase): email_field = EmailField\(max_length=100\) float_field = FloatField\(\) integer_field = IntegerField\(.*\) - null_boolean_field = BooleanField\(allow_null=True, default=False, required=False\) + null_boolean_field = BooleanField\(allow_null=True, required=False\) positive_integer_field = IntegerField\(.*\) positive_small_integer_field = IntegerField\(.*\) slug_field = SlugField\(allow_unicode=False, max_length=100\) @@ -210,7 +210,7 @@ class TestRegularFieldMappings(TestCase): length_limit_field = CharField\(max_length=12, min_length=3\) blank_field = CharField\(allow_blank=True, max_length=10, required=False\) null_field = IntegerField\(allow_null=True,.*required=False\) - default_field = IntegerField\(default=0,.*required=False\) + default_field = IntegerField\(.*required=False\) descriptive_field = IntegerField\(help_text='Some help text', label='A label'.*\) choices_field = ChoiceField\(choices=(?:\[|\()\('red', 'Red'\), \('blue', 'Blue'\), \('green', 'Green'\)(?:\]|\))\) text_choices_field = ChoiceField\(choices=(?:\[|\()\('red', 'Red'\), \('blue', 'Blue'\), \('green', 'Green'\)(?:\]|\))\) From 400b4c54419c5c88542ddd0b97219ad4fa8ee29a Mon Sep 17 00:00:00 2001 From: Asif Saif Uddin Date: Fri, 22 Mar 2024 15:39:30 +0600 Subject: [PATCH 08/64] =?UTF-8?q?Revert=20"Fix=20NamespaceVersioning=20ign?= =?UTF-8?q?oring=20DEFAULT=5FVERSION=20on=20non-None=20namespac=E2=80=A6"?= =?UTF-8?q?=20(#9335)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This reverts commit 71f87a586400074f1840276c5cf36fc7da1c2c4c. --- rest_framework/versioning.py | 17 ++++--- tests/test_versioning.py | 93 +----------------------------------- 2 files changed, 9 insertions(+), 101 deletions(-) diff --git a/rest_framework/versioning.py b/rest_framework/versioning.py index a1c0ce4d7..c2764c7a4 100644 --- a/rest_framework/versioning.py +++ b/rest_framework/versioning.py @@ -119,16 +119,15 @@ class NamespaceVersioning(BaseVersioning): def determine_version(self, request, *args, **kwargs): resolver_match = getattr(request, 'resolver_match', None) - if resolver_match is not None and resolver_match.namespace: - # Allow for possibly nested namespaces. - possible_versions = resolver_match.namespace.split(':') - for version in possible_versions: - if self.is_allowed_version(version): - return version + if resolver_match is None or not resolver_match.namespace: + return self.default_version - if not self.is_allowed_version(self.default_version): - raise exceptions.NotFound(self.invalid_version_message) - return self.default_version + # Allow for possibly nested namespaces. + possible_versions = resolver_match.namespace.split(':') + for version in possible_versions: + if self.is_allowed_version(version): + return version + raise exceptions.NotFound(self.invalid_version_message) def reverse(self, viewname, args=None, kwargs=None, request=None, format=None, **extra): if request.version is not None: diff --git a/tests/test_versioning.py b/tests/test_versioning.py index 1ccecae0b..b21646184 100644 --- a/tests/test_versioning.py +++ b/tests/test_versioning.py @@ -272,7 +272,7 @@ class TestInvalidVersion: assert response.status_code == status.HTTP_404_NOT_FOUND -class TestAcceptHeaderAllowedAndDefaultVersion: +class TestAllowedAndDefaultVersion: def test_missing_without_default(self): scheme = versioning.AcceptHeaderVersioning view = AllowedVersionsView.as_view(versioning_class=scheme) @@ -318,97 +318,6 @@ class TestAcceptHeaderAllowedAndDefaultVersion: assert response.data == {'version': 'v2'} -class TestNamespaceAllowedAndDefaultVersion: - def test_no_namespace_without_default(self): - class FakeResolverMatch: - namespace = None - - scheme = versioning.NamespaceVersioning - view = AllowedVersionsView.as_view(versioning_class=scheme) - - request = factory.get('/endpoint/') - request.resolver_match = FakeResolverMatch - response = view(request) - assert response.status_code == status.HTTP_404_NOT_FOUND - - def test_no_namespace_with_default(self): - class FakeResolverMatch: - namespace = None - - scheme = versioning.NamespaceVersioning - view = AllowedAndDefaultVersionsView.as_view(versioning_class=scheme) - - request = factory.get('/endpoint/') - request.resolver_match = FakeResolverMatch - response = view(request) - assert response.status_code == status.HTTP_200_OK - assert response.data == {'version': 'v2'} - - def test_no_match_without_default(self): - class FakeResolverMatch: - namespace = 'no_match' - - scheme = versioning.NamespaceVersioning - view = AllowedVersionsView.as_view(versioning_class=scheme) - - request = factory.get('/endpoint/') - request.resolver_match = FakeResolverMatch - response = view(request) - assert response.status_code == status.HTTP_404_NOT_FOUND - - def test_no_match_with_default(self): - class FakeResolverMatch: - namespace = 'no_match' - - scheme = versioning.NamespaceVersioning - view = AllowedAndDefaultVersionsView.as_view(versioning_class=scheme) - - request = factory.get('/endpoint/') - request.resolver_match = FakeResolverMatch - response = view(request) - assert response.status_code == status.HTTP_200_OK - assert response.data == {'version': 'v2'} - - def test_with_default(self): - class FakeResolverMatch: - namespace = 'v1' - - scheme = versioning.NamespaceVersioning - view = AllowedAndDefaultVersionsView.as_view(versioning_class=scheme) - - request = factory.get('/endpoint/') - request.resolver_match = FakeResolverMatch - response = view(request) - assert response.status_code == status.HTTP_200_OK - assert response.data == {'version': 'v1'} - - def test_no_match_without_default_but_none_allowed(self): - class FakeResolverMatch: - namespace = 'no_match' - - scheme = versioning.NamespaceVersioning - view = AllowedWithNoneVersionsView.as_view(versioning_class=scheme) - - request = factory.get('/endpoint/') - request.resolver_match = FakeResolverMatch - response = view(request) - assert response.status_code == status.HTTP_200_OK - assert response.data == {'version': None} - - def test_no_match_with_default_and_none_allowed(self): - class FakeResolverMatch: - namespace = 'no_match' - - scheme = versioning.NamespaceVersioning - view = AllowedWithNoneAndDefaultVersionsView.as_view(versioning_class=scheme) - - request = factory.get('/endpoint/') - request.resolver_match = FakeResolverMatch - response = view(request) - assert response.status_code == status.HTTP_200_OK - assert response.data == {'version': 'v2'} - - class TestHyperlinkedRelatedField(URLPatternsTestCase, APITestCase): included = [ path('namespaced//', dummy_pk_view, name='namespaced'), From eb361d289deb4bc99ad2ebab9c5f50a92de40339 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Fri, 22 Mar 2024 10:52:43 +0000 Subject: [PATCH 09/64] SearchFilter.get_search_terms returns list. (#9338) --- rest_framework/filters.py | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/rest_framework/filters.py b/rest_framework/filters.py index 86effe24e..5742f512e 100644 --- a/rest_framework/filters.py +++ b/rest_framework/filters.py @@ -24,15 +24,19 @@ def search_smart_split(search_terms): """generator that first splits string by spaces, leaving quoted phrases together, then it splits non-quoted phrases by commas. """ + split_terms = [] for term in smart_split(search_terms): # trim commas to avoid bad matching for quoted phrases term = term.strip(',') if term.startswith(('"', "'")) and term[0] == term[-1]: # quoted phrases are kept together without any other split - yield unescape_string_literal(term) + split_terms.append(unescape_string_literal(term)) else: # non-quoted tokens are split by comma, keeping only non-empty ones - yield from (sub_term.strip() for sub_term in term.split(',') if sub_term) + for sub_term in term.split(','): + if sub_term: + split_terms.append(sub_term.strip()) + return split_terms class BaseFilterBackend: @@ -85,7 +89,8 @@ class SearchFilter(BaseFilterBackend): """ value = request.query_params.get(self.search_param, '') field = CharField(trim_whitespace=False, allow_blank=True) - return field.run_validation(value) + cleaned_value = field.run_validation(value) + return search_smart_split(cleaned_value) def construct_search(self, field_name, queryset): lookup = self.lookup_prefixes.get(field_name[0]) @@ -163,7 +168,7 @@ class SearchFilter(BaseFilterBackend): reduce( operator.or_, (models.Q(**{orm_lookup: term}) for orm_lookup in orm_lookups) - ) for term in search_smart_split(search_terms) + ) for term in search_terms ) queryset = queryset.filter(reduce(operator.and_, conditions)) From 328591693d7a3e734ca5a740dddf85e11ccd208f Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Fri, 22 Mar 2024 11:47:50 +0000 Subject: [PATCH 10/64] Version 3.15.1 (#9339) * Version 3.15.1 --- docs/community/3.15-announcement.md | 8 -------- docs/community/release-notes.md | 7 +++++++ rest_framework/__init__.py | 2 +- 3 files changed, 8 insertions(+), 9 deletions(-) diff --git a/docs/community/3.15-announcement.md b/docs/community/3.15-announcement.md index 5bcff6969..848d534b2 100644 --- a/docs/community/3.15-announcement.md +++ b/docs/community/3.15-announcement.md @@ -31,10 +31,6 @@ The current minimum versions of Django still is 3.0 and Python 3.6. `ModelSerializer` generates validators for [UniqueConstraint](https://docs.djangoproject.com/en/4.0/ref/models/constraints/#uniqueconstraint) (both UniqueValidator and UniqueTogetherValidator) -## ValidationErrors improvements - -The `ValidationError` has been aligned with Django's, currently supporting the same style (signature) and nesting. - ## SimpleRouter non-regex matching support By default the URLs created by `SimpleRouter` use regular expressions. This behavior can be modified by setting the `use_regex_path` argument to `False` when instantiating the router. @@ -47,10 +43,6 @@ Dependency on pytz has been removed and deprecation warnings have been added, Dj Searches now may contain _quoted phrases_ with spaces, each phrase is considered as a single search term, and it will raise a validation error if any null-character is provided in search. See the [Filtering API guide](../api-guide/filtering.md) for more information. -## Default values propagation - -Model fields' default values are now propagated to serializer fields, for more information see the [Serializer fields API guide](../api-guide/fields.md#default). - ## Other fixes and improvements There are a number of fixes and minor improvements in this release, ranging from documentation, internal infrastructure (typing, testing, requirements, deprecation, etc.), security and overall behaviour. diff --git a/docs/community/release-notes.md b/docs/community/release-notes.md index 07220ef0f..6da3efb9f 100644 --- a/docs/community/release-notes.md +++ b/docs/community/release-notes.md @@ -36,6 +36,13 @@ You can determine your currently installed version using `pip show`: ## 3.15.x series +### 3.15.1 + +Date: 22nd March 2024 + +* Fix `SearchFilter` handling of quoted and comma separated strings, when `.get_search_terms` is being called into by a custom class. See [[#9338](https://github.com/encode/django-rest-framework/issues/9338)] +* Revert number of 3.15.0 issues which included unintended side-effects. See [[#9331](https://github.com/encode/django-rest-framework/issues/9331)] + ### 3.15.0 Date: 15th March 2024 diff --git a/rest_framework/__init__.py b/rest_framework/__init__.py index 45ff90980..fe2eab04b 100644 --- a/rest_framework/__init__.py +++ b/rest_framework/__init__.py @@ -10,7 +10,7 @@ ______ _____ _____ _____ __ import django __title__ = 'Django REST framework' -__version__ = '3.15.0' +__version__ = '3.15.1' __author__ = 'Tom Christie' __license__ = 'BSD 3-Clause' __copyright__ = 'Copyright 2011-2023 Encode OSS Ltd' From 6df509863d0ec2684e7c2cc13b27686aa87cffb6 Mon Sep 17 00:00:00 2001 From: Bradley Wells Date: Fri, 22 Mar 2024 20:33:00 +0200 Subject: [PATCH 11/64] Add @api_view example to caching documentation (#9131) --- docs/api-guide/caching.md | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/docs/api-guide/caching.md b/docs/api-guide/caching.md index 503acb09e..d5f7ecb3a 100644 --- a/docs/api-guide/caching.md +++ b/docs/api-guide/caching.md @@ -59,6 +59,31 @@ class PostView(APIView): return Response(content) ``` + +## Using cache with @api_view decorator + +When using @api_view decorator, the Django-provided method-based cache decorators such as [`cache_page`][page], +[`vary_on_cookie`][cookie] and [`vary_on_headers`][headers] can be called directly. + +```python +from django.views.decorators.cache import cache_page +from django.views.decorators.vary import vary_on_cookie + +from rest_framework.decorators import api_view +from rest_framework.response import Response + + +@cache_page(60 * 15) +@vary_on_cookie +@api_view(['GET']) +def get_user_list(request): + content = { + 'user_feed': request.user.get_user_feed() + } + return Response(content) +``` + + **NOTE:** The [`cache_page`][page] decorator only caches the `GET` and `HEAD` responses with status 200. From f4194c4684420ac86485d9610adf760064db381f Mon Sep 17 00:00:00 2001 From: "John-Anthony G. Thevos" Date: Fri, 22 Mar 2024 15:37:20 -0400 Subject: [PATCH 12/64] Update docstring (#9340) --- rest_framework/filters.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/rest_framework/filters.py b/rest_framework/filters.py index 5742f512e..435c30c88 100644 --- a/rest_framework/filters.py +++ b/rest_framework/filters.py @@ -21,9 +21,7 @@ from rest_framework.settings import api_settings def search_smart_split(search_terms): - """generator that first splits string by spaces, leaving quoted phrases together, - then it splits non-quoted phrases by commas. - """ + """Returns sanitized search terms as a list.""" split_terms = [] for term in smart_split(search_terms): # trim commas to avoid bad matching for quoted phrases From 085b7e166ba80aa973645e5249b441f2dbdc0c96 Mon Sep 17 00:00:00 2001 From: "John-Anthony G. Thevos" Date: Wed, 27 Mar 2024 06:39:49 -0400 Subject: [PATCH 13/64] Apply black formatting to caching markdown (#9341) It _looks_ like blacken-docs is failing on this file. Running black locally fails with a failed to reformat. This is because it expects python code, and when it hits the ">", there's invalid python. --- docs/api-guide/caching.md | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/docs/api-guide/caching.md b/docs/api-guide/caching.md index d5f7ecb3a..c4ab215c8 100644 --- a/docs/api-guide/caching.md +++ b/docs/api-guide/caching.md @@ -75,11 +75,9 @@ from rest_framework.response import Response @cache_page(60 * 15) @vary_on_cookie -@api_view(['GET']) +@api_view(["GET"]) def get_user_list(request): - content = { - 'user_feed': request.user.get_user_feed() - } + content = {"user_feed": request.user.get_user_feed()} return Response(content) ``` From 63063da0820e23ef0edbf92a3031103a6c2ce254 Mon Sep 17 00:00:00 2001 From: Marcelo Galigniana Date: Thu, 4 Apr 2024 04:12:19 -0300 Subject: [PATCH 14/64] Update renderers documentation example (#9362) --- docs/api-guide/renderers.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/api-guide/renderers.md b/docs/api-guide/renderers.md index 44f1b6021..d48f785ab 100644 --- a/docs/api-guide/renderers.md +++ b/docs/api-guide/renderers.md @@ -283,7 +283,7 @@ By default this will include the following keys: `view`, `request`, `response`, The following is an example plaintext renderer that will return a response with the `data` parameter as the content of the response. - from django.utils.encoding import smart_text + from django.utils.encoding import smart_str from rest_framework import renderers @@ -292,7 +292,7 @@ The following is an example plaintext renderer that will return a response with format = 'txt' def render(self, data, accepted_media_type=None, renderer_context=None): - return smart_text(data, encoding=self.charset) + return smart_str(data, encoding=self.charset) ## Setting the character set From 9864c47018238dd9f8477df0e4ab2ef62a24fd2b Mon Sep 17 00:00:00 2001 From: JAEGYUN JUNG Date: Mon, 8 Apr 2024 20:26:02 +0900 Subject: [PATCH 15/64] Removing live examples of tutorial code that are no longer hosted (#9363) * docs: Removing live examples of tutorial code that are no longer hosted * docs: Remove all references to tutorial links --- README.md | 3 --- docs/community/project-management.md | 2 -- docs/index.md | 1 - docs/tutorial/1-serialization.md | 5 ++--- 4 files changed, 2 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index 56933a4c8..75d177001 100644 --- a/README.md +++ b/README.md @@ -45,8 +45,6 @@ Some reasons you might want to use REST framework: * Customizable all the way down - just use [regular function-based views][functionview-section] if you don't need the [more][generic-views] [powerful][viewsets] [features][routers]. * [Extensive documentation][docs], and [great community support][group]. -There is a live example API for testing purposes, [available here][sandbox]. - **Below**: *Screenshot from the browsable API* ![Screenshot][image] @@ -188,7 +186,6 @@ Please see the [security policy][security-policy]. [pypi]: https://pypi.org/project/djangorestframework/ [twitter]: https://twitter.com/starletdreaming [group]: https://groups.google.com/forum/?fromgroups#!forum/django-rest-framework -[sandbox]: https://restframework.herokuapp.com/ [funding]: https://fund.django-rest-framework.org/topics/funding/ [sponsors]: https://fund.django-rest-framework.org/topics/funding/#our-sponsors diff --git a/docs/community/project-management.md b/docs/community/project-management.md index 92132ae7e..8545fe2b7 100644 --- a/docs/community/project-management.md +++ b/docs/community/project-management.md @@ -199,7 +199,6 @@ If `@tomchristie` ceases to participate in the project then `@j4mie` has respons The following issues still need to be addressed: * Ensure `@jamie` has back-up access to the `django-rest-framework.org` domain setup and admin. -* Document ownership of the [live example][sandbox] API. * Document ownership of the [mailing list][mailing-list] and IRC channel. * Document ownership and management of the security mailing list. @@ -208,5 +207,4 @@ The following issues still need to be addressed: [transifex-project]: https://www.transifex.com/projects/p/django-rest-framework/ [transifex-client]: https://pypi.org/project/transifex-client/ [translation-memory]: http://docs.transifex.com/guides/tm#let-tm-automatically-populate-translations -[sandbox]: https://restframework.herokuapp.com/ [mailing-list]: https://groups.google.com/forum/#!forum/django-rest-framework diff --git a/docs/index.md b/docs/index.md index 07d233107..222b28de5 100644 --- a/docs/index.md +++ b/docs/index.md @@ -247,7 +247,6 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. [serializer-section]: api-guide/serializers#serializers [modelserializer-section]: api-guide/serializers#modelserializer [functionview-section]: api-guide/views#function-based-views -[sandbox]: https://restframework.herokuapp.com/ [sponsors]: https://fund.django-rest-framework.org/topics/funding/#our-sponsors [quickstart]: tutorial/quickstart.md diff --git a/docs/tutorial/1-serialization.md b/docs/tutorial/1-serialization.md index 6db3ea282..c86008104 100644 --- a/docs/tutorial/1-serialization.md +++ b/docs/tutorial/1-serialization.md @@ -8,7 +8,7 @@ The tutorial is fairly in-depth, so you should probably get a cookie and a cup o --- -**Note**: The code for this tutorial is available in the [encode/rest-framework-tutorial][repo] repository on GitHub. The completed implementation is also online as a sandbox version for testing, [available here][sandbox]. +**Note**: The code for this tutorial is available in the [encode/rest-framework-tutorial][repo] repository on GitHub. Feel free to clone the repository and see the code in action. --- @@ -307,7 +307,7 @@ Quit out of the shell... Validating models... 0 errors found - Django version 4.0, using settings 'tutorial.settings' + Django version 5.0, using settings 'tutorial.settings' Starting Development server at http://127.0.0.1:8000/ Quit the server with CONTROL-C. @@ -371,7 +371,6 @@ We'll see how we can start to improve things in [part 2 of the tutorial][tut-2]. [quickstart]: quickstart.md [repo]: https://github.com/encode/rest-framework-tutorial -[sandbox]: https://restframework.herokuapp.com/ [venv]: https://docs.python.org/3/library/venv.html [tut-2]: 2-requests-and-responses.md [httpie]: https://github.com/httpie/httpie#installation From f4daa98f48f9a25079531058fba4387949a4b54f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20Bl=C3=A4ul?= <6819464+gogowitsch@users.noreply.github.com> Date: Fri, 26 Apr 2024 14:18:16 +0200 Subject: [PATCH 16/64] Remove an unnecessary step from quickstart.md (#9387) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Since django is a dependency of djangorestframework, we don’t need to install it manually. --- docs/tutorial/quickstart.md | 1 - 1 file changed, 1 deletion(-) diff --git a/docs/tutorial/quickstart.md b/docs/tutorial/quickstart.md index 7b46a44e6..a140dbce0 100644 --- a/docs/tutorial/quickstart.md +++ b/docs/tutorial/quickstart.md @@ -15,7 +15,6 @@ Create a new Django project named `tutorial`, then start a new app called `quick source env/bin/activate # On Windows use `env\Scripts\activate` # Install Django and Django REST framework into the virtual environment - pip install django pip install djangorestframework # Set up a new project with a single application From 97c5617edcf4189e8f9dca688ce8364992567b72 Mon Sep 17 00:00:00 2001 From: Jakub Szaredko <54867641+Szaroslav@users.noreply.github.com> Date: Sat, 27 Apr 2024 12:57:48 +0200 Subject: [PATCH 17/64] Docs: Add Python 3.12 to the requirements (#9382) --- docs/index.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/index.md b/docs/index.md index 222b28de5..4c5f20c48 100644 --- a/docs/index.md +++ b/docs/index.md @@ -86,7 +86,7 @@ continued development by **[signing up for a paid plan][funding]**. REST framework requires the following: -* Python (3.6, 3.7, 3.8, 3.9, 3.10, 3.11) +* Python (3.6, 3.7, 3.8, 3.9, 3.10, 3.11, 3.12) * Django (3.0, 3.1, 3.2, 4.0, 4.1, 4.2, 5.0) We **highly recommend** and only officially support the latest patch release of From f96c065607e6f4651edaeef5cb75e0b44b324c56 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Sat, 27 Apr 2024 11:58:44 +0100 Subject: [PATCH 18/64] Update README.md (#9375) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Drop unnecessary self-serving promo text. (blergh) 😅 --- README.md | 3 --- 1 file changed, 3 deletions(-) diff --git a/README.md b/README.md index 75d177001..cadfd73a5 100644 --- a/README.md +++ b/README.md @@ -172,8 +172,6 @@ Full documentation for the project is available at [https://www.django-rest-fram For questions and support, use the [REST framework discussion group][group], or `#restframework` on libera.chat IRC. -You may also want to [follow the author on Twitter][twitter]. - # Security Please see the [security policy][security-policy]. @@ -184,7 +182,6 @@ Please see the [security policy][security-policy]. [codecov]: https://codecov.io/github/encode/django-rest-framework?branch=master [pypi-version]: https://img.shields.io/pypi/v/djangorestframework.svg [pypi]: https://pypi.org/project/djangorestframework/ -[twitter]: https://twitter.com/starletdreaming [group]: https://groups.google.com/forum/?fromgroups#!forum/django-rest-framework [funding]: https://fund.django-rest-framework.org/topics/funding/ From 7f18ec1b536a90b1fc194ef6140a6dcd8b605051 Mon Sep 17 00:00:00 2001 From: Max Muoto Date: Sat, 27 Apr 2024 06:07:05 -0500 Subject: [PATCH 19/64] Revert "Ensure CursorPagination respects nulls in the ordering field (#8912)" (#9381) This reverts commit b1cec517ff33d633d3ebcf5794a5f0f0583fabe6. --- rest_framework/pagination.py | 16 ++--- tests/test_pagination.py | 134 +---------------------------------- 2 files changed, 8 insertions(+), 142 deletions(-) diff --git a/rest_framework/pagination.py b/rest_framework/pagination.py index 2b20e76af..a543ceeb5 100644 --- a/rest_framework/pagination.py +++ b/rest_framework/pagination.py @@ -11,7 +11,6 @@ from urllib import parse from django.core.paginator import InvalidPage from django.core.paginator import Paginator as DjangoPaginator -from django.db.models import Q from django.template import loader from django.utils.encoding import force_str from django.utils.translation import gettext_lazy as _ @@ -631,7 +630,7 @@ class CursorPagination(BasePagination): queryset = queryset.order_by(*self.ordering) # If we have a cursor with a fixed position then filter by that. - if str(current_position) != 'None': + if current_position is not None: order = self.ordering[0] is_reversed = order.startswith('-') order_attr = order.lstrip('-') @@ -642,12 +641,7 @@ class CursorPagination(BasePagination): else: kwargs = {order_attr + '__gt': current_position} - filter_query = Q(**kwargs) - # If some records contain a null for the ordering field, don't lose them. - # When reverse ordering, nulls will come last and need to be included. - if (reverse and not is_reversed) or is_reversed: - filter_query |= Q(**{order_attr + '__isnull': True}) - queryset = queryset.filter(filter_query) + queryset = queryset.filter(**kwargs) # If we have an offset cursor then offset the entire page by that amount. # We also always fetch an extra item in order to determine if there is a @@ -720,7 +714,7 @@ class CursorPagination(BasePagination): # The item in this position and the item following it # have different positions. We can use this position as # our marker. - has_item_with_unique_position = position is not None + has_item_with_unique_position = True break # The item in this position has the same position as the item @@ -773,7 +767,7 @@ class CursorPagination(BasePagination): # The item in this position and the item following it # have different positions. We can use this position as # our marker. - has_item_with_unique_position = position is not None + has_item_with_unique_position = True break # The item in this position has the same position as the item @@ -896,7 +890,7 @@ class CursorPagination(BasePagination): attr = instance[field_name] else: attr = getattr(instance, field_name) - return None if attr is None else str(attr) + return str(attr) def get_paginated_response(self, data): return Response({ diff --git a/tests/test_pagination.py b/tests/test_pagination.py index 090eb0d81..02d443ade 100644 --- a/tests/test_pagination.py +++ b/tests/test_pagination.py @@ -972,24 +972,17 @@ class TestCursorPagination(CursorPaginationTestsMixin): def __init__(self, items): self.items = items - def filter(self, q): - q_args = dict(q.deconstruct()[1]) - if not q_args: - # django 3.0.x artifact - q_args = dict(q.deconstruct()[2]) - created__gt = q_args.get('created__gt') - created__lt = q_args.get('created__lt') - + def filter(self, created__gt=None, created__lt=None): if created__gt is not None: return MockQuerySet([ item for item in self.items - if item.created is None or item.created > int(created__gt) + if item.created > int(created__gt) ]) assert created__lt is not None return MockQuerySet([ item for item in self.items - if item.created is None or item.created < int(created__lt) + if item.created < int(created__lt) ]) def order_by(self, *ordering): @@ -1108,127 +1101,6 @@ class TestCursorPaginationWithValueQueryset(CursorPaginationTestsMixin, TestCase return (previous, current, next, previous_url, next_url) -class NullableCursorPaginationModel(models.Model): - created = models.IntegerField(null=True) - - -class TestCursorPaginationWithNulls(TestCase): - """ - Unit tests for `pagination.CursorPagination` with ordering on a nullable field. - """ - - def setUp(self): - class ExamplePagination(pagination.CursorPagination): - page_size = 1 - ordering = 'created' - - self.pagination = ExamplePagination() - data = [ - None, None, 3, 4 - ] - for idx in data: - NullableCursorPaginationModel.objects.create(created=idx) - - self.queryset = NullableCursorPaginationModel.objects.all() - - get_pages = TestCursorPagination.get_pages - - def test_ascending(self): - """Test paginating one row at a time, current should go 1, 2, 3, 4, 3, 2, 1.""" - (previous, current, next, previous_url, next_url) = self.get_pages('/') - - assert previous is None - assert current == [None] - assert next == [None] - - (previous, current, next, previous_url, next_url) = self.get_pages(next_url) - - assert previous == [None] - assert current == [None] - assert next == [3] - - (previous, current, next, previous_url, next_url) = self.get_pages(next_url) - - assert previous == [3] # [None] paging artifact documented at https://github.com/ddelange/django-rest-framework/blob/3.14.0/rest_framework/pagination.py#L789 - assert current == [3] - assert next == [4] - - (previous, current, next, previous_url, next_url) = self.get_pages(next_url) - - assert previous == [3] - assert current == [4] - assert next is None - assert next_url is None - - (previous, current, next, previous_url, next_url) = self.get_pages(previous_url) - - assert previous == [None] - assert current == [3] - assert next == [4] - - (previous, current, next, previous_url, next_url) = self.get_pages(previous_url) - - assert previous == [None] - assert current == [None] - assert next == [None] # [3] paging artifact documented at https://github.com/ddelange/django-rest-framework/blob/3.14.0/rest_framework/pagination.py#L731 - - (previous, current, next, previous_url, next_url) = self.get_pages(previous_url) - - assert previous is None - assert current == [None] - assert next == [None] - - def test_descending(self): - """Test paginating one row at a time, current should go 4, 3, 2, 1, 2, 3, 4.""" - self.pagination.ordering = ('-created',) - (previous, current, next, previous_url, next_url) = self.get_pages('/') - - assert previous is None - assert current == [4] - assert next == [3] - - (previous, current, next, previous_url, next_url) = self.get_pages(next_url) - - assert previous == [None] # [4] paging artifact - assert current == [3] - assert next == [None] - - (previous, current, next, previous_url, next_url) = self.get_pages(next_url) - - assert previous == [None] # [3] paging artifact - assert current == [None] - assert next == [None] - - (previous, current, next, previous_url, next_url) = self.get_pages(next_url) - - assert previous == [None] - assert current == [None] - assert next is None - assert next_url is None - - (previous, current, next, previous_url, next_url) = self.get_pages(previous_url) - - assert previous == [3] - assert current == [None] - assert next == [None] - - (previous, current, next, previous_url, next_url) = self.get_pages(previous_url) - - assert previous == [None] - assert current == [3] - assert next == [3] # [4] paging artifact documented at https://github.com/ddelange/django-rest-framework/blob/3.14.0/rest_framework/pagination.py#L731 - - # skip back artifact - (previous, current, next, previous_url, next_url) = self.get_pages(previous_url) - (previous, current, next, previous_url, next_url) = self.get_pages(previous_url) - - (previous, current, next, previous_url, next_url) = self.get_pages(previous_url) - - assert previous is None - assert current == [4] - assert next == [3] - - def test_get_displayed_page_numbers(): """ Test our contextual page display function. From e596f43c4e8183e4639996f95477b71eba3c8d80 Mon Sep 17 00:00:00 2001 From: Terence Honles Date: Sat, 27 Apr 2024 13:15:06 +0200 Subject: [PATCH 20/64] use warnings rather than logging a warning for DecimalField warnings (#9367) --- rest_framework/fields.py | 8 +++----- tests/test_fields.py | 23 ++++++++++++++--------- 2 files changed, 17 insertions(+), 14 deletions(-) diff --git a/rest_framework/fields.py b/rest_framework/fields.py index fda656507..cbc02e2c2 100644 --- a/rest_framework/fields.py +++ b/rest_framework/fields.py @@ -4,9 +4,9 @@ import datetime import decimal import functools import inspect -import logging import re import uuid +import warnings from collections.abc import Mapping from enum import Enum @@ -44,8 +44,6 @@ from rest_framework.utils.formatting import lazy_format from rest_framework.utils.timezone import valid_datetime from rest_framework.validators import ProhibitSurrogateCharactersValidator -logger = logging.getLogger("rest_framework.fields") - class empty: """ @@ -989,9 +987,9 @@ class DecimalField(Field): self.min_value = min_value if self.max_value is not None and not isinstance(self.max_value, decimal.Decimal): - logger.warning("max_value in DecimalField should be Decimal type.") + warnings.warn("max_value should be a Decimal instance.") if self.min_value is not None and not isinstance(self.min_value, decimal.Decimal): - logger.warning("min_value in DecimalField should be Decimal type.") + warnings.warn("min_value should be a Decimal instance.") if self.max_digits is not None and self.decimal_places is not None: self.max_whole_digits = self.max_digits - self.decimal_places diff --git a/tests/test_fields.py b/tests/test_fields.py index 2b6fc56ac..9ac84dd21 100644 --- a/tests/test_fields.py +++ b/tests/test_fields.py @@ -4,6 +4,7 @@ import os import re import sys import uuid +import warnings from decimal import ROUND_DOWN, ROUND_UP, Decimal from enum import auto from unittest.mock import patch @@ -1254,15 +1255,19 @@ class TestMinMaxDecimalField(FieldValues): ) def test_warning_when_not_decimal_types(self, caplog): - import logging - serializers.DecimalField( - max_digits=3, decimal_places=1, - min_value=10, max_value=20 - ) - assert caplog.record_tuples == [ - ("rest_framework.fields", logging.WARNING, "max_value in DecimalField should be Decimal type."), - ("rest_framework.fields", logging.WARNING, "min_value in DecimalField should be Decimal type.") - ] + with warnings.catch_warnings(record=True) as w: + warnings.simplefilter('always') + + serializers.DecimalField( + max_digits=3, decimal_places=1, + min_value=10, max_value=20 + ) + + assert len(w) == 2 + assert all(issubclass(i.category, UserWarning) for i in w) + + assert 'max_value should be a Decimal instance' in str(w[0].message) + assert 'min_value should be a Decimal instance' in str(w[1].message) class TestAllowEmptyStrDecimalFieldWithValidators(FieldValues): From 430de731e7b521e95ca7d5b77a561ffc94d970c5 Mon Sep 17 00:00:00 2001 From: Peter Thomassen Date: Fri, 26 Apr 2024 12:13:17 +0200 Subject: [PATCH 21/64] Clean up project management docs --- docs/community/project-management.md | 58 +++------------------------- 1 file changed, 6 insertions(+), 52 deletions(-) diff --git a/docs/community/project-management.md b/docs/community/project-management.md index 8545fe2b7..5fadca7b6 100644 --- a/docs/community/project-management.md +++ b/docs/community/project-management.md @@ -13,55 +13,13 @@ The aim is to ensure that the project has a high ## Maintenance team -We have a quarterly maintenance cycle where new members may join the maintenance team. We currently cap the size of the team at 5 members, and may encourage folks to step out of the team for a cycle to allow new members to participate. +[Participating actively in the REST framework project](contributing.md) **does not require being part of the maintenance team**. Almost every important part of issue triage and project improvement can be actively worked on regardless of your collaborator status on the repository. -#### Current team +#### Composition -The [maintenance team for Q4 2015](https://github.com/encode/django-rest-framework/issues/2190): +The composition of the maintenance team is handled by [@tomchristie](https://github.com/encode/). Team members will be added as collaborators to the repository. -* [@tomchristie](https://github.com/encode/) -* [@xordoquy](https://github.com/xordoquy/) (Release manager.) -* [@carltongibson](https://github.com/carltongibson/) -* [@kevin-brown](https://github.com/kevin-brown/) -* [@jpadilla](https://github.com/jpadilla/) - -#### Maintenance cycles - -Each maintenance cycle is initiated by an issue being opened with the `Process` label. - -* To be considered for a maintainer role simply comment against the issue. -* Existing members must explicitly opt-in to the next cycle by check-marking their name. -* The final decision on the incoming team will be made by `@tomchristie`. - -Members of the maintenance team will be added as collaborators to the repository. - -The following template should be used for the description of the issue, and serves as the formal process for selecting the team. - - This issue is for determining the maintenance team for the *** period. - - Please see the [Project management](https://www.django-rest-framework.org/topics/project-management/) section of our documentation for more details. - - --- - - #### Renewing existing members. - - The following people are the current maintenance team. Please checkmark your name if you wish to continue to have write permission on the repository for the *** period. - - - [ ] @*** - - [ ] @*** - - [ ] @*** - - [ ] @*** - - [ ] @*** - - --- - - #### New members. - - If you wish to be considered for this or a future date, please comment against this or subsequent issues. - - To modify this process for future maintenance cycles make a pull request to the [project management](https://www.django-rest-framework.org/topics/project-management/) documentation. - -#### Responsibilities of team members +#### Responsibilities Team members have the following responsibilities. @@ -78,16 +36,12 @@ Further notes for maintainers: * Each issue/pull request should have exactly one label once triaged. * Search for un-triaged issues with [is:open no:label][un-triaged]. -It should be noted that participating actively in the REST framework project clearly **does not require being part of the maintenance team**. Almost every import part of issue triage and project improvement can be actively worked on regardless of your collaborator status on the repository. - --- ## Release process -The release manager is selected on every quarterly maintenance cycle. - -* The manager should be selected by `@tomchristie`. -* The manager will then have the maintainer role added to PyPI package. +* The release manager is selected by `@tomchristie`. +* The release manager will then have the maintainer role added to PyPI package. * The previous manager will then have the maintainer role removed from the PyPI package. Our PyPI releases will be handled by either the current release manager, or by `@tomchristie`. Every release should have an open issue tagged with the `Release` label and marked against the appropriate milestone. From f642d85be26e0c7b7733a774363528022e769f7f Mon Sep 17 00:00:00 2001 From: Peter Thomassen Date: Fri, 26 Apr 2024 12:13:39 +0200 Subject: [PATCH 22/64] Fix docs typo --- docs/community/project-management.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/community/project-management.md b/docs/community/project-management.md index 5fadca7b6..4f203e13b 100644 --- a/docs/community/project-management.md +++ b/docs/community/project-management.md @@ -152,7 +152,7 @@ If `@tomchristie` ceases to participate in the project then `@j4mie` has respons The following issues still need to be addressed: -* Ensure `@jamie` has back-up access to the `django-rest-framework.org` domain setup and admin. +* Ensure `@j4mie` has back-up access to the `django-rest-framework.org` domain setup and admin. * Document ownership of the [mailing list][mailing-list] and IRC channel. * Document ownership and management of the security mailing list. From 52bfe20decf2e637cbb4ce848601556999c72bdb Mon Sep 17 00:00:00 2001 From: Peter Thomassen Date: Fri, 26 Apr 2024 15:13:37 +0200 Subject: [PATCH 23/64] Adapt docs to reflect stability-focused contribution policy --- docs/community/contributing.md | 15 ++++++--------- docs/community/release-notes.md | 8 +++++--- docs/index.md | 2 +- docs_theme/css/default.css | 14 ++++++++++++++ mkdocs.yml | 1 + 5 files changed, 27 insertions(+), 13 deletions(-) diff --git a/docs/community/contributing.md b/docs/community/contributing.md index 994226b97..5dea6426d 100644 --- a/docs/community/contributing.md +++ b/docs/community/contributing.md @@ -6,11 +6,9 @@ There are many ways you can contribute to Django REST framework. We'd like it to be a community-led project, so please get involved and help shape the future of the project. ---- +!!! note -**Note**: At this point in it's lifespan we consider Django REST framework to be essentially feature-complete. We may accept pull requests that track the continued development of Django versions, but would prefer not to accept new features or code formatting changes. - ---- + At this point in its lifespan we consider Django REST framework to be feature-complete. We focus on pull requests that track the continued development of Django versions, and generally do not accept new features or code formatting changes. ## Community @@ -36,10 +34,9 @@ Our contribution process is that the [GitHub discussions page](https://github.co Some tips on good potential issue reporting: -* When describing issues try to phrase your ticket in terms of the *behavior* you think needs changing rather than the *code* you think need changing. +* Django REST framework is considered feature-complete. Please do not file requests to change behavior, unless it is required for security reasons or to maintain compatibility with upcoming Django or Python versions. * Search the GitHub project page for related items, and make sure you're running the latest version of REST framework before reporting an issue. -* Feature requests will often be closed with a recommendation that they be implemented outside of the core REST framework library. Keeping new feature requests implemented as third party libraries allows us to keep down the maintenance overhead of REST framework, so that the focus can be on continued stability, bugfixes, and great documentation. At this point in it's lifespan we consider Django REST framework to be essentially feature-complete. -* Closing an issue doesn't necessarily mean the end of a discussion. If you believe your issue has been closed incorrectly, explain why and we'll consider if it needs to be reopened. +* Feature requests will typically be closed with a recommendation that they be implemented outside the core REST framework library (e.g. as third-party libraries). This approach allows us to keep down the maintenance overhead of REST framework, so that the focus can be on continued stability and great documentation. ## Triaging issues @@ -48,8 +45,8 @@ Getting involved in triaging incoming issues is a good way to start contributing * Read through the ticket - does it make sense, is it missing any context that would help explain it better? * Is the ticket reported in the correct place, would it be better suited as a discussion on the discussion group? * If the ticket is a bug report, can you reproduce it? Are you able to write a failing test case that demonstrates the issue and that can be submitted as a pull request? -* If the ticket is a feature request, do you agree with it, and could the feature request instead be implemented as a third party package? -* If a ticket hasn't had much activity and it addresses something you need, then comment on the ticket and try to find out what's needed to get it moving again. +* If the ticket is a feature request, could the feature request instead be implemented as a third party package? +* If a ticket hasn't had much activity and addresses something you need, then comment on the ticket and try to find out what's needed to get it moving again. # Development diff --git a/docs/community/release-notes.md b/docs/community/release-notes.md index 6da3efb9f..f983424da 100644 --- a/docs/community/release-notes.md +++ b/docs/community/release-notes.md @@ -2,11 +2,13 @@ ## Versioning -Minor version numbers (0.0.x) are used for changes that are API compatible. You should be able to upgrade between minor point releases without any other code changes. +- **Minor** version numbers (0.0.x) are used for changes that are API compatible. You should be able to upgrade between minor point releases without any other code changes. -Medium version numbers (0.x.0) may include API changes, in line with the [deprecation policy][deprecation-policy]. You should read the release notes carefully before upgrading between medium point releases. +- **Medium** version numbers (0.x.0) may include API changes, in line with the [deprecation policy][deprecation-policy]. You should read the release notes carefully before upgrading between medium point releases. -Major version numbers (x.0.0) are reserved for substantial project milestones. +- **Major** version numbers (x.0.0) are reserved for substantial project milestones. + +As REST Framework is considered feature-complete, most releases are expected to be minor releases. ## Deprecation policy diff --git a/docs/index.md b/docs/index.md index 4c5f20c48..adc66226e 100644 --- a/docs/index.md +++ b/docs/index.md @@ -184,7 +184,7 @@ Can't wait to get started? The [quickstart guide][quickstart] is the fastest way ## Development See the [Contribution guidelines][contributing] for information on how to clone -the repository, run the test suite and contribute changes back to REST +the repository, run the test suite and help maintain the code base of REST Framework. ## Support diff --git a/docs_theme/css/default.css b/docs_theme/css/default.css index 7006f2a66..dfde26293 100644 --- a/docs_theme/css/default.css +++ b/docs_theme/css/default.css @@ -439,3 +439,17 @@ ul.sponsor { display: inline-block !important; } +/* admonition */ +.admonition { + border: .075rem solid #448aff; + border-radius: .2rem; + margin: 1.5625em 0; + padding: 0 .6rem; +} +.admonition-title { + background: #448aff1a; + font-weight: 700; + margin: 0 -.6rem 1em; + padding: 0.4rem 0.6rem; +} + diff --git a/mkdocs.yml b/mkdocs.yml index 79831fe95..a031dd69b 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -9,6 +9,7 @@ theme: custom_dir: docs_theme markdown_extensions: + - admonition - toc: anchorlink: True From 861b7ac42b2c0f3a82f9c3fc0fd49e2393e52ca3 Mon Sep 17 00:00:00 2001 From: Peter Thomassen Date: Fri, 26 Apr 2024 15:27:55 +0200 Subject: [PATCH 24/64] Adapt issue/PR template to better reflect contribution policy --- .github/ISSUE_TEMPLATE/1-issue.md | 9 ++++++++- PULL_REQUEST_TEMPLATE.md | 2 +- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/1-issue.md b/.github/ISSUE_TEMPLATE/1-issue.md index 0da154953..87fa57a89 100644 --- a/.github/ISSUE_TEMPLATE/1-issue.md +++ b/.github/ISSUE_TEMPLATE/1-issue.md @@ -5,6 +5,13 @@ about: Please only raise an issue if you've been advised to do so after discussi ## Checklist + + - [ ] Raised initially as discussion #... -- [ ] This cannot be dealt with as a third party library. (We prefer new functionality to be [in the form of third party libraries](https://www.django-rest-framework.org/community/third-party-packages/#about-third-party-packages) where possible.) +- [ ] This is not a feature request suitable for implementation outside this project. Please elaborate what it is: + - [ ] compatibility fix for new Django/Python version ... + - [ ] other type of bug fix + - [ ] other type of improvement that does not touch existing code or change existing behavior (e.g. wrapper for new Django field) - [ ] I have reduced the issue to the simplest possible case. diff --git a/PULL_REQUEST_TEMPLATE.md b/PULL_REQUEST_TEMPLATE.md index e9230d5c9..1c6881858 100644 --- a/PULL_REQUEST_TEMPLATE.md +++ b/PULL_REQUEST_TEMPLATE.md @@ -1,4 +1,4 @@ -*Note*: Before submitting this pull request, please review our [contributing guidelines](https://www.django-rest-framework.org/community/contributing/#pull-requests). +*Note*: Before submitting a code change, please review our [contributing guidelines](https://www.django-rest-framework.org/community/contributing/#pull-requests). ## Description From 7900778fbeec79a1994d577ed33e13b0ef2e51f3 Mon Sep 17 00:00:00 2001 From: Peter Thomassen Date: Fri, 26 Apr 2024 20:04:12 +0200 Subject: [PATCH 25/64] Remove obsolete sentence from docs --- docs/api-guide/serializers.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/docs/api-guide/serializers.md b/docs/api-guide/serializers.md index 0f355c76d..eae79b62f 100644 --- a/docs/api-guide/serializers.md +++ b/docs/api-guide/serializers.md @@ -845,8 +845,6 @@ Here's an example of how you might choose to implement multiple updates: class Meta: list_serializer_class = BookListSerializer -It is possible that a third party package may be included alongside the 3.1 release that provides some automatic support for multiple update operations, similar to the `allow_add_remove` behavior that was present in REST framework 2. - #### Customizing ListSerializer initialization When a serializer with `many=True` is instantiated, we need to determine which arguments and keyword arguments should be passed to the `.__init__()` method for both the child `Serializer` class, and for the parent `ListSerializer` class. From 91bbac1f673390df32a39f59cede25d7a4577ff0 Mon Sep 17 00:00:00 2001 From: Peter Thomassen Date: Fri, 26 Apr 2024 15:48:51 +0200 Subject: [PATCH 26/64] bump mkdocs, no longer need to pin jinja2 Addresses https://github.com/encode/django-rest-framework/security/dependabot/11 --- requirements/requirements-documentation.txt | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/requirements/requirements-documentation.txt b/requirements/requirements-documentation.txt index 25f5121f2..2cf936ef3 100644 --- a/requirements/requirements-documentation.txt +++ b/requirements/requirements-documentation.txt @@ -1,6 +1,5 @@ # MkDocs to build our documentation. -mkdocs==1.2.4 -jinja2>=2.10,<3.1.0 # contextfilter has been renamed +mkdocs==1.6.0 # pylinkvalidator to check for broken links in documentation. pylinkvalidator==0.3 From 1f2daaf53cb1e62080be99ea15986f607a193817 Mon Sep 17 00:00:00 2001 From: Peter Thomassen Date: Fri, 26 Apr 2024 16:49:58 +0200 Subject: [PATCH 27/64] Drop support for Django < 4.2 and Python < 3.8 Discussion: https://github.com/encode/django-rest-framework/discussions/8814#discussioncomment-9237791 --- .github/workflows/main.yml | 11 ----------- README.md | 4 ++-- docs/index.md | 4 ++-- setup.py | 13 +++---------- tox.ini | 12 ++---------- 5 files changed, 9 insertions(+), 35 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index c3c587cbf..6276dddcd 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -14,8 +14,6 @@ jobs: strategy: matrix: python-version: - - '3.6' - - '3.7' - '3.8' - '3.9' - '3.10' @@ -37,18 +35,9 @@ jobs: - name: Install dependencies run: python -m pip install --upgrade codecov tox - - name: Install tox-py - if: ${{ matrix.python-version == '3.6' }} - run: python -m pip install --upgrade tox-py - - name: Run tox targets for ${{ matrix.python-version }} - if: ${{ matrix.python-version != '3.6' }} run: tox run -f py$(echo ${{ matrix.python-version }} | tr -d .) - - name: Run tox targets for ${{ matrix.python-version }} - if: ${{ matrix.python-version == '3.6' }} - run: tox --py current - - name: Run extra tox targets if: ${{ matrix.python-version == '3.9' }} run: | diff --git a/README.md b/README.md index cadfd73a5..d32fbc331 100644 --- a/README.md +++ b/README.md @@ -53,8 +53,8 @@ Some reasons you might want to use REST framework: # Requirements -* Python 3.6+ -* Django 5.0, 4.2, 4.1, 4.0, 3.2, 3.1, 3.0 +* Python 3.8+ +* Django 5.0, 4.2 We **highly recommend** and only officially support the latest patch release of each Python and Django series. diff --git a/docs/index.md b/docs/index.md index adc66226e..98cbf6a40 100644 --- a/docs/index.md +++ b/docs/index.md @@ -86,8 +86,8 @@ continued development by **[signing up for a paid plan][funding]**. REST framework requires the following: -* Python (3.6, 3.7, 3.8, 3.9, 3.10, 3.11, 3.12) -* Django (3.0, 3.1, 3.2, 4.0, 4.1, 4.2, 5.0) +* Django (4.2, 5.0) +* Python (3.8, 3.9, 3.10, 3.11, 3.12) We **highly recommend** and only officially support the latest patch release of each Python and Django series. diff --git a/setup.py b/setup.py index 40898b6c1..d2cfe877e 100755 --- a/setup.py +++ b/setup.py @@ -8,7 +8,7 @@ from io import open from setuptools import find_packages, setup CURRENT_PYTHON = sys.version_info[:2] -REQUIRED_PYTHON = (3, 6) +REQUIRED_PYTHON = (3, 8) # This check and everything above must remain compatible with Python 2.7. if CURRENT_PYTHON < REQUIRED_PYTHON: @@ -83,18 +83,13 @@ setup( author_email='tom@tomchristie.com', # SEE NOTE BELOW (*) packages=find_packages(exclude=['tests*']), include_package_data=True, - install_requires=["django>=3.0", 'backports.zoneinfo;python_version<"3.9"'], - python_requires=">=3.6", + install_requires=["django>=4.2", 'backports.zoneinfo;python_version<"3.9"'], + python_requires=">=3.8", zip_safe=False, classifiers=[ 'Development Status :: 5 - Production/Stable', 'Environment :: Web Environment', 'Framework :: Django', - 'Framework :: Django :: 3.0', - 'Framework :: Django :: 3.1', - 'Framework :: Django :: 3.2', - 'Framework :: Django :: 4.0', - 'Framework :: Django :: 4.1', 'Framework :: Django :: 4.2', 'Framework :: Django :: 5.0', 'Intended Audience :: Developers', @@ -102,8 +97,6 @@ setup( 'Operating System :: OS Independent', 'Programming Language :: Python', 'Programming Language :: Python :: 3', - 'Programming Language :: Python :: 3.6', - 'Programming Language :: Python :: 3.7', 'Programming Language :: Python :: 3.8', 'Programming Language :: Python :: 3.9', 'Programming Language :: Python :: 3.10', diff --git a/tox.ini b/tox.ini index ffcbd6729..45429371d 100644 --- a/tox.ini +++ b/tox.ini @@ -1,10 +1,7 @@ [tox] envlist = - {py36,py37,py38,py39}-django30 - {py36,py37,py38,py39}-django31 - {py36,py37,py38,py39,py310}-django32 - {py38,py39,py310}-{django40,django41,django42,djangomain} - {py311}-{django41,django42,django50,djangomain} + {py38,py39,py310}-{django42,djangomain} + {py311}-{django42,django50,djangomain} {py312}-{django42,djanggo50,djangomain} base dist @@ -17,11 +14,6 @@ setenv = PYTHONDONTWRITEBYTECODE=1 PYTHONWARNINGS=once deps = - django30: Django>=3.0,<3.1 - django31: Django>=3.1,<3.2 - django32: Django>=3.2,<4.0 - django40: Django>=4.0,<4.1 - django41: Django>=4.1,<4.2 django42: Django>=4.2,<5.0 django50: Django>=5.0,<5.1 djangomain: https://github.com/django/django/archive/main.tar.gz From 82d91a85fffea6a3cab2aa2c8bdd86e5581ccd7a Mon Sep 17 00:00:00 2001 From: Peter Thomassen Date: Fri, 26 Apr 2024 17:16:28 +0200 Subject: [PATCH 28/64] Fix tox config --- tox.ini | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/tox.ini b/tox.ini index 45429371d..16cc3f8f4 100644 --- a/tox.ini +++ b/tox.ini @@ -1,8 +1,9 @@ [tox] envlist = - {py38,py39,py310}-{django42,djangomain} + {py38,py39}-{django42} + {py310}-{django42,django50,djangomain} {py311}-{django42,django50,djangomain} - {py312}-{django42,djanggo50,djangomain} + {py312}-{django42,django50,djangomain} base dist docs From d38aab39e49e2c9ba8f06b89b3de1e0ff29c207f Mon Sep 17 00:00:00 2001 From: Peter Thomassen Date: Fri, 26 Apr 2024 16:50:27 +0200 Subject: [PATCH 29/64] Remove unused code --- rest_framework/__init__.py | 6 ------ rest_framework/authtoken/__init__.py | 4 ---- rest_framework/compat.py | 24 --------------------- rest_framework/filters.py | 4 ---- rest_framework/parsers.py | 2 +- rest_framework/renderers.py | 3 ++- rest_framework/request.py | 2 +- rest_framework/test.py | 17 ++------------- rest_framework/utils/mediatypes.py | 2 +- tests/authentication/test_authentication.py | 17 ++++----------- tests/conftest.py | 8 +------ tests/test_model_serializer.py | 10 +++------ tests/test_testing.py | 17 ++++----------- 13 files changed, 19 insertions(+), 97 deletions(-) diff --git a/rest_framework/__init__.py b/rest_framework/__init__.py index fe2eab04b..4bd5bdbda 100644 --- a/rest_framework/__init__.py +++ b/rest_framework/__init__.py @@ -7,8 +7,6 @@ ______ _____ _____ _____ __ \_| \_\____/\____/ \_/ |_| |_| \__,_|_| |_| |_|\___| \_/\_/ \___/|_| |_|\_| """ -import django - __title__ = 'Django REST framework' __version__ = '3.15.1' __author__ = 'Tom Christie' @@ -25,10 +23,6 @@ HTTP_HEADER_ENCODING = 'iso-8859-1' ISO_8601 = 'iso-8601' -if django.VERSION < (3, 2): - default_app_config = 'rest_framework.apps.RestFrameworkConfig' - - class RemovedInDRF315Warning(DeprecationWarning): pass diff --git a/rest_framework/authtoken/__init__.py b/rest_framework/authtoken/__init__.py index 285fe15c6..e69de29bb 100644 --- a/rest_framework/authtoken/__init__.py +++ b/rest_framework/authtoken/__init__.py @@ -1,4 +0,0 @@ -import django - -if django.VERSION < (3, 2): - default_app_config = 'rest_framework.authtoken.apps.AuthTokenConfig' diff --git a/rest_framework/compat.py b/rest_framework/compat.py index afc06b6cb..27c5632be 100644 --- a/rest_framework/compat.py +++ b/rest_framework/compat.py @@ -151,30 +151,6 @@ else: return False -if django.VERSION >= (4, 2): - # Django 4.2+: use the stock parse_header_parameters function - # Note: Django 4.1 also has an implementation of parse_header_parameters - # which is slightly different from the one in 4.2, it needs - # the compatibility shim as well. - from django.utils.http import parse_header_parameters -else: - # Django <= 4.1: create a compatibility shim for parse_header_parameters - from django.http.multipartparser import parse_header - - def parse_header_parameters(line): - # parse_header works with bytes, but parse_header_parameters - # works with strings. Call encode to convert the line to bytes. - main_value_pair, params = parse_header(line.encode()) - return main_value_pair, { - # parse_header will convert *some* values to string. - # parse_header_parameters converts *all* values to string. - # Make sure all values are converted by calling decode on - # any remaining non-string values. - k: v if isinstance(v, str) else v.decode() - for k, v in params.items() - } - - if django.VERSION >= (5, 1): # Django 5.1+: use the stock ip_address_validators function # Note: Before Django 5.1, ip_address_validators returns a tuple containing diff --git a/rest_framework/filters.py b/rest_framework/filters.py index 435c30c88..3f4730da8 100644 --- a/rest_framework/filters.py +++ b/rest_framework/filters.py @@ -114,10 +114,6 @@ class SearchFilter(BaseFilterBackend): if hasattr(field, "path_infos"): # Update opts to follow the relation. opts = field.path_infos[-1].to_opts - # django < 4.1 - elif hasattr(field, 'get_path_info'): - # Update opts to follow the relation. - opts = field.get_path_info()[-1].to_opts # Otherwise, use the field with icontains. lookup = 'icontains' return LOOKUP_SEP.join([field_name, lookup]) diff --git a/rest_framework/parsers.py b/rest_framework/parsers.py index f0fd2b884..0e8e4bcb8 100644 --- a/rest_framework/parsers.py +++ b/rest_framework/parsers.py @@ -15,9 +15,9 @@ from django.http.multipartparser import ChunkIter from django.http.multipartparser import \ MultiPartParser as DjangoMultiPartParser from django.http.multipartparser import MultiPartParserError +from django.utils.http import parse_header_parameters from rest_framework import renderers -from rest_framework.compat import parse_header_parameters from rest_framework.exceptions import ParseError from rest_framework.settings import api_settings from rest_framework.utils import json diff --git a/rest_framework/renderers.py b/rest_framework/renderers.py index db1fdd128..ea73c6657 100644 --- a/rest_framework/renderers.py +++ b/rest_framework/renderers.py @@ -19,12 +19,13 @@ from django.core.paginator import Page from django.template import engines, loader from django.urls import NoReverseMatch from django.utils.html import mark_safe +from django.utils.http import parse_header_parameters from django.utils.safestring import SafeString from rest_framework import VERSION, exceptions, serializers, status from rest_framework.compat import ( INDENT_SEPARATORS, LONG_SEPARATORS, SHORT_SEPARATORS, coreapi, coreschema, - parse_header_parameters, pygments_css, yaml + pygments_css, yaml ) from rest_framework.exceptions import ParseError from rest_framework.request import is_form_media_type, override_method diff --git a/rest_framework/request.py b/rest_framework/request.py index 93109226d..f30578fa2 100644 --- a/rest_framework/request.py +++ b/rest_framework/request.py @@ -16,9 +16,9 @@ from django.conf import settings from django.http import HttpRequest, QueryDict from django.http.request import RawPostDataException from django.utils.datastructures import MultiValueDict +from django.utils.http import parse_header_parameters from rest_framework import exceptions -from rest_framework.compat import parse_header_parameters from rest_framework.settings import api_settings diff --git a/rest_framework/test.py b/rest_framework/test.py index 04409f962..e939adcd7 100644 --- a/rest_framework/test.py +++ b/rest_framework/test.py @@ -3,7 +3,6 @@ import io from importlib import import_module -import django from django.conf import settings from django.core.exceptions import ImproperlyConfigured from django.core.handlers.wsgi import WSGIHandler @@ -394,19 +393,7 @@ class URLPatternsTestCase(testcases.SimpleTestCase): cls._override.enable() - if django.VERSION > (4, 0): - cls.addClassCleanup(cls._override.disable) - cls.addClassCleanup(cleanup_url_patterns, cls) + cls.addClassCleanup(cls._override.disable) + cls.addClassCleanup(cleanup_url_patterns, cls) super().setUpClass() - - if django.VERSION < (4, 0): - @classmethod - def tearDownClass(cls): - super().tearDownClass() - cls._override.disable() - - if hasattr(cls, '_module_urlpatterns'): - cls._module.urlpatterns = cls._module_urlpatterns - else: - del cls._module.urlpatterns diff --git a/rest_framework/utils/mediatypes.py b/rest_framework/utils/mediatypes.py index b9004d496..8641732f0 100644 --- a/rest_framework/utils/mediatypes.py +++ b/rest_framework/utils/mediatypes.py @@ -3,7 +3,7 @@ Handling of media types, as found in HTTP Content-Type and Accept headers. See https://www.w3.org/Protocols/rfc2616/rfc2616-sec3.html#sec3.7 """ -from rest_framework.compat import parse_header_parameters +from django.utils.http import parse_header_parameters def media_type_matches(lhs, rhs): diff --git a/tests/authentication/test_authentication.py b/tests/authentication/test_authentication.py index 22e837ef4..2f05ce7d1 100644 --- a/tests/authentication/test_authentication.py +++ b/tests/authentication/test_authentication.py @@ -1,6 +1,5 @@ import base64 -import django import pytest from django.conf import settings from django.contrib.auth.models import User @@ -235,21 +234,13 @@ class SessionAuthTests(TestCase): Ensure POSTing form over session authentication with CSRF token succeeds. Regression test for #6088 """ - # Remove this shim when dropping support for Django 3.0. - if django.VERSION < (3, 1): - from django.middleware.csrf import _get_new_csrf_token - else: - from django.middleware.csrf import ( - _get_new_csrf_string, _mask_cipher_secret - ) - - def _get_new_csrf_token(): - return _mask_cipher_secret(_get_new_csrf_string()) - self.csrf_client.login(username=self.username, password=self.password) # Set the csrf_token cookie so that CsrfViewMiddleware._get_token() works - token = _get_new_csrf_token() + from django.middleware.csrf import ( + _get_new_csrf_string, _mask_cipher_secret + ) + token = _mask_cipher_secret(_get_new_csrf_string()) self.csrf_client.cookies[settings.CSRF_COOKIE_NAME] = token # Post the token matching the cookie value diff --git a/tests/conftest.py b/tests/conftest.py index b67475d8a..01914ae77 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -13,8 +13,6 @@ def pytest_addoption(parser): def pytest_configure(config): from django.conf import settings - # USE_L10N is deprecated, and will be removed in Django 5.0. - use_l10n = {"USE_L10N": True} if django.VERSION < (4, 0) else {} settings.configure( DEBUG_PROPAGATE_EXCEPTIONS=True, DATABASES={ @@ -64,7 +62,6 @@ def pytest_configure(config): PASSWORD_HASHERS=( 'django.contrib.auth.hashers.MD5PasswordHasher', ), - **use_l10n, ) # guardian is optional @@ -87,10 +84,7 @@ def pytest_configure(config): import rest_framework settings.STATIC_ROOT = os.path.join(os.path.dirname(rest_framework.__file__), 'static-root') backend = 'django.contrib.staticfiles.storage.ManifestStaticFilesStorage' - if django.VERSION < (4, 2): - settings.STATICFILES_STORAGE = backend - else: - settings.STORAGES['staticfiles']['BACKEND'] = backend + settings.STORAGES['staticfiles']['BACKEND'] = backend django.setup() diff --git a/tests/test_model_serializer.py b/tests/test_model_serializer.py index d69f1652d..ae1a2b0fa 100644 --- a/tests/test_model_serializer.py +++ b/tests/test_model_serializer.py @@ -12,7 +12,6 @@ import re import sys import tempfile -import django import pytest from django.core.exceptions import ImproperlyConfigured from django.core.serializers.json import DjangoJSONEncoder @@ -453,14 +452,11 @@ class TestPosgresFieldsMapping(TestCase): model = ArrayFieldModel fields = ['array_field', 'array_field_with_blank'] - validators = "" - if django.VERSION < (4, 1): - validators = ", validators=[]" expected = dedent(""" TestSerializer(): - array_field = ListField(allow_empty=False, child=CharField(label='Array field'%s)) - array_field_with_blank = ListField(child=CharField(label='Array field with blank'%s), required=False) - """ % (validators, validators)) + array_field = ListField(allow_empty=False, child=CharField(label='Array field')) + array_field_with_blank = ListField(child=CharField(label='Array field with blank'), required=False) + """) self.assertEqual(repr(TestSerializer()), expected) @pytest.mark.skipif(hasattr(models, 'JSONField'), reason='has models.JSONField') diff --git a/tests/test_testing.py b/tests/test_testing.py index 196319a29..7c2a09fae 100644 --- a/tests/test_testing.py +++ b/tests/test_testing.py @@ -2,7 +2,6 @@ import itertools from io import BytesIO from unittest.mock import patch -import django from django.contrib.auth.models import User from django.http import HttpResponseRedirect from django.shortcuts import redirect @@ -334,18 +333,10 @@ class TestUrlPatternTestCase(URLPatternsTestCase): super().setUpClass() assert urlpatterns is cls.urlpatterns - if django.VERSION > (4, 0): - cls.addClassCleanup( - check_urlpatterns, - cls - ) - - if django.VERSION < (4, 0): - @classmethod - def tearDownClass(cls): - assert urlpatterns is cls.urlpatterns - super().tearDownClass() - assert urlpatterns is not cls.urlpatterns + cls.addClassCleanup( + check_urlpatterns, + cls + ) def test_urlpatterns(self): assert self.client.get('/').status_code == 200 From d58b8da591120abedc94c1b71576cb9afb2d7868 Mon Sep 17 00:00:00 2001 From: Peter Thomassen Date: Fri, 26 Apr 2024 16:50:40 +0200 Subject: [PATCH 30/64] Update deprecation hints --- rest_framework/__init__.py | 2 +- rest_framework/schemas/openapi.py | 6 +++--- tests/test_fields.py | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/rest_framework/__init__.py b/rest_framework/__init__.py index 4bd5bdbda..bc16b221b 100644 --- a/rest_framework/__init__.py +++ b/rest_framework/__init__.py @@ -23,7 +23,7 @@ HTTP_HEADER_ENCODING = 'iso-8859-1' ISO_8601 = 'iso-8601' -class RemovedInDRF315Warning(DeprecationWarning): +class RemovedInDRF316Warning(DeprecationWarning): pass diff --git a/rest_framework/schemas/openapi.py b/rest_framework/schemas/openapi.py index 38031e646..f35106fe5 100644 --- a/rest_framework/schemas/openapi.py +++ b/rest_framework/schemas/openapi.py @@ -12,7 +12,7 @@ from django.db import models from django.utils.encoding import force_str from rest_framework import ( - RemovedInDRF315Warning, exceptions, renderers, serializers + RemovedInDRF316Warning, exceptions, renderers, serializers ) from rest_framework.compat import inflection, uritemplate from rest_framework.fields import _UnvalidatedField, empty @@ -725,7 +725,7 @@ class AutoSchema(ViewInspector): def _get_reference(self, serializer): warnings.warn( "Method `_get_reference()` has been renamed to `get_reference()`. " - "The old name will be removed in DRF v3.15.", - RemovedInDRF315Warning, stacklevel=2 + "The old name will be removed in DRF v3.16.", + RemovedInDRF316Warning, stacklevel=2 ) return self.get_reference(serializer) diff --git a/tests/test_fields.py b/tests/test_fields.py index 9ac84dd21..430681763 100644 --- a/tests/test_fields.py +++ b/tests/test_fields.py @@ -1633,7 +1633,7 @@ class TestCustomTimezoneForDateTimeField(TestCase): assert rendered_date == rendered_date_in_timezone -@pytest.mark.skipif(pytz is None, reason="As Django 4.0 has deprecated pytz, this test should eventually be able to get removed.") +@pytest.mark.skipif(pytz is None, reason="Django 5.0 has removed pytz; this test should eventually be able to get removed.") class TestPytzNaiveDayLightSavingTimeTimeZoneDateTimeField(FieldValues): """ Invalid values for `DateTimeField` with datetime in DST shift (non-existing or ambiguous) and timezone with DST. From 22377241a89c8233b45441b5adde5b858edef371 Mon Sep 17 00:00:00 2001 From: Peter Thomassen Date: Fri, 26 Apr 2024 16:01:11 +0200 Subject: [PATCH 31/64] bump pygments (security hygiene) Addresses https://github.com/encode/django-rest-framework/security/dependabot/9 --- requirements/requirements-optionals.txt | 2 +- tests/test_description.py | 2 +- tests/test_renderers.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/requirements/requirements-optionals.txt b/requirements/requirements-optionals.txt index e54100f52..bac597c95 100644 --- a/requirements/requirements-optionals.txt +++ b/requirements/requirements-optionals.txt @@ -6,5 +6,5 @@ django-guardian>=2.4.0,<2.5 inflection==0.5.1 markdown>=3.3.7 psycopg2-binary>=2.9.5,<2.10 -pygments>=2.12.0,<2.14.0 +pygments~=2.17.0 pyyaml>=5.3.1,<5.4 diff --git a/tests/test_description.py b/tests/test_description.py index ecc6b9776..93539a838 100644 --- a/tests/test_description.py +++ b/tests/test_description.py @@ -41,7 +41,7 @@ MARKDOWN_DOCSTRING = """

an example docstring

indented

hash style header

-
[{
"alpha": 1,
"beta": "this is a string"
}]
+
[{
"alpha": 1,
"beta": "this is a string"
}]


""" diff --git a/tests/test_renderers.py b/tests/test_renderers.py index 247737576..d04ff300f 100644 --- a/tests/test_renderers.py +++ b/tests/test_renderers.py @@ -910,7 +910,7 @@ class TestDocumentationRenderer(TestCase): 'link': coreapi.Link(url='/data/', action='get', fields=[]), } html = template.render(context) - assert 'testcases list' in html + assert 'testcases list' in html @pytest.mark.skipif(not coreapi, reason='coreapi is not installed') From ab681f2d5e4a9645aa68eabf1ff18e41d0d5f642 Mon Sep 17 00:00:00 2001 From: Peter Thomassen Date: Fri, 26 Apr 2024 17:09:08 +0200 Subject: [PATCH 32/64] Update requirements in docs --- docs/index.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/index.md b/docs/index.md index 98cbf6a40..864c1d072 100644 --- a/docs/index.md +++ b/docs/index.md @@ -95,8 +95,8 @@ each Python and Django series. The following packages are optional: * [PyYAML][pyyaml], [uritemplate][uriteemplate] (5.1+, 3.0.0+) - Schema generation support. -* [Markdown][markdown] (3.0.0+) - Markdown support for the browsable API. -* [Pygments][pygments] (2.4.0+) - Add syntax highlighting to Markdown processing. +* [Markdown][markdown] (3.3.0+) - Markdown support for the browsable API. +* [Pygments][pygments] (2.7.0+) - Add syntax highlighting to Markdown processing. * [django-filter][django-filter] (1.0.1+) - Filtering support. * [django-guardian][django-guardian] (1.1.1+) - Object level permissions support. From b34bde47d7fff403df4143a35c71975d7c2e7763 Mon Sep 17 00:00:00 2001 From: Peter Thomassen Date: Fri, 26 Apr 2024 18:02:19 +0200 Subject: [PATCH 33/64] Fix typo in setup.cfg setting --- setup.cfg | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.cfg b/setup.cfg index e7e288816..459238836 100644 --- a/setup.cfg +++ b/setup.cfg @@ -3,7 +3,7 @@ license_files = LICENSE.md [tool:pytest] addopts=--tb=short --strict-markers -ra -testspath = tests +testpaths = tests filterwarnings = ignore:CoreAPI compatibility is deprecated*:rest_framework.RemovedInDRF317Warning [flake8] From 9d4ed054bf8acfac6209b7e7f837fc97517affcc Mon Sep 17 00:00:00 2001 From: Peter Thomassen Date: Fri, 26 Apr 2024 18:05:13 +0200 Subject: [PATCH 34/64] Don't use Windows line endings --- docs/community/funding.md | 796 +++++++++++++++++----------------- docs/topics/html-and-forms.md | 440 +++++++++---------- tests/test_description.py | 310 ++++++------- 3 files changed, 773 insertions(+), 773 deletions(-) diff --git a/docs/community/funding.md b/docs/community/funding.md index 951833682..10e09bf71 100644 --- a/docs/community/funding.md +++ b/docs/community/funding.md @@ -1,398 +1,398 @@ - - - - -# Funding - -If you use REST framework commercially we strongly encourage you to invest in its continued development by signing up for a paid plan. - -**We believe that collaboratively funded software can offer outstanding returns on investment, by encouraging our users to collectively share the cost of development.** - -Signing up for a paid plan will: - -* Directly contribute to faster releases, more features, and higher quality software. -* Allow more time to be invested in documentation, issue triage, and community support. -* Safeguard the future development of REST framework. - -REST framework continues to be open-source and permissively licensed, but we firmly believe it is in the commercial best-interest for users of the project to invest in its ongoing development. - ---- - -## What funding has enabled so far - -* The [3.4](https://www.django-rest-framework.org/community/3.4-announcement/) and [3.5](https://www.django-rest-framework.org/community/3.5-announcement/) releases, including schema generation for both Swagger and RAML, a Python client library, a Command Line client, and addressing of a large number of outstanding issues. -* The [3.6](https://www.django-rest-framework.org/community/3.6-announcement/) release, including JavaScript client library, and API documentation, complete with auto-generated code samples. -* The [3.7 release](https://www.django-rest-framework.org/community/3.7-announcement/), made possible due to our collaborative funding model, focuses on improvements to schema generation and the interactive API documentation. -* The recent [3.8 release](https://www.django-rest-framework.org/community/3.8-announcement/). -* Tom Christie, the creator of Django REST framework, working on the project full-time. -* Around 80-90 issues and pull requests closed per month since Tom Christie started working on the project full-time. -* A community & operations manager position part-time for 4 months, helping mature the business and grow sponsorship. -* Contracting development time for the work on the JavaScript client library and API documentation tooling. - ---- - -## What future funding will enable - -* Realtime API support, using WebSockets. This will consist of documentation and support for using REST framework together with Django Channels, plus integrating WebSocket support into the client libraries. -* Better authentication defaults, possibly bringing JWT & CORS support into the core package. -* Securing the community & operations manager position long-term. -* Opening up and securing a part-time position to focus on ticket triage and resolution. -* Paying for development time on building API client libraries in a range of programming languages. These would be integrated directly into the upcoming API documentation. - -Sign up for a paid plan today, and help ensure that REST framework becomes a sustainable, full-time funded project. - ---- - -## What our sponsors and users say - -> As a developer, Django REST framework feels like an obvious and natural extension to all the great things that make up Django and it's community. Getting started is easy while providing simple abstractions which makes it flexible and customizable. Contributing and supporting Django REST framework helps ensure its future and one way or another it also helps Django, and the Python ecosystem. -> -> — José Padilla, Django REST framework contributor - -  - -> The number one feature of the Python programming language is its community. Such a community is only possible because of the Open Source nature of the language and all the culture that comes from it. Building great Open Source projects require great minds. Given that, we at Vinta are not only proud to sponsor the team behind DRF but we also recognize the ROI that comes from it. -> -> — Filipe Ximenes, Vinta Software - -  - -> It's really awesome that this project continues to endure. The code base is top notch and the maintainers are committed to the highest level of quality. -DRF is one of the core reasons why Django is top choice among web frameworks today. In my opinion, it sets the standard for rest frameworks for the development community at large. -> -> — Andrew Conti, Django REST framework user - ---- - -## Individual plan - -This subscription is recommended for individuals with an interest in seeing REST framework continue to improve. - -If you are using REST framework as a full-time employee, consider recommending that your company takes out a [corporate plan](#corporate-plans). - -
-
-
-
- {{ symbol }} - {{ rates.personal1 }} - /month{% if vat %} +VAT{% endif %} -
-
Individual
-
-
- Support ongoing development -
-
- Credited on the site -
-
- -
-
-
-
- -*Billing is monthly and you can cancel at any time.* - ---- - -## Corporate plans - -These subscriptions are recommended for companies and organizations using REST framework either publicly or privately. - -In exchange for funding you'll also receive advertising space on our site, allowing you to **promote your company or product to many tens of thousands of developers worldwide**. - -Our professional and premium plans also include **priority support**. At any time your engineers can escalate an issue or discussion group thread, and we'll ensure it gets a guaranteed response within the next working day. - -
-
-
-
- {{ symbol }} - {{ rates.corporate1 }} - /month{% if vat %} +VAT{% endif %} -
-
Basic
-
-
- Support ongoing development -
-
- Funding page ad placement -
-
- -
-
-
-
-
- {{ symbol }} - {{ rates.corporate2 }} - /month{% if vat %} +VAT{% endif %} -
-
Professional
-
-
- Support ongoing development -
-
- Sidebar ad placement -
-
- Priority support for your engineers -
-
- -
-
-
-
-
- {{ symbol }} - {{ rates.corporate3 }} - /month{% if vat %} +VAT{% endif %} -
-
Premium
-
-
- Support ongoing development -
-
- Homepage ad placement -
-
- Sidebar ad placement -
-
- Priority support for your engineers -
-
- -
-
-
- -
- -*Billing is monthly and you can cancel at any time.* - -Once you've signed up, we will contact you via email and arrange your ad placements on the site. - -For further enquires please contact funding@django-rest-framework.org. - ---- - -## Accountability - -In an effort to keep the project as transparent as possible, we are releasing [monthly progress reports](https://www.encode.io/reports/march-2018/) and regularly include financial reports and cost breakdowns. - - - - -
-
-
-

Stay up to date, with our monthly progress reports...

-
- - -
-
- - -
- -
-
-
-
- - - ---- - -## Frequently asked questions - -**Q: Can you issue monthly invoices?** -A: Yes, we are happy to issue monthly invoices. Please just email us and let us know who to issue the invoice to (name and address) and which email address to send it to each month. - -**Q: Does sponsorship include VAT?** -A: Sponsorship is VAT exempt. - -**Q: Do I have to sign up for a certain time period?** -A: No, we appreciate your support for any time period that is convenient for you. Also, you can cancel your sponsorship anytime. - -**Q: Can I pay yearly? Can I pay upfront fox X amount of months at a time?** -A: We are currently only set up to accept monthly payments. However, if you'd like to support Django REST framework and you can only do yearly/upfront payments, we are happy to work with you and figure out a convenient solution. - -**Q: Are you only looking for corporate sponsors?** -A: No, we value individual sponsors just as much as corporate sponsors and appreciate any kind of support. - ---- - -## Our sponsors - -
- - + + + + +# Funding + +If you use REST framework commercially we strongly encourage you to invest in its continued development by signing up for a paid plan. + +**We believe that collaboratively funded software can offer outstanding returns on investment, by encouraging our users to collectively share the cost of development.** + +Signing up for a paid plan will: + +* Directly contribute to faster releases, more features, and higher quality software. +* Allow more time to be invested in documentation, issue triage, and community support. +* Safeguard the future development of REST framework. + +REST framework continues to be open-source and permissively licensed, but we firmly believe it is in the commercial best-interest for users of the project to invest in its ongoing development. + +--- + +## What funding has enabled so far + +* The [3.4](https://www.django-rest-framework.org/community/3.4-announcement/) and [3.5](https://www.django-rest-framework.org/community/3.5-announcement/) releases, including schema generation for both Swagger and RAML, a Python client library, a Command Line client, and addressing of a large number of outstanding issues. +* The [3.6](https://www.django-rest-framework.org/community/3.6-announcement/) release, including JavaScript client library, and API documentation, complete with auto-generated code samples. +* The [3.7 release](https://www.django-rest-framework.org/community/3.7-announcement/), made possible due to our collaborative funding model, focuses on improvements to schema generation and the interactive API documentation. +* The recent [3.8 release](https://www.django-rest-framework.org/community/3.8-announcement/). +* Tom Christie, the creator of Django REST framework, working on the project full-time. +* Around 80-90 issues and pull requests closed per month since Tom Christie started working on the project full-time. +* A community & operations manager position part-time for 4 months, helping mature the business and grow sponsorship. +* Contracting development time for the work on the JavaScript client library and API documentation tooling. + +--- + +## What future funding will enable + +* Realtime API support, using WebSockets. This will consist of documentation and support for using REST framework together with Django Channels, plus integrating WebSocket support into the client libraries. +* Better authentication defaults, possibly bringing JWT & CORS support into the core package. +* Securing the community & operations manager position long-term. +* Opening up and securing a part-time position to focus on ticket triage and resolution. +* Paying for development time on building API client libraries in a range of programming languages. These would be integrated directly into the upcoming API documentation. + +Sign up for a paid plan today, and help ensure that REST framework becomes a sustainable, full-time funded project. + +--- + +## What our sponsors and users say + +> As a developer, Django REST framework feels like an obvious and natural extension to all the great things that make up Django and it's community. Getting started is easy while providing simple abstractions which makes it flexible and customizable. Contributing and supporting Django REST framework helps ensure its future and one way or another it also helps Django, and the Python ecosystem. +> +> — José Padilla, Django REST framework contributor + +  + +> The number one feature of the Python programming language is its community. Such a community is only possible because of the Open Source nature of the language and all the culture that comes from it. Building great Open Source projects require great minds. Given that, we at Vinta are not only proud to sponsor the team behind DRF but we also recognize the ROI that comes from it. +> +> — Filipe Ximenes, Vinta Software + +  + +> It's really awesome that this project continues to endure. The code base is top notch and the maintainers are committed to the highest level of quality. +DRF is one of the core reasons why Django is top choice among web frameworks today. In my opinion, it sets the standard for rest frameworks for the development community at large. +> +> — Andrew Conti, Django REST framework user + +--- + +## Individual plan + +This subscription is recommended for individuals with an interest in seeing REST framework continue to improve. + +If you are using REST framework as a full-time employee, consider recommending that your company takes out a [corporate plan](#corporate-plans). + +
+
+
+
+ {{ symbol }} + {{ rates.personal1 }} + /month{% if vat %} +VAT{% endif %} +
+
Individual
+
+
+ Support ongoing development +
+
+ Credited on the site +
+
+ +
+
+
+
+ +*Billing is monthly and you can cancel at any time.* + +--- + +## Corporate plans + +These subscriptions are recommended for companies and organizations using REST framework either publicly or privately. + +In exchange for funding you'll also receive advertising space on our site, allowing you to **promote your company or product to many tens of thousands of developers worldwide**. + +Our professional and premium plans also include **priority support**. At any time your engineers can escalate an issue or discussion group thread, and we'll ensure it gets a guaranteed response within the next working day. + +
+
+
+
+ {{ symbol }} + {{ rates.corporate1 }} + /month{% if vat %} +VAT{% endif %} +
+
Basic
+
+
+ Support ongoing development +
+
+ Funding page ad placement +
+
+ +
+
+
+
+
+ {{ symbol }} + {{ rates.corporate2 }} + /month{% if vat %} +VAT{% endif %} +
+
Professional
+
+
+ Support ongoing development +
+
+ Sidebar ad placement +
+
+ Priority support for your engineers +
+
+ +
+
+
+
+
+ {{ symbol }} + {{ rates.corporate3 }} + /month{% if vat %} +VAT{% endif %} +
+
Premium
+
+
+ Support ongoing development +
+
+ Homepage ad placement +
+
+ Sidebar ad placement +
+
+ Priority support for your engineers +
+
+ +
+
+
+ +
+ +*Billing is monthly and you can cancel at any time.* + +Once you've signed up, we will contact you via email and arrange your ad placements on the site. + +For further enquires please contact funding@django-rest-framework.org. + +--- + +## Accountability + +In an effort to keep the project as transparent as possible, we are releasing [monthly progress reports](https://www.encode.io/reports/march-2018/) and regularly include financial reports and cost breakdowns. + + + + +
+
+
+

Stay up to date, with our monthly progress reports...

+
+ + +
+
+ + +
+ +
+
+
+
+ + + +--- + +## Frequently asked questions + +**Q: Can you issue monthly invoices?** +A: Yes, we are happy to issue monthly invoices. Please just email us and let us know who to issue the invoice to (name and address) and which email address to send it to each month. + +**Q: Does sponsorship include VAT?** +A: Sponsorship is VAT exempt. + +**Q: Do I have to sign up for a certain time period?** +A: No, we appreciate your support for any time period that is convenient for you. Also, you can cancel your sponsorship anytime. + +**Q: Can I pay yearly? Can I pay upfront fox X amount of months at a time?** +A: We are currently only set up to accept monthly payments. However, if you'd like to support Django REST framework and you can only do yearly/upfront payments, we are happy to work with you and figure out a convenient solution. + +**Q: Are you only looking for corporate sponsors?** +A: No, we value individual sponsors just as much as corporate sponsors and appreciate any kind of support. + +--- + +## Our sponsors + +
+ + diff --git a/docs/topics/html-and-forms.md b/docs/topics/html-and-forms.md index 17c9e3314..c7e51c152 100644 --- a/docs/topics/html-and-forms.md +++ b/docs/topics/html-and-forms.md @@ -1,220 +1,220 @@ -# HTML & Forms - -REST framework is suitable for returning both API style responses, and regular HTML pages. Additionally, serializers can be used as HTML forms and rendered in templates. - -## Rendering HTML - -In order to return HTML responses you'll need to use either `TemplateHTMLRenderer`, or `StaticHTMLRenderer`. - -The `TemplateHTMLRenderer` class expects the response to contain a dictionary of context data, and renders an HTML page based on a template that must be specified either in the view or on the response. - -The `StaticHTMLRender` class expects the response to contain a string of the pre-rendered HTML content. - -Because static HTML pages typically have different behavior from API responses you'll probably need to write any HTML views explicitly, rather than relying on the built-in generic views. - -Here's an example of a view that returns a list of "Profile" instances, rendered in an HTML template: - -**views.py**: - - from my_project.example.models import Profile - from rest_framework.renderers import TemplateHTMLRenderer - from rest_framework.response import Response - from rest_framework.views import APIView - - - class ProfileList(APIView): - renderer_classes = [TemplateHTMLRenderer] - template_name = 'profile_list.html' - - def get(self, request): - queryset = Profile.objects.all() - return Response({'profiles': queryset}) - -**profile_list.html**: - - -

Profiles

-
    - {% for profile in profiles %} -
  • {{ profile.name }}
  • - {% endfor %} -
- - -## Rendering Forms - -Serializers may be rendered as forms by using the `render_form` template tag, and including the serializer instance as context to the template. - -The following view demonstrates an example of using a serializer in a template for viewing and updating a model instance: - -**views.py**: - - from django.shortcuts import get_object_or_404 - from my_project.example.models import Profile - from rest_framework.renderers import TemplateHTMLRenderer - from rest_framework.views import APIView - - - class ProfileDetail(APIView): - renderer_classes = [TemplateHTMLRenderer] - template_name = 'profile_detail.html' - - def get(self, request, pk): - profile = get_object_or_404(Profile, pk=pk) - serializer = ProfileSerializer(profile) - return Response({'serializer': serializer, 'profile': profile}) - - def post(self, request, pk): - profile = get_object_or_404(Profile, pk=pk) - serializer = ProfileSerializer(profile, data=request.data) - if not serializer.is_valid(): - return Response({'serializer': serializer, 'profile': profile}) - serializer.save() - return redirect('profile-list') - -**profile_detail.html**: - - {% load rest_framework %} - - - -

Profile - {{ profile.name }}

- -
- {% csrf_token %} - {% render_form serializer %} - -
- - - -### Using template packs - -The `render_form` tag takes an optional `template_pack` argument, that specifies which template directory should be used for rendering the form and form fields. - -REST framework includes three built-in template packs, all based on Bootstrap 3. The built-in styles are `horizontal`, `vertical`, and `inline`. The default style is `horizontal`. To use any of these template packs you'll want to also include the Bootstrap 3 CSS. - -The following HTML will link to a CDN hosted version of the Bootstrap 3 CSS: - - - … - - - -Third party packages may include alternate template packs, by bundling a template directory containing the necessary form and field templates. - -Let's take a look at how to render each of the three available template packs. For these examples we'll use a single serializer class to present a "Login" form. - - class LoginSerializer(serializers.Serializer): - email = serializers.EmailField( - max_length=100, - style={'placeholder': 'Email', 'autofocus': True} - ) - password = serializers.CharField( - max_length=100, - style={'input_type': 'password', 'placeholder': 'Password'} - ) - remember_me = serializers.BooleanField() - ---- - -#### `rest_framework/vertical` - -Presents form labels above their corresponding control inputs, using the standard Bootstrap layout. - -*This is the default template pack.* - - {% load rest_framework %} - - ... - -
- {% csrf_token %} - {% render_form serializer template_pack='rest_framework/vertical' %} - -
- -![Vertical form example](../img/vertical.png) - ---- - -#### `rest_framework/horizontal` - -Presents labels and controls alongside each other, using a 2/10 column split. - -*This is the form style used in the browsable API and admin renderers.* - - {% load rest_framework %} - - ... - -
- {% csrf_token %} - {% render_form serializer %} -
-
- -
-
-
- -![Horizontal form example](../img/horizontal.png) - ---- - -#### `rest_framework/inline` - -A compact form style that presents all the controls inline. - - {% load rest_framework %} - - ... - -
- {% csrf_token %} - {% render_form serializer template_pack='rest_framework/inline' %} - -
- -![Inline form example](../img/inline.png) - -## Field styles - -Serializer fields can have their rendering style customized by using the `style` keyword argument. This argument is a dictionary of options that control the template and layout used. - -The most common way to customize the field style is to use the `base_template` style keyword argument to select which template in the template pack should be use. - -For example, to render a `CharField` as an HTML textarea rather than the default HTML input, you would use something like this: - - details = serializers.CharField( - max_length=1000, - style={'base_template': 'textarea.html'} - ) - -If you instead want a field to be rendered using a custom template that is *not part of an included template pack*, you can instead use the `template` style option, to fully specify a template name: - - details = serializers.CharField( - max_length=1000, - style={'template': 'my-field-templates/custom-input.html'} - ) - -Field templates can also use additional style properties, depending on their type. For example, the `textarea.html` template also accepts a `rows` property that can be used to affect the sizing of the control. - - details = serializers.CharField( - max_length=1000, - style={'base_template': 'textarea.html', 'rows': 10} - ) - -The complete list of `base_template` options and their associated style options is listed below. - -base_template | Valid field types | Additional style options ------------------------|-------------------------------------------------------------|----------------------------------------------- -input.html | Any string, numeric or date/time field | input_type, placeholder, hide_label, autofocus -textarea.html | `CharField` | rows, placeholder, hide_label -select.html | `ChoiceField` or relational field types | hide_label -radio.html | `ChoiceField` or relational field types | inline, hide_label -select_multiple.html | `MultipleChoiceField` or relational fields with `many=True` | hide_label -checkbox_multiple.html | `MultipleChoiceField` or relational fields with `many=True` | inline, hide_label -checkbox.html | `BooleanField` | hide_label -fieldset.html | Nested serializer | hide_label -list_fieldset.html | `ListField` or nested serializer with `many=True` | hide_label +# HTML & Forms + +REST framework is suitable for returning both API style responses, and regular HTML pages. Additionally, serializers can be used as HTML forms and rendered in templates. + +## Rendering HTML + +In order to return HTML responses you'll need to use either `TemplateHTMLRenderer`, or `StaticHTMLRenderer`. + +The `TemplateHTMLRenderer` class expects the response to contain a dictionary of context data, and renders an HTML page based on a template that must be specified either in the view or on the response. + +The `StaticHTMLRender` class expects the response to contain a string of the pre-rendered HTML content. + +Because static HTML pages typically have different behavior from API responses you'll probably need to write any HTML views explicitly, rather than relying on the built-in generic views. + +Here's an example of a view that returns a list of "Profile" instances, rendered in an HTML template: + +**views.py**: + + from my_project.example.models import Profile + from rest_framework.renderers import TemplateHTMLRenderer + from rest_framework.response import Response + from rest_framework.views import APIView + + + class ProfileList(APIView): + renderer_classes = [TemplateHTMLRenderer] + template_name = 'profile_list.html' + + def get(self, request): + queryset = Profile.objects.all() + return Response({'profiles': queryset}) + +**profile_list.html**: + + +

Profiles

+
    + {% for profile in profiles %} +
  • {{ profile.name }}
  • + {% endfor %} +
+ + +## Rendering Forms + +Serializers may be rendered as forms by using the `render_form` template tag, and including the serializer instance as context to the template. + +The following view demonstrates an example of using a serializer in a template for viewing and updating a model instance: + +**views.py**: + + from django.shortcuts import get_object_or_404 + from my_project.example.models import Profile + from rest_framework.renderers import TemplateHTMLRenderer + from rest_framework.views import APIView + + + class ProfileDetail(APIView): + renderer_classes = [TemplateHTMLRenderer] + template_name = 'profile_detail.html' + + def get(self, request, pk): + profile = get_object_or_404(Profile, pk=pk) + serializer = ProfileSerializer(profile) + return Response({'serializer': serializer, 'profile': profile}) + + def post(self, request, pk): + profile = get_object_or_404(Profile, pk=pk) + serializer = ProfileSerializer(profile, data=request.data) + if not serializer.is_valid(): + return Response({'serializer': serializer, 'profile': profile}) + serializer.save() + return redirect('profile-list') + +**profile_detail.html**: + + {% load rest_framework %} + + + +

Profile - {{ profile.name }}

+ +
+ {% csrf_token %} + {% render_form serializer %} + +
+ + + +### Using template packs + +The `render_form` tag takes an optional `template_pack` argument, that specifies which template directory should be used for rendering the form and form fields. + +REST framework includes three built-in template packs, all based on Bootstrap 3. The built-in styles are `horizontal`, `vertical`, and `inline`. The default style is `horizontal`. To use any of these template packs you'll want to also include the Bootstrap 3 CSS. + +The following HTML will link to a CDN hosted version of the Bootstrap 3 CSS: + + + … + + + +Third party packages may include alternate template packs, by bundling a template directory containing the necessary form and field templates. + +Let's take a look at how to render each of the three available template packs. For these examples we'll use a single serializer class to present a "Login" form. + + class LoginSerializer(serializers.Serializer): + email = serializers.EmailField( + max_length=100, + style={'placeholder': 'Email', 'autofocus': True} + ) + password = serializers.CharField( + max_length=100, + style={'input_type': 'password', 'placeholder': 'Password'} + ) + remember_me = serializers.BooleanField() + +--- + +#### `rest_framework/vertical` + +Presents form labels above their corresponding control inputs, using the standard Bootstrap layout. + +*This is the default template pack.* + + {% load rest_framework %} + + ... + +
+ {% csrf_token %} + {% render_form serializer template_pack='rest_framework/vertical' %} + +
+ +![Vertical form example](../img/vertical.png) + +--- + +#### `rest_framework/horizontal` + +Presents labels and controls alongside each other, using a 2/10 column split. + +*This is the form style used in the browsable API and admin renderers.* + + {% load rest_framework %} + + ... + +
+ {% csrf_token %} + {% render_form serializer %} +
+
+ +
+
+
+ +![Horizontal form example](../img/horizontal.png) + +--- + +#### `rest_framework/inline` + +A compact form style that presents all the controls inline. + + {% load rest_framework %} + + ... + +
+ {% csrf_token %} + {% render_form serializer template_pack='rest_framework/inline' %} + +
+ +![Inline form example](../img/inline.png) + +## Field styles + +Serializer fields can have their rendering style customized by using the `style` keyword argument. This argument is a dictionary of options that control the template and layout used. + +The most common way to customize the field style is to use the `base_template` style keyword argument to select which template in the template pack should be use. + +For example, to render a `CharField` as an HTML textarea rather than the default HTML input, you would use something like this: + + details = serializers.CharField( + max_length=1000, + style={'base_template': 'textarea.html'} + ) + +If you instead want a field to be rendered using a custom template that is *not part of an included template pack*, you can instead use the `template` style option, to fully specify a template name: + + details = serializers.CharField( + max_length=1000, + style={'template': 'my-field-templates/custom-input.html'} + ) + +Field templates can also use additional style properties, depending on their type. For example, the `textarea.html` template also accepts a `rows` property that can be used to affect the sizing of the control. + + details = serializers.CharField( + max_length=1000, + style={'base_template': 'textarea.html', 'rows': 10} + ) + +The complete list of `base_template` options and their associated style options is listed below. + +base_template | Valid field types | Additional style options +-----------------------|-------------------------------------------------------------|----------------------------------------------- +input.html | Any string, numeric or date/time field | input_type, placeholder, hide_label, autofocus +textarea.html | `CharField` | rows, placeholder, hide_label +select.html | `ChoiceField` or relational field types | hide_label +radio.html | `ChoiceField` or relational field types | inline, hide_label +select_multiple.html | `MultipleChoiceField` or relational fields with `many=True` | hide_label +checkbox_multiple.html | `MultipleChoiceField` or relational fields with `many=True` | inline, hide_label +checkbox.html | `BooleanField` | hide_label +fieldset.html | Nested serializer | hide_label +list_fieldset.html | `ListField` or nested serializer with `many=True` | hide_label diff --git a/tests/test_description.py b/tests/test_description.py index 93539a838..7fb93ed4e 100644 --- a/tests/test_description.py +++ b/tests/test_description.py @@ -1,155 +1,155 @@ -import pytest -from django.test import TestCase - -from rest_framework.compat import apply_markdown -from rest_framework.utils.formatting import dedent -from rest_framework.views import APIView - -# We check that docstrings get nicely un-indented. -DESCRIPTION = """an example docstring -==================== - -* list -* list - -another header --------------- - - code block - -indented - -# hash style header # - -```json -[{ - "alpha": 1, - "beta": "this is a string" -}] -```""" - - -# If markdown is installed we also test it's working -# (and that our wrapped forces '=' to h2 and '-' to h3) -MARKDOWN_DOCSTRING = """

an example docstring

-
    -
  • list
  • -
  • list
  • -
-

another header

-
code block
-
-

indented

-

hash style header

-
[{
"alpha": 1,
"beta": "this is a string"
}]
-


""" - - -class TestViewNamesAndDescriptions(TestCase): - def test_view_name_uses_class_name(self): - """ - Ensure view names are based on the class name. - """ - class MockView(APIView): - pass - assert MockView().get_view_name() == 'Mock' - - def test_view_name_uses_name_attribute(self): - class MockView(APIView): - name = 'Foo' - assert MockView().get_view_name() == 'Foo' - - def test_view_name_uses_suffix_attribute(self): - class MockView(APIView): - suffix = 'List' - assert MockView().get_view_name() == 'Mock List' - - def test_view_name_preferences_name_over_suffix(self): - class MockView(APIView): - name = 'Foo' - suffix = 'List' - assert MockView().get_view_name() == 'Foo' - - def test_view_description_uses_docstring(self): - """Ensure view descriptions are based on the docstring.""" - class MockView(APIView): - """an example docstring - ==================== - - * list - * list - - another header - -------------- - - code block - - indented - - # hash style header # - - ```json - [{ - "alpha": 1, - "beta": "this is a string" - }] - ```""" - - assert MockView().get_view_description() == DESCRIPTION - - def test_view_description_uses_description_attribute(self): - class MockView(APIView): - description = 'Foo' - assert MockView().get_view_description() == 'Foo' - - def test_view_description_allows_empty_description(self): - class MockView(APIView): - """Description.""" - description = '' - assert MockView().get_view_description() == '' - - def test_view_description_can_be_empty(self): - """ - Ensure that if a view has no docstring, - then it's description is the empty string. - """ - class MockView(APIView): - pass - assert MockView().get_view_description() == '' - - def test_view_description_can_be_promise(self): - """ - Ensure a view may have a docstring that is actually a lazily evaluated - class that can be converted to a string. - - See: https://github.com/encode/django-rest-framework/issues/1708 - """ - # use a mock object instead of gettext_lazy to ensure that we can't end - # up with a test case string in our l10n catalog - - class MockLazyStr: - def __init__(self, string): - self.s = string - - def __str__(self): - return self.s - - class MockView(APIView): - __doc__ = MockLazyStr("a gettext string") - - assert MockView().get_view_description() == 'a gettext string' - - @pytest.mark.skipif(not apply_markdown, reason="Markdown is not installed") - def test_markdown(self): - """ - Ensure markdown to HTML works as expected. - """ - assert apply_markdown(DESCRIPTION) == MARKDOWN_DOCSTRING - - -def test_dedent_tabs(): - result = 'first string\n\nsecond string' - assert dedent(" first string\n\n second string") == result - assert dedent("first string\n\n second string") == result - assert dedent("\tfirst string\n\n\tsecond string") == result - assert dedent("first string\n\n\tsecond string") == result +import pytest +from django.test import TestCase + +from rest_framework.compat import apply_markdown +from rest_framework.utils.formatting import dedent +from rest_framework.views import APIView + +# We check that docstrings get nicely un-indented. +DESCRIPTION = """an example docstring +==================== + +* list +* list + +another header +-------------- + + code block + +indented + +# hash style header # + +```json +[{ + "alpha": 1, + "beta": "this is a string" +}] +```""" + + +# If markdown is installed we also test it's working +# (and that our wrapped forces '=' to h2 and '-' to h3) +MARKDOWN_DOCSTRING = """

an example docstring

+
    +
  • list
  • +
  • list
  • +
+

another header

+
code block
+
+

indented

+

hash style header

+
[{
"alpha": 1,
"beta": "this is a string"
}]
+


""" + + +class TestViewNamesAndDescriptions(TestCase): + def test_view_name_uses_class_name(self): + """ + Ensure view names are based on the class name. + """ + class MockView(APIView): + pass + assert MockView().get_view_name() == 'Mock' + + def test_view_name_uses_name_attribute(self): + class MockView(APIView): + name = 'Foo' + assert MockView().get_view_name() == 'Foo' + + def test_view_name_uses_suffix_attribute(self): + class MockView(APIView): + suffix = 'List' + assert MockView().get_view_name() == 'Mock List' + + def test_view_name_preferences_name_over_suffix(self): + class MockView(APIView): + name = 'Foo' + suffix = 'List' + assert MockView().get_view_name() == 'Foo' + + def test_view_description_uses_docstring(self): + """Ensure view descriptions are based on the docstring.""" + class MockView(APIView): + """an example docstring + ==================== + + * list + * list + + another header + -------------- + + code block + + indented + + # hash style header # + + ```json + [{ + "alpha": 1, + "beta": "this is a string" + }] + ```""" + + assert MockView().get_view_description() == DESCRIPTION + + def test_view_description_uses_description_attribute(self): + class MockView(APIView): + description = 'Foo' + assert MockView().get_view_description() == 'Foo' + + def test_view_description_allows_empty_description(self): + class MockView(APIView): + """Description.""" + description = '' + assert MockView().get_view_description() == '' + + def test_view_description_can_be_empty(self): + """ + Ensure that if a view has no docstring, + then it's description is the empty string. + """ + class MockView(APIView): + pass + assert MockView().get_view_description() == '' + + def test_view_description_can_be_promise(self): + """ + Ensure a view may have a docstring that is actually a lazily evaluated + class that can be converted to a string. + + See: https://github.com/encode/django-rest-framework/issues/1708 + """ + # use a mock object instead of gettext_lazy to ensure that we can't end + # up with a test case string in our l10n catalog + + class MockLazyStr: + def __init__(self, string): + self.s = string + + def __str__(self): + return self.s + + class MockView(APIView): + __doc__ = MockLazyStr("a gettext string") + + assert MockView().get_view_description() == 'a gettext string' + + @pytest.mark.skipif(not apply_markdown, reason="Markdown is not installed") + def test_markdown(self): + """ + Ensure markdown to HTML works as expected. + """ + assert apply_markdown(DESCRIPTION) == MARKDOWN_DOCSTRING + + +def test_dedent_tabs(): + result = 'first string\n\nsecond string' + assert dedent(" first string\n\n second string") == result + assert dedent("first string\n\n second string") == result + assert dedent("\tfirst string\n\n\tsecond string") == result + assert dedent("first string\n\n\tsecond string") == result From 36d5c0e74f562cbe3055f0d20818bd48d3c32359 Mon Sep 17 00:00:00 2001 From: Stanislav Levin Date: Tue, 7 May 2024 10:05:03 +0300 Subject: [PATCH 35/64] tests: Check urlpatterns after cleanups (#9400) According to docs: https://docs.python.org/3/library/unittest.html#unittest.TestCase.addClassCleanup > Add a function to be called after tearDownClass() to cleanup resources used during the test class. Functions will be called in reverse order to the order they are added (LIFO). This was revealed with recent change in pytest (`8.2.0`): > pytest-dev/pytest#11728: For unittest-based tests, exceptions during class cleanup (as raised by functions registered with TestCase.addClassCleanup) are now reported instead of silently failing. `check_urlpatterns` is called before `cleanup_url_patterns` and fails (problem was hidden by `pytest < 8.2.0`). `doClassCleanups` can be used instead to check after-cleanup state: https://docs.python.org/3/library/unittest.html#unittest.TestCase.doClassCleanups > This method is called unconditionally after tearDownClass(), or after setUpClass() if setUpClass() raises an exception. It is responsible for calling all the cleanup functions added by addClassCleanup(). If you need cleanup functions to be called prior to tearDownClass() then you can call doClassCleanups() yourself. Fixes: https://github.com/encode/django-rest-framework/issues/9399 Signed-off-by: Stanislav Levin --- tests/test_testing.py | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/tests/test_testing.py b/tests/test_testing.py index 7c2a09fae..a7e00ab63 100644 --- a/tests/test_testing.py +++ b/tests/test_testing.py @@ -318,10 +318,6 @@ class TestAPIRequestFactory(TestCase): assert request.META['CONTENT_TYPE'] == 'application/json' -def check_urlpatterns(cls): - assert urlpatterns is not cls.urlpatterns - - class TestUrlPatternTestCase(URLPatternsTestCase): urlpatterns = [ path('', view), @@ -333,10 +329,11 @@ class TestUrlPatternTestCase(URLPatternsTestCase): super().setUpClass() assert urlpatterns is cls.urlpatterns - cls.addClassCleanup( - check_urlpatterns, - cls - ) + @classmethod + def doClassCleanups(cls): + assert urlpatterns is cls.urlpatterns + super().doClassCleanups() + assert urlpatterns is not cls.urlpatterns def test_urlpatterns(self): assert self.client.get('/').status_code == 200 From fbdab09c776d5ceef041793a7acd1c9e91695e5d Mon Sep 17 00:00:00 2001 From: wkwkhautbois Date: Sun, 2 Jun 2024 13:14:37 +0900 Subject: [PATCH 36/64] docs: Correct some evaluation results and a httpie option in Tutorial1 (#9421) * Tutorial 1: Added --unsorted option to httpie calls to prevent automatic json key sorting * Tutorial 1: Changed evaluation results accurate --- docs/tutorial/1-serialization.md | 66 ++++++++++++++++++-------------- 1 file changed, 37 insertions(+), 29 deletions(-) diff --git a/docs/tutorial/1-serialization.md b/docs/tutorial/1-serialization.md index c86008104..1dac5e0d8 100644 --- a/docs/tutorial/1-serialization.md +++ b/docs/tutorial/1-serialization.md @@ -150,7 +150,7 @@ At this point we've translated the model instance into Python native datatypes. content = JSONRenderer().render(serializer.data) content - # b'{"id": 2, "title": "", "code": "print(\\"hello, world\\")\\n", "linenos": false, "language": "python", "style": "friendly"}' + # b'{"id":2,"title":"","code":"print(\\"hello, world\\")\\n","linenos":false,"language":"python","style":"friendly"}' Deserialization is similar. First we parse a stream into Python native datatypes... @@ -165,7 +165,7 @@ Deserialization is similar. First we parse a stream into Python native datatype serializer.is_valid() # True serializer.validated_data - # OrderedDict([('title', ''), ('code', 'print("hello, world")\n'), ('linenos', False), ('language', 'python'), ('style', 'friendly')]) + # {'title': '', 'code': 'print("hello, world")', 'linenos': False, 'language': 'python', 'style': 'friendly'} serializer.save() # @@ -175,7 +175,7 @@ We can also serialize querysets instead of model instances. To do so we simply serializer = SnippetSerializer(Snippet.objects.all(), many=True) serializer.data - # [OrderedDict([('id', 1), ('title', ''), ('code', 'foo = "bar"\n'), ('linenos', False), ('language', 'python'), ('style', 'friendly')]), OrderedDict([('id', 2), ('title', ''), ('code', 'print("hello, world")\n'), ('linenos', False), ('language', 'python'), ('style', 'friendly')]), OrderedDict([('id', 3), ('title', ''), ('code', 'print("hello, world")'), ('linenos', False), ('language', 'python'), ('style', 'friendly')])] + # [{'id': 1, 'title': '', 'code': 'foo = "bar"\n', 'linenos': False, 'language': 'python', 'style': 'friendly'}, {'id': 2, 'title': '', 'code': 'print("hello, world")\n', 'linenos': False, 'language': 'python', 'style': 'friendly'}, {'id': 3, 'title': '', 'code': 'print("hello, world")', 'linenos': False, 'language': 'python', 'style': 'friendly'}] ## Using ModelSerializers @@ -321,42 +321,50 @@ You can install httpie using pip: Finally, we can get a list of all of the snippets: - http http://127.0.0.1:8000/snippets/ + http http://127.0.0.1:8000/snippets/ --unsorted HTTP/1.1 200 OK ... [ - { - "id": 1, - "title": "", - "code": "foo = \"bar\"\n", - "linenos": false, - "language": "python", - "style": "friendly" - }, - { + { + "id": 1, + "title": "", + "code": "foo = \"bar\"\n", + "linenos": false, + "language": "python", + "style": "friendly" + }, + { + "id": 2, + "title": "", + "code": "print(\"hello, world\")\n", + "linenos": false, + "language": "python", + "style": "friendly" + }, + { + "id": 3, + "title": "", + "code": "print(\"hello, world\")", + "linenos": false, + "language": "python", + "style": "friendly" + } + ] + +Or we can get a particular snippet by referencing its id: + + http http://127.0.0.1:8000/snippets/2/ --unsorted + + HTTP/1.1 200 OK + ... + { "id": 2, "title": "", "code": "print(\"hello, world\")\n", "linenos": false, "language": "python", "style": "friendly" - } - ] - -Or we can get a particular snippet by referencing its id: - - http http://127.0.0.1:8000/snippets/2/ - - HTTP/1.1 200 OK - ... - { - "id": 2, - "title": "", - "code": "print(\"hello, world\")\n", - "linenos": false, - "language": "python", - "style": "friendly" } Similarly, you can have the same json displayed by visiting these URLs in a web browser. From fe92f0dd0d4c587eed000c7de611ddbff241bd6a Mon Sep 17 00:00:00 2001 From: Ivan Studinsky Date: Mon, 10 Jun 2024 13:19:06 +0700 Subject: [PATCH 37/64] Add `__hash__` method for `permissions.OperandHolder` class (#9417) `OperandHolder` is not hashable, so need to add `__hash__` method --- rest_framework/permissions.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/rest_framework/permissions.py b/rest_framework/permissions.py index 71de226f9..7c15eca58 100644 --- a/rest_framework/permissions.py +++ b/rest_framework/permissions.py @@ -54,6 +54,9 @@ class OperandHolder(OperationHolderMixin): self.op2_class == other.op2_class ) + def __hash__(self): + return hash((self.operator_class, self.op1_class, self.op2_class)) + class AND: def __init__(self, op1, op2): From 3b41f0124194430da957b119712978fa2266b642 Mon Sep 17 00:00:00 2001 From: Seokchan Yoon Date: Fri, 14 Jun 2024 18:52:02 +0900 Subject: [PATCH 38/64] Fix potential XSS vulnerability in break_long_headers template filter (#9435) The header input is now properly escaped before splitting and joining with
tags. This prevents potential XSS attacks if the header contains unsanitized user input. --- rest_framework/templatetags/rest_framework.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rest_framework/templatetags/rest_framework.py b/rest_framework/templatetags/rest_framework.py index e01568cf2..dba8153b1 100644 --- a/rest_framework/templatetags/rest_framework.py +++ b/rest_framework/templatetags/rest_framework.py @@ -322,5 +322,5 @@ def break_long_headers(header): when possible (are comma separated) """ if len(header) > 160 and ',' in header: - header = mark_safe('
' + ',
'.join(header.split(','))) + header = mark_safe('
' + ',
'.join(escape(header).split(','))) return header From c7a7eae551528b6887614df816c8a26df70272d6 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Fri, 14 Jun 2024 16:34:21 +0100 Subject: [PATCH 39/64] Version 3.15.2 (#9439) --- docs/community/release-notes.md | 9 +++++++++ rest_framework/__init__.py | 2 +- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/docs/community/release-notes.md b/docs/community/release-notes.md index f983424da..5ec415a79 100644 --- a/docs/community/release-notes.md +++ b/docs/community/release-notes.md @@ -38,6 +38,15 @@ You can determine your currently installed version using `pip show`: ## 3.15.x series +### 3.15.2 + +**Date**: 14th June 2024 + +* Fix potential XSS vulnerability in browsable API. [#9435](https://github.com/encode/django-rest-framework/pull/9157) +* Revert "Ensure CursorPagination respects nulls in the ordering field". [#9381](https://github.com/encode/django-rest-framework/pull/9381) +* Use warnings rather than logging a warning for DecimalField. [#9367](https://github.com/encode/django-rest-framework/pull/9367) +* Remove unused code. [#9393](https://github.com/encode/django-rest-framework/pull/9393) + ### 3.15.1 Date: 22nd March 2024 diff --git a/rest_framework/__init__.py b/rest_framework/__init__.py index bc16b221b..636f0c8ad 100644 --- a/rest_framework/__init__.py +++ b/rest_framework/__init__.py @@ -8,7 +8,7 @@ ______ _____ _____ _____ __ """ __title__ = 'Django REST framework' -__version__ = '3.15.1' +__version__ = '3.15.2' __author__ = 'Tom Christie' __license__ = 'BSD 3-Clause' __copyright__ = 'Copyright 2011-2023 Encode OSS Ltd' From 1e9b5c15ecc165e6d7658a6db13de98560f2b8df Mon Sep 17 00:00:00 2001 From: Ivan Studinsky Date: Sat, 15 Jun 2024 15:00:28 +0700 Subject: [PATCH 40/64] Provide tests for hashing of `OperandHolder` (#9437) --- tests/test_permissions.py | 56 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 56 insertions(+) diff --git a/tests/test_permissions.py b/tests/test_permissions.py index aefff981e..39b7ed662 100644 --- a/tests/test_permissions.py +++ b/tests/test_permissions.py @@ -716,3 +716,59 @@ class PermissionsCompositionTests(TestCase): composed_perm = (IsAuthenticatedUserOwner | permissions.IsAdminUser) hasperm = composed_perm().has_object_permission(request, None, None) assert hasperm is False + + def test_operand_holder_is_hashable(self): + assert hash((permissions.IsAuthenticated & permissions.IsAdminUser)) + + def test_operand_holder_hash_same_for_same_operands_and_operator(self): + first_operand_holder = ( + permissions.IsAuthenticated & permissions.IsAdminUser + ) + second_operand_holder = ( + permissions.IsAuthenticated & permissions.IsAdminUser + ) + + assert hash(first_operand_holder) == hash(second_operand_holder) + + def test_operand_holder_hash_differs_for_different_operands(self): + first_operand_holder = ( + permissions.IsAuthenticated & permissions.IsAdminUser + ) + second_operand_holder = ( + permissions.AllowAny & permissions.IsAdminUser + ) + third_operand_holder = ( + permissions.IsAuthenticated & permissions.AllowAny + ) + + assert hash(first_operand_holder) != hash(second_operand_holder) + assert hash(first_operand_holder) != hash(third_operand_holder) + assert hash(second_operand_holder) != hash(third_operand_holder) + + def test_operand_holder_hash_differs_for_different_operators(self): + first_operand_holder = ( + permissions.IsAuthenticated & permissions.IsAdminUser + ) + second_operand_holder = ( + permissions.IsAuthenticated | permissions.IsAdminUser + ) + + assert hash(first_operand_holder) != hash(second_operand_holder) + + def test_filtering_permissions(self): + unfiltered_permissions = [ + permissions.IsAuthenticated & permissions.IsAdminUser, + permissions.IsAuthenticated & permissions.IsAdminUser, + permissions.AllowAny, + ] + expected_permissions = [ + permissions.IsAuthenticated & permissions.IsAdminUser, + permissions.AllowAny, + ] + + filtered_permissions = [ + perm for perm + in dict.fromkeys(unfiltered_permissions) + ] + + assert filtered_permissions == expected_permissions From e13688f0c0d32672d31ef3b9474c2a9f9dd12ae9 Mon Sep 17 00:00:00 2001 From: Devid <13779643+sevdog@users.noreply.github.com> Date: Tue, 18 Jun 2024 19:03:37 +0100 Subject: [PATCH 41/64] Remove long deprecated code from request wrapper (#9441) --- rest_framework/request.py | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/rest_framework/request.py b/rest_framework/request.py index f30578fa2..b29e64d16 100644 --- a/rest_framework/request.py +++ b/rest_framework/request.py @@ -422,13 +422,6 @@ class Request: except AttributeError: return self.__getattribute__(attr) - @property - def DATA(self): - raise NotImplementedError( - '`request.DATA` has been deprecated in favor of `request.data` ' - 'since version 3.0, and has been fully removed as of version 3.2.' - ) - @property def POST(self): # Ensure that request.POST uses our request parsing. @@ -447,13 +440,6 @@ class Request: self._load_data_and_files() return self._files - @property - def QUERY_PARAMS(self): - raise NotImplementedError( - '`request.QUERY_PARAMS` has been deprecated in favor of `request.query_params` ' - 'since version 3.0, and has been fully removed as of version 3.2.' - ) - def force_plaintext_errors(self, value): # Hack to allow our exception handler to force choice of # plaintext or html error responses. From 7297f197015dce61b7f00fae80540df0cc7a4fc8 Mon Sep 17 00:00:00 2001 From: Martijn Jacobs Date: Mon, 24 Jun 2024 11:31:49 +0200 Subject: [PATCH 42/64] Add adrf as a third party package for Django REST framework (#9198) --- docs/community/third-party-packages.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/community/third-party-packages.md b/docs/community/third-party-packages.md index a92da82fc..593836411 100644 --- a/docs/community/third-party-packages.md +++ b/docs/community/third-party-packages.md @@ -46,6 +46,10 @@ Check out a grid detailing all the packages and ecosystem around Django REST Fra To submit new content, [open an issue][drf-create-issue] or [create a pull request][drf-create-pr]. +## Async Support + +* [adrf](https://github.com/em1208/adrf) - Async support, provides async Views, ViewSets, and Serializers. + ### Authentication * [djangorestframework-digestauth][djangorestframework-digestauth] - Provides Digest Access Authentication support. From 4d0662663a360bc0656f019cf215c554e466bb75 Mon Sep 17 00:00:00 2001 From: Francesco Cataldo <78490028+FraCata00@users.noreply.github.com> Date: Thu, 27 Jun 2024 14:19:59 +0200 Subject: [PATCH 43/64] fix(release-notes): fix wronk link PR(#9435) on release-notes 3.15.2 (#9444) Co-authored-by: Francesco --- docs/community/release-notes.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/community/release-notes.md b/docs/community/release-notes.md index 5ec415a79..c3294f595 100644 --- a/docs/community/release-notes.md +++ b/docs/community/release-notes.md @@ -42,7 +42,7 @@ You can determine your currently installed version using `pip show`: **Date**: 14th June 2024 -* Fix potential XSS vulnerability in browsable API. [#9435](https://github.com/encode/django-rest-framework/pull/9157) +* Fix potential XSS vulnerability in browsable API. [#9435](https://github.com/encode/django-rest-framework/pull/9435) * Revert "Ensure CursorPagination respects nulls in the ordering field". [#9381](https://github.com/encode/django-rest-framework/pull/9381) * Use warnings rather than logging a warning for DecimalField. [#9367](https://github.com/encode/django-rest-framework/pull/9367) * Remove unused code. [#9393](https://github.com/encode/django-rest-framework/pull/9393) From e9f3fd250aa4176b43753097add1ddcd73ea9d55 Mon Sep 17 00:00:00 2001 From: Alvaro Vega Date: Sat, 29 Jun 2024 09:48:31 +0200 Subject: [PATCH 44/64] Update release-notes.md (#9451) --- docs/community/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/community/release-notes.md b/docs/community/release-notes.md index c3294f595..3e5d3ebc5 100644 --- a/docs/community/release-notes.md +++ b/docs/community/release-notes.md @@ -46,6 +46,7 @@ You can determine your currently installed version using `pip show`: * Revert "Ensure CursorPagination respects nulls in the ordering field". [#9381](https://github.com/encode/django-rest-framework/pull/9381) * Use warnings rather than logging a warning for DecimalField. [#9367](https://github.com/encode/django-rest-framework/pull/9367) * Remove unused code. [#9393](https://github.com/encode/django-rest-framework/pull/9393) +* Django < 4.2 and Python < 3.8 no longer supported. [#9393](https://github.com/encode/django-rest-framework/pull/9393) ### 3.15.1 From ccfe0a963799f52c137a7ed555648e7bbffa0d93 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Fri, 5 Jul 2024 16:56:58 +0100 Subject: [PATCH 45/64] Add Zuplo to sponsors (#9460) --- README.md | 5 ++++- docs/img/premium/zuplo-readme.png | Bin 0 -> 12409 bytes docs/index.md | 3 ++- setup.py | 1 - 4 files changed, 6 insertions(+), 3 deletions(-) create mode 100644 docs/img/premium/zuplo-readme.png diff --git a/README.md b/README.md index d32fbc331..fc44a461e 100644 --- a/README.md +++ b/README.md @@ -28,8 +28,9 @@ The initial aim is to provide a single full-time position on REST framework. [![][cryptapi-img]][cryptapi-url] [![][fezto-img]][fezto-url] [![][svix-img]][svix-url] +[![][zuplo-img]][zuplo-url] -Many thanks to all our [wonderful sponsors][sponsors], and in particular to our premium backers, [Sentry][sentry-url], [Stream][stream-url], [Spacinov][spacinov-url], [Retool][retool-url], [bit.io][bitio-url], [PostHog][posthog-url], [CryptAPI][cryptapi-url], [FEZTO][fezto-url], and [Svix][svix-url]. +Many thanks to all our [wonderful sponsors][sponsors], and in particular to our premium backers, [Sentry][sentry-url], [Stream][stream-url], [Spacinov][spacinov-url], [Retool][retool-url], [bit.io][bitio-url], [PostHog][posthog-url], [CryptAPI][cryptapi-url], [FEZTO][fezto-url], [Svix][svix-url], and [Zuplo][zuplo-url]. --- @@ -196,6 +197,7 @@ Please see the [security policy][security-policy]. [cryptapi-img]: https://raw.githubusercontent.com/encode/django-rest-framework/master/docs/img/premium/cryptapi-readme.png [fezto-img]: https://raw.githubusercontent.com/encode/django-rest-framework/master/docs/img/premium/fezto-readme.png [svix-img]: https://raw.githubusercontent.com/encode/django-rest-framework/master/docs/img/premium/svix-premium.png +[zuplo-img]: https://raw.githubusercontent.com/encode/django-rest-framework/master/docs/img/premium/zuplo-readme.png [sentry-url]: https://getsentry.com/welcome/ [stream-url]: https://getstream.io/?utm_source=DjangoRESTFramework&utm_medium=Webpage_Logo_Ad&utm_content=Developer&utm_campaign=DjangoRESTFramework_Jan2022_HomePage @@ -206,6 +208,7 @@ Please see the [security policy][security-policy]. [cryptapi-url]: https://cryptapi.io [fezto-url]: https://www.fezto.xyz/?utm_source=DjangoRESTFramework [svix-url]: https://www.svix.com/?utm_source=django-REST&utm_medium=sponsorship +[zuplo-url]: https://zuplo.link/django-gh [oauth1-section]: https://www.django-rest-framework.org/api-guide/authentication/#django-rest-framework-oauth [oauth2-section]: https://www.django-rest-framework.org/api-guide/authentication/#django-oauth-toolkit diff --git a/docs/img/premium/zuplo-readme.png b/docs/img/premium/zuplo-readme.png new file mode 100644 index 0000000000000000000000000000000000000000..245ded35e6ca23cc9497992c19a887dba04b6ae4 GIT binary patch literal 12409 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

aZ^{Oj(1+6AB14x6l!eykDz61_S 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 nITL&=^qt86PwD?8OSb}WnRxI9W>>-qmb{mzys literal 0 HcmV?d00001 diff --git a/docs/index.md b/docs/index.md index 864c1d072..719eb6e6e 100644 --- a/docs/index.md +++ b/docs/index.md @@ -75,10 +75,11 @@ continued development by **[signing up for a paid plan][funding]**.

  • CryptAPI
  • FEZTO
  • Svix
  • +
  • Zuplo
  • -*Many thanks to all our [wonderful sponsors][sponsors], and in particular to our premium backers, [Sentry](https://getsentry.com/welcome/), [Stream](https://getstream.io/?utm_source=DjangoRESTFramework&utm_medium=Webpage_Logo_Ad&utm_content=Developer&utm_campaign=DjangoRESTFramework_Jan2022_HomePage), [Spacinov](https://www.spacinov.com/), [Retool](https://retool.com/?utm_source=djangorest&utm_medium=sponsorship), [bit.io](https://bit.io/jobs?utm_source=DRF&utm_medium=sponsor&utm_campaign=DRF_sponsorship), [PostHog](https://posthog.com?utm_source=DRF&utm_medium=sponsor&utm_campaign=DRF_sponsorship), [CryptAPI](https://cryptapi.io), [FEZTO](https://www.fezto.xyz/?utm_source=DjangoRESTFramework), and [Svix](https://www.svix.com/?utm_source=django-REST&utm_medium=sponsorship).* +*Many thanks to all our [wonderful sponsors][sponsors], and in particular to our premium backers, [Sentry](https://getsentry.com/welcome/), [Stream](https://getstream.io/?utm_source=DjangoRESTFramework&utm_medium=Webpage_Logo_Ad&utm_content=Developer&utm_campaign=DjangoRESTFramework_Jan2022_HomePage), [Spacinov](https://www.spacinov.com/), [Retool](https://retool.com/?utm_source=djangorest&utm_medium=sponsorship), [bit.io](https://bit.io/jobs?utm_source=DRF&utm_medium=sponsor&utm_campaign=DRF_sponsorship), [PostHog](https://posthog.com?utm_source=DRF&utm_medium=sponsor&utm_campaign=DRF_sponsorship), [CryptAPI](https://cryptapi.io), [FEZTO](https://www.fezto.xyz/?utm_source=DjangoRESTFramework), [Svix](https://www.svix.com/?utm_source=django-REST&utm_medium=sponsorship), , and [Zuplo](https://zuplo.link/django-web).* --- diff --git a/setup.py b/setup.py index d2cfe877e..568909bbc 100755 --- a/setup.py +++ b/setup.py @@ -1,4 +1,3 @@ -#!/usr/bin/env python3 import os import re import shutil From f74185b6ddd51758f5c54a4ac10e354de0c98f1e Mon Sep 17 00:00:00 2001 From: Devid <13779643+sevdog@users.noreply.github.com> Date: Mon, 15 Jul 2024 17:03:40 +0100 Subject: [PATCH 46/64] Fix get_template_context to handle also lists (#9467) --- rest_framework/renderers.py | 4 ++++ tests/test_htmlrenderer.py | 14 ++++++++++++++ 2 files changed, 18 insertions(+) diff --git a/rest_framework/renderers.py b/rest_framework/renderers.py index ea73c6657..b81f9ab46 100644 --- a/rest_framework/renderers.py +++ b/rest_framework/renderers.py @@ -171,6 +171,10 @@ class TemplateHTMLRenderer(BaseRenderer): def get_template_context(self, data, renderer_context): response = renderer_context['response'] + # in case a ValidationError is caught the data parameter may be a list + # see rest_framework.views.exception_handler + if isinstance(data, list): + return {'details': data, 'status_code': response.status_code} if response.exception: data['status_code'] = response.status_code return data diff --git a/tests/test_htmlrenderer.py b/tests/test_htmlrenderer.py index fa0f4efc6..aa0cfb19c 100644 --- a/tests/test_htmlrenderer.py +++ b/tests/test_htmlrenderer.py @@ -8,6 +8,7 @@ from django.urls import path from rest_framework import status from rest_framework.decorators import api_view, renderer_classes +from rest_framework.exceptions import ValidationError from rest_framework.renderers import TemplateHTMLRenderer from rest_framework.response import Response @@ -34,10 +35,17 @@ def not_found(request): raise Http404() +@api_view(('GET',)) +@renderer_classes((TemplateHTMLRenderer,)) +def validation_error(request): + raise ValidationError('error') + + urlpatterns = [ path('', example), path('permission_denied', permission_denied), path('not_found', not_found), + path('validation_error', validation_error), ] @@ -91,6 +99,12 @@ class TemplateHTMLRendererTests(TestCase): self.assertEqual(response.content, b"403 Forbidden") self.assertEqual(response['Content-Type'], 'text/html; charset=utf-8') + def test_validation_error_html_view(self): + response = self.client.get('/validation_error') + self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) + self.assertEqual(response.content, b"400 Bad Request") + self.assertEqual(response['Content-Type'], 'text/html; charset=utf-8') + # 2 tests below are based on order of if statements in corresponding method # of TemplateHTMLRenderer def test_get_template_names_returns_own_template_name(self): From b6ea11028f59677b62afc96b7f0faf017fc62814 Mon Sep 17 00:00:00 2001 From: Andrea Grandi Date: Wed, 17 Jul 2024 18:50:09 +0200 Subject: [PATCH 47/64] Update tutorials-and-resources.md (#9476) --- docs/community/tutorials-and-resources.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/docs/community/tutorials-and-resources.md b/docs/community/tutorials-and-resources.md index f283e0e4c..408df21d7 100644 --- a/docs/community/tutorials-and-resources.md +++ b/docs/community/tutorials-and-resources.md @@ -115,8 +115,10 @@ Want your Django REST Framework talk/tutorial/article to be added to our website [chatbot-using-drf-part1]: https://chatbotslife.com/chatbot-using-django-rest-framework-api-ai-slack-part-1-3-69c7e38b7b1e#.g2aceuncf [new-django-admin-with-drf-and-emberjs]: https://blog.levit.be/new-django-admin-with-emberjs-what-are-the-news/ [drf-schema]: https://drf-schema-adapter.readthedocs.io/en/latest/ -[creating-a-production-ready-api-with-python-and-drf-part1]: https://www.andreagrandi.it/2016/09/28/creating-production-ready-api-python-django-rest-framework-part-1/ -[creating-a-production-ready-api-with-python-and-drf-part2]: https://www.andreagrandi.it/2016/10/01/creating-a-production-ready-api-with-python-and-django-rest-framework-part-2/ +[creating-a-production-ready-api-with-python-and-drf-part1]: https://www.andreagrandi.it/posts/creating-production-ready-api-python-django-rest-framework-part-1/ +[creating-a-production-ready-api-with-python-and-drf-part2]: https://www.andreagrandi.it/posts/creating-a-production-ready-api-with-python-and-django-rest-framework-part-2/ +[creating-a-production-ready-api-with-python-and-drf-part3]: https://www.andreagrandi.it/posts/creating-a-production-ready-api-with-python-and-django-rest-framework-part-3/ +[creating-a-production-ready-api-with-python-and-drf-part4]: https://www.andreagrandi.it/posts/creating-a-production-ready-api-with-python-and-django-rest-framework-part-4/ [django-rest-framework-tutorial-build-a-blog]: https://wsvincent.com/django-rest-framework-tutorial/ [django-rest-framework-react-tutorial-build-a-todo-list]: https://wsvincent.com/django-rest-framework-react-tutorial/ [django-rest-api-so-easy]: https://www.youtube.com/watch?v=cqP758k1BaQ From 8e304e1adbb0f99f91a15ed3abd379104bba3b89 Mon Sep 17 00:00:00 2001 From: James McHugh Date: Wed, 17 Jul 2024 12:51:39 -0400 Subject: [PATCH 48/64] Fixed AttributeError raised by data property being silently ignored (#9455) Signed-off-by: James Riley McHugh Co-authored-by: James Riley McHugh --- rest_framework/request.py | 11 +++++++---- tests/test_request.py | 19 +++++++++++++++++++ 2 files changed, 26 insertions(+), 4 deletions(-) diff --git a/rest_framework/request.py b/rest_framework/request.py index b29e64d16..1527e435b 100644 --- a/rest_framework/request.py +++ b/rest_framework/request.py @@ -217,7 +217,8 @@ class Request: @property def data(self): if not _hasattr(self, '_full_data'): - self._load_data_and_files() + with wrap_attributeerrors(): + self._load_data_and_files() return self._full_data @property @@ -420,13 +421,14 @@ class Request: _request = self.__getattribute__("_request") return getattr(_request, attr) except AttributeError: - return self.__getattribute__(attr) + raise AttributeError(f"'{self.__class__.__name__}' object has no attribute '{attr}'") @property def POST(self): # Ensure that request.POST uses our request parsing. if not _hasattr(self, '_data'): - self._load_data_and_files() + with wrap_attributeerrors(): + self._load_data_and_files() if is_form_media_type(self.content_type): return self._data return QueryDict('', encoding=self._request._encoding) @@ -437,7 +439,8 @@ class Request: # Different from the other two cases, which are not valid property # names on the WSGIRequest class. if not _hasattr(self, '_files'): - self._load_data_and_files() + with wrap_attributeerrors(): + self._load_data_and_files() return self._files def force_plaintext_errors(self, value): diff --git a/tests/test_request.py b/tests/test_request.py index e37aa7dda..4263f2682 100644 --- a/tests/test_request.py +++ b/tests/test_request.py @@ -126,6 +126,25 @@ class TestContentParsing(TestCase): request.parsers = (PlainTextParser(), ) assert request.data == content + def test_calling_data_fails_when_attribute_error_is_raised(self): + """ + Ensure attribute errors raised when parsing are properly re-raised. + """ + expected_message = "Internal error" + + class BrokenParser: + media_type = "application/json" + + def parse(self, *args, **kwargs): + raise AttributeError(expected_message) + + http_request = factory.post('/', data={}, format="json") + request = Request(http_request) + request.parsers = (BrokenParser,) + + with self.assertRaisesMessage(WrappedAttributeError, expected_message): + request.data + class MockView(APIView): authentication_classes = (SessionAuthentication,) From 518eb22e67b443521f4a7e2be41cce603e59480b Mon Sep 17 00:00:00 2001 From: Yuekui Date: Mon, 5 Aug 2024 03:36:50 -0700 Subject: [PATCH 49/64] Fix unique_together validation with source (#9482) --- rest_framework/validators.py | 13 +++++++------ tests/test_validators.py | 22 ++++++++++++++++++++++ 2 files changed, 29 insertions(+), 6 deletions(-) diff --git a/rest_framework/validators.py b/rest_framework/validators.py index 3f09c15cd..71ebc2ca9 100644 --- a/rest_framework/validators.py +++ b/rest_framework/validators.py @@ -159,17 +159,18 @@ class UniqueTogetherValidator: queryset = self.filter_queryset(attrs, queryset, serializer) queryset = self.exclude_current_instance(attrs, queryset, serializer.instance) + checked_names = [ + serializer.fields[field_name].source for field_name in self.fields + ] # Ignore validation if any field is None if serializer.instance is None: - checked_values = [ - value for field, value in attrs.items() if field in self.fields - ] + checked_values = [attrs[field_name] for field_name in checked_names] else: # Ignore validation if all field values are unchanged checked_values = [ - value - for field, value in attrs.items() - if field in self.fields and value != getattr(serializer.instance, field) + attrs[field_name] + for field_name in checked_names + if attrs[field_name] != getattr(serializer.instance, field_name) ] if checked_values and None not in checked_values and qs_exists(queryset): diff --git a/tests/test_validators.py b/tests/test_validators.py index c38dc1134..4bb8658d5 100644 --- a/tests/test_validators.py +++ b/tests/test_validators.py @@ -469,6 +469,28 @@ class TestUniquenessTogetherValidation(TestCase): assert serializer.is_valid() assert not mock.called + @patch("rest_framework.validators.qs_exists") + def test_unique_together_with_source(self, mock_qs_exists): + class UniqueTogetherWithSourceSerializer(serializers.ModelSerializer): + name = serializers.CharField(source="race_name") + pos = serializers.IntegerField(source="position") + + class Meta: + model = UniquenessTogetherModel + fields = ["name", "pos"] + + data = {"name": "Paris Marathon", "pos": 1} + instance = UniquenessTogetherModel.objects.create( + race_name="Paris Marathon", position=1 + ) + serializer = UniqueTogetherWithSourceSerializer(data=data) + assert not serializer.is_valid() + assert mock_qs_exists.called + mock_qs_exists.reset_mock() + serializer = UniqueTogetherWithSourceSerializer(data=data, instance=instance) + assert serializer.is_valid() + assert not mock_qs_exists.called + def test_filter_queryset_do_not_skip_existing_attribute(self): """ filter_queryset should add value from existing instance attribute From f113ab6b68e5ab8c395a93f168fe46c77c05b324 Mon Sep 17 00:00:00 2001 From: Dave Kalu Date: Mon, 5 Aug 2024 17:26:41 +0400 Subject: [PATCH 50/64] Update tutorials-and-resources.md (#9479) Included the remaining parts (part 3 and 4) of Andrea Grandi's tutorial on creating production ready APIs with DRF. --- docs/community/tutorials-and-resources.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/community/tutorials-and-resources.md b/docs/community/tutorials-and-resources.md index 408df21d7..b128160da 100644 --- a/docs/community/tutorials-and-resources.md +++ b/docs/community/tutorials-and-resources.md @@ -39,6 +39,8 @@ There are a wide range of resources available for learning and using Django REST * [Check Credentials Using Django REST Framework][check-credentials-using-django-rest-framework] * [Creating a Production Ready API with Python and Django REST Framework – Part 1][creating-a-production-ready-api-with-python-and-drf-part1] * [Creating a Production Ready API with Python and Django REST Framework – Part 2][creating-a-production-ready-api-with-python-and-drf-part2] +* [Creating a Production Ready API with Python and Django REST Framework – Part 3][creating-a-production-ready-api-with-python-and-drf-part3] +* [Creating a Production Ready API with Python and Django REST Framework – Part 4][creating-a-production-ready-api-with-python-and-drf-part4] * [Django REST Framework Tutorial - Build a Blog API][django-rest-framework-tutorial-build-a-blog] * [Django REST Framework & React Tutorial - Build a Todo List API][django-rest-framework-react-tutorial-build-a-todo-list] * [Tutorial: Django REST with React (Django 2.0)][django-rest-react-valentinog] From 2f28e7086da23efd3309d0767eb1b513e420d9ae Mon Sep 17 00:00:00 2001 From: Pedro Schlickmann Mendes Date: Mon, 26 Aug 2024 06:28:15 -0300 Subject: [PATCH 51/64] Update jobs.md (#9480) --- docs/community/jobs.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/community/jobs.md b/docs/community/jobs.md index aa1c5d4b4..f3ce37d15 100644 --- a/docs/community/jobs.md +++ b/docs/community/jobs.md @@ -7,6 +7,7 @@ Looking for a new Django REST Framework related role? On this site we provide a * [https://www.djangoproject.com/community/jobs/][djangoproject-website] * [https://www.python.org/jobs/][python-org-jobs] +* [https://django.on-remote.com][django-on-remote] * [https://djangogigs.com][django-gigs-com] * [https://djangojobs.net/jobs/][django-jobs-net] * [https://findwork.dev/django-rest-framework-jobs][findwork-dev] @@ -26,6 +27,7 @@ Wonder how else you can help? One of the best ways you can help Django REST Fram [djangoproject-website]: https://www.djangoproject.com/community/jobs/ [python-org-jobs]: https://www.python.org/jobs/ +[django-on-remote]: https://django.on-remote.com/ [django-gigs-com]: https://djangogigs.com [django-jobs-net]: https://djangojobs.net/jobs/ [findwork-dev]: https://findwork.dev/django-rest-framework-jobs From f6ea019bd9de190906973a5f743cb758a605ff81 Mon Sep 17 00:00:00 2001 From: akkuman <1075768094@qq.com> Date: Mon, 26 Aug 2024 17:31:07 +0800 Subject: [PATCH 52/64] Update django.po (#9505) --- rest_framework/locale/zh_CN/LC_MESSAGES/django.po | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/rest_framework/locale/zh_CN/LC_MESSAGES/django.po b/rest_framework/locale/zh_CN/LC_MESSAGES/django.po index 7e131db42..719df05a1 100644 --- a/rest_framework/locale/zh_CN/LC_MESSAGES/django.po +++ b/rest_framework/locale/zh_CN/LC_MESSAGES/django.po @@ -353,12 +353,12 @@ msgstr "列表字段不能为空值。" #: fields.py:1605 #, python-brace-format msgid "Ensure this field has at least {min_length} elements." -msgstr "" +msgstr "请确保这个字段至少包含 {min_length} 个元素。" #: fields.py:1606 #, python-brace-format msgid "Ensure this field has no more than {max_length} elements." -msgstr "" +msgstr "请确保这个字段不能超过 {max_length} 个元素。" #: fields.py:1682 #, python-brace-format @@ -367,7 +367,7 @@ msgstr "期望是包含类目的字典,得到类型为 “{input_type}”。" #: fields.py:1683 msgid "This dictionary may not be empty." -msgstr "" +msgstr "这个字典可能不是空的。" #: fields.py:1755 msgid "Value must be valid JSON." From 5cc1028c2ff3bdfd9c367494d1fa36d1ca5c8805 Mon Sep 17 00:00:00 2001 From: Noam Date: Mon, 26 Aug 2024 18:32:50 +0300 Subject: [PATCH 53/64] Fix "Converter is already registered" deprecation warning. (#9512) --- rest_framework/urlpatterns.py | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/rest_framework/urlpatterns.py b/rest_framework/urlpatterns.py index bed5708eb..47a8194cf 100644 --- a/rest_framework/urlpatterns.py +++ b/rest_framework/urlpatterns.py @@ -1,10 +1,11 @@ from django.urls import URLResolver, include, path, re_path, register_converter +from django.urls.converters import get_converters from django.urls.resolvers import RoutePattern from rest_framework.settings import api_settings -def _get_format_path_converter(suffix_kwarg, allowed): +def _get_format_path_converter(allowed): if allowed: if len(allowed) == 1: allowed_pattern = allowed[0] @@ -23,11 +24,14 @@ def _get_format_path_converter(suffix_kwarg, allowed): def to_url(self, value): return '.' + value + '/' + return FormatSuffixConverter + + +def _generate_converter_name(allowed): converter_name = 'drf_format_suffix' if allowed: converter_name += '_' + '_'.join(allowed) - - return converter_name, FormatSuffixConverter + return converter_name def apply_suffix_patterns(urlpatterns, suffix_pattern, suffix_required, suffix_route=None): @@ -104,8 +108,10 @@ def format_suffix_patterns(urlpatterns, suffix_required=False, allowed=None): else: suffix_pattern = r'\.(?P<%s>[a-z0-9]+)/?$' % suffix_kwarg - converter_name, suffix_converter = _get_format_path_converter(suffix_kwarg, allowed) - register_converter(suffix_converter, converter_name) + converter_name = _generate_converter_name(allowed) + if converter_name not in get_converters(): + suffix_converter = _get_format_path_converter(allowed) + register_converter(suffix_converter, converter_name) suffix_route = '<%s:%s>' % (converter_name, suffix_kwarg) From f593f5752c45e06147231bbfd74c02384791e074 Mon Sep 17 00:00:00 2001 From: rafaelgramoschi <45084757+rafaelgramoschi@users.noreply.github.com> Date: Tue, 27 Aug 2024 09:44:04 +0200 Subject: [PATCH 54/64] Update browsable-api.md (#9509) * Update browsable-api.md Deprecated url(), use re_path(). Show imports. * Update docs/topics/browsable-api.md * Update docs/topics/browsable-api.md --------- Co-authored-by: Asif Saif Uddin --- docs/topics/browsable-api.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/topics/browsable-api.md b/docs/topics/browsable-api.md index 9a95edfc6..fe35be8b3 100644 --- a/docs/topics/browsable-api.md +++ b/docs/topics/browsable-api.md @@ -20,9 +20,11 @@ By default, the API will return the format specified by the headers, which in th To quickly add authentication to the browesable api, add a routes named `"login"` and `"logout"` under the namespace `"rest_framework"`. DRF provides default routes for this which you can add to your urlconf: ```python +from django.urls import include, path + urlpatterns = [ # ... - url(r"^api-auth/", include("rest_framework.urls", namespace="rest_framework")) + path("api-auth/", include("rest_framework.urls", namespace="rest_framework")) ] ``` From 125ad42eb3922cd5ce1f4b332c91baf6d6162fb3 Mon Sep 17 00:00:00 2001 From: Bruno Alla Date: Sat, 7 Sep 2024 12:07:28 +0100 Subject: [PATCH 55/64] Accept integers as min/max values of DecimalField (#9515) * Use Decimal for min/max values of DecimalField in tests * Update docs to mention that min/max values should be Decimal objects * Accept integer values for DecimalField min and max values * Update expected error messages in tests * Update expected warning message in tests --- docs/api-guide/fields.md | 4 ++-- rest_framework/fields.py | 8 ++++---- tests/test_fields.py | 12 ++++++------ 3 files changed, 12 insertions(+), 12 deletions(-) diff --git a/docs/api-guide/fields.md b/docs/api-guide/fields.md index 94b6e7c21..5cbedd964 100644 --- a/docs/api-guide/fields.md +++ b/docs/api-guide/fields.md @@ -291,8 +291,8 @@ Corresponds to `django.db.models.fields.DecimalField`. * `max_digits` The maximum number of digits allowed in the number. It must be either `None` or an integer greater than or equal to `decimal_places`. * `decimal_places` The number of decimal places to store with the number. * `coerce_to_string` Set to `True` if string values should be returned for the representation, or `False` if `Decimal` objects should be returned. Defaults to the same value as the `COERCE_DECIMAL_TO_STRING` settings key, which will be `True` unless overridden. If `Decimal` objects are returned by the serializer, then the final output format will be determined by the renderer. Note that setting `localize` will force the value to `True`. -* `max_value` Validate that the number provided is no greater than this value. -* `min_value` Validate that the number provided is no less than this value. +* `max_value` Validate that the number provided is no greater than this value. Should be an integer or `Decimal` object. +* `min_value` Validate that the number provided is no less than this value. Should be an integer or `Decimal` object. * `localize` Set to `True` to enable localization of input and output based on the current locale. This will also force `coerce_to_string` to `True`. Defaults to `False`. Note that data formatting is enabled if you have set `USE_L10N=True` in your settings file. * `rounding` Sets the rounding mode used when quantizing to the configured precision. Valid values are [`decimal` module rounding modes][python-decimal-rounding-modes]. Defaults to `None`. * `normalize_output` Will normalize the decimal value when serialized. This will strip all trailing zeroes and change the value's precision to the minimum required precision to be able to represent the value without losing data. Defaults to `False`. diff --git a/rest_framework/fields.py b/rest_framework/fields.py index cbc02e2c2..6989edc0a 100644 --- a/rest_framework/fields.py +++ b/rest_framework/fields.py @@ -986,10 +986,10 @@ class DecimalField(Field): self.max_value = max_value self.min_value = min_value - if self.max_value is not None and not isinstance(self.max_value, decimal.Decimal): - warnings.warn("max_value should be a Decimal instance.") - if self.min_value is not None and not isinstance(self.min_value, decimal.Decimal): - warnings.warn("min_value should be a Decimal instance.") + if self.max_value is not None and not isinstance(self.max_value, (int, decimal.Decimal)): + warnings.warn("max_value should be an integer or Decimal instance.") + if self.min_value is not None and not isinstance(self.min_value, (int, decimal.Decimal)): + warnings.warn("min_value should be an integer or Decimal instance.") if self.max_digits is not None and self.decimal_places is not None: self.max_whole_digits = self.max_digits - self.decimal_places diff --git a/tests/test_fields.py b/tests/test_fields.py index 430681763..1403a6a35 100644 --- a/tests/test_fields.py +++ b/tests/test_fields.py @@ -1245,13 +1245,13 @@ class TestMinMaxDecimalField(FieldValues): '20.0': Decimal('20.0'), } invalid_inputs = { - '9.9': ['Ensure this value is greater than or equal to 10.'], - '20.1': ['Ensure this value is less than or equal to 20.'], + '9.9': ['Ensure this value is greater than or equal to 10.0.'], + '20.1': ['Ensure this value is less than or equal to 20.0.'], } outputs = {} field = serializers.DecimalField( max_digits=3, decimal_places=1, - min_value=10, max_value=20 + min_value=10.0, max_value=20.0 ) def test_warning_when_not_decimal_types(self, caplog): @@ -1260,14 +1260,14 @@ class TestMinMaxDecimalField(FieldValues): serializers.DecimalField( max_digits=3, decimal_places=1, - min_value=10, max_value=20 + min_value=10.0, max_value=20.0 ) assert len(w) == 2 assert all(issubclass(i.category, UserWarning) for i in w) - assert 'max_value should be a Decimal instance' in str(w[0].message) - assert 'min_value should be a Decimal instance' in str(w[1].message) + assert 'max_value should be an integer or Decimal instance' in str(w[0].message) + assert 'min_value should be an integer or Decimal instance' in str(w[1].message) class TestAllowEmptyStrDecimalFieldWithValidators(FieldValues): From 2ede857de0bbcc8863fadd6acdb1f8d5570d87dc Mon Sep 17 00:00:00 2001 From: Bruno Alla Date: Sat, 7 Sep 2024 12:21:18 +0100 Subject: [PATCH 56/64] 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 --- README.md | 2 +- docs/api-guide/authentication.md | 7 +++ docs/index.md | 2 +- rest_framework/views.py | 6 +++ rest_framework/viewsets.py | 7 +++ setup.py | 1 + tests/test_middleware.py | 74 +++++++++++++++++++++++++++++++- tests/test_views.py | 12 ++++++ tests/test_viewsets.py | 7 +++ tox.ini | 13 ++---- 10 files changed, 119 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index fc44a461e..6e62fb39a 100644 --- a/README.md +++ b/README.md @@ -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. diff --git a/docs/api-guide/authentication.md b/docs/api-guide/authentication.md index d6e6293fd..8409a83c8 100644 --- a/docs/api-guide/authentication.md +++ b/docs/api-guide/authentication.md @@ -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 \ No newline at end of file diff --git a/docs/index.md b/docs/index.md index 719eb6e6e..0f809ec07 100644 --- a/docs/index.md +++ b/docs/index.md @@ -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 diff --git a/rest_framework/views.py b/rest_framework/views.py index 411c1ee38..327ebe903 100644 --- a/rest_framework/views.py +++ b/rest_framework/views.py @@ -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) diff --git a/rest_framework/viewsets.py b/rest_framework/viewsets.py index 2eba17b4a..a9c90a8d9 100644 --- a/rest_framework/viewsets.py +++ b/rest_framework/viewsets.py @@ -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): diff --git a/setup.py b/setup.py index 568909bbc..08ef6df88 100755 --- a/setup.py +++ b/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', diff --git a/tests/test_middleware.py b/tests/test_middleware.py index 6b2c91db7..11d4bc01e 100644 --- a/tests/test_middleware.py +++ b/tests/test_middleware.py @@ -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 diff --git a/tests/test_views.py b/tests/test_views.py index 2648c9fb3..11f24906e 100644 --- a/tests/test_views.py +++ b/tests/test_views.py @@ -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 diff --git a/tests/test_viewsets.py b/tests/test_viewsets.py index 8e439c86e..68b1207c3 100644 --- a/tests/test_viewsets.py +++ b/tests/test_viewsets.py @@ -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): diff --git a/tox.ini b/tox.ini index 16cc3f8f4..eee1de490 100644 --- a/tox.ini +++ b/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 From 7141bdb2dfcfa5fa23548485b8af6343d7258d13 Mon Sep 17 00:00:00 2001 From: Sina Amini <67522403+Sinaatkd@users.noreply.github.com> Date: Sat, 7 Sep 2024 15:00:13 +0330 Subject: [PATCH 57/64] Spelling mistakes in Farsi language were corrected (#9521) --- .../locale/fa/LC_MESSAGES/django.po | 3 ++- .../locale/fa_IR/LC_MESSAGES/django.mo | Bin 11989 -> 11989 bytes .../locale/fa_IR/LC_MESSAGES/django.po | 3 ++- 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/rest_framework/locale/fa/LC_MESSAGES/django.po b/rest_framework/locale/fa/LC_MESSAGES/django.po index 6a5b99acf..fd7001d32 100644 --- a/rest_framework/locale/fa/LC_MESSAGES/django.po +++ b/rest_framework/locale/fa/LC_MESSAGES/django.po @@ -7,6 +7,7 @@ # Aryan Baghi , 2020 # Omid Zarin , 2019 # Xavier Ordoquy , 2020 +# Sina Amini , 2024 msgid "" msgstr "" "Project-Id-Version: Django REST framework\n" @@ -187,7 +188,7 @@ msgstr "مطمعن شوید طول این مقدار حداقل {min_length} ا #: fields.py:816 msgid "Enter a valid email address." -msgstr "پست الکترونیکی صحبح وارد کنید." +msgstr "پست الکترونیکی صحیح وارد کنید." #: fields.py:827 msgid "This value does not match the required pattern." diff --git a/rest_framework/locale/fa_IR/LC_MESSAGES/django.mo b/rest_framework/locale/fa_IR/LC_MESSAGES/django.mo index 52d3f3bf84e43fd3972255b9ae363c56677e4d00..35775d9f2b5a8bd8177936a4563c023bc3ed6b99 100644 GIT binary patch delta 15 XcmcZ_do^~$V>zbVJ)57(ec%TGMEVFc delta 15 XcmcZ_do^~$V>zZ9D>grs`@jzXMU4ne diff --git a/rest_framework/locale/fa_IR/LC_MESSAGES/django.po b/rest_framework/locale/fa_IR/LC_MESSAGES/django.po index 61361d50e..280725a73 100644 --- a/rest_framework/locale/fa_IR/LC_MESSAGES/django.po +++ b/rest_framework/locale/fa_IR/LC_MESSAGES/django.po @@ -7,6 +7,7 @@ # Aryan Baghi , 2020 # Omid Zarin , 2019 # Xavier Ordoquy , 2020 +# Sina Amini , 2024 msgid "" msgstr "" "Project-Id-Version: Django REST framework\n" @@ -187,7 +188,7 @@ msgstr "مطمعن شوید طول این مقدار حداقل {min_length} ا #: fields.py:816 msgid "Enter a valid email address." -msgstr "پست الکترونیکی صحبح وارد کنید." +msgstr "پست الکترونیکی صحیح وارد کنید." #: fields.py:827 msgid "This value does not match the required pattern." From 61e33761ebf04a5ce795dac38b7ebe75405385b6 Mon Sep 17 00:00:00 2001 From: Bruno Alla Date: Tue, 10 Sep 2024 12:29:31 +0100 Subject: [PATCH 58/64] Docs: move path converter into a separate section (#9524) And remove note regarding only working with Django 2.x or above, as it's been a while the feature is present, and DRF only supports Django 4.2+. I was looking for this but couldn't see it in the docs, having it in a separate section would make it easier to find and link to. Moreover, as it stands, one might think that the feature is limited to SimpleRouter, while it's also available for DefaultRouter. --- docs/api-guide/routers.md | 38 +++++++++++++++++++------------------- 1 file changed, 19 insertions(+), 19 deletions(-) diff --git a/docs/api-guide/routers.md b/docs/api-guide/routers.md index 91ef0b96e..d6bdeb235 100644 --- a/docs/api-guide/routers.md +++ b/docs/api-guide/routers.md @@ -142,6 +142,24 @@ The above example would now generate the following URL pattern: * URL path: `^users/{pk}/change-password/$` * URL name: `'user-change_password'` +### Using Django `path()` with routers + +By default, the URLs created by routers use regular expressions. This behavior can be modified by setting the `use_regex_path` argument to `False` when instantiating the router, in this case [path converters][path-converters-topic-reference] are used. For example: + + router = SimpleRouter(use_regex_path=False) + +The router will match lookup values containing any characters except slashes and period characters. For a more restrictive (or lenient) lookup pattern, set the `lookup_value_regex` attribute on the viewset or `lookup_value_converter` if using path converters. For example, you can limit the lookup to valid UUIDs: + + class MyModelViewSet(mixins.RetrieveModelMixin, viewsets.GenericViewSet): + lookup_field = 'my_model_id' + lookup_value_regex = '[0-9a-f]{32}' + + class MyPathModelViewSet(mixins.RetrieveModelMixin, viewsets.GenericViewSet): + lookup_field = 'my_model_uuid' + lookup_value_converter = 'uuid' + +Note that path converters will be used on all URLs registered in the router, including viewset actions. + # API Guide ## SimpleRouter @@ -160,30 +178,13 @@ This router includes routes for the standard set of `list`, `create`, `retrieve` {prefix}/{lookup}/{url_path}/GET, or as specified by `methods` argument`@action(detail=True)` decorated method{basename}-{url_name} -By default the URLs created by `SimpleRouter` are appended with a trailing slash. +By default, the URLs created by `SimpleRouter` are appended with a trailing slash. This behavior can be modified by setting the `trailing_slash` argument to `False` when instantiating the router. For example: router = SimpleRouter(trailing_slash=False) Trailing slashes are conventional in Django, but are not used by default in some other frameworks such as Rails. Which style you choose to use is largely a matter of preference, although some javascript frameworks may expect a particular routing style. -By default the URLs created by `SimpleRouter` use regular expressions. This behavior can be modified by setting the `use_regex_path` argument to `False` when instantiating the router, in this case [path converters][path-converters-topic-reference] are used. For example: - - router = SimpleRouter(use_regex_path=False) - -**Note**: `use_regex_path=False` only works with Django 2.x or above, since this feature was introduced in 2.0.0. See [release note][simplified-routing-release-note] - - -The router will match lookup values containing any characters except slashes and period characters. For a more restrictive (or lenient) lookup pattern, set the `lookup_value_regex` attribute on the viewset or `lookup_value_converter` if using path converters. For example, you can limit the lookup to valid UUIDs: - - class MyModelViewSet(mixins.RetrieveModelMixin, viewsets.GenericViewSet): - lookup_field = 'my_model_id' - lookup_value_regex = '[0-9a-f]{32}' - - class MyPathModelViewSet(mixins.RetrieveModelMixin, viewsets.GenericViewSet): - lookup_field = 'my_model_uuid' - lookup_value_converter = 'uuid' - ## DefaultRouter This router is similar to `SimpleRouter` as above, but additionally includes a default API root view, that returns a response containing hyperlinks to all the list views. It also generates routes for optional `.json` style format suffixes. @@ -351,5 +352,4 @@ The [`DRF-extensions` package][drf-extensions] provides [routers][drf-extensions [drf-extensions-customizable-endpoint-names]: https://chibisov.github.io/drf-extensions/docs/#controller-endpoint-name [url-namespace-docs]: https://docs.djangoproject.com/en/4.0/topics/http/urls/#url-namespaces [include-api-reference]: https://docs.djangoproject.com/en/4.0/ref/urls/#include -[simplified-routing-release-note]: https://docs.djangoproject.com/en/2.0/releases/2.0/#simplified-url-routing-syntax [path-converters-topic-reference]: https://docs.djangoproject.com/en/2.0/topics/http/urls/#path-converters From a59aa2dfe195dcc22aca18d2a1d6995bbeb38186 Mon Sep 17 00:00:00 2001 From: Bruno Alla Date: Wed, 11 Sep 2024 10:37:33 +0100 Subject: [PATCH 59/64] Add test covering Update view without queryset attribute (#9528) --- tests/test_prefetch_related.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/tests/test_prefetch_related.py b/tests/test_prefetch_related.py index b07087c97..12ecbf2e6 100644 --- a/tests/test_prefetch_related.py +++ b/tests/test_prefetch_related.py @@ -56,3 +56,17 @@ class TestPrefetchRelatedUpdates(TestCase): 'email': 'tom@example.com' } assert response.data == expected + + def test_can_update_without_queryset_on_class_view(self): + class UserUpdateWithoutQuerySet(generics.UpdateAPIView): + serializer_class = UserSerializer + + def get_object(self): + return User.objects.get(pk=self.kwargs['pk']) + + request = factory.patch('/', {'username': 'new'}) + response = UserUpdateWithoutQuerySet.as_view()(request, pk=self.user.pk) + assert response.data['id'] == self.user.id + assert response.data['username'] == 'new' + self.user.refresh_from_db() + assert self.user.username == 'new' From b25028ac8f46984459f9d9e28069b797986ae476 Mon Sep 17 00:00:00 2001 From: Bruno Alla Date: Wed, 11 Sep 2024 10:39:52 +0100 Subject: [PATCH 60/64] Add support for Python 3.13 (#9527) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Add support for Python 3.13 * Fix extracting tox env with -dev Python versions * Fix view description inspection in Python 3.13 Python 3.13 introduced docstrings for None: https://github.com/python/cpython/pull/117813 In Python 3.12, this is an empty string: ``` ➜ python3.12 Python 3.12.6 (main, Sep 10 2024, 19:06:17) [Clang 15.0.0 (clang-1500.3.9.4)] on darwin Type "help", "copyright", "credits" or "license" for more information. >>> d = None >>> d.__doc__ >>> ``` In Python 3.13, it's no longer empty: ``` ➜ python3.13 Python 3.13.0rc2+ (heads/3.13:660baa1, Sep 10 2024, 18:57:50) [Clang 15.0.0 (clang-1500.3.9.4)] on darwin Type "help", "copyright", "credits" or "license" for more information. >>> d = None >>> d.__doc__ 'The type of the None singleton.' >>> ``` Adding a check in the inspector that get the view description out the view function docstring to catch this edge case. --- .github/workflows/main.yml | 3 ++- docs/index.md | 2 +- rest_framework/schemas/inspectors.py | 5 +++-- setup.py | 1 + tox.ini | 1 + 5 files changed, 8 insertions(+), 4 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 6276dddcd..c48b3cf0c 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -19,6 +19,7 @@ jobs: - '3.10' - '3.11' - '3.12' + - '3.13-dev' steps: - uses: actions/checkout@v4 @@ -36,7 +37,7 @@ jobs: run: python -m pip install --upgrade codecov tox - name: Run tox targets for ${{ matrix.python-version }} - run: tox run -f py$(echo ${{ matrix.python-version }} | tr -d .) + run: tox run -f py$(echo ${{ matrix.python-version }} | tr -d . | cut -f 1 -d '-') - name: Run extra tox targets if: ${{ matrix.python-version == '3.9' }} diff --git a/docs/index.md b/docs/index.md index 0f809ec07..24ae81672 100644 --- a/docs/index.md +++ b/docs/index.md @@ -88,7 +88,7 @@ continued development by **[signing up for a paid plan][funding]**. REST framework requires the following: * Django (4.2, 5.0, 5.1) -* Python (3.8, 3.9, 3.10, 3.11, 3.12) +* Python (3.8, 3.9, 3.10, 3.11, 3.12, 3.13) We **highly recommend** and only officially support the latest patch release of each Python and Django series. diff --git a/rest_framework/schemas/inspectors.py b/rest_framework/schemas/inspectors.py index cb880e79d..e027b46a7 100644 --- a/rest_framework/schemas/inspectors.py +++ b/rest_framework/schemas/inspectors.py @@ -79,8 +79,9 @@ class ViewInspector: view = self.view method_name = getattr(view, 'action', method.lower()) - method_docstring = getattr(view, method_name, None).__doc__ - if method_docstring: + method_func = getattr(view, method_name, None) + method_docstring = method_func.__doc__ + if method_func and method_docstring: # An explicit docstring on the method or action. return self._get_description_section(view, method.lower(), formatting.dedent(smart_str(method_docstring))) else: diff --git a/setup.py b/setup.py index 08ef6df88..67904ec61 100755 --- a/setup.py +++ b/setup.py @@ -102,6 +102,7 @@ setup( 'Programming Language :: Python :: 3.10', 'Programming Language :: Python :: 3.11', 'Programming Language :: Python :: 3.12', + 'Programming Language :: Python :: 3.13', 'Programming Language :: Python :: 3 :: Only', 'Topic :: Internet :: WWW/HTTP', ], diff --git a/tox.ini b/tox.ini index eee1de490..f565a1281 100644 --- a/tox.ini +++ b/tox.ini @@ -4,6 +4,7 @@ envlist = {py310}-{django42,django50,django51,djangomain} {py311}-{django42,django50,django51,djangomain} {py312}-{django42,django50,django51,djangomain} + {py313}-{django51,djangomain} base dist docs From 9921c7554f3d8ce435e45bafd27f4b241beb66f1 Mon Sep 17 00:00:00 2001 From: Peter Thomassen <4242683+peterthomassen@users.noreply.github.com> Date: Thu, 19 Sep 2024 14:11:31 +0200 Subject: [PATCH 61/64] Fix Transifex link (#9541) --- docs/topics/internationalization.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/topics/internationalization.md b/docs/topics/internationalization.md index 267ccdb37..2f8f2abf0 100644 --- a/docs/topics/internationalization.md +++ b/docs/topics/internationalization.md @@ -105,7 +105,7 @@ For API clients the most appropriate of these will typically be to use the `Acce [cite]: https://youtu.be/Wa0VfS2q94Y [django-translation]: https://docs.djangoproject.com/en/stable/topics/i18n/translation [custom-exception-handler]: ../api-guide/exceptions.md#custom-exception-handling -[transifex-project]: https://www.transifex.com/projects/p/django-rest-framework/ +[transifex-project]: https://explore.transifex.com/django-rest-framework-1/django-rest-framework/ [django-po-source]: https://raw.githubusercontent.com/encode/django-rest-framework/master/rest_framework/locale/en_US/LC_MESSAGES/django.po [django-language-preference]: https://docs.djangoproject.com/en/stable/topics/i18n/translation/#how-django-discovers-language-preference [django-locale-paths]: https://docs.djangoproject.com/en/stable/ref/settings/#std:setting-LOCALE_PATHS From 81613c0e51b5a755a6a22782452ccd3cc8364f72 Mon Sep 17 00:00:00 2001 From: Gabriel Mitelman Tkacz <55806524+gtkacz@users.noreply.github.com> Date: Fri, 20 Sep 2024 07:34:37 -0300 Subject: [PATCH 62/64] Fixing and adding pt-br translations (#9535) --- .../locale/pt_BR/LC_MESSAGES/django.po | 61 ++++++++++--------- 1 file changed, 31 insertions(+), 30 deletions(-) diff --git a/rest_framework/locale/pt_BR/LC_MESSAGES/django.po b/rest_framework/locale/pt_BR/LC_MESSAGES/django.po index 40651552d..4d47ce3b9 100644 --- a/rest_framework/locale/pt_BR/LC_MESSAGES/django.po +++ b/rest_framework/locale/pt_BR/LC_MESSAGES/django.po @@ -9,6 +9,7 @@ # Filipe Rinaldi , 2015 # Hugo Leonardo Chalhoub Mendonça , 2015 # Jonatas Baldin , 2017 +# Gabriel Mitelman Tkacz , 2024 msgid "" msgstr "" "Project-Id-Version: Django REST framework\n" @@ -106,11 +107,11 @@ msgstr "Ocorreu um erro de servidor." #: exceptions.py:142 msgid "Invalid input." -msgstr "" +msgstr "Entrada inválida" #: exceptions.py:161 msgid "Malformed request." -msgstr "Pedido malformado." +msgstr "Requisição malformada." #: exceptions.py:167 msgid "Incorrect authentication credentials." @@ -149,12 +150,12 @@ msgstr "Pedido foi limitado." #: exceptions.py:224 #, python-brace-format msgid "Expected available in {wait} second." -msgstr "" +msgstr "Disponível em {wait} segundo." #: exceptions.py:225 #, python-brace-format msgid "Expected available in {wait} seconds." -msgstr "" +msgstr "Disponível em {wait} segundos." #: fields.py:316 relations.py:245 relations.py:279 validators.py:90 #: validators.py:183 @@ -167,15 +168,15 @@ msgstr "Este campo não pode ser nulo." #: fields.py:701 msgid "Must be a valid boolean." -msgstr "" +msgstr "Deve ser um valor booleano válido." #: fields.py:766 msgid "Not a valid string." -msgstr "" +msgstr "Não é uma string válida." #: fields.py:767 msgid "This field may not be blank." -msgstr "Este campo não pode ser em branco." +msgstr "Este campo não pode estar em branco." #: fields.py:768 fields.py:1881 #, python-brace-format @@ -205,7 +206,7 @@ msgstr "Entrar um \"slug\" válido que consista de letras, números, sublinhados msgid "" "Enter a valid \"slug\" consisting of Unicode letters, numbers, underscores, " "or hyphens." -msgstr "" +msgstr "Digite um \"slug\" válido que consista de letras Unicode, números, sublinhados ou hífens." #: fields.py:854 msgid "Enter a valid URL." @@ -213,7 +214,7 @@ msgstr "Entrar um URL válido." #: fields.py:867 msgid "Must be a valid UUID." -msgstr "" +msgstr "Deve ser um UUID válido." #: fields.py:903 msgid "Enter a valid IPv4 or IPv6 address." @@ -271,11 +272,11 @@ msgstr "Necessário uma data e hora mas recebeu uma data." #: fields.py:1150 #, python-brace-format msgid "Invalid datetime for the timezone \"{timezone}\"." -msgstr "" +msgstr "Data e hora inválidas para o fuso horário \"{timezone}\"." #: fields.py:1151 msgid "Datetime value out of range." -msgstr "" +msgstr "Valor de data e hora fora do intervalo." #: fields.py:1236 #, python-brace-format @@ -299,7 +300,7 @@ msgstr "Formato inválido para Duração. Use um dos formatos a seguir: {format} #: fields.py:1399 fields.py:1456 #, python-brace-format msgid "\"{input}\" is not a valid choice." -msgstr "\"{input}\" não é um escolha válido." +msgstr "\"{input}\" não é um escolha válida." #: fields.py:1402 #, python-brace-format @@ -309,7 +310,7 @@ msgstr "Mais de {count} itens..." #: fields.py:1457 fields.py:1603 relations.py:485 serializers.py:570 #, python-brace-format msgid "Expected a list of items but got type \"{input_type}\"." -msgstr "Necessário uma lista de itens, mas recebeu tipo \"{input_type}\"." +msgstr "Esperava uma lista de itens, mas recebeu tipo \"{input_type}\"." #: fields.py:1458 msgid "This selection may not be empty." @@ -356,21 +357,21 @@ msgstr "Esta lista não pode estar vazia." #: fields.py:1605 #, python-brace-format msgid "Ensure this field has at least {min_length} elements." -msgstr "" +msgstr "Certifique-se de que este campo tenha pelo menos {min_length} elementos." #: fields.py:1606 #, python-brace-format msgid "Ensure this field has no more than {max_length} elements." -msgstr "" +msgstr "Certifique-se de que este campo não tenha mais que {max_length} elementos." #: fields.py:1682 #, python-brace-format msgid "Expected a dictionary of items but got type \"{input_type}\"." -msgstr "Esperado um dicionário de itens mas recebeu tipo \"{input_type}\"." +msgstr "Esperava um dicionário de itens mas recebeu tipo \"{input_type}\"." #: fields.py:1683 msgid "This dictionary may not be empty." -msgstr "" +msgstr "Este dicionário não pode estar vazio." #: fields.py:1755 msgid "Value must be valid JSON." @@ -382,7 +383,7 @@ msgstr "Buscar" #: filters.py:50 msgid "A search term." -msgstr "" +msgstr "Um termo de busca." #: filters.py:180 templates/rest_framework/filters/ordering.html:3 msgid "Ordering" @@ -390,7 +391,7 @@ msgstr "Ordenando" #: filters.py:181 msgid "Which field to use when ordering the results." -msgstr "" +msgstr "Qual campo usar ao ordenar os resultados." #: filters.py:287 msgid "ascending" @@ -402,11 +403,11 @@ msgstr "descendente" #: pagination.py:174 msgid "A page number within the paginated result set." -msgstr "" +msgstr "Um número de página dentro do conjunto de resultados paginado." #: pagination.py:179 pagination.py:372 pagination.py:590 msgid "Number of results to return per page." -msgstr "" +msgstr "Número de resultados a serem retornados por página." #: pagination.py:189 msgid "Invalid page." @@ -414,11 +415,11 @@ msgstr "Página inválida." #: pagination.py:374 msgid "The initial index from which to return the results." -msgstr "" +msgstr "O índice inicial a partir do qual retornar os resultados." #: pagination.py:581 msgid "The pagination cursor value." -msgstr "" +msgstr "O valor do cursor de paginação." #: pagination.py:583 msgid "Invalid cursor" @@ -432,7 +433,7 @@ msgstr "Pk inválido \"{pk_value}\" - objeto não existe." #: relations.py:247 #, python-brace-format msgid "Incorrect type. Expected pk value, received {data_type}." -msgstr "Tipo incorreto. Esperado valor pk, recebeu {data_type}." +msgstr "Tipo incorreto. Esperava valor pk, recebeu {data_type}." #: relations.py:280 msgid "Invalid hyperlink - No URL match." @@ -462,20 +463,20 @@ msgstr "Valor inválido." #: schemas/utils.py:32 msgid "unique integer value" -msgstr "" +msgstr "valor inteiro único" #: schemas/utils.py:34 msgid "UUID string" -msgstr "" +msgstr "string UUID" #: schemas/utils.py:36 msgid "unique value" -msgstr "" +msgstr "valor único" #: schemas/utils.py:38 #, python-brace-format msgid "A {value_type} identifying this {name}." -msgstr "" +msgstr "Um {value_type} que identifica este {name}." #: serializers.py:337 #, python-brace-format @@ -485,7 +486,7 @@ msgstr "Dado inválido. Necessário um dicionário mas recebeu {datatype}." #: templates/rest_framework/admin.html:116 #: templates/rest_framework/base.html:136 msgid "Extra Actions" -msgstr "" +msgstr "Ações Extras" #: templates/rest_framework/admin.html:130 #: templates/rest_framework/base.html:150 @@ -540,7 +541,7 @@ msgstr "Os campos {field_names} devem criar um set único." #: validators.py:171 #, python-brace-format msgid "Surrogate characters are not allowed: U+{code_point:X}." -msgstr "" +msgstr "Caracteres substitutos não são permitidos: U+{code_point:X}." #: validators.py:243 #, python-brace-format From 10b25e7d631afd7a44bb19772ed14ffbbdb5022f Mon Sep 17 00:00:00 2001 From: Oliver Sauder Date: Thu, 10 Oct 2024 02:39:36 +0400 Subject: [PATCH 63/64] Use final version of Python 3.13 (#9556) Final version of Python 3.13 has been released and is now also available on GitHub actions. --- .github/workflows/main.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index c48b3cf0c..d08655451 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -19,7 +19,7 @@ jobs: - '3.10' - '3.11' - '3.12' - - '3.13-dev' + - '3.13' steps: - uses: actions/checkout@v4 From d3dd45b3f48856c3513ab1eb5354194fa9898f39 Mon Sep 17 00:00:00 2001 From: Gabriel Date: Wed, 9 Oct 2024 19:41:39 -0300 Subject: [PATCH 64/64] Update 1-serialization.md (#9543) Making explicit the http GET method of the httpie calls. For some reason it is sending a POST instead of a GET request as it should be described here: https://httpie.io/docs/cli/optional-get-and-post Note: I was following the docs and testing it within the Git Bash windows console app for making the requests and debugging the DRF projenct in pycharm. --- docs/tutorial/1-serialization.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/tutorial/1-serialization.md b/docs/tutorial/1-serialization.md index 1dac5e0d8..b9bf67acb 100644 --- a/docs/tutorial/1-serialization.md +++ b/docs/tutorial/1-serialization.md @@ -321,7 +321,7 @@ You can install httpie using pip: Finally, we can get a list of all of the snippets: - http http://127.0.0.1:8000/snippets/ --unsorted + http GET http://127.0.0.1:8000/snippets/ --unsorted HTTP/1.1 200 OK ... @@ -354,7 +354,7 @@ Finally, we can get a list of all of the snippets: Or we can get a particular snippet by referencing its id: - http http://127.0.0.1:8000/snippets/2/ --unsorted + http GET http://127.0.0.1:8000/snippets/2/ --unsorted HTTP/1.1 200 OK ...