From d5e63d2d7f6570d6306e755bd7366c75dd1b845b Mon Sep 17 00:00:00 2001 From: Fa773N M0nK Date: Tue, 18 Oct 2016 15:36:04 +0530 Subject: [PATCH 001/190] Reflect that '@detail_route' responds to GET only by default. (#4582) --- docs/tutorial/6-viewsets-and-routers.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/tutorial/6-viewsets-and-routers.md b/docs/tutorial/6-viewsets-and-routers.md index 6e1321093..f4eb918bf 100644 --- a/docs/tutorial/6-viewsets-and-routers.md +++ b/docs/tutorial/6-viewsets-and-routers.md @@ -51,7 +51,7 @@ This time we've used the `ModelViewSet` class in order to get the complete set o Notice that we've also used the `@detail_route` decorator to create a custom action, named `highlight`. This decorator can be used to add any custom endpoints that don't fit into the standard `create`/`update`/`delete` style. -Custom actions which use the `@detail_route` decorator will respond to `GET` requests. We can use the `methods` argument if we wanted an action that responded to `POST` requests. +Custom actions which use the `@detail_route` decorator will respond to `GET` requests by default. We can use the `methods` argument if we wanted an action that responded to `POST` requests. The URLs for custom actions by default depend on the method name itself. If you want to change the way url should be constructed, you can include url_path as a decorator keyword argument. From fcff16c5c658fc3033b7ecefdd74221a596ae24c Mon Sep 17 00:00:00 2001 From: Zach Wernberg Date: Tue, 18 Oct 2016 23:14:55 -0500 Subject: [PATCH 002/190] minor typo --- rest_framework/filters.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rest_framework/filters.py b/rest_framework/filters.py index 65233978e..c377cec0d 100644 --- a/rest_framework/filters.py +++ b/rest_framework/filters.py @@ -222,7 +222,7 @@ class SearchFilter(BaseFilterBackend): # Filtering against a many-to-many field requires us to # call queryset.distinct() in order to avoid duplicate items # in the resulting queryset. - # We try to avoid this is possible, for performance reasons. + # We try to avoid this if possible, for performance reasons. queryset = distinct(queryset, base) return queryset From 3f6004c5a9edab6336e93da85ce3849dee0b1311 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Thu, 20 Oct 2016 09:42:40 +0100 Subject: [PATCH 003/190] Use pk for URL conf and views. (#4592) --- docs/tutorial/1-serialization.md | 6 +++--- docs/tutorial/2-requests-and-responses.md | 8 ++++---- docs/tutorial/3-class-based-views.md | 18 +++++++++--------- .../4-authentication-and-permissions.md | 2 +- .../5-relationships-and-hyperlinked-apis.md | 8 ++++---- docs/tutorial/6-viewsets-and-routers.md | 6 +++--- 6 files changed, 24 insertions(+), 24 deletions(-) diff --git a/docs/tutorial/1-serialization.md b/docs/tutorial/1-serialization.md index 434072e11..04fb6914a 100644 --- a/docs/tutorial/1-serialization.md +++ b/docs/tutorial/1-serialization.md @@ -259,12 +259,12 @@ Note that because we want to be able to POST to this view from clients that won' We'll also need a view which corresponds to an individual snippet, and can be used to retrieve, update or delete the snippet. @csrf_exempt - def snippet_detail(request, id): + def snippet_detail(request, pk): """ Retrieve, update or delete a code snippet. """ try: - snippet = Snippet.objects.get(id=id) + snippet = Snippet.objects.get(pk=pk) except Snippet.DoesNotExist: return HttpResponse(status=404) @@ -291,7 +291,7 @@ Finally we need to wire these views up. Create the `snippets/urls.py` file: urlpatterns = [ url(r'^snippets/$', views.snippet_list), - url(r'^snippets/(?P[0-9]+)/$', views.snippet_detail), + url(r'^snippets/(?P[0-9]+)/$', views.snippet_detail), ] We also need to wire up the root urlconf, in the `tutorial/urls.py` file, to include our snippet app's URLs. diff --git a/docs/tutorial/2-requests-and-responses.md b/docs/tutorial/2-requests-and-responses.md index 4aa0062a3..5c020a1f7 100644 --- a/docs/tutorial/2-requests-and-responses.md +++ b/docs/tutorial/2-requests-and-responses.md @@ -66,12 +66,12 @@ Our instance view is an improvement over the previous example. It's a little mo Here is the view for an individual snippet, in the `views.py` module. @api_view(['GET', 'PUT', 'DELETE']) - def snippet_detail(request, id): + def snippet_detail(request, pk): """ Retrieve, update or delete a snippet instance. """ try: - snippet = Snippet.objects.get(id=id) + snippet = Snippet.objects.get(pk=pk) except Snippet.DoesNotExist: return Response(status=status.HTTP_404_NOT_FOUND) @@ -104,7 +104,7 @@ Start by adding a `format` keyword argument to both of the views, like so. and - def snippet_detail(request, id, format=None): + def snippet_detail(request, pk, format=None): Now update the `urls.py` file slightly, to append a set of `format_suffix_patterns` in addition to the existing URLs. @@ -114,7 +114,7 @@ Now update the `urls.py` file slightly, to append a set of `format_suffix_patter urlpatterns = [ url(r'^snippets/$', views.snippet_list), - url(r'^snippets/(?P[0-9]+)$', views.snippet_detail), + url(r'^snippets/(?P[0-9]+)$', views.snippet_detail), ] urlpatterns = format_suffix_patterns(urlpatterns) diff --git a/docs/tutorial/3-class-based-views.md b/docs/tutorial/3-class-based-views.md index 6303994cd..f018666f5 100644 --- a/docs/tutorial/3-class-based-views.md +++ b/docs/tutorial/3-class-based-views.md @@ -36,27 +36,27 @@ So far, so good. It looks pretty similar to the previous case, but we've got be """ Retrieve, update or delete a snippet instance. """ - def get_object(self, id): + def get_object(self, pk): try: - return Snippet.objects.get(id=id) + return Snippet.objects.get(pk=pk) except Snippet.DoesNotExist: raise Http404 - def get(self, request, id, format=None): - snippet = self.get_object(id) + def get(self, request, pk, format=None): + snippet = self.get_object(pk) serializer = SnippetSerializer(snippet) return Response(serializer.data) - def put(self, request, id, format=None): - snippet = self.get_object(id) + def put(self, request, pk, format=None): + snippet = self.get_object(pk) serializer = SnippetSerializer(snippet, data=request.data) if serializer.is_valid(): serializer.save() return Response(serializer.data) return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) - def delete(self, request, id, format=None): - snippet = self.get_object(id) + def delete(self, request, pk, format=None): + snippet = self.get_object(pk) snippet.delete() return Response(status=status.HTTP_204_NO_CONTENT) @@ -70,7 +70,7 @@ We'll also need to refactor our `urls.py` slightly now we're using class-based v urlpatterns = [ url(r'^snippets/$', views.SnippetList.as_view()), - url(r'^snippets/(?P[0-9]+)/$', views.SnippetDetail.as_view()), + url(r'^snippets/(?P[0-9]+)/$', views.SnippetDetail.as_view()), ] urlpatterns = format_suffix_patterns(urlpatterns) diff --git a/docs/tutorial/4-authentication-and-permissions.md b/docs/tutorial/4-authentication-and-permissions.md index 958f9d3f0..098194c29 100644 --- a/docs/tutorial/4-authentication-and-permissions.md +++ b/docs/tutorial/4-authentication-and-permissions.md @@ -88,7 +88,7 @@ Make sure to also import the `UserSerializer` class Finally we need to add those views into the API, by referencing them from the URL conf. Add the following to the patterns in `urls.py`. url(r'^users/$', views.UserList.as_view()), - url(r'^users/(?P[0-9]+)/$', views.UserDetail.as_view()), + url(r'^users/(?P[0-9]+)/$', views.UserDetail.as_view()), ## Associating Snippets with Users diff --git a/docs/tutorial/5-relationships-and-hyperlinked-apis.md b/docs/tutorial/5-relationships-and-hyperlinked-apis.md index 9fb6c53e0..9fd61b414 100644 --- a/docs/tutorial/5-relationships-and-hyperlinked-apis.md +++ b/docs/tutorial/5-relationships-and-hyperlinked-apis.md @@ -48,7 +48,7 @@ We'll add a url pattern for our new API root in `snippets/urls.py`: And then add a url pattern for the snippet highlights: - url(r'^snippets/(?P[0-9]+)/highlight/$', views.SnippetHighlight.as_view()), + url(r'^snippets/(?P[0-9]+)/highlight/$', views.SnippetHighlight.as_view()), ## Hyperlinking our API @@ -116,16 +116,16 @@ After adding all those names into our URLconf, our final `snippets/urls.py` file url(r'^snippets/$', views.SnippetList.as_view(), name='snippet-list'), - url(r'^snippets/(?P[0-9]+)/$', + url(r'^snippets/(?P[0-9]+)/$', views.SnippetDetail.as_view(), name='snippet-detail'), - url(r'^snippets/(?P[0-9]+)/highlight/$', + url(r'^snippets/(?P[0-9]+)/highlight/$', views.SnippetHighlight.as_view(), name='snippet-highlight'), url(r'^users/$', views.UserList.as_view(), name='user-list'), - url(r'^users/(?P[0-9]+)/$', + url(r'^users/(?P[0-9]+)/$', views.UserDetail.as_view(), name='user-detail') ]) diff --git a/docs/tutorial/6-viewsets-and-routers.md b/docs/tutorial/6-viewsets-and-routers.md index f4eb918bf..6189c7771 100644 --- a/docs/tutorial/6-viewsets-and-routers.md +++ b/docs/tutorial/6-viewsets-and-routers.md @@ -92,10 +92,10 @@ Now that we've bound our resources into concrete views, we can register the view urlpatterns = format_suffix_patterns([ url(r'^$', api_root), url(r'^snippets/$', snippet_list, name='snippet-list'), - url(r'^snippets/(?P[0-9]+)/$', snippet_detail, name='snippet-detail'), - url(r'^snippets/(?P[0-9]+)/highlight/$', snippet_highlight, name='snippet-highlight'), + url(r'^snippets/(?P[0-9]+)/$', snippet_detail, name='snippet-detail'), + url(r'^snippets/(?P[0-9]+)/highlight/$', snippet_highlight, name='snippet-highlight'), url(r'^users/$', user_list, name='user-list'), - url(r'^users/(?P[0-9]+)/$', user_detail, name='user-detail') + url(r'^users/(?P[0-9]+)/$', user_detail, name='user-detail') ]) ## Using Routers From 2395fb53867538ad83db335f3aaaef472eb2f0f4 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Thu, 20 Oct 2016 10:47:09 +0100 Subject: [PATCH 004/190] Deprecate DjangoFilter backend (#4593) Deprecate the built-in `rest_framework.filters.DjangoFilterBackend` in favour of the third-party `django_filters.rest_framework.DjangoFilterBackend`. --- docs/api-guide/filtering.md | 40 +++++--- requirements/requirements-optionals.txt | 2 +- rest_framework/filters.py | 128 ++++-------------------- tests/test_filters.py | 34 +++++++ 4 files changed, 83 insertions(+), 121 deletions(-) diff --git a/docs/api-guide/filtering.md b/docs/api-guide/filtering.md index 40a097174..1b49d3a73 100644 --- a/docs/api-guide/filtering.md +++ b/docs/api-guide/filtering.md @@ -89,24 +89,24 @@ Generic filters can also present themselves as HTML controls in the browsable AP ## Setting filter backends -The default filter backends may be set globally, using the `DEFAULT_FILTER_BACKENDS` setting. For example. +The default filter backends may be set globally, using the `DEFAULT_FILTER_BACKENDS` setting. For example. REST_FRAMEWORK = { - 'DEFAULT_FILTER_BACKENDS': ('rest_framework.filters.DjangoFilterBackend',) + 'DEFAULT_FILTER_BACKENDS': ('django_filters.rest_framework.DjangoFilterBackend',) } You can also set the filter backends on a per-view, or per-viewset basis, using the `GenericAPIView` class-based views. + import django_filters from django.contrib.auth.models import User from myapp.serializers import UserSerializer - from rest_framework import filters from rest_framework import generics class UserListView(generics.ListAPIView): queryset = User.objects.all() serializer_class = UserSerializer - filter_backends = (filters.DjangoFilterBackend,) + filter_backends = (django_filters.rest_framework.DjangoFilterBackend,) ## Filtering and object lookups @@ -139,12 +139,27 @@ Note that you can use both an overridden `.get_queryset()` and generic filtering ## DjangoFilterBackend -The `DjangoFilterBackend` class supports highly customizable field filtering, using the [django-filter package][django-filter]. +The `django-filter` library includes a `DjangoFilterBackend` class which +supports highly customizable field filtering for REST framework. -To use REST framework's `DjangoFilterBackend`, first install `django-filter`. +To use `DjangoFilterBackend`, first install `django-filter`. pip install django-filter +You should now either add the filter backend to your settings: + + REST_FRAMEWORK = { + 'DEFAULT_FILTER_BACKENDS': ('django_filters.rest_framework.DjangoFilterBackend',) + } + +Or add the filter backend to an individual View or ViewSet. + + from django_filters.rest_framework import DjangoFilterBackend + + class UserListView(generics.ListAPIView): + ... + 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 @@ -174,10 +189,9 @@ For more advanced filtering requirements you can specify a `FilterSet` class tha import django_filters from myapp.models import Product from myapp.serializers import ProductSerializer - from rest_framework import filters from rest_framework import generics - class ProductFilter(filters.FilterSet): + 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: @@ -187,7 +201,7 @@ For more advanced filtering requirements you can specify a `FilterSet` class tha class ProductList(generics.ListAPIView): queryset = Product.objects.all() serializer_class = ProductSerializer - filter_backends = (filters.DjangoFilterBackend,) + filter_backends = (django_filters.rest_framework.DjangoFilterBackend,) filter_class = ProductFilter @@ -199,12 +213,12 @@ 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 filters from rest_framework import generics - class ProductFilter(filters.FilterSet): + class ProductFilter(django_filters.rest_framework.FilterSet): class Meta: model = Product fields = ['category', 'in_stock', 'manufacturer__name'] @@ -218,10 +232,9 @@ This is nice, but it exposes the Django's double underscore convention as part o import django_filters from myapp.models import Product from myapp.serializers import ProductSerializer - from rest_framework import filters from rest_framework import generics - class ProductFilter(filters.FilterSet): + class ProductFilter(django_filters.rest_framework.FilterSet): manufacturer = django_filters.CharFilter(name="manufacturer__name") class Meta: @@ -454,4 +467,3 @@ The [djangorestframework-word-filter][django-rest-framework-word-search-filter] [django-rest-framework-word-search-filter]: https://github.com/trollknurr/django-rest-framework-word-search-filter [django-url-filter]: https://github.com/miki725/django-url-filter [drf-url-filter]: https://github.com/manjitkumar/drf-url-filters - diff --git a/requirements/requirements-optionals.txt b/requirements/requirements-optionals.txt index 31f24f4b7..86c4f7709 100644 --- a/requirements/requirements-optionals.txt +++ b/requirements/requirements-optionals.txt @@ -1,5 +1,5 @@ # Optional packages which may be used with REST framework. markdown==2.6.4 django-guardian==1.4.6 -django-filter==0.14.0 +django-filter==0.15.3 coreapi==2.0.8 diff --git a/rest_framework/filters.py b/rest_framework/filters.py index c377cec0d..f55297b39 100644 --- a/rest_framework/filters.py +++ b/rest_framework/filters.py @@ -5,9 +5,9 @@ returned by list views. from __future__ import unicode_literals import operator +import warnings from functools import reduce -from django.conf import settings from django.core.exceptions import ImproperlyConfigured from django.db import models from django.db.models.constants import LOOKUP_SEP @@ -16,50 +16,10 @@ from django.utils import six from django.utils.translation import ugettext_lazy as _ from rest_framework.compat import ( - coreapi, crispy_forms, distinct, django_filters, guardian, template_render + coreapi, distinct, django_filters, guardian, template_render ) from rest_framework.settings import api_settings -if 'crispy_forms' in settings.INSTALLED_APPS and crispy_forms and django_filters: - # If django-crispy-forms is installed, use it to get a bootstrap3 rendering - # of the DjangoFilterBackend controls when displayed as HTML. - from crispy_forms.helper import FormHelper - from crispy_forms.layout import Layout, Submit - - class FilterSet(django_filters.FilterSet): - def __init__(self, *args, **kwargs): - super(FilterSet, self).__init__(*args, **kwargs) - for field in self.form.fields.values(): - field.help_text = None - - layout_components = list(self.form.fields.keys()) + [ - Submit('', _('Submit'), css_class='btn-default'), - ] - - helper = FormHelper() - helper.form_method = 'GET' - helper.template_pack = 'bootstrap3' - helper.layout = Layout(*layout_components) - - self.form.helper = helper - - filter_template = 'rest_framework/filters/django_filter_crispyforms.html' - -elif django_filters: - # If django-crispy-forms is not installed, use the standard - # 'form.as_p' rendering when DjangoFilterBackend is displayed as HTML. - class FilterSet(django_filters.FilterSet): - def __init__(self, *args, **kwargs): - super(FilterSet, self).__init__(*args, **kwargs) - for field in self.form.fields.values(): - field.help_text = None - - filter_template = 'rest_framework/filters/django_filter.html' - -else: - FilterSet = None - filter_template = None - class BaseFilterBackend(object): """ @@ -77,78 +37,34 @@ class BaseFilterBackend(object): return [] +class FilterSet(object): + def __new__(cls, *args, **kwargs): + warnings.warn( + "The built in 'rest_framework.filters.FilterSet' is pending deprecation. " + "You should use 'django_filters.rest_framework.FilterSet' instead.", + PendingDeprecationWarning + ) + from django_filters.rest_framework import FilterSet + return FilterSet(*args, **kwargs) + + class DjangoFilterBackend(BaseFilterBackend): """ A filter backend that uses django-filter. """ - default_filter_set = FilterSet - template = filter_template - - def __init__(self): + def __new__(cls, *args, **kwargs): assert django_filters, 'Using DjangoFilterBackend, but django-filter is not installed' + assert django_filters.VERSION >= (0, 15, 3), 'django-filter 0.15.3 and above is required' - def get_filter_class(self, view, queryset=None): - """ - Return the django-filters `FilterSet` used to filter the queryset. - """ - filter_class = getattr(view, 'filter_class', None) - filter_fields = getattr(view, 'filter_fields', None) + warnings.warn( + "The built in 'rest_framework.filters.DjangoFilterBackend' is pending deprecation. " + "You should use 'django_filters.rest_framework.DjangoFilterBackend' instead.", + PendingDeprecationWarning + ) - if filter_class: - filter_model = filter_class.Meta.model + from django_filters.rest_framework import DjangoFilterBackend - assert issubclass(queryset.model, filter_model), \ - 'FilterSet model %s does not match queryset model %s' % \ - (filter_model, queryset.model) - - return filter_class - - if filter_fields: - class AutoFilterSet(self.default_filter_set): - class Meta: - model = queryset.model - fields = filter_fields - - return AutoFilterSet - - return None - - def filter_queryset(self, request, queryset, view): - filter_class = self.get_filter_class(view, queryset) - - if filter_class: - return filter_class(request.query_params, queryset=queryset).qs - - return queryset - - def to_html(self, request, queryset, view): - filter_class = self.get_filter_class(view, queryset) - if not filter_class: - return None - filter_instance = filter_class(request.query_params, queryset=queryset) - context = { - 'filter': filter_instance - } - template = loader.get_template(self.template) - return template_render(template, context) - - def get_schema_fields(self, view): - assert coreapi is not None, 'coreapi must be installed to use `get_schema_fields()`' - filter_class = getattr(view, 'filter_class', None) - if filter_class: - return [ - coreapi.Field(name=field_name, required=False, location='query') - for field_name in filter_class().filters.keys() - ] - - filter_fields = getattr(view, 'filter_fields', None) - if filter_fields: - return [ - coreapi.Field(name=field_name, required=False, location='query') - for field_name in filter_fields - ] - - return [] + return DjangoFilterBackend(*args, **kwargs) class SearchFilter(BaseFilterBackend): diff --git a/tests/test_filters.py b/tests/test_filters.py index c67412dd7..9795230d6 100644 --- a/tests/test_filters.py +++ b/tests/test_filters.py @@ -2,6 +2,7 @@ from __future__ import unicode_literals import datetime import unittest +import warnings from decimal import Decimal from django.conf.urls import url @@ -134,6 +135,39 @@ class IntegrationTestFiltering(CommonFilteringTestCase): Integration tests for filtered list views. """ + @unittest.skipUnless(django_filters, 'django-filter not installed') + def test_backend_deprecation(self): + with warnings.catch_warnings(record=True) as w: + warnings.simplefilter("always") + + view = FilterFieldsRootView.as_view() + request = factory.get('/') + response = view(request).render() + + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(response.data, self.data) + + self.assertTrue(issubclass(w[-1].category, PendingDeprecationWarning)) + self.assertIn("'rest_framework.filters.DjangoFilterBackend' is pending deprecation.", str(w[-1].message)) + + @unittest.skipUnless(django_filters, 'django-filter not installed') + def test_no_df_deprecation(self): + with warnings.catch_warnings(record=True) as w: + warnings.simplefilter("always") + + import django_filters.rest_framework + + class DFFilterFieldsRootView(FilterFieldsRootView): + filter_backends = (django_filters.rest_framework.DjangoFilterBackend,) + + view = DFFilterFieldsRootView.as_view() + request = factory.get('/') + response = view(request).render() + + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(response.data, self.data) + self.assertEqual(len(w), 0) + @unittest.skipUnless(django_filters, 'django-filter not installed') def test_get_filtered_fields_root_view(self): """ From 3b9afb571b6e7c2a445be675163c9f43d680d899 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Thu, 20 Oct 2016 16:25:40 +0100 Subject: [PATCH 005/190] Version 3.5.0 (#4596) --- docs/api-guide/schemas.md | 47 +++++- docs/api-guide/testing.md | 4 +- docs/img/raml.png | Bin 0 -> 37216 bytes docs/index.md | 2 + docs/topics/3.5-announcement.md | 266 ++++++++++++++++++++++++++++++++ docs/topics/api-clients.md | 2 +- docs/topics/release-notes.md | 9 ++ mkdocs.yml | 1 + 8 files changed, 324 insertions(+), 7 deletions(-) create mode 100644 docs/img/raml.png create mode 100644 docs/topics/3.5-announcement.md diff --git a/docs/api-guide/schemas.md b/docs/api-guide/schemas.md index 16d2bbb01..7da619034 100644 --- a/docs/api-guide/schemas.md +++ b/docs/api-guide/schemas.md @@ -344,12 +344,12 @@ Typically you'll instantiate `SchemaGenerator` with a single argument, like so: Arguments: -* `title` - The name of the API. **required** +* `title` **required** - The name of the API. * `url` - The root URL of the API schema. This option is not required unless the schema is included under path prefix. * `patterns` - A list of URLs to inspect when generating the schema. Defaults to the project's URL conf. * `urlconf` - A URL conf module name to use when generating the schema. Defaults to `settings.ROOT_URLCONF`. -### get_schema() +### get_schema(self, request) Returns a `coreapi.Document` instance that represents the API schema. @@ -359,9 +359,48 @@ Returns a `coreapi.Document` instance that represents the API schema. generator = schemas.SchemaGenerator(title='Bookings API') return Response(generator.get_schema()) -Arguments: +The `request` argument is optional, and may be used if you want to apply per-user +permissions to the resulting schema generation. -* `request` - The incoming request. Optionally used if you want to apply per-user permissions to the schema-generation. +### get_links(self, request) + +Return a nested dictionary containing all the links that should be included in the API schema. + +This is a good point to override if you want to modify the resulting structure of the generated schema, +as you can build a new dictionary with a different layout. + +### get_link(self, path, method, view) + +Returns a `coreapi.Link` instance corresponding to the given view. + +You can override this if you need to provide custom behaviors for particular views. + +### get_description(self, path, method, view) + +Returns a string to use as the link description. By default this is based on the +view docstring as described in the "Schemas as Documentation" section above. + +### get_encoding(self, path, method, view) + +Returns a string to indicate the encoding for any request body, when interacting +with the given view. Eg. `'application/json'`. May return a blank string for views +that do not expect a request body. + +### get_path_fields(self, path, method, view): + +Return a list of `coreapi.Link()` instances. One for each path parameter in the URL. + +### get_serializer_fields(self, path, method, view) + +Return a list of `coreapi.Link()` instances. One for each field in the serializer class used by the view. + +### get_pagination_fields(self, path, method, view + +Return a list of `coreapi.Link()` instances, as returned by the `get_schema_fields()` method on any pagination class used by the view. + +### get_filter_fields(self, path, method, view) + +Return a list of `coreapi.Link()` instances, as returned by the `get_schema_fields()` method of any filter classes used by the view. --- diff --git a/docs/api-guide/testing.md b/docs/api-guide/testing.md index fdd60b7f4..1f8c89233 100644 --- a/docs/api-guide/testing.md +++ b/docs/api-guide/testing.md @@ -194,6 +194,7 @@ directly. client = RequestsClient() response = client.get('http://testserver/users/') + assert response.status_code == 200 Note that the requests client requires you to pass fully qualified URLs. @@ -251,9 +252,8 @@ The CoreAPIClient allows you to interact with your API using the Python `coreapi` client library. # Fetch the API schema - url = reverse('schema') client = CoreAPIClient() - schema = client.get(url) + schema = client.get('http://testserver/schema/') # Create a new organisation params = {'name': 'MegaCorp', 'status': 'active'} diff --git a/docs/img/raml.png b/docs/img/raml.png new file mode 100644 index 0000000000000000000000000000000000000000..87790dc484f9c6c0617189ae2766bdc1e00a3bfa GIT binary patch literal 37216 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 literal 0 HcmV?d00001 diff --git a/docs/index.md b/docs/index.md index 3019467ed..9b0913f00 100644 --- a/docs/index.md +++ b/docs/index.md @@ -244,6 +244,7 @@ General guides to using REST framework. * [3.2 Announcement][3.2-announcement] * [3.3 Announcement][3.3-announcement] * [3.4 Announcement][3.4-announcement] +* [3.5 Announcement][3.5-announcement] * [Kickstarter Announcement][kickstarter-announcement] * [Mozilla Grant][mozilla-grant] * [Funding][funding] @@ -370,6 +371,7 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. [3.2-announcement]: topics/3.2-announcement.md [3.3-announcement]: topics/3.3-announcement.md [3.4-announcement]: topics/3.4-announcement.md +[3.5-announcement]: topics/3.5-announcement.md [kickstarter-announcement]: topics/kickstarter-announcement.md [mozilla-grant]: topics/mozilla-grant.md [funding]: topics/funding.md diff --git a/docs/topics/3.5-announcement.md b/docs/topics/3.5-announcement.md new file mode 100644 index 000000000..2ed8adf8e --- /dev/null +++ b/docs/topics/3.5-announcement.md @@ -0,0 +1,266 @@ + + +# Django REST framework 3.5 + +The 3.5 release is the second in a planned series that is addressing schema +generation, hypermedia support, API client libraries, and finally realtime support. + +--- + +## Funding + +The 3.5 release would not have been possible without our [collaborative funding model][funding]. +If you use REST framework commercially and would like to see this work continue, +we strongly encourage you to invest in its continued development by +**[signing up for a paid plan][funding]**. + + +
+ +*Many thanks to all our [sponsors][sponsors], and in particular to our premium backers, [Rover](http://jobs.rover.com/), [Sentry](https://getsentry.com/welcome/), [Stream](https://getstream.io/?utm_source=drf&utm_medium=banner&utm_campaign=drf), and [Machinalis](http://www.machinalis.com/#services).* + +--- + +## Improved schema generation + +Docstrings on views are now pulled through into schema definitions, allowing +you to [use the schema definition to document your API][schema-docs]. + +There is now also a shortcut function, `get_schema_view()`, which makes it easier to +[adding schema views][schema-view] to your API. + +For example, to include a swagger schema to your API, you would do the following: + +* Run `pip install django-rest-swagger`. + +* Add `'rest_framework_swagger'` to your `INSTALLED_APPS` setting. + +* Include the schema view in your URL conf: + +```py +from rest_framework.schemas import get_schema_view +from rest_framework_swagger.renderers import OpenAPIRenderer, SwaggerUIRenderer + +schema_view = get_schema_view( + title='Example API', + renderer_classes=[OpenAPIRenderer, SwaggerUIRenderer] +) + +urlpatterns = [ + url(r'^swagger/$', schema_view), + ... +] +``` + +There have been a large number of fixes to the schema generation. These should +resolve issues for anyone using the latest version of the `django-rest-swagger` +package. + +Some of these changes do affect the resulting schema structure, +so if you're already using schema generation you should make sure to review +[the deprecation notes](#deprecations), particularly if you're currently using +a dynamic client library to interact with your API. + +Finally, we're also now exposing the schema generation as a +[publicly documented API][schema-generation-api], allowing you to more easily +override the behaviour. + +## Requests test client + +You can now test your project using the `requests` library. + +This exposes exactly the same interface as if you were using a standard +requests session instance. + + client = RequestsClient() + response = client.get('http://testserver/users/') + assert response.status_code == 200 + +Rather than sending any HTTP requests to the network, this interface will +coerce all outgoing requests into WSGI, and call into your application directly. + +## Core API client + +You can also now test your project by interacting with it using the `coreapi` +client library. + + # Fetch the API schema + client = CoreAPIClient() + schema = client.get('http://testserver/schema/') + + # Create a new organisation + params = {'name': 'MegaCorp', 'status': 'active'} + client.action(schema, ['organisations', 'create'], params) + + # Ensure that the organisation exists in the listing + data = client.action(schema, ['organisations', 'list']) + assert(len(data) == 1) + assert(data == [{'name': 'MegaCorp', 'status': 'active'}]) + +Again, this will call directly into the application using the WSGI interface, +rather than making actual network calls. + +This is a good option if you are planning for clients to mainly interact with +your API using the `coreapi` client library, or some other auto-generated client. + +## Live tests + +One interesting aspect of both the `requests` client and the `coreapi` client +is that they allow you to write tests in such a way that they can also be made +to run against a live service. + +By switching the WSGI based client instances to actual instances of `requests.Session` +or `coreapi.Client` you can have the test cases make actual network calls. + +Being able to write test cases that can exercise your staging or production +environment is a powerful tool. However in order to do this, you'll need to pay +close attention to how you handle setup and teardown to ensure a strict isolation +of test data from other live or staging data. + +## RAML support + +We now have preliminary support for [RAML documentation generation][django-rest-raml]. + +![RAML Example][raml-image] + +Further work on the encoding and documentation generation is planned, in order to +make features such as the 'Try it now' support available at a later date. + +This work also now means that you can use the Core API client libraries to interact +with APIs that expose a RAML specification. The [RAML codec][raml-codec] gives some examples of +interacting with the Spotify API in this way. + +## Validation codes + +Exceptions raised by REST framework now include short code identifiers. +When used together with our customizable error handling, this now allows you to +modify the style of API error messages. + +As an example, this allows for the following style of error responses: + + { + "message": "You do not have permission to perform this action.", + "code": "permission_denied" + } + +This is particularly useful with validation errors, which use appropriate +codes to identify differing kinds of failure... + + { + "name": {"message": "This field is required.", "code": "required"}, + "age": {"message": "A valid integer is required.", "code": "invalid"} + } + +## Client upload & download support + +The Python `coreapi` client library and the Core API command line tool both +now fully support file [uploads][uploads] and [downloads][downloads]. + +--- + +## Deprecations + +### Generating schemas from Router + +The router arguments for generating a schema view, such as `schema_title`, +are now pending deprecation. + +Instead of using `DefaultRouter(schema_title='Example API')`, you should use +the `get_schema_view()` function, and include the view in your URL conf. + +Make sure to include the view before your router urls. For example: + + from rest_framework.schemas import get_schema_view + from my_project.routers import router + + schema_view = get_schema_view(title='Example API') + + urlpatterns = [ + url('^$', schema_view), + url(r'^', include(router.urls)), + ] + +### Schema path representations + +The `'pk'` identifier in schema paths is now mapped onto the actually model field +name by default. This will typically be `'id'`. + +This gives a better external representation for schemas, with less implementation +detail being exposed. It also reflects the behaviour of using a ModelSerializer +class with `fields = '__all__'`. + +You can revert to the previous behaviour by setting `'SCHEMA_COERCE_PATH_PK': False` +in the REST framework settings. + +### Schema action name representations + +The internal `retrieve()` and `destroy()` method names are now coerced to an +external representation of `read` and `delete`. + +You can revert to the previous behaviour by setting `'SCHEMA_COERCE_METHOD_NAMES': {}` +in the REST framework settings. + +### DjangoFilterBackend + +The functionality of the built-in `DjangoFilterBackend` is now completely +included by the `django-filter` package. + +You should change your imports and REST framework filter settings as follows: + +* `rest_framework.filters.DjangoFilterBackend` becomes `django_filters.rest_framework.DjangoFilterBackend`. +* `rest_framework.filters.FilterSet` becomes `django_filters.rest_framework.FilterSet`. + +The existing imports will continue to work but are now pending deprecation. + +### CoreJSON media type + +The media type for `CoreJSON` is now `application/json+coreapi`, rather than +the previous `application/vnd.json+coreapi`. This brings it more into line with +other custom media types, such as those used by Swagger and RAML. + +The clients currently accept either media type. The old style-media type will +be deprecated at a later date. + +### ModelSerializer 'fields' and 'exclude' + +ModelSerializer and HyperlinkedModelSerializer must include either a fields +option, or an exclude option. The fields = '__all__' shortcut may be used to +explicitly include all fields. + +Failing to set either `fields` or `exclude` raised a pending deprecation warning +in version 3.3 and raised a deprecation warning in 3.4. Its usage is now mandatory. + +--- + +[sponsors]: https://fund.django-rest-framework.org/topics/funding/#our-sponsors +[funding]: funding.md +[uploads]: http://core-api.github.io/python-client/api-guide/utils/#file +[downloads]: http://core-api.github.io/python-client/api-guide/codecs/#downloadcodec +[schema-generation-api]: ../api-guide/schemas/#schemagenerator +[schema-docs]: ../api-guide/schemas/#schemas-as-documentation +[schema-view]: ../api-guide/schemas/#the-get_schema_view-shortcut +[django-rest-raml]: https://github.com/tomchristie/django-rest-raml +[raml-image]: ../img/raml.png +[raml-codec]: https://github.com/core-api/python-raml-codec diff --git a/docs/topics/api-clients.md b/docs/topics/api-clients.md index f17f5e4d4..c12551aa6 100644 --- a/docs/topics/api-clients.md +++ b/docs/topics/api-clients.md @@ -257,7 +257,7 @@ Codecs are responsible for encoding or decoding Documents. The decoding process is used by a client to take a bytestring of an API schema definition, and returning the Core API `Document` that represents that interface. -A codec should be associated with a particular media type, such as **TODO**. +A codec should be associated with a particular media type, such as `'application/coreapi+json'`. This media type is used by the server in the response `Content-Type` header, in order to indicate what kind of data is being returned in the response. diff --git a/docs/topics/release-notes.md b/docs/topics/release-notes.md index 446abdd14..30244f6d2 100644 --- a/docs/topics/release-notes.md +++ b/docs/topics/release-notes.md @@ -38,6 +38,14 @@ You can determine your currently installed version using `pip freeze`: --- +## 3.5.x series + +### 3.5.0 + +**Date**: [20th October 2016][3.5.0-milestone] + +--- + ## 3.4.x series ### 3.4.7 @@ -596,6 +604,7 @@ For older release notes, [please see the version 2.x documentation][old-release- [3.4.5-milestone]: https://github.com/tomchristie/django-rest-framework/issues?q=milestone%3A%223.4.5+Release%22 [3.4.6-milestone]: https://github.com/tomchristie/django-rest-framework/issues?q=milestone%3A%223.4.6+Release%22 [3.4.7-milestone]: https://github.com/tomchristie/django-rest-framework/issues?q=milestone%3A%223.4.7+Release%22 +[3.5.0-milestone]: https://github.com/tomchristie/django-rest-framework/issues?q=milestone%3A%223.5.0+Release%22 [gh2013]: https://github.com/tomchristie/django-rest-framework/issues/2013 diff --git a/mkdocs.yml b/mkdocs.yml index 0b89988b1..01c59caaa 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -66,6 +66,7 @@ pages: - '3.2 Announcement': 'topics/3.2-announcement.md' - '3.3 Announcement': 'topics/3.3-announcement.md' - '3.4 Announcement': 'topics/3.4-announcement.md' + - '3.5 Announcement': 'topics/3.5-announcement.md' - 'Kickstarter Announcement': 'topics/kickstarter-announcement.md' - 'Mozilla Grant': 'topics/mozilla-grant.md' - 'Funding': 'topics/funding.md' From c6f1686571026d1f2f3bb6bf9d73ca9a61b93182 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Thu, 20 Oct 2016 16:26:56 +0100 Subject: [PATCH 006/190] Remove erronous file [ci skip] --- schema-support | 0 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 schema-support diff --git a/schema-support b/schema-support deleted file mode 100644 index e69de29bb..000000000 From 1aa6dff0b54e39d1fe7a0c7058f6f74ee01475fd Mon Sep 17 00:00:00 2001 From: Maxime Lorant Date: Thu, 20 Oct 2016 17:47:59 +0200 Subject: [PATCH 007/190] Fix code formatting missing in 3.5 announcement (#4597) ... in section ModelSerializer 'fields' and 'exclude' --- docs/topics/3.5-announcement.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/topics/3.5-announcement.md b/docs/topics/3.5-announcement.md index 2ed8adf8e..ea50b2418 100644 --- a/docs/topics/3.5-announcement.md +++ b/docs/topics/3.5-announcement.md @@ -246,7 +246,7 @@ be deprecated at a later date. ### ModelSerializer 'fields' and 'exclude' ModelSerializer and HyperlinkedModelSerializer must include either a fields -option, or an exclude option. The fields = '__all__' shortcut may be used to +option, or an exclude option. The `fields = '__all__'` shortcut may be used to explicitly include all fields. Failing to set either `fields` or `exclude` raised a pending deprecation warning From 856f086ce35e1b8e2abda16c860371543a8b12f2 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Fri, 21 Oct 2016 14:42:42 +0100 Subject: [PATCH 008/190] Remove broken wheel check in setup.py [ci skip] --- setup.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/setup.py b/setup.py index 86870489f..ca62366ed 100755 --- a/setup.py +++ b/setup.py @@ -61,9 +61,6 @@ if sys.argv[-1] == 'publish': import pypandoc except ImportError: print("pypandoc not installed.\nUse `pip install pypandoc`.\nExiting.") - if os.system("pip freeze | grep wheel"): - print("wheel not installed.\nUse `pip install wheel`.\nExiting.") - sys.exit() if os.system("pip freeze | grep twine"): print("twine not installed.\nUse `pip install twine`.\nExiting.") sys.exit() From e3686aca93a8224280b38b7669342bcadb24176e Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Fri, 21 Oct 2016 14:47:26 +0100 Subject: [PATCH 009/190] Don't use bare 'raise'. [ci skip] --- rest_framework/views.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rest_framework/views.py b/rest_framework/views.py index a8710c7a0..07575fba7 100644 --- a/rest_framework/views.py +++ b/rest_framework/views.py @@ -445,7 +445,7 @@ class APIView(View): renderer_format = getattr(request.accepted_renderer, 'format') use_plaintext_traceback = renderer_format not in ('html', 'api', 'admin') request.force_plaintext_errors(use_plaintext_traceback) - raise + raise exc # Note: Views are made CSRF exempt from within `as_view` as to prevent # accidental removal of this exemption in cases where `dispatch` needs to From 0b346e94b1ed3016e79dd39be3ee48691cecb035 Mon Sep 17 00:00:00 2001 From: Lukasz Karolewski Date: Fri, 21 Oct 2016 07:00:25 -0700 Subject: [PATCH 010/190] changing order of imports (#4601) when using with django-filter and rest_framework_swagger need to import coreapi before django-filter as django filter tries to load rest_framework.coreapi which is undefined at this point --- rest_framework/compat.py | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/rest_framework/compat.py b/rest_framework/compat.py index 7ec39ba63..2d6e7843c 100644 --- a/rest_framework/compat.py +++ b/rest_framework/compat.py @@ -170,6 +170,16 @@ except ImportError: JSONField = None +# coreapi is optional (Note that uritemplate is a dependency of coreapi) +try: + import coreapi + import uritemplate +except (ImportError, SyntaxError): + # SyntaxError is possible under python 3.2 + coreapi = None + uritemplate = None + + # django-filter is optional try: import django_filters @@ -184,16 +194,6 @@ except ImportError: crispy_forms = None -# coreapi is optional (Note that uritemplate is a dependency of coreapi) -try: - import coreapi - import uritemplate -except (ImportError, SyntaxError): - # SyntaxError is possible under python 3.2 - coreapi = None - uritemplate = None - - # requests is optional try: import requests From f1bdce17b547859a6d59e5c0fe85b9c46d061f44 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Fri, 21 Oct 2016 15:21:23 +0100 Subject: [PATCH 011/190] Fix for case of ListSerializer with single item (#4609) --- rest_framework/serializers.py | 4 ++-- tests/test_serializer.py | 13 +++++++++++++ 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index 098c3cd23..39987cd07 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -507,7 +507,7 @@ class Serializer(BaseSerializer): @property def errors(self): ret = super(Serializer, self).errors - if isinstance(ret, list) and len(ret) == 1 and ret[0].code == 'null': + if isinstance(ret, list) and len(ret) == 1 and getattr(ret[0], 'code', None) == 'null': # Edge case. Provide a more descriptive error than # "this field may not be null", when no data is passed. detail = ErrorDetail('No data provided', code='null') @@ -705,7 +705,7 @@ class ListSerializer(BaseSerializer): @property def errors(self): ret = super(ListSerializer, self).errors - if isinstance(ret, list) and len(ret) == 1 and ret[0].code == 'null': + if isinstance(ret, list) and len(ret) == 1 and getattr(ret[0], 'code', None) == 'null': # Edge case. Provide a more descriptive error than # "this field may not be null", when no data is passed. detail = ErrorDetail('No data provided', code='null') diff --git a/tests/test_serializer.py b/tests/test_serializer.py index a2817f6a4..32be39faa 100644 --- a/tests/test_serializer.py +++ b/tests/test_serializer.py @@ -357,3 +357,16 @@ class TestSerializerValidationWithCompiledRegexField: assert serializer.is_valid() assert serializer.validated_data == {'name': '2'} assert serializer.errors == {} + + +class Test4606Regression: + def setup(self): + class ExampleSerializer(serializers.Serializer): + name = serializers.CharField(required=True) + choices = serializers.CharField(required=True) + self.Serializer = ExampleSerializer + + def test_4606_regression(self): + serializer = self.Serializer(data=[{"name": "liz"}], many=True) + with pytest.raises(serializers.ValidationError): + serializer.is_valid(raise_exception=True) From d647d37a99df3673000f17f7d565a8dab0770805 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Fri, 21 Oct 2016 15:45:28 +0100 Subject: [PATCH 012/190] Fix Accept header in tutorial. Closes #4604. [ci skip] --- docs/tutorial/7-schemas-and-client-libraries.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/tutorial/7-schemas-and-client-libraries.md b/docs/tutorial/7-schemas-and-client-libraries.md index 705b79da6..eb1982955 100644 --- a/docs/tutorial/7-schemas-and-client-libraries.md +++ b/docs/tutorial/7-schemas-and-client-libraries.md @@ -53,10 +53,10 @@ representation become available as an option. We can also request the schema from the command line, by specifying the desired content type in the `Accept` header. - $ http http://127.0.0.1:8000/schema/ Accept:application/vnd.coreapi+json + $ http http://127.0.0.1:8000/schema/ Accept:application/coreapi+json HTTP/1.0 200 OK Allow: GET, HEAD, OPTIONS - Content-Type: application/vnd.coreapi+json + Content-Type: application/coreapi+json { "_meta": { From 0fe0e1e7038e74f071e93a77bd5900dc191b086d Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Fri, 21 Oct 2016 16:59:34 +0100 Subject: [PATCH 013/190] Fix schema base paths (#4611) --- rest_framework/schemas.py | 15 +++++++++++++-- tests/test_schemas.py | 11 +++++++++++ 2 files changed, 24 insertions(+), 2 deletions(-) diff --git a/rest_framework/schemas.py b/rest_framework/schemas.py index af861426c..9b9984699 100644 --- a/rest_framework/schemas.py +++ b/rest_framework/schemas.py @@ -1,4 +1,3 @@ -import os import re from collections import OrderedDict from importlib import import_module @@ -37,6 +36,18 @@ types_lookup = ClassLookupDict({ }) +def common_path(paths): + split_paths = [path.strip('/').split('/') for path in paths] + s1 = min(split_paths) + s2 = max(split_paths) + common = s1 + for i, c in enumerate(s1): + if c != s2[i]: + common = s1[:i] + break + return '/' + '/'.join(common) + + def get_pk_name(model): meta = model._meta.concrete_model._meta return _get_pk(meta).name @@ -292,7 +303,7 @@ class SchemaGenerator(object): # one URL that doesn't have a path prefix. return '/' prefixes.append('/' + prefix + '/') - return os.path.commonprefix(prefixes) + return common_path(prefixes) def create_view(self, callback, method, request=None): """ diff --git a/tests/test_schemas.py b/tests/test_schemas.py index 7188087c4..80b456ea0 100644 --- a/tests/test_schemas.py +++ b/tests/test_schemas.py @@ -335,3 +335,14 @@ class TestSchemaGeneratorNotAtRoot(TestCase): } ) self.assertEqual(schema, expected) + + +@unittest.skipUnless(coreapi, 'coreapi is not installed') +class Test4605Regression(TestCase): + def test_4605_regression(self): + generator = SchemaGenerator() + prefix = generator.determine_path_prefix([ + '/api/v1/items/', + '/auth/convert-token/' + ]) + assert prefix == '/' From 30bf9df5d0dc983d180000e413ec2254d7946ff8 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Fri, 21 Oct 2016 16:59:43 +0100 Subject: [PATCH 014/190] Fix guardian import (#4612) --- rest_framework/compat.py | 1 - rest_framework/filters.py | 7 ++++++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/rest_framework/compat.py b/rest_framework/compat.py index 2d6e7843c..b0e076203 100644 --- a/rest_framework/compat.py +++ b/rest_framework/compat.py @@ -207,7 +207,6 @@ guardian = None try: if 'guardian' in settings.INSTALLED_APPS: import guardian - import guardian.shortcuts # Fixes #1624 except ImportError: pass diff --git a/rest_framework/filters.py b/rest_framework/filters.py index f55297b39..47d9a0342 100644 --- a/rest_framework/filters.py +++ b/rest_framework/filters.py @@ -289,6 +289,11 @@ class DjangoObjectPermissionsFilter(BaseFilterBackend): perm_format = '%(app_label)s.view_%(model_name)s' def filter_queryset(self, request, queryset, view): + # We want to defer this import until run-time, rather than import-time. + # See https://github.com/tomchristie/django-rest-framework/issues/4608 + # (Also see #1624 for why we need to make this import explicitly) + from guardian.shortcuts import get_objects_for_user + extra = {} user = request.user model_cls = queryset.model @@ -302,4 +307,4 @@ class DjangoObjectPermissionsFilter(BaseFilterBackend): extra = {'accept_global_perms': False} else: extra = {} - return guardian.shortcuts.get_objects_for_user(user, permission, queryset, **extra) + return get_objects_for_user(user, permission, queryset, **extra) From 3b39d2d13a227b1f159663530ad754b183b7225c Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Fri, 21 Oct 2016 17:10:38 +0100 Subject: [PATCH 015/190] Version 3.5.1 [ci skip] --- docs/topics/release-notes.md | 21 +++++++++++++++++++++ rest_framework/__init__.py | 2 +- 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/docs/topics/release-notes.md b/docs/topics/release-notes.md index 30244f6d2..3d3935684 100644 --- a/docs/topics/release-notes.md +++ b/docs/topics/release-notes.md @@ -40,6 +40,15 @@ You can determine your currently installed version using `pip freeze`: ## 3.5.x series +### 3.5.1 + +**Date**: [21st October 2016][3.5.1-milestone] + +* Make `rest_framework/compat.py` imports. ([#4612][gh4612], [#4608][gh4608], [#4601][gh4601]) +* Fix bug in schema base path generation. ([#4611][gh4611], [#4605][gh4605]) +* Fix broken case of ListSerializer with single item. ([#4609][gh4609], [#4606][gh4606]) +* Remove bare `raise` for Python 3.5 compat. ([#4600][gh4600]) + ### 3.5.0 **Date**: [20th October 2016][3.5.0-milestone] @@ -605,6 +614,7 @@ For older release notes, [please see the version 2.x documentation][old-release- [3.4.6-milestone]: https://github.com/tomchristie/django-rest-framework/issues?q=milestone%3A%223.4.6+Release%22 [3.4.7-milestone]: https://github.com/tomchristie/django-rest-framework/issues?q=milestone%3A%223.4.7+Release%22 [3.5.0-milestone]: https://github.com/tomchristie/django-rest-framework/issues?q=milestone%3A%223.5.0+Release%22 +[3.5.1-milestone]: https://github.com/tomchristie/django-rest-framework/issues?q=milestone%3A%223.5.1+Release%22 [gh2013]: https://github.com/tomchristie/django-rest-framework/issues/2013 @@ -1146,3 +1156,14 @@ For older release notes, [please see the version 2.x documentation][old-release- [gh4465]: https://github.com/tomchristie/django-rest-framework/issues/4465 [gh4462]: https://github.com/tomchristie/django-rest-framework/issues/4462 [gh4458]: https://github.com/tomchristie/django-rest-framework/issues/4458 + + + +[gh4612]: https://github.com/tomchristie/django-rest-framework/issues/4612 +[gh4608]: https://github.com/tomchristie/django-rest-framework/issues/4608 +[gh4601]: https://github.com/tomchristie/django-rest-framework/issues/4601 +[gh4611]: https://github.com/tomchristie/django-rest-framework/issues/4611 +[gh4605]: https://github.com/tomchristie/django-rest-framework/issues/4605 +[gh4609]: https://github.com/tomchristie/django-rest-framework/issues/4609 +[gh4606]: https://github.com/tomchristie/django-rest-framework/issues/4606 +[gh4600]: https://github.com/tomchristie/django-rest-framework/issues/4600 diff --git a/rest_framework/__init__.py b/rest_framework/__init__.py index 68e96703f..0a520bf80 100644 --- a/rest_framework/__init__.py +++ b/rest_framework/__init__.py @@ -8,7 +8,7 @@ ______ _____ _____ _____ __ """ __title__ = 'Django REST framework' -__version__ = '3.5.0' +__version__ = '3.5.1' __author__ = 'Tom Christie' __license__ = 'BSD 2-Clause' __copyright__ = 'Copyright 2011-2016 Tom Christie' From 8ac524915ca3759d7728206da101b63c6f33b6d3 Mon Sep 17 00:00:00 2001 From: Mads Jensen Date: Sat, 22 Oct 2016 17:37:23 +0200 Subject: [PATCH 016/190] added on_delete=models.CASCADE to models.ForeignKey in the documentation (#4614) --- docs/api-guide/relations.md | 6 +++--- docs/tutorial/4-authentication-and-permissions.md | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/api-guide/relations.md b/docs/api-guide/relations.md index aeefae8b1..aabe49412 100644 --- a/docs/api-guide/relations.md +++ b/docs/api-guide/relations.md @@ -39,7 +39,7 @@ In order to explain the various types of relational fields, we'll use a couple o artist = models.CharField(max_length=100) class Track(models.Model): - album = models.ForeignKey(Album, related_name='tracks') + album = models.ForeignKey(Album, related_name='tracks', on_delete=models.CASCADE) order = models.IntegerField() title = models.CharField(max_length=100) duration = models.IntegerField() @@ -484,7 +484,7 @@ Note that reverse relationships are not automatically included by the `ModelSeri You'll normally want to ensure that you've set an appropriate `related_name` argument on the relationship, that you can use as the field name. For example: class Track(models.Model): - album = models.ForeignKey(Album, related_name='tracks') + album = models.ForeignKey(Album, related_name='tracks', on_delete=models.CASCADE) ... If you have not set a related name for the reverse relationship, you'll need to use the automatically generated related name in the `fields` argument. For example: @@ -508,7 +508,7 @@ For example, given the following model for a tag, which has a generic relationsh See: https://docs.djangoproject.com/en/dev/ref/contrib/contenttypes/ """ tag_name = models.SlugField() - content_type = models.ForeignKey(ContentType) + content_type = models.ForeignKey(ContentType, on_delete=models.CASCADE) object_id = models.PositiveIntegerField() tagged_object = GenericForeignKey('content_type', 'object_id') diff --git a/docs/tutorial/4-authentication-and-permissions.md b/docs/tutorial/4-authentication-and-permissions.md index 098194c29..43ccf9186 100644 --- a/docs/tutorial/4-authentication-and-permissions.md +++ b/docs/tutorial/4-authentication-and-permissions.md @@ -14,7 +14,7 @@ First, let's add a couple of fields. One of those fields will be used to repres Add the following two fields to the `Snippet` model in `models.py`. - owner = models.ForeignKey('auth.User', related_name='snippets') + owner = models.ForeignKey('auth.User', related_name='snippets', on_delete=models.CASCADE) highlighted = models.TextField() We'd also need to make sure that when the model is saved, that we populate the highlighted field, using the `pygments` code highlighting library. From 72dc6d1d5cae32353e87b7724e666afbd8f04312 Mon Sep 17 00:00:00 2001 From: Phil Krylov Date: Sun, 23 Oct 2016 04:36:36 +0300 Subject: [PATCH 017/190] Add `drf-proxy-pagination` reference to Pagination docs --- docs/api-guide/pagination.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/docs/api-guide/pagination.md b/docs/api-guide/pagination.md index f990128c5..f82614eca 100644 --- a/docs/api-guide/pagination.md +++ b/docs/api-guide/pagination.md @@ -321,9 +321,14 @@ The following third party packages are also available. The [`DRF-extensions` package][drf-extensions] includes a [`PaginateByMaxMixin` mixin class][paginate-by-max-mixin] that allows your API clients to specify `?page_size=max` to obtain the maximum allowed page size. +## drf-proxy-pagination + +The [`drf-proxy-pagination` package][drf-proxy-pagination] includes a `ProxyPagination` class which allows to choose pagination class with a query parameter. + [cite]: https://docs.djangoproject.com/en/dev/topics/pagination/ [github-link-pagination]: https://developer.github.com/guides/traversing-with-pagination/ [link-header]: ../img/link-header-pagination.png [drf-extensions]: http://chibisov.github.io/drf-extensions/docs/ [paginate-by-max-mixin]: http://chibisov.github.io/drf-extensions/docs/#paginatebymaxmixin +[drf-proxy-pagination]: https://github.com/tuffnatty/drf-proxy-pagination [disqus-cursor-api]: http://cramer.io/2011/03/08/building-cursors-for-the-disqus-api From eafc9a2393139445815feafe0392c170f1172532 Mon Sep 17 00:00:00 2001 From: Ryan P Kilby Date: Tue, 25 Oct 2016 15:47:24 -0400 Subject: [PATCH 018/190] Fix is_simple_callable with variable args, kwargs (#4622) --- rest_framework/fields.py | 9 +++++++-- tests/test_fields.py | 23 +++++++++++++++++++++++ 2 files changed, 30 insertions(+), 2 deletions(-) diff --git a/rest_framework/fields.py b/rest_framework/fields.py index e48285005..f75fcfe05 100644 --- a/rest_framework/fields.py +++ b/rest_framework/fields.py @@ -54,12 +54,17 @@ if six.PY3: """ True if the object is a callable that takes no arguments. """ - if not callable(obj): + if not (inspect.isfunction(obj) or inspect.ismethod(obj)): return False sig = inspect.signature(obj) params = sig.parameters.values() - return all(param.default != param.empty for param in params) + return all( + param.kind == param.VAR_POSITIONAL or + param.kind == param.VAR_KEYWORD or + param.default != param.empty + for param in params + ) else: def is_simple_callable(obj): diff --git a/tests/test_fields.py b/tests/test_fields.py index 6fea249ba..92030e3ca 100644 --- a/tests/test_fields.py +++ b/tests/test_fields.py @@ -37,6 +37,9 @@ class TestIsSimpleCallable: def valid_kwargs(self, param='value'): pass + def valid_vargs_kwargs(self, *args, **kwargs): + pass + def invalid(self, param): pass @@ -45,11 +48,13 @@ class TestIsSimpleCallable: # unbound methods assert not is_simple_callable(Foo.valid) assert not is_simple_callable(Foo.valid_kwargs) + assert not is_simple_callable(Foo.valid_vargs_kwargs) assert not is_simple_callable(Foo.invalid) # bound methods assert is_simple_callable(Foo().valid) assert is_simple_callable(Foo().valid_kwargs) + assert is_simple_callable(Foo().valid_vargs_kwargs) assert not is_simple_callable(Foo().invalid) def test_function(self): @@ -59,13 +64,31 @@ class TestIsSimpleCallable: def valid(param='value', param2='value'): pass + def valid_vargs_kwargs(*args, **kwargs): + pass + def invalid(param, param2='value'): pass assert is_simple_callable(simple) assert is_simple_callable(valid) + assert is_simple_callable(valid_vargs_kwargs) assert not is_simple_callable(invalid) + def test_4602_regression(self): + from django.db import models + + class ChoiceModel(models.Model): + choice_field = models.CharField( + max_length=1, default='a', + choices=(('a', 'A'), ('b', 'B')), + ) + + class Meta: + app_label = 'tests' + + assert is_simple_callable(ChoiceModel().get_choice_field_display) + @unittest.skipUnless(typings, 'requires python 3.5') def test_type_annotation(self): # The annotation will otherwise raise a syntax error in python < 3.5 From 46f837a9d13d5f02fabfa49aef33c16c561a68cf Mon Sep 17 00:00:00 2001 From: Josep Cugat Date: Fri, 28 Oct 2016 13:05:32 +0200 Subject: [PATCH 019/190] Fix APIException full_details() typo in documentation (#4633) APIException has a get_full_details() method but the documentation refers to full_details(). --- docs/api-guide/exceptions.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/api-guide/exceptions.md b/docs/api-guide/exceptions.md index f0f178d92..df8cad42d 100644 --- a/docs/api-guide/exceptions.md +++ b/docs/api-guide/exceptions.md @@ -119,7 +119,7 @@ The available attributes and methods are: * `.detail` - Return the textual description of the error. * `.get_codes()` - Return the code identifier of the error. -* `.full_details()` - Return both the textual description and the code identifier. +* `.get_full_details()` - Return both the textual description and the code identifier. In most cases the error detail will be a simple item: @@ -127,7 +127,7 @@ In most cases the error detail will be a simple item: You do not have permission to perform this action. >>> print(exc.get_codes()) permission_denied - >>> print(exc.full_details()) + >>> print(exc.get_full_details()) {'message':'You do not have permission to perform this action.','code':'permission_denied'} In the case of validation errors the error detail will be either a list or From 895c67c9a22eff1057703a8c578c4833f63a9d70 Mon Sep 17 00:00:00 2001 From: Alex Kahan Date: Mon, 31 Oct 2016 16:41:54 -0400 Subject: [PATCH 020/190] Fixes #4532 (#4636) --- .../rest_framework/admin/dict_value.html | 11 +++ tests/test_templatetags.py | 75 +++++++++++++++---- 2 files changed, 73 insertions(+), 13 deletions(-) diff --git a/rest_framework/templates/rest_framework/admin/dict_value.html b/rest_framework/templates/rest_framework/admin/dict_value.html index e69de29bb..3392c901b 100644 --- a/rest_framework/templates/rest_framework/admin/dict_value.html +++ b/rest_framework/templates/rest_framework/admin/dict_value.html @@ -0,0 +1,11 @@ +{% load rest_framework %} + + + {% for key, value in value.items %} + + + + + {% endfor %} + +
{{ key|format_value }}{{ value|format_value }}
diff --git a/tests/test_templatetags.py b/tests/test_templatetags.py index 28390320b..cac1abf50 100644 --- a/tests/test_templatetags.py +++ b/tests/test_templatetags.py @@ -41,6 +41,9 @@ class TemplateTagTests(TestCase): self.assertEqual(format_value(None), 'null') def test_format_value_hyperlink(self): + """ + Tests format_value with a URL + """ url = 'http://url.com' name = 'name_of_url' hyperlink = Hyperlink(url, name) @@ -54,6 +57,25 @@ class TemplateTagTests(TestCase): self.assertEqual(format_value(list_items), '\n item1, item2, item3\n') self.assertEqual(format_value([]), '\n\n') + def test_format_value_dict(self): + """ + Tests format_value with a dict + """ + test_dict = {'a': 'b'} + expected_dict_format = """ + + + + + + + +
ab
""" + self.assertEqual( + format_html(format_value(test_dict)), + format_html(expected_dict_format) + ) + def test_format_value_table(self): """ Tests format_value with a list of lists/dicts @@ -84,20 +106,47 @@ class TemplateTagTests(TestCase): expected_dict_format = """ - - 0 - - - - 1 - - - - 2 - - + + 0 + + + + + item1 + value1 + + + + + + + 1 + + + + + item2 + value2 + + + + + + + 2 + + + + + item3 + value3 + + + + + - """ + """ list_of_dicts = [{'item1': 'value1'}, {'item2': 'value2'}, {'item3': 'value3'}] self.assertEqual( From 7eb6cdca0080e0f321e660c48fa1d4ac5a6e7dab Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Tue, 1 Nov 2016 10:22:30 +0000 Subject: [PATCH 021/190] Don't lose exception info (#4638) --- rest_framework/views.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rest_framework/views.py b/rest_framework/views.py index 07575fba7..a8710c7a0 100644 --- a/rest_framework/views.py +++ b/rest_framework/views.py @@ -445,7 +445,7 @@ class APIView(View): renderer_format = getattr(request.accepted_renderer, 'format') use_plaintext_traceback = renderer_format not in ('html', 'api', 'admin') request.force_plaintext_errors(use_plaintext_traceback) - raise exc + raise # Note: Views are made CSRF exempt from within `as_view` as to prevent # accidental removal of this exemption in cases where `dispatch` needs to From 5c54b227c11688f12ce17f8aa9575e4ab52463d3 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Tue, 1 Nov 2016 10:24:53 +0000 Subject: [PATCH 022/190] Drop redundant requests adapter (#4639) --- rest_framework/test.py | 9 --------- 1 file changed, 9 deletions(-) diff --git a/rest_framework/test.py b/rest_framework/test.py index 16b1b4cd5..241f94c91 100644 --- a/rest_framework/test.py +++ b/rest_framework/test.py @@ -106,15 +106,6 @@ if requests is not None: def close(self): pass - class NoExternalRequestsAdapter(requests.adapters.HTTPAdapter): - def send(self, request, *args, **kwargs): - msg = ( - 'RequestsClient refusing to make an outgoing network request ' - 'to "%s". Only "testserver" or hostnames in your ALLOWED_HOSTS ' - 'setting are valid.' % request.url - ) - raise RuntimeError(msg) - class RequestsClient(requests.Session): def __init__(self, *args, **kwargs): super(RequestsClient, self).__init__(*args, **kwargs) From d92b24a0b74f681a538f6faff59bb00c6cd447e2 Mon Sep 17 00:00:00 2001 From: Ryan P Kilby Date: Tue, 1 Nov 2016 06:27:11 -0400 Subject: [PATCH 023/190] Make serializer fields import explicit (#4628) --- rest_framework/serializers.py | 29 +++++++++++++++++++--- tests/test_serializer.py | 45 ++++++++++++++++++++++++++++++++++- 2 files changed, 70 insertions(+), 4 deletions(-) diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index 39987cd07..1bdcd12c3 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -12,18 +12,27 @@ response content is handled by parsers and renderers. """ from __future__ import unicode_literals +import copy +import inspect import traceback +from collections import OrderedDict +from django.core.exceptions import ValidationError as DjangoValidationError +from django.core.exceptions import ImproperlyConfigured from django.db import models from django.db.models import DurationField as ModelDurationField from django.db.models.fields import Field as DjangoModelField from django.db.models.fields import FieldDoesNotExist +from django.utils import six, timezone from django.utils.functional import cached_property from django.utils.translation import ugettext_lazy as _ from rest_framework.compat import JSONField as ModelJSONField from rest_framework.compat import postgres_fields, set_many, unicode_to_repr -from rest_framework.utils import model_meta +from rest_framework.exceptions import ErrorDetail, ValidationError +from rest_framework.fields import get_error_detail, set_value +from rest_framework.settings import api_settings +from rest_framework.utils import html, model_meta, representation from rest_framework.utils.field_mapping import ( ClassLookupDict, get_field_kwargs, get_nested_relation_kwargs, get_relation_kwargs, get_url_kwargs @@ -42,9 +51,23 @@ from rest_framework.validators import ( # # This helps keep the separation between model fields, form fields, and # serializer fields more explicit. +from rest_framework.fields import ( # NOQA # isort:skip + BooleanField, CharField, ChoiceField, DateField, DateTimeField, DecimalField, + DictField, DurationField, EmailField, Field, FileField, FilePathField, FloatField, + HiddenField, IPAddressField, ImageField, IntegerField, JSONField, ListField, + ModelField, MultipleChoiceField, NullBooleanField, ReadOnlyField, RegexField, + SerializerMethodField, SlugField, TimeField, URLField, UUIDField, +) +from rest_framework.relations import ( # NOQA # isort:skip + HyperlinkedIdentityField, HyperlinkedRelatedField, ManyRelatedField, + PrimaryKeyRelatedField, RelatedField, SlugRelatedField, StringRelatedField, +) -from rest_framework.fields import * # NOQA # isort:skip -from rest_framework.relations import * # NOQA # isort:skip +# Non-field imports, but public API +from rest_framework.fields import ( # NOQA # isort:skip + CreateOnlyDefault, CurrentUserDefault, SkipField, empty +) +from rest_framework.relations import Hyperlink, PKOnlyObject # NOQA # isort:skip # We assume that 'validators' are intended for the child serializer, # rather than the parent serializer. diff --git a/tests/test_serializer.py b/tests/test_serializer.py index 32be39faa..8c8b5b163 100644 --- a/tests/test_serializer.py +++ b/tests/test_serializer.py @@ -1,17 +1,60 @@ # coding: utf-8 from __future__ import unicode_literals +import inspect import pickle import re import pytest -from rest_framework import serializers +from rest_framework import fields, relations, serializers from rest_framework.compat import unicode_repr +from rest_framework.fields import Field from .utils import MockObject +# Test serializer fields imports. +# ------------------------------- + +class TestFieldImports: + def is_field(self, name, value): + return ( + isinstance(value, type) and + issubclass(value, Field) and + not name.startswith('_') + ) + + def test_fields(self): + msg = "Expected `fields.%s` to be imported in `serializers`" + field_classes = [ + key for key, value + in inspect.getmembers(fields) + if self.is_field(key, value) + ] + + # sanity check + assert 'Field' in field_classes + assert 'BooleanField' in field_classes + + for field in field_classes: + assert hasattr(serializers, field), msg % field + + def test_relations(self): + msg = "Expected `relations.%s` to be imported in `serializers`" + field_classes = [ + key for key, value + in inspect.getmembers(relations) + if self.is_field(key, value) + ] + + # sanity check + assert 'RelatedField' in field_classes + + for field in field_classes: + assert hasattr(serializers, field), msg % field + + # Tests for core functionality. # ----------------------------- From 98df932194722d6fc81becedc55eb695a32f925f Mon Sep 17 00:00:00 2001 From: Kieran Spear Date: Tue, 1 Nov 2016 18:30:17 +0800 Subject: [PATCH 024/190] Fix FilterSet proxy (#4620) --- rest_framework/filters.py | 35 ++++++++++++++++++++++++++--------- tests/test_filters.py | 23 +++++++++++++++++++++++ 2 files changed, 49 insertions(+), 9 deletions(-) diff --git a/rest_framework/filters.py b/rest_framework/filters.py index 47d9a0342..531531efc 100644 --- a/rest_framework/filters.py +++ b/rest_framework/filters.py @@ -37,15 +37,32 @@ class BaseFilterBackend(object): return [] -class FilterSet(object): - def __new__(cls, *args, **kwargs): - warnings.warn( - "The built in 'rest_framework.filters.FilterSet' is pending deprecation. " - "You should use 'django_filters.rest_framework.FilterSet' instead.", - PendingDeprecationWarning - ) - from django_filters.rest_framework import FilterSet - return FilterSet(*args, **kwargs) +if django_filters: + from django_filters.filterset import FilterSetMetaclass as DFFilterSetMetaclass + from django_filters.rest_framework.filterset import FilterSet as DFFilterSet + + class FilterSetMetaclass(DFFilterSetMetaclass): + def __new__(cls, name, bases, attrs): + warnings.warn( + "The built in 'rest_framework.filters.FilterSet' is pending deprecation. " + "You should use 'django_filters.rest_framework.FilterSet' instead.", + PendingDeprecationWarning + ) + return super(FilterSetMetaclass, cls).__new__(cls, name, bases, attrs) + _BaseFilterSet = DFFilterSet +else: + # Dummy metaclass just so we can give a user-friendly error message. + class FilterSetMetaclass(type): + def __init__(self, name, bases, attrs): + # Assert only on subclasses, so we can define FilterSet below. + if bases != (object,): + assert False, 'django-filter must be installed to use the `FilterSet` class' + super(FilterSetMetaclass, self).__init__(name, bases, attrs) + _BaseFilterSet = object + + +class FilterSet(six.with_metaclass(FilterSetMetaclass, _BaseFilterSet)): + pass class DjangoFilterBackend(BaseFilterBackend): diff --git a/tests/test_filters.py b/tests/test_filters.py index 9795230d6..12fb85895 100644 --- a/tests/test_filters.py +++ b/tests/test_filters.py @@ -79,12 +79,23 @@ if django_filters: model = BaseFilterableItem fields = '__all__' + # Test the same filter using the deprecated internal FilterSet class. + class BaseFilterableItemFilterWithProxy(filters.FilterSet): + text = django_filters.CharFilter() + + class Meta: + model = BaseFilterableItem + fields = '__all__' + class BaseFilterableItemFilterRootView(generics.ListCreateAPIView): queryset = FilterableItem.objects.all() serializer_class = FilterableItemSerializer filter_class = BaseFilterableItemFilter filter_backends = (filters.DjangoFilterBackend,) + class BaseFilterableItemFilterWithProxyRootView(BaseFilterableItemFilterRootView): + filter_class = BaseFilterableItemFilterWithProxy + # Regression test for #814 class FilterFieldsQuerysetView(generics.ListCreateAPIView): queryset = FilterableItem.objects.all() @@ -296,6 +307,18 @@ class IntegrationTestFiltering(CommonFilteringTestCase): self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual(len(response.data), 1) + @unittest.skipUnless(django_filters, 'django-filter not installed') + def test_base_model_filter_with_proxy(self): + """ + The `get_filter_class` model checks should allow base model filters. + """ + view = BaseFilterableItemFilterWithProxyRootView.as_view() + + request = factory.get('/?text=aaa') + response = view(request).render() + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(len(response.data), 1) + @unittest.skipUnless(django_filters, 'django-filter not installed') def test_unknown_filter(self): """ From 97d848413e5e13e9de83b7e840fc719b9ec0d9c7 Mon Sep 17 00:00:00 2001 From: Nicolas Delaby Date: Tue, 1 Nov 2016 11:38:56 +0100 Subject: [PATCH 025/190] Fix support of get_full_details() for Throttled exceptions (#4627) Since `str` objects are immutable, appending to existing `str` creates in fact a new `str` instance. Thus `ErrorDetail.detail.code` attribute is lost after `str` concatenation operation. --- rest_framework/exceptions.py | 20 ++++++++++---------- tests/test_exceptions.py | 20 +++++++++++++++++++- 2 files changed, 29 insertions(+), 11 deletions(-) diff --git a/rest_framework/exceptions.py b/rest_framework/exceptions.py index e41655fef..e84074a07 100644 --- a/rest_framework/exceptions.py +++ b/rest_framework/exceptions.py @@ -210,14 +210,14 @@ class Throttled(APIException): default_code = 'throttled' def __init__(self, wait=None, detail=None, code=None): + if detail is None: + detail = force_text(self.default_detail) + if wait is not None: + wait = math.ceil(wait) + detail = ' '.join(( + detail, + force_text(ungettext(self.extra_detail_singular.format(wait=wait), + self.extra_detail_plural.format(wait=wait), + wait)))) + self.wait = wait super(Throttled, self).__init__(detail, code) - - if wait is None: - self.wait = None - else: - self.wait = math.ceil(wait) - self.detail += ' ' + force_text(ungettext( - self.extra_detail_singular.format(wait=self.wait), - self.extra_detail_plural.format(wait=self.wait), - self.wait - )) diff --git a/tests/test_exceptions.py b/tests/test_exceptions.py index 29703cb77..f1d172211 100644 --- a/tests/test_exceptions.py +++ b/tests/test_exceptions.py @@ -1,9 +1,12 @@ from __future__ import unicode_literals from django.test import TestCase +from django.utils import six from django.utils.translation import ugettext_lazy as _ -from rest_framework.exceptions import ErrorDetail, _get_error_details +from rest_framework.exceptions import ( + ErrorDetail, Throttled, _get_error_details +) class ExceptionTestCase(TestCase): @@ -39,3 +42,18 @@ class ExceptionTestCase(TestCase): _get_error_details([[lazy_example]])[0][0], ErrorDetail ) + + def test_get_full_details_with_throttling(self): + exception = Throttled() + assert exception.get_full_details() == { + 'message': 'Request was throttled.', 'code': 'throttled'} + + exception = Throttled(wait=2) + assert exception.get_full_details() == { + 'message': 'Request was throttled. Expected available in {} seconds.'.format(2 if six.PY3 else 2.), + 'code': 'throttled'} + + exception = Throttled(wait=2, detail='Slow down!') + assert exception.get_full_details() == { + 'message': 'Slow down! Expected available in {} seconds.'.format(2 if six.PY3 else 2.), + 'code': 'throttled'} From 70385711572e7ea141644f349b7180c68b4c15d2 Mon Sep 17 00:00:00 2001 From: Kennedy Mwenja Date: Tue, 1 Nov 2016 13:42:01 +0300 Subject: [PATCH 026/190] Enable cursor pagination of value querysets. (#4569) To do `GROUP_BY` queries in django requires one to use `.values()` eg this groups posts by user getting a count of posts per user. ``` Posts.objects.order_by('user').values('user').annotate(post_count=Count('post')) ``` This would produce a value queryset which serializes its result objects as dictionaries while `CursorPagination` requires a queryset with result objects that are model instances. This commit enables cursor pagination for value querysets. - had to mangle the tests a bit to test it out. They might need some refactoring. - tried the same for `.values_list()` but it turned out to be trickier than I expected since you have to use tuple indexes. --- rest_framework/pagination.py | 6 +- tests/test_pagination.py | 221 ++++++++++++++++++++++------------- 2 files changed, 147 insertions(+), 80 deletions(-) diff --git a/rest_framework/pagination.py b/rest_framework/pagination.py index 708da29cd..8ccdc342c 100644 --- a/rest_framework/pagination.py +++ b/rest_framework/pagination.py @@ -711,7 +711,11 @@ class CursorPagination(BasePagination): return replace_query_param(self.base_url, self.cursor_query_param, encoded) def _get_position_from_instance(self, instance, ordering): - attr = getattr(instance, ordering[0].lstrip('-')) + field_name = ordering[0].lstrip('-') + if isinstance(instance, dict): + attr = instance[field_name] + else: + attr = getattr(instance, field_name) return six.text_type(attr) def get_paginated_response(self, data): diff --git a/tests/test_pagination.py b/tests/test_pagination.py index 170d95899..9f2e1c57c 100644 --- a/tests/test_pagination.py +++ b/tests/test_pagination.py @@ -3,6 +3,8 @@ from __future__ import unicode_literals import pytest from django.core.paginator import Paginator as DjangoPaginator +from django.db import models +from django.test import TestCase from rest_framework import ( exceptions, filters, generics, pagination, serializers, status @@ -530,85 +532,7 @@ class TestLimitOffset: assert content.get('previous') == prev_url -class TestCursorPagination: - """ - Unit tests for `pagination.CursorPagination`. - """ - - def setup(self): - class MockObject(object): - def __init__(self, idx): - self.created = idx - - class MockQuerySet(object): - def __init__(self, items): - self.items = items - - 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 > int(created__gt) - ]) - - assert created__lt is not None - return MockQuerySet([ - item for item in self.items - if item.created < int(created__lt) - ]) - - def order_by(self, *ordering): - if ordering[0].startswith('-'): - return MockQuerySet(list(reversed(self.items))) - return self - - def __getitem__(self, sliced): - return self.items[sliced] - - class ExamplePagination(pagination.CursorPagination): - page_size = 5 - ordering = 'created' - - self.pagination = ExamplePagination() - self.queryset = MockQuerySet([ - MockObject(idx) for idx in [ - 1, 1, 1, 1, 1, - 1, 2, 3, 4, 4, - 4, 4, 5, 6, 7, - 7, 7, 7, 7, 7, - 7, 7, 7, 8, 9, - 9, 9, 9, 9, 9 - ] - ]) - - def get_pages(self, url): - """ - Given a URL return a tuple of: - - (previous page, current page, next page, previous url, next url) - """ - request = Request(factory.get(url)) - queryset = self.pagination.paginate_queryset(self.queryset, request) - current = [item.created for item in queryset] - - next_url = self.pagination.get_next_link() - previous_url = self.pagination.get_previous_link() - - if next_url is not None: - request = Request(factory.get(next_url)) - queryset = self.pagination.paginate_queryset(self.queryset, request) - next = [item.created for item in queryset] - else: - next = None - - if previous_url is not None: - request = Request(factory.get(previous_url)) - queryset = self.pagination.paginate_queryset(self.queryset, request) - previous = [item.created for item in queryset] - else: - previous = None - - return (previous, current, next, previous_url, next_url) +class CursorPaginationTestsMixin: def test_invalid_cursor(self): request = Request(factory.get('/', {'cursor': '123'})) @@ -703,6 +627,145 @@ class TestCursorPagination: assert isinstance(self.pagination.to_html(), type('')) +class TestCursorPagination(CursorPaginationTestsMixin): + """ + Unit tests for `pagination.CursorPagination`. + """ + + def setup(self): + class MockObject(object): + def __init__(self, idx): + self.created = idx + + class MockQuerySet(object): + def __init__(self, items): + self.items = items + + 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 > int(created__gt) + ]) + + assert created__lt is not None + return MockQuerySet([ + item for item in self.items + if item.created < int(created__lt) + ]) + + def order_by(self, *ordering): + if ordering[0].startswith('-'): + return MockQuerySet(list(reversed(self.items))) + return self + + def __getitem__(self, sliced): + return self.items[sliced] + + class ExamplePagination(pagination.CursorPagination): + page_size = 5 + ordering = 'created' + + self.pagination = ExamplePagination() + self.queryset = MockQuerySet([ + MockObject(idx) for idx in [ + 1, 1, 1, 1, 1, + 1, 2, 3, 4, 4, + 4, 4, 5, 6, 7, + 7, 7, 7, 7, 7, + 7, 7, 7, 8, 9, + 9, 9, 9, 9, 9 + ] + ]) + + def get_pages(self, url): + """ + Given a URL return a tuple of: + + (previous page, current page, next page, previous url, next url) + """ + request = Request(factory.get(url)) + queryset = self.pagination.paginate_queryset(self.queryset, request) + current = [item.created for item in queryset] + + next_url = self.pagination.get_next_link() + previous_url = self.pagination.get_previous_link() + + if next_url is not None: + request = Request(factory.get(next_url)) + queryset = self.pagination.paginate_queryset(self.queryset, request) + next = [item.created for item in queryset] + else: + next = None + + if previous_url is not None: + request = Request(factory.get(previous_url)) + queryset = self.pagination.paginate_queryset(self.queryset, request) + previous = [item.created for item in queryset] + else: + previous = None + + return (previous, current, next, previous_url, next_url) + + +class CursorPaginationModel(models.Model): + created = models.IntegerField() + + +class TestCursorPaginationWithValueQueryset(CursorPaginationTestsMixin, TestCase): + """ + Unit tests for `pagination.CursorPagination` for value querysets. + """ + + def setUp(self): + class ExamplePagination(pagination.CursorPagination): + page_size = 5 + ordering = 'created' + + self.pagination = ExamplePagination() + data = [ + 1, 1, 1, 1, 1, + 1, 2, 3, 4, 4, + 4, 4, 5, 6, 7, + 7, 7, 7, 7, 7, + 7, 7, 7, 8, 9, + 9, 9, 9, 9, 9 + ] + for idx in data: + CursorPaginationModel.objects.create(created=idx) + + self.queryset = CursorPaginationModel.objects.values() + + def get_pages(self, url): + """ + Given a URL return a tuple of: + + (previous page, current page, next page, previous url, next url) + """ + request = Request(factory.get(url)) + queryset = self.pagination.paginate_queryset(self.queryset, request) + current = [item['created'] for item in queryset] + + next_url = self.pagination.get_next_link() + previous_url = self.pagination.get_previous_link() + + if next_url is not None: + request = Request(factory.get(next_url)) + queryset = self.pagination.paginate_queryset(self.queryset, request) + next = [item['created'] for item in queryset] + else: + next = None + + if previous_url is not None: + request = Request(factory.get(previous_url)) + queryset = self.pagination.paginate_queryset(self.queryset, request) + previous = [item['created'] for item in queryset] + else: + previous = None + + return (previous, current, next, previous_url, next_url) + + def test_get_displayed_page_numbers(): """ Test our contextual page display function. From 276ed80fd302184dddb3af01e53d43be4aef15e4 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Tue, 1 Nov 2016 11:11:34 +0000 Subject: [PATCH 027/190] Support 'on'/'off' literals with BooleanField. Closes #4624 (#4640) --- rest_framework/fields.py | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/rest_framework/fields.py b/rest_framework/fields.py index f75fcfe05..13b5145ba 100644 --- a/rest_framework/fields.py +++ b/rest_framework/fields.py @@ -644,8 +644,20 @@ class BooleanField(Field): } default_empty_html = False initial = False - TRUE_VALUES = {'t', 'T', 'true', 'True', 'TRUE', '1', 1, True} - FALSE_VALUES = {'f', 'F', 'false', 'False', 'FALSE', '0', 0, 0.0, False} + TRUE_VALUES = { + 't', 'T', + 'true', 'True', 'TRUE', + 'on', 'On', 'ON', + '1', 1, + True + } + FALSE_VALUES = { + 'f', 'F', + 'false', 'False', 'FALSE', + 'off', 'Off', 'OFF', + '0', 0, 0.0, + False + } def __init__(self, **kwargs): assert 'allow_null' not in kwargs, '`allow_null` is not a valid option. Use `NullBooleanField` instead.' From 2bf082a6236cf979d2d125f1f5a60f18509323fc Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Tue, 1 Nov 2016 11:31:20 +0000 Subject: [PATCH 028/190] Version 3.5.2 [ci skip] (#4641) --- docs/topics/release-notes.md | 31 +++++++++++++++++++++++++++++++ rest_framework/__init__.py | 2 +- 2 files changed, 32 insertions(+), 1 deletion(-) diff --git a/docs/topics/release-notes.md b/docs/topics/release-notes.md index 3d3935684..d25a46ba0 100644 --- a/docs/topics/release-notes.md +++ b/docs/topics/release-notes.md @@ -40,6 +40,20 @@ You can determine your currently installed version using `pip freeze`: ## 3.5.x series +### 3.5.2 + +**Date**: [1st November 2016][3.5.2-milestone] + +* Restore exception tracebacks in Python 2.7. ([#4631][gh4631], [#4638][gh4638]) +* Properly display dicts in the admin console. ([#4532][gh4532], [#4636][gh4636]) +* Fix is_simple_callable with variable args, kwargs. ([#4622][gh4622], [#4602][gh4602]) +* Support 'on'/'off' literals with BooleanField. ([#4640][gh4640], [#4624][gh4624]) +* Enable cursor pagination of value querysets. ([#4569][gh4569]) +* Fix support of get_full_details() for Throttled exceptions. ([#4627][gh4627]) +* Fix FilterSet proxy. ([#4620][gh4620]) +* Make serializer fields import explicit. ([#4628][gh4628]) +* Drop redundant requests adapter. ([#4639][gh4639]) + ### 3.5.1 **Date**: [21st October 2016][3.5.1-milestone] @@ -615,6 +629,7 @@ For older release notes, [please see the version 2.x documentation][old-release- [3.4.7-milestone]: https://github.com/tomchristie/django-rest-framework/issues?q=milestone%3A%223.4.7+Release%22 [3.5.0-milestone]: https://github.com/tomchristie/django-rest-framework/issues?q=milestone%3A%223.5.0+Release%22 [3.5.1-milestone]: https://github.com/tomchristie/django-rest-framework/issues?q=milestone%3A%223.5.1+Release%22 +[3.5.2-milestone]: https://github.com/tomchristie/django-rest-framework/issues?q=milestone%3A%223.5.2+Release%22 [gh2013]: https://github.com/tomchristie/django-rest-framework/issues/2013 @@ -1167,3 +1182,19 @@ For older release notes, [please see the version 2.x documentation][old-release- [gh4609]: https://github.com/tomchristie/django-rest-framework/issues/4609 [gh4606]: https://github.com/tomchristie/django-rest-framework/issues/4606 [gh4600]: https://github.com/tomchristie/django-rest-framework/issues/4600 + + + +[gh4631]: https://github.com/tomchristie/django-rest-framework/issues/4631 +[gh4638]: https://github.com/tomchristie/django-rest-framework/issues/4638 +[gh4532]: https://github.com/tomchristie/django-rest-framework/issues/4532 +[gh4636]: https://github.com/tomchristie/django-rest-framework/issues/4636 +[gh4622]: https://github.com/tomchristie/django-rest-framework/issues/4622 +[gh4602]: https://github.com/tomchristie/django-rest-framework/issues/4602 +[gh4640]: https://github.com/tomchristie/django-rest-framework/issues/4640 +[gh4624]: https://github.com/tomchristie/django-rest-framework/issues/4624 +[gh4569]: https://github.com/tomchristie/django-rest-framework/issues/4569 +[gh4627]: https://github.com/tomchristie/django-rest-framework/issues/4627 +[gh4620]: https://github.com/tomchristie/django-rest-framework/issues/4620 +[gh4628]: https://github.com/tomchristie/django-rest-framework/issues/4628 +[gh4639]: https://github.com/tomchristie/django-rest-framework/issues/4639 diff --git a/rest_framework/__init__.py b/rest_framework/__init__.py index 0a520bf80..fe82763a9 100644 --- a/rest_framework/__init__.py +++ b/rest_framework/__init__.py @@ -8,7 +8,7 @@ ______ _____ _____ _____ __ """ __title__ = 'Django REST framework' -__version__ = '3.5.1' +__version__ = '3.5.2' __author__ = 'Tom Christie' __license__ = 'BSD 2-Clause' __copyright__ = 'Copyright 2011-2016 Tom Christie' From 45e058d7bace860300d3edf537b86996aec15c33 Mon Sep 17 00:00:00 2001 From: Andrzej Pragacz Date: Wed, 2 Nov 2016 10:04:01 +0100 Subject: [PATCH 029/190] Fix unhandled Http404, PermissionDenied in schema generation (#4645) (#4646) --- rest_framework/schemas.py | 4 ++- tests/test_schemas.py | 68 ++++++++++++++++++++++++++++++++++++++- 2 files changed, 70 insertions(+), 2 deletions(-) diff --git a/rest_framework/schemas.py b/rest_framework/schemas.py index 9b9984699..9439cb691 100644 --- a/rest_framework/schemas.py +++ b/rest_framework/schemas.py @@ -4,6 +4,8 @@ from importlib import import_module from django.conf import settings from django.contrib.admindocs.views import simplify_regex +from django.core.exceptions import PermissionDenied +from django.http import Http404 from django.utils import six from django.utils.encoding import force_text, smart_text @@ -339,7 +341,7 @@ class SchemaGenerator(object): try: view.check_permissions(view.request) - except exceptions.APIException: + except (exceptions.APIException, Http404, PermissionDenied): return False return True diff --git a/tests/test_schemas.py b/tests/test_schemas.py index 80b456ea0..e196a1f61 100644 --- a/tests/test_schemas.py +++ b/tests/test_schemas.py @@ -1,17 +1,22 @@ import unittest from django.conf.urls import include, url +from django.core.exceptions import PermissionDenied +from django.http import Http404 from django.test import TestCase, override_settings from rest_framework import filters, pagination, permissions, serializers from rest_framework.compat import coreapi from rest_framework.decorators import detail_route, list_route +from rest_framework.request import Request from rest_framework.routers import DefaultRouter from rest_framework.schemas import SchemaGenerator, get_schema_view -from rest_framework.test import APIClient +from rest_framework.test import APIClient, APIRequestFactory from rest_framework.views import APIView from rest_framework.viewsets import ModelViewSet +factory = APIRequestFactory() + class MockUser(object): def is_authenticated(self): @@ -215,6 +220,32 @@ class TestRouterGeneratedSchema(TestCase): self.assertEqual(response.data, expected) +class DenyAllUsingHttp404(permissions.BasePermission): + + def has_permission(self, request, view): + raise Http404() + + def has_object_permission(self, request, view, obj): + raise Http404() + + +class DenyAllUsingPermissionDenied(permissions.BasePermission): + + def has_permission(self, request, view): + raise PermissionDenied() + + def has_object_permission(self, request, view, obj): + raise PermissionDenied() + + +class Http404ExampleViewSet(ExampleViewSet): + permission_classes = [DenyAllUsingHttp404] + + +class PermissionDeniedExampleViewSet(ExampleViewSet): + permission_classes = [DenyAllUsingPermissionDenied] + + class ExampleListView(APIView): permission_classes = [permissions.IsAuthenticatedOrReadOnly] @@ -337,6 +368,41 @@ class TestSchemaGeneratorNotAtRoot(TestCase): self.assertEqual(schema, expected) +@unittest.skipUnless(coreapi, 'coreapi is not installed') +class TestSchemaGeneratorWithRestrictedViewSets(TestCase): + def setUp(self): + router = DefaultRouter() + router.register('example1', Http404ExampleViewSet, base_name='example1') + router.register('example2', PermissionDeniedExampleViewSet, base_name='example2') + self.patterns = [ + url('^example/?$', ExampleListView.as_view()), + url(r'^', include(router.urls)) + ] + + def test_schema_for_regular_views(self): + """ + Ensure that schema generation works for ViewSet classes + with permission classes raising exceptions. + """ + generator = SchemaGenerator(title='Example API', patterns=self.patterns) + request = factory.get('/') + schema = generator.get_schema(Request(request)) + expected = coreapi.Document( + url='', + title='Example API', + content={ + 'example': { + 'list': coreapi.Link( + url='/example/', + action='get', + fields=[] + ), + }, + } + ) + self.assertEqual(schema, expected) + + @unittest.skipUnless(coreapi, 'coreapi is not installed') class Test4605Regression(TestCase): def test_4605_regression(self): From d55e176a1e882d498b63b46a01f0ac389428f084 Mon Sep 17 00:00:00 2001 From: Carlos de la Torre Date: Wed, 2 Nov 2016 11:03:53 -0300 Subject: [PATCH 030/190] Fix documentation error: removed unused variable (#4647) --- docs/api-guide/fields.md | 1 - 1 file changed, 1 deletion(-) diff --git a/docs/api-guide/fields.md b/docs/api-guide/fields.md index f986f1508..17168b721 100644 --- a/docs/api-guide/fields.md +++ b/docs/api-guide/fields.md @@ -626,7 +626,6 @@ The `.fail()` method is a shortcut for raising `ValidationError` that takes a me def to_internal_value(self, data): if not isinstance(data, six.text_type): - msg = 'Incorrect type. Expected a string, but got %s' self.fail('incorrect_type', input_type=type(data).__name__) if not re.match(r'^rgb\([0-9]+,[0-9]+,[0-9]+\)$', data): From 7f437123bdd6f81c3f6c1216e6308aad4aac5068 Mon Sep 17 00:00:00 2001 From: pkrzyzaniak Date: Mon, 7 Nov 2016 06:12:52 +0800 Subject: [PATCH 031/190] Added "drf_tweaks" to third party packages (#4659) --- docs/topics/third-party-resources.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/topics/third-party-resources.md b/docs/topics/third-party-resources.md index 3fba9b5da..1713da1a1 100644 --- a/docs/topics/third-party-resources.md +++ b/docs/topics/third-party-resources.md @@ -251,6 +251,7 @@ To submit new content, [open an issue][drf-create-issue] or [create a pull reque * [ember-django-adapter][ember-django-adapter] - An adapter for working with Ember.js * [django-versatileimagefield][django-versatileimagefield] - Provides a drop-in replacement for Django's stock `ImageField` that makes it easy to serve images in multiple sizes/renditions from a single field. For DRF-specific implementation docs, [click here][django-versatileimagefield-drf-docs]. * [drf-tracking][drf-tracking] - Utilities to track requests to DRF API views. +* [drf_tweaks][drf_tweaks] - Serializers with one-step validation (and more), pagination without counts and other tweaks. * [django-rest-framework-braces][django-rest-framework-braces] - Collection of utilities for working with Django Rest Framework. The most notable ones are [FormSerializer](https://django-rest-framework-braces.readthedocs.io/en/latest/overview.html#formserializer) and [SerializerForm](https://django-rest-framework-braces.readthedocs.io/en/latest/overview.html#serializerform), which are adapters between DRF serializers and Django forms. * [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. @@ -363,3 +364,4 @@ To submit new content, [open an issue][drf-create-issue] or [create a pull reque [django-rest-messaging-js]: https://github.com/raphaelgyory/django-rest-messaging-js [medium-django-rest-framework]: https://medium.com/django-rest-framework [django-rest-framework-course]: https://teamtreehouse.com/library/django-rest-framework +[drf_tweaks]: https://github.com/ArabellaTech/drf_tweaks From 0b9304014d4505cec6c4a7df09bc66616d3827b8 Mon Sep 17 00:00:00 2001 From: Aaron Lelevier Date: Mon, 7 Nov 2016 12:30:46 +0100 Subject: [PATCH 032/190] Add documentation link for single 'field-level validation' to the Validator docs page (#3772) (#4657) --- docs/api-guide/validators.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/docs/api-guide/validators.md b/docs/api-guide/validators.md index 9df15ec15..1af9f02a5 100644 --- a/docs/api-guide/validators.md +++ b/docs/api-guide/validators.md @@ -272,6 +272,12 @@ A validator may be any callable that raises a `serializers.ValidationError` on f if value % 2 != 0: raise serializers.ValidationError('This field must be an even number.') +#### Field-level validation + +You can specify custom field-level validation by adding `.validate_` methods +to your `Serializer` subclass. This is documented in the +[Serializer docs](http://www.django-rest-framework.org/api-guide/serializers/#field-level-validation) + ## Class-based To write a class-based validator, use the `__call__` method. Class-based validators are useful as they allow you to parameterize and reuse behavior. From befacfb00d4c39436a307ddc4bf1edb6f82a1a5e Mon Sep 17 00:00:00 2001 From: James Beith Date: Mon, 7 Nov 2016 22:34:53 +1100 Subject: [PATCH 033/190] Add autofocus support for input.html templates (#4650) This change adds support to use `'autofocus': True` in the style options and have the `autofocus` attribute included on the input field when rendered. --- docs/topics/html-and-forms.md | 6 +++--- .../templates/rest_framework/horizontal/input.html | 2 +- rest_framework/templates/rest_framework/inline/input.html | 2 +- rest_framework/templates/rest_framework/vertical/input.html | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/docs/topics/html-and-forms.md b/docs/topics/html-and-forms.md index 6660607fe..f77ae3af7 100644 --- a/docs/topics/html-and-forms.md +++ b/docs/topics/html-and-forms.md @@ -108,7 +108,7 @@ Let's take a look at how to render each of the three available template packs. F class LoginSerializer(serializers.Serializer): email = serializers.EmailField( max_length=100, - style={'placeholder': 'Email'} + style={'placeholder': 'Email', 'autofocus': True} ) password = serializers.CharField( max_length=100, @@ -207,9 +207,9 @@ Field templates can also use additional style properties, depending on their typ The complete list of `base_template` options and their associated style options is listed below. -base_template | Valid field types | Additional style options +base_template | Valid field types | Additional style options ----|----|---- -input.html | Any string, numeric or date/time field | input_type, placeholder, hide_label +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 diff --git a/rest_framework/templates/rest_framework/horizontal/input.html b/rest_framework/templates/rest_framework/horizontal/input.html index 9e5bbd0f7..a6d657d7d 100644 --- a/rest_framework/templates/rest_framework/horizontal/input.html +++ b/rest_framework/templates/rest_framework/horizontal/input.html @@ -6,7 +6,7 @@ {% endif %}
- + {% if field.errors %} {% for error in field.errors %} diff --git a/rest_framework/templates/rest_framework/inline/input.html b/rest_framework/templates/rest_framework/inline/input.html index f28e8f11c..085c63cb3 100644 --- a/rest_framework/templates/rest_framework/inline/input.html +++ b/rest_framework/templates/rest_framework/inline/input.html @@ -5,5 +5,5 @@ {% endif %} - +
diff --git a/rest_framework/templates/rest_framework/vertical/input.html b/rest_framework/templates/rest_framework/vertical/input.html index 504dcc28e..a7cff2ca6 100644 --- a/rest_framework/templates/rest_framework/vertical/input.html +++ b/rest_framework/templates/rest_framework/vertical/input.html @@ -3,7 +3,7 @@ {% endif %} - + {% if field.errors %} {% for error in field.errors %} From ee60aaa945213723f9574317fa81ca43b7170454 Mon Sep 17 00:00:00 2001 From: Angel Velasquez Date: Mon, 7 Nov 2016 08:37:58 -0300 Subject: [PATCH 034/190] Update versions of Django on tox.ini (#4651) Bump release versions to 1.10.3, 1.9.11 and 1.8.16 More info on: https://www.djangoproject.com/weblog/2016/nov/01/security-releases/ --- tox.ini | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tox.ini b/tox.ini index bac1569a0..e89274661 100644 --- a/tox.ini +++ b/tox.ini @@ -15,9 +15,9 @@ setenv = PYTHONDONTWRITEBYTECODE=1 PYTHONWARNINGS=once deps = - django18: Django==1.8.15 - django19: Django==1.9.10 - django110: Django==1.10.2 + django18: Django==1.8.16 + django19: Django==1.9.11 + django110: Django==1.10.3 djangomaster: https://github.com/django/django/archive/master.tar.gz -rrequirements/requirements-testing.txt -rrequirements/requirements-optionals.txt From 06df61e38c6ed99732007b0e9f3cc26e8317389e Mon Sep 17 00:00:00 2001 From: Rex Salisbury Date: Mon, 7 Nov 2016 03:41:10 -0800 Subject: [PATCH 035/190] handle error when no links are found (#4649) This is to address https://github.com/tomchristie/django-rest-raml/issues/5 The problem is that if you try to generate RAML docs when you haven't set up any views, you get the above error (min called on an empty list). unfortunately, this PR is not very helpful since it doesn't actually surface a readable error to the user. Not sure what the best way to address this would be... --- rest_framework/schemas.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/rest_framework/schemas.py b/rest_framework/schemas.py index 9439cb691..773df6261 100644 --- a/rest_framework/schemas.py +++ b/rest_framework/schemas.py @@ -263,6 +263,8 @@ class SchemaGenerator(object): view_endpoints.append((path, method, view)) # Only generate the path prefix for paths that will be included + if not paths: + return None prefix = self.determine_path_prefix(paths) for path, method, view in view_endpoints: From 8d72535be9d7ab31c8a46ce5fbbdb39a6d8e0209 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Mon, 7 Nov 2016 12:55:18 +0000 Subject: [PATCH 036/190] Fix FilterSet warnings. (#4660) --- rest_framework/filters.py | 22 +++++----------------- 1 file changed, 5 insertions(+), 17 deletions(-) diff --git a/rest_framework/filters.py b/rest_framework/filters.py index 531531efc..00e753d42 100644 --- a/rest_framework/filters.py +++ b/rest_framework/filters.py @@ -38,31 +38,19 @@ class BaseFilterBackend(object): if django_filters: - from django_filters.filterset import FilterSetMetaclass as DFFilterSetMetaclass from django_filters.rest_framework.filterset import FilterSet as DFFilterSet - class FilterSetMetaclass(DFFilterSetMetaclass): - def __new__(cls, name, bases, attrs): + class FilterSet(DFFilterSet): + def __init__(self, *args, **kwargs): warnings.warn( "The built in 'rest_framework.filters.FilterSet' is pending deprecation. " "You should use 'django_filters.rest_framework.FilterSet' instead.", PendingDeprecationWarning ) - return super(FilterSetMetaclass, cls).__new__(cls, name, bases, attrs) - _BaseFilterSet = DFFilterSet + return super(FilterSet, self).__init__(*args, **kwargs) else: - # Dummy metaclass just so we can give a user-friendly error message. - class FilterSetMetaclass(type): - def __init__(self, name, bases, attrs): - # Assert only on subclasses, so we can define FilterSet below. - if bases != (object,): - assert False, 'django-filter must be installed to use the `FilterSet` class' - super(FilterSetMetaclass, self).__init__(name, bases, attrs) - _BaseFilterSet = object - - -class FilterSet(six.with_metaclass(FilterSetMetaclass, _BaseFilterSet)): - pass + def FilterSet(): + assert False, 'django-filter must be installed to use the `FilterSet` class' class DjangoFilterBackend(BaseFilterBackend): From ea60872e9e18909577a2603f5ff630a7c9d04b16 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Mon, 7 Nov 2016 13:38:48 +0000 Subject: [PATCH 037/190] Version 3.5.3 [ci skip] --- docs/topics/release-notes.md | 18 ++++++++++++++++++ rest_framework/__init__.py | 2 +- 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/docs/topics/release-notes.md b/docs/topics/release-notes.md index d25a46ba0..e5683360b 100644 --- a/docs/topics/release-notes.md +++ b/docs/topics/release-notes.md @@ -40,6 +40,14 @@ You can determine your currently installed version using `pip freeze`: ## 3.5.x series +### 3.5.3 + +**Date**: [7th November 2016][3.5.3-milestone] + +* Don't raise incorrect FilterSet deprecation warnings. ([#4660][gh4660], [#4643][gh4643], [#4644][gh4644]) +* Schema generation should not raise 404 when a view permission class does. ([#4645][gh4645], [#4646][gh4646]) +* Add `autofocus` support for input controls. ([#4650][gh4650]) + ### 3.5.2 **Date**: [1st November 2016][3.5.2-milestone] @@ -630,6 +638,7 @@ For older release notes, [please see the version 2.x documentation][old-release- [3.5.0-milestone]: https://github.com/tomchristie/django-rest-framework/issues?q=milestone%3A%223.5.0+Release%22 [3.5.1-milestone]: https://github.com/tomchristie/django-rest-framework/issues?q=milestone%3A%223.5.1+Release%22 [3.5.2-milestone]: https://github.com/tomchristie/django-rest-framework/issues?q=milestone%3A%223.5.2+Release%22 +[3.5.3-milestone]: https://github.com/tomchristie/django-rest-framework/issues?q=milestone%3A%223.5.3+Release%22 [gh2013]: https://github.com/tomchristie/django-rest-framework/issues/2013 @@ -1198,3 +1207,12 @@ For older release notes, [please see the version 2.x documentation][old-release- [gh4620]: https://github.com/tomchristie/django-rest-framework/issues/4620 [gh4628]: https://github.com/tomchristie/django-rest-framework/issues/4628 [gh4639]: https://github.com/tomchristie/django-rest-framework/issues/4639 + + + +[gh4660]: https://github.com/tomchristie/django-rest-framework/issues/4660 +[gh4643]: https://github.com/tomchristie/django-rest-framework/issues/4643 +[gh4644]: https://github.com/tomchristie/django-rest-framework/issues/4644 +[gh4645]: https://github.com/tomchristie/django-rest-framework/issues/4645 +[gh4646]: https://github.com/tomchristie/django-rest-framework/issues/4646 +[gh4650]: https://github.com/tomchristie/django-rest-framework/issues/4650 diff --git a/rest_framework/__init__.py b/rest_framework/__init__.py index fe82763a9..795b84e95 100644 --- a/rest_framework/__init__.py +++ b/rest_framework/__init__.py @@ -8,7 +8,7 @@ ______ _____ _____ _____ __ """ __title__ = 'Django REST framework' -__version__ = '3.5.2' +__version__ = '3.5.3' __author__ = 'Tom Christie' __license__ = 'BSD 2-Clause' __copyright__ = 'Copyright 2011-2016 Tom Christie' From 388cf7622c24fee33f998cbe44198bada1872ebb Mon Sep 17 00:00:00 2001 From: Michael Barr Date: Wed, 9 Nov 2016 07:59:11 -0500 Subject: [PATCH 038/190] Adds Django/Python Trove Classifiers (#4662) --- setup.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/setup.py b/setup.py index ca62366ed..5d14ddf6a 100755 --- a/setup.py +++ b/setup.py @@ -92,11 +92,19 @@ setup( 'Development Status :: 5 - Production/Stable', 'Environment :: Web Environment', 'Framework :: Django', + 'Framework :: Django :: 1.8', + 'Framework :: Django :: 1.9', + 'Framework :: Django :: 1.10', 'Intended Audience :: Developers', 'License :: OSI Approved :: BSD License', 'Operating System :: OS Independent', 'Programming Language :: Python', + 'Programming Language :: Python :: 2', + 'Programming Language :: Python :: 2.7', 'Programming Language :: Python :: 3', + 'Programming Language :: Python :: 3.3', + 'Programming Language :: Python :: 3.4', + 'Programming Language :: Python :: 3.5', 'Topic :: Internet :: WWW/HTTP', ] ) From 8bab7f8d582c6c8c25e913ec4092899c78875a2c Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Thu, 10 Nov 2016 16:36:56 +0000 Subject: [PATCH 039/190] Only apply the nested writes test to writable fields. (#4669) --- rest_framework/serializers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index 1bdcd12c3..02c24b70e 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -769,7 +769,7 @@ def raise_errors_on_nested_writes(method_name, serializer, validated_data): isinstance(field, BaseSerializer) and (field.source in validated_data) and isinstance(validated_data[field.source], (list, dict)) - for key, field in serializer.fields.items() + for field in serializer._writable_fields ), ( 'The `.{method_name}()` method does not support writable nested ' 'fields by default.\nWrite an explicit `.{method_name}()` method for ' From 24791cb353d1924086b30abe2188280547d9a6c4 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Fri, 11 Nov 2016 09:44:35 +0000 Subject: [PATCH 040/190] Invalidate any existing prefetch cache on PUT requests. (#4668) --- docs/api-guide/testing.md | 11 ++++++++++- rest_framework/mixins.py | 5 ++--- tests/test_prefetch_related.py | 20 +++++++++++++++++++- 3 files changed, 31 insertions(+), 5 deletions(-) diff --git a/docs/api-guide/testing.md b/docs/api-guide/testing.md index 1f8c89233..de79a1e2f 100644 --- a/docs/api-guide/testing.md +++ b/docs/api-guide/testing.md @@ -187,7 +187,12 @@ As usual CSRF validation will only apply to any session authenticated views. Th # RequestsClient REST framework also includes a client for interacting with your application -using the popular Python library, `requests`. +using the popular Python library, `requests`. This may be useful if: + +* You are expecting to interface with the API primarily from another Python service, +and want to test the service at the same level as the client will see. +* You want to write tests in such a way that they can also be run against a staging or +live environment. (See "Live tests" below.) This exposes exactly the same interface as if you were using a requests session directly. @@ -198,6 +203,10 @@ directly. Note that the requests client requires you to pass fully qualified URLs. +## `RequestsClient` and working with the database + +The `RequestsClient` class is useful if + ## Headers & Authentication Custom headers and authentication credentials can be provided in the same way diff --git a/rest_framework/mixins.py b/rest_framework/mixins.py index 47a4923a1..f3695e665 100644 --- a/rest_framework/mixins.py +++ b/rest_framework/mixins.py @@ -71,9 +71,8 @@ class UpdateModelMixin(object): if getattr(instance, '_prefetched_objects_cache', None): # If 'prefetch_related' has been applied to a queryset, we need to - # refresh the instance from the database. - instance = self.get_object() - serializer = self.get_serializer(instance) + # forcibly invalidate the prefetch cache on the instance. + instance._prefetched_objects_cache = {} return Response(serializer.data) diff --git a/tests/test_prefetch_related.py b/tests/test_prefetch_related.py index fc697adc1..a9fa238ea 100644 --- a/tests/test_prefetch_related.py +++ b/tests/test_prefetch_related.py @@ -14,7 +14,7 @@ class UserSerializer(serializers.ModelSerializer): class UserUpdate(generics.UpdateAPIView): - queryset = User.objects.all().prefetch_related('groups') + queryset = User.objects.exclude(username='exclude').prefetch_related('groups') serializer_class = UserSerializer @@ -39,3 +39,21 @@ class TestPrefetchRelatedUpdates(TestCase): 'email': 'tom@example.com' } assert response.data == expected + + def test_prefetch_related_excluding_instance_from_original_queryset(self): + """ + Regression test for https://github.com/tomchristie/django-rest-framework/issues/4661 + """ + 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 0c02bbbfa728267909ea73aeac57b2e99aba5857 Mon Sep 17 00:00:00 2001 From: Bruno Alla Date: Mon, 14 Nov 2016 16:58:16 +0000 Subject: [PATCH 041/190] Correct a small typo in exceptions documentation --- docs/api-guide/exceptions.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/api-guide/exceptions.md b/docs/api-guide/exceptions.md index df8cad42d..03f16222d 100644 --- a/docs/api-guide/exceptions.md +++ b/docs/api-guide/exceptions.md @@ -47,7 +47,7 @@ Any example validation error might look like this: You can implement custom exception handling by creating a handler function that converts exceptions raised in your API views into response objects. This allows you to control the style of error responses used by your API. -The function must take a pair of arguments, this first is the exception to be handled, and the second is a dictionary containing any extra context such as the view currently being handled. The exception handler function should either return a `Response` object, or return `None` if the exception cannot be handled. If the handler returns `None` then the exception will be re-raised and Django will return a standard HTTP 500 'server error' response. +The function must take a pair of arguments, the first is the exception to be handled, and the second is a dictionary containing any extra context such as the view currently being handled. The exception handler function should either return a `Response` object, or return `None` if the exception cannot be handled. If the handler returns `None` then the exception will be re-raised and Django will return a standard HTTP 500 'server error' response. For example, you might want to ensure that all error responses include the HTTP status code in the body of the response, like so: From 1b9013ebae6f4b28de6b88c65a97b5ab0e817200 Mon Sep 17 00:00:00 2001 From: Asif Saifuddin Auvi Date: Tue, 22 Nov 2016 19:46:12 +0600 Subject: [PATCH 042/190] update django-filter to version 1.0.0 --- requirements/requirements-optionals.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements/requirements-optionals.txt b/requirements/requirements-optionals.txt index 86c4f7709..a59f153d1 100644 --- a/requirements/requirements-optionals.txt +++ b/requirements/requirements-optionals.txt @@ -1,5 +1,5 @@ # Optional packages which may be used with REST framework. markdown==2.6.4 django-guardian==1.4.6 -django-filter==0.15.3 +django-filter==1.0.0 coreapi==2.0.8 From 8030f5b74f49e3144e8fb45f367227bac1192aec Mon Sep 17 00:00:00 2001 From: Vinay Anantharaman Date: Tue, 22 Nov 2016 14:50:47 -0800 Subject: [PATCH 043/190] Edit to the import in Setting filter backends `django_filters` doesn't export `rest_framework` by default so it's required to import it. --- docs/api-guide/filtering.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/api-guide/filtering.md b/docs/api-guide/filtering.md index 1b49d3a73..3f212ced3 100644 --- a/docs/api-guide/filtering.md +++ b/docs/api-guide/filtering.md @@ -98,7 +98,7 @@ The default filter backends may be set globally, using the `DEFAULT_FILTER_BACKE You can also set the filter backends on a per-view, or per-viewset basis, using the `GenericAPIView` class-based views. - import django_filters + import django_filters.rest_framework from django.contrib.auth.models import User from myapp.serializers import UserSerializer from rest_framework import generics From 4b59ec27fa725a83646792b560fae1b646baadcd Mon Sep 17 00:00:00 2001 From: Asif Saifuddin Auvi Date: Wed, 23 Nov 2016 19:17:00 +0600 Subject: [PATCH 044/190] convert tests asserts to pytest style (#4696) --- tests/test_views.py | 16 ++++++++-------- tests/test_viewsets.py | 10 +++++----- 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/tests/test_views.py b/tests/test_views.py index 05c499481..adafa1cd7 100644 --- a/tests/test_views.py +++ b/tests/test_views.py @@ -71,8 +71,8 @@ class ClassBasedViewIntegrationTests(TestCase): expected = { 'detail': JSON_ERROR } - self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) - self.assertEqual(sanitise_json_error(response.data), expected) + assert response.status_code == status.HTTP_400_BAD_REQUEST + assert sanitise_json_error(response.data) == expected class FunctionBasedViewIntegrationTests(TestCase): @@ -85,8 +85,8 @@ class FunctionBasedViewIntegrationTests(TestCase): expected = { 'detail': JSON_ERROR } - self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) - self.assertEqual(sanitise_json_error(response.data), expected) + assert response.status_code == status.HTTP_400_BAD_REQUEST + assert sanitise_json_error(response.data) == expected class TestCustomExceptionHandler(TestCase): @@ -107,8 +107,8 @@ class TestCustomExceptionHandler(TestCase): request = factory.get('/', content_type='application/json') response = view(request) expected = 'Error!' - self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) - self.assertEqual(response.data, expected) + assert response.status_code == status.HTTP_400_BAD_REQUEST + assert response.data == expected def test_function_based_view_exception_handler(self): view = error_view @@ -116,5 +116,5 @@ class TestCustomExceptionHandler(TestCase): request = factory.get('/', content_type='application/json') response = view(request) expected = 'Error!' - self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) - self.assertEqual(response.data, expected) + assert response.status_code == status.HTTP_400_BAD_REQUEST + assert response.data == expected diff --git a/tests/test_viewsets.py b/tests/test_viewsets.py index 0d9b6b310..a3c4e6be5 100644 --- a/tests/test_viewsets.py +++ b/tests/test_viewsets.py @@ -21,15 +21,15 @@ class InitializeViewSetsTestCase(TestCase): }) response = my_view(request) - self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(response.data, {'ACTION': 'LIST'}) + assert response.status_code == status.HTTP_200_OK + assert response.data == {'ACTION': 'LIST'} def test_initialize_view_set_with_empty_actions(self): try: BasicViewSet.as_view() except TypeError as e: - self.assertEqual(str(e), "The `actions` argument must be provided " - "when calling `.as_view()` on a ViewSet. " - "For example `.as_view({'get': 'list'})`") + assert str(e) == ("The `actions` argument must be provided " + "when calling `.as_view()` on a ViewSet. " + "For example `.as_view({'get': 'list'})`") else: self.fail("actions must not be empty.") From 5ec223bca0f2df5be06b583f58012e115d2f52a2 Mon Sep 17 00:00:00 2001 From: Asif Saifuddin Auvi Date: Wed, 23 Nov 2016 20:05:34 +0600 Subject: [PATCH 045/190] converted validators and write_only_fields test to pytest style (#4697) --- tests/test_validators.py | 13 +++++-------- tests/test_write_only_fields.py | 6 +++--- 2 files changed, 8 insertions(+), 11 deletions(-) diff --git a/tests/test_validators.py b/tests/test_validators.py index ab2c87c11..37e662b8e 100644 --- a/tests/test_validators.py +++ b/tests/test_validators.py @@ -94,23 +94,20 @@ class TestUniquenessValidation(TestCase): def test_doesnt_pollute_model(self): instance = AnotherUniquenessModel.objects.create(code='100') serializer = AnotherUniquenessSerializer(instance) - self.assertEqual( - AnotherUniquenessModel._meta.get_field('code').validators, []) + assert AnotherUniquenessModel._meta.get_field('code').validators == [] # Accessing data shouldn't effect validators on the model serializer.data - self.assertEqual( - AnotherUniquenessModel._meta.get_field('code').validators, []) + assert AnotherUniquenessModel._meta.get_field('code').validators == [] def test_related_model_is_unique(self): data = {'username': 'Existing', 'email': 'new-email@example.com'} rs = RelatedModelSerializer(data=data) - self.assertFalse(rs.is_valid()) - self.assertEqual(rs.errors, - {'username': ['This field must be unique.']}) + assert not rs.is_valid() + assert rs.errors == {'username': ['This field must be unique.']} data = {'username': 'new-username', 'email': 'new-email@example.com'} rs = RelatedModelSerializer(data=data) - self.assertTrue(rs.is_valid()) + assert rs.is_valid() def test_value_error_treated_as_not_unique(self): serializer = UniquenessIntegerSerializer(data={'integer': 'abc'}) diff --git a/tests/test_write_only_fields.py b/tests/test_write_only_fields.py index 3a289afab..272a05ff3 100644 --- a/tests/test_write_only_fields.py +++ b/tests/test_write_only_fields.py @@ -20,8 +20,8 @@ class WriteOnlyFieldTests(TestCase): 'password': '123' } serializer = self.Serializer(data=data) - self.assertTrue(serializer.is_valid()) - self.assertEqual(serializer.validated_data, data) + assert serializer.is_valid() + assert serializer.validated_data == data def write_only_fields_are_not_present_on_output(self): instance = { @@ -29,4 +29,4 @@ class WriteOnlyFieldTests(TestCase): 'password': '123' } serializer = self.Serializer(instance) - self.assertEqual(serializer.data, {'email': 'foo@example.com'}) + assert serializer.data == {'email': 'foo@example.com'} From a13b8d5560845612318fb030eb610bd34d986955 Mon Sep 17 00:00:00 2001 From: Nik Nyby Date: Wed, 23 Nov 2016 10:13:03 -0500 Subject: [PATCH 046/190] docs: grammar fix --- docs/api-guide/validators.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/api-guide/validators.md b/docs/api-guide/validators.md index 1af9f02a5..e5b949496 100644 --- a/docs/api-guide/validators.md +++ b/docs/api-guide/validators.md @@ -159,7 +159,7 @@ If you want the date field to be entirely hidden from the user, then use `Hidden --- -**Note**: The `UniqueForValidation` classes always imposes an implicit constraint that the fields they are applied to are always treated as required. Fields with `default` values are an exception to this as they always supply a value even when omitted from user input. +**Note**: The `UniqueForValidation` classes impose an implicit constraint that the fields they are applied to are always treated as required. Fields with `default` values are an exception to this as they always supply a value even when omitted from user input. --- From eaec60ae1d4e34cfef115df6c626cf96f6750e56 Mon Sep 17 00:00:00 2001 From: Nik Nyby Date: Wed, 23 Nov 2016 11:10:39 -0500 Subject: [PATCH 047/190] docs: grammar fix - it's -> its (#4698) --- docs/api-guide/validators.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/api-guide/validators.md b/docs/api-guide/validators.md index e5b949496..d6350ae5e 100644 --- a/docs/api-guide/validators.md +++ b/docs/api-guide/validators.md @@ -153,7 +153,7 @@ The field will not be writable to the user, but the default value will still be #### Using with a hidden date field. -If you want the date field to be entirely hidden from the user, then use `HiddenField`. This field type does not accept user input, but instead always returns it's default value to the `validated_data` in the serializer. +If you want the date field to be entirely hidden from the user, then use `HiddenField`. This field type does not accept user input, but instead always returns its default value to the `validated_data` in the serializer. published = serializers.HiddenField(default=timezone.now) From abc62afddb9e49b199ee5f4f491093014515da0e Mon Sep 17 00:00:00 2001 From: Nik Nyby Date: Thu, 24 Nov 2016 04:39:18 -0500 Subject: [PATCH 048/190] docs typo fix (#4701) Remove unnecessary "a" --- docs/api-guide/validators.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/api-guide/validators.md b/docs/api-guide/validators.md index d6350ae5e..e041e9072 100644 --- a/docs/api-guide/validators.md +++ b/docs/api-guide/validators.md @@ -20,7 +20,7 @@ With `ModelForm` the validation is performed partially on the form, and partiall * It is easy to switch between using shortcut `ModelSerializer` classes and using explicit `Serializer` classes. Any validation behavior being used for `ModelSerializer` is simple to replicate. * Printing the `repr` of a serializer instance will show you exactly what validation rules it applies. There's no extra hidden validation behavior being called on the model instance. -When you're using `ModelSerializer` all of this is handled automatically for you. If you want to drop down to using a `Serializer` classes instead, then you need to define the validation rules explicitly. +When you're using `ModelSerializer` all of this is handled automatically for you. If you want to drop down to using `Serializer` classes instead, then you need to define the validation rules explicitly. #### Example From cd3fd36d0e5fa7e2d4907a7f74cd45641a8ddd03 Mon Sep 17 00:00:00 2001 From: Asif Saifuddin Auvi Date: Sun, 27 Nov 2016 23:55:09 +0600 Subject: [PATCH 049/190] converted generic relations assert to pytest style --- tests/test_relations_generic.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_relations_generic.py b/tests/test_relations_generic.py index babc2269c..a3798b0a3 100644 --- a/tests/test_relations_generic.py +++ b/tests/test_relations_generic.py @@ -75,7 +75,7 @@ class TestGenericRelations(TestCase): 'tags': ['django', 'python'], 'url': 'https://www.djangoproject.com/' } - self.assertEqual(serializer.data, expected) + assert serializer.data == expected def test_generic_fk(self): """ @@ -105,4 +105,4 @@ class TestGenericRelations(TestCase): 'tagged_item': 'Note: Remember the milk' } ] - self.assertEqual(serializer.data, expected) + assert serializer.data == expected From 42d6098c74e3c8536458f4d423bedb652da16db7 Mon Sep 17 00:00:00 2001 From: Asif Saifuddin Auvi Date: Mon, 28 Nov 2016 15:43:48 +0600 Subject: [PATCH 050/190] converted primary key relations test asserts to pytest (#4709) --- tests/test_relations_pk.py | 118 ++++++++++++++++++------------------- 1 file changed, 59 insertions(+), 59 deletions(-) diff --git a/tests/test_relations_pk.py b/tests/test_relations_pk.py index 7a3d45927..ea02f2577 100644 --- a/tests/test_relations_pk.py +++ b/tests/test_relations_pk.py @@ -84,7 +84,7 @@ class PKManyToManyTests(TestCase): {'id': 3, 'name': 'source-3', 'targets': [1, 2, 3]} ] with self.assertNumQueries(4): - self.assertEqual(serializer.data, expected) + assert serializer.data == expected def test_many_to_many_retrieve_prefetch_related(self): queryset = ManyToManySource.objects.all().prefetch_related('targets') @@ -101,15 +101,15 @@ class PKManyToManyTests(TestCase): {'id': 3, 'name': 'target-3', 'sources': [3]} ] with self.assertNumQueries(4): - self.assertEqual(serializer.data, expected) + assert serializer.data == expected def test_many_to_many_update(self): data = {'id': 1, 'name': 'source-1', 'targets': [1, 2, 3]} instance = ManyToManySource.objects.get(pk=1) serializer = ManyToManySourceSerializer(instance, data=data) - self.assertTrue(serializer.is_valid()) + assert serializer.is_valid() serializer.save() - self.assertEqual(serializer.data, data) + assert serializer.data == data # Ensure source 1 is updated, and everything else is as expected queryset = ManyToManySource.objects.all() @@ -119,15 +119,15 @@ class PKManyToManyTests(TestCase): {'id': 2, 'name': 'source-2', 'targets': [1, 2]}, {'id': 3, 'name': 'source-3', 'targets': [1, 2, 3]} ] - self.assertEqual(serializer.data, expected) + assert serializer.data == expected def test_reverse_many_to_many_update(self): data = {'id': 1, 'name': 'target-1', 'sources': [1]} instance = ManyToManyTarget.objects.get(pk=1) serializer = ManyToManyTargetSerializer(instance, data=data) - self.assertTrue(serializer.is_valid()) + assert serializer.is_valid() serializer.save() - self.assertEqual(serializer.data, data) + assert serializer.data == data # Ensure target 1 is updated, and everything else is as expected queryset = ManyToManyTarget.objects.all() @@ -137,15 +137,15 @@ class PKManyToManyTests(TestCase): {'id': 2, 'name': 'target-2', 'sources': [2, 3]}, {'id': 3, 'name': 'target-3', 'sources': [3]} ] - self.assertEqual(serializer.data, expected) + assert serializer.data == expected def test_many_to_many_create(self): data = {'id': 4, 'name': 'source-4', 'targets': [1, 3]} serializer = ManyToManySourceSerializer(data=data) - self.assertTrue(serializer.is_valid()) + assert serializer.is_valid() obj = serializer.save() - self.assertEqual(serializer.data, data) - self.assertEqual(obj.name, 'source-4') + assert serializer.data == data + assert obj.name == 'source-4' # Ensure source 4 is added, and everything else is as expected queryset = ManyToManySource.objects.all() @@ -156,7 +156,7 @@ class PKManyToManyTests(TestCase): {'id': 3, 'name': 'source-3', 'targets': [1, 2, 3]}, {'id': 4, 'name': 'source-4', 'targets': [1, 3]}, ] - self.assertEqual(serializer.data, expected) + assert serializer.data == expected def test_many_to_many_unsaved(self): source = ManyToManySource(name='source-unsaved') @@ -166,15 +166,15 @@ class PKManyToManyTests(TestCase): expected = {'id': None, 'name': 'source-unsaved', 'targets': []} # no query if source hasn't been created yet with self.assertNumQueries(0): - self.assertEqual(serializer.data, expected) + assert serializer.data == expected def test_reverse_many_to_many_create(self): data = {'id': 4, 'name': 'target-4', 'sources': [1, 3]} serializer = ManyToManyTargetSerializer(data=data) - self.assertTrue(serializer.is_valid()) + assert serializer.is_valid() obj = serializer.save() - self.assertEqual(serializer.data, data) - self.assertEqual(obj.name, 'target-4') + assert serializer.data == data + assert obj.name == 'target-4' # Ensure target 4 is added, and everything else is as expected queryset = ManyToManyTarget.objects.all() @@ -185,7 +185,7 @@ class PKManyToManyTests(TestCase): {'id': 3, 'name': 'target-3', 'sources': [3]}, {'id': 4, 'name': 'target-4', 'sources': [1, 3]} ] - self.assertEqual(serializer.data, expected) + assert serializer.data == expected class PKForeignKeyTests(TestCase): @@ -207,7 +207,7 @@ class PKForeignKeyTests(TestCase): {'id': 3, 'name': 'source-3', 'target': 1} ] with self.assertNumQueries(1): - self.assertEqual(serializer.data, expected) + assert serializer.data == expected def test_reverse_foreign_key_retrieve(self): queryset = ForeignKeyTarget.objects.all() @@ -217,7 +217,7 @@ class PKForeignKeyTests(TestCase): {'id': 2, 'name': 'target-2', 'sources': []}, ] with self.assertNumQueries(3): - self.assertEqual(serializer.data, expected) + assert serializer.data == expected def test_reverse_foreign_key_retrieve_prefetch_related(self): queryset = ForeignKeyTarget.objects.all().prefetch_related('sources') @@ -229,9 +229,9 @@ class PKForeignKeyTests(TestCase): data = {'id': 1, 'name': 'source-1', 'target': 2} instance = ForeignKeySource.objects.get(pk=1) serializer = ForeignKeySourceSerializer(instance, data=data) - self.assertTrue(serializer.is_valid()) + assert serializer.is_valid() serializer.save() - self.assertEqual(serializer.data, data) + assert serializer.data == data # Ensure source 1 is updated, and everything else is as expected queryset = ForeignKeySource.objects.all() @@ -241,20 +241,20 @@ class PKForeignKeyTests(TestCase): {'id': 2, 'name': 'source-2', 'target': 1}, {'id': 3, 'name': 'source-3', 'target': 1} ] - self.assertEqual(serializer.data, expected) + assert serializer.data == expected def test_foreign_key_update_incorrect_type(self): data = {'id': 1, 'name': 'source-1', 'target': 'foo'} instance = ForeignKeySource.objects.get(pk=1) serializer = ForeignKeySourceSerializer(instance, data=data) - self.assertFalse(serializer.is_valid()) - self.assertEqual(serializer.errors, {'target': ['Incorrect type. Expected pk value, received %s.' % six.text_type.__name__]}) + assert not serializer.is_valid() + assert serializer.errors == {'target': ['Incorrect type. Expected pk value, received %s.' % six.text_type.__name__]} def test_reverse_foreign_key_update(self): data = {'id': 2, 'name': 'target-2', 'sources': [1, 3]} instance = ForeignKeyTarget.objects.get(pk=2) serializer = ForeignKeyTargetSerializer(instance, data=data) - self.assertTrue(serializer.is_valid()) + assert serializer.is_valid() # We shouldn't have saved anything to the db yet since save # hasn't been called. queryset = ForeignKeyTarget.objects.all() @@ -263,10 +263,10 @@ class PKForeignKeyTests(TestCase): {'id': 1, 'name': 'target-1', 'sources': [1, 2, 3]}, {'id': 2, 'name': 'target-2', 'sources': []}, ] - self.assertEqual(new_serializer.data, expected) + assert new_serializer.data == expected serializer.save() - self.assertEqual(serializer.data, data) + assert serializer.data == data # Ensure target 2 is update, and everything else is as expected queryset = ForeignKeyTarget.objects.all() @@ -275,15 +275,15 @@ class PKForeignKeyTests(TestCase): {'id': 1, 'name': 'target-1', 'sources': [2]}, {'id': 2, 'name': 'target-2', 'sources': [1, 3]}, ] - self.assertEqual(serializer.data, expected) + assert serializer.data == expected def test_foreign_key_create(self): data = {'id': 4, 'name': 'source-4', 'target': 2} serializer = ForeignKeySourceSerializer(data=data) - self.assertTrue(serializer.is_valid()) + assert serializer.is_valid() obj = serializer.save() - self.assertEqual(serializer.data, data) - self.assertEqual(obj.name, 'source-4') + assert serializer.data == data + assert obj.name == 'source-4' # Ensure source 4 is added, and everything else is as expected queryset = ForeignKeySource.objects.all() @@ -294,15 +294,15 @@ class PKForeignKeyTests(TestCase): {'id': 3, 'name': 'source-3', 'target': 1}, {'id': 4, 'name': 'source-4', 'target': 2}, ] - self.assertEqual(serializer.data, expected) + assert serializer.data == expected def test_reverse_foreign_key_create(self): data = {'id': 3, 'name': 'target-3', 'sources': [1, 3]} serializer = ForeignKeyTargetSerializer(data=data) - self.assertTrue(serializer.is_valid()) + assert serializer.is_valid() obj = serializer.save() - self.assertEqual(serializer.data, data) - self.assertEqual(obj.name, 'target-3') + assert serializer.data == data + assert obj.name == 'target-3' # Ensure target 3 is added, and everything else is as expected queryset = ForeignKeyTarget.objects.all() @@ -312,14 +312,14 @@ class PKForeignKeyTests(TestCase): {'id': 2, 'name': 'target-2', 'sources': []}, {'id': 3, 'name': 'target-3', 'sources': [1, 3]}, ] - self.assertEqual(serializer.data, expected) + assert serializer.data == expected def test_foreign_key_update_with_invalid_null(self): data = {'id': 1, 'name': 'source-1', 'target': None} instance = ForeignKeySource.objects.get(pk=1) serializer = ForeignKeySourceSerializer(instance, data=data) - self.assertFalse(serializer.is_valid()) - self.assertEqual(serializer.errors, {'target': ['This field may not be null.']}) + assert not serializer.is_valid() + assert serializer.errors == {'target': ['This field may not be null.']} def test_foreign_key_with_unsaved(self): source = ForeignKeySource(name='source-unsaved') @@ -329,7 +329,7 @@ class PKForeignKeyTests(TestCase): # no query if source hasn't been created yet with self.assertNumQueries(0): - self.assertEqual(serializer.data, expected) + assert serializer.data == expected def test_foreign_key_with_empty(self): """ @@ -338,7 +338,7 @@ class PKForeignKeyTests(TestCase): https://github.com/tomchristie/django-rest-framework/issues/1072 """ serializer = NullableForeignKeySourceSerializer() - self.assertEqual(serializer.data['target'], None) + assert serializer.data['target'] is None def test_foreign_key_not_required(self): """ @@ -350,7 +350,7 @@ class PKForeignKeyTests(TestCase): extra_kwargs = {'target': {'required': False}} serializer = ModelSerializer(data={'name': 'test'}) serializer.is_valid(raise_exception=True) - self.assertNotIn('target', serializer.validated_data) + assert 'target' not in serializer.validated_data class PKNullableForeignKeyTests(TestCase): @@ -371,15 +371,15 @@ class PKNullableForeignKeyTests(TestCase): {'id': 2, 'name': 'source-2', 'target': 1}, {'id': 3, 'name': 'source-3', 'target': None}, ] - self.assertEqual(serializer.data, expected) + assert serializer.data == expected def test_foreign_key_create_with_valid_null(self): data = {'id': 4, 'name': 'source-4', 'target': None} serializer = NullableForeignKeySourceSerializer(data=data) - self.assertTrue(serializer.is_valid()) + assert serializer.is_valid() obj = serializer.save() - self.assertEqual(serializer.data, data) - self.assertEqual(obj.name, 'source-4') + assert serializer.data == data + assert obj.name == 'source-4' # Ensure source 4 is created, and everything else is as expected queryset = NullableForeignKeySource.objects.all() @@ -390,7 +390,7 @@ class PKNullableForeignKeyTests(TestCase): {'id': 3, 'name': 'source-3', 'target': None}, {'id': 4, 'name': 'source-4', 'target': None} ] - self.assertEqual(serializer.data, expected) + assert serializer.data == expected def test_foreign_key_create_with_valid_emptystring(self): """ @@ -400,10 +400,10 @@ class PKNullableForeignKeyTests(TestCase): data = {'id': 4, 'name': 'source-4', 'target': ''} expected_data = {'id': 4, 'name': 'source-4', 'target': None} serializer = NullableForeignKeySourceSerializer(data=data) - self.assertTrue(serializer.is_valid()) + assert serializer.is_valid() obj = serializer.save() - self.assertEqual(serializer.data, expected_data) - self.assertEqual(obj.name, 'source-4') + assert serializer.data == expected_data + assert obj.name == 'source-4' # Ensure source 4 is created, and everything else is as expected queryset = NullableForeignKeySource.objects.all() @@ -414,15 +414,15 @@ class PKNullableForeignKeyTests(TestCase): {'id': 3, 'name': 'source-3', 'target': None}, {'id': 4, 'name': 'source-4', 'target': None} ] - self.assertEqual(serializer.data, expected) + assert serializer.data == expected def test_foreign_key_update_with_valid_null(self): data = {'id': 1, 'name': 'source-1', 'target': None} instance = NullableForeignKeySource.objects.get(pk=1) serializer = NullableForeignKeySourceSerializer(instance, data=data) - self.assertTrue(serializer.is_valid()) + assert serializer.is_valid() serializer.save() - self.assertEqual(serializer.data, data) + assert serializer.data == data # Ensure source 1 is updated, and everything else is as expected queryset = NullableForeignKeySource.objects.all() @@ -432,7 +432,7 @@ class PKNullableForeignKeyTests(TestCase): {'id': 2, 'name': 'source-2', 'target': 1}, {'id': 3, 'name': 'source-3', 'target': None} ] - self.assertEqual(serializer.data, expected) + assert serializer.data == expected def test_foreign_key_update_with_valid_emptystring(self): """ @@ -443,9 +443,9 @@ class PKNullableForeignKeyTests(TestCase): expected_data = {'id': 1, 'name': 'source-1', 'target': None} instance = NullableForeignKeySource.objects.get(pk=1) serializer = NullableForeignKeySourceSerializer(instance, data=data) - self.assertTrue(serializer.is_valid()) + assert serializer.is_valid() serializer.save() - self.assertEqual(serializer.data, expected_data) + assert serializer.data == expected_data # Ensure source 1 is updated, and everything else is as expected queryset = NullableForeignKeySource.objects.all() @@ -455,18 +455,18 @@ class PKNullableForeignKeyTests(TestCase): {'id': 2, 'name': 'source-2', 'target': 1}, {'id': 3, 'name': 'source-3', 'target': None} ] - self.assertEqual(serializer.data, expected) + assert serializer.data == expected def test_null_uuid_foreign_key_serializes_as_none(self): source = NullableUUIDForeignKeySource(name='Source') serializer = NullableUUIDForeignKeySourceSerializer(source) data = serializer.data - self.assertEqual(data["target"], None) + assert data["target"] is None def test_nullable_uuid_foreign_key_is_valid_when_none(self): data = {"name": "Source", "target": None} serializer = NullableUUIDForeignKeySourceSerializer(data=data) - self.assertTrue(serializer.is_valid(), serializer.errors) + assert serializer.is_valid(), serializer.errors class PKNullableOneToOneTests(TestCase): @@ -485,4 +485,4 @@ class PKNullableOneToOneTests(TestCase): {'id': 1, 'name': 'target-1', 'nullable_source': None}, {'id': 2, 'name': 'target-2', 'nullable_source': 1}, ] - self.assertEqual(serializer.data, expected) + assert serializer.data == expected From 9f4c9691f47c534338cdaa4d8a22efc201c8fb62 Mon Sep 17 00:00:00 2001 From: Asif Saifuddin Auvi Date: Mon, 28 Nov 2016 20:31:27 +0600 Subject: [PATCH 051/190] converted filters tests asserts to pytest style (#4711) --- tests/test_filters.py | 300 +++++++++++++++++------------------------- 1 file changed, 124 insertions(+), 176 deletions(-) diff --git a/tests/test_filters.py b/tests/test_filters.py index 12fb85895..0cc326239 100644 --- a/tests/test_filters.py +++ b/tests/test_filters.py @@ -155,8 +155,8 @@ class IntegrationTestFiltering(CommonFilteringTestCase): request = factory.get('/') response = view(request).render() - self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(response.data, self.data) + assert response.status_code == status.HTTP_200_OK + assert response.data == self.data self.assertTrue(issubclass(w[-1].category, PendingDeprecationWarning)) self.assertIn("'rest_framework.filters.DjangoFilterBackend' is pending deprecation.", str(w[-1].message)) @@ -175,9 +175,9 @@ class IntegrationTestFiltering(CommonFilteringTestCase): request = factory.get('/') response = view(request).render() - self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(response.data, self.data) - self.assertEqual(len(w), 0) + assert response.status_code == status.HTTP_200_OK + assert response.data == self.data + assert len(w) == 0 @unittest.skipUnless(django_filters, 'django-filter not installed') def test_get_filtered_fields_root_view(self): @@ -189,24 +189,24 @@ class IntegrationTestFiltering(CommonFilteringTestCase): # Basic test with no filter. request = factory.get('/') response = view(request).render() - self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(response.data, self.data) + assert response.status_code == status.HTTP_200_OK + assert response.data == self.data # Tests that the decimal filter works. search_decimal = Decimal('2.25') request = factory.get('/', {'decimal': '%s' % search_decimal}) response = view(request).render() - self.assertEqual(response.status_code, status.HTTP_200_OK) + assert response.status_code == status.HTTP_200_OK expected_data = [f for f in self.data if Decimal(f['decimal']) == search_decimal] - self.assertEqual(response.data, expected_data) + assert response.data == expected_data # Tests that the date filter works. search_date = datetime.date(2012, 9, 22) request = factory.get('/', {'date': '%s' % search_date}) # search_date str: '2012-09-22' response = view(request).render() - self.assertEqual(response.status_code, status.HTTP_200_OK) + assert response.status_code == status.HTTP_200_OK expected_data = [f for f in self.data if parse_date(f['date']) == search_date] - self.assertEqual(response.data, expected_data) + assert response.data == expected_data @unittest.skipUnless(django_filters, 'django-filter not installed') def test_filter_with_queryset(self): @@ -219,9 +219,9 @@ class IntegrationTestFiltering(CommonFilteringTestCase): search_decimal = Decimal('2.25') request = factory.get('/', {'decimal': '%s' % search_decimal}) response = view(request).render() - self.assertEqual(response.status_code, status.HTTP_200_OK) + assert response.status_code == status.HTTP_200_OK expected_data = [f for f in self.data if Decimal(f['decimal']) == search_decimal] - self.assertEqual(response.data, expected_data) + assert response.data == expected_data @unittest.skipUnless(django_filters, 'django-filter not installed') def test_filter_with_get_queryset_only(self): @@ -245,32 +245,32 @@ class IntegrationTestFiltering(CommonFilteringTestCase): # Basic test with no filter. request = factory.get('/') response = view(request).render() - self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(response.data, self.data) + assert response.status_code == status.HTTP_200_OK + assert response.data == self.data # Tests that the decimal filter set with 'lt' in the filter class works. search_decimal = Decimal('4.25') request = factory.get('/', {'decimal': '%s' % search_decimal}) response = view(request).render() - self.assertEqual(response.status_code, status.HTTP_200_OK) + assert response.status_code == status.HTTP_200_OK expected_data = [f for f in self.data if Decimal(f['decimal']) < search_decimal] - self.assertEqual(response.data, expected_data) + assert response.data == expected_data # Tests that the date filter set with 'gt' in the filter class works. search_date = datetime.date(2012, 10, 2) request = factory.get('/', {'date': '%s' % search_date}) # search_date str: '2012-10-02' response = view(request).render() - self.assertEqual(response.status_code, status.HTTP_200_OK) + assert response.status_code == status.HTTP_200_OK expected_data = [f for f in self.data if parse_date(f['date']) > search_date] - self.assertEqual(response.data, expected_data) + assert response.data == expected_data # Tests that the text filter set with 'icontains' in the filter class works. search_text = 'ff' request = factory.get('/', {'text': '%s' % search_text}) response = view(request).render() - self.assertEqual(response.status_code, status.HTTP_200_OK) + assert response.status_code == status.HTTP_200_OK expected_data = [f for f in self.data if search_text in f['text'].lower()] - self.assertEqual(response.data, expected_data) + assert response.data == expected_data # Tests that multiple filters works. search_decimal = Decimal('5.25') @@ -280,10 +280,10 @@ class IntegrationTestFiltering(CommonFilteringTestCase): 'date': '%s' % (search_date,) }) response = view(request).render() - self.assertEqual(response.status_code, status.HTTP_200_OK) + assert response.status_code == status.HTTP_200_OK expected_data = [f for f in self.data if parse_date(f['date']) > search_date and Decimal(f['decimal']) < search_decimal] - self.assertEqual(response.data, expected_data) + assert response.data == expected_data @unittest.skipUnless(django_filters, 'django-filter not installed') def test_incorrectly_configured_filter(self): @@ -304,8 +304,8 @@ class IntegrationTestFiltering(CommonFilteringTestCase): request = factory.get('/?text=aaa') response = view(request).render() - self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(len(response.data), 1) + assert response.status_code == status.HTTP_200_OK + assert len(response.data) == 1 @unittest.skipUnless(django_filters, 'django-filter not installed') def test_base_model_filter_with_proxy(self): @@ -316,8 +316,8 @@ class IntegrationTestFiltering(CommonFilteringTestCase): request = factory.get('/?text=aaa') response = view(request).render() - self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(len(response.data), 1) + assert response.status_code == status.HTTP_200_OK + assert len(response.data) == 1 @unittest.skipUnless(django_filters, 'django-filter not installed') def test_unknown_filter(self): @@ -329,7 +329,7 @@ class IntegrationTestFiltering(CommonFilteringTestCase): search_integer = 10 request = factory.get('/', {'integer': '%s' % search_integer}) response = view(request).render() - self.assertEqual(response.status_code, status.HTTP_200_OK) + assert response.status_code == status.HTTP_200_OK @override_settings(ROOT_URLCONF='tests.test_filters') @@ -351,8 +351,8 @@ class IntegrationTestDetailFiltering(CommonFilteringTestCase): # Basic test with no filter. response = self.client.get(self._get_url(item)) - self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(response.data, data) + assert response.status_code == status.HTTP_200_OK + assert response.data == data # Tests that the decimal filter set that should fail. search_decimal = Decimal('4.25') @@ -360,7 +360,7 @@ class IntegrationTestDetailFiltering(CommonFilteringTestCase): response = self.client.get( '{url}'.format(url=self._get_url(high_item)), {'decimal': '{param}'.format(param=search_decimal)}) - self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) + assert response.status_code == status.HTTP_404_NOT_FOUND # Tests that the decimal filter set that should succeed. search_decimal = Decimal('4.25') @@ -369,8 +369,8 @@ class IntegrationTestDetailFiltering(CommonFilteringTestCase): response = self.client.get( '{url}'.format(url=self._get_url(low_item)), {'decimal': '{param}'.format(param=search_decimal)}) - self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(response.data, low_item_data) + assert response.status_code == status.HTTP_200_OK + assert response.data == low_item_data # Tests that multiple filters works. search_decimal = Decimal('5.25') @@ -382,8 +382,8 @@ class IntegrationTestDetailFiltering(CommonFilteringTestCase): 'decimal': '{decimal}'.format(decimal=search_decimal), 'date': '{date}'.format(date=search_date) }) - self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(response.data, valid_item_data) + assert response.status_code == status.HTTP_200_OK + assert response.data == valid_item_data class SearchFilterModel(models.Model): @@ -424,13 +424,10 @@ class SearchFilterTests(TestCase): view = SearchListView.as_view() request = factory.get('/', {'search': 'b'}) response = view(request) - self.assertEqual( - response.data, - [ - {'id': 1, 'title': 'z', 'text': 'abc'}, - {'id': 2, 'title': 'zz', 'text': 'bcd'} - ] - ) + assert response.data == [ + {'id': 1, 'title': 'z', 'text': 'abc'}, + {'id': 2, 'title': 'zz', 'text': 'bcd'} + ] def test_exact_search(self): class SearchListView(generics.ListAPIView): @@ -442,12 +439,9 @@ class SearchFilterTests(TestCase): view = SearchListView.as_view() request = factory.get('/', {'search': 'zzz'}) response = view(request) - self.assertEqual( - response.data, - [ - {'id': 3, 'title': 'zzz', 'text': 'cde'} - ] - ) + assert response.data == [ + {'id': 3, 'title': 'zzz', 'text': 'cde'} + ] def test_startswith_search(self): class SearchListView(generics.ListAPIView): @@ -459,12 +453,9 @@ class SearchFilterTests(TestCase): view = SearchListView.as_view() request = factory.get('/', {'search': 'b'}) response = view(request) - self.assertEqual( - response.data, - [ - {'id': 2, 'title': 'zz', 'text': 'bcd'} - ] - ) + assert response.data == [ + {'id': 2, 'title': 'zz', 'text': 'bcd'} + ] def test_regexp_search(self): class SearchListView(generics.ListAPIView): @@ -476,12 +467,9 @@ class SearchFilterTests(TestCase): view = SearchListView.as_view() request = factory.get('/', {'search': 'z{2} ^b'}) response = view(request) - self.assertEqual( - response.data, - [ - {'id': 2, 'title': 'zz', 'text': 'bcd'} - ] - ) + assert response.data == [ + {'id': 2, 'title': 'zz', 'text': 'bcd'} + ] def test_search_with_nonstandard_search_param(self): with override_settings(REST_FRAMEWORK={'SEARCH_PARAM': 'query'}): @@ -496,13 +484,10 @@ class SearchFilterTests(TestCase): view = SearchListView.as_view() request = factory.get('/', {'query': 'b'}) response = view(request) - self.assertEqual( - response.data, - [ - {'id': 1, 'title': 'z', 'text': 'abc'}, - {'id': 2, 'title': 'zz', 'text': 'bcd'} - ] - ) + assert response.data == [ + {'id': 1, 'title': 'z', 'text': 'abc'}, + {'id': 2, 'title': 'zz', 'text': 'bcd'} + ] reload_module(filters) @@ -528,15 +513,13 @@ class SearchFilterFkTests(TestCase): filter_ = filters.SearchFilter() prefixes = [''] + list(filter_.lookup_prefixes) for prefix in prefixes: - self.assertFalse( - filter_.must_call_distinct( - SearchFilterModelFk._meta, ["%stitle" % prefix] - ) + assert not filter_.must_call_distinct( + SearchFilterModelFk._meta, + ["%stitle" % prefix] ) - self.assertFalse( - filter_.must_call_distinct( - SearchFilterModelFk._meta, ["%stitle" % prefix, "%sattribute__label" % prefix] - ) + assert not filter_.must_call_distinct( + SearchFilterModelFk._meta, + ["%stitle" % prefix, "%sattribute__label" % prefix] ) def test_must_call_distinct_restores_meta_for_each_field(self): @@ -545,10 +528,9 @@ class SearchFilterFkTests(TestCase): filter_ = filters.SearchFilter() prefixes = [''] + list(filter_.lookup_prefixes) for prefix in prefixes: - self.assertFalse( - filter_.must_call_distinct( - SearchFilterModelFk._meta, ["%sattribute__label" % prefix, "%stitle" % prefix] - ) + assert not filter_.must_call_distinct( + SearchFilterModelFk._meta, + ["%sattribute__label" % prefix, "%stitle" % prefix] ) @@ -596,21 +578,20 @@ class SearchFilterM2MTests(TestCase): view = SearchListView.as_view() request = factory.get('/', {'search': 'zz'}) response = view(request) - self.assertEqual(len(response.data), 1) + assert len(response.data) == 1 def test_must_call_distinct(self): filter_ = filters.SearchFilter() prefixes = [''] + list(filter_.lookup_prefixes) for prefix in prefixes: - self.assertFalse( - filter_.must_call_distinct( - SearchFilterModelM2M._meta, ["%stitle" % prefix] - ) + assert not filter_.must_call_distinct( + SearchFilterModelM2M._meta, + ["%stitle" % prefix] ) - self.assertTrue( - filter_.must_call_distinct( - SearchFilterModelM2M._meta, ["%stitle" % prefix, "%sattributes__label" % prefix] - ) + + assert filter_.must_call_distinct( + SearchFilterModelM2M._meta, + ["%stitle" % prefix, "%sattributes__label" % prefix] ) @@ -672,14 +653,11 @@ class DjangoFilterOrderingTests(TestCase): request = factory.get('/') response = view(request) - self.assertEqual( - response.data, - [ - {'id': 3, 'date': '2014-10-08', 'text': 'cde'}, - {'id': 2, 'date': '2013-10-08', 'text': 'bcd'}, - {'id': 1, 'date': '2012-10-08', 'text': 'abc'} - ] - ) + assert response.data == [ + {'id': 3, 'date': '2014-10-08', 'text': 'cde'}, + {'id': 2, 'date': '2013-10-08', 'text': 'bcd'}, + {'id': 1, 'date': '2012-10-08', 'text': 'abc'} + ] class OrderingFilterTests(TestCase): @@ -713,14 +691,11 @@ class OrderingFilterTests(TestCase): view = OrderingListView.as_view() request = factory.get('/', {'ordering': 'text'}) response = view(request) - self.assertEqual( - response.data, - [ - {'id': 1, 'title': 'zyx', 'text': 'abc'}, - {'id': 2, 'title': 'yxw', 'text': 'bcd'}, - {'id': 3, 'title': 'xwv', 'text': 'cde'}, - ] - ) + assert response.data == [ + {'id': 1, 'title': 'zyx', 'text': 'abc'}, + {'id': 2, 'title': 'yxw', 'text': 'bcd'}, + {'id': 3, 'title': 'xwv', 'text': 'cde'}, + ] def test_reverse_ordering(self): class OrderingListView(generics.ListAPIView): @@ -733,14 +708,11 @@ class OrderingFilterTests(TestCase): view = OrderingListView.as_view() request = factory.get('/', {'ordering': '-text'}) response = view(request) - self.assertEqual( - response.data, - [ - {'id': 3, 'title': 'xwv', 'text': 'cde'}, - {'id': 2, 'title': 'yxw', 'text': 'bcd'}, - {'id': 1, 'title': 'zyx', 'text': 'abc'}, - ] - ) + assert response.data == [ + {'id': 3, 'title': 'xwv', 'text': 'cde'}, + {'id': 2, 'title': 'yxw', 'text': 'bcd'}, + {'id': 1, 'title': 'zyx', 'text': 'abc'}, + ] def test_incorrectfield_ordering(self): class OrderingListView(generics.ListAPIView): @@ -753,14 +725,11 @@ class OrderingFilterTests(TestCase): view = OrderingListView.as_view() request = factory.get('/', {'ordering': 'foobar'}) response = view(request) - self.assertEqual( - response.data, - [ - {'id': 3, 'title': 'xwv', 'text': 'cde'}, - {'id': 2, 'title': 'yxw', 'text': 'bcd'}, - {'id': 1, 'title': 'zyx', 'text': 'abc'}, - ] - ) + assert response.data == [ + {'id': 3, 'title': 'xwv', 'text': 'cde'}, + {'id': 2, 'title': 'yxw', 'text': 'bcd'}, + {'id': 1, 'title': 'zyx', 'text': 'abc'}, + ] def test_default_ordering(self): class OrderingListView(generics.ListAPIView): @@ -773,14 +742,11 @@ class OrderingFilterTests(TestCase): view = OrderingListView.as_view() request = factory.get('') response = view(request) - self.assertEqual( - response.data, - [ - {'id': 3, 'title': 'xwv', 'text': 'cde'}, - {'id': 2, 'title': 'yxw', 'text': 'bcd'}, - {'id': 1, 'title': 'zyx', 'text': 'abc'}, - ] - ) + assert response.data == [ + {'id': 3, 'title': 'xwv', 'text': 'cde'}, + {'id': 2, 'title': 'yxw', 'text': 'bcd'}, + {'id': 1, 'title': 'zyx', 'text': 'abc'}, + ] def test_default_ordering_using_string(self): class OrderingListView(generics.ListAPIView): @@ -793,14 +759,11 @@ class OrderingFilterTests(TestCase): view = OrderingListView.as_view() request = factory.get('') response = view(request) - self.assertEqual( - response.data, - [ - {'id': 3, 'title': 'xwv', 'text': 'cde'}, - {'id': 2, 'title': 'yxw', 'text': 'bcd'}, - {'id': 1, 'title': 'zyx', 'text': 'abc'}, - ] - ) + assert response.data == [ + {'id': 3, 'title': 'xwv', 'text': 'cde'}, + {'id': 2, 'title': 'yxw', 'text': 'bcd'}, + {'id': 1, 'title': 'zyx', 'text': 'abc'}, + ] def test_ordering_by_aggregate_field(self): # create some related models to aggregate order by @@ -824,14 +787,11 @@ class OrderingFilterTests(TestCase): view = OrderingListView.as_view() request = factory.get('/', {'ordering': 'relateds__count'}) response = view(request) - self.assertEqual( - response.data, - [ - {'id': 1, 'title': 'zyx', 'text': 'abc'}, - {'id': 3, 'title': 'xwv', 'text': 'cde'}, - {'id': 2, 'title': 'yxw', 'text': 'bcd'}, - ] - ) + assert response.data == [ + {'id': 1, 'title': 'zyx', 'text': 'abc'}, + {'id': 3, 'title': 'xwv', 'text': 'cde'}, + {'id': 2, 'title': 'yxw', 'text': 'bcd'}, + ] def test_ordering_with_nonstandard_ordering_param(self): with override_settings(REST_FRAMEWORK={'ORDERING_PARAM': 'order'}): @@ -847,14 +807,11 @@ class OrderingFilterTests(TestCase): view = OrderingListView.as_view() request = factory.get('/', {'order': 'text'}) response = view(request) - self.assertEqual( - response.data, - [ - {'id': 1, 'title': 'zyx', 'text': 'abc'}, - {'id': 2, 'title': 'yxw', 'text': 'bcd'}, - {'id': 3, 'title': 'xwv', 'text': 'cde'}, - ] - ) + assert response.data == [ + {'id': 1, 'title': 'zyx', 'text': 'abc'}, + {'id': 2, 'title': 'yxw', 'text': 'bcd'}, + {'id': 3, 'title': 'xwv', 'text': 'cde'}, + ] reload_module(filters) @@ -884,14 +841,11 @@ class OrderingFilterTests(TestCase): view = OrderingListView.as_view() request = factory.get('/', {'ordering': 'text'}) response = view(request) - self.assertEqual( - response.data, - [ - {'id': 1, 'title': 'zyx', 'text': 'abc'}, - {'id': 2, 'title': 'yxw', 'text': 'bcd'}, - {'id': 3, 'title': 'xwv', 'text': 'cde'}, - ] - ) + assert response.data == [ + {'id': 1, 'title': 'zyx', 'text': 'abc'}, + {'id': 2, 'title': 'yxw', 'text': 'bcd'}, + {'id': 3, 'title': 'xwv', 'text': 'cde'}, + ] def test_ordering_with_improper_configuration(self): class OrderingListView(generics.ListAPIView): @@ -967,14 +921,11 @@ class SensitiveOrderingFilterTests(TestCase): username_field = 'username' # Note: Inverse username ordering correctly applied. - self.assertEqual( - response.data, - [ - {'id': 3, username_field: 'userC'}, - {'id': 2, username_field: 'userB'}, - {'id': 1, username_field: 'userA'}, - ] - ) + assert response.data == [ + {'id': 3, username_field: 'userC'}, + {'id': 2, username_field: 'userB'}, + {'id': 1, username_field: 'userA'}, + ] def test_cannot_order_by_non_serializer_fields(self): for serializer_cls in [ @@ -997,11 +948,8 @@ class SensitiveOrderingFilterTests(TestCase): username_field = 'username' # Note: The passwords are not in order. Default ordering is used. - self.assertEqual( - response.data, - [ - {'id': 1, username_field: 'userA'}, # PassB - {'id': 2, username_field: 'userB'}, # PassC - {'id': 3, username_field: 'userC'}, # PassA - ] - ) + assert response.data == [ + {'id': 1, username_field: 'userA'}, # PassB + {'id': 2, username_field: 'userB'}, # PassC + {'id': 3, username_field: 'userC'}, # PassA + ] From 649876674933c7c05225e8f33a278ce4210df598 Mon Sep 17 00:00:00 2001 From: Ryan P Kilby Date: Tue, 29 Nov 2016 04:49:18 -0500 Subject: [PATCH 052/190] Fix django deprecation warnings (#4712) --- tests/test_model_serializer.py | 4 ++-- tests/test_prefetch_related.py | 3 ++- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/tests/test_model_serializer.py b/tests/test_model_serializer.py index 06848302f..b839f56ca 100644 --- a/tests/test_model_serializer.py +++ b/tests/test_model_serializer.py @@ -94,7 +94,7 @@ class Issue3674ParentModel(models.Model): class Issue3674ChildModel(models.Model): - parent = models.ForeignKey(Issue3674ParentModel, related_name='children') + parent = models.ForeignKey(Issue3674ParentModel, related_name='children', on_delete=models.CASCADE) value = models.CharField(primary_key=True, max_length=64) @@ -1013,7 +1013,7 @@ class Issue3674Test(TestCase): title = models.CharField(max_length=64) class TestChildModel(models.Model): - parent = models.ForeignKey(TestParentModel, related_name='children') + parent = models.ForeignKey(TestParentModel, related_name='children', on_delete=models.CASCADE) value = models.CharField(primary_key=True, max_length=64) class TestChildModelSerializer(serializers.ModelSerializer): diff --git a/tests/test_prefetch_related.py b/tests/test_prefetch_related.py index a9fa238ea..750173b38 100644 --- a/tests/test_prefetch_related.py +++ b/tests/test_prefetch_related.py @@ -2,6 +2,7 @@ from django.contrib.auth.models import Group, User from django.test import TestCase from rest_framework import generics, serializers +from rest_framework.compat import set_many from rest_framework.test import APIRequestFactory factory = APIRequestFactory() @@ -22,7 +23,7 @@ class TestPrefetchRelatedUpdates(TestCase): def setUp(self): self.user = User.objects.create(username='tom', email='tom@example.com') self.groups = [Group.objects.create(name='a'), Group.objects.create(name='b')] - self.user.groups = self.groups + set_many(self.user, 'groups', self.groups) self.user.save() def test_prefetch_related_updates(self): From 1e0988686cda1f9c150e239b945c1d9a0ea9b19c Mon Sep 17 00:00:00 2001 From: Xavier Ordoquy Date: Tue, 29 Nov 2016 13:27:00 +0100 Subject: [PATCH 053/190] Update the Python doc links to use https and point to Python 3 (#4713) --- docs/api-guide/fields.md | 2 +- docs/api-guide/settings.md | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/api-guide/fields.md b/docs/api-guide/fields.md index 17168b721..3dcc9d0a5 100644 --- a/docs/api-guide/fields.md +++ b/docs/api-guide/fields.md @@ -669,7 +669,7 @@ The [django-rest-framework-hstore][django-rest-framework-hstore] package provide [html-and-forms]: ../topics/html-and-forms.md [FILE_UPLOAD_HANDLERS]: https://docs.djangoproject.com/en/dev/ref/settings/#std:setting-FILE_UPLOAD_HANDLERS [ecma262]: http://ecma-international.org/ecma-262/5.1/#sec-15.9.1.15 -[strftime]: http://docs.python.org/2/library/datetime.html#strftime-and-strptime-behavior +[strftime]: https://docs.python.org/3/library/datetime.html#strftime-and-strptime-behavior [django-widgets]: https://docs.djangoproject.com/en/dev/ref/forms/widgets/ [iso8601]: http://www.w3.org/TR/NOTE-datetime [drf-compound-fields]: https://drf-compound-fields.readthedocs.io diff --git a/docs/api-guide/settings.md b/docs/api-guide/settings.md index 58ceeeeb4..a1ea12d6e 100644 --- a/docs/api-guide/settings.md +++ b/docs/api-guide/settings.md @@ -456,7 +456,7 @@ An integer of 0 or more, that may be used to specify the number of application p Default: `None` -[cite]: http://www.python.org/dev/peps/pep-0020/ +[cite]: https://www.python.org/dev/peps/pep-0020/ [rfc4627]: http://www.ietf.org/rfc/rfc4627.txt [heroku-minified-json]: https://github.com/interagent/http-api-design#keep-json-minified-in-all-responses -[strftime]: http://docs.python.org/2/library/time.html#time.strftime +[strftime]: https://docs.python.org/3/library/time.html#time.strftime From aed8387e05a7a665439044d90b7fac5d1fd61225 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Germ=C3=A1n=20Larra=C3=ADn?= Date: Tue, 29 Nov 2016 12:35:43 -0300 Subject: [PATCH 054/190] docs: Fix description of `DecimalField`'s `max_digits` (#4714) As of PR #4377, `max_digits=None` is allowed for `DecimalField`. --- docs/api-guide/fields.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/api-guide/fields.md b/docs/api-guide/fields.md index 3dcc9d0a5..6b6ae612d 100644 --- a/docs/api-guide/fields.md +++ b/docs/api-guide/fields.md @@ -261,7 +261,7 @@ Corresponds to `django.db.models.fields.DecimalField`. **Signature**: `DecimalField(max_digits, decimal_places, coerce_to_string=None, max_value=None, min_value=None)` -- `max_digits` The maximum number of digits allowed in the number. Note that this number must be greater than or equal to decimal_places. +- `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. From 27641e07b5b56ea6fcd4676419e8fd99c58e9c66 Mon Sep 17 00:00:00 2001 From: Asif Saifuddin Auvi Date: Wed, 30 Nov 2016 01:13:21 +0600 Subject: [PATCH 055/190] converted test asserts of generics-test to pytest --- tests/test_generics.py | 102 ++++++++++++++++++++--------------------- 1 file changed, 51 insertions(+), 51 deletions(-) diff --git a/tests/test_generics.py b/tests/test_generics.py index 247237584..2c3679c46 100644 --- a/tests/test_generics.py +++ b/tests/test_generics.py @@ -98,8 +98,8 @@ class TestRootView(TestCase): request = factory.get('/') with self.assertNumQueries(1): response = self.view(request).render() - self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(response.data, self.data) + assert response.status_code == status.HTTP_200_OK + assert response.data == self.data def test_post_root_view(self): """ @@ -109,10 +109,10 @@ class TestRootView(TestCase): request = factory.post('/', data, format='json') with self.assertNumQueries(1): response = self.view(request).render() - self.assertEqual(response.status_code, status.HTTP_201_CREATED) - self.assertEqual(response.data, {'id': 4, 'text': 'foobar'}) + assert response.status_code == status.HTTP_201_CREATED + assert response.data == {'id': 4, 'text': 'foobar'} created = self.objects.get(id=4) - self.assertEqual(created.text, 'foobar') + assert created.text == 'foobar' def test_put_root_view(self): """ @@ -122,8 +122,8 @@ class TestRootView(TestCase): request = factory.put('/', data, format='json') with self.assertNumQueries(0): response = self.view(request).render() - self.assertEqual(response.status_code, status.HTTP_405_METHOD_NOT_ALLOWED) - self.assertEqual(response.data, {"detail": 'Method "PUT" not allowed.'}) + assert response.status_code == status.HTTP_405_METHOD_NOT_ALLOWED + assert response.data == {"detail": 'Method "PUT" not allowed.'} def test_delete_root_view(self): """ @@ -132,8 +132,8 @@ class TestRootView(TestCase): request = factory.delete('/') with self.assertNumQueries(0): response = self.view(request).render() - self.assertEqual(response.status_code, status.HTTP_405_METHOD_NOT_ALLOWED) - self.assertEqual(response.data, {"detail": 'Method "DELETE" not allowed.'}) + assert response.status_code == status.HTTP_405_METHOD_NOT_ALLOWED + assert response.data == {"detail": 'Method "DELETE" not allowed.'} def test_post_cannot_set_id(self): """ @@ -143,10 +143,10 @@ class TestRootView(TestCase): request = factory.post('/', data, format='json') with self.assertNumQueries(1): response = self.view(request).render() - self.assertEqual(response.status_code, status.HTTP_201_CREATED) - self.assertEqual(response.data, {'id': 4, 'text': 'foobar'}) + assert response.status_code == status.HTTP_201_CREATED + assert response.data == {'id': 4, 'text': 'foobar'} created = self.objects.get(id=4) - self.assertEqual(created.text, 'foobar') + assert created.text == 'foobar' def test_post_error_root_view(self): """ @@ -156,7 +156,7 @@ class TestRootView(TestCase): request = factory.post('/', data, HTTP_ACCEPT='text/html') response = self.view(request).render() expected_error = 'Ensure this field has no more than 100 characters.' - self.assertIn(expected_error, response.rendered_content.decode('utf-8')) + assert expected_error in response.rendered_content.decode('utf-8') EXPECTED_QUERIES_FOR_PUT = 2 @@ -185,8 +185,8 @@ class TestInstanceView(TestCase): request = factory.get('/1') with self.assertNumQueries(1): response = self.view(request, pk=1).render() - self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(response.data, self.data[0]) + assert response.status_code == status.HTTP_200_OK + assert response.data == self.data[0] def test_post_instance_view(self): """ @@ -196,8 +196,8 @@ class TestInstanceView(TestCase): request = factory.post('/', data, format='json') with self.assertNumQueries(0): response = self.view(request).render() - self.assertEqual(response.status_code, status.HTTP_405_METHOD_NOT_ALLOWED) - self.assertEqual(response.data, {"detail": 'Method "POST" not allowed.'}) + assert response.status_code == status.HTTP_405_METHOD_NOT_ALLOWED + assert response.data == {"detail": 'Method "POST" not allowed.'} def test_put_instance_view(self): """ @@ -207,10 +207,10 @@ class TestInstanceView(TestCase): request = factory.put('/1', data, format='json') with self.assertNumQueries(EXPECTED_QUERIES_FOR_PUT): response = self.view(request, pk='1').render() - self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(dict(response.data), {'id': 1, 'text': 'foobar'}) + assert response.status_code == status.HTTP_200_OK + assert dict(response.data) == {'id': 1, 'text': 'foobar'} updated = self.objects.get(id=1) - self.assertEqual(updated.text, 'foobar') + assert updated.text == 'foobar' def test_patch_instance_view(self): """ @@ -221,10 +221,10 @@ class TestInstanceView(TestCase): with self.assertNumQueries(EXPECTED_QUERIES_FOR_PUT): response = self.view(request, pk=1).render() - self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(response.data, {'id': 1, 'text': 'foobar'}) + assert response.status_code == status.HTTP_200_OK + assert response.data == {'id': 1, 'text': 'foobar'} updated = self.objects.get(id=1) - self.assertEqual(updated.text, 'foobar') + assert updated.text == 'foobar' def test_delete_instance_view(self): """ @@ -233,10 +233,10 @@ class TestInstanceView(TestCase): request = factory.delete('/1') with self.assertNumQueries(2): response = self.view(request, pk=1).render() - self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT) - self.assertEqual(response.content, six.b('')) + assert response.status_code == status.HTTP_204_NO_CONTENT + assert response.content == six.b('') ids = [obj.id for obj in self.objects.all()] - self.assertEqual(ids, [2, 3]) + assert ids == [2, 3] def test_get_instance_view_incorrect_arg(self): """ @@ -246,7 +246,7 @@ class TestInstanceView(TestCase): request = factory.get('/a') with self.assertNumQueries(0): response = self.view(request, pk='a').render() - self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) + assert response.status_code == status.HTTP_404_NOT_FOUND def test_put_cannot_set_id(self): """ @@ -256,10 +256,10 @@ class TestInstanceView(TestCase): request = factory.put('/1', data, format='json') with self.assertNumQueries(EXPECTED_QUERIES_FOR_PUT): response = self.view(request, pk=1).render() - self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(response.data, {'id': 1, 'text': 'foobar'}) + assert response.status_code == status.HTTP_200_OK + assert response.data == {'id': 1, 'text': 'foobar'} updated = self.objects.get(id=1) - self.assertEqual(updated.text, 'foobar') + assert updated.text == 'foobar' def test_put_to_deleted_instance(self): """ @@ -271,7 +271,7 @@ class TestInstanceView(TestCase): request = factory.put('/1', data, format='json') with self.assertNumQueries(1): response = self.view(request, pk=1).render() - self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) + assert response.status_code == status.HTTP_404_NOT_FOUND def test_put_to_filtered_out_instance(self): """ @@ -282,7 +282,7 @@ class TestInstanceView(TestCase): filtered_out_pk = BasicModel.objects.filter(text='filtered out')[0].pk request = factory.put('/{0}'.format(filtered_out_pk), data, format='json') response = self.view(request, pk=filtered_out_pk).render() - self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) + assert response.status_code == status.HTTP_404_NOT_FOUND def test_patch_cannot_create_an_object(self): """ @@ -292,8 +292,8 @@ class TestInstanceView(TestCase): request = factory.patch('/999', data, format='json') with self.assertNumQueries(1): response = self.view(request, pk=999).render() - self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) - self.assertFalse(self.objects.filter(id=999).exists()) + assert response.status_code == status.HTTP_404_NOT_FOUND + assert not self.objects.filter(id=999).exists() def test_put_error_instance_view(self): """ @@ -303,7 +303,7 @@ class TestInstanceView(TestCase): request = factory.put('/', data, HTTP_ACCEPT='text/html') response = self.view(request, pk=1).render() expected_error = 'Ensure this field has no more than 100 characters.' - self.assertIn(expected_error, response.rendered_content.decode('utf-8')) + assert expected_error in response.rendered_content.decode('utf-8') class TestFKInstanceView(TestCase): @@ -363,8 +363,8 @@ class TestOverriddenGetObject(TestCase): request = factory.get('/1') with self.assertNumQueries(1): response = self.view(request, pk=1).render() - self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(response.data, self.data[0]) + assert response.status_code == status.HTTP_200_OK + assert response.data == self.data[0] # Regression test for #285 @@ -394,9 +394,9 @@ class TestCreateModelWithAutoNowAddField(TestCase): data = {'email': 'foobar@example.com', 'content': 'foobar'} request = factory.post('/', data, format='json') response = self.view(request).render() - self.assertEqual(response.status_code, status.HTTP_201_CREATED) + assert response.status_code == status.HTTP_201_CREATED created = self.objects.get(id=1) - self.assertEqual(created.content, 'foobar') + assert created.content == 'foobar' # Test for particularly ugly regression with m2m in browsable API @@ -432,7 +432,7 @@ class TestM2MBrowsableAPI(TestCase): request = factory.get('/', HTTP_ACCEPT='text/html') view = ExampleView().as_view() response = view(request).render() - self.assertEqual(response.status_code, status.HTTP_200_OK) + assert response.status_code == status.HTTP_200_OK class InclusiveFilterBackend(object): @@ -489,9 +489,9 @@ class TestFilterBackendAppliedToViews(TestCase): root_view = RootView.as_view(filter_backends=(InclusiveFilterBackend,)) request = factory.get('/') response = root_view(request).render() - self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(len(response.data), 1) - self.assertEqual(response.data, [{'id': 1, 'text': 'foo'}]) + assert response.status_code == status.HTTP_200_OK + assert len(response.data) == 1 + assert response.data == [{'id': 1, 'text': 'foo'}] def test_get_root_view_filters_out_all_models_with_exclusive_filter_backend(self): """ @@ -500,8 +500,8 @@ class TestFilterBackendAppliedToViews(TestCase): root_view = RootView.as_view(filter_backends=(ExclusiveFilterBackend,)) request = factory.get('/') response = root_view(request).render() - self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(response.data, []) + assert response.status_code == status.HTTP_200_OK + assert response.data == [] def test_get_instance_view_filters_out_name_with_filter_backend(self): """ @@ -510,8 +510,8 @@ class TestFilterBackendAppliedToViews(TestCase): instance_view = InstanceView.as_view(filter_backends=(ExclusiveFilterBackend,)) request = factory.get('/1') response = instance_view(request, pk=1).render() - self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) - self.assertEqual(response.data, {'detail': 'Not found.'}) + assert response.status_code == status.HTTP_404_NOT_FOUND + assert response.data == {'detail': 'Not found.'} def test_get_instance_view_will_return_single_object_when_filter_does_not_exclude_it(self): """ @@ -520,8 +520,8 @@ class TestFilterBackendAppliedToViews(TestCase): instance_view = InstanceView.as_view(filter_backends=(InclusiveFilterBackend,)) request = factory.get('/1') response = instance_view(request, pk=1).render() - self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(response.data, {'id': 1, 'text': 'foo'}) + assert response.status_code == status.HTTP_200_OK + assert response.data == {'id': 1, 'text': 'foo'} def test_dynamic_serializer_form_in_browsable_api(self): """ @@ -530,8 +530,8 @@ class TestFilterBackendAppliedToViews(TestCase): view = DynamicSerializerView.as_view() request = factory.get('/') response = view(request).render() - self.assertContains(response, 'field_b') - self.assertNotContains(response, 'field_a') + assert 'field_b' in response + assert 'field_a' not in response class TestGuardedQueryset(TestCase): From 4a0829d6ec58b8b2fd12c36a34cdcc319def2695 Mon Sep 17 00:00:00 2001 From: Asif Saifuddin Auvi Date: Wed, 30 Nov 2016 02:08:37 +0600 Subject: [PATCH 056/190] attempt to fix test --- tests/test_generics.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_generics.py b/tests/test_generics.py index 2c3679c46..67191d329 100644 --- a/tests/test_generics.py +++ b/tests/test_generics.py @@ -530,7 +530,7 @@ class TestFilterBackendAppliedToViews(TestCase): view = DynamicSerializerView.as_view() request = factory.get('/') response = view(request).render() - assert 'field_b' in response + assert response is 'field_b' assert 'field_a' not in response From a5c8a8c2265c6462208c2defc0b943bc51ace6df Mon Sep 17 00:00:00 2001 From: Jeff Fein-Worton Date: Tue, 29 Nov 2016 18:00:10 -0800 Subject: [PATCH 057/190] typo --- docs/api-guide/permissions.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/api-guide/permissions.md b/docs/api-guide/permissions.md index 7cdb59531..f2d2fcaae 100644 --- a/docs/api-guide/permissions.md +++ b/docs/api-guide/permissions.md @@ -164,7 +164,7 @@ As with `DjangoModelPermissions`, this permission must only be applied to views Note that `DjangoObjectPermissions` **does not** require the `django-guardian` package, and should support other object-level backends equally well. -As with `DjangoModelPermissions` you can use custom model permissions by overriding `DjangoModelPermissions` and setting the `.perms_map` property. Refer to the source code for details. +As with `DjangoModelPermissions` you can use custom model permissions by overriding `DjangoObjectPermissions` and setting the `.perms_map` property. Refer to the source code for details. --- From a5bb9825f3b238d89fdb5d8754d057116fff56a9 Mon Sep 17 00:00:00 2001 From: Asif Saifuddin Auvi Date: Wed, 30 Nov 2016 09:56:22 +0600 Subject: [PATCH 058/190] attempt to fix test again --- tests/test_generics.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_generics.py b/tests/test_generics.py index 67191d329..2c3679c46 100644 --- a/tests/test_generics.py +++ b/tests/test_generics.py @@ -530,7 +530,7 @@ class TestFilterBackendAppliedToViews(TestCase): view = DynamicSerializerView.as_view() request = factory.get('/') response = view(request).render() - assert response is 'field_b' + assert 'field_b' in response assert 'field_a' not in response From f5a900a404a85163ba36ec34034d0bacd626b010 Mon Sep 17 00:00:00 2001 From: Asif Saifuddin Auvi Date: Wed, 30 Nov 2016 10:01:37 +0600 Subject: [PATCH 059/190] some reverts to fix test --- tests/test_generics.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_generics.py b/tests/test_generics.py index 2c3679c46..aa3951154 100644 --- a/tests/test_generics.py +++ b/tests/test_generics.py @@ -530,8 +530,8 @@ class TestFilterBackendAppliedToViews(TestCase): view = DynamicSerializerView.as_view() request = factory.get('/') response = view(request).render() - assert 'field_b' in response - assert 'field_a' not in response + self.assertContains(response, 'field_b') + self.assertNotContains(response, 'field_a') class TestGuardedQueryset(TestCase): From 10b5f36fec92871e1d96c85a008324683a32f6b5 Mon Sep 17 00:00:00 2001 From: Asif Saifuddin Auvi Date: Wed, 30 Nov 2016 12:35:34 +0600 Subject: [PATCH 060/190] added fixes --- tests/test_generics.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/tests/test_generics.py b/tests/test_generics.py index aa3951154..c24cda006 100644 --- a/tests/test_generics.py +++ b/tests/test_generics.py @@ -530,8 +530,9 @@ class TestFilterBackendAppliedToViews(TestCase): view = DynamicSerializerView.as_view() request = factory.get('/') response = view(request).render() - self.assertContains(response, 'field_b') - self.assertNotContains(response, 'field_a') + content = response.content.decode('utf8') + assert 'field_b' in content + assert 'field_a' not in content class TestGuardedQueryset(TestCase): From e03d88ced7f2b22f96f50c2a885264147b242b4a Mon Sep 17 00:00:00 2001 From: Asif Saifuddin Auvi Date: Wed, 30 Nov 2016 15:48:33 +0600 Subject: [PATCH 061/190] more pytest style assert (#4719) --- tests/test_validation.py | 38 +++++++++++++++++++--------------- tests/test_validation_error.py | 16 +++++++------- 2 files changed, 29 insertions(+), 25 deletions(-) diff --git a/tests/test_validation.py b/tests/test_validation.py index bc950dd22..8ff4aaf38 100644 --- a/tests/test_validation.py +++ b/tests/test_validation.py @@ -60,11 +60,11 @@ class TestNestedValidationError(TestCase): } }) - self.assertEqual(serializers.as_serializer_error(e), { + assert serializers.as_serializer_error(e) == { 'nested': { 'field': ['error'], } - }) + } class TestPreSaveValidationExclusionsSerializer(TestCase): @@ -75,20 +75,20 @@ class TestPreSaveValidationExclusionsSerializer(TestCase): # We've set `required=False` on the serializer, but the model # does not have `blank=True`, so this serializer should not validate. serializer = ShouldValidateModelSerializer(data={'renamed': ''}) - self.assertEqual(serializer.is_valid(), False) - self.assertIn('renamed', serializer.errors) - self.assertNotIn('should_validate_field', serializer.errors) + assert serializer.is_valid() is False + assert 'renamed' in serializer.errors + assert 'should_validate_field' not in serializer.errors class TestCustomValidationMethods(TestCase): def test_custom_validation_method_is_executed(self): serializer = ShouldValidateModelSerializer(data={'renamed': 'fo'}) - self.assertFalse(serializer.is_valid()) - self.assertIn('renamed', serializer.errors) + assert not serializer.is_valid() + assert 'renamed' in serializer.errors def test_custom_validation_method_passing(self): serializer = ShouldValidateModelSerializer(data={'renamed': 'foo'}) - self.assertTrue(serializer.is_valid()) + assert serializer.is_valid() class ValidationSerializer(serializers.Serializer): @@ -108,12 +108,12 @@ class TestAvoidValidation(TestCase): """ def test_serializer_errors_has_only_invalid_data_error(self): serializer = ValidationSerializer(data='invalid data') - self.assertFalse(serializer.is_valid()) - self.assertDictEqual(serializer.errors, { + assert not serializer.is_valid() + assert serializer.errors == { 'non_field_errors': [ 'Invalid data. Expected a dictionary, but got %s.' % type('').__name__ ] - }) + } # regression tests for issue: 1493 @@ -137,27 +137,31 @@ class TestMaxValueValidatorValidation(TestCase): def test_max_value_validation_serializer_success(self): serializer = ValidationMaxValueValidatorModelSerializer(data={'number_value': 99}) - self.assertTrue(serializer.is_valid()) + assert serializer.is_valid() def test_max_value_validation_serializer_fails(self): serializer = ValidationMaxValueValidatorModelSerializer(data={'number_value': 101}) - self.assertFalse(serializer.is_valid()) - self.assertDictEqual({'number_value': ['Ensure this value is less than or equal to 100.']}, serializer.errors) + assert not serializer.is_valid() + assert serializer.errors == { + 'number_value': [ + 'Ensure this value is less than or equal to 100.' + ] + } def test_max_value_validation_success(self): obj = ValidationMaxValueValidatorModel.objects.create(number_value=100) request = factory.patch('/{0}'.format(obj.pk), {'number_value': 98}, format='json') view = UpdateMaxValueValidationModel().as_view() response = view(request, pk=obj.pk).render() - self.assertEqual(response.status_code, status.HTTP_200_OK) + assert response.status_code == status.HTTP_200_OK def test_max_value_validation_fail(self): obj = ValidationMaxValueValidatorModel.objects.create(number_value=100) request = factory.patch('/{0}'.format(obj.pk), {'number_value': 101}, format='json') view = UpdateMaxValueValidationModel().as_view() response = view(request, pk=obj.pk).render() - self.assertEqual(response.content, b'{"number_value":["Ensure this value is less than or equal to 100."]}') - self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) + assert response.content == b'{"number_value":["Ensure this value is less than or equal to 100."]}' + assert response.status_code == status.HTTP_400_BAD_REQUEST # regression tests for issue: 1533 diff --git a/tests/test_validation_error.py b/tests/test_validation_error.py index 8e371a349..562fe37e6 100644 --- a/tests/test_validation_error.py +++ b/tests/test_validation_error.py @@ -54,16 +54,16 @@ class TestValidationErrorWithFullDetails(TestCase): request = factory.get('/', content_type='application/json') response = view(request) - self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) - self.assertEqual(response.data, self.expected_response_data) + assert response.status_code == status.HTTP_400_BAD_REQUEST + assert response.data == self.expected_response_data def test_function_based_view_exception_handler(self): view = error_view request = factory.get('/', content_type='application/json') response = view(request) - self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) - self.assertEqual(response.data, self.expected_response_data) + assert response.status_code == status.HTTP_400_BAD_REQUEST + assert response.data == self.expected_response_data class TestValidationErrorWithCodes(TestCase): @@ -89,13 +89,13 @@ class TestValidationErrorWithCodes(TestCase): request = factory.get('/', content_type='application/json') response = view(request) - self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) - self.assertEqual(response.data, self.expected_response_data) + assert response.status_code == status.HTTP_400_BAD_REQUEST + assert response.data == self.expected_response_data def test_function_based_view_exception_handler(self): view = error_view request = factory.get('/', content_type='application/json') response = view(request) - self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) - self.assertEqual(response.data, self.expected_response_data) + assert response.status_code == status.HTTP_400_BAD_REQUEST + assert response.data == self.expected_response_data From 504f4b44c6dc96e1c1dd2cece3e9d3ac1052011d Mon Sep 17 00:00:00 2001 From: Asif Saifuddin Auvi Date: Wed, 30 Nov 2016 16:17:30 +0600 Subject: [PATCH 062/190] converted asserts of atomic requests test to pytest --- tests/test_atomic_requests.py | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/tests/test_atomic_requests.py b/tests/test_atomic_requests.py index 09d7f2fb1..9085bfc89 100644 --- a/tests/test_atomic_requests.py +++ b/tests/test_atomic_requests.py @@ -67,8 +67,8 @@ class DBTransactionTests(TestCase): with self.assertNumQueries(1): response = self.view(request) - self.assertFalse(transaction.get_rollback()) - self.assertEqual(response.status_code, status.HTTP_200_OK) + assert not transaction.get_rollback() + assert response.status_code == status.HTTP_200_OK assert BasicModel.objects.count() == 1 @@ -98,7 +98,7 @@ class DBTransactionErrorTests(TestCase): # 3 - release savepoint with transaction.atomic(): self.assertRaises(Exception, self.view, request) - self.assertFalse(transaction.get_rollback()) + assert not transaction.get_rollback() assert BasicModel.objects.count() == 1 @@ -128,9 +128,8 @@ class DBTransactionAPIExceptionTests(TestCase): # 4 - release savepoint (django>=1.8 only) with transaction.atomic(): response = self.view(request) - self.assertTrue(transaction.get_rollback()) - self.assertEqual(response.status_code, - status.HTTP_500_INTERNAL_SERVER_ERROR) + assert transaction.get_rollback() + assert response.status_code == status.HTTP_500_INTERNAL_SERVER_ERROR assert BasicModel.objects.count() == 0 @@ -151,5 +150,4 @@ class NonAtomicDBTransactionAPIExceptionTests(TransactionTestCase): # without checking connection.in_atomic_block view raises 500 # due attempt to rollback without transaction - self.assertEqual(response.status_code, - status.HTTP_404_NOT_FOUND) + assert response.status_code == status.HTTP_404_NOT_FOUND From a9b6c974852c7b5c6fb95c586fdab9fbe29e73f7 Mon Sep 17 00:00:00 2001 From: Asif Saifuddin Auvi Date: Wed, 30 Nov 2016 16:24:48 +0600 Subject: [PATCH 063/190] converted asserts of decorators test to pytest --- tests/test_decorators.py | 30 ++++++++++++++---------------- 1 file changed, 14 insertions(+), 16 deletions(-) diff --git a/tests/test_decorators.py b/tests/test_decorators.py index 46e4a6ad7..b187e5fd6 100644 --- a/tests/test_decorators.py +++ b/tests/test_decorators.py @@ -56,11 +56,11 @@ class DecoratorTestCase(TestCase): request = self.factory.get('/') response = view(request) - self.assertEqual(response.status_code, status.HTTP_200_OK) + assert response.status_code == status.HTTP_200_OK request = self.factory.post('/') response = view(request) - self.assertEqual(response.status_code, status.HTTP_405_METHOD_NOT_ALLOWED) + assert response.status_code == status.HTTP_405_METHOD_NOT_ALLOWED def test_calling_put_method(self): @@ -70,11 +70,11 @@ class DecoratorTestCase(TestCase): request = self.factory.put('/') response = view(request) - self.assertEqual(response.status_code, status.HTTP_200_OK) + assert response.status_code == status.HTTP_200_OK request = self.factory.post('/') response = view(request) - self.assertEqual(response.status_code, status.HTTP_405_METHOD_NOT_ALLOWED) + assert response.status_code == status.HTTP_405_METHOD_NOT_ALLOWED def test_calling_patch_method(self): @@ -84,11 +84,11 @@ class DecoratorTestCase(TestCase): request = self.factory.patch('/') response = view(request) - self.assertEqual(response.status_code, status.HTTP_200_OK) + assert response.status_code == status.HTTP_200_OK request = self.factory.post('/') response = view(request) - self.assertEqual(response.status_code, status.HTTP_405_METHOD_NOT_ALLOWED) + assert response.status_code == status.HTTP_405_METHOD_NOT_ALLOWED def test_renderer_classes(self): @@ -99,16 +99,15 @@ class DecoratorTestCase(TestCase): request = self.factory.get('/') response = view(request) - self.assertTrue(isinstance(response.accepted_renderer, JSONRenderer)) + assert isinstance(response.accepted_renderer, JSONRenderer) def test_parser_classes(self): @api_view(['GET']) @parser_classes([JSONParser]) def view(request): - self.assertEqual(len(request.parsers), 1) - self.assertTrue(isinstance(request.parsers[0], - JSONParser)) + assert len(request.parsers) == 1 + assert isinstance(request.parsers[0], JSONParser) return Response({}) request = self.factory.get('/') @@ -119,9 +118,8 @@ class DecoratorTestCase(TestCase): @api_view(['GET']) @authentication_classes([BasicAuthentication]) def view(request): - self.assertEqual(len(request.authenticators), 1) - self.assertTrue(isinstance(request.authenticators[0], - BasicAuthentication)) + assert len(request.authenticators) == 1 + assert isinstance(request.authenticators[0], BasicAuthentication) return Response({}) request = self.factory.get('/') @@ -136,7 +134,7 @@ class DecoratorTestCase(TestCase): request = self.factory.get('/') response = view(request) - self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) + assert response.status_code == status.HTTP_403_FORBIDDEN def test_throttle_classes(self): class OncePerDayUserThrottle(UserRateThrottle): @@ -149,7 +147,7 @@ class DecoratorTestCase(TestCase): request = self.factory.get('/') response = view(request) - self.assertEqual(response.status_code, status.HTTP_200_OK) + assert response.status_code == status.HTTP_200_OK response = view(request) - self.assertEqual(response.status_code, status.HTTP_429_TOO_MANY_REQUESTS) + assert response.status_code == status.HTTP_429_TOO_MANY_REQUESTS From 9a3f8d9a9c63fc14a4e13738407f2b262de6649a Mon Sep 17 00:00:00 2001 From: Asif Saifuddin Auvi Date: Wed, 30 Nov 2016 16:42:43 +0600 Subject: [PATCH 064/190] converted asserts of descriptions test to pytest --- tests/test_description.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/tests/test_description.py b/tests/test_description.py index fcb88287b..08d8bddec 100644 --- a/tests/test_description.py +++ b/tests/test_description.py @@ -60,7 +60,7 @@ class TestViewNamesAndDescriptions(TestCase): """ class MockView(APIView): pass - self.assertEqual(MockView().get_view_name(), 'Mock') + assert MockView().get_view_name() == 'Mock' def test_view_description_uses_docstring(self): """Ensure view descriptions are based on the docstring.""" @@ -80,7 +80,7 @@ class TestViewNamesAndDescriptions(TestCase): # hash style header #""" - self.assertEqual(MockView().get_view_description(), DESCRIPTION) + assert MockView().get_view_description() == DESCRIPTION def test_view_description_can_be_empty(self): """ @@ -89,7 +89,7 @@ class TestViewNamesAndDescriptions(TestCase): """ class MockView(APIView): pass - self.assertEqual(MockView().get_view_description(), '') + assert MockView().get_view_description() == '' def test_view_description_can_be_promise(self): """ @@ -111,7 +111,7 @@ class TestViewNamesAndDescriptions(TestCase): class MockView(APIView): __doc__ = MockLazyStr("a gettext string") - self.assertEqual(MockView().get_view_description(), 'a gettext string') + assert MockView().get_view_description() == 'a gettext string' def test_markdown(self): """ @@ -120,7 +120,7 @@ class TestViewNamesAndDescriptions(TestCase): if apply_markdown: gte_21_match = apply_markdown(DESCRIPTION) == MARKED_DOWN_gte_21 lt_21_match = apply_markdown(DESCRIPTION) == MARKED_DOWN_lt_21 - self.assertTrue(gte_21_match or lt_21_match) + assert gte_21_match or lt_21_match def test_dedent_tabs(): From 7e8b01dbd2e780b1204d8c362dcb40d31d1b27d5 Mon Sep 17 00:00:00 2001 From: Asif Saifuddin Auvi Date: Wed, 30 Nov 2016 16:45:48 +0600 Subject: [PATCH 065/190] converted asserts of encoders test to pytest --- tests/test_encoders.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/tests/test_encoders.py b/tests/test_encoders.py index d6f681932..687141476 100644 --- a/tests/test_encoders.py +++ b/tests/test_encoders.py @@ -20,21 +20,21 @@ class JSONEncoderTests(TestCase): Tests encoding a decimal """ d = Decimal(3.14) - self.assertEqual(d, float(d)) + assert d == float(d) def test_encode_datetime(self): """ Tests encoding a datetime object """ current_time = datetime.now() - self.assertEqual(self.encoder.default(current_time), current_time.isoformat()) + assert self.encoder.default(current_time) == current_time.isoformat() def test_encode_time(self): """ Tests encoding a timezone """ current_time = datetime.now().time() - self.assertEqual(self.encoder.default(current_time), current_time.isoformat()[:12]) + assert self.encoder.default(current_time) == current_time.isoformat()[:12] def test_encode_time_tz(self): """ @@ -64,18 +64,18 @@ class JSONEncoderTests(TestCase): Tests encoding a date object """ current_date = date.today() - self.assertEqual(self.encoder.default(current_date), current_date.isoformat()) + assert self.encoder.default(current_date) == current_date.isoformat() def test_encode_timedelta(self): """ Tests encoding a timedelta object """ delta = timedelta(hours=1) - self.assertEqual(self.encoder.default(delta), str(delta.total_seconds())) + assert self.encoder.default(delta) == str(delta.total_seconds()) def test_encode_uuid(self): """ Tests encoding a UUID object """ unique_id = uuid4() - self.assertEqual(self.encoder.default(unique_id), str(unique_id)) + assert self.encoder.default(unique_id) == str(unique_id) From 1a741bb2a2759d9b20afc6160b5c7d58ec5ff382 Mon Sep 17 00:00:00 2001 From: Asif Saifuddin Auvi Date: Wed, 30 Nov 2016 17:12:01 +0600 Subject: [PATCH 066/190] converted asserts of exceptions test to pytest (#4723) --- tests/test_exceptions.py | 18 ++++++------------ 1 file changed, 6 insertions(+), 12 deletions(-) diff --git a/tests/test_exceptions.py b/tests/test_exceptions.py index f1d172211..8b5628ef2 100644 --- a/tests/test_exceptions.py +++ b/tests/test_exceptions.py @@ -16,28 +16,22 @@ class ExceptionTestCase(TestCase): example = "string" lazy_example = _(example) - self.assertEqual( - _get_error_details(lazy_example), - example - ) + assert _get_error_details(lazy_example) == example + assert isinstance( _get_error_details(lazy_example), ErrorDetail ) - self.assertEqual( - _get_error_details({'nested': lazy_example})['nested'], - example - ) + assert _get_error_details({'nested': lazy_example})['nested'] == example + assert isinstance( _get_error_details({'nested': lazy_example})['nested'], ErrorDetail ) - self.assertEqual( - _get_error_details([[lazy_example]])[0][0], - example - ) + assert _get_error_details([[lazy_example]])[0][0] == example + assert isinstance( _get_error_details([[lazy_example]])[0][0], ErrorDetail From 4f6c326a9955e70e64eb83499015bba10175468a Mon Sep 17 00:00:00 2001 From: Asif Saifuddin Auvi Date: Wed, 30 Nov 2016 18:52:32 +0600 Subject: [PATCH 067/190] converted remaining unittes asserts of fields test to pytest (#4724) --- tests/test_fields.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/tests/test_fields.py b/tests/test_fields.py index 92030e3ca..069ba879d 100644 --- a/tests/test_fields.py +++ b/tests/test_fields.py @@ -1014,16 +1014,16 @@ class TestLocalizedDecimalField(TestCase): @override_settings(USE_L10N=True, LANGUAGE_CODE='pl') def test_to_internal_value(self): field = serializers.DecimalField(max_digits=2, decimal_places=1, localize=True) - self.assertEqual(field.to_internal_value('1,1'), Decimal('1.1')) + assert field.to_internal_value('1,1') == Decimal('1.1') @override_settings(USE_L10N=True, LANGUAGE_CODE='pl') def test_to_representation(self): field = serializers.DecimalField(max_digits=2, decimal_places=1, localize=True) - self.assertEqual(field.to_representation(Decimal('1.1')), '1,1') + assert field.to_representation(Decimal('1.1')) == '1,1' def test_localize_forces_coerce_to_string(self): field = serializers.DecimalField(max_digits=2, decimal_places=1, coerce_to_string=False, localize=True) - self.assertTrue(isinstance(field.to_representation(Decimal('1.1')), six.string_types)) + assert isinstance(field.to_representation(Decimal('1.1')), six.string_types) class TestQuantizedValueForDecimal(TestCase): @@ -1031,19 +1031,19 @@ class TestQuantizedValueForDecimal(TestCase): field = serializers.DecimalField(max_digits=4, decimal_places=2) value = field.to_internal_value(12).as_tuple() expected_digit_tuple = (0, (1, 2, 0, 0), -2) - self.assertEqual(value, expected_digit_tuple) + assert value == expected_digit_tuple def test_string_quantized_value_for_decimal(self): field = serializers.DecimalField(max_digits=4, decimal_places=2) value = field.to_internal_value('12').as_tuple() expected_digit_tuple = (0, (1, 2, 0, 0), -2) - self.assertEqual(value, expected_digit_tuple) + assert value == expected_digit_tuple def test_part_precision_string_quantized_value_for_decimal(self): field = serializers.DecimalField(max_digits=4, decimal_places=2) value = field.to_internal_value('12.0').as_tuple() expected_digit_tuple = (0, (1, 2, 0, 0), -2) - self.assertEqual(value, expected_digit_tuple) + assert value == expected_digit_tuple class TestNoDecimalPlaces(FieldValues): From 22578525eff075e329e8840c42c358d59105a0d3 Mon Sep 17 00:00:00 2001 From: Xavier Ordoquy Date: Wed, 30 Nov 2016 13:58:34 +0100 Subject: [PATCH 068/190] Documentation update (#4717) --- docs/api-guide/authentication.md | 2 +- docs/api-guide/fields.md | 6 +++--- docs/api-guide/filtering.md | 4 ++-- docs/api-guide/generic-views.md | 2 +- docs/api-guide/pagination.md | 2 +- docs/api-guide/parsers.md | 2 +- docs/api-guide/permissions.md | 4 ++-- docs/api-guide/relations.md | 6 +++--- docs/api-guide/renderers.md | 4 ++-- docs/api-guide/responses.md | 2 +- docs/api-guide/reverse.md | 4 ++-- docs/api-guide/schemas.md | 4 ++-- docs/api-guide/serializers.md | 2 +- docs/api-guide/testing.md | 4 ++-- docs/api-guide/throttling.md | 4 ++-- docs/api-guide/validators.md | 2 +- docs/topics/2.2-announcement.md | 8 ++++---- docs/topics/2.4-announcement.md | 2 +- docs/topics/3.0-announcement.md | 4 ++-- docs/topics/ajax-csrf-cors.md | 2 +- docs/topics/release-notes.md | 2 +- 21 files changed, 36 insertions(+), 36 deletions(-) diff --git a/docs/api-guide/authentication.md b/docs/api-guide/authentication.md index bf3a31eb7..4a01188f3 100644 --- a/docs/api-guide/authentication.md +++ b/docs/api-guide/authentication.md @@ -363,7 +363,7 @@ HTTP Signature (currently a [IETF draft][http-signature-ietf-draft]) provides a [oauth]: http://oauth.net/2/ [permission]: permissions.md [throttling]: throttling.md -[csrf-ajax]: https://docs.djangoproject.com/en/dev/ref/csrf/#ajax +[csrf-ajax]: https://docs.djangoproject.com/en/stable/ref/csrf/#ajax [mod_wsgi_official]: http://code.google.com/p/modwsgi/wiki/ConfigurationDirectives#WSGIPassAuthorization [django-oauth-toolkit-getting-started]: https://django-oauth-toolkit.readthedocs.io/en/latest/rest-framework/getting_started.html [django-rest-framework-oauth]: http://jpadilla.github.io/django-rest-framework-oauth/ diff --git a/docs/api-guide/fields.md b/docs/api-guide/fields.md index 6b6ae612d..b527b016b 100644 --- a/docs/api-guide/fields.md +++ b/docs/api-guide/fields.md @@ -665,12 +665,12 @@ The [django-rest-framework-gis][django-rest-framework-gis] package provides geog The [django-rest-framework-hstore][django-rest-framework-hstore] package provides an `HStoreField` to support [django-hstore][django-hstore] `DictionaryField` model field. -[cite]: https://docs.djangoproject.com/en/dev/ref/forms/api/#django.forms.Form.cleaned_data +[cite]: https://docs.djangoproject.com/en/stable/ref/forms/api/#django.forms.Form.cleaned_data [html-and-forms]: ../topics/html-and-forms.md -[FILE_UPLOAD_HANDLERS]: https://docs.djangoproject.com/en/dev/ref/settings/#std:setting-FILE_UPLOAD_HANDLERS +[FILE_UPLOAD_HANDLERS]: https://docs.djangoproject.com/en/stable/ref/settings/#std:setting-FILE_UPLOAD_HANDLERS [ecma262]: http://ecma-international.org/ecma-262/5.1/#sec-15.9.1.15 [strftime]: https://docs.python.org/3/library/datetime.html#strftime-and-strptime-behavior -[django-widgets]: https://docs.djangoproject.com/en/dev/ref/forms/widgets/ +[django-widgets]: https://docs.djangoproject.com/en/stable/ref/forms/widgets/ [iso8601]: http://www.w3.org/TR/NOTE-datetime [drf-compound-fields]: https://drf-compound-fields.readthedocs.io [drf-extra-fields]: https://github.com/Hipo/drf-extra-fields diff --git a/docs/api-guide/filtering.md b/docs/api-guide/filtering.md index 3f212ced3..8a23a2ea3 100644 --- a/docs/api-guide/filtering.md +++ b/docs/api-guide/filtering.md @@ -455,14 +455,14 @@ The [djangorestframework-word-filter][django-rest-framework-word-search-filter] [drf-url-filter][drf-url-filter] is a simple Django app to apply filters on drf `ModelViewSet`'s `Queryset` in a clean, simple and configurable way. It also supports validations on incoming query params and their values. A beautiful python package `Voluptuous` is being used for validations on the incoming query parameters. The best part about voluptuous is you can define your own validations as per your query params requirements. -[cite]: https://docs.djangoproject.com/en/dev/topics/db/queries/#retrieving-specific-objects-with-filters +[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 [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 [nullbooleanselect]: https://github.com/django/django/blob/master/django/forms/widgets.py -[search-django-admin]: https://docs.djangoproject.com/en/dev/ref/contrib/admin/#django.contrib.admin.ModelAdmin.search_fields +[search-django-admin]: https://docs.djangoproject.com/en/stable/ref/contrib/admin/#django.contrib.admin.ModelAdmin.search_fields [django-rest-framework-filters]: https://github.com/philipn/django-rest-framework-filters [django-rest-framework-word-search-filter]: https://github.com/trollknurr/django-rest-framework-word-search-filter [django-url-filter]: https://github.com/miki725/django-url-filter diff --git a/docs/api-guide/generic-views.md b/docs/api-guide/generic-views.md index c368d0b46..606a3787a 100644 --- a/docs/api-guide/generic-views.md +++ b/docs/api-guide/generic-views.md @@ -382,7 +382,7 @@ The [django-rest-framework-bulk package][django-rest-framework-bulk] implements [Django Rest Multiple Models][django-rest-multiple-models] provides a generic view (and mixin) for sending multiple serialized models and/or querysets via a single API request. -[cite]: https://docs.djangoproject.com/en/dev/ref/class-based-views/#base-vs-generic-views +[cite]: https://docs.djangoproject.com/en/stable/ref/class-based-views/#base-vs-generic-views [GenericAPIView]: #genericapiview [ListModelMixin]: #listmodelmixin [CreateModelMixin]: #createmodelmixin diff --git a/docs/api-guide/pagination.md b/docs/api-guide/pagination.md index f82614eca..bc7a5602d 100644 --- a/docs/api-guide/pagination.md +++ b/docs/api-guide/pagination.md @@ -325,7 +325,7 @@ The [`DRF-extensions` package][drf-extensions] includes a [`PaginateByMaxMixin` The [`drf-proxy-pagination` package][drf-proxy-pagination] includes a `ProxyPagination` class which allows to choose pagination class with a query parameter. -[cite]: https://docs.djangoproject.com/en/dev/topics/pagination/ +[cite]: https://docs.djangoproject.com/en/stable/topics/pagination/ [github-link-pagination]: https://developer.github.com/guides/traversing-with-pagination/ [link-header]: ../img/link-header-pagination.png [drf-extensions]: http://chibisov.github.io/drf-extensions/docs/ diff --git a/docs/api-guide/parsers.md b/docs/api-guide/parsers.md index ef2859fe1..7bf932d06 100644 --- a/docs/api-guide/parsers.md +++ b/docs/api-guide/parsers.md @@ -224,7 +224,7 @@ Modify your REST framework settings. [jquery-ajax]: http://api.jquery.com/jQuery.ajax/ [cite]: https://groups.google.com/d/topic/django-developers/dxI4qVzrBY4/discussion -[upload-handlers]: https://docs.djangoproject.com/en/dev/topics/http/file-uploads/#upload-handlers +[upload-handlers]: https://docs.djangoproject.com/en/stable/topics/http/file-uploads/#upload-handlers [rest-framework-yaml]: http://jpadilla.github.io/django-rest-framework-yaml/ [rest-framework-xml]: http://jpadilla.github.io/django-rest-framework-xml/ [yaml]: http://www.yaml.org/ diff --git a/docs/api-guide/permissions.md b/docs/api-guide/permissions.md index f2d2fcaae..be2981327 100644 --- a/docs/api-guide/permissions.md +++ b/docs/api-guide/permissions.md @@ -269,8 +269,8 @@ The [Django Rest Framework Roles][django-rest-framework-roles] package makes it [authentication]: authentication.md [throttling]: throttling.md [filtering]: filtering.md -[contribauth]: https://docs.djangoproject.com/en/dev/topics/auth/customizing/#custom-permissions -[objectpermissions]: https://docs.djangoproject.com/en/dev/topics/auth/customizing/#handling-object-permissions +[contribauth]: https://docs.djangoproject.com/en/stable/topics/auth/customizing/#custom-permissions +[objectpermissions]: https://docs.djangoproject.com/en/stable/topics/auth/customizing/#handling-object-permissions [guardian]: https://github.com/lukaszb/django-guardian [get_objects_for_user]: http://pythonhosted.org/django-guardian/api/guardian.shortcuts.html#get-objects-for-user [2.2-announcement]: ../topics/2.2-announcement.md diff --git a/docs/api-guide/relations.md b/docs/api-guide/relations.md index aabe49412..662fd4809 100644 --- a/docs/api-guide/relations.md +++ b/docs/api-guide/relations.md @@ -505,7 +505,7 @@ For example, given the following model for a tag, which has a generic relationsh """ Tags arbitrary model instances using a generic relation. - See: https://docs.djangoproject.com/en/dev/ref/contrib/contenttypes/ + See: https://docs.djangoproject.com/en/stable/ref/contrib/contenttypes/ """ tag_name = models.SlugField() content_type = models.ForeignKey(ContentType, on_delete=models.CASCADE) @@ -593,9 +593,9 @@ The [drf-nested-routers package][drf-nested-routers] provides routers and relati The [rest-framework-generic-relations][drf-nested-relations] library provides read/write serialization for generic foreign keys. [cite]: http://lwn.net/Articles/193245/ -[reverse-relationships]: https://docs.djangoproject.com/en/dev/topics/db/queries/#following-relationships-backward +[reverse-relationships]: https://docs.djangoproject.com/en/stable/topics/db/queries/#following-relationships-backward [routers]: http://www.django-rest-framework.org/api-guide/routers#defaultrouter -[generic-relations]: https://docs.djangoproject.com/en/dev/ref/contrib/contenttypes/#id1 +[generic-relations]: https://docs.djangoproject.com/en/stable/ref/contrib/contenttypes/#id1 [2.2-announcement]: ../topics/2.2-announcement.md [drf-nested-routers]: https://github.com/alanjds/drf-nested-routers [drf-nested-relations]: https://github.com/Ian-Foote/rest-framework-generic-relations diff --git a/docs/api-guide/renderers.md b/docs/api-guide/renderers.md index a95778350..648eafdde 100644 --- a/docs/api-guide/renderers.md +++ b/docs/api-guide/renderers.md @@ -476,7 +476,7 @@ Comma-separated values are a plain-text tabular data format, that can be easily [Rest Framework Latex] provides a renderer that outputs PDFs using Laulatex. It is maintained by [Pebble (S/F Software)][mypebble]. -[cite]: https://docs.djangoproject.com/en/dev/ref/template-response/#the-rendering-process +[cite]: https://docs.djangoproject.com/en/stable/stable/template-response/#the-rendering-process [conneg]: content-negotiation.md [html-and-forms]: ../topics/html-and-forms.md [browser-accept-headers]: http://www.gethifi.com/blog/browser-rest-http-accept-headers @@ -485,7 +485,7 @@ Comma-separated values are a plain-text tabular data format, that can be easily [quote]: http://roy.gbiv.com/untangled/2008/rest-apis-must-be-hypertext-driven [application/vnd.github+json]: http://developer.github.com/v3/media/ [application/vnd.collection+json]: http://www.amundsen.com/media-types/collection/ -[django-error-views]: https://docs.djangoproject.com/en/dev/topics/http/views/#customizing-error-views +[django-error-views]: https://docs.djangoproject.com/en/stable/topics/http/views/#customizing-error-views [rest-framework-jsonp]: http://jpadilla.github.io/django-rest-framework-jsonp/ [cors]: http://www.w3.org/TR/cors/ [cors-docs]: http://www.django-rest-framework.org/topics/ajax-csrf-cors/ diff --git a/docs/api-guide/responses.md b/docs/api-guide/responses.md index 97f312710..8ee14eefa 100644 --- a/docs/api-guide/responses.md +++ b/docs/api-guide/responses.md @@ -91,5 +91,5 @@ As with any other `TemplateResponse`, this method is called to render the serial You won't typically need to call `.render()` yourself, as it's handled by Django's standard response cycle. -[cite]: https://docs.djangoproject.com/en/dev/ref/template-response/ +[cite]: https://docs.djangoproject.com/en/stable/stable/template-response/ [statuscodes]: status-codes.md diff --git a/docs/api-guide/reverse.md b/docs/api-guide/reverse.md index 35d88e2db..ee0b2054f 100644 --- a/docs/api-guide/reverse.md +++ b/docs/api-guide/reverse.md @@ -51,5 +51,5 @@ As with the `reverse` function, you should **include the request as a keyword ar api_root = reverse_lazy('api-root', request=request) [cite]: http://www.ics.uci.edu/~fielding/pubs/dissertation/rest_arch_style.htm#sec_5_1_5 -[reverse]: https://docs.djangoproject.com/en/dev/topics/http/urls/#reverse -[reverse-lazy]: https://docs.djangoproject.com/en/dev/topics/http/urls/#reverse-lazy +[reverse]: https://docs.djangoproject.com/en/stable/topics/http/urls/#reverse +[reverse-lazy]: https://docs.djangoproject.com/en/stable/topics/http/urls/#reverse-lazy diff --git a/docs/api-guide/schemas.md b/docs/api-guide/schemas.md index 7da619034..c89c100fe 100644 --- a/docs/api-guide/schemas.md +++ b/docs/api-guide/schemas.md @@ -541,5 +541,5 @@ A short description of the meaning and intended usage of the input field. [open-api]: https://openapis.org/ [json-hyperschema]: http://json-schema.org/latest/json-schema-hypermedia.html [api-blueprint]: https://apiblueprint.org/ -[static-files]: https://docs.djangoproject.com/en/dev/howto/static-files/ -[named-arguments]: https://docs.djangoproject.com/en/dev/topics/http/urls/#named-groups +[static-files]: https://docs.djangoproject.com/en/stable/howto/static-files/ +[named-arguments]: https://docs.djangoproject.com/en/stable/topics/http/urls/#named-groups diff --git a/docs/api-guide/serializers.md b/docs/api-guide/serializers.md index 290e32f4f..d36812f3f 100644 --- a/docs/api-guide/serializers.md +++ b/docs/api-guide/serializers.md @@ -1118,7 +1118,7 @@ The [html-json-forms][html-json-forms] package provides an algorithm and seriali [cite]: https://groups.google.com/d/topic/django-users/sVFaOfQi4wY/discussion [relations]: relations.md -[model-managers]: https://docs.djangoproject.com/en/dev/topics/db/managers/ +[model-managers]: https://docs.djangoproject.com/en/stable/topics/db/managers/ [encapsulation-blogpost]: http://www.dabapps.com/blog/django-models-and-encapsulation/ [django-rest-marshmallow]: http://tomchristie.github.io/django-rest-marshmallow/ [marshmallow]: https://marshmallow.readthedocs.io/en/latest/ diff --git a/docs/api-guide/testing.md b/docs/api-guide/testing.md index de79a1e2f..410f6d78a 100644 --- a/docs/api-guide/testing.md +++ b/docs/api-guide/testing.md @@ -373,6 +373,6 @@ For example, to add support for using `format='html'` in test requests, you migh } [cite]: http://jacobian.org/writing/django-apps-with-buildout/#s-create-a-test-wrapper -[client]: https://docs.djangoproject.com/en/dev/topics/testing/tools/#the-test-client -[requestfactory]: https://docs.djangoproject.com/en/dev/topics/testing/advanced/#django.test.client.RequestFactory +[client]: https://docs.djangoproject.com/en/stable/topics/testing/tools/#the-test-client +[requestfactory]: https://docs.djangoproject.com/en/stable/topics/testing/advanced/#django.test.client.RequestFactory [configuration]: #configuration diff --git a/docs/api-guide/throttling.md b/docs/api-guide/throttling.md index da4d5f725..2a6337e81 100644 --- a/docs/api-guide/throttling.md +++ b/docs/api-guide/throttling.md @@ -193,5 +193,5 @@ The following is an example of a rate throttle, that will randomly throttle 1 in [cite]: https://dev.twitter.com/docs/error-codes-responses [permissions]: permissions.md [identifing-clients]: http://oxpedia.org/wiki/index.php?title=AppSuite:Grizzly#Multiple_Proxies_in_front_of_the_cluster -[cache-setting]: https://docs.djangoproject.com/en/dev/ref/settings/#caches -[cache-docs]: https://docs.djangoproject.com/en/dev/topics/cache/#setting-up-the-cache +[cache-setting]: https://docs.djangoproject.com/en/stable/stable/settings/#caches +[cache-docs]: https://docs.djangoproject.com/en/stable/topics/cache/#setting-up-the-cache diff --git a/docs/api-guide/validators.md b/docs/api-guide/validators.md index e041e9072..0e58c6fff 100644 --- a/docs/api-guide/validators.md +++ b/docs/api-guide/validators.md @@ -300,4 +300,4 @@ In some advanced cases you might want a validator to be passed the serializer fi # In `__call__` we can then use that information to modify the validation behavior. self.is_update = serializer_field.parent.instance is not None -[cite]: https://docs.djangoproject.com/en/dev/ref/validators/ +[cite]: https://docs.djangoproject.com/en/stable/ref/validators/ diff --git a/docs/topics/2.2-announcement.md b/docs/topics/2.2-announcement.md index e6220f427..ca4ed2efa 100644 --- a/docs/topics/2.2-announcement.md +++ b/docs/topics/2.2-announcement.md @@ -147,10 +147,10 @@ When using a serializer with a `HyperlinkedRelatedField` or `HyperlinkedIdentity From version 2.2 onwards, serializers with hyperlinked relationships *always* require a `'request'` key to be supplied in the context dictionary. The implicit behavior will continue to function, but its use will raise a `PendingDeprecationWarning`. [xordoquy]: https://github.com/xordoquy -[django-python-3]: https://docs.djangoproject.com/en/dev/faq/install/#can-i-use-django-with-python-3 -[porting-python-3]: https://docs.djangoproject.com/en/dev/topics/python3/ -[python-compat]: https://docs.djangoproject.com/en/dev/releases/1.5/#python-compatibility -[django-deprecation-policy]: https://docs.djangoproject.com/en/dev/internals/release-process/#internal-release-deprecation-policy +[django-python-3]: https://docs.djangoproject.com/en/stable/faq/install/#can-i-use-django-with-python-3 +[porting-python-3]: https://docs.djangoproject.com/en/stable/topics/python3/ +[python-compat]: https://docs.djangoproject.com/en/stable/releases/1.5/#python-compatibility +[django-deprecation-policy]: https://docs.djangoproject.com/en/stable/internals/release-process/#internal-release-deprecation-policy [credits]: http://www.django-rest-framework.org/topics/credits [mailing-list]: https://groups.google.com/forum/?fromgroups#!forum/django-rest-framework [django-rest-framework-docs]: https://github.com/marcgibbons/django-rest-framework-docs diff --git a/docs/topics/2.4-announcement.md b/docs/topics/2.4-announcement.md index 3009daa49..96f68c865 100644 --- a/docs/topics/2.4-announcement.md +++ b/docs/topics/2.4-announcement.md @@ -162,7 +162,7 @@ The next planned release will be 3.0, featuring an improved and simplified seria Once again, many thanks to all the generous [backers and sponsors][kickstarter-sponsors] who've helped make this possible! -[lts-releases]: https://docs.djangoproject.com/en/dev/internals/release-process/#long-term-support-lts-releases +[lts-releases]: https://docs.djangoproject.com/en/stable/internals/release-process/#long-term-support-lts-releases [2-4-release-notes]: release-notes#240 [view-name-and-description-settings]: ../api-guide/settings#view-names-and-descriptions [client-ip-identification]: ../api-guide/throttling#how-clients-are-identified diff --git a/docs/topics/3.0-announcement.md b/docs/topics/3.0-announcement.md index e6cbf7238..25ab4fd5b 100644 --- a/docs/topics/3.0-announcement.md +++ b/docs/topics/3.0-announcement.md @@ -870,7 +870,7 @@ The `COMPACT_JSON` setting has been added, and can be used to revert this behavi #### File fields as URLs -The `FileField` and `ImageField` classes are now represented as URLs by default. You should ensure you set Django's [standard `MEDIA_URL` setting](https://docs.djangoproject.com/en/dev/ref/settings/#std:setting-MEDIA_URL) appropriately, and ensure your application [serves the uploaded files](https://docs.djangoproject.com/en/dev/howto/static-files/#serving-uploaded-files-in-development). +The `FileField` and `ImageField` classes are now represented as URLs by default. You should ensure you set Django's [standard `MEDIA_URL` setting](https://docs.djangoproject.com/en/stable/ref/settings/#std:setting-MEDIA_URL) appropriately, and ensure your application [serves the uploaded files](https://docs.djangoproject.com/en/stable/howto/static-files/#serving-uploaded-files-in-development). You can revert this behavior, and display filenames in the representation by using the `UPLOADED_FILES_USE_URL` settings key: @@ -962,4 +962,4 @@ You can follow development on the GitHub site, where we use [milestones to indic [kickstarter]: http://kickstarter.com/projects/tomchristie/django-rest-framework-3 [sponsors]: http://www.django-rest-framework.org/topics/kickstarter-announcement/#sponsors [mixins.py]: https://github.com/tomchristie/django-rest-framework/blob/master/rest_framework/mixins.py -[django-localization]: https://docs.djangoproject.com/en/dev/topics/i18n/translation/#localization-how-to-create-language-files +[django-localization]: https://docs.djangoproject.com/en/stable/topics/i18n/translation/#localization-how-to-create-language-files diff --git a/docs/topics/ajax-csrf-cors.md b/docs/topics/ajax-csrf-cors.md index ad88810da..4960e0881 100644 --- a/docs/topics/ajax-csrf-cors.md +++ b/docs/topics/ajax-csrf-cors.md @@ -35,7 +35,7 @@ The best way to deal with CORS in REST framework is to add the required response [cite]: http://www.codinghorror.com/blog/2008/10/preventing-csrf-and-xsrf-attacks.html [csrf]: https://www.owasp.org/index.php/Cross-Site_Request_Forgery_(CSRF) -[csrf-ajax]: https://docs.djangoproject.com/en/dev/ref/csrf/#ajax +[csrf-ajax]: https://docs.djangoproject.com/en/stable/ref/csrf/#ajax [cors]: http://www.w3.org/TR/cors/ [ottoyiu]: https://github.com/ottoyiu/ [django-cors-headers]: https://github.com/ottoyiu/django-cors-headers/ diff --git a/docs/topics/release-notes.md b/docs/topics/release-notes.md index e5683360b..5628bcaec 100644 --- a/docs/topics/release-notes.md +++ b/docs/topics/release-notes.md @@ -598,7 +598,7 @@ For older release notes, [please see the version 2.x documentation][old-release- [cite]: http://www.catb.org/~esr/writings/cathedral-bazaar/cathedral-bazaar/ar01s04.html [deprecation-policy]: #deprecation-policy -[django-deprecation-policy]: https://docs.djangoproject.com/en/dev/internals/release-process/#internal-release-deprecation-policy +[django-deprecation-policy]: https://docs.djangoproject.com/en/stable/internals/release-process/#internal-release-deprecation-policy [defusedxml-announce]: http://blog.python.org/2013/02/announcing-defusedxml-fixes-for-xml.html [743]: https://github.com/tomchristie/django-rest-framework/pull/743 [staticfiles14]: https://docs.djangoproject.com/en/1.4/howto/static-files/#with-a-template-tag From 16f5d42cbc3e966ab2d4d8dc00163942f2769e43 Mon Sep 17 00:00:00 2001 From: Carlton Gibson Date: Thu, 1 Dec 2016 10:11:25 +0100 Subject: [PATCH 069/190] Add additional link to HTML & Forms topic page (#4726) Just makes the topic page easier to find. Closes #1673 --- docs/api-guide/renderers.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/api-guide/renderers.md b/docs/api-guide/renderers.md index 648eafdde..236504850 100644 --- a/docs/api-guide/renderers.md +++ b/docs/api-guide/renderers.md @@ -123,6 +123,8 @@ You can use `TemplateHTMLRenderer` either to return regular HTML pages using RES If you're building websites that use `TemplateHTMLRenderer` along with other renderer classes, you should consider listing `TemplateHTMLRenderer` as the first class in the `renderer_classes` list, so that it will be prioritised first even for browsers that send poorly formed `ACCEPT:` headers. +See the [_HTML & Forms_ Topic Page][html-and-forms] for further examples of `TemplateHTMLRenderer` usage. + **.media_type**: `text/html` **.format**: `'.html'` From 932d04a4beb79afa7a52bfec7f66bb6ccfe53814 Mon Sep 17 00:00:00 2001 From: Asif Saifuddin Auvi Date: Thu, 1 Dec 2016 22:17:36 +0600 Subject: [PATCH 070/190] Browsable API tests asserts to pytest (#4725) --- tests/browsable_api/test_browsable_api.py | 18 ++++++++++++------ .../browsable_api/test_browsable_nested_api.py | 8 ++++---- tests/browsable_api/test_form_rendering.py | 4 ++-- 3 files changed, 18 insertions(+), 12 deletions(-) diff --git a/tests/browsable_api/test_browsable_api.py b/tests/browsable_api/test_browsable_api.py index 3d49c353b..684d7ae14 100644 --- a/tests/browsable_api/test_browsable_api.py +++ b/tests/browsable_api/test_browsable_api.py @@ -26,16 +26,19 @@ class DropdownWithAuthTests(TestCase): def test_name_shown_when_logged_in(self): self.client.login(username=self.username, password=self.password) response = self.client.get('/') - self.assertContains(response, 'john') + content = response.content.decode('utf8') + assert 'john' in content def test_logout_shown_when_logged_in(self): self.client.login(username=self.username, password=self.password) response = self.client.get('/') - self.assertContains(response, '>Log out<') + content = response.content.decode('utf8') + assert '>Log out<' in content def test_login_shown_when_logged_out(self): response = self.client.get('/') - self.assertContains(response, '>Log in<') + content = response.content.decode('utf8') + assert '>Log in<' in content @override_settings(ROOT_URLCONF='tests.browsable_api.no_auth_urls') @@ -58,13 +61,16 @@ class NoDropdownWithoutAuthTests(TestCase): def test_name_shown_when_logged_in(self): self.client.login(username=self.username, password=self.password) response = self.client.get('/') - self.assertContains(response, 'john') + content = response.content.decode('utf8') + assert 'john' in content def test_dropdown_not_shown_when_logged_in(self): self.client.login(username=self.username, password=self.password) response = self.client.get('/') - self.assertNotContains(response, '