Merge branch 'master' into master

This commit is contained in:
Ryan 2019-04-09 22:41:29 +07:00 committed by GitHub
commit 890613c0bb
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
102 changed files with 1014 additions and 316 deletions

View File

@ -1,9 +1,6 @@
language: python
cache: pip
dist: xenial
sudo: false
matrix:
fast_finish: true
include:
@ -15,15 +12,17 @@ matrix:
- { python: "3.5", env: DJANGO=1.11 }
- { python: "3.5", env: DJANGO=2.0 }
- { python: "3.5", env: DJANGO=2.1 }
- { python: "3.5", env: DJANGO=master }
- { python: "3.5", env: DJANGO=2.2 }
- { python: "3.6", env: DJANGO=1.11 }
- { python: "3.6", env: DJANGO=2.0 }
- { python: "3.6", env: DJANGO=2.1 }
- { python: "3.6", env: DJANGO=2.2 }
- { python: "3.6", env: DJANGO=master }
- { python: "3.7", env: DJANGO=2.0 }
- { python: "3.7", env: DJANGO=2.1 }
- { python: "3.7", env: DJANGO=2.2 }
- { python: "3.7", env: DJANGO=master }
- { python: "3.7", env: TOXENV=base }

View File

@ -3,7 +3,7 @@
- [ ] I have verified that that issue exists against the `master` branch of Django REST framework.
- [ ] I have searched for similar issues in both open and closed tickets and cannot find a duplicate.
- [ ] This is not a usage question. (Those should be directed to the [discussion group](https://groups.google.com/forum/#!forum/django-rest-framework) instead.)
- [ ] This cannot be dealt with as a third party library. (We prefer new functionality to be [in the form of third party libraries](https://www.django-rest-framework.org/topics/third-party-resources/#about-third-party-packages) where possible.)
- [ ] This cannot be dealt with as a third party library. (We prefer new functionality to be [in the form of third party libraries](https://www.django-rest-framework.org/community/third-party-packages/#about-third-party-packages) where possible.)
- [ ] I have reduced the issue to the simplest possible case.
- [ ] I have included a failing test as a pull request. (If you are unable to do so we can still accept the issue.)

View File

@ -19,17 +19,15 @@ continued development by [signing up for a paid plan][funding].
The initial aim is to provide a single full-time position on REST framework.
*Every single sign-up makes a significant impact towards making that possible.*
[![][rover-img]][rover-url]
[![][sentry-img]][sentry-url]
[![][stream-img]][stream-url]
[![][rollbar-img]][rollbar-url]
[![][cadre-img]][cadre-url]
[![][load-impact-img]][load-impact-url]
[![][kloudless-img]][kloudless-url]
[![][auklet-img]][auklet-url]
[![][release-history-img]][release-history-url]
[![][lightson-img]][lightson-url]
Many thanks to all our [wonderful sponsors][sponsors], and in particular to our premium backers, [Rover][rover-url], [Sentry][sentry-url], [Stream][stream-url], [Rollbar][rollbar-url], [Cadre][cadre-url], [Load Impact][load-impact-url], [Kloudless][kloudless-url], [Auklet][auklet-url], and [Lights On Software][lightson-url].
Many thanks to all our [wonderful sponsors][sponsors], and in particular to our premium backers, [Sentry][sentry-url], [Stream][stream-url], [Rollbar][rollbar-url], [Cadre][cadre-url], [Kloudless][kloudless-url], [Release History][release-history-url], and [Lights On Software][lightson-url].
---
@ -56,7 +54,7 @@ There is a live example API for testing purposes, [available here][sandbox].
# Requirements
* Python (2.7, 3.4, 3.5, 3.6, 3.7)
* Django (1.11, 2.0, 2.1)
* Django (1.11, 2.0, 2.1, 2.2)
We **highly recommend** and only officially support the latest patch release of
each Python and Django series.
@ -201,7 +199,7 @@ Send a description of the issue via email to [rest-framework-security@googlegrou
[cadre-img]: https://raw.githubusercontent.com/encode/django-rest-framework/master/docs/img/premium/cadre-readme.png
[load-impact-img]: https://raw.githubusercontent.com/encode/django-rest-framework/master/docs/img/premium/load-impact-readme.png
[kloudless-img]: https://raw.githubusercontent.com/encode/django-rest-framework/master/docs/img/premium/kloudless-readme.png
[auklet-img]: https://raw.githubusercontent.com/encode/django-rest-framework/master/docs/img/premium/auklet-readme.png
[release-history-img]: https://raw.githubusercontent.com/encode/django-rest-framework/master/docs/img/premium/release-history.png
[lightson-img]: https://raw.githubusercontent.com/encode/django-rest-framework/master/docs/img/premium/lightson-readme.png
[rover-url]: http://jobs.rover.com/
@ -211,7 +209,7 @@ Send a description of the issue via email to [rest-framework-security@googlegrou
[cadre-url]: https://cadre.com/
[load-impact-url]: https://loadimpact.com/?utm_campaign=Sponsorship%20links&utm_source=drf&utm_medium=drf
[kloudless-url]: https://hubs.ly/H0f30Lf0
[auklet-url]: https://auklet.io/
[release-history-url]: https://releasehistory.io
[lightson-url]: https://lightsonsoftware.com
[oauth1-section]: https://www.django-rest-framework.org/api-guide/authentication/#django-rest-framework-oauth

View File

@ -13,7 +13,7 @@ provided in Django.
Django provides a [`method_decorator`][decorator] to use
decorators with class based views. This can be used with
with other cache decorators such as [`cache_page`][page] and
other cache decorators such as [`cache_page`][page] and
[`vary_on_cookie`][cookie].
```python

View File

@ -124,7 +124,14 @@ A boolean representation.
When using HTML encoded form input be aware that omitting a value will always be treated as setting a field to `False`, even if it has a `default=True` option specified. This is because HTML checkbox inputs represent the unchecked state by omitting the value, so REST framework treats omission as if it is an empty checkbox input.
Note that default `BooleanField` instances will be generated with a `required=False` option (since Django `models.BooleanField` is always `blank=True`). If you want to change this behaviour explicitly declare the `BooleanField` on the serializer class.
Note that Django 2.1 removed the `blank` kwarg from `models.BooleanField`.
Prior to Django 2.1 `models.BooleanField` fields were always `blank=True`. Thus
since Django 2.1 default `serializers.BooleanField` instances will be generated
without the `required` kwarg (i.e. equivalent to `required=True`) whereas with
previous versions of Django, default `BooleanField` instances will be generated
with a `required=False` option. If you want to control this behaviour manually,
explicitly declare the `BooleanField` on the serializer class, or use the
`extra_kwargs` option to set the `required` flag.
Corresponds to `django.db.models.fields.BooleanField`.
@ -299,10 +306,11 @@ A date and time representation.
Corresponds to `django.db.models.fields.DateTimeField`.
**Signature:** `DateTimeField(format=api_settings.DATETIME_FORMAT, input_formats=None)`
**Signature:** `DateTimeField(format=api_settings.DATETIME_FORMAT, input_formats=None, default_timezone=None)`
* `format` - A string representing the output format. If not specified, this defaults to the same value as the `DATETIME_FORMAT` settings key, which will be `'iso-8601'` unless set. Setting to a format string indicates that `to_representation` return values should be coerced to string output. Format strings are described below. Setting this value to `None` indicates that Python `datetime` objects should be returned by `to_representation`. In this case the datetime encoding will be determined by the renderer.
* `input_formats` - A list of strings representing the input formats which may be used to parse the date. If not specified, the `DATETIME_INPUT_FORMATS` setting will be used, which defaults to `['iso-8601']`.
* `default_timezone` - A `pytz.timezone` representing the timezone. If not specified and the `USE_TZ` setting is enabled, this defaults to the [current timezone][django-current-timezone]. If `USE_TZ` is disabled, then datetime objects will be naive.
#### `DateTimeField` format strings.
@ -828,3 +836,4 @@ The [django-rest-framework-hstore][django-rest-framework-hstore] package provide
[django-rest-framework-hstore]: https://github.com/djangonauts/django-rest-framework-hstore
[django-hstore]: https://github.com/djangonauts/django-hstore
[python-decimal-rounding-modes]: https://docs.python.org/3/library/decimal.html#rounding-modes
[django-current-timezone]: https://docs.djangoproject.com/en/stable/topics/i18n/timezones/#default-time-zone-and-current-time-zone

View File

@ -127,7 +127,7 @@ Note that you can use both an overridden `.get_queryset()` and generic filtering
"""
model = Product
serializer_class = ProductSerializer
filter_class = ProductFilter
filterset_class = ProductFilter
def get_queryset(self):
user = self.request.user
@ -160,13 +160,13 @@ Or add the filter backend to an individual View or ViewSet.
...
filter_backends = (DjangoFilterBackend,)
If all you need is simple equality-based filtering, you can set a `filter_fields` attribute on the view, or viewset, listing the set of fields you wish to filter against.
If all you need is simple equality-based filtering, you can set a `filterset_fields` attribute on the view, or viewset, listing the set of fields you wish to filter against.
class ProductList(generics.ListAPIView):
queryset = Product.objects.all()
serializer_class = ProductSerializer
filter_backends = (DjangoFilterBackend,)
filter_fields = ('category', 'in_stock')
filterset_fields = ('category', 'in_stock')
This will automatically create a `FilterSet` class for the given fields, and will allow you to make requests such as:
@ -218,6 +218,16 @@ For example:
By default, the search parameter is named `'search`', but this may be overridden with the `SEARCH_PARAM` setting.
To dynamically change search fields based on request content, it's possible to subclass the `SearchFilter` and override the `get_search_fields()` function. For example, the following subclass will only search on `title` if the query parameter `title_only` is in the request:
from rest_framework import filters
class CustomSearchFilter(filters.SearchFilter):
def get_search_fields(self, view, request):
if request.query_params.get('title_only'):
return ('title',)
return super(CustomSearchFilter, self).get_search_fields(view, request)
For more details, see the [Django documentation][search-django-admin].
---
@ -298,9 +308,9 @@ A complete example using both `DjangoObjectPermissionsFilter` and `DjangoObjectP
**permissions.py**:
class CustomObjectPermissions(permissions.DjangoObjectPermissions):
"""
Similar to `DjangoObjectPermissions`, but adding 'view' permissions.
"""
"""
Similar to `DjangoObjectPermissions`, but adding 'view' permissions.
"""
perms_map = {
'GET': ['%(app_label)s.view_%(model_name)s'],
'OPTIONS': ['%(app_label)s.view_%(model_name)s'],
@ -314,11 +324,11 @@ A complete example using both `DjangoObjectPermissionsFilter` and `DjangoObjectP
**views.py**:
class EventViewSet(viewsets.ModelViewSet):
"""
Viewset that only lists events if user has 'view' permissions, and only
allows operations on individual events if user has appropriate 'view', 'add',
'change' or 'delete' permissions.
"""
"""
Viewset that only lists events if user has 'view' permissions, and only
allows operations on individual events if user has appropriate 'view', 'add',
'change' or 'delete' permissions.
"""
queryset = Event.objects.all()
serializer_class = EventSerializer
filter_backends = (filters.DjangoObjectPermissionsFilter,)

View File

@ -311,7 +311,7 @@ The [`drf-proxy-pagination` package][drf-proxy-pagination] includes a `ProxyPagi
## link-header-pagination
The [`django-rest-framework-link-header-pagination` package][drf-link-header-pagination] includes a `LinkHeaderPagination` class which provides pagination via an HTTP `Link` header as desribed in [Github's developer documentation](github-link-pagination).
The [`django-rest-framework-link-header-pagination` package][drf-link-header-pagination] includes a `LinkHeaderPagination` class which provides pagination via an HTTP `Link` header as described in [Github's developer documentation](github-link-pagination).
[cite]: https://docs.djangoproject.com/en/stable/topics/pagination/
[link-header]: ../img/link-header-pagination.png

View File

@ -10,9 +10,9 @@ Together with [authentication] and [throttling], permissions determine whether a
Permission checks are always run at the very start of the view, before any other code is allowed to proceed. Permission checks will typically use the authentication information in the `request.user` and `request.auth` properties to determine if the incoming request should be permitted.
Permissions are used to grant or deny access different classes of users to different parts of the API.
Permissions are used to grant or deny access for different classes of users to different parts of the API.
The simplest style of permission would be to allow access to any authenticated user, and deny access to any unauthenticated user. This corresponds the `IsAuthenticated` class in REST framework.
The simplest style of permission would be to allow access to any authenticated user, and deny access to any unauthenticated user. This corresponds to the `IsAuthenticated` class in REST framework.
A slightly less strict style of permission would be to allow full access to authenticated users, but allow read-only access to unauthenticated users. This corresponds to the `IsAuthenticatedOrReadOnly` class in REST framework.
@ -48,6 +48,19 @@ For example:
self.check_object_permissions(self.request, obj)
return obj
---
**Note**: With the exception of `DjangoObjectPermissions`, the provided
permission classes in `rest_framework.permissions` **do not** implement the
methods necessary to check object permissions.
If you wish to use the provided permission classes in order to check object
permissions, **you must** subclass them and implement the
`has_object_permission()` method described in the [_Custom
permissions_](#custom-permissions) section (below).
---
#### Limitations of object level permissions
For performance reasons the generic views will not automatically apply object level permissions to each instance in a queryset when returning a list of objects.
@ -121,7 +134,7 @@ Provided they inherit from `rest_framework.permissions.BasePermission`, permissi
}
return Response(content)
__Note:__ it only supports & -and- and | -or-.
__Note:__ it supports & (and), | (or) and ~ (not).
---
@ -284,9 +297,9 @@ The [DRY Rest Permissions][dry-rest-permissions] package provides the ability to
The [Django Rest Framework Roles][django-rest-framework-roles] package makes it easier to parameterize your API over multiple types of users.
## Django Rest Framework API Key
## Django REST Framework API Key
The [Django Rest Framework API Key][django-rest-framework-api-key] package allows you to ensure that every request made to the server requires an API key header. You can generate one from the django admin interface.
The [Django REST Framework API Key][djangorestframework-api-key] package provides the ability to authorize clients based on customizable API key headers. This package is targeted at situations in which regular user-based authentication (e.g. `TokenAuthentication`) is not suitable, e.g. allowing non-human clients to safely use your API. API keys are generated and validated through cryptographic methods and can be created and revoked from the Django admin interface at anytime.
## Django Rest Framework Role Filters
@ -304,6 +317,6 @@ The [Django Rest Framework Role Filters][django-rest-framework-role-filters] pac
[rest-condition]: https://github.com/caxap/rest_condition
[dry-rest-permissions]: https://github.com/Helioscene/dry-rest-permissions
[django-rest-framework-roles]: https://github.com/computer-lab/django-rest-framework-roles
[django-rest-framework-api-key]: https://github.com/manosim/django-rest-framework-api-key
[djangorestframework-api-key]: https://github.com/florimondmanca/djangorestframework-api-key
[django-rest-framework-role-filters]: https://github.com/allisson/django-rest-framework-role-filters
[django-rest-framework-guardian]: https://github.com/rpkilby/django-rest-framework-guardian

View File

@ -46,12 +46,12 @@ In order to explain the various types of relational fields, we'll use a couple o
unique_together = ('album', 'order')
ordering = ['order']
def __unicode__(self):
def __str__(self):
return '%d: %s' % (self.order, self.title)
## StringRelatedField
`StringRelatedField` may be used to represent the target of the relationship using its `__unicode__` method.
`StringRelatedField` may be used to represent the target of the relationship using its `__str__` method.
For example, the following serializer.
@ -510,7 +510,7 @@ For example, given the following model for a tag, which has a generic relationsh
object_id = models.PositiveIntegerField()
tagged_object = GenericForeignKey('content_type', 'object_id')
def __unicode__(self):
def __str__(self):
return self.tag_name
And the following two models, which may have associated tags:

View File

@ -89,7 +89,7 @@ The default JSON encoding style can be altered using the `UNICODE_JSON` and `COM
**.media_type**: `application/json`
**.format**: `'.json'`
**.format**: `'json'`
**.charset**: `None`
@ -127,7 +127,7 @@ See the [_HTML & Forms_ Topic Page][html-and-forms] for further examples of `Tem
**.media_type**: `text/html`
**.format**: `'.html'`
**.format**: `'html'`
**.charset**: `utf-8`
@ -149,7 +149,7 @@ You can use `StaticHTMLRenderer` either to return regular HTML pages using REST
**.media_type**: `text/html`
**.format**: `'.html'`
**.format**: `'html'`
**.charset**: `utf-8`
@ -165,7 +165,7 @@ This renderer will determine which other renderer would have been given highest
**.media_type**: `text/html`
**.format**: `'.api'`
**.format**: `'api'`
**.charset**: `utf-8`
@ -200,7 +200,7 @@ Note that views that have nested or list serializers for their input won't work
**.media_type**: `text/html`
**.format**: `'.admin'`
**.format**: `'admin'`
**.charset**: `utf-8`
@ -224,7 +224,7 @@ For more information see the [HTML & Forms][html-and-forms] documentation.
**.media_type**: `text/html`
**.format**: `'.form'`
**.format**: `'form'`
**.charset**: `utf-8`
@ -236,7 +236,7 @@ This renderer is used for rendering HTML multipart form data. **It is not suita
**.media_type**: `multipart/form-data; boundary=BoUnDaRyStRiNg`
**.format**: `'.multipart'`
**.format**: `'multipart'`
**.charset**: `utf-8`

View File

@ -50,7 +50,7 @@ The request exposes some properties that allow you to determine the result of th
## .accepted_renderer
The renderer instance what was selected by the content negotiation stage.
The renderer instance that was selected by the content negotiation stage.
## .accepted_media_type

View File

@ -20,7 +20,7 @@ can render the schema into the commonly used YAML-based OpenAPI format.
## Quickstart
There are two different ways you can serve a schema description for you API.
There are two different ways you can serve a schema description for your API.
### Generating a schema with the `generateschema` management command
@ -112,8 +112,8 @@ has to be rendered into the actual bytes that are used in the response.
REST framework includes a few different renderers that you can use for
encoding the API schema.
* `renderers.OpenAPIRenderer` - Renders into YAML-based [OpenAPI][openapi], the most widely used API schema format.
* `renderers.JSONOpenAPIRenderer` - Renders into JSON-based [OpenAPI][openapi].
* `renderers.OpenAPIRenderer` - Renders into YAML-based [OpenAPI][open-api], the most widely used API schema format.
* `renderers.JSONOpenAPIRenderer` - Renders into JSON-based [OpenAPI][open-api].
* `renderers.CoreJSONRenderer` - Renders into [Core JSON][corejson], a format designed for
use with the `coreapi` client library.
@ -827,19 +827,11 @@ A short description of the meaning and intended usage of the input field.
[drf-yasg][drf-yasg] generates [OpenAPI][open-api] documents suitable for code generation - nested schemas,
named models, response bodies, enum/pattern/min/max validators, form parameters, etc.
## DRF OpenAPI
[DRF OpenAPI][drf-openapi] renders the schema generated by Django Rest Framework
in [OpenAPI][open-api] format.
[cite]: https://blog.heroku.com/archives/2014/1/8/json_schema_for_heroku_platform_api
[coreapi]: https://www.coreapi.org/
[corejson]: https://www.coreapi.org/specification/encoding/#core-json-encoding
[drf-yasg]: https://github.com/axnsan12/drf-yasg/
[open-api]: https://openapis.org/
[drf-openapi]: https://github.com/limdauto/drf_openapi
[json-hyperschema]: https://json-schema.org/latest/json-schema-hypermedia.html
[api-blueprint]: https://apiblueprint.org/
[static-files]: https://docs.djangoproject.com/en/stable/howto/static-files/

View File

@ -152,7 +152,7 @@ When deserializing data, you always need to call `is_valid()` before attempting
serializer.is_valid()
# False
serializer.errors
# {'email': [u'Enter a valid e-mail address.'], 'created': [u'This field is required.']}
# {'email': ['Enter a valid e-mail address.'], 'created': ['This field is required.']}
Each key in the dictionary will be the field name, and the values will be lists of strings of any error messages corresponding to that field. The `non_field_errors` key may also be present, and will list any general validation errors. The name of the `non_field_errors` key may be customized using the `NON_FIELD_ERRORS_KEY` REST framework setting.
@ -253,7 +253,7 @@ When passing data to a serializer instance, the unmodified data will be made ava
By default, serializers must be passed values for all required fields or they will raise validation errors. You can use the `partial` argument in order to allow partial updates.
# Update `comment` with partial data
serializer = CommentSerializer(comment, data={'content': u'foo bar'}, partial=True)
serializer = CommentSerializer(comment, data={'content': 'foo bar'}, partial=True)
## Dealing with nested objects
@ -293,7 +293,7 @@ When dealing with nested representations that support deserializing the data, an
serializer.is_valid()
# False
serializer.errors
# {'user': {'email': [u'Enter a valid e-mail address.']}, 'created': [u'This field is required.']}
# {'user': {'email': ['Enter a valid e-mail address.']}, 'created': ['This field is required.']}
Similarly, the `.validated_data` property will include nested data structures.
@ -415,7 +415,7 @@ You can provide arbitrary additional context by passing a `context` argument whe
serializer = AccountSerializer(account, context={'request': request})
serializer.data
# {'id': 6, 'owner': u'denvercoder9', 'created': datetime.datetime(2013, 2, 12, 09, 44, 56, 678870), 'details': 'http://example.com/accounts/6/details'}
# {'id': 6, 'owner': 'denvercoder9', 'created': datetime.datetime(2013, 2, 12, 09, 44, 56, 678870), 'details': 'http://example.com/accounts/6/details'}
The context dictionary can be used within any serializer field logic, such as a custom `.to_representation()` method, by accessing the `self.context` attribute.
@ -1094,10 +1094,10 @@ This would then allow you to do the following:
>>> model = User
>>> fields = ('id', 'username', 'email')
>>>
>>> print UserSerializer(user)
>>> print(UserSerializer(user))
{'id': 2, 'username': 'jonwatts', 'email': 'jon@example.com'}
>>>
>>> print UserSerializer(user, fields=('id', 'email'))
>>> print(UserSerializer(user, fields=('id', 'email')))
{'id': 2, 'email': 'jon@example.com'}
## Customizing the default fields

View File

@ -209,7 +209,7 @@ directly.
Note that the requests client requires you to pass fully qualified URLs.
## `RequestsClient` and working with the database
## RequestsClient and working with the database
The `RequestsClient` class is useful if you want to write tests that solely interact with the service interface. This is a little stricter than using the standard Django test client, as it means that all interactions should be via the API.

View File

@ -82,8 +82,10 @@ The throttle classes provided by REST framework use Django's cache backend. You
If you need to use a cache other than `'default'`, you can do so by creating a custom throttle class and setting the `cache` attribute. For example:
from django.core.cache import caches
class CustomAnonRateThrottle(AnonRateThrottle):
cache = get_cache('alternate')
cache = caches['alternate']
You'll need to remember to also set your custom throttle class in the `'DEFAULT_THROTTLE_CLASSES'` settings key, or using the `throttle_classes` view attribute.

View File

@ -389,7 +389,7 @@ You can include `expiry_date` as a field option on a `ModelSerializer` class.
These fields will be mapped to `serializers.ReadOnlyField()` instances.
>>> serializer = InvitationSerializer()
>>> print repr(serializer)
>>> print(repr(serializer))
InvitationSerializer():
to_email = EmailField(max_length=75)
message = CharField(max_length=1000)
@ -960,6 +960,6 @@ The 3.2 release is planned to introduce an alternative admin-style interface to
You can follow development on the GitHub site, where we use [milestones to indicate planning timescales](https://github.com/encode/django-rest-framework/milestones).
[kickstarter]: https://www.kickstarter.com/projects/tomchristie/django-rest-framework-3
[sponsors]: https://www.django-rest-framework.org/topics/kickstarter-announcement/#sponsors
[sponsors]: https://www.django-rest-framework.org/community/kickstarter-announcement/#sponsors
[mixins.py]: https://github.com/encode/django-rest-framework/blob/master/rest_framework/mixins.py
[django-localization]: https://docs.djangoproject.com/en/stable/topics/i18n/translation/#localization-how-to-create-language-files

View File

@ -30,7 +30,7 @@ Note that as a result of this work a number of settings keys and generic view at
Until now, there has only been a single built-in pagination style in REST framework. We now have page, limit/offset and cursor based schemes included by default.
The cursor based pagination scheme is particularly smart, and is a better approach for clients iterating through large or frequently changing result sets. The scheme supports paging against non-unique indexes, by using both cursor and limit/offset information. It also allows for both forward and reverse cursor pagination. Much credit goes to David Cramer for [this blog post](http://cramer.io/2011/03/08/building-cursors-for-the-disqus-api) on the subject.
The cursor based pagination scheme is particularly smart, and is a better approach for clients iterating through large or frequently changing result sets. The scheme supports paging against non-unique indexes, by using both cursor and limit/offset information. It also allows for both forward and reverse cursor pagination. Much credit goes to David Cramer for [this blog post](https://cra.mr/2011/03/08/building-cursors-for-the-disqus-api) on the subject.
#### Pagination controls in the browsable API.
@ -114,7 +114,7 @@ Note that the structure of the error responses is still the same. We still have
We include built-in translations both for standard exception cases, and for serializer validation errors.
The full list of supported languages can be found on our [Transifex project page](https://www.transifex.com/projects/p/django-rest-framework/).
The full list of supported languages can be found on our [Transifex project page](https://www.transifex.com/django-rest-framework-1/django-rest-framework/).
If you only wish to support a subset of the supported languages, use Django's standard `LANGUAGES` setting:
@ -155,14 +155,14 @@ We've now moved a number of packages out of the core of REST framework, and into
We're making this change in order to help distribute the maintenance workload, and keep better focus of the core essentials of the framework.
The change also means we can be more flexible with which external packages we recommend. For example, the excellently maintained [Django OAuth toolkit](https://github.com/evonove/django-oauth-toolkit) has now been promoted as our recommended option for integrating OAuth support.
The change also means we can be more flexible with which external packages we recommend. For example, the excellently maintained [Django OAuth toolkit](https://github.com/jazzband/django-oauth-toolkit) has now been promoted as our recommended option for integrating OAuth support.
The following packages are now moved out of core and should be separately installed:
* OAuth - [djangorestframework-oauth](https://jpadilla.github.io/django-rest-framework-oauth/)
* XML - [djangorestframework-xml](https://jpadilla.github.io/django-rest-framework-xml)
* YAML - [djangorestframework-yaml](https://jpadilla.github.io/django-rest-framework-yaml)
* JSONP - [djangorestframework-jsonp](https://jpadilla.github.io/django-rest-framework-jsonp)
* XML - [djangorestframework-xml](https://jpadilla.github.io/django-rest-framework-xml/)
* YAML - [djangorestframework-yaml](https://jpadilla.github.io/django-rest-framework-yaml/)
* JSONP - [djangorestframework-jsonp](https://jpadilla.github.io/django-rest-framework-jsonp/)
It's worth reiterating that this change in policy shouldn't mean any work in your codebase other than adding a new requirement and modifying some import paths. For example to install XML rendering, you would now do:

View File

@ -10,7 +10,7 @@ We've also fixed a huge number of issues, and made numerous cleanups and improve
Over the course of the 3.1.x series we've [resolved nearly 600 tickets](https://github.com/encode/django-rest-framework/issues?utf8=%E2%9C%93&q=closed%3A%3E2015-03-05) on our GitHub issue tracker. This means we're currently running at a rate of **closing around 100 issues or pull requests per month**.
None of this would have been possible without the support of our wonderful Kickstarter backers. If you're looking for a job in Django development we'd strongly recommend taking [a look through our sponsors](https://www.django-rest-framework.org/topics/kickstarter-announcement/#sponsors) and finding out who's hiring.
None of this would have been possible without the support of our wonderful Kickstarter backers. If you're looking for a job in Django development we'd strongly recommend taking [a look through our sponsors](https://www.django-rest-framework.org/community/kickstarter-announcement/#sponsors) and finding out who's hiring.
## AdminRenderer

View File

@ -36,13 +36,13 @@ Right now we're over 60% of the way towards achieving that.
*Every single sign-up makes a significant impact.*
<ul class="premium-promo promo">
<li><a href="http://jobs.rover.com/" style="background-image: url(https://fund-rest-framework.s3.amazonaws.com/rover_130x130.png)">Rover.com</a></li>
<li><a href="https://getsentry.com/welcome/" style="background-image: url(https://fund-rest-framework.s3.amazonaws.com/sentry130.png)">Sentry</a></li>
<li><a href="https://www.rover.com/careers/" style="background-image: url(https://fund-rest-framework.s3.amazonaws.com/rover_130x130.png)">Rover.com</a></li>
<li><a href="https://sentry.io/welcome/" style="background-image: url(https://fund-rest-framework.s3.amazonaws.com/sentry130.png)">Sentry</a></li>
<li><a href="https://getstream.io/?utm_source=drf&utm_medium=banner&utm_campaign=drf" style="background-image: url(https://fund-rest-framework.s3.amazonaws.com/stream-130.png)">Stream</a></li>
</ul>
<div style="clear: both; padding-bottom: 20px;"></div>
*Many thanks to all our [awesome sponsors][sponsors], and in particular to our premium backers, [Rover](http://jobs.rover.com/), [Sentry](https://getsentry.com/welcome/), and [Stream](https://getstream.io/?utm_source=drf&utm_medium=banner&utm_campaign=drf).*
*Many thanks to all our [awesome sponsors][sponsors], and in particular to our premium backers, [Rover](https://www.rover.com/careers/), [Sentry](https://sentry.io/welcome/), and [Stream](https://getstream.io/?utm_source=drf&utm_medium=banner&utm_campaign=drf).*
---

View File

@ -32,14 +32,14 @@ we strongly encourage you to invest in its continued development by
**[signing up for a paid&nbsp;plan][funding]**.
<ul class="premium-promo promo">
<li><a href="http://jobs.rover.com/" style="background-image: url(https://fund-rest-framework.s3.amazonaws.com/rover_130x130.png)">Rover.com</a></li>
<li><a href="https://getsentry.com/welcome/" style="background-image: url(https://fund-rest-framework.s3.amazonaws.com/sentry130.png)">Sentry</a></li>
<li><a href="https://www.rover.com/careers/" style="background-image: url(https://fund-rest-framework.s3.amazonaws.com/rover_130x130.png)">Rover.com</a></li>
<li><a href="https://sentry.io/welcome/" style="background-image: url(https://fund-rest-framework.s3.amazonaws.com/sentry130.png)">Sentry</a></li>
<li><a href="https://getstream.io/?utm_source=drf&utm_medium=banner&utm_campaign=drf" style="background-image: url(https://fund-rest-framework.s3.amazonaws.com/stream-130.png)">Stream</a></li>
<li><a href="https://www.machinalis.com/#services" style="background-image: url(https://fund-rest-framework.s3.amazonaws.com/Machinalis130.png)">Machinalis</a></li>
</ul>
<div style="clear: both; padding-bottom: 20px;"></div>
*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](https://www.machinalis.com/#services).*
*Many thanks to all our [sponsors][sponsors], and in particular to our premium backers, [Rover](https://www.rover.com/careers/), [Sentry](https://sentry.io/welcome/), [Stream](https://getstream.io/?utm_source=drf&utm_medium=banner&utm_campaign=drf), and [Machinalis](https://www.machinalis.com/#services).*
---

View File

@ -39,16 +39,16 @@ we strongly encourage you to invest in its continued development by
**[signing up for a paid&nbsp;plan][funding]**.
<ul class="premium-promo promo">
<li><a href="http://jobs.rover.com/" style="background-image: url(https://fund-rest-framework.s3.amazonaws.com/rover_130x130.png)">Rover.com</a></li>
<li><a href="https://getsentry.com/welcome/" style="background-image: url(https://fund-rest-framework.s3.amazonaws.com/sentry130.png)">Sentry</a></li>
<li><a href="https://www.rover.com/careers/" style="background-image: url(https://fund-rest-framework.s3.amazonaws.com/rover_130x130.png)">Rover.com</a></li>
<li><a href="https://sentry.io/welcome/" style="background-image: url(https://fund-rest-framework.s3.amazonaws.com/sentry130.png)">Sentry</a></li>
<li><a href="https://getstream.io/try-the-api/?utm_source=drf&utm_medium=banner&utm_campaign=drf" style="background-image: url(https://fund-rest-framework.s3.amazonaws.com/stream-130.png)">Stream</a></li>
<li><a href="https://hello.machinalis.co.uk/" style="background-image: url(https://fund-rest-framework.s3.amazonaws.com/Machinalis130.png)">Machinalis</a></li>
<li><a href="https://machinalis.com/" style="background-image: url(https://fund-rest-framework.s3.amazonaws.com/Machinalis130.png)">Machinalis</a></li>
<li><a href="https://rollbar.com" style="background-image: url(https://fund-rest-framework.s3.amazonaws.com/rollbar.png)">Rollbar</a></li>
<li><a href="https://micropyramid.com/django-rest-framework-development-services/" style="background-image: url(https://fund-rest-framework.s3.amazonaws.com/mp-text-logo.png)">MicroPyramid</a></li>
</ul>
<div style="clear: both; padding-bottom: 20px;"></div>
*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), [Machinalis](https://hello.machinalis.co.uk/), [Rollbar](https://rollbar.com), and [MicroPyramid](https://micropyramid.com/django-rest-framework-development-services/).*
*Many thanks to all our [sponsors][sponsors], and in particular to our premium backers, [Rover](https://www.rover.com/careers/), [Sentry](https://sentry.io/welcome/), [Stream](https://getstream.io/?utm_source=drf&utm_medium=banner&utm_campaign=drf), [Machinalis](https://machinalis.com/), [Rollbar](https://rollbar.com), and [MicroPyramid](https://micropyramid.com/django-rest-framework-development-services/).*
---

View File

@ -33,15 +33,15 @@ If you use REST framework commercially and would like to see this work continue,
**[signing up for a paid&nbsp;plan][funding]**.
<ul class="premium-promo promo">
<li><a href="http://jobs.rover.com/" style="background-image: url(https://fund-rest-framework.s3.amazonaws.com/rover_130x130.png)">Rover.com</a></li>
<li><a href="https://getsentry.com/welcome/" style="background-image: url(https://fund-rest-framework.s3.amazonaws.com/sentry130.png)">Sentry</a></li>
<li><a href="https://www.rover.com/careers/" style="background-image: url(https://fund-rest-framework.s3.amazonaws.com/rover_130x130.png)">Rover.com</a></li>
<li><a href="https://sentry.io/welcome/" style="background-image: url(https://fund-rest-framework.s3.amazonaws.com/sentry130.png)">Sentry</a></li>
<li><a href="https://getstream.io/try-the-api/?utm_source=drf&utm_medium=banner&utm_campaign=drf" style="background-image: url(https://fund-rest-framework.s3.amazonaws.com/stream-130.png)">Stream</a></li>
<li><a href="https://hello.machinalis.co.uk/" style="background-image: url(https://fund-rest-framework.s3.amazonaws.com/Machinalis130.png)">Machinalis</a></li>
<li><a href="https://machinalis.com/" style="background-image: url(https://fund-rest-framework.s3.amazonaws.com/Machinalis130.png)">Machinalis</a></li>
<li><a href="https://rollbar.com" style="background-image: url(https://fund-rest-framework.s3.amazonaws.com/rollbar.png)">Rollbar</a></li>
</ul>
<div style="clear: both; padding-bottom: 20px;"></div>
*As well as our release sponsor, we'd like to say thanks in particular 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), [Machinalis](https://hello.machinalis.co.uk/), and [Rollbar](https://rollbar.com).*
*As well as our release sponsor, we'd like to say thanks in particular our premium backers, [Rover](https://www.rover.com/careers/), [Sentry](https://sentry.io/welcome/), [Stream](https://getstream.io/?utm_source=drf&utm_medium=banner&utm_campaign=drf), [Machinalis](https://machinalis.com/), and [Rollbar](https://rollbar.com).*
---

View File

@ -30,7 +30,7 @@ If you use REST framework commercially and would like to see this work continue,
**[signing up for a paid&nbsp;plan][funding]**.
*We'd like to say thanks in particular 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), [Machinalis](https://hello.machinalis.co.uk/), and [Rollbar](https://rollbar.com).*
*We'd like to say thanks in particular our premium backers, [Rover](https://www.rover.com/careers/), [Sentry](https://sentry.io/welcome/), [Stream](https://getstream.io/?utm_source=drf&utm_medium=banner&utm_campaign=drf), [Machinalis](https://machinalis.com/), and [Rollbar](https://rollbar.com).*
---

View File

@ -30,8 +30,8 @@ If you use REST framework commercially and would like to see this work continue,
<ul class="premium-promo promo">
<li><a href="http://jobs.rover.com/" style="background-image: url(https://fund-rest-framework.s3.amazonaws.com/rover_130x130.png)">Rover.com</a></li>
<li><a href="https://getsentry.com/welcome/" style="background-image: url(https://fund-rest-framework.s3.amazonaws.com/sentry130.png)">Sentry</a></li>
<li><a href="https://www.rover.com/careers/" style="background-image: url(https://fund-rest-framework.s3.amazonaws.com/rover_130x130.png)">Rover.com</a></li>
<li><a href="https://sentry.io/welcome/" style="background-image: url(https://fund-rest-framework.s3.amazonaws.com/sentry130.png)">Sentry</a></li>
<li><a href="https://getstream.io/try-the-api/?utm_source=drf&utm_medium=banner&utm_campaign=drf" style="background-image: url(https://fund-rest-framework.s3.amazonaws.com/stream-130.png)">Stream</a></li>
<li><a href="https://auklet.io" style="background-image: url(https://fund-rest-framework.s3.amazonaws.com/auklet-new.png)">Auklet</a></li>
<li><a href="https://rollbar.com" style="background-image: url(https://fund-rest-framework.s3.amazonaws.com/rollbar2.png)">Rollbar</a></li>
@ -41,7 +41,7 @@ If you use REST framework commercially and would like to see this work continue,
</ul>
<div style="clear: both; padding-bottom: 20px;"></div>
*Many thanks to all our [wonderful 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), [Auklet](https://auklet.io/), [Rollbar](https://rollbar.com), [Cadre](https://cadre.com), [Load Impact](https://loadimpact.com/?utm_campaign=Sponsorship%20links&utm_source=drf&utm_medium=drf), and [Kloudless](https://hubs.ly/H0f30Lf0).*
*Many thanks to all our [wonderful sponsors][sponsors], and in particular to our premium backers, [Rover](https://www.rover.com/careers/), [Sentry](https://sentry.io/welcome/), [Stream](https://getstream.io/?utm_source=drf&utm_medium=banner&utm_campaign=drf), [Auklet](https://auklet.io/), [Rollbar](https://rollbar.com), [Cadre](https://cadre.com), [Load Impact](https://loadimpact.com/?utm_campaign=Sponsorship%20links&utm_source=drf&utm_medium=drf), and [Kloudless](https://hubs.ly/H0f30Lf0).*
---

View File

@ -123,10 +123,10 @@ REST framework continues to be open-source and permissively licensed, but we fir
## What funding has enabled so far
* The [3.4](https://www.django-rest-framework.org/topics/3.4-announcement/) and [3.5](https://www.django-rest-framework.org/topics/3.5-announcement/) releases, including schema generation for both Swagger and RAML, a Python client library, a Command Line client, and addressing of a large number of outstanding issues.
* The [3.6](https://www.django-rest-framework.org/topics/3.6-announcement/) release, including JavaScript client library, and API documentation, complete with auto-generated code samples.
* The [3.7 release](https://www.django-rest-framework.org/topics/3.7-announcement/), made possible due to our collaborative funding model, focuses on improvements to schema generation and the interactive API documentation.
* The recent [3.8 release](https://www.django-rest-framework.org/topics/3.8-announcement/).
* The [3.4](https://www.django-rest-framework.org/community/3.4-announcement/) and [3.5](https://www.django-rest-framework.org/community/3.5-announcement/) releases, including schema generation for both Swagger and RAML, a Python client library, a Command Line client, and addressing of a large number of outstanding issues.
* The [3.6](https://www.django-rest-framework.org/community/3.6-announcement/) release, including JavaScript client library, and API documentation, complete with auto-generated code samples.
* The [3.7 release](https://www.django-rest-framework.org/community/3.7-announcement/), made possible due to our collaborative funding model, focuses on improvements to schema generation and the interactive API documentation.
* The recent [3.8 release](https://www.django-rest-framework.org/community/3.8-announcement/).
* Tom Christie, the creator of Django REST framework, working on the project full-time.
* Around 80-90 issues and pull requests closed per month since Tom Christie started working on the project full-time.
* A community & operations manager position part-time for 4 months, helping mature the business and grow sponsorship.
@ -341,7 +341,7 @@ For further enquires please contact <a href=mailto:funding@django-rest-framework
## Accountability
In an effort to keep the project as transparent as possible, we are releasing [monthly progress reports](https://www.encode.io/reports/march-2018) and regularly include financial reports and cost breakdowns.
In an effort to keep the project as transparent as possible, we are releasing [monthly progress reports](https://www.encode.io/reports/march-2018/) and regularly include financial reports and cost breakdowns.
<!-- Begin MailChimp Signup Form -->
<link href="//cdn-images.mailchimp.com/embedcode/classic-10_7.css" rel="stylesheet" type="text/css">

View File

@ -9,14 +9,12 @@ Looking for a new Django REST Framework related role? On this site we provide a
* [https://www.python.org/jobs/][python-org-jobs]
* [https://djangogigs.com][django-gigs-com]
* [https://djangojobs.net/jobs/][django-jobs-net]
* [http://djangojobbers.com][django-jobbers-com]
* [https://www.indeed.com/q-Django-jobs.html][indeed-com]
* [https://stackoverflow.com/jobs/developer-jobs-using-django][stackoverflow-com]
* [https://www.upwork.com/o/jobs/browse/skill/django-framework/][upwork-com]
* [https://www.technojobs.co.uk/django-jobs][technobjobs-co-uk]
* [https://remoteok.io/remote-django-jobs][remoteok-io]
* [https://www.remotepython.com/jobs/][remotepython-com]
* [https://weworkcontract.com/python-contract-jobs][weworkcontract-com]
Know of any other great resources for Django REST Framework jobs that are missing in our list? Please [submit a pull request][submit-pr] or [email us][anna-email].
@ -28,14 +26,12 @@ Wonder how else you can help? One of the best ways you can help Django REST Fram
[python-org-jobs]: https://www.python.org/jobs/
[django-gigs-com]: https://djangogigs.com
[django-jobs-net]: https://djangojobs.net/jobs/
[django-jobbers-com]: http://djangojobbers.com
[indeed-com]: https://www.indeed.com/q-Django-jobs.html
[stackoverflow-com]: https://stackoverflow.com/jobs/developer-jobs-using-django
[upwork-com]: https://www.upwork.com/o/jobs/browse/skill/django-framework/
[technobjobs-co-uk]: https://www.technojobs.co.uk/django-jobs
[remoteok-io]: https://remoteok.io/remote-django-jobs
[remotepython-com]: https://www.remotepython.com/jobs/
[weworkcontract-com]: https://weworkcontract.com/python-contract-jobs
[drf-funding]: https://fund.django-rest-framework.org/topics/funding/
[submit-pr]: https://github.com/encode/django-rest-framework
[anna-email]: mailto:anna@django-rest-framework.org

View File

@ -47,8 +47,8 @@ Our platinum sponsors have each made a hugely substantial contribution to the fu
</ul>
<ul class="sponsor platinum">
<li><a href="https://www.divio.ch/" rel="nofollow" style="background-image:url(../../img/sponsors/1-divio.png);">Divio</a></li>
<li><a href="http://company.onlulu.com/en/" rel="nofollow" style="background-image:url(../../img/sponsors/1-lulu.png);">Lulu</a></li>
<li><a href="https://www.divio.com/" rel="nofollow" style="background-image:url(../../img/sponsors/1-divio.png);">Divio</a></li>
<li><a href="https://onlulu.com" rel="nofollow" style="background-image:url(../../img/sponsors/1-lulu.png);">Lulu</a></li>
<li><a href="https://p.ota.to/" rel="nofollow" style="background-image:url(../../img/sponsors/1-potato.png);">Potato</a></li>
<li><a href="http://www.wiredrive.com/" rel="nofollow" style="background-image:url(../../img/sponsors/1-wiredrive.png);">Wiredrive</a></li>
<li><a href="http://www.cyaninc.com/" rel="nofollow" style="background-image:url(../../img/sponsors/1-cyan.png);">Cyan</a></li>
@ -81,8 +81,8 @@ Our gold sponsors include companies large and small. Many thanks for their signi
<li><a href="https://www.lightningkite.com/" rel="nofollow" style="background-image:url(../../img/sponsors/2-lightning_kite.png);">Lightning Kite</a></li>
<li><a href="https://opbeat.com/" rel="nofollow" style="background-image:url(../../img/sponsors/2-opbeat.png);">Opbeat</a></li>
<li><a href="https://koordinates.com" rel="nofollow" style="background-image:url(../../img/sponsors/2-koordinates.png);">Koordinates</a></li>
<li><a href="http://pulsecode.ca" rel="nofollow" style="background-image:url(../../img/sponsors/2-pulsecode.png);">Pulsecode Inc.</a></li>
<li><a href="http://singinghorsestudio.com" rel="nofollow" style="background-image:url(../../img/sponsors/2-singing-horse.png);">Singing Horse Studio Ltd.</a></li>
<li><a rel="nofollow" style="background-image:url(../../img/sponsors/2-pulsecode.png);">Pulsecode Inc.</a></li>
<li><a rel="nofollow" style="background-image:url(../../img/sponsors/2-singing-horse.png);">Singing Horse Studio Ltd.</a></li>
<li><a href="https://www.heroku.com/" rel="nofollow" style="background-image:url(../../img/sponsors/2-heroku.png);">Heroku</a></li>
<li><a href="https://www.rheinwerk-verlag.de/" rel="nofollow" style="background-image:url(../../img/sponsors/2-rheinwerk_verlag.png);">Rheinwerk Verlag</a></li>
<li><a href="https://www.securitycompass.com/" rel="nofollow" style="background-image:url(../../img/sponsors/2-security_compass.png);">Security Compass</a></li>
@ -90,9 +90,9 @@ Our gold sponsors include companies large and small. Many thanks for their signi
<li><a href="http://www.hipflaskapp.com" rel="nofollow" style="background-image:url(../../img/sponsors/2-hipflask.png);">Hipflask</a></li>
<li><a href="http://www.crate.io/" rel="nofollow" style="background-image:url(../../img/sponsors/2-crate.png);">Crate</a></li>
<li><a href="http://crypticocorp.com/" rel="nofollow" style="background-image:url(../../img/sponsors/2-cryptico.png);">Cryptico Corp</a></li>
<li><a href="http://www.nexthub.com/" rel="nofollow" style="background-image:url(../../img/sponsors/2-nexthub.png);">NextHub</a></li>
<li><a rel="nofollow" style="background-image:url(../../img/sponsors/2-nexthub.png);">NextHub</a></li>
<li><a href="https://www.compile.com/" rel="nofollow" style="background-image:url(../../img/sponsors/2-compile.png);">Compile</a></li>
<li><a href="http://wusawork.org" rel="nofollow" style="background-image:url(../../img/sponsors/2-wusawork.png);">WusaWork</a></li>
<li><a rel="nofollow" style="background-image:url(../../img/sponsors/2-wusawork.png);">WusaWork</a></li>
<li><a href="http://envisionlinux.org/blog" rel="nofollow">Envision Linux</a></li>
</ul>

View File

@ -18,9 +18,9 @@ REST framework releases follow a formal deprecation policy, which is in line wit
The timeline for deprecation of a feature present in version 1.0 would work as follows:
* Version 1.1 would remain **fully backwards compatible** with 1.0, but would raise `PendingDeprecationWarning` warnings if you use the feature that are due to be deprecated. These warnings are **silent by default**, but can be explicitly enabled when you're ready to start migrating any required changes. For example if you start running your tests using `python -Wd manage.py test`, you'll be warned of any API changes you need to make.
* Version 1.1 would remain **fully backwards compatible** with 1.0, but would raise `RemovedInDRF13Warning` warnings, subclassing `PendingDeprecationWarning`, if you use the feature that are due to be deprecated. These warnings are **silent by default**, but can be explicitly enabled when you're ready to start migrating any required changes. For example if you start running your tests using `python -Wd manage.py test`, you'll be warned of any API changes you need to make.
* Version 1.2 would escalate these warnings to `DeprecationWarning`, which is loud by default.
* Version 1.2 would escalate these warnings to subclass `DeprecationWarning`, which is loud by default.
* Version 1.3 would remove the deprecated bits of API entirely.
@ -40,6 +40,35 @@ You can determine your currently installed version using `pip show`:
## 3.9.x series
### 3.9.2
**Date**: [3rd March 2019][3.9.1-milestone]
* Routers: invalidate `_urls` cache on `register()` [#6407][gh6407]
* Deferred schema renderer creation to avoid requiring pyyaml. [#6416][gh6416]
* Added 'request_forms' block to base.html [#6340][gh6340]
* Fixed SchemaView to reset renderer on exception. [#6429][gh6429]
* Update Django Guardian dependency. [#6430][gh6430]
* Ensured support for Django 2.2 [#6422][gh6422] & [#6455][gh6455]
* Made templates compatible with session-based CSRF. [#6207][gh6207]
* Adjusted field `validators` to accept non-list iterables. [#6282][gh6282]
* Added SearchFilter.get_search_fields() hook. [#6279][gh6279]
* Fix DeprecationWarning when accessing collections.abc classes via collections [#6268][gh6268]
* Allowed Q objects in limit_choices_to introspection. [#6472][gh6472]
* Added lazy evaluation to composed permissions. [#6463][gh6463]
* Add negation ~ operator to permissions composition [#6361][gh6361]
* Avoided calling distinct on annotated fields in SearchFilter. [#6240][gh6240]
* Introduced `RemovedInDRF…Warning` classes to simplify deprecations. [#6480][gh6480]
### 3.9.1
**Date**: [16th January 2019][3.9.1-milestone]
* Resolve XSS issue in browsable API. [#6330][gh6330]
* Upgrade Bootstrap to 3.4.0 to resolve XSS issue.
* Resolve issues with composable permissions. [#6299][gh6299]
* Respect `limit_choices_to` on foreign keys. [#6371][gh6371]
### 3.9.0
**Date**: [18th October 2018][3.9.0-milestone]
@ -1135,6 +1164,8 @@ For older release notes, [please see the version 2.x documentation][old-release-
[3.8.1-milestone]: https://github.com/encode/django-rest-framework/milestone/67?closed=1
[3.8.2-milestone]: https://github.com/encode/django-rest-framework/milestone/68?closed=1
[3.9.0-milestone]: https://github.com/encode/django-rest-framework/milestone/66?closed=1
[3.9.1-milestone]: https://github.com/encode/django-rest-framework/milestone/70?closed=1
[3.9.1-milestone]: https://github.com/encode/django-rest-framework/milestone/71?closed=1
<!-- 3.0.1 -->
[gh2013]: https://github.com/encode/django-rest-framework/issues/2013
@ -2052,3 +2083,26 @@ For older release notes, [please see the version 2.x documentation][old-release-
[gh6233]: https://github.com/encode/django-rest-framework/issues/6233
[gh5753]: https://github.com/encode/django-rest-framework/issues/5753
[gh6229]: https://github.com/encode/django-rest-framework/issues/6229
<!-- 3.9.1 -->
[gh6330]: https://github.com/encode/django-rest-framework/issues/6330
[gh6299]: https://github.com/encode/django-rest-framework/issues/6299
[gh6371]: https://github.com/encode/django-rest-framework/issues/6371
<!-- 3.9.2 -->
[gh6480]: https://github.com/encode/django-rest-framework/issues/6480
[gh6240]: https://github.com/encode/django-rest-framework/issues/6240
[gh6361]: https://github.com/encode/django-rest-framework/issues/6361
[gh6463]: https://github.com/encode/django-rest-framework/issues/6463
[gh6472]: https://github.com/encode/django-rest-framework/issues/6472
[gh6268]: https://github.com/encode/django-rest-framework/issues/6268
[gh6279]: https://github.com/encode/django-rest-framework/issues/6279
[gh6282]: https://github.com/encode/django-rest-framework/issues/6282
[gh6207]: https://github.com/encode/django-rest-framework/issues/6207
[gh6455]: https://github.com/encode/django-rest-framework/issues/6455
[gh6422]: https://github.com/encode/django-rest-framework/issues/6422
[gh6430]: https://github.com/encode/django-rest-framework/issues/6430
[gh6429]: https://github.com/encode/django-rest-framework/issues/6429
[gh6340]: https://github.com/encode/django-rest-framework/issues/6340
[gh6416]: https://github.com/encode/django-rest-framework/issues/6416
[gh6407]: https://github.com/encode/django-rest-framework/issues/6407

View File

@ -263,6 +263,7 @@ To submit new content, [open an issue][drf-create-issue] or [create a pull reque
* [django-rest-messaging][django-rest-messaging], [django-rest-messaging-centrifugo][django-rest-messaging-centrifugo] and [django-rest-messaging-js][django-rest-messaging-js] - A real-time pluggable messaging service using DRM.
* [djangorest-alchemy][djangorest-alchemy] - SQLAlchemy support for REST framework.
* [djangorestframework-datatables][djangorestframework-datatables] - Seamless integration between Django REST framework and [Datatables](https://datatables.net).
* [django-rest-framework-condition][django-rest-framework-condition] - Decorators for managing HTTP cache headers for Django REST framework (ETag and Last-modified).
[cite]: http://www.software-ecosystems.com/Software_Ecosystems/Ecosystems.html
[cookiecutter]: https://github.com/jpadilla/cookiecutter-django-rest-framework
@ -336,3 +337,4 @@ To submit new content, [open an issue][drf-create-issue] or [create a pull reque
[drfpasswordless]: https://github.com/aaronn/django-rest-framework-passwordless
[djangorest-alchemy]: https://github.com/dealertrack/djangorest-alchemy
[djangorestframework-datatables]: https://github.com/izimobil/django-rest-framework-datatables
[django-rest-framework-condition]: https://github.com/jozo/django-rest-framework-condition

Binary file not shown.

Before

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

View File

@ -66,19 +66,17 @@ continued development by **[signing up for a paid plan][funding]**.
*Every single sign-up helps us make REST framework long-term financially sustainable.*
<ul class="premium-promo promo">
<li><a href="http://jobs.rover.com/" style="background-image: url(https://fund-rest-framework.s3.amazonaws.com/rover_130x130.png)">Rover.com</a></li>
<li><a href="https://getsentry.com/welcome/" style="background-image: url(https://fund-rest-framework.s3.amazonaws.com/sentry130.png)">Sentry</a></li>
<li><a href="https://getstream.io/try-the-api/?utm_source=drf&utm_medium=banner&utm_campaign=drf" style="background-image: url(https://fund-rest-framework.s3.amazonaws.com/stream-130.png)">Stream</a></li>
<li><a href="https://auklet.io" style="background-image: url(https://fund-rest-framework.s3.amazonaws.com/auklet-new.png)">Auklet</a></li>
<li><a href="https://releasehistory.io" style="background-image: url(https://fund-rest-framework.s3.amazonaws.com/release-history.png)">Release History</a></li>
<li><a href="https://rollbar.com" style="background-image: url(https://fund-rest-framework.s3.amazonaws.com/rollbar2.png)">Rollbar</a></li>
<li><a href="https://cadre.com" style="background-image: url(https://fund-rest-framework.s3.amazonaws.com/cadre.png)">Cadre</a></li>
<li><a href="https://loadimpact.com/?utm_campaign=Sponsorship%20links&utm_source=drf&utm_medium=drf" style="background-image: url(https://fund-rest-framework.s3.amazonaws.com/load-impact.png)">Load Impact</a></li>
<li><a href="https://hubs.ly/H0f30Lf0" style="background-image: url(https://fund-rest-framework.s3.amazonaws.com/kloudless-plus-text.png)">Kloudless</a></li>
<li><a href="https://lightsonsoftware.com" style="background-image: url(https://fund-rest-framework.s3.amazonaws.com/lightson-dark.png)">Lights On Software</a></li>
</ul>
<div style="clear: both; padding-bottom: 20px;"></div>
*Many thanks to all our [wonderful 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), [Auklet](https://auklet.io/), [Rollbar](https://rollbar.com), [Cadre](https://cadre.com), [Load Impact](https://loadimpact.com/?utm_campaign=Sponsorship%20links&utm_source=drf&utm_medium=drf), [Kloudless](https://hubs.ly/H0f30Lf0), and [Lights On Software](https://lightsonsoftware.com).*
*Many thanks to all our [wonderful sponsors][sponsors], and in particular to our premium backers, [Sentry](https://getsentry.com/welcome/), [Stream](https://getstream.io/?utm_source=drf&utm_medium=banner&utm_campaign=drf), [Release History](https://releasehistory.io), [Rollbar](https://rollbar.com), [Cadre](https://cadre.com), [Kloudless](https://hubs.ly/H0f30Lf0), and [Lights On Software](https://lightsonsoftware.com).*
---
@ -87,7 +85,7 @@ continued development by **[signing up for a paid plan][funding]**.
REST framework requires the following:
* Python (2.7, 3.4, 3.5, 3.6, 3.7)
* Django (1.11, 2.0, 2.1)
* Django (1.11, 2.0, 2.1, 2.2)
We **highly recommend** and only officially support the latest patch release of
each Python and Django series.

View File

@ -81,7 +81,7 @@ was later [dropped from the spec][html5]. There remains
as well as how to support content types other than form-encoded data.
[cite]: https://www.amazon.com/RESTful-Web-Services-Leonard-Richardson/dp/0596529260
[ajax-form]: https://github.com/encode/ajax-form
[ajax-form]: https://github.com/tomchristie/ajax-form
[rails]: https://guides.rubyonrails.org/form_helpers.html#how-do-forms-with-put-or-delete-methods-work
[html5]: https://www.w3.org/TR/html5-diff/#changes-2010-06-24
[put_delete]: http://amundsen.com/examples/put-delete-forms/

View File

@ -8,7 +8,7 @@ The tutorial is fairly in-depth, so you should probably get a cookie and a cup o
---
**Note**: The code for this tutorial is available in the [tomchristie/rest-framework-tutorial][repo] repository on GitHub. The completed implementation is also online as a sandbox version for testing, [available here][sandbox].
**Note**: The code for this tutorial is available in the [encode/rest-framework-tutorial][repo] repository on GitHub. The completed implementation is also online as a sandbox version for testing, [available here][sandbox].
---
@ -137,20 +137,20 @@ Okay, once we've got a few imports out of the way, let's create a couple of code
snippet = Snippet(code='foo = "bar"\n')
snippet.save()
snippet = Snippet(code='print "hello, world"\n')
snippet = Snippet(code='print("hello, world")\n')
snippet.save()
We've now got a few snippet instances to play with. Let's take a look at serializing one of those instances.
serializer = SnippetSerializer(snippet)
serializer.data
# {'id': 2, 'title': u'', 'code': u'print "hello, world"\n', 'linenos': False, 'language': u'python', 'style': u'friendly'}
# {'id': 2, 'title': '', 'code': 'print("hello, world")\n', 'linenos': False, 'language': 'python', 'style': 'friendly'}
At this point we've translated the model instance into Python native datatypes. To finalize the serialization process we render the data into `json`.
content = JSONRenderer().render(serializer.data)
content
# '{"id": 2, "title": "", "code": "print \\"hello, world\\"\\n", "linenos": false, "language": "python", "style": "friendly"}'
# b'{"id": 2, "title": "", "code": "print(\\"hello, world\\")\\n", "linenos": false, "language": "python", "style": "friendly"}'
Deserialization is similar. First we parse a stream into Python native datatypes...
@ -165,7 +165,7 @@ Deserialization is similar. First we parse a stream into Python native datatype
serializer.is_valid()
# True
serializer.validated_data
# OrderedDict([('title', ''), ('code', 'print "hello, world"\n'), ('linenos', False), ('language', 'python'), ('style', 'friendly')])
# OrderedDict([('title', ''), ('code', 'print("hello, world")\n'), ('linenos', False), ('language', 'python'), ('style', 'friendly')])
serializer.save()
# <Snippet: Snippet object>
@ -175,7 +175,7 @@ We can also serialize querysets instead of model instances. To do so we simply
serializer = SnippetSerializer(Snippet.objects.all(), many=True)
serializer.data
# [OrderedDict([('id', 1), ('title', u''), ('code', u'foo = "bar"\n'), ('linenos', False), ('language', 'python'), ('style', 'friendly')]), OrderedDict([('id', 2), ('title', u''), ('code', u'print "hello, world"\n'), ('linenos', False), ('language', 'python'), ('style', 'friendly')]), OrderedDict([('id', 3), ('title', u''), ('code', u'print "hello, world"'), ('linenos', False), ('language', 'python'), ('style', 'friendly')])]
# [OrderedDict([('id', 1), ('title', ''), ('code', 'foo = "bar"\n'), ('linenos', False), ('language', 'python'), ('style', 'friendly')]), OrderedDict([('id', 2), ('title', ''), ('code', 'print("hello, world")\n'), ('linenos', False), ('language', 'python'), ('style', 'friendly')]), OrderedDict([('id', 3), ('title', ''), ('code', 'print("hello, world")'), ('linenos', False), ('language', 'python'), ('style', 'friendly')])]
## Using ModelSerializers
@ -218,7 +218,6 @@ Edit the `snippets/views.py` file, and add the following.
from django.http import HttpResponse, JsonResponse
from django.views.decorators.csrf import csrf_exempt
from rest_framework.renderers import JSONRenderer
from rest_framework.parsers import JSONParser
from snippets.models import Snippet
from snippets.serializers import SnippetSerializer
@ -338,7 +337,7 @@ Finally, we can get a list of all of the snippets:
{
"id": 2,
"title": "",
"code": "print \"hello, world\"\n",
"code": "print(\"hello, world\")\n",
"linenos": false,
"language": "python",
"style": "friendly"
@ -354,7 +353,7 @@ Or we can get a particular snippet by referencing its id:
{
"id": 2,
"title": "",
"code": "print \"hello, world\"\n",
"code": "print(\"hello, world\")\n",
"linenos": false,
"language": "python",
"style": "friendly"

View File

@ -143,7 +143,7 @@ We can get a list of all of the snippets, as before.
{
"id": 2,
"title": "",
"code": "print \"hello, world\"\n",
"code": "print(\"hello, world\")\n",
"linenos": false,
"language": "python",
"style": "friendly"
@ -163,24 +163,24 @@ Or by appending a format suffix:
Similarly, we can control the format of the request that we send, using the `Content-Type` header.
# POST using form data
http --form POST http://127.0.0.1:8000/snippets/ code="print 123"
http --form POST http://127.0.0.1:8000/snippets/ code="print(123)"
{
"id": 3,
"title": "",
"code": "print 123",
"code": "print(123)",
"linenos": false,
"language": "python",
"style": "friendly"
}
# POST using JSON
http --json POST http://127.0.0.1:8000/snippets/ code="print 456"
http --json POST http://127.0.0.1:8000/snippets/ code="print(456)"
{
"id": 4,
"title": "",
"code": "print 456",
"code": "print(456)",
"linenos": false,
"language": "python",
"style": "friendly"

View File

@ -197,7 +197,7 @@ If we're interacting with the API programmatically we need to explicitly provide
If we try to create a snippet without authenticating, we'll get an error:
http POST http://127.0.0.1:8000/snippets/ code="print 123"
http POST http://127.0.0.1:8000/snippets/ code="print(123)"
{
"detail": "Authentication credentials were not provided."
@ -205,13 +205,13 @@ If we try to create a snippet without authenticating, we'll get an error:
We can make a successful request by including the username and password of one of the users we created earlier.
http -a admin:password123 POST http://127.0.0.1:8000/snippets/ code="print 789"
http -a admin:password123 POST http://127.0.0.1:8000/snippets/ code="print(789)"
{
"id": 1,
"owner": "admin",
"title": "foo",
"code": "print 789",
"code": "print(789)",
"linenos": false,
"language": "python",
"style": "friendly"

View File

@ -29,9 +29,10 @@ automatically generated schemas. Since we're using viewsets and routers,
we can simply use the automatic schema generation.
You'll need to install the `coreapi` python package in order to include an
API schema.
API schema, and `pyyaml` to render the schema into the commonly used
YAML-based OpenAPI format.
$ pip install coreapi
$ pip install coreapi pyyaml
We can now include a schema for our API, by including an autogenerated schema
view in our URL configuration.

View File

@ -111,7 +111,7 @@ We can easily break these down into individual views if we need to, but using vi
Okay, now let's wire up the API URLs. On to `tutorial/urls.py`...
from django.conf.urls import url, include
from django.urls import include, path
from rest_framework import routers
from tutorial.quickstart import views
@ -122,8 +122,8 @@ Okay, now let's wire up the API URLs. On to `tutorial/urls.py`...
# Wire up our API using automatic URL routing.
# Additionally, we include login URLs for the browsable API.
urlpatterns = [
url(r'^', include(router.urls)),
url(r'^api-auth/', include('rest_framework.urls', namespace='rest_framework'))
path('', include(router.urls)),
path('api-auth/', include('rest_framework.urls', namespace='rest_framework'))
]
Because we're using viewsets instead of views, we can automatically generate the URL conf for our API, by simply registering the viewsets with a router class.

0
docs_theme/css/bootstrap-responsive.css vendored Executable file → Normal file
View File

0
docs_theme/css/bootstrap.css vendored Executable file → Normal file
View File

0
docs_theme/js/bootstrap-2.1.1-min.js vendored Executable file → Normal file
View File

View File

@ -1,7 +1,7 @@
# Optional packages which may be used with REST framework.
psycopg2-binary==2.7.5
markdown==2.6.11
django-guardian==1.4.9
django-guardian==1.5.0
django-filter==1.1.0
coreapi==2.3.1
coreschema==0.0.4

View File

@ -1,4 +1,4 @@
# Pytest for running the tests.
pytest==3.6.2
pytest-django==3.3.2
pytest-cov==2.5.1
pytest==4.3.0
pytest-django==3.4.8
pytest-cov==2.6.1

View File

@ -8,10 +8,10 @@ ______ _____ _____ _____ __
"""
__title__ = 'Django REST framework'
__version__ = '3.9.0'
__version__ = '3.9.2'
__author__ = 'Tom Christie'
__license__ = 'BSD 2-Clause'
__copyright__ = 'Copyright 2011-2018 Tom Christie'
__copyright__ = 'Copyright 2011-2019 Encode OSS Ltd'
# Version synonym
VERSION = __version__
@ -23,3 +23,11 @@ HTTP_HEADER_ENCODING = 'iso-8859-1'
ISO_8601 = 'iso-8601'
default_app_config = 'rest_framework.apps.RestFrameworkConfig'
class RemovedInDRF310Warning(DeprecationWarning):
pass
class RemovedInDRF311Warning(PendingDeprecationWarning):
pass

View File

@ -5,6 +5,8 @@ versions of Django/Python, and compatibility wrappers around optional packages.
from __future__ import unicode_literals
import sys
from django.conf import settings
from django.core import validators
from django.utils import six
@ -12,17 +14,10 @@ from django.views.generic import View
try:
# Python 3
from collections.abc import Mapping # noqa
from collections.abc import Mapping, MutableMapping # noqa
except ImportError:
# Python 2.7
from collections import Mapping # noqa
try:
# Python 3
import urllib.parse as urlparse # noqa
except ImportError:
# Python 2.7
from urlparse import urlparse # noqa
from collections import Mapping, MutableMapping # noqa
try:
from django.urls import ( # noqa
@ -41,6 +36,11 @@ try:
except ImportError:
ProhibitNullCharactersValidator = None
try:
from unittest import mock
except ImportError:
mock = None
def get_original_route(urlpattern):
"""
@ -168,6 +168,10 @@ def is_guardian_installed():
"""
django-guardian is optional and only imported if in INSTALLED_APPS.
"""
if six.PY2:
# Guardian 1.5.0, for Django 2.2 is NOT compatible with Python 2.7.
# Remove when dropping PY2.
return False
return 'guardian' in settings.INSTALLED_APPS
@ -317,3 +321,7 @@ class MinLengthValidator(CustomValidatorMessage, validators.MinLengthValidator):
class MaxLengthValidator(CustomValidatorMessage, validators.MaxLengthValidator):
pass
# Version Constants.
PY36 = sys.version_info >= (3, 6)

View File

@ -14,6 +14,7 @@ import warnings
from django.forms.utils import pretty_name
from django.utils import six
from rest_framework import RemovedInDRF310Warning
from rest_framework.views import APIView
@ -225,7 +226,7 @@ def detail_route(methods=None, **kwargs):
warnings.warn(
"`detail_route` is deprecated and will be removed in 3.10 in favor of "
"`action`, which accepts a `detail` bool. Use `@action(detail=True)` instead.",
DeprecationWarning, stacklevel=2
RemovedInDRF310Warning, stacklevel=2
)
def decorator(func):
@ -243,7 +244,7 @@ def list_route(methods=None, **kwargs):
warnings.warn(
"`list_route` is deprecated and will be removed in 3.10 in favor of "
"`action`, which accepts a `detail` bool. Use `@action(detail=False)` instead.",
DeprecationWarning, stacklevel=2
RemovedInDRF310Warning, stacklevel=2
)
def decorator(func):

View File

@ -1,6 +1,5 @@
from __future__ import unicode_literals
import collections
import copy
import datetime
import decimal
@ -33,7 +32,7 @@ from pytz.exceptions import InvalidTimeError
from rest_framework import ISO_8601
from rest_framework.compat import (
MaxLengthValidator, MaxValueValidator, MinLengthValidator,
Mapping, MaxLengthValidator, MaxValueValidator, MinLengthValidator,
MinValueValidator, ProhibitNullCharactersValidator, unicode_repr,
unicode_to_repr
)
@ -96,7 +95,7 @@ def get_attribute(instance, attrs):
"""
for attr in attrs:
try:
if isinstance(instance, collections.Mapping):
if isinstance(instance, Mapping):
instance = instance[attr]
else:
instance = getattr(instance, attr)
@ -350,7 +349,7 @@ class Field(object):
self.default_empty_html = default
if validators is not None:
self.validators = validators[:]
self.validators = list(validators)
# These are set up by `.bind()` when the field is added to a serializer.
self.field_name = None
@ -410,7 +409,7 @@ class Field(object):
self._validators = validators
def get_validators(self):
return self.default_validators[:]
return list(self.default_validators)
def get_initial(self):
"""
@ -1487,7 +1486,7 @@ class MultipleChoiceField(ChoiceField):
return dictionary.get(self.field_name, empty)
def to_internal_value(self, data):
if isinstance(data, type('')) or not hasattr(data, '__iter__'):
if isinstance(data, six.text_type) or not hasattr(data, '__iter__'):
self.fail('not_a_list', input_type=type(data).__name__)
if not self.allow_empty and len(data) == 0:
self.fail('empty')
@ -1661,7 +1660,7 @@ class ListField(Field):
"""
if html.is_html_input(data):
data = html.parse_html_list(data, default=[])
if isinstance(data, type('')) or isinstance(data, collections.Mapping) or not hasattr(data, '__iter__'):
if isinstance(data, (six.text_type, Mapping)) or not hasattr(data, '__iter__'):
self.fail('not_a_list', input_type=type(data).__name__)
if not self.allow_empty and len(data) == 0:
self.fail('empty')
@ -1725,9 +1724,6 @@ class DictField(Field):
return self.run_child_validation(data)
def to_representation(self, value):
"""
List of object instances -> List of dicts of primitive datatypes.
"""
return {
six.text_type(key): self.child.to_representation(val) if val is not None else None
for key, val in value.items()

View File

@ -17,6 +17,7 @@ from django.utils import six
from django.utils.encoding import force_text
from django.utils.translation import ugettext_lazy as _
from rest_framework import RemovedInDRF310Warning
from rest_framework.compat import (
coreapi, coreschema, distinct, is_guardian_installed
)
@ -53,6 +54,14 @@ class SearchFilter(BaseFilterBackend):
search_title = _('Search')
search_description = _('A search term.')
def get_search_fields(self, view, request):
"""
Search fields are obtained from the view, but the request is always
passed to this method. Sub-classes can override this method to
dynamically change the search fields based on request content.
"""
return getattr(view, 'search_fields', None)
def get_search_terms(self, request):
"""
Search terms are set by a ?search=... query parameter,
@ -77,6 +86,9 @@ class SearchFilter(BaseFilterBackend):
opts = queryset.model._meta
if search_field[0] in self.lookup_prefixes:
search_field = search_field[1:]
# Annotated fields do not need to be distinct
if isinstance(queryset, models.QuerySet) and search_field in queryset.query.annotations:
return False
parts = search_field.split(LOOKUP_SEP)
for part in parts:
field = opts.get_field(part)
@ -90,7 +102,7 @@ class SearchFilter(BaseFilterBackend):
return False
def filter_queryset(self, request, queryset, view):
search_fields = getattr(view, 'search_fields', None)
search_fields = self.get_search_fields(view, request)
search_terms = self.get_search_terms(request)
if not search_fields or not search_terms:
@ -288,7 +300,7 @@ class DjangoObjectPermissionsFilter(BaseFilterBackend):
warnings.warn(
"`DjangoObjectPermissionsFilter` has been deprecated and moved to "
"the 3rd-party django-rest-framework-guardian package.",
DeprecationWarning, stacklevel=2
RemovedInDRF310Warning, stacklevel=2
)
assert is_guardian_installed(), 'Using DjangoObjectPermissionsFilter, but django-guardian is not installed'

View File

@ -32,8 +32,10 @@ class Command(BaseCommand):
self.stdout.write(output.decode('utf-8'))
def get_renderer(self, format):
return {
'corejson': CoreJSONRenderer(),
'openapi': OpenAPIRenderer(),
'openapi-json': JSONOpenAPIRenderer()
renderer_cls = {
'corejson': CoreJSONRenderer,
'openapi': OpenAPIRenderer,
'openapi-json': JSONOpenAPIRenderer,
}[format]
return renderer_cls()

View File

@ -24,6 +24,19 @@ class OperationHolderMixin:
def __ror__(self, other):
return OperandHolder(OR, other, self)
def __invert__(self):
return SingleOperandHolder(NOT, self)
class SingleOperandHolder(OperationHolderMixin):
def __init__(self, operator_class, op1_class):
self.operator_class = operator_class
self.op1_class = op1_class
def __call__(self, *args, **kwargs):
op1 = self.op1_class(*args, **kwargs)
return self.operator_class(op1)
class OperandHolder(OperationHolderMixin):
def __init__(self, operator_class, op1_class, op2_class):
@ -44,13 +57,13 @@ class AND:
def has_permission(self, request, view):
return (
self.op1.has_permission(request, view) &
self.op1.has_permission(request, view) and
self.op2.has_permission(request, view)
)
def has_object_permission(self, request, view, obj):
return (
self.op1.has_object_permission(request, view, obj) &
self.op1.has_object_permission(request, view, obj) and
self.op2.has_object_permission(request, view, obj)
)
@ -62,17 +75,28 @@ class OR:
def has_permission(self, request, view):
return (
self.op1.has_permission(request, view) |
self.op1.has_permission(request, view) or
self.op2.has_permission(request, view)
)
def has_object_permission(self, request, view, obj):
return (
self.op1.has_object_permission(request, view, obj) |
self.op1.has_object_permission(request, view, obj) or
self.op2.has_object_permission(request, view, obj)
)
class NOT:
def __init__(self, op1):
self.op1 = op1
def has_permission(self, request, view):
return not self.op1.has_permission(request, view)
def has_object_permission(self, request, view, obj):
return not self.op1.has_object_permission(request, view, obj)
class BasePermissionMetaclass(OperationHolderMixin, type):
pass

View File

@ -518,7 +518,7 @@ class ManyRelatedField(Field):
return dictionary.get(self.field_name, empty)
def to_internal_value(self, data):
if isinstance(data, type('')) or not hasattr(data, '__iter__'):
if isinstance(data, six.text_type) or not hasattr(data, '__iter__'):
self.fail('not_a_list', input_type=type(data).__name__)
if not self.allow_empty and len(data) == 0:
self.fail('empty')

View File

@ -21,11 +21,12 @@ from django.test.client import encode_multipart
from django.urls import NoReverseMatch
from django.utils import six
from django.utils.html import mark_safe
from django.utils.six.moves.urllib import parse as urlparse
from rest_framework import VERSION, exceptions, serializers, status
from rest_framework.compat import (
INDENT_SEPARATORS, LONG_SEPARATORS, SHORT_SEPARATORS, coreapi, coreschema,
pygments_css, urlparse, yaml
pygments_css, yaml
)
from rest_framework.exceptions import ParseError
from rest_framework.request import is_form_media_type, override_method

View File

@ -25,7 +25,9 @@ from django.urls import NoReverseMatch
from django.utils import six
from django.utils.deprecation import RenameMethodsBase
from rest_framework import views
from rest_framework import (
RemovedInDRF310Warning, RemovedInDRF311Warning, views
)
from rest_framework.response import Response
from rest_framework.reverse import reverse
from rest_framework.schemas import SchemaGenerator
@ -43,7 +45,7 @@ class DynamicDetailRoute(object):
"`DynamicDetailRoute` is deprecated and will be removed in 3.10 "
"in favor of `DynamicRoute`, which accepts a `detail` boolean. Use "
"`DynamicRoute(url, name, True, initkwargs)` instead.",
DeprecationWarning, stacklevel=2
RemovedInDRF310Warning, stacklevel=2
)
return DynamicRoute(url, name, True, initkwargs)
@ -54,7 +56,7 @@ class DynamicListRoute(object):
"`DynamicListRoute` is deprecated and will be removed in 3.10 in "
"favor of `DynamicRoute`, which accepts a `detail` boolean. Use "
"`DynamicRoute(url, name, False, initkwargs)` instead.",
DeprecationWarning, stacklevel=2
RemovedInDRF310Warning, stacklevel=2
)
return DynamicRoute(url, name, False, initkwargs)
@ -77,7 +79,7 @@ def flatten(list_of_lists):
class RenameRouterMethods(RenameMethodsBase):
renamed_methods = (
('get_default_base_name', 'get_default_basename', PendingDeprecationWarning),
('get_default_base_name', 'get_default_basename', RemovedInDRF311Warning),
)
@ -88,7 +90,7 @@ class BaseRouter(six.with_metaclass(RenameRouterMethods)):
def register(self, prefix, viewset, basename=None, base_name=None):
if base_name is not None:
msg = "The `base_name` argument is pending deprecation in favor of `basename`."
warnings.warn(msg, PendingDeprecationWarning, 2)
warnings.warn(msg, RemovedInDRF311Warning, 2)
assert not (basename and base_name), (
"Do not provide both the `basename` and `base_name` arguments.")
@ -100,6 +102,10 @@ class BaseRouter(six.with_metaclass(RenameRouterMethods)):
basename = self.get_default_basename(viewset)
self.registry.append((prefix, viewset, basename))
# invalidate the urls cache
if hasattr(self, '_urls'):
del self._urls
def get_default_basename(self, viewset):
"""
If `basename` is not specified, attempt to automatically determine

View File

@ -51,8 +51,10 @@ def field_to_schema(field):
description=description
)
elif isinstance(field, serializers.ManyRelatedField):
related_field_schema = field_to_schema(field.child_relation)
return coreschema.Array(
items=coreschema.String(),
items=related_field_schema,
title=title,
description=description
)

View File

@ -31,3 +31,11 @@ class SchemaView(APIView):
if schema is None:
raise exceptions.PermissionDenied()
return Response(schema)
def handle_exception(self, exc):
# Schema renderers do not render exceptions, so re-perform content
# negotiation with default renderers.
self.renderer_classes = api_settings.DEFAULT_RENDERER_CLASSES
neg = self.perform_content_negotiation(self.request, force=True)
self.request.accepted_renderer, self.request.accepted_media_type = neg
return super(SchemaView, self).handle_exception(exc)

View File

@ -393,7 +393,7 @@ class Serializer(BaseSerializer):
# Used by the lazily-evaluated `validators` property.
meta = getattr(self, 'Meta', None)
validators = getattr(meta, 'validators', None)
return validators[:] if validators else []
return list(validators) if validators else []
def get_initial(self):
if hasattr(self, 'initial_data'):
@ -461,8 +461,11 @@ class Serializer(BaseSerializer):
"""
Add read_only fields with defaults to value before running validators.
"""
to_validate = self._read_only_defaults()
to_validate.update(value)
if isinstance(value, dict):
to_validate = self._read_only_defaults()
to_validate.update(value)
else:
to_validate = value
super(Serializer, self).run_validators(to_validate)
def to_internal_value(self, data):
@ -1477,7 +1480,7 @@ class ModelSerializer(Serializer):
# If the validators have been declared explicitly then use that.
validators = getattr(getattr(self, 'Meta', None), 'validators', None)
if validators is not None:
return validators[:]
return list(validators)
# Otherwise use the default set of validators.
return (

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

View File

View File

View File

Before

Width:  |  Height:  |  Size: 197 KiB

After

Width:  |  Height:  |  Size: 197 KiB

View File

View File

File diff suppressed because one or more lines are too long

View File

@ -38,7 +38,7 @@ function sameOrigin(url) {
!(/^(\/\/|http:|https:).*/.test(url));
}
var csrftoken = getCookie(window.drf.csrfCookieName);
var csrftoken = window.drf.csrfToken;
$.ajaxSetup({
beforeSend: function(xhr, settings) {

View File

@ -247,7 +247,7 @@
<script>
window.drf = {
csrfHeaderName: "{{ csrf_header_name|default:'X-CSRFToken' }}",
csrfCookieName: "{{ csrf_cookie_name|default:'csrftoken' }}"
csrfToken: "{{ csrf_token }}"
};
</script>
<script src="{% static "rest_framework/js/jquery-3.3.1.min.js" %}"></script>

View File

@ -76,6 +76,8 @@
{% block content %}
<div class="region" aria-label="{% trans "request form" %}">
{% block request_forms %}
{% if 'GET' in allowed_methods %}
<form id="get-form" class="pull-right">
<fieldset>
@ -148,6 +150,8 @@
{% trans "Filters" %}
</button>
{% endif %}
{% endblock request_forms %}
</div>
<div class="content-main" role="main" aria-label="{% trans "main content" %}">
@ -171,10 +175,10 @@
</div>
<div class="response-info" aria-label="{% trans "response info" %}">
<pre class="prettyprint"><span class="meta nocode"><b>HTTP {{ response.status_code }} {{ response.status_text }}</b>{% autoescape off %}{% for key, val in response_headers|items %}
<pre class="prettyprint"><span class="meta nocode"><b>HTTP {{ response.status_code }} {{ response.status_text }}</b>{% for key, val in response_headers|items %}
<b>{{ key }}:</b> <span class="lit">{{ val|break_long_headers|urlize_quoted_links }}</span>{% endfor %}
</span>{{ content|urlize_quoted_links }}</pre>{% endautoescape %}
</span>{{ content|urlize_quoted_links }}</pre>
</div>
</div>
@ -286,7 +290,7 @@
<script>
window.drf = {
csrfHeaderName: "{{ csrf_header_name|default:'X-CSRFToken' }}",
csrfCookieName: "{{ csrf_cookie_name|default:'csrftoken' }}"
csrfToken: "{% if request %}{{ csrf_token }}{% endif %}"
};
</script>
<script src="{% static "rest_framework/js/jquery-3.3.1.min.js" %}"></script>

View File

@ -3,4 +3,4 @@
$ coreapi get {{ document.url }}{% if schema_format %} --format {{ schema_format }}{% endif %}
# Interact with the API endpoint
$ coreapi action {% if section_key %}{{ section_key }} {% endif %}{{ link_key }}{% for field in link.fields %} -p {{ field.name }}=...{% endfor %}{% endcode %}</code></pre>
$ coreapi action {% if section_key %}{{ section_key }} {% endif %}{{ link_key|cut:"> " }}{% for field in link.fields %} -p {{ field.name }}=...{% endfor %}{% endcode %}</code></pre>

View File

@ -336,6 +336,12 @@ def urlize_quoted_links(text, trim_url_limit=None, nofollow=True, autoescape=Tru
return limit is not None and (len(x) > limit and ('%s...' % x[:max(0, limit - 3)])) or x
safe_input = isinstance(text, SafeData)
# Unfortunately, Django built-in cannot be used here, because escaping
# is to be performed on words, which have been forcibly coerced to text
def conditional_escape(text):
return escape(text) if autoescape and not safe_input else text
words = word_split_re.split(force_text(text))
for i, word in enumerate(words):
if '.' in word or '@' in word or ':' in word:
@ -376,21 +382,15 @@ def urlize_quoted_links(text, trim_url_limit=None, nofollow=True, autoescape=Tru
# Make link.
if url:
trimmed = trim_url(middle)
if autoescape and not safe_input:
lead, trail = escape(lead), escape(trail)
url, trimmed = escape(url), escape(trimmed)
lead, trail = conditional_escape(lead), conditional_escape(trail)
url, trimmed = conditional_escape(url), conditional_escape(trimmed)
middle = '<a href="%s"%s>%s</a>' % (url, nofollow_attr, trimmed)
words[i] = mark_safe('%s%s%s' % (lead, middle, trail))
words[i] = '%s%s%s' % (lead, middle, trail)
else:
if safe_input:
words[i] = mark_safe(word)
elif autoescape:
words[i] = escape(word)
elif safe_input:
words[i] = mark_safe(word)
elif autoescape:
words[i] = escape(word)
return ''.join(words)
words[i] = conditional_escape(word)
else:
words[i] = conditional_escape(word)
return mark_safe(''.join(words))
@register.filter

View File

@ -106,8 +106,7 @@ def get_field_kwargs(field_name, model_field):
if model_field.null and not isinstance(model_field, models.NullBooleanField):
kwargs['allow_null'] = True
if model_field.blank and (isinstance(model_field, models.CharField) or
isinstance(model_field, models.TextField)):
if model_field.blank and (isinstance(model_field, (models.CharField, models.TextField))):
kwargs['allow_blank'] = True
if isinstance(model_field, models.FilePathField):
@ -193,9 +192,7 @@ def get_field_kwargs(field_name, model_field):
# Ensure that max_length is passed explicitly as a keyword arg,
# rather than as a validator.
max_length = getattr(model_field, 'max_length', None)
if max_length is not None and (isinstance(model_field, models.CharField) or
isinstance(model_field, models.TextField) or
isinstance(model_field, models.FileField)):
if max_length is not None and (isinstance(model_field, (models.CharField, models.TextField, models.FileField))):
kwargs['max_length'] = max_length
validator_kwarg = [
validator for validator in validator_kwarg
@ -249,6 +246,12 @@ def get_relation_kwargs(field_name, relation_info):
if to_field:
kwargs['to_field'] = to_field
limit_choices_to = model_field and model_field.get_limit_choices_to()
if limit_choices_to:
if not isinstance(limit_choices_to, models.Q):
limit_choices_to = models.Q(**limit_choices_to)
kwargs['queryset'] = kwargs['queryset'].filter(limit_choices_to)
if has_through_model:
kwargs['read_only'] = True
kwargs.pop('queryset', None)

View File

@ -1,11 +1,10 @@
from __future__ import unicode_literals
import collections
from collections import OrderedDict
from django.utils.encoding import force_text
from rest_framework.compat import unicode_to_repr
from rest_framework.compat import MutableMapping, unicode_to_repr
from rest_framework.utils import json
@ -130,7 +129,7 @@ class NestedBoundField(BoundField):
return self.__class__(self._field, values, self.errors, self._prefix)
class BindingDict(collections.MutableMapping):
class BindingDict(MutableMapping):
"""
This dict-like object is used to store fields on a serializer.

View File

@ -75,6 +75,9 @@ class URLPathVersioning(BaseVersioning):
def determine_version(self, request, *args, **kwargs):
version = kwargs.get(self.version_param, self.default_version)
if version is None:
version = self.default_version
if not self.is_allowed_version(version):
raise exceptions.NotFound(self.invalid_version_message)
return version

View File

@ -463,7 +463,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

View File

@ -61,6 +61,7 @@ setup(
'Framework :: Django :: 1.11',
'Framework :: Django :: 2.0',
'Framework :: Django :: 2.1',
'Framework :: Django :: 2.2',
'Intended Audience :: Developers',
'License :: OSI Approved :: BSD License',
'Operating System :: OS Independent',
@ -71,6 +72,7 @@ setup(
'Programming Language :: Python :: 3.4',
'Programming Language :: Python :: 3.5',
'Programming Language :: Python :: 3.6',
'Programming Language :: Python :: 3.7',
'Topic :: Internet :: WWW/HTTP',
]
)

View File

View File

@ -0,0 +1,24 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.CreateModel(
name='CustomToken',
fields=[
('key', models.CharField(max_length=40, primary_key=True, serialize=False)),
('user', models.OneToOneField(on_delete=models.CASCADE, to=settings.AUTH_USER_MODEL)),
],
),
]

View File

@ -0,0 +1,10 @@
# coding: utf-8
from __future__ import unicode_literals
from django.conf import settings
from django.db import models
class CustomToken(models.Model):
key = models.CharField(max_length=40, primary_key=True)
user = models.OneToOneField(settings.AUTH_USER_MODEL, on_delete=models.CASCADE)

View File

@ -8,7 +8,6 @@ import pytest
from django.conf import settings
from django.conf.urls import include, url
from django.contrib.auth.models import User
from django.db import models
from django.http import HttpResponse
from django.test import TestCase, override_settings
from django.utils import six
@ -26,14 +25,11 @@ from rest_framework.response import Response
from rest_framework.test import APIClient, APIRequestFactory
from rest_framework.views import APIView
from .models import CustomToken
factory = APIRequestFactory()
class CustomToken(models.Model):
key = models.CharField(max_length=40, primary_key=True)
user = models.OneToOneField(User, on_delete=models.CASCADE)
class CustomTokenAuthentication(TokenAuthentication):
model = CustomToken
@ -87,7 +83,7 @@ urlpatterns = [
]
@override_settings(ROOT_URLCONF='tests.test_authentication')
@override_settings(ROOT_URLCONF=__name__)
class BasicAuthTests(TestCase):
"""Basic authentication"""
def setUp(self):
@ -169,7 +165,7 @@ class BasicAuthTests(TestCase):
assert response.status_code == status.HTTP_401_UNAUTHORIZED
@override_settings(ROOT_URLCONF='tests.test_authentication')
@override_settings(ROOT_URLCONF=__name__)
class SessionAuthTests(TestCase):
"""User session authentication"""
def setUp(self):
@ -370,7 +366,7 @@ class BaseTokenAuthTests(object):
assert response.status_code == status.HTTP_401_UNAUTHORIZED
@override_settings(ROOT_URLCONF='tests.test_authentication')
@override_settings(ROOT_URLCONF=__name__)
class TokenAuthTests(BaseTokenAuthTests, TestCase):
model = Token
path = '/token/'
@ -429,13 +425,13 @@ class TokenAuthTests(BaseTokenAuthTests, TestCase):
assert response.data['token'] == self.key
@override_settings(ROOT_URLCONF='tests.test_authentication')
@override_settings(ROOT_URLCONF=__name__)
class CustomTokenAuthTests(BaseTokenAuthTests, TestCase):
model = CustomToken
path = '/customtoken/'
@override_settings(ROOT_URLCONF='tests.test_authentication')
@override_settings(ROOT_URLCONF=__name__)
class CustomKeywordTokenAuthTests(BaseTokenAuthTests, TestCase):
model = Token
path = '/customkeywordtoken/'
@ -549,7 +545,7 @@ class BasicAuthenticationUnitTests(TestCase):
authentication.authenticate = old_authenticate
@override_settings(ROOT_URLCONF='tests.test_authentication',
@override_settings(ROOT_URLCONF=__name__,
AUTHENTICATION_BACKENDS=('django.contrib.auth.backends.RemoteUserBackend',))
class RemoteUserAuthenticationUnitTests(TestCase):
def setUp(self):

View File

@ -56,6 +56,8 @@ def pytest_configure(config):
'django.contrib.staticfiles',
'rest_framework',
'rest_framework.authtoken',
'tests.authentication',
'tests.generic_relations',
'tests.importable',
'tests',
),

View File

View File

@ -0,0 +1,36 @@
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
('contenttypes', '0002_remove_content_type_name'),
]
operations = [
migrations.CreateModel(
name='Bookmark',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('url', models.URLField()),
],
),
migrations.CreateModel(
name='Note',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('text', models.TextField()),
],
),
migrations.CreateModel(
name='Tag',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('tag', models.SlugField()),
('object_id', models.PositiveIntegerField()),
('content_type', models.ForeignKey(on_delete=models.CASCADE, to='contenttypes.ContentType')),
],
),
]

View File

@ -0,0 +1,46 @@
from __future__ import unicode_literals
from django.contrib.contenttypes.fields import (
GenericForeignKey, GenericRelation
)
from django.contrib.contenttypes.models import ContentType
from django.db import models
from django.utils.encoding import python_2_unicode_compatible
@python_2_unicode_compatible
class Tag(models.Model):
"""
Tags have a descriptive slug, and are attached to an arbitrary object.
"""
tag = models.SlugField()
content_type = models.ForeignKey(ContentType, on_delete=models.CASCADE)
object_id = models.PositiveIntegerField()
tagged_item = GenericForeignKey('content_type', 'object_id')
def __str__(self):
return self.tag
@python_2_unicode_compatible
class Bookmark(models.Model):
"""
A URL bookmark that may have multiple tags attached.
"""
url = models.URLField()
tags = GenericRelation(Tag)
def __str__(self):
return 'Bookmark: %s' % self.url
@python_2_unicode_compatible
class Note(models.Model):
"""
A textual note that may have multiple tags attached.
"""
text = models.TextField()
tags = GenericRelation(Tag)
def __str__(self):
return 'Note: %s' % self.text

View File

@ -1,52 +1,10 @@
from __future__ import unicode_literals
from django.contrib.contenttypes.fields import (
GenericForeignKey, GenericRelation
)
from django.contrib.contenttypes.models import ContentType
from django.db import models
from django.test import TestCase
from django.utils.encoding import python_2_unicode_compatible
from rest_framework import serializers
@python_2_unicode_compatible
class Tag(models.Model):
"""
Tags have a descriptive slug, and are attached to an arbitrary object.
"""
tag = models.SlugField()
content_type = models.ForeignKey(ContentType, on_delete=models.CASCADE)
object_id = models.PositiveIntegerField()
tagged_item = GenericForeignKey('content_type', 'object_id')
def __str__(self):
return self.tag
@python_2_unicode_compatible
class Bookmark(models.Model):
"""
A URL bookmark that may have multiple tags attached.
"""
url = models.URLField()
tags = GenericRelation(Tag)
def __str__(self):
return 'Bookmark: %s' % self.url
@python_2_unicode_compatible
class Note(models.Model):
"""
A textual note that may have multiple tags attached.
"""
text = models.TextField()
tags = GenericRelation(Tag)
def __str__(self):
return 'Note: %s' % self.text
from .models import Bookmark, Note, Tag
class TestGenericRelations(TestCase):

View File

@ -52,6 +52,20 @@ class ForeignKeySource(RESTFrameworkModel):
on_delete=models.CASCADE)
class ForeignKeySourceWithLimitedChoices(RESTFrameworkModel):
target = models.ForeignKey(ForeignKeyTarget, help_text='Target',
verbose_name='Target',
limit_choices_to={"name__startswith": "limited-"},
on_delete=models.CASCADE)
class ForeignKeySourceWithQLimitedChoices(RESTFrameworkModel):
target = models.ForeignKey(ForeignKeyTarget, help_text='Target',
verbose_name='Target',
limit_choices_to=models.Q(name__startswith="limited-"),
on_delete=models.CASCADE)
# Nullable ForeignKey
class NullableForeignKeySource(RESTFrameworkModel):
name = models.CharField(max_length=100)

View File

@ -3,7 +3,7 @@ from __future__ import unicode_literals
import pytest
from django.test import TestCase
from rest_framework import status
from rest_framework import RemovedInDRF310Warning, status
from rest_framework.authentication import BasicAuthentication
from rest_framework.decorators import (
action, api_view, authentication_classes, detail_route, list_route,
@ -290,7 +290,7 @@ class ActionDecoratorTestCase(TestCase):
raise NotImplementedError
def test_detail_route_deprecation(self):
with pytest.warns(DeprecationWarning) as record:
with pytest.warns(RemovedInDRF310Warning) as record:
@detail_route()
def view(request):
raise NotImplementedError
@ -303,7 +303,7 @@ class ActionDecoratorTestCase(TestCase):
)
def test_list_route_deprecation(self):
with pytest.warns(DeprecationWarning) as record:
with pytest.warns(RemovedInDRF310Warning) as record:
@list_route()
def view(request):
raise NotImplementedError
@ -317,7 +317,7 @@ class ActionDecoratorTestCase(TestCase):
def test_route_url_name_from_path(self):
# pre-3.8 behavior was to base the `url_name` off of the `url_path`
with pytest.warns(DeprecationWarning):
with pytest.warns(RemovedInDRF310Warning):
@list_route(url_path='foo_bar')
def view(request):
raise NotImplementedError

View File

@ -740,6 +740,25 @@ class TestCharField(FieldValues):
'Null characters are not allowed.'
]
def test_iterable_validators(self):
"""
Ensure `validators` parameter is compatible with reasonable iterables.
"""
value = 'example'
for validators in ([], (), set()):
field = serializers.CharField(validators=validators)
field.run_validation(value)
def raise_exception(value):
raise exceptions.ValidationError('Raised error')
for validators in ([raise_exception], (raise_exception,), set([raise_exception])):
field = serializers.CharField(validators=validators)
with pytest.raises(serializers.ValidationError) as exc_info:
field.run_validation(value)
assert exc_info.value.detail == ['Raised error']
class TestEmailField(FieldValues):
"""

View File

@ -5,6 +5,7 @@ import datetime
import pytest
from django.core.exceptions import ImproperlyConfigured
from django.db import models
from django.db.models.functions import Concat, Upper
from django.test import TestCase
from django.test.utils import override_settings
from django.utils.six.moves import reload_module
@ -156,6 +157,31 @@ class SearchFilterTests(TestCase):
reload_module(filters)
def test_search_with_filter_subclass(self):
class CustomSearchFilter(filters.SearchFilter):
# Filter that dynamically changes search fields
def get_search_fields(self, view, request):
if request.query_params.get('title_only'):
return ('$title',)
return super(CustomSearchFilter, self).get_search_fields(view, request)
class SearchListView(generics.ListAPIView):
queryset = SearchFilterModel.objects.all()
serializer_class = SearchFilterSerializer
filter_backends = (CustomSearchFilter,)
search_fields = ('$title', '$text')
view = SearchListView.as_view()
request = factory.get('/', {'search': r'^\w{3}$'})
response = view(request)
assert len(response.data) == 10
request = factory.get('/', {'search': r'^\w{3}$', 'title_only': 'true'})
response = view(request)
assert response.data == [
{'id': 3, 'title': 'zzz', 'text': 'cde'}
]
class AttributeModel(models.Model):
label = models.CharField(max_length=32)
@ -221,7 +247,7 @@ class SearchFilterM2MTests(TestCase):
# ...
for idx in range(3):
label = 'w' * (idx + 1)
AttributeModel(label=label)
AttributeModel.objects.create(label=label)
for idx in range(10):
title = 'z' * (idx + 1)
@ -304,6 +330,38 @@ class SearchFilterToManyTests(TestCase):
assert len(response.data) == 1
class SearchFilterAnnotatedSerializer(serializers.ModelSerializer):
title_text = serializers.CharField()
class Meta:
model = SearchFilterModel
fields = ('title', 'text', 'title_text')
class SearchFilterAnnotatedFieldTests(TestCase):
@classmethod
def setUpTestData(cls):
SearchFilterModel.objects.create(title='abc', text='def')
SearchFilterModel.objects.create(title='ghi', text='jkl')
def test_search_in_annotated_field(self):
class SearchListView(generics.ListAPIView):
queryset = SearchFilterModel.objects.annotate(
title_text=Upper(
Concat(models.F('title'), models.F('text'))
)
).all()
serializer_class = SearchFilterAnnotatedSerializer
filter_backends = (filters.SearchFilter,)
search_fields = ('title_text',)
view = SearchListView.as_view()
request = factory.get('/', {'search': 'ABCDEF'})
response = view(request)
assert len(response.data) == 1
assert response.data[0]['title_text'] == 'ABCDEF'
class OrderingFilterModel(models.Model):
title = models.CharField(max_length=20, verbose_name='verbose title')
text = models.CharField(max_length=100)

View File

@ -0,0 +1,88 @@
from __future__ import unicode_literals
import pytest
from django.conf.urls import url
from django.core.management import call_command
from django.test import TestCase
from django.test.utils import override_settings
from django.utils import six
from rest_framework.compat import coreapi
from rest_framework.utils import formatting, json
from rest_framework.views import APIView
class FooView(APIView):
def get(self, request):
pass
urlpatterns = [
url(r'^$', FooView.as_view())
]
@override_settings(ROOT_URLCONF='tests.test_generateschema')
@pytest.mark.skipif(not coreapi, reason='coreapi is not installed')
class GenerateSchemaTests(TestCase):
"""Tests for management command generateschema."""
def setUp(self):
self.out = six.StringIO()
@pytest.mark.skipif(six.PY2, reason='PyYAML unicode output is malformed on PY2.')
def test_renders_default_schema_with_custom_title_url_and_description(self):
expected_out = """info:
description: Sample description
title: SampleAPI
version: ''
openapi: 3.0.0
paths:
/:
get:
operationId: list
servers:
- url: http://api.sample.com/
"""
call_command('generateschema',
'--title=SampleAPI',
'--url=http://api.sample.com',
'--description=Sample description',
stdout=self.out)
self.assertIn(formatting.dedent(expected_out), self.out.getvalue())
def test_renders_openapi_json_schema(self):
expected_out = {
"openapi": "3.0.0",
"info": {
"version": "",
"title": "",
"description": ""
},
"servers": [
{
"url": ""
}
],
"paths": {
"/": {
"get": {
"operationId": "list"
}
}
}
}
call_command('generateschema',
'--format=openapi-json',
stdout=self.out)
out_json = json.loads(self.out.getvalue())
self.assertDictEqual(out_json, expected_out)
def test_renders_corejson_schema(self):
expected_out = """{"_type":"document","":{"list":{"_type":"link","url":"/","action":"get"}}}"""
call_command('generateschema',
'--format=corejson',
stdout=self.out)
self.assertIn(expected_out, self.out.getvalue())

View File

@ -5,6 +5,7 @@ import pytest
from django.core.paginator import Paginator as DjangoPaginator
from django.db import models
from django.test import TestCase
from django.utils import six
from rest_framework import (
exceptions, filters, generics, pagination, serializers, status
@ -207,7 +208,7 @@ class TestPageNumberPagination:
]
}
assert self.pagination.display_page_controls
assert isinstance(self.pagination.to_html(), type(''))
assert isinstance(self.pagination.to_html(), six.text_type)
def test_second_page(self):
request = Request(factory.get('/', {'page': 2}))
@ -313,7 +314,7 @@ class TestPageNumberPaginationOverride:
]
}
assert not self.pagination.display_page_controls
assert isinstance(self.pagination.to_html(), type(''))
assert isinstance(self.pagination.to_html(), six.text_type)
def test_invalid_page(self):
request = Request(factory.get('/', {'page': 'invalid'}))
@ -368,7 +369,7 @@ class TestLimitOffset:
]
}
assert self.pagination.display_page_controls
assert isinstance(self.pagination.to_html(), type(''))
assert isinstance(self.pagination.to_html(), six.text_type)
def test_pagination_not_applied_if_limit_or_default_limit_not_set(self):
class MockPagination(pagination.LimitOffsetPagination):
@ -631,7 +632,7 @@ class CursorPaginationTestsMixin:
assert current == [1, 1, 1, 1, 1]
assert next == [1, 2, 3, 4, 4]
assert isinstance(self.pagination.to_html(), type(''))
assert isinstance(self.pagination.to_html(), six.text_type)
def test_cursor_pagination_with_page_size(self):
(previous, current, next, previous_url, next_url) = self.get_pages('/?page_size=20')

View File

@ -5,16 +5,17 @@ import unittest
import warnings
import django
import pytest
from django.contrib.auth.models import AnonymousUser, Group, Permission, User
from django.db import models
from django.test import TestCase
from django.urls import ResolverMatch
from rest_framework import (
HTTP_HEADER_ENCODING, authentication, generics, permissions, serializers,
status, views
HTTP_HEADER_ENCODING, RemovedInDRF310Warning, authentication, generics,
permissions, serializers, status, views
)
from rest_framework.compat import is_guardian_installed
from rest_framework.compat import PY36, is_guardian_installed, mock
from rest_framework.filters import DjangoObjectPermissionsFilter
from rest_framework.routers import DefaultRouter
from rest_framework.test import APIRequestFactory
@ -426,7 +427,7 @@ class ObjectPermissionsIntegrationTests(TestCase):
message = ("`DjangoObjectPermissionsFilter` has been deprecated and moved "
"to the 3rd-party django-rest-framework-guardian package.")
self.assertEqual(len(w), 1)
self.assertIs(w[-1].category, DeprecationWarning)
self.assertIs(w[-1].category, RemovedInDRF310Warning)
self.assertEqual(str(w[-1].message), message)
def test_can_read_list_permissions(self):
@ -579,7 +580,19 @@ class PermissionsCompositionTests(TestCase):
composed_perm = permissions.IsAuthenticated | permissions.AllowAny
assert composed_perm().has_permission(request, None) is True
def test_several_levels(self):
def test_not_false(self):
request = factory.get('/1', format='json')
request.user = AnonymousUser()
composed_perm = ~permissions.IsAuthenticated
assert composed_perm().has_permission(request, None) is True
def test_not_true(self):
request = factory.get('/1', format='json')
request.user = self.user
composed_perm = ~permissions.AllowAny
assert composed_perm().has_permission(request, None) is False
def test_several_levels_without_negation(self):
request = factory.get('/1', format='json')
request.user = self.user
composed_perm = (
@ -590,6 +603,17 @@ class PermissionsCompositionTests(TestCase):
)
assert composed_perm().has_permission(request, None) is True
def test_several_levels_and_precedence_with_negation(self):
request = factory.get('/1', format='json')
request.user = self.user
composed_perm = (
permissions.IsAuthenticated &
~ permissions.IsAdminUser &
permissions.IsAuthenticated &
~(permissions.IsAdminUser & permissions.IsAdminUser)
)
assert composed_perm().has_permission(request, None) is True
def test_several_levels_and_precedence(self):
request = factory.get('/1', format='json')
request.user = self.user
@ -600,3 +624,87 @@ class PermissionsCompositionTests(TestCase):
permissions.IsAuthenticated
)
assert composed_perm().has_permission(request, None) is True
@pytest.mark.skipif(not PY36, reason="assert_called_once() not available")
def test_or_lazyness(self):
request = factory.get('/1', format='json')
request.user = AnonymousUser()
with mock.patch.object(permissions.AllowAny, 'has_permission', return_value=True) as mock_allow:
with mock.patch.object(permissions.IsAuthenticated, 'has_permission', return_value=False) as mock_deny:
composed_perm = (permissions.AllowAny | permissions.IsAuthenticated)
hasperm = composed_perm().has_permission(request, None)
self.assertIs(hasperm, True)
mock_allow.assert_called_once()
mock_deny.assert_not_called()
with mock.patch.object(permissions.AllowAny, 'has_permission', return_value=True) as mock_allow:
with mock.patch.object(permissions.IsAuthenticated, 'has_permission', return_value=False) as mock_deny:
composed_perm = (permissions.IsAuthenticated | permissions.AllowAny)
hasperm = composed_perm().has_permission(request, None)
self.assertIs(hasperm, True)
mock_deny.assert_called_once()
mock_allow.assert_called_once()
@pytest.mark.skipif(not PY36, reason="assert_called_once() not available")
def test_object_or_lazyness(self):
request = factory.get('/1', format='json')
request.user = AnonymousUser()
with mock.patch.object(permissions.AllowAny, 'has_object_permission', return_value=True) as mock_allow:
with mock.patch.object(permissions.IsAuthenticated, 'has_object_permission', return_value=False) as mock_deny:
composed_perm = (permissions.AllowAny | permissions.IsAuthenticated)
hasperm = composed_perm().has_object_permission(request, None, None)
self.assertIs(hasperm, True)
mock_allow.assert_called_once()
mock_deny.assert_not_called()
with mock.patch.object(permissions.AllowAny, 'has_object_permission', return_value=True) as mock_allow:
with mock.patch.object(permissions.IsAuthenticated, 'has_object_permission', return_value=False) as mock_deny:
composed_perm = (permissions.IsAuthenticated | permissions.AllowAny)
hasperm = composed_perm().has_object_permission(request, None, None)
self.assertIs(hasperm, True)
mock_deny.assert_called_once()
mock_allow.assert_called_once()
@pytest.mark.skipif(not PY36, reason="assert_called_once() not available")
def test_and_lazyness(self):
request = factory.get('/1', format='json')
request.user = AnonymousUser()
with mock.patch.object(permissions.AllowAny, 'has_permission', return_value=True) as mock_allow:
with mock.patch.object(permissions.IsAuthenticated, 'has_permission', return_value=False) as mock_deny:
composed_perm = (permissions.AllowAny & permissions.IsAuthenticated)
hasperm = composed_perm().has_permission(request, None)
self.assertIs(hasperm, False)
mock_allow.assert_called_once()
mock_deny.assert_called_once()
with mock.patch.object(permissions.AllowAny, 'has_permission', return_value=True) as mock_allow:
with mock.patch.object(permissions.IsAuthenticated, 'has_permission', return_value=False) as mock_deny:
composed_perm = (permissions.IsAuthenticated & permissions.AllowAny)
hasperm = composed_perm().has_permission(request, None)
self.assertIs(hasperm, False)
mock_allow.assert_not_called()
mock_deny.assert_called_once()
@pytest.mark.skipif(not PY36, reason="assert_called_once() not available")
def test_object_and_lazyness(self):
request = factory.get('/1', format='json')
request.user = AnonymousUser()
with mock.patch.object(permissions.AllowAny, 'has_object_permission', return_value=True) as mock_allow:
with mock.patch.object(permissions.IsAuthenticated, 'has_object_permission', return_value=False) as mock_deny:
composed_perm = (permissions.AllowAny & permissions.IsAuthenticated)
hasperm = composed_perm().has_object_permission(request, None, None)
self.assertIs(hasperm, False)
mock_allow.assert_called_once()
mock_deny.assert_called_once()
with mock.patch.object(permissions.AllowAny, 'has_object_permission', return_value=True) as mock_allow:
with mock.patch.object(permissions.IsAuthenticated, 'has_object_permission', return_value=False) as mock_deny:
composed_perm = (permissions.IsAuthenticated & permissions.AllowAny)
hasperm = composed_perm().has_object_permission(request, None, None)
self.assertIs(hasperm, False)
mock_allow.assert_not_called()
mock_deny.assert_called_once()

View File

@ -5,8 +5,9 @@ from django.utils import six
from rest_framework import serializers
from tests.models import (
ForeignKeySource, ForeignKeyTarget, ManyToManySource, ManyToManyTarget,
NullableForeignKeySource, NullableOneToOneSource,
ForeignKeySource, ForeignKeySourceWithLimitedChoices,
ForeignKeySourceWithQLimitedChoices, ForeignKeyTarget, ManyToManySource,
ManyToManyTarget, NullableForeignKeySource, NullableOneToOneSource,
NullableUUIDForeignKeySource, OneToOnePKSource, OneToOneTarget,
UUIDForeignKeyTarget
)
@ -38,6 +39,12 @@ class ForeignKeySourceSerializer(serializers.ModelSerializer):
fields = ('id', 'name', 'target')
class ForeignKeySourceWithLimitedChoicesSerializer(serializers.ModelSerializer):
class Meta:
model = ForeignKeySourceWithLimitedChoices
fields = ("id", "target")
# Nullable ForeignKey
class NullableForeignKeySourceSerializer(serializers.ModelSerializer):
class Meta:
@ -360,6 +367,30 @@ class PKForeignKeyTests(TestCase):
serializer.is_valid(raise_exception=True)
assert 'target' not in serializer.validated_data
def test_queryset_size_without_limited_choices(self):
limited_target = ForeignKeyTarget(name="limited-target")
limited_target.save()
queryset = ForeignKeySourceSerializer().fields["target"].get_queryset()
assert len(queryset) == 3
def test_queryset_size_with_limited_choices(self):
limited_target = ForeignKeyTarget(name="limited-target")
limited_target.save()
queryset = ForeignKeySourceWithLimitedChoicesSerializer().fields["target"].get_queryset()
assert len(queryset) == 1
def test_queryset_size_with_Q_limited_choices(self):
limited_target = ForeignKeyTarget(name="limited-target")
limited_target.save()
class QLimitedChoicesSerializer(serializers.ModelSerializer):
class Meta:
model = ForeignKeySourceWithQLimitedChoices
fields = ("id", "target")
queryset = QLimitedChoicesSerializer().fields["target"].get_queryset()
assert len(queryset) == 1
class PKNullableForeignKeyTests(TestCase):
def setUp(self):

View File

@ -2,20 +2,21 @@
from __future__ import unicode_literals
import re
from collections import MutableMapping, OrderedDict
from collections import OrderedDict
import pytest
from django.conf.urls import include, url
from django.core.cache import cache
from django.db import models
from django.http.request import HttpRequest
from django.template import loader
from django.test import TestCase, override_settings
from django.utils import six
from django.utils.safestring import SafeText
from django.utils.translation import ugettext_lazy as _
from rest_framework import permissions, serializers, status
from rest_framework.compat import coreapi
from rest_framework.compat import MutableMapping, coreapi
from rest_framework.decorators import action
from rest_framework.renderers import (
AdminRenderer, BaseRenderer, BrowsableAPIRenderer, DocumentationRenderer,
@ -635,7 +636,9 @@ class BrowsableAPIRendererTests(URLPatternsTestCase):
raise NotImplementedError
router = SimpleRouter()
router.register(r'examples/', ExampleViewSet, basename='example')
router.register('examples', ExampleViewSet, basename='example')
urlpatterns = [url(r'^api/', include(router.urls))]
def setUp(self):
@ -827,6 +830,16 @@ class TestDocumentationRenderer(TestCase):
html = renderer.render(document, accepted_media_type="text/html", renderer_context={"request": request})
assert '<h1>Data Endpoint API</h1>' in html
def test_shell_code_example_rendering(self):
template = loader.get_template('rest_framework/docs/langs/shell.html')
context = {
'document': coreapi.Document(url='https://api.example.org/'),
'link_key': 'testcases > list',
'link': coreapi.Link(url='/data/', action='get', fields=[]),
}
html = template.render(context)
assert 'testcases list' in html
@pytest.mark.skipif(not coreapi, reason='coreapi is not installed')
class TestSchemaJSRenderer(TestCase):

View File

@ -10,7 +10,9 @@ from django.db import models
from django.test import TestCase, override_settings
from django.urls import resolve, reverse
from rest_framework import permissions, serializers, viewsets
from rest_framework import (
RemovedInDRF311Warning, permissions, serializers, viewsets
)
from rest_framework.compat import get_regex_pattern
from rest_framework.decorators import action
from rest_framework.response import Response
@ -119,6 +121,7 @@ class BasicViewSet(viewsets.ViewSet):
class TestSimpleRouter(URLPatternsTestCase, TestCase):
router = SimpleRouter()
router.register(r'basics/', BasicViewSet, basename='basic')
urlpatterns = [
@ -158,6 +161,12 @@ class TestSimpleRouter(URLPatternsTestCase, TestCase):
response = self.client.delete(reverse('basic-action3', args=[1]))
assert response.data == {'delete': '1'}
def test_register_after_accessing_urls(self):
self.router.register(r'notes', NoteViewSet)
assert len(self.router.urls) == 2 # list and detail
self.router.register(r'notes_bis', NoteViewSet)
assert len(self.router.urls) == 4
class TestRootView(URLPatternsTestCase, TestCase):
urlpatterns = [
@ -502,7 +511,7 @@ class TestBaseNameRename(TestCase):
def test_base_name_argument_deprecation(self):
router = SimpleRouter()
with pytest.warns(PendingDeprecationWarning) as w:
with pytest.warns(RemovedInDRF311Warning) as w:
warnings.simplefilter('always')
router.register('mock', MockViewSet, base_name='mock')
@ -529,7 +538,7 @@ class TestBaseNameRename(TestCase):
msg = "`CustomRouter.get_default_base_name` method should be renamed `get_default_basename`."
# Class definition should raise a warning
with pytest.warns(PendingDeprecationWarning) as w:
with pytest.warns(RemovedInDRF311Warning) as w:
warnings.simplefilter('always')
class CustomRouter(SimpleRouter):

View File

@ -24,7 +24,7 @@ from rest_framework.utils import formatting
from rest_framework.views import APIView
from rest_framework.viewsets import GenericViewSet, ModelViewSet
from .models import BasicModel, ForeignKeySource
from .models import BasicModel, ForeignKeySource, ManyToManySource
factory = APIRequestFactory()
@ -701,6 +701,51 @@ class TestSchemaGeneratorWithForeignKey(TestCase):
assert schema == expected
class ManyToManySourceSerializer(serializers.ModelSerializer):
class Meta:
model = ManyToManySource
fields = ('id', 'name', 'targets')
class ManyToManySourceView(generics.CreateAPIView):
queryset = ManyToManySource.objects.all()
serializer_class = ManyToManySourceSerializer
@unittest.skipUnless(coreapi, 'coreapi is not installed')
class TestSchemaGeneratorWithManyToMany(TestCase):
def setUp(self):
self.patterns = [
url(r'^example/?$', ManyToManySourceView.as_view()),
]
def test_schema_for_regular_views(self):
"""
Ensure that AutoField many to many fields are output as Integer.
"""
generator = SchemaGenerator(title='Example API', patterns=self.patterns)
schema = generator.get_schema()
expected = coreapi.Document(
url='',
title='Example API',
content={
'example': {
'create': coreapi.Link(
url='/example/',
action='post',
encoding='application/json',
fields=[
coreapi.Field('name', required=True, location='form', schema=coreschema.String(title='Name')),
coreapi.Field('targets', required=True, location='form', schema=coreschema.Array(title='Targets', items=coreschema.Integer())),
]
)
}
}
)
assert schema == expected
@unittest.skipUnless(coreapi, 'coreapi is not installed')
class Test4605Regression(TestCase):
def test_4605_regression(self):
@ -1304,3 +1349,13 @@ class TestAutoSchemaAllowsFilters(object):
def test_FOO(self):
assert not self._test('FOO')
@pytest.mark.skipif(not coreapi, reason='coreapi is not installed')
def test_schema_handles_exception():
schema_view = get_schema_view(permission_classes=[DenyAllUsingPermissionDenied])
request = factory.get('/')
response = schema_view(request)
response.render()
assert response.status_code == 403
assert "You do not have permission to perform this action." in str(response.content)

View File

@ -5,13 +5,12 @@ import inspect
import pickle
import re
import unittest
from collections import Mapping
import pytest
from django.db import models
from rest_framework import fields, relations, serializers
from rest_framework.compat import unicode_repr
from rest_framework import exceptions, fields, relations, serializers
from rest_framework.compat import Mapping, unicode_repr
from rest_framework.fields import Field
from .models import (
@ -156,6 +155,65 @@ class TestSerializer:
assert serializer.validated_data == {'char': 'abc', 'integer': 123}
assert serializer.errors == {}
def test_custom_to_internal_value(self):
"""
to_internal_value() is expected to return a dict, but subclasses may
return application specific type.
"""
class Point(object):
def __init__(self, srid, x, y):
self.srid = srid
self.coords = (x, y)
# Declares a serializer that converts data into an object
class NestedPointSerializer(serializers.Serializer):
longitude = serializers.FloatField(source='x')
latitude = serializers.FloatField(source='y')
def to_internal_value(self, data):
kwargs = super(NestedPointSerializer, self).to_internal_value(data)
return Point(srid=4326, **kwargs)
serializer = NestedPointSerializer(data={'longitude': 6.958307, 'latitude': 50.941357})
assert serializer.is_valid()
assert isinstance(serializer.validated_data, Point)
assert serializer.validated_data.srid == 4326
assert serializer.validated_data.coords[0] == 6.958307
assert serializer.validated_data.coords[1] == 50.941357
assert serializer.errors == {}
def test_iterable_validators(self):
"""
Ensure `validators` parameter is compatible with reasonable iterables.
"""
data = {'char': 'abc', 'integer': 123}
for validators in ([], (), set()):
class ExampleSerializer(serializers.Serializer):
char = serializers.CharField(validators=validators)
integer = serializers.IntegerField()
serializer = ExampleSerializer(data=data)
assert serializer.is_valid()
assert serializer.validated_data == data
assert serializer.errors == {}
def raise_exception(value):
raise exceptions.ValidationError('Raised error')
for validators in ([raise_exception], (raise_exception,), set([raise_exception])):
class ExampleSerializer(serializers.Serializer):
char = serializers.CharField(validators=validators)
integer = serializers.IntegerField()
serializer = ExampleSerializer(data=data)
assert not serializer.is_valid()
assert serializer.data == data
assert serializer.validated_data == {}
assert serializer.errors == {'char': [
exceptions.ErrorDetail(string='Raised error', code='invalid')
]}
class TestValidateMethod:
def test_non_field_error_validate_method(self):

View File

@ -1,7 +1,17 @@
import re
from django.shortcuts import render
def test_base_template_with_context():
context = {'request': True, 'csrf_token': 'TOKEN'}
result = render({}, 'rest_framework/base.html', context=context)
assert re.search(r'\bcsrfToken: "TOKEN"', result.content.decode('utf-8'))
def test_base_template_with_no_context():
# base.html should be renderable with no context,
# so it can be easily extended.
render({}, 'rest_framework/base.html')
result = render({}, 'rest_framework/base.html')
# note that this response will not include a valid CSRF token
assert re.search(r'\bcsrfToken: ""', result.content.decode('utf-8'))

View File

@ -305,6 +305,15 @@ class URLizerTests(TestCase):
'&quot;foo_set&quot;: [\n &quot;<a href="http://api/foos/1/">http://api/foos/1/</a>&quot;\n], '
self._urlize_dict_check(data)
def test_template_render_with_autoescape(self):
"""
Test that HTML is correctly escaped in Browsable API views.
"""
template = Template("{% load rest_framework %}{{ content|urlize_quoted_links }}")
rendered = template.render(Context({'content': '<script>alert()</script> http://example.com'}))
assert rendered == '&lt;script&gt;alert()&lt;/script&gt;' \
' <a href="http://example.com" rel="nofollow">http://example.com</a>'
def test_template_render_with_noautoescape(self):
"""
Test if the autoescape value is getting passed to urlize_quoted_links filter.
@ -312,8 +321,8 @@ class URLizerTests(TestCase):
template = Template("{% load rest_framework %}"
"{% autoescape off %}{{ content|urlize_quoted_links }}"
"{% endautoescape %}")
rendered = template.render(Context({'content': '"http://example.com"'}))
assert rendered == '"<a href="http://example.com" rel="nofollow">http://example.com</a>"'
rendered = template.render(Context({'content': '<b> "http://example.com" </b>'}))
assert rendered == '<b> "<a href="http://example.com" rel="nofollow">http://example.com</a>" </b>'
@unittest.skipUnless(coreapi, 'coreapi is not installed')

Some files were not shown because too many files have changed in this diff Show More