mirror of
https://github.com/encode/django-rest-framework.git
synced 2025-07-30 18:09:59 +03:00
fixed merge conflicts
This commit is contained in:
commit
7e0d41608c
17
.travis.yml
17
.travis.yml
|
@ -1,8 +1,6 @@
|
|||
language: python
|
||||
cache: pip
|
||||
|
||||
sudo: false
|
||||
|
||||
dist: xenial
|
||||
matrix:
|
||||
fast_finish: true
|
||||
include:
|
||||
|
@ -13,22 +11,25 @@ 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, dist: xenial, sudo: true }
|
||||
- { python: "3.7", env: DJANGO=2.1, dist: xenial, sudo: true }
|
||||
- { python: "3.7", env: DJANGO=master, dist: xenial, sudo: true }
|
||||
- { 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.6", env: TOXENV=base }
|
||||
- { python: "3.6", env: TOXENV=lint }
|
||||
- { python: "3.6", env: TOXENV=docs }
|
||||
|
||||
- python: "3.6"
|
||||
|
||||
- python: "3.7"
|
||||
env: TOXENV=dist
|
||||
script:
|
||||
- python setup.py bdist_wheel
|
||||
|
|
|
@ -20,7 +20,7 @@ When answering questions make sure to help future contributors find their way ar
|
|||
|
||||
Please keep the tone polite & professional. For some users a discussion on the REST framework mailing list or ticket tracker may be their first engagement with the open source community. First impressions count, so let's try to make everyone feel welcome.
|
||||
|
||||
Be mindful in the language you choose. As an example, in an environment that is heavily male-dominated, posts that start 'Hey guys,' can come across as unintentionally exclusive. It's just as easy, and more inclusive to use gender neutral language in those situations.
|
||||
Be mindful in the language you choose. As an example, in an environment that is heavily male-dominated, posts that start 'Hey guys,' can come across as unintentionally exclusive. It's just as easy, and more inclusive to use gender neutral language in those situations. (e.g. 'Hey folks,')
|
||||
|
||||
The [Django code of conduct][code-of-conduct] gives a fuller set of guidelines for participating in community forums.
|
||||
|
||||
|
@ -50,7 +50,7 @@ Getting involved in triaging incoming issues is a good way to start contributing
|
|||
|
||||
To start developing on Django REST framework, clone the repo:
|
||||
|
||||
git clone git@github.com:encode/django-rest-framework.git
|
||||
git clone https://github.com/encode/django-rest-framework
|
||||
|
||||
Changes should broadly follow the [PEP 8][pep-8] style conventions, and we recommend you set up your editor to automatically indicate non-conforming styles.
|
||||
|
||||
|
|
|
@ -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.)
|
||||
|
||||
|
|
11
README.md
11
README.md
|
@ -27,8 +27,9 @@ The initial aim is to provide a single full-time position on REST framework.
|
|||
[![][load-impact-img]][load-impact-url]
|
||||
[![][kloudless-img]][kloudless-url]
|
||||
[![][auklet-img]][auklet-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], and [Auklet][auklet-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].
|
||||
|
||||
---
|
||||
|
||||
|
@ -55,7 +56,11 @@ There is a live example API for testing purposes, [available here][sandbox].
|
|||
# Requirements
|
||||
|
||||
* Python (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.
|
||||
|
||||
# Installation
|
||||
|
||||
|
@ -198,6 +203,7 @@ Send a description of the issue via email to [rest-framework-security@googlegrou
|
|||
[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
|
||||
[lightson-img]: https://raw.githubusercontent.com/encode/django-rest-framework/master/docs/img/premium/lightson-readme.png
|
||||
|
||||
[rover-url]: http://jobs.rover.com/
|
||||
[sentry-url]: https://getsentry.com/welcome/
|
||||
|
@ -207,6 +213,7 @@ Send a description of the issue via email to [rest-framework-security@googlegrou
|
|||
[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/
|
||||
[lightson-url]: https://lightsonsoftware.com
|
||||
|
||||
[oauth1-section]: https://www.django-rest-framework.org/api-guide/authentication/#django-rest-framework-oauth
|
||||
[oauth2-section]: https://www.django-rest-framework.org/api-guide/authentication/#django-oauth-toolkit
|
||||
|
|
|
@ -137,7 +137,7 @@ You'll also need to create tokens for your users.
|
|||
from rest_framework.authtoken.models import Token
|
||||
|
||||
token = Token.objects.create(user=...)
|
||||
print token.key
|
||||
print(token.key)
|
||||
|
||||
For clients to authenticate, the token key should be included in the `Authorization` HTTP header. The key should be prefixed by the string literal "Token", with whitespace separating the two strings. For example:
|
||||
|
||||
|
@ -391,10 +391,6 @@ Install the package using `pip`.
|
|||
|
||||
For details on configuration and usage see the Django REST framework OAuth documentation for [authentication][django-rest-framework-oauth-authentication] and [permissions][django-rest-framework-oauth-permissions].
|
||||
|
||||
## Digest Authentication
|
||||
|
||||
HTTP digest authentication is a widely implemented scheme that was intended to replace HTTP basic authentication, and which provides a simple encrypted authentication mechanism. [Juan Riaza][juanriaza] maintains the [djangorestframework-digestauth][djangorestframework-digestauth] package which provides HTTP digest authentication support for REST framework.
|
||||
|
||||
## JSON Web Token Authentication
|
||||
|
||||
JSON Web Token is a fairly new standard which can be used for token-based authentication. Unlike the built-in TokenAuthentication scheme, JWT Authentication doesn't need to use a database to validate a token. A package for JWT authentication is [djangorestframework-simplejwt][djangorestframework-simplejwt] which provides some features as well as a pluggable token blacklist app.
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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`.
|
||||
|
||||
|
|
|
@ -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:
|
||||
|
||||
|
@ -188,7 +188,7 @@ When in use, the browsable API will include a `SearchFilter` control:
|
|||
The `SearchFilter` class will only be applied if the view has a `search_fields` attribute set. The `search_fields` attribute should be a list of names of text type fields on the model, such as `CharField` or `TextField`.
|
||||
|
||||
from rest_framework import filters
|
||||
|
||||
|
||||
class UserListView(generics.ListAPIView):
|
||||
queryset = User.objects.all()
|
||||
serializer_class = UserSerializer
|
||||
|
@ -218,6 +218,13 @@ 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:
|
||||
|
||||
class CustomSearchFilter(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 +305,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 +321,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,)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -102,7 +102,7 @@ If it is called without a `filename` URL keyword argument, then the client must
|
|||
|
||||
##### Notes:
|
||||
|
||||
* The `FileUploadParser` is for usage with native clients that can upload the file as a raw data request. For web-based uploads, or for native clients with multipart upload support, you should use the `MultiPartParser` parser instead.
|
||||
* The `FileUploadParser` is for usage with native clients that can upload the file as a raw data request. For web-based uploads, or for native clients with multipart upload support, you should use the `MultiPartParser` instead.
|
||||
* Since this parser's `media_type` matches any content type, `FileUploadParser` should generally be the only parser set on an API view.
|
||||
* `FileUploadParser` respects Django's standard `FILE_UPLOAD_HANDLERS` setting, and the `request.upload_handlers` attribute. See the [Django documentation][upload-handlers] for more details.
|
||||
|
||||
|
|
|
@ -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.permssions` **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.
|
||||
|
@ -104,7 +117,7 @@ __Note:__ when you set new permission classes through class attribute or decorat
|
|||
|
||||
Provided they inherit from `rest_framework.permissions.BasePermission`, permissions can be composed using standard Python bitwise operators. For example, `IsAuthenticatedOrReadOnly` could be written:
|
||||
|
||||
from rest_framework.permissions import BasePermission, IsAuthenticated
|
||||
from rest_framework.permissions import BasePermission, IsAuthenticated, SAFE_METHODS
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.views import APIView
|
||||
|
||||
|
@ -113,7 +126,7 @@ Provided they inherit from `rest_framework.permissions.BasePermission`, permissi
|
|||
return request.method in SAFE_METHODS
|
||||
|
||||
class ExampleView(APIView):
|
||||
permission_classes = (IsAuthenticated|ReadOnly)
|
||||
permission_classes = (IsAuthenticated|ReadOnly,)
|
||||
|
||||
def get(self, request, format=None):
|
||||
content = {
|
||||
|
@ -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).
|
||||
|
||||
---
|
||||
|
||||
|
@ -274,7 +287,7 @@ The [Composed Permissions][composed-permissions] package provides a simple way t
|
|||
|
||||
## REST Condition
|
||||
|
||||
The [REST Condition][rest-condition] package is another extension for building complex permissions in a simple and convenient way. The extension allows you to combine permissions with logical operators.
|
||||
The [REST Condition][rest-condition] package is another extension for building complex permissions in a simple and convenient way. The extension allows you to combine permissions with logical operators.
|
||||
|
||||
## DRY Rest Permissions
|
||||
|
||||
|
@ -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
|
||||
|
|
|
@ -2,11 +2,9 @@ source: relations.py
|
|||
|
||||
# Serializer relations
|
||||
|
||||
> Bad programmers worry about the code.
|
||||
> Good programmers worry about data structures and their relationships.
|
||||
> Data structures, not algorithms, are central to programming.
|
||||
>
|
||||
> — [Linus Torvalds][cite]
|
||||
|
||||
> — [Rob Pike][cite]
|
||||
|
||||
Relational fields are used to represent model relationships. They can be applied to `ForeignKey`, `ManyToManyField` and `OneToOneField` relationships, as well as to reverse relationships, and custom relationships such as `GenericForeignKey`.
|
||||
|
||||
|
@ -24,7 +22,7 @@ To do so, open the Django shell, using `python manage.py shell`, then import the
|
|||
|
||||
>>> from myapp.serializers import AccountSerializer
|
||||
>>> serializer = AccountSerializer()
|
||||
>>> print repr(serializer) # Or `print(repr(serializer))` in Python 3.x.
|
||||
>>> print(repr(serializer))
|
||||
AccountSerializer():
|
||||
id = IntegerField(label='ID', read_only=True)
|
||||
name = CharField(allow_blank=True, max_length=100, required=False)
|
||||
|
@ -48,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.
|
||||
|
||||
|
@ -512,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:
|
||||
|
@ -592,7 +590,7 @@ The [drf-nested-routers package][drf-nested-routers] provides routers and relati
|
|||
|
||||
The [rest-framework-generic-relations][drf-nested-relations] library provides read/write serialization for generic foreign keys.
|
||||
|
||||
[cite]: https://lwn.net/Articles/193245/
|
||||
[cite]: http://users.ece.utexas.edu/~adnan/pike.html
|
||||
[reverse-relationships]: https://docs.djangoproject.com/en/stable/topics/db/queries/#following-relationships-backward
|
||||
[routers]: https://www.django-rest-framework.org/api-guide/routers#defaultrouter
|
||||
[generic-relations]: https://docs.djangoproject.com/en/stable/ref/contrib/contenttypes/#id1
|
||||
|
|
|
@ -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`
|
||||
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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/
|
||||
|
|
|
@ -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.
|
||||
|
||||
|
@ -208,7 +208,7 @@ To do any other validation that requires access to multiple fields, add a method
|
|||
|
||||
def validate(self, data):
|
||||
"""
|
||||
Check that the start is before the stop.
|
||||
Check that start is before finish.
|
||||
"""
|
||||
if data['start'] > data['finish']:
|
||||
raise serializers.ValidationError("finish must occur after start")
|
||||
|
@ -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
|
||||
|
|
|
@ -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.
|
||||
|
||||
|
|
|
@ -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.
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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:
|
||||
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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).*
|
||||
|
||||
---
|
||||
|
||||
|
|
|
@ -32,14 +32,14 @@ we strongly encourage you to invest in its continued development by
|
|||
**[signing up for a paid 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).*
|
||||
|
||||
---
|
||||
|
||||
|
|
|
@ -39,16 +39,16 @@ we strongly encourage you to invest in its continued development by
|
|||
**[signing up for a paid 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/).*
|
||||
|
||||
---
|
||||
|
||||
|
|
|
@ -33,15 +33,15 @@ If you use REST framework commercially and would like to see this work continue,
|
|||
**[signing up for a paid 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).*
|
||||
|
||||
---
|
||||
|
||||
|
|
|
@ -30,7 +30,7 @@ If you use REST framework commercially and would like to see this work continue,
|
|||
**[signing up for a paid 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).*
|
||||
|
||||
---
|
||||
|
||||
|
|
|
@ -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).*
|
||||
|
||||
---
|
||||
|
||||
|
@ -168,7 +168,7 @@ Both `APIView.exclude_from_schema` and the `exclude_from_schema` argument to the
|
|||
|
||||
For `APIView` you should instead set a `schema = None` attribute on the view class.
|
||||
|
||||
For function based views the `@schema` decorator can be used to exclude the view from the schema, by using `@schema(None)`.
|
||||
For function-based views the `@schema` decorator can be used to exclude the view from the schema, by using `@schema(None)`.
|
||||
|
||||
---
|
||||
|
||||
|
@ -179,7 +179,7 @@ There are a large number of minor fixes and improvements in this release. See th
|
|||
|
||||
## What's next
|
||||
|
||||
We're planning to iteratively working towards OpenAPI becoming the standard schema
|
||||
We're planning to iteratively work towards OpenAPI becoming the standard schema
|
||||
representation. This will mean that the `coreapi` dependency will gradually become
|
||||
removed, and we'll instead generate the schema directly, rather than building
|
||||
a CoreAPI `Document` object.
|
||||
|
@ -200,7 +200,7 @@ with the possibility that some of this work will eventually [feed back into
|
|||
Django](https://www.aeracode.org/2018/06/04/django-async-roadmap/).
|
||||
|
||||
There will be further work on the [Uvicorn](https://www.uvicorn.org/)
|
||||
webserver, as well as lots of functionality planned for the [Starlette](https://www.starlette.io/)
|
||||
web server, as well as lots of functionality planned for the [Starlette](https://www.starlette.io/)
|
||||
web framework, which is building a foundational set of tooling for working with
|
||||
ASGI.
|
||||
|
||||
|
|
|
@ -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">
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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>
|
||||
|
||||
|
|
|
@ -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,9 +40,38 @@ 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 Janurary 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**: [18st October 2018][3.9.0-milestone]
|
||||
**Date**: [18th October 2018][3.9.0-milestone]
|
||||
|
||||
* Improvements to ViewSet extra actions [#5605][gh5605]
|
||||
* Fix `action` support for ViewSet suffixes [#6081][gh6081]
|
||||
|
@ -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
|
||||
|
|
|
@ -183,8 +183,7 @@ To submit new content, [open an issue][drf-create-issue] or [create a pull reque
|
|||
|
||||
* [djangorestframework-digestauth][djangorestframework-digestauth] - Provides Digest Access Authentication support.
|
||||
* [django-oauth-toolkit][django-oauth-toolkit] - Provides OAuth 2.0 support.
|
||||
* [djangorestframework-jwt][djangorestframework-jwt] - Provides JSON Web Token Authentication support.
|
||||
* [djangorestframework-simplejwt][djangorestframework-simplejwt] - An alternative package that provides JSON Web Token Authentication support.
|
||||
* [djangorestframework-simplejwt][djangorestframework-simplejwt] - Provides JSON Web Token Authentication support.
|
||||
* [hawkrest][hawkrest] - Provides Hawk HTTP Authorization.
|
||||
* [djangorestframework-httpsignature][djangorestframework-httpsignature] - Provides an easy to use HTTP Signature Authentication mechanism.
|
||||
* [djoser][djoser] - Provides a set of views to handle basic actions such as registration, login, logout, password reset and account activation.
|
||||
|
|
Binary file not shown.
Before Width: | Height: | Size: 74 KiB |
Binary file not shown.
Before Width: | Height: | Size: 30 KiB After Width: | Height: | Size: 14 KiB |
BIN
docs/img/premium/lightson-readme.png
Normal file
BIN
docs/img/premium/lightson-readme.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 16 KiB |
|
@ -73,11 +73,12 @@ continued development by **[signing up for a paid plan][funding]**.
|
|||
<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.png)">Kloudless</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), and [Kloudless](https://hubs.ly/H0f30Lf0).*
|
||||
*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).*
|
||||
|
||||
---
|
||||
|
||||
|
@ -86,7 +87,10 @@ 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.
|
||||
|
||||
The following packages are optional:
|
||||
|
||||
|
@ -106,7 +110,7 @@ Install using `pip`, including any optional packages you want...
|
|||
|
||||
...or clone the project from github.
|
||||
|
||||
git clone git@github.com:encode/django-rest-framework.git
|
||||
git clone https://github.com/encode/django-rest-framework
|
||||
|
||||
Add `'rest_framework'` to your `INSTALLED_APPS` setting.
|
||||
|
||||
|
|
|
@ -94,6 +94,8 @@ To add branding and customize the look-and-feel of the login template, create a
|
|||
|
||||
You can add your site name or branding by including the branding block:
|
||||
|
||||
{% extends "rest_framework/login_base.html" %}
|
||||
|
||||
{% block branding %}
|
||||
<h3 style="margin: 0 0 20px;">My Site Name</h3>
|
||||
{% endblock %}
|
||||
|
|
|
@ -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/
|
||||
|
|
|
@ -193,22 +193,6 @@ This also translates into a very useful interactive documentation viewer in the
|
|||
|
||||
![Screenshot - drf-yasg][image-drf-yasg]
|
||||
|
||||
|
||||
#### DRF OpenAPI
|
||||
|
||||
[DRF OpenAPI][drf-openapi] bridges the gap between OpenAPI specification and tool chain with the schema exposed
|
||||
out-of-the-box by Django Rest Framework. Its goals are:
|
||||
|
||||
* To be dropped into any existing DRF project without any code change necessary.
|
||||
* Provide clear disctinction between request schema and response schema.
|
||||
* Provide a versioning mechanism for each schema. Support defining schema by version range syntax, e.g. >1.0, <=2.0
|
||||
* Support multiple response codes, not just 200
|
||||
* All this information should be bound to view methods, not view classes.
|
||||
|
||||
It also tries to stay current with the maturing schema generation mechanism provided by DRF.
|
||||
|
||||
![Screenshot - DRF OpenAPI][image-drf-openapi]
|
||||
|
||||
---
|
||||
|
||||
#### DRF Docs
|
||||
|
@ -338,8 +322,6 @@ To implement a hypermedia API you'll need to decide on an appropriate media type
|
|||
[cite]: https://roy.gbiv.com/untangled/2008/rest-apis-must-be-hypertext-driven
|
||||
[drf-yasg]: https://github.com/axnsan12/drf-yasg/
|
||||
[image-drf-yasg]: ../img/drf-yasg.png
|
||||
[drf-openapi]: https://github.com/limdauto/drf_openapi/
|
||||
[image-drf-openapi]: ../img/drf-openapi.png
|
||||
[drfdocs-repo]: https://github.com/ekonstantinidis/django-rest-framework-docs
|
||||
[drfdocs-website]: https://www.drfdocs.com/
|
||||
[drfdocs-demo]: http://demo.drfdocs.com/
|
||||
|
|
|
@ -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"}'
|
||||
# '{"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
|
||||
|
||||
|
@ -338,7 +338,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 +354,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"
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -106,7 +106,7 @@ If we're going to have a hyperlinked API, we need to make sure we name our URL p
|
|||
|
||||
After adding all those names into our URLconf, our final `snippets/urls.py` file should look like this:
|
||||
|
||||
from django.conf.urls import url, include
|
||||
from django.urls import path
|
||||
from rest_framework.urlpatterns import format_suffix_patterns
|
||||
from snippets import views
|
||||
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -46,7 +46,7 @@ The project layout should look like:
|
|||
./tutorial/urls.py
|
||||
./tutorial/wsgi.py
|
||||
|
||||
It may look unusual that the application has been created within the project directory. Using the project's namespace avoids name clashes with external module (topic goes outside the scope of the quickstart).
|
||||
It may look unusual that the application has been created within the project directory. Using the project's namespace avoids name clashes with external modules (a topic that goes outside the scope of the quickstart).
|
||||
|
||||
Now sync your database for the first time:
|
||||
|
||||
|
@ -77,7 +77,7 @@ First up we're going to define some serializers. Let's create a new module named
|
|||
model = Group
|
||||
fields = ('url', 'name')
|
||||
|
||||
Notice that we're using hyperlinked relations in this case, with `HyperlinkedModelSerializer`. You can also use primary key and various other relationships, but hyperlinking is good RESTful design.
|
||||
Notice that we're using hyperlinked relations in this case with `HyperlinkedModelSerializer`. You can also use primary key and various other relationships, but hyperlinking is good RESTful design.
|
||||
|
||||
## Views
|
||||
|
||||
|
@ -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.
|
||||
|
@ -133,7 +133,7 @@ Again, if we need more control over the API URLs we can simply drop down to usin
|
|||
Finally, we're including default login and logout views for use with the browsable API. That's optional, but useful if your API requires authentication and you want to use the browsable API.
|
||||
|
||||
## Pagination
|
||||
Pagination allows you to control how many objects per page are returned. To enable it add following lines to the `tutorial/settings.py`
|
||||
Pagination allows you to control how many objects per page are returned. To enable it add the following lines to `tutorial/settings.py`
|
||||
|
||||
REST_FRAMEWORK = {
|
||||
'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.PageNumberPagination',
|
||||
|
|
0
docs_theme/css/bootstrap-responsive.css
vendored
Executable file → Normal file
0
docs_theme/css/bootstrap-responsive.css
vendored
Executable file → Normal file
0
docs_theme/css/bootstrap.css
vendored
Executable file → Normal file
0
docs_theme/css/bootstrap.css
vendored
Executable file → Normal file
0
docs_theme/js/bootstrap-2.1.1-min.js
vendored
Executable file → Normal file
0
docs_theme/js/bootstrap-2.1.1-min.js
vendored
Executable file → Normal 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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -3,6 +3,8 @@ The `compat` module provides support for backwards compatibility with older
|
|||
versions of Django/Python, and compatibility wrappers around optional packages.
|
||||
"""
|
||||
|
||||
import sys
|
||||
|
||||
from django.conf import settings
|
||||
from django.core import validators
|
||||
from django.utils import six
|
||||
|
@ -10,17 +12,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
|
||||
|
@ -39,6 +34,11 @@ try:
|
|||
except ImportError:
|
||||
ProhibitNullCharactersValidator = None
|
||||
|
||||
try:
|
||||
from unittest import mock
|
||||
except ImportError:
|
||||
mock = None
|
||||
|
||||
|
||||
def get_original_route(urlpattern):
|
||||
"""
|
||||
|
@ -166,6 +166,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
|
||||
|
||||
|
||||
|
@ -315,3 +319,7 @@ class MinLengthValidator(CustomValidatorMessage, validators.MinLengthValidator):
|
|||
|
||||
class MaxLengthValidator(CustomValidatorMessage, validators.MaxLengthValidator):
|
||||
pass
|
||||
|
||||
|
||||
# Version Constants.
|
||||
PY36 = sys.version_info >= (3, 6)
|
||||
|
|
|
@ -13,6 +13,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
|
||||
|
||||
|
||||
|
@ -224,7 +225,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):
|
||||
|
@ -242,7 +243,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):
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
import collections
|
||||
import copy
|
||||
import datetime
|
||||
import decimal
|
||||
|
@ -31,7 +30,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
|
||||
)
|
||||
|
@ -94,7 +93,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)
|
||||
|
@ -348,7 +347,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
|
||||
|
@ -408,7 +407,7 @@ class Field(object):
|
|||
self._validators = validators
|
||||
|
||||
def get_validators(self):
|
||||
return self.default_validators[:]
|
||||
return list(self.default_validators)
|
||||
|
||||
def get_initial(self):
|
||||
"""
|
||||
|
@ -1659,7 +1658,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, (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')
|
||||
|
@ -1723,9 +1722,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()
|
||||
|
|
|
@ -16,6 +16,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
|
||||
)
|
||||
|
@ -52,6 +53,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,
|
||||
|
@ -76,6 +85,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)
|
||||
|
@ -89,7 +101,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:
|
||||
|
@ -287,7 +299,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'
|
||||
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -10,7 +10,34 @@ from rest_framework import exceptions
|
|||
SAFE_METHODS = ('GET', 'HEAD', 'OPTIONS')
|
||||
|
||||
|
||||
class OperandHolder:
|
||||
class OperationHolderMixin:
|
||||
def __and__(self, other):
|
||||
return OperandHolder(AND, self, other)
|
||||
|
||||
def __or__(self, other):
|
||||
return OperandHolder(OR, self, other)
|
||||
|
||||
def __rand__(self, other):
|
||||
return OperandHolder(AND, other, self)
|
||||
|
||||
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):
|
||||
self.operator_class = operator_class
|
||||
self.op1_class = op1_class
|
||||
|
@ -29,13 +56,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)
|
||||
)
|
||||
|
||||
|
@ -47,29 +74,30 @@ 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 BasePermissionMetaclass(type):
|
||||
def __and__(cls, other):
|
||||
return OperandHolder(AND, cls, other)
|
||||
class NOT:
|
||||
def __init__(self, op1):
|
||||
self.op1 = op1
|
||||
|
||||
def __or__(cls, other):
|
||||
return OperandHolder(OR, cls, other)
|
||||
def has_permission(self, request, view):
|
||||
return not self.op1.has_permission(request, view)
|
||||
|
||||
def __rand__(cls, other):
|
||||
return OperandHolder(AND, other, cls)
|
||||
def has_object_permission(self, request, view, obj):
|
||||
return not self.op1.has_object_permission(request, view, obj)
|
||||
|
||||
def __ror__(cls, other):
|
||||
return OperandHolder(OR, other, cls)
|
||||
|
||||
class BasePermissionMetaclass(OperationHolderMixin, type):
|
||||
pass
|
||||
|
||||
|
||||
@six.add_metaclass(BasePermissionMetaclass)
|
||||
|
@ -109,7 +137,7 @@ class IsAuthenticated(BasePermission):
|
|||
"""
|
||||
|
||||
def has_permission(self, request, view):
|
||||
return request.user and request.user.is_authenticated
|
||||
return bool(request.user and request.user.is_authenticated)
|
||||
|
||||
|
||||
class IsAdminUser(BasePermission):
|
||||
|
@ -118,7 +146,7 @@ class IsAdminUser(BasePermission):
|
|||
"""
|
||||
|
||||
def has_permission(self, request, view):
|
||||
return request.user and request.user.is_staff
|
||||
return bool(request.user and request.user.is_staff)
|
||||
|
||||
|
||||
class IsAuthenticatedOrReadOnly(BasePermission):
|
||||
|
@ -127,7 +155,7 @@ class IsAuthenticatedOrReadOnly(BasePermission):
|
|||
"""
|
||||
|
||||
def has_permission(self, request, view):
|
||||
return (
|
||||
return bool(
|
||||
request.method in SAFE_METHODS or
|
||||
request.user and
|
||||
request.user.is_authenticated
|
||||
|
|
|
@ -20,11 +20,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
|
||||
|
@ -947,7 +948,7 @@ class _BaseOpenAPIRenderer:
|
|||
schema = {}
|
||||
if instance.__class__ in CLASS_TO_TYPENAME:
|
||||
schema['type'] = CLASS_TO_TYPENAME[instance.__class__]
|
||||
schema['title'] = instance.title,
|
||||
schema['title'] = instance.title
|
||||
schema['description'] = instance.description
|
||||
if hasattr(instance, 'enum'):
|
||||
schema['enum'] = instance.enum
|
||||
|
|
|
@ -24,7 +24,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
|
||||
|
@ -42,7 +44,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)
|
||||
|
||||
|
@ -53,7 +55,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)
|
||||
|
||||
|
@ -76,7 +78,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),
|
||||
)
|
||||
|
||||
|
||||
|
@ -87,7 +89,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.")
|
||||
|
@ -99,6 +101,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
|
||||
|
|
|
@ -57,7 +57,7 @@ Schema Naming Collision.
|
|||
coreapi.Link for URL path {value_url} cannot be inserted into schema.
|
||||
Position conflicts with coreapi.Link for URL path {target_url}.
|
||||
|
||||
Attemped to insert link with keys: {keys}.
|
||||
Attempted to insert link with keys: {keys}.
|
||||
|
||||
Adjust URLs to avoid naming collision or override `SchemaGenerator.get_keys()`
|
||||
to customise schema structure.
|
||||
|
|
|
@ -162,7 +162,10 @@ class ViewInspector:
|
|||
@property
|
||||
def view(self):
|
||||
"""View property."""
|
||||
assert self._view is not None, "Schema generation REQUIRES a view instance. (Hint: you accessed `schema` from the view class rather than an instance.)"
|
||||
assert self._view is not None, (
|
||||
"Schema generation REQUIRES a view instance. (Hint: you accessed "
|
||||
"`schema` from the view class rather than an instance.)"
|
||||
)
|
||||
return self._view
|
||||
|
||||
@view.setter
|
||||
|
@ -192,7 +195,7 @@ class AutoSchema(ViewInspector):
|
|||
"""
|
||||
Default inspector for APIView
|
||||
|
||||
Responsible for per-view instrospection and schema generation.
|
||||
Responsible for per-view introspection and schema generation.
|
||||
"""
|
||||
def __init__(self, manual_fields=None):
|
||||
"""
|
||||
|
@ -467,7 +470,7 @@ class ManualSchema(ViewInspector):
|
|||
Parameters:
|
||||
|
||||
* `fields`: list of `coreapi.Field` instances.
|
||||
* `descripton`: String description for view. Optional.
|
||||
* `description`: String description for view. Optional.
|
||||
"""
|
||||
super().__init__()
|
||||
assert all(isinstance(f, coreapi.Field) for f in fields), "`fields` must be a list of coreapi.Field instances"
|
||||
|
@ -497,7 +500,9 @@ class DefaultSchema(ViewInspector):
|
|||
return result
|
||||
|
||||
inspector_class = api_settings.DEFAULT_SCHEMA_CLASS
|
||||
assert issubclass(inspector_class, ViewInspector), "DEFAULT_SCHEMA_CLASS must be set to a ViewInspector (usually an AutoSchema) subclass"
|
||||
assert issubclass(inspector_class, ViewInspector), (
|
||||
"DEFAULT_SCHEMA_CLASS must be set to a ViewInspector (usually an AutoSchema) subclass"
|
||||
)
|
||||
inspector = inspector_class()
|
||||
inspector.view = instance
|
||||
return inspector
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -392,7 +392,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'):
|
||||
|
@ -460,8 +460,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):
|
||||
|
@ -1476,7 +1479,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
0
rest_framework/static/rest_framework/docs/css/jquery.json-view.min.css
vendored
Executable file → Normal file
0
rest_framework/static/rest_framework/docs/css/jquery.json-view.min.css
vendored
Executable file → Normal file
0
rest_framework/static/rest_framework/docs/js/jquery.json-view.min.js
vendored
Executable file → Normal file
0
rest_framework/static/rest_framework/docs/js/jquery.json-view.min.js
vendored
Executable file → Normal file
0
rest_framework/static/rest_framework/fonts/fontawesome-webfont.eot
Executable file → Normal file
0
rest_framework/static/rest_framework/fonts/fontawesome-webfont.eot
Executable file → Normal file
0
rest_framework/static/rest_framework/fonts/fontawesome-webfont.svg
Executable file → Normal file
0
rest_framework/static/rest_framework/fonts/fontawesome-webfont.svg
Executable file → Normal file
Before Width: | Height: | Size: 197 KiB After Width: | Height: | Size: 197 KiB |
0
rest_framework/static/rest_framework/fonts/fontawesome-webfont.ttf
Executable file → Normal file
0
rest_framework/static/rest_framework/fonts/fontawesome-webfont.ttf
Executable file → Normal file
0
rest_framework/static/rest_framework/fonts/fontawesome-webfont.woff
Executable file → Normal file
0
rest_framework/static/rest_framework/fonts/fontawesome-webfont.woff
Executable file → Normal file
File diff suppressed because one or more lines are too long
|
@ -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) {
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
||||
|
@ -201,7 +205,7 @@
|
|||
{% csrf_token %}
|
||||
{{ post_form }}
|
||||
<div class="form-actions">
|
||||
<button class="btn btn-primary" title="Make a POST request on the {{ name }} resource">POST</button>
|
||||
<button class="btn btn-primary js-tooltip" title="Make a POST request on the {{ name }} resource">POST</button>
|
||||
</div>
|
||||
</fieldset>
|
||||
</form>
|
||||
|
@ -215,7 +219,7 @@
|
|||
<fieldset>
|
||||
{% include "rest_framework/raw_data_form.html" %}
|
||||
<div class="form-actions">
|
||||
<button class="btn btn-primary" title="Make a POST request on the {{ name }} resource">POST</button>
|
||||
<button class="btn btn-primary js-tooltip" title="Make a POST request on the {{ name }} resource">POST</button>
|
||||
</div>
|
||||
</fieldset>
|
||||
</form>
|
||||
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -334,6 +334,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:
|
||||
|
@ -374,21 +380,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
|
||||
|
|
|
@ -88,6 +88,9 @@ def get_field_kwargs(field_name, model_field):
|
|||
if decimal_places is not None:
|
||||
kwargs['decimal_places'] = decimal_places
|
||||
|
||||
if isinstance(model_field, models.SlugField):
|
||||
kwargs['allow_unicode'] = model_field.allow_unicode
|
||||
|
||||
if isinstance(model_field, models.TextField) or (postgres_fields and isinstance(model_field, postgres_fields.JSONField)):
|
||||
kwargs['style'] = {'base_template': 'textarea.html'}
|
||||
|
||||
|
@ -246,6 +249,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)
|
||||
|
|
|
@ -1,9 +1,8 @@
|
|||
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
|
||||
|
||||
|
||||
|
@ -128,7 +127,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.
|
||||
|
||||
|
|
|
@ -72,6 +72,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
|
||||
|
|
2
setup.py
2
setup.py
|
@ -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',
|
||||
]
|
||||
)
|
||||
|
|
0
tests/authentication/__init__.py
Normal file
0
tests/authentication/__init__.py
Normal file
24
tests/authentication/migrations/0001_initial.py
Normal file
24
tests/authentication/migrations/0001_initial.py
Normal 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)),
|
||||
],
|
||||
),
|
||||
]
|
0
tests/authentication/migrations/__init__.py
Normal file
0
tests/authentication/migrations/__init__.py
Normal file
10
tests/authentication/models.py
Normal file
10
tests/authentication/models.py
Normal 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)
|
|
@ -4,7 +4,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
|
||||
|
@ -22,14 +21,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
|
||||
|
||||
|
@ -83,7 +79,7 @@ urlpatterns = [
|
|||
]
|
||||
|
||||
|
||||
@override_settings(ROOT_URLCONF='tests.test_authentication')
|
||||
@override_settings(ROOT_URLCONF=__name__)
|
||||
class BasicAuthTests(TestCase):
|
||||
"""Basic authentication"""
|
||||
def setUp(self):
|
||||
|
@ -165,7 +161,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):
|
||||
|
@ -366,7 +362,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/'
|
||||
|
@ -425,13 +421,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/'
|
||||
|
@ -545,7 +541,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):
|
|
@ -56,6 +56,8 @@ def pytest_configure(config):
|
|||
'django.contrib.staticfiles',
|
||||
'rest_framework',
|
||||
'rest_framework.authtoken',
|
||||
'tests.authentication',
|
||||
'tests.generic_relations',
|
||||
'tests.importable',
|
||||
'tests',
|
||||
),
|
||||
|
|
0
tests/generic_relations/__init__.py
Normal file
0
tests/generic_relations/__init__.py
Normal file
36
tests/generic_relations/migrations/0001_initial.py
Normal file
36
tests/generic_relations/migrations/0001_initial.py
Normal 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')),
|
||||
],
|
||||
),
|
||||
]
|
0
tests/generic_relations/migrations/__init__.py
Normal file
0
tests/generic_relations/migrations/__init__.py
Normal file
46
tests/generic_relations/models.py
Normal file
46
tests/generic_relations/models.py
Normal 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
|
|
@ -3,6 +3,7 @@ from django.contrib.contenttypes.fields import (
|
|||
)
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.db import models
|
||||
|
||||
from django.test import TestCase
|
||||
|
||||
from rest_framework import serializers
|
|
@ -50,6 +50,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)
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
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,
|
||||
|
@ -288,7 +288,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
|
||||
|
@ -301,7 +301,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
|
||||
|
@ -315,7 +315,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
|
||||
|
|
|
@ -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):
|
||||
"""
|
||||
|
|
|
@ -3,6 +3,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
|
||||
|
@ -154,6 +155,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': '^\w{3}$'})
|
||||
response = view(request)
|
||||
assert len(response.data) == 10
|
||||
|
||||
request = factory.get('/', {'search': '^\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)
|
||||
|
@ -219,7 +245,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)
|
||||
|
@ -302,6 +328,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)
|
||||
|
|
88
tests/test_generateschema.py
Normal file
88
tests/test_generateschema.py
Normal 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())
|
|
@ -181,7 +181,7 @@ class TestRegularFieldMappings(TestCase):
|
|||
null_boolean_field = NullBooleanField(required=False)
|
||||
positive_integer_field = IntegerField()
|
||||
positive_small_integer_field = IntegerField()
|
||||
slug_field = SlugField(max_length=100)
|
||||
slug_field = SlugField(allow_unicode=False, max_length=100)
|
||||
small_integer_field = IntegerField()
|
||||
text_field = CharField(max_length=100, style={'base_template': 'textarea.html'})
|
||||
file_field = FileField(max_length=100)
|
||||
|
|
|
@ -3,16 +3,17 @@ import unittest
|
|||
import warnings
|
||||
|
||||
import django
|
||||
from django.contrib.auth.models import Group, Permission, User
|
||||
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
|
||||
|
@ -424,7 +425,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):
|
||||
|
@ -540,39 +541,58 @@ class CustomPermissionsTests(TestCase):
|
|||
self.assertEqual(detail, self.custom_message)
|
||||
|
||||
|
||||
class FakeUser:
|
||||
def __init__(self, auth=False):
|
||||
self.is_authenticated = auth
|
||||
|
||||
|
||||
class PermissionsCompositionTests(TestCase):
|
||||
|
||||
def setUp(self):
|
||||
self.username = 'john'
|
||||
self.email = 'lennon@thebeatles.com'
|
||||
self.password = 'password'
|
||||
self.user = User.objects.create_user(
|
||||
self.username,
|
||||
self.email,
|
||||
self.password
|
||||
)
|
||||
self.client.login(username=self.username, password=self.password)
|
||||
|
||||
def test_and_false(self):
|
||||
request = factory.get('/1', format='json')
|
||||
request.user = FakeUser(auth=False)
|
||||
request.user = AnonymousUser()
|
||||
composed_perm = permissions.IsAuthenticated & permissions.AllowAny
|
||||
assert composed_perm().has_permission(request, None) is False
|
||||
|
||||
def test_and_true(self):
|
||||
request = factory.get('/1', format='json')
|
||||
request.user = FakeUser(auth=True)
|
||||
request.user = self.user
|
||||
composed_perm = permissions.IsAuthenticated & permissions.AllowAny
|
||||
assert composed_perm().has_permission(request, None) is True
|
||||
|
||||
def test_or_false(self):
|
||||
request = factory.get('/1', format='json')
|
||||
request.user = FakeUser(auth=False)
|
||||
request.user = AnonymousUser()
|
||||
composed_perm = permissions.IsAuthenticated | permissions.AllowAny
|
||||
assert composed_perm().has_permission(request, None) is True
|
||||
|
||||
def test_or_true(self):
|
||||
request = factory.get('/1', format='json')
|
||||
request.user = FakeUser(auth=True)
|
||||
request.user = self.user
|
||||
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 = FakeUser(auth=True)
|
||||
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 = (
|
||||
permissions.IsAuthenticated &
|
||||
permissions.IsAuthenticated &
|
||||
|
@ -580,3 +600,109 @@ class PermissionsCompositionTests(TestCase):
|
|||
permissions.IsAuthenticated
|
||||
)
|
||||
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
|
||||
composed_perm = (
|
||||
permissions.IsAuthenticated &
|
||||
permissions.IsAuthenticated |
|
||||
permissions.IsAuthenticated &
|
||||
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()
|
||||
|
|
|
@ -3,8 +3,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
|
||||
)
|
||||
|
@ -36,6 +37,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:
|
||||
|
@ -358,6 +365,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):
|
||||
|
|
|
@ -1,18 +1,19 @@
|
|||
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,
|
||||
|
@ -824,6 +825,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):
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user