From 2196cd2bbbc0b1ce36538187170e639b534a7aae Mon Sep 17 00:00:00 2001 From: hurturk Date: Tue, 18 Apr 2017 23:44:18 -0400 Subject: [PATCH 1/9] Generate schema with respect to http_method_names provided by CBV --- rest_framework/schemas.py | 5 +++- tests/test_schemas.py | 60 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 64 insertions(+), 1 deletion(-) diff --git a/rest_framework/schemas.py b/rest_framework/schemas.py index 859a6c9bd..6ead6d885 100644 --- a/rest_framework/schemas.py +++ b/rest_framework/schemas.py @@ -246,7 +246,10 @@ class EndpointInspector(object): Return a list of the valid HTTP methods for this endpoint. """ if hasattr(callback, 'actions'): - return [method.upper() for method in callback.actions.keys()] + return [ + method.upper() for method in + set(callback.actions.keys()).intersection(set(callback.cls().http_method_names)) + ] return [ method for method in diff --git a/tests/test_schemas.py b/tests/test_schemas.py index f75370170..24131480f 100644 --- a/tests/test_schemas.py +++ b/tests/test_schemas.py @@ -246,6 +246,11 @@ class PermissionDeniedExampleViewSet(ExampleViewSet): permission_classes = [DenyAllUsingPermissionDenied] +class MethodLimitedViewSet(ExampleViewSet): + permission_classes = [] + http_method_names = ['get', 'head', 'options'] + + class ExampleListView(APIView): permission_classes = [permissions.IsAuthenticatedOrReadOnly] @@ -368,6 +373,61 @@ class TestSchemaGeneratorNotAtRoot(TestCase): assert schema == expected +@unittest.skipUnless(coreapi, 'coreapi is not installed') +class TestSchemaGeneratorWithMethodLimitedViewSets(TestCase): + def setUp(self): + router = DefaultRouter() + router.register('example1', MethodLimitedViewSet, base_name='example1') + self.patterns = [ + url(r'^', include(router.urls)) + ] + + def test_schema_for_regular_views(self): + """ + Ensure that schema generation works for ViewSet classes + with method limitation by Django CBV's http_method_names attribute + """ + generator = SchemaGenerator(title='Example API', patterns=self.patterns) + request = factory.get('/example1/') + schema = generator.get_schema(Request(request)) + + expected = coreapi.Document( + url='http://testserver/example1/', + title='Example API', + content={ + 'example1': { + 'list': coreapi.Link( + url='/example1/', + action='get', + fields=[ + coreapi.Field('page', required=False, location='query', schema=coreschema.Integer(title='Page', description='A page number within the paginated result set.')), + coreapi.Field('page_size', required=False, location='query', schema=coreschema.Integer(title='Page size', description='Number of results to return per page.')), + coreapi.Field('ordering', required=False, location='query', schema=coreschema.String(title='Ordering', description='Which field to use when ordering the results.')) + ] + ), + 'custom_list_action': coreapi.Link( + url='/example1/custom_list_action/', + action='get' + ), + 'custom_list_action_multiple_methods': { + 'read': coreapi.Link( + url='/example1/custom_list_action_multiple_methods/', + action='get' + ) + }, + 'read': coreapi.Link( + url='/example1/{id}/', + action='get', + fields=[ + coreapi.Field('id', required=True, location='path', schema=coreschema.String()) + ] + ) + } + } + ) + assert schema == expected + + @unittest.skipUnless(coreapi, 'coreapi is not installed') class TestSchemaGeneratorWithRestrictedViewSets(TestCase): def setUp(self): From 6075f8051a726bc8e1b83b2d71212c10b917993e Mon Sep 17 00:00:00 2001 From: hurturk Date: Tue, 2 May 2017 17:34:53 -0400 Subject: [PATCH 2/9] Refactor CBV schema method limitation for set operations --- rest_framework/schemas.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/rest_framework/schemas.py b/rest_framework/schemas.py index 6ead6d885..889ed48a8 100644 --- a/rest_framework/schemas.py +++ b/rest_framework/schemas.py @@ -246,10 +246,9 @@ class EndpointInspector(object): Return a list of the valid HTTP methods for this endpoint. """ if hasattr(callback, 'actions'): - return [ - method.upper() for method in - set(callback.actions.keys()).intersection(set(callback.cls().http_method_names)) - ] + actions = set(callback.actions.keys()) + http_method_names = set(callback.cls().http_method_names) + return [method.upper() for method in actions & http_method_names] return [ method for method in From a1e6d835338f7b483a0dcca265ee52ef6bf57a45 Mon Sep 17 00:00:00 2001 From: Xavier Ordoquy Date: Wed, 3 May 2017 02:19:30 +0200 Subject: [PATCH 3/9] Add missing pk keyword argument to the detail_route. --- docs/api-guide/routers.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/api-guide/routers.md b/docs/api-guide/routers.md index d3f4186ad..948bf8989 100644 --- a/docs/api-guide/routers.md +++ b/docs/api-guide/routers.md @@ -270,7 +270,7 @@ Let's take a look at the routes our `CustomReadOnlyRouter` would generate for a lookup_field = 'username' @detail_route() - def group_names(self, request): + def group_names(self, request, pk=None): """ Returns a list of all the group names that the given user belongs to. From 518bb44a9ef19f9df2d06322f0af2c896997e878 Mon Sep 17 00:00:00 2001 From: hurturk Date: Wed, 3 May 2017 03:49:04 -0400 Subject: [PATCH 4/9] Remove excessive class instance while getting http_method_names --- rest_framework/schemas.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rest_framework/schemas.py b/rest_framework/schemas.py index 889ed48a8..d234123b5 100644 --- a/rest_framework/schemas.py +++ b/rest_framework/schemas.py @@ -247,7 +247,7 @@ class EndpointInspector(object): """ if hasattr(callback, 'actions'): actions = set(callback.actions.keys()) - http_method_names = set(callback.cls().http_method_names) + http_method_names = set(callback.cls.http_method_names) return [method.upper() for method in actions & http_method_names] return [ From 67f382394b153f716b28d17beb2eb4fc76a7c107 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Wed, 3 May 2017 09:25:40 +0100 Subject: [PATCH 5/9] Update third-party-packages.md --- docs/topics/third-party-packages.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/topics/third-party-packages.md b/docs/topics/third-party-packages.md index aa406084c..5d5fa3c68 100644 --- a/docs/topics/third-party-packages.md +++ b/docs/topics/third-party-packages.md @@ -261,7 +261,7 @@ To submit new content, [open an issue][drf-create-issue] or [create a pull reque * [drf-haystack][drf-haystack] - Haystack search for Django Rest Framework * [django-rest-framework-version-transforms][django-rest-framework-version-transforms] - Enables the use of delta transformations for versioning of DRF resource representations. * [django-rest-messaging][django-rest-messaging], [django-rest-messaging-centrifugo][django-rest-messaging-centrifugo] and [django-rest-messaging-js][django-rest-messaging-js] - A real-time pluggable messaging service using DRM. - +* [djangorest-alchemy][djangorest-alchemy] - SQLAlchemy support for REST framework. [cite]: http://www.software-ecosystems.com/Software_Ecosystems/Ecosystems.html [cookiecutter]: https://github.com/jpadilla/cookiecutter-django-rest-framework @@ -332,3 +332,4 @@ To submit new content, [open an issue][drf-create-issue] or [create a pull reque [drf-serializer-extensions]: https://github.com/evenicoulddoit/django-rest-framework-serializer-extensions [djangorestframework-queryfields]: https://github.com/wimglenn/djangorestframework-queryfields [drfpasswordless]: https://github.com/aaronn/django-rest-framework-passwordless +[djangorest-alchemy]: https://github.com/dealertrack/djangorest-alchemy From 7322aa6b68d33b8fc52c2978059be60229930ff7 Mon Sep 17 00:00:00 2001 From: Ryan P Kilby Date: Wed, 3 May 2017 11:03:11 -0400 Subject: [PATCH 6/9] Simplify django-filter docs, add drf integration link --- docs/api-guide/filtering.md | 87 ++---------------------------------- docs/img/django-filter.png | Bin 13678 -> 0 bytes 2 files changed, 4 insertions(+), 83 deletions(-) delete mode 100644 docs/img/django-filter.png diff --git a/docs/api-guide/filtering.md b/docs/api-guide/filtering.md index 461f3c5ba..58bf286f6 100644 --- a/docs/api-guide/filtering.md +++ b/docs/api-guide/filtering.md @@ -160,16 +160,6 @@ Or add the filter backend to an individual View or ViewSet. ... filter_backends = (DjangoFilterBackend,) -If you are using the browsable API or admin API you may also want to install `django-crispy-forms`, which will enhance the presentation of the filter forms in HTML views, by allowing them to render Bootstrap 3 HTML. - - pip install django-crispy-forms - -With crispy forms installed and added to Django's `INSTALLED_APPS`, the browsable API will present a filtering control for `DjangoFilterBackend`, like so: - -![Django Filter](../img/django-filter.png) - -#### Specifying filter fields - If all you need is simple equality-based filtering, you can set a `filter_fields` attribute on the view, or viewset, listing the set of fields you wish to filter against. class ProductList(generics.ListAPIView): @@ -182,80 +172,10 @@ This will automatically create a `FilterSet` class for the given fields, and wil http://example.com/api/products?category=clothing&in_stock=True -#### Specifying a FilterSet +For more advanced filtering requirements you can specify a `FilterSet` class that should be used by the view. +You can read more about `FilterSet`s in the [django-filter documentation][django-filter-docs]. +It's also recommended that you read the section on [DRF integration][django-filter-drf-docs]. -For more advanced filtering requirements you can specify a `FilterSet` class that should be used by the view. For example: - - import django_filters - from myapp.models import Product - from myapp.serializers import ProductSerializer - from rest_framework import generics - - class ProductFilter(django_filters.rest_framework.FilterSet): - min_price = django_filters.NumberFilter(name="price", lookup_expr='gte') - max_price = django_filters.NumberFilter(name="price", lookup_expr='lte') - class Meta: - model = Product - fields = ['category', 'in_stock', 'min_price', 'max_price'] - - class ProductList(generics.ListAPIView): - queryset = Product.objects.all() - serializer_class = ProductSerializer - filter_backends = (django_filters.rest_framework.DjangoFilterBackend,) - filter_class = ProductFilter - - -Which will allow you to make requests such as: - - http://example.com/api/products?category=clothing&max_price=10.00 - -You can also span relationships using `django-filter`, let's assume that each -product has foreign key to `Manufacturer` model, so we create filter that -filters using `Manufacturer` name. For example: - - import django_filters - from myapp.models import Product - from myapp.serializers import ProductSerializer - from rest_framework import generics - - class ProductFilter(django_filters.rest_framework.FilterSet): - class Meta: - model = Product - fields = ['category', 'in_stock', 'manufacturer__name'] - -This enables us to make queries like: - - http://example.com/api/products?manufacturer__name=foo - -This is nice, but it exposes the Django's double underscore convention as part of the API. If you instead want to explicitly name the filter argument you can instead explicitly include it on the `FilterSet` class: - - import django_filters - from myapp.models import Product - from myapp.serializers import ProductSerializer - from rest_framework import generics - - class ProductFilter(django_filters.rest_framework.FilterSet): - manufacturer = django_filters.CharFilter(name="manufacturer__name") - - class Meta: - model = Product - fields = ['category', 'in_stock', 'manufacturer'] - -And now you can execute: - - http://example.com/api/products?manufacturer=foo - -For more details on using filter sets see the [django-filter documentation][django-filter-docs]. - ---- - -**Hints & Tips** - -* By default filtering is not enabled. If you want to use `DjangoFilterBackend` remember to make sure it is installed by using the `'DEFAULT_FILTER_BACKENDS'` setting. -* When using boolean fields, you should use the values `True` and `False` in the URL query parameters, rather than `0`, `1`, `true` or `false`. (The allowed boolean values are currently hardwired in Django's [NullBooleanSelect implementation][nullbooleanselect].) -* `django-filter` supports filtering across relationships, using Django's double-underscore syntax. - ---- ## SearchFilter @@ -461,6 +381,7 @@ The [djangorestframework-word-filter][django-rest-framework-word-search-filter] [cite]: https://docs.djangoproject.com/en/stable/topics/db/queries/#retrieving-specific-objects-with-filters [django-filter]: https://github.com/alex/django-filter [django-filter-docs]: https://django-filter.readthedocs.io/en/latest/index.html +[django-filter-drf-docs]: https://django-filter.readthedocs.io/en/develop/guide/rest_framework.html [guardian]: https://django-guardian.readthedocs.io/ [view-permissions]: https://django-guardian.readthedocs.io/en/latest/userguide/assign.html [view-permissions-blogpost]: http://blog.nyaruka.com/adding-a-view-permission-to-django-models diff --git a/docs/img/django-filter.png b/docs/img/django-filter.png deleted file mode 100644 index 5baef32f7e89ce973cb097544810756d26c29c95..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 13678 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 Date: Wed, 3 May 2017 12:49:39 -0400 Subject: [PATCH 7/9] Add failing test for filter backend mro --- requirements/requirements-optionals.txt | 2 +- tests/test_filters.py | 15 +++++++++++++++ 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/requirements/requirements-optionals.txt b/requirements/requirements-optionals.txt index 5a0eb9cb2..83fe6d955 100644 --- a/requirements/requirements-optionals.txt +++ b/requirements/requirements-optionals.txt @@ -1,6 +1,6 @@ # Optional packages which may be used with REST framework. markdown==2.6.4 django-guardian==1.4.8 -django-filter==1.0.0 +django-filter==1.0.2 coreapi==2.2.4 coreschema==0.0.4 diff --git a/tests/test_filters.py b/tests/test_filters.py index 1b6f6f273..d2c11d258 100644 --- a/tests/test_filters.py +++ b/tests/test_filters.py @@ -201,6 +201,21 @@ class IntegrationTestFiltering(CommonFilteringTestCase): assert response.data == self.data assert len(w) == 0 + @unittest.skipUnless(django_filters, 'django-filter not installed') + def test_backend_mro(self): + class CustomBackend(filters.DjangoFilterBackend): + def filter_queryset(self, request, queryset, view): + assert False, "custom filter_queryset should run" + + class DFFilterFieldsRootView(FilterFieldsRootView): + filter_backends = (CustomBackend,) + + view = DFFilterFieldsRootView.as_view() + request = factory.get('/') + + with pytest.raises(AssertionError, message="custom filter_queryset should run"): + view(request).render() + @unittest.skipUnless(django_filters, 'django-filter not installed') def test_get_filtered_fields_root_view(self): """ From 01ffb8961de3c9b0531cedae2a524ce1a9927415 Mon Sep 17 00:00:00 2001 From: Ryan P Kilby Date: Wed, 3 May 2017 12:51:44 -0400 Subject: [PATCH 8/9] Fix DjangoFilterBackend mro --- rest_framework/filters.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/rest_framework/filters.py b/rest_framework/filters.py index 20e196a14..429b79c77 100644 --- a/rest_framework/filters.py +++ b/rest_framework/filters.py @@ -50,12 +50,17 @@ if django_filters: DeprecationWarning ) return super(FilterSet, self).__init__(*args, **kwargs) + + DFBase = django_filters.rest_framework.DjangoFilterBackend + else: def FilterSet(): assert False, 'django-filter must be installed to use the `FilterSet` class' + DFBase = BaseFilterBackend -class DjangoFilterBackend(BaseFilterBackend): + +class DjangoFilterBackend(DFBase): """ A filter backend that uses django-filter. """ @@ -69,9 +74,7 @@ class DjangoFilterBackend(BaseFilterBackend): DeprecationWarning ) - from django_filters.rest_framework import DjangoFilterBackend - - return DjangoFilterBackend(*args, **kwargs) + return super(DjangoFilterBackend, cls).__new__(cls, *args, **kwargs) class SearchFilter(BaseFilterBackend): From 5246a5a44ebc52a2d2df88ca41d8a69b09c987c3 Mon Sep 17 00:00:00 2001 From: German Ilyin Date: Thu, 4 May 2017 10:13:58 +0600 Subject: [PATCH 9/9] Fix a typo in a comment --- rest_framework/utils/field_mapping.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rest_framework/utils/field_mapping.py b/rest_framework/utils/field_mapping.py index cc82492dc..44c6d4b70 100644 --- a/rest_framework/utils/field_mapping.py +++ b/rest_framework/utils/field_mapping.py @@ -138,7 +138,7 @@ def get_field_kwargs(field_name, model_field): if not isinstance(validator, validators.MaxValueValidator) ] - # Ensure that max_value is passed explicitly as a keyword arg, + # Ensure that min_value is passed explicitly as a keyword arg, # rather than as a validator. min_value = next(( validator.limit_value for validator in validator_kwarg