mirror of
https://github.com/encode/django-rest-framework.git
synced 2025-07-30 01:49:50 +03:00
Merge branch 'master' into master
This commit is contained in:
commit
890613c0bb
|
@ -1,9 +1,6 @@
|
|||
language: python
|
||||
cache: pip
|
||||
dist: xenial
|
||||
|
||||
sudo: false
|
||||
|
||||
matrix:
|
||||
fast_finish: true
|
||||
include:
|
||||
|
@ -15,15 +12,17 @@ matrix:
|
|||
- { python: "3.5", env: DJANGO=1.11 }
|
||||
- { python: "3.5", env: DJANGO=2.0 }
|
||||
- { python: "3.5", env: DJANGO=2.1 }
|
||||
- { python: "3.5", env: DJANGO=master }
|
||||
- { python: "3.5", env: DJANGO=2.2 }
|
||||
|
||||
- { python: "3.6", env: DJANGO=1.11 }
|
||||
- { python: "3.6", env: DJANGO=2.0 }
|
||||
- { python: "3.6", env: DJANGO=2.1 }
|
||||
- { python: "3.6", env: DJANGO=2.2 }
|
||||
- { python: "3.6", env: DJANGO=master }
|
||||
|
||||
- { python: "3.7", env: DJANGO=2.0 }
|
||||
- { python: "3.7", env: DJANGO=2.1 }
|
||||
- { python: "3.7", env: DJANGO=2.2 }
|
||||
- { python: "3.7", env: DJANGO=master }
|
||||
|
||||
- { python: "3.7", env: TOXENV=base }
|
||||
|
|
|
@ -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.)
|
||||
|
||||
|
|
12
README.md
12
README.md
|
@ -19,17 +19,15 @@ continued development by [signing up for a paid plan][funding].
|
|||
The initial aim is to provide a single full-time position on REST framework.
|
||||
*Every single sign-up makes a significant impact towards making that possible.*
|
||||
|
||||
[![][rover-img]][rover-url]
|
||||
[![][sentry-img]][sentry-url]
|
||||
[![][stream-img]][stream-url]
|
||||
[![][rollbar-img]][rollbar-url]
|
||||
[![][cadre-img]][cadre-url]
|
||||
[![][load-impact-img]][load-impact-url]
|
||||
[![][kloudless-img]][kloudless-url]
|
||||
[![][auklet-img]][auklet-url]
|
||||
[![][release-history-img]][release-history-url]
|
||||
[![][lightson-img]][lightson-url]
|
||||
|
||||
Many thanks to all our [wonderful sponsors][sponsors], and in particular to our premium backers, [Rover][rover-url], [Sentry][sentry-url], [Stream][stream-url], [Rollbar][rollbar-url], [Cadre][cadre-url], [Load Impact][load-impact-url], [Kloudless][kloudless-url], [Auklet][auklet-url], and [Lights On Software][lightson-url].
|
||||
Many thanks to all our [wonderful sponsors][sponsors], and in particular to our premium backers, [Sentry][sentry-url], [Stream][stream-url], [Rollbar][rollbar-url], [Cadre][cadre-url], [Kloudless][kloudless-url], [Release History][release-history-url], and [Lights On Software][lightson-url].
|
||||
|
||||
---
|
||||
|
||||
|
@ -56,7 +54,7 @@ There is a live example API for testing purposes, [available here][sandbox].
|
|||
# Requirements
|
||||
|
||||
* Python (2.7, 3.4, 3.5, 3.6, 3.7)
|
||||
* Django (1.11, 2.0, 2.1)
|
||||
* Django (1.11, 2.0, 2.1, 2.2)
|
||||
|
||||
We **highly recommend** and only officially support the latest patch release of
|
||||
each Python and Django series.
|
||||
|
@ -201,7 +199,7 @@ Send a description of the issue via email to [rest-framework-security@googlegrou
|
|||
[cadre-img]: https://raw.githubusercontent.com/encode/django-rest-framework/master/docs/img/premium/cadre-readme.png
|
||||
[load-impact-img]: https://raw.githubusercontent.com/encode/django-rest-framework/master/docs/img/premium/load-impact-readme.png
|
||||
[kloudless-img]: https://raw.githubusercontent.com/encode/django-rest-framework/master/docs/img/premium/kloudless-readme.png
|
||||
[auklet-img]: https://raw.githubusercontent.com/encode/django-rest-framework/master/docs/img/premium/auklet-readme.png
|
||||
[release-history-img]: https://raw.githubusercontent.com/encode/django-rest-framework/master/docs/img/premium/release-history.png
|
||||
[lightson-img]: https://raw.githubusercontent.com/encode/django-rest-framework/master/docs/img/premium/lightson-readme.png
|
||||
|
||||
[rover-url]: http://jobs.rover.com/
|
||||
|
@ -211,7 +209,7 @@ Send a description of the issue via email to [rest-framework-security@googlegrou
|
|||
[cadre-url]: https://cadre.com/
|
||||
[load-impact-url]: https://loadimpact.com/?utm_campaign=Sponsorship%20links&utm_source=drf&utm_medium=drf
|
||||
[kloudless-url]: https://hubs.ly/H0f30Lf0
|
||||
[auklet-url]: https://auklet.io/
|
||||
[release-history-url]: https://releasehistory.io
|
||||
[lightson-url]: https://lightsonsoftware.com
|
||||
|
||||
[oauth1-section]: https://www.django-rest-framework.org/api-guide/authentication/#django-rest-framework-oauth
|
||||
|
|
|
@ -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`.
|
||||
|
||||
|
@ -299,10 +306,11 @@ A date and time representation.
|
|||
|
||||
Corresponds to `django.db.models.fields.DateTimeField`.
|
||||
|
||||
**Signature:** `DateTimeField(format=api_settings.DATETIME_FORMAT, input_formats=None)`
|
||||
**Signature:** `DateTimeField(format=api_settings.DATETIME_FORMAT, input_formats=None, default_timezone=None)`
|
||||
|
||||
* `format` - A string representing the output format. If not specified, this defaults to the same value as the `DATETIME_FORMAT` settings key, which will be `'iso-8601'` unless set. Setting to a format string indicates that `to_representation` return values should be coerced to string output. Format strings are described below. Setting this value to `None` indicates that Python `datetime` objects should be returned by `to_representation`. In this case the datetime encoding will be determined by the renderer.
|
||||
* `input_formats` - A list of strings representing the input formats which may be used to parse the date. If not specified, the `DATETIME_INPUT_FORMATS` setting will be used, which defaults to `['iso-8601']`.
|
||||
* `default_timezone` - A `pytz.timezone` representing the timezone. If not specified and the `USE_TZ` setting is enabled, this defaults to the [current timezone][django-current-timezone]. If `USE_TZ` is disabled, then datetime objects will be naive.
|
||||
|
||||
#### `DateTimeField` format strings.
|
||||
|
||||
|
@ -828,3 +836,4 @@ The [django-rest-framework-hstore][django-rest-framework-hstore] package provide
|
|||
[django-rest-framework-hstore]: https://github.com/djangonauts/django-rest-framework-hstore
|
||||
[django-hstore]: https://github.com/djangonauts/django-hstore
|
||||
[python-decimal-rounding-modes]: https://docs.python.org/3/library/decimal.html#rounding-modes
|
||||
[django-current-timezone]: https://docs.djangoproject.com/en/stable/topics/i18n/timezones/#default-time-zone-and-current-time-zone
|
||||
|
|
|
@ -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,16 @@ For example:
|
|||
|
||||
By default, the search parameter is named `'search`', but this may be overridden with the `SEARCH_PARAM` setting.
|
||||
|
||||
To dynamically change search fields based on request content, it's possible to subclass the `SearchFilter` and override the `get_search_fields()` function. For example, the following subclass will only search on `title` if the query parameter `title_only` is in the request:
|
||||
|
||||
from rest_framework import filters
|
||||
|
||||
class CustomSearchFilter(filters.SearchFilter):
|
||||
def get_search_fields(self, view, request):
|
||||
if request.query_params.get('title_only'):
|
||||
return ('title',)
|
||||
return super(CustomSearchFilter, self).get_search_fields(view, request)
|
||||
|
||||
For more details, see the [Django documentation][search-django-admin].
|
||||
|
||||
---
|
||||
|
@ -298,9 +308,9 @@ A complete example using both `DjangoObjectPermissionsFilter` and `DjangoObjectP
|
|||
**permissions.py**:
|
||||
|
||||
class CustomObjectPermissions(permissions.DjangoObjectPermissions):
|
||||
"""
|
||||
Similar to `DjangoObjectPermissions`, but adding 'view' permissions.
|
||||
"""
|
||||
"""
|
||||
Similar to `DjangoObjectPermissions`, but adding 'view' permissions.
|
||||
"""
|
||||
perms_map = {
|
||||
'GET': ['%(app_label)s.view_%(model_name)s'],
|
||||
'OPTIONS': ['%(app_label)s.view_%(model_name)s'],
|
||||
|
@ -314,11 +324,11 @@ A complete example using both `DjangoObjectPermissionsFilter` and `DjangoObjectP
|
|||
**views.py**:
|
||||
|
||||
class EventViewSet(viewsets.ModelViewSet):
|
||||
"""
|
||||
Viewset that only lists events if user has 'view' permissions, and only
|
||||
allows operations on individual events if user has appropriate 'view', 'add',
|
||||
'change' or 'delete' permissions.
|
||||
"""
|
||||
"""
|
||||
Viewset that only lists events if user has 'view' permissions, and only
|
||||
allows operations on individual events if user has appropriate 'view', 'add',
|
||||
'change' or 'delete' permissions.
|
||||
"""
|
||||
queryset = Event.objects.all()
|
||||
serializer_class = EventSerializer
|
||||
filter_backends = (filters.DjangoObjectPermissionsFilter,)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -10,9 +10,9 @@ Together with [authentication] and [throttling], permissions determine whether a
|
|||
|
||||
Permission checks are always run at the very start of the view, before any other code is allowed to proceed. Permission checks will typically use the authentication information in the `request.user` and `request.auth` properties to determine if the incoming request should be permitted.
|
||||
|
||||
Permissions are used to grant or deny access different classes of users to different parts of the API.
|
||||
Permissions are used to grant or deny access for different classes of users to different parts of the API.
|
||||
|
||||
The simplest style of permission would be to allow access to any authenticated user, and deny access to any unauthenticated user. This corresponds the `IsAuthenticated` class in REST framework.
|
||||
The simplest style of permission would be to allow access to any authenticated user, and deny access to any unauthenticated user. This corresponds to the `IsAuthenticated` class in REST framework.
|
||||
|
||||
A slightly less strict style of permission would be to allow full access to authenticated users, but allow read-only access to unauthenticated users. This corresponds to the `IsAuthenticatedOrReadOnly` class in REST framework.
|
||||
|
||||
|
@ -48,6 +48,19 @@ For example:
|
|||
self.check_object_permissions(self.request, obj)
|
||||
return obj
|
||||
|
||||
---
|
||||
|
||||
**Note**: With the exception of `DjangoObjectPermissions`, the provided
|
||||
permission classes in `rest_framework.permissions` **do not** implement the
|
||||
methods necessary to check object permissions.
|
||||
|
||||
If you wish to use the provided permission classes in order to check object
|
||||
permissions, **you must** subclass them and implement the
|
||||
`has_object_permission()` method described in the [_Custom
|
||||
permissions_](#custom-permissions) section (below).
|
||||
|
||||
---
|
||||
|
||||
#### Limitations of object level permissions
|
||||
|
||||
For performance reasons the generic views will not automatically apply object level permissions to each instance in a queryset when returning a list of objects.
|
||||
|
@ -121,7 +134,7 @@ Provided they inherit from `rest_framework.permissions.BasePermission`, permissi
|
|||
}
|
||||
return Response(content)
|
||||
|
||||
__Note:__ it only supports & -and- and | -or-.
|
||||
__Note:__ it supports & (and), | (or) and ~ (not).
|
||||
|
||||
---
|
||||
|
||||
|
@ -284,9 +297,9 @@ The [DRY Rest Permissions][dry-rest-permissions] package provides the ability to
|
|||
|
||||
The [Django Rest Framework Roles][django-rest-framework-roles] package makes it easier to parameterize your API over multiple types of users.
|
||||
|
||||
## Django Rest Framework API Key
|
||||
## Django REST Framework API Key
|
||||
|
||||
The [Django Rest Framework API Key][django-rest-framework-api-key] package allows you to ensure that every request made to the server requires an API key header. You can generate one from the django admin interface.
|
||||
The [Django REST Framework API Key][djangorestframework-api-key] package provides the ability to authorize clients based on customizable API key headers. This package is targeted at situations in which regular user-based authentication (e.g. `TokenAuthentication`) is not suitable, e.g. allowing non-human clients to safely use your API. API keys are generated and validated through cryptographic methods and can be created and revoked from the Django admin interface at anytime.
|
||||
|
||||
## Django Rest Framework Role Filters
|
||||
|
||||
|
@ -304,6 +317,6 @@ The [Django Rest Framework Role Filters][django-rest-framework-role-filters] pac
|
|||
[rest-condition]: https://github.com/caxap/rest_condition
|
||||
[dry-rest-permissions]: https://github.com/Helioscene/dry-rest-permissions
|
||||
[django-rest-framework-roles]: https://github.com/computer-lab/django-rest-framework-roles
|
||||
[django-rest-framework-api-key]: https://github.com/manosim/django-rest-framework-api-key
|
||||
[djangorestframework-api-key]: https://github.com/florimondmanca/djangorestframework-api-key
|
||||
[django-rest-framework-role-filters]: https://github.com/allisson/django-rest-framework-role-filters
|
||||
[django-rest-framework-guardian]: https://github.com/rpkilby/django-rest-framework-guardian
|
||||
|
|
|
@ -46,12 +46,12 @@ In order to explain the various types of relational fields, we'll use a couple o
|
|||
unique_together = ('album', 'order')
|
||||
ordering = ['order']
|
||||
|
||||
def __unicode__(self):
|
||||
def __str__(self):
|
||||
return '%d: %s' % (self.order, self.title)
|
||||
|
||||
## StringRelatedField
|
||||
|
||||
`StringRelatedField` may be used to represent the target of the relationship using its `__unicode__` method.
|
||||
`StringRelatedField` may be used to represent the target of the relationship using its `__str__` method.
|
||||
|
||||
For example, the following serializer.
|
||||
|
||||
|
@ -510,7 +510,7 @@ For example, given the following model for a tag, which has a generic relationsh
|
|||
object_id = models.PositiveIntegerField()
|
||||
tagged_object = GenericForeignKey('content_type', 'object_id')
|
||||
|
||||
def __unicode__(self):
|
||||
def __str__(self):
|
||||
return self.tag_name
|
||||
|
||||
And the following two models, which may have associated tags:
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -20,7 +20,7 @@ can render the schema into the commonly used YAML-based OpenAPI format.
|
|||
|
||||
## Quickstart
|
||||
|
||||
There are two different ways you can serve a schema description for you API.
|
||||
There are two different ways you can serve a schema description for your API.
|
||||
|
||||
### Generating a schema with the `generateschema` management command
|
||||
|
||||
|
@ -112,8 +112,8 @@ has to be rendered into the actual bytes that are used in the response.
|
|||
REST framework includes a few different renderers that you can use for
|
||||
encoding the API schema.
|
||||
|
||||
* `renderers.OpenAPIRenderer` - Renders into YAML-based [OpenAPI][openapi], the most widely used API schema format.
|
||||
* `renderers.JSONOpenAPIRenderer` - Renders into JSON-based [OpenAPI][openapi].
|
||||
* `renderers.OpenAPIRenderer` - Renders into YAML-based [OpenAPI][open-api], the most widely used API schema format.
|
||||
* `renderers.JSONOpenAPIRenderer` - Renders into JSON-based [OpenAPI][open-api].
|
||||
* `renderers.CoreJSONRenderer` - Renders into [Core JSON][corejson], a format designed for
|
||||
use with the `coreapi` client library.
|
||||
|
||||
|
@ -827,19 +827,11 @@ A short description of the meaning and intended usage of the input field.
|
|||
[drf-yasg][drf-yasg] generates [OpenAPI][open-api] documents suitable for code generation - nested schemas,
|
||||
named models, response bodies, enum/pattern/min/max validators, form parameters, etc.
|
||||
|
||||
|
||||
## DRF OpenAPI
|
||||
|
||||
[DRF OpenAPI][drf-openapi] renders the schema generated by Django Rest Framework
|
||||
in [OpenAPI][open-api] format.
|
||||
|
||||
|
||||
[cite]: https://blog.heroku.com/archives/2014/1/8/json_schema_for_heroku_platform_api
|
||||
[coreapi]: https://www.coreapi.org/
|
||||
[corejson]: https://www.coreapi.org/specification/encoding/#core-json-encoding
|
||||
[drf-yasg]: https://github.com/axnsan12/drf-yasg/
|
||||
[open-api]: https://openapis.org/
|
||||
[drf-openapi]: https://github.com/limdauto/drf_openapi
|
||||
[json-hyperschema]: https://json-schema.org/latest/json-schema-hypermedia.html
|
||||
[api-blueprint]: https://apiblueprint.org/
|
||||
[static-files]: https://docs.djangoproject.com/en/stable/howto/static-files/
|
||||
|
|
|
@ -152,7 +152,7 @@ When deserializing data, you always need to call `is_valid()` before attempting
|
|||
serializer.is_valid()
|
||||
# False
|
||||
serializer.errors
|
||||
# {'email': [u'Enter a valid e-mail address.'], 'created': [u'This field is required.']}
|
||||
# {'email': ['Enter a valid e-mail address.'], 'created': ['This field is required.']}
|
||||
|
||||
Each key in the dictionary will be the field name, and the values will be lists of strings of any error messages corresponding to that field. The `non_field_errors` key may also be present, and will list any general validation errors. The name of the `non_field_errors` key may be customized using the `NON_FIELD_ERRORS_KEY` REST framework setting.
|
||||
|
||||
|
@ -253,7 +253,7 @@ When passing data to a serializer instance, the unmodified data will be made ava
|
|||
By default, serializers must be passed values for all required fields or they will raise validation errors. You can use the `partial` argument in order to allow partial updates.
|
||||
|
||||
# Update `comment` with partial data
|
||||
serializer = CommentSerializer(comment, data={'content': u'foo bar'}, partial=True)
|
||||
serializer = CommentSerializer(comment, data={'content': 'foo bar'}, partial=True)
|
||||
|
||||
## Dealing with nested objects
|
||||
|
||||
|
@ -293,7 +293,7 @@ When dealing with nested representations that support deserializing the data, an
|
|||
serializer.is_valid()
|
||||
# False
|
||||
serializer.errors
|
||||
# {'user': {'email': [u'Enter a valid e-mail address.']}, 'created': [u'This field is required.']}
|
||||
# {'user': {'email': ['Enter a valid e-mail address.']}, 'created': ['This field is required.']}
|
||||
|
||||
Similarly, the `.validated_data` property will include nested data structures.
|
||||
|
||||
|
@ -415,7 +415,7 @@ You can provide arbitrary additional context by passing a `context` argument whe
|
|||
|
||||
serializer = AccountSerializer(account, context={'request': request})
|
||||
serializer.data
|
||||
# {'id': 6, 'owner': u'denvercoder9', 'created': datetime.datetime(2013, 2, 12, 09, 44, 56, 678870), 'details': 'http://example.com/accounts/6/details'}
|
||||
# {'id': 6, 'owner': 'denvercoder9', 'created': datetime.datetime(2013, 2, 12, 09, 44, 56, 678870), 'details': 'http://example.com/accounts/6/details'}
|
||||
|
||||
The context dictionary can be used within any serializer field logic, such as a custom `.to_representation()` method, by accessing the `self.context` attribute.
|
||||
|
||||
|
@ -1094,10 +1094,10 @@ This would then allow you to do the following:
|
|||
>>> model = User
|
||||
>>> fields = ('id', 'username', 'email')
|
||||
>>>
|
||||
>>> print UserSerializer(user)
|
||||
>>> print(UserSerializer(user))
|
||||
{'id': 2, 'username': 'jonwatts', 'email': 'jon@example.com'}
|
||||
>>>
|
||||
>>> print UserSerializer(user, fields=('id', 'email'))
|
||||
>>> print(UserSerializer(user, fields=('id', 'email')))
|
||||
{'id': 2, 'email': 'jon@example.com'}
|
||||
|
||||
## Customizing the default fields
|
||||
|
|
|
@ -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).*
|
||||
|
||||
---
|
||||
|
||||
|
|
|
@ -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,6 +40,35 @@ You can determine your currently installed version using `pip show`:
|
|||
|
||||
## 3.9.x series
|
||||
|
||||
### 3.9.2
|
||||
|
||||
**Date**: [3rd March 2019][3.9.1-milestone]
|
||||
|
||||
* Routers: invalidate `_urls` cache on `register()` [#6407][gh6407]
|
||||
* Deferred schema renderer creation to avoid requiring pyyaml. [#6416][gh6416]
|
||||
* Added 'request_forms' block to base.html [#6340][gh6340]
|
||||
* Fixed SchemaView to reset renderer on exception. [#6429][gh6429]
|
||||
* Update Django Guardian dependency. [#6430][gh6430]
|
||||
* Ensured support for Django 2.2 [#6422][gh6422] & [#6455][gh6455]
|
||||
* Made templates compatible with session-based CSRF. [#6207][gh6207]
|
||||
* Adjusted field `validators` to accept non-list iterables. [#6282][gh6282]
|
||||
* Added SearchFilter.get_search_fields() hook. [#6279][gh6279]
|
||||
* Fix DeprecationWarning when accessing collections.abc classes via collections [#6268][gh6268]
|
||||
* Allowed Q objects in limit_choices_to introspection. [#6472][gh6472]
|
||||
* Added lazy evaluation to composed permissions. [#6463][gh6463]
|
||||
* Add negation ~ operator to permissions composition [#6361][gh6361]
|
||||
* Avoided calling distinct on annotated fields in SearchFilter. [#6240][gh6240]
|
||||
* Introduced `RemovedInDRF…Warning` classes to simplify deprecations. [#6480][gh6480]
|
||||
|
||||
### 3.9.1
|
||||
|
||||
**Date**: [16th January 2019][3.9.1-milestone]
|
||||
|
||||
* Resolve XSS issue in browsable API. [#6330][gh6330]
|
||||
* Upgrade Bootstrap to 3.4.0 to resolve XSS issue.
|
||||
* Resolve issues with composable permissions. [#6299][gh6299]
|
||||
* Respect `limit_choices_to` on foreign keys. [#6371][gh6371]
|
||||
|
||||
### 3.9.0
|
||||
|
||||
**Date**: [18th October 2018][3.9.0-milestone]
|
||||
|
@ -1135,6 +1164,8 @@ For older release notes, [please see the version 2.x documentation][old-release-
|
|||
[3.8.1-milestone]: https://github.com/encode/django-rest-framework/milestone/67?closed=1
|
||||
[3.8.2-milestone]: https://github.com/encode/django-rest-framework/milestone/68?closed=1
|
||||
[3.9.0-milestone]: https://github.com/encode/django-rest-framework/milestone/66?closed=1
|
||||
[3.9.1-milestone]: https://github.com/encode/django-rest-framework/milestone/70?closed=1
|
||||
[3.9.1-milestone]: https://github.com/encode/django-rest-framework/milestone/71?closed=1
|
||||
|
||||
<!-- 3.0.1 -->
|
||||
[gh2013]: https://github.com/encode/django-rest-framework/issues/2013
|
||||
|
@ -2052,3 +2083,26 @@ For older release notes, [please see the version 2.x documentation][old-release-
|
|||
[gh6233]: https://github.com/encode/django-rest-framework/issues/6233
|
||||
[gh5753]: https://github.com/encode/django-rest-framework/issues/5753
|
||||
[gh6229]: https://github.com/encode/django-rest-framework/issues/6229
|
||||
|
||||
<!-- 3.9.1 -->
|
||||
[gh6330]: https://github.com/encode/django-rest-framework/issues/6330
|
||||
[gh6299]: https://github.com/encode/django-rest-framework/issues/6299
|
||||
[gh6371]: https://github.com/encode/django-rest-framework/issues/6371
|
||||
|
||||
<!-- 3.9.2 -->
|
||||
[gh6480]: https://github.com/encode/django-rest-framework/issues/6480
|
||||
[gh6240]: https://github.com/encode/django-rest-framework/issues/6240
|
||||
[gh6361]: https://github.com/encode/django-rest-framework/issues/6361
|
||||
[gh6463]: https://github.com/encode/django-rest-framework/issues/6463
|
||||
[gh6472]: https://github.com/encode/django-rest-framework/issues/6472
|
||||
[gh6268]: https://github.com/encode/django-rest-framework/issues/6268
|
||||
[gh6279]: https://github.com/encode/django-rest-framework/issues/6279
|
||||
[gh6282]: https://github.com/encode/django-rest-framework/issues/6282
|
||||
[gh6207]: https://github.com/encode/django-rest-framework/issues/6207
|
||||
[gh6455]: https://github.com/encode/django-rest-framework/issues/6455
|
||||
[gh6422]: https://github.com/encode/django-rest-framework/issues/6422
|
||||
[gh6430]: https://github.com/encode/django-rest-framework/issues/6430
|
||||
[gh6429]: https://github.com/encode/django-rest-framework/issues/6429
|
||||
[gh6340]: https://github.com/encode/django-rest-framework/issues/6340
|
||||
[gh6416]: https://github.com/encode/django-rest-framework/issues/6416
|
||||
[gh6407]: https://github.com/encode/django-rest-framework/issues/6407
|
||||
|
|
|
@ -263,6 +263,7 @@ To submit new content, [open an issue][drf-create-issue] or [create a pull reque
|
|||
* [django-rest-messaging][django-rest-messaging], [django-rest-messaging-centrifugo][django-rest-messaging-centrifugo] and [django-rest-messaging-js][django-rest-messaging-js] - A real-time pluggable messaging service using DRM.
|
||||
* [djangorest-alchemy][djangorest-alchemy] - SQLAlchemy support for REST framework.
|
||||
* [djangorestframework-datatables][djangorestframework-datatables] - Seamless integration between Django REST framework and [Datatables](https://datatables.net).
|
||||
* [django-rest-framework-condition][django-rest-framework-condition] - Decorators for managing HTTP cache headers for Django REST framework (ETag and Last-modified).
|
||||
|
||||
[cite]: http://www.software-ecosystems.com/Software_Ecosystems/Ecosystems.html
|
||||
[cookiecutter]: https://github.com/jpadilla/cookiecutter-django-rest-framework
|
||||
|
@ -336,3 +337,4 @@ To submit new content, [open an issue][drf-create-issue] or [create a pull reque
|
|||
[drfpasswordless]: https://github.com/aaronn/django-rest-framework-passwordless
|
||||
[djangorest-alchemy]: https://github.com/dealertrack/djangorest-alchemy
|
||||
[djangorestframework-datatables]: https://github.com/izimobil/django-rest-framework-datatables
|
||||
[django-rest-framework-condition]: https://github.com/jozo/django-rest-framework-condition
|
||||
|
|
Binary file not shown.
Before Width: | Height: | Size: 48 KiB |
BIN
docs/img/premium/release-history.png
Normal file
BIN
docs/img/premium/release-history.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 18 KiB |
|
@ -66,19 +66,17 @@ continued development by **[signing up for a paid plan][funding]**.
|
|||
*Every single sign-up helps us make REST framework long-term financially sustainable.*
|
||||
|
||||
<ul class="premium-promo promo">
|
||||
<li><a href="http://jobs.rover.com/" style="background-image: url(https://fund-rest-framework.s3.amazonaws.com/rover_130x130.png)">Rover.com</a></li>
|
||||
<li><a href="https://getsentry.com/welcome/" style="background-image: url(https://fund-rest-framework.s3.amazonaws.com/sentry130.png)">Sentry</a></li>
|
||||
<li><a href="https://getstream.io/try-the-api/?utm_source=drf&utm_medium=banner&utm_campaign=drf" style="background-image: url(https://fund-rest-framework.s3.amazonaws.com/stream-130.png)">Stream</a></li>
|
||||
<li><a href="https://auklet.io" style="background-image: url(https://fund-rest-framework.s3.amazonaws.com/auklet-new.png)">Auklet</a></li>
|
||||
<li><a href="https://releasehistory.io" style="background-image: url(https://fund-rest-framework.s3.amazonaws.com/release-history.png)">Release History</a></li>
|
||||
<li><a href="https://rollbar.com" style="background-image: url(https://fund-rest-framework.s3.amazonaws.com/rollbar2.png)">Rollbar</a></li>
|
||||
<li><a href="https://cadre.com" style="background-image: url(https://fund-rest-framework.s3.amazonaws.com/cadre.png)">Cadre</a></li>
|
||||
<li><a href="https://loadimpact.com/?utm_campaign=Sponsorship%20links&utm_source=drf&utm_medium=drf" style="background-image: url(https://fund-rest-framework.s3.amazonaws.com/load-impact.png)">Load Impact</a></li>
|
||||
<li><a href="https://hubs.ly/H0f30Lf0" style="background-image: url(https://fund-rest-framework.s3.amazonaws.com/kloudless-plus-text.png)">Kloudless</a></li>
|
||||
<li><a href="https://lightsonsoftware.com" style="background-image: url(https://fund-rest-framework.s3.amazonaws.com/lightson-dark.png)">Lights On Software</a></li>
|
||||
</ul>
|
||||
<div style="clear: both; padding-bottom: 20px;"></div>
|
||||
|
||||
*Many thanks to all our [wonderful sponsors][sponsors], and in particular to our premium backers, [Rover](http://jobs.rover.com/), [Sentry](https://getsentry.com/welcome/), [Stream](https://getstream.io/?utm_source=drf&utm_medium=banner&utm_campaign=drf), [Auklet](https://auklet.io/), [Rollbar](https://rollbar.com), [Cadre](https://cadre.com), [Load Impact](https://loadimpact.com/?utm_campaign=Sponsorship%20links&utm_source=drf&utm_medium=drf), [Kloudless](https://hubs.ly/H0f30Lf0), and [Lights On Software](https://lightsonsoftware.com).*
|
||||
*Many thanks to all our [wonderful sponsors][sponsors], and in particular to our premium backers, [Sentry](https://getsentry.com/welcome/), [Stream](https://getstream.io/?utm_source=drf&utm_medium=banner&utm_campaign=drf), [Release History](https://releasehistory.io), [Rollbar](https://rollbar.com), [Cadre](https://cadre.com), [Kloudless](https://hubs.ly/H0f30Lf0), and [Lights On Software](https://lightsonsoftware.com).*
|
||||
|
||||
---
|
||||
|
||||
|
@ -87,7 +85,7 @@ continued development by **[signing up for a paid plan][funding]**.
|
|||
REST framework requires the following:
|
||||
|
||||
* Python (2.7, 3.4, 3.5, 3.6, 3.7)
|
||||
* Django (1.11, 2.0, 2.1)
|
||||
* Django (1.11, 2.0, 2.1, 2.2)
|
||||
|
||||
We **highly recommend** and only officially support the latest patch release of
|
||||
each Python and Django series.
|
||||
|
|
|
@ -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/
|
||||
|
|
|
@ -8,7 +8,7 @@ The tutorial is fairly in-depth, so you should probably get a cookie and a cup o
|
|||
|
||||
---
|
||||
|
||||
**Note**: The code for this tutorial is available in the [tomchristie/rest-framework-tutorial][repo] repository on GitHub. The completed implementation is also online as a sandbox version for testing, [available here][sandbox].
|
||||
**Note**: The code for this tutorial is available in the [encode/rest-framework-tutorial][repo] repository on GitHub. The completed implementation is also online as a sandbox version for testing, [available here][sandbox].
|
||||
|
||||
---
|
||||
|
||||
|
@ -137,20 +137,20 @@ Okay, once we've got a few imports out of the way, let's create a couple of code
|
|||
snippet = Snippet(code='foo = "bar"\n')
|
||||
snippet.save()
|
||||
|
||||
snippet = Snippet(code='print "hello, world"\n')
|
||||
snippet = Snippet(code='print("hello, world")\n')
|
||||
snippet.save()
|
||||
|
||||
We've now got a few snippet instances to play with. Let's take a look at serializing one of those instances.
|
||||
|
||||
serializer = SnippetSerializer(snippet)
|
||||
serializer.data
|
||||
# {'id': 2, 'title': u'', 'code': u'print "hello, world"\n', 'linenos': False, 'language': u'python', 'style': u'friendly'}
|
||||
# {'id': 2, 'title': '', 'code': 'print("hello, world")\n', 'linenos': False, 'language': 'python', 'style': 'friendly'}
|
||||
|
||||
At this point we've translated the model instance into Python native datatypes. To finalize the serialization process we render the data into `json`.
|
||||
|
||||
content = JSONRenderer().render(serializer.data)
|
||||
content
|
||||
# '{"id": 2, "title": "", "code": "print \\"hello, world\\"\\n", "linenos": false, "language": "python", "style": "friendly"}'
|
||||
# b'{"id": 2, "title": "", "code": "print(\\"hello, world\\")\\n", "linenos": false, "language": "python", "style": "friendly"}'
|
||||
|
||||
Deserialization is similar. First we parse a stream into Python native datatypes...
|
||||
|
||||
|
@ -165,7 +165,7 @@ Deserialization is similar. First we parse a stream into Python native datatype
|
|||
serializer.is_valid()
|
||||
# True
|
||||
serializer.validated_data
|
||||
# OrderedDict([('title', ''), ('code', 'print "hello, world"\n'), ('linenos', False), ('language', 'python'), ('style', 'friendly')])
|
||||
# OrderedDict([('title', ''), ('code', 'print("hello, world")\n'), ('linenos', False), ('language', 'python'), ('style', 'friendly')])
|
||||
serializer.save()
|
||||
# <Snippet: Snippet object>
|
||||
|
||||
|
@ -175,7 +175,7 @@ We can also serialize querysets instead of model instances. To do so we simply
|
|||
|
||||
serializer = SnippetSerializer(Snippet.objects.all(), many=True)
|
||||
serializer.data
|
||||
# [OrderedDict([('id', 1), ('title', u''), ('code', u'foo = "bar"\n'), ('linenos', False), ('language', 'python'), ('style', 'friendly')]), OrderedDict([('id', 2), ('title', u''), ('code', u'print "hello, world"\n'), ('linenos', False), ('language', 'python'), ('style', 'friendly')]), OrderedDict([('id', 3), ('title', u''), ('code', u'print "hello, world"'), ('linenos', False), ('language', 'python'), ('style', 'friendly')])]
|
||||
# [OrderedDict([('id', 1), ('title', ''), ('code', 'foo = "bar"\n'), ('linenos', False), ('language', 'python'), ('style', 'friendly')]), OrderedDict([('id', 2), ('title', ''), ('code', 'print("hello, world")\n'), ('linenos', False), ('language', 'python'), ('style', 'friendly')]), OrderedDict([('id', 3), ('title', ''), ('code', 'print("hello, world")'), ('linenos', False), ('language', 'python'), ('style', 'friendly')])]
|
||||
|
||||
## Using ModelSerializers
|
||||
|
||||
|
@ -218,7 +218,6 @@ Edit the `snippets/views.py` file, and add the following.
|
|||
|
||||
from django.http import HttpResponse, JsonResponse
|
||||
from django.views.decorators.csrf import csrf_exempt
|
||||
from rest_framework.renderers import JSONRenderer
|
||||
from rest_framework.parsers import JSONParser
|
||||
from snippets.models import Snippet
|
||||
from snippets.serializers import SnippetSerializer
|
||||
|
@ -338,7 +337,7 @@ Finally, we can get a list of all of the snippets:
|
|||
{
|
||||
"id": 2,
|
||||
"title": "",
|
||||
"code": "print \"hello, world\"\n",
|
||||
"code": "print(\"hello, world\")\n",
|
||||
"linenos": false,
|
||||
"language": "python",
|
||||
"style": "friendly"
|
||||
|
@ -354,7 +353,7 @@ Or we can get a particular snippet by referencing its id:
|
|||
{
|
||||
"id": 2,
|
||||
"title": "",
|
||||
"code": "print \"hello, world\"\n",
|
||||
"code": "print(\"hello, world\")\n",
|
||||
"linenos": false,
|
||||
"language": "python",
|
||||
"style": "friendly"
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -111,7 +111,7 @@ We can easily break these down into individual views if we need to, but using vi
|
|||
|
||||
Okay, now let's wire up the API URLs. On to `tutorial/urls.py`...
|
||||
|
||||
from django.conf.urls import url, include
|
||||
from django.urls import include, path
|
||||
from rest_framework import routers
|
||||
from tutorial.quickstart import views
|
||||
|
||||
|
@ -122,8 +122,8 @@ Okay, now let's wire up the API URLs. On to `tutorial/urls.py`...
|
|||
# Wire up our API using automatic URL routing.
|
||||
# Additionally, we include login URLs for the browsable API.
|
||||
urlpatterns = [
|
||||
url(r'^', include(router.urls)),
|
||||
url(r'^api-auth/', include('rest_framework.urls', namespace='rest_framework'))
|
||||
path('', include(router.urls)),
|
||||
path('api-auth/', include('rest_framework.urls', namespace='rest_framework'))
|
||||
]
|
||||
|
||||
Because we're using viewsets instead of views, we can automatically generate the URL conf for our API, by simply registering the viewsets with a router class.
|
||||
|
|
0
docs_theme/css/bootstrap-responsive.css
vendored
Executable file → Normal file
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
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
# Pytest for running the tests.
|
||||
pytest==3.6.2
|
||||
pytest-django==3.3.2
|
||||
pytest-cov==2.5.1
|
||||
pytest==4.3.0
|
||||
pytest-django==3.4.8
|
||||
pytest-cov==2.6.1
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -5,6 +5,8 @@ versions of Django/Python, and compatibility wrappers around optional packages.
|
|||
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import sys
|
||||
|
||||
from django.conf import settings
|
||||
from django.core import validators
|
||||
from django.utils import six
|
||||
|
@ -12,17 +14,10 @@ from django.views.generic import View
|
|||
|
||||
try:
|
||||
# Python 3
|
||||
from collections.abc import Mapping # noqa
|
||||
from collections.abc import Mapping, MutableMapping # noqa
|
||||
except ImportError:
|
||||
# Python 2.7
|
||||
from collections import Mapping # noqa
|
||||
|
||||
try:
|
||||
# Python 3
|
||||
import urllib.parse as urlparse # noqa
|
||||
except ImportError:
|
||||
# Python 2.7
|
||||
from urlparse import urlparse # noqa
|
||||
from collections import Mapping, MutableMapping # noqa
|
||||
|
||||
try:
|
||||
from django.urls import ( # noqa
|
||||
|
@ -41,6 +36,11 @@ try:
|
|||
except ImportError:
|
||||
ProhibitNullCharactersValidator = None
|
||||
|
||||
try:
|
||||
from unittest import mock
|
||||
except ImportError:
|
||||
mock = None
|
||||
|
||||
|
||||
def get_original_route(urlpattern):
|
||||
"""
|
||||
|
@ -168,6 +168,10 @@ def is_guardian_installed():
|
|||
"""
|
||||
django-guardian is optional and only imported if in INSTALLED_APPS.
|
||||
"""
|
||||
if six.PY2:
|
||||
# Guardian 1.5.0, for Django 2.2 is NOT compatible with Python 2.7.
|
||||
# Remove when dropping PY2.
|
||||
return False
|
||||
return 'guardian' in settings.INSTALLED_APPS
|
||||
|
||||
|
||||
|
@ -317,3 +321,7 @@ class MinLengthValidator(CustomValidatorMessage, validators.MinLengthValidator):
|
|||
|
||||
class MaxLengthValidator(CustomValidatorMessage, validators.MaxLengthValidator):
|
||||
pass
|
||||
|
||||
|
||||
# Version Constants.
|
||||
PY36 = sys.version_info >= (3, 6)
|
||||
|
|
|
@ -14,6 +14,7 @@ import warnings
|
|||
from django.forms.utils import pretty_name
|
||||
from django.utils import six
|
||||
|
||||
from rest_framework import RemovedInDRF310Warning
|
||||
from rest_framework.views import APIView
|
||||
|
||||
|
||||
|
@ -225,7 +226,7 @@ def detail_route(methods=None, **kwargs):
|
|||
warnings.warn(
|
||||
"`detail_route` is deprecated and will be removed in 3.10 in favor of "
|
||||
"`action`, which accepts a `detail` bool. Use `@action(detail=True)` instead.",
|
||||
DeprecationWarning, stacklevel=2
|
||||
RemovedInDRF310Warning, stacklevel=2
|
||||
)
|
||||
|
||||
def decorator(func):
|
||||
|
@ -243,7 +244,7 @@ def list_route(methods=None, **kwargs):
|
|||
warnings.warn(
|
||||
"`list_route` is deprecated and will be removed in 3.10 in favor of "
|
||||
"`action`, which accepts a `detail` bool. Use `@action(detail=False)` instead.",
|
||||
DeprecationWarning, stacklevel=2
|
||||
RemovedInDRF310Warning, stacklevel=2
|
||||
)
|
||||
|
||||
def decorator(func):
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
from __future__ import unicode_literals
|
||||
|
||||
import collections
|
||||
import copy
|
||||
import datetime
|
||||
import decimal
|
||||
|
@ -33,7 +32,7 @@ from pytz.exceptions import InvalidTimeError
|
|||
|
||||
from rest_framework import ISO_8601
|
||||
from rest_framework.compat import (
|
||||
MaxLengthValidator, MaxValueValidator, MinLengthValidator,
|
||||
Mapping, MaxLengthValidator, MaxValueValidator, MinLengthValidator,
|
||||
MinValueValidator, ProhibitNullCharactersValidator, unicode_repr,
|
||||
unicode_to_repr
|
||||
)
|
||||
|
@ -96,7 +95,7 @@ def get_attribute(instance, attrs):
|
|||
"""
|
||||
for attr in attrs:
|
||||
try:
|
||||
if isinstance(instance, collections.Mapping):
|
||||
if isinstance(instance, Mapping):
|
||||
instance = instance[attr]
|
||||
else:
|
||||
instance = getattr(instance, attr)
|
||||
|
@ -350,7 +349,7 @@ class Field(object):
|
|||
self.default_empty_html = default
|
||||
|
||||
if validators is not None:
|
||||
self.validators = validators[:]
|
||||
self.validators = list(validators)
|
||||
|
||||
# These are set up by `.bind()` when the field is added to a serializer.
|
||||
self.field_name = None
|
||||
|
@ -410,7 +409,7 @@ class Field(object):
|
|||
self._validators = validators
|
||||
|
||||
def get_validators(self):
|
||||
return self.default_validators[:]
|
||||
return list(self.default_validators)
|
||||
|
||||
def get_initial(self):
|
||||
"""
|
||||
|
@ -1487,7 +1486,7 @@ class MultipleChoiceField(ChoiceField):
|
|||
return dictionary.get(self.field_name, empty)
|
||||
|
||||
def to_internal_value(self, data):
|
||||
if isinstance(data, type('')) or not hasattr(data, '__iter__'):
|
||||
if isinstance(data, six.text_type) or not hasattr(data, '__iter__'):
|
||||
self.fail('not_a_list', input_type=type(data).__name__)
|
||||
if not self.allow_empty and len(data) == 0:
|
||||
self.fail('empty')
|
||||
|
@ -1661,7 +1660,7 @@ class ListField(Field):
|
|||
"""
|
||||
if html.is_html_input(data):
|
||||
data = html.parse_html_list(data, default=[])
|
||||
if isinstance(data, type('')) or isinstance(data, collections.Mapping) or not hasattr(data, '__iter__'):
|
||||
if isinstance(data, (six.text_type, Mapping)) or not hasattr(data, '__iter__'):
|
||||
self.fail('not_a_list', input_type=type(data).__name__)
|
||||
if not self.allow_empty and len(data) == 0:
|
||||
self.fail('empty')
|
||||
|
@ -1725,9 +1724,6 @@ class DictField(Field):
|
|||
return self.run_child_validation(data)
|
||||
|
||||
def to_representation(self, value):
|
||||
"""
|
||||
List of object instances -> List of dicts of primitive datatypes.
|
||||
"""
|
||||
return {
|
||||
six.text_type(key): self.child.to_representation(val) if val is not None else None
|
||||
for key, val in value.items()
|
||||
|
|
|
@ -17,6 +17,7 @@ from django.utils import six
|
|||
from django.utils.encoding import force_text
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
from rest_framework import RemovedInDRF310Warning
|
||||
from rest_framework.compat import (
|
||||
coreapi, coreschema, distinct, is_guardian_installed
|
||||
)
|
||||
|
@ -53,6 +54,14 @@ class SearchFilter(BaseFilterBackend):
|
|||
search_title = _('Search')
|
||||
search_description = _('A search term.')
|
||||
|
||||
def get_search_fields(self, view, request):
|
||||
"""
|
||||
Search fields are obtained from the view, but the request is always
|
||||
passed to this method. Sub-classes can override this method to
|
||||
dynamically change the search fields based on request content.
|
||||
"""
|
||||
return getattr(view, 'search_fields', None)
|
||||
|
||||
def get_search_terms(self, request):
|
||||
"""
|
||||
Search terms are set by a ?search=... query parameter,
|
||||
|
@ -77,6 +86,9 @@ class SearchFilter(BaseFilterBackend):
|
|||
opts = queryset.model._meta
|
||||
if search_field[0] in self.lookup_prefixes:
|
||||
search_field = search_field[1:]
|
||||
# Annotated fields do not need to be distinct
|
||||
if isinstance(queryset, models.QuerySet) and search_field in queryset.query.annotations:
|
||||
return False
|
||||
parts = search_field.split(LOOKUP_SEP)
|
||||
for part in parts:
|
||||
field = opts.get_field(part)
|
||||
|
@ -90,7 +102,7 @@ class SearchFilter(BaseFilterBackend):
|
|||
return False
|
||||
|
||||
def filter_queryset(self, request, queryset, view):
|
||||
search_fields = getattr(view, 'search_fields', None)
|
||||
search_fields = self.get_search_fields(view, request)
|
||||
search_terms = self.get_search_terms(request)
|
||||
|
||||
if not search_fields or not search_terms:
|
||||
|
@ -288,7 +300,7 @@ class DjangoObjectPermissionsFilter(BaseFilterBackend):
|
|||
warnings.warn(
|
||||
"`DjangoObjectPermissionsFilter` has been deprecated and moved to "
|
||||
"the 3rd-party django-rest-framework-guardian package.",
|
||||
DeprecationWarning, stacklevel=2
|
||||
RemovedInDRF310Warning, stacklevel=2
|
||||
)
|
||||
assert is_guardian_installed(), 'Using DjangoObjectPermissionsFilter, but django-guardian is not installed'
|
||||
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -24,6 +24,19 @@ class OperationHolderMixin:
|
|||
def __ror__(self, other):
|
||||
return OperandHolder(OR, other, self)
|
||||
|
||||
def __invert__(self):
|
||||
return SingleOperandHolder(NOT, self)
|
||||
|
||||
|
||||
class SingleOperandHolder(OperationHolderMixin):
|
||||
def __init__(self, operator_class, op1_class):
|
||||
self.operator_class = operator_class
|
||||
self.op1_class = op1_class
|
||||
|
||||
def __call__(self, *args, **kwargs):
|
||||
op1 = self.op1_class(*args, **kwargs)
|
||||
return self.operator_class(op1)
|
||||
|
||||
|
||||
class OperandHolder(OperationHolderMixin):
|
||||
def __init__(self, operator_class, op1_class, op2_class):
|
||||
|
@ -44,13 +57,13 @@ class AND:
|
|||
|
||||
def has_permission(self, request, view):
|
||||
return (
|
||||
self.op1.has_permission(request, view) &
|
||||
self.op1.has_permission(request, view) and
|
||||
self.op2.has_permission(request, view)
|
||||
)
|
||||
|
||||
def has_object_permission(self, request, view, obj):
|
||||
return (
|
||||
self.op1.has_object_permission(request, view, obj) &
|
||||
self.op1.has_object_permission(request, view, obj) and
|
||||
self.op2.has_object_permission(request, view, obj)
|
||||
)
|
||||
|
||||
|
@ -62,17 +75,28 @@ class OR:
|
|||
|
||||
def has_permission(self, request, view):
|
||||
return (
|
||||
self.op1.has_permission(request, view) |
|
||||
self.op1.has_permission(request, view) or
|
||||
self.op2.has_permission(request, view)
|
||||
)
|
||||
|
||||
def has_object_permission(self, request, view, obj):
|
||||
return (
|
||||
self.op1.has_object_permission(request, view, obj) |
|
||||
self.op1.has_object_permission(request, view, obj) or
|
||||
self.op2.has_object_permission(request, view, obj)
|
||||
)
|
||||
|
||||
|
||||
class NOT:
|
||||
def __init__(self, op1):
|
||||
self.op1 = op1
|
||||
|
||||
def has_permission(self, request, view):
|
||||
return not self.op1.has_permission(request, view)
|
||||
|
||||
def has_object_permission(self, request, view, obj):
|
||||
return not self.op1.has_object_permission(request, view, obj)
|
||||
|
||||
|
||||
class BasePermissionMetaclass(OperationHolderMixin, type):
|
||||
pass
|
||||
|
||||
|
|
|
@ -518,7 +518,7 @@ class ManyRelatedField(Field):
|
|||
return dictionary.get(self.field_name, empty)
|
||||
|
||||
def to_internal_value(self, data):
|
||||
if isinstance(data, type('')) or not hasattr(data, '__iter__'):
|
||||
if isinstance(data, six.text_type) or not hasattr(data, '__iter__'):
|
||||
self.fail('not_a_list', input_type=type(data).__name__)
|
||||
if not self.allow_empty and len(data) == 0:
|
||||
self.fail('empty')
|
||||
|
|
|
@ -21,11 +21,12 @@ from django.test.client import encode_multipart
|
|||
from django.urls import NoReverseMatch
|
||||
from django.utils import six
|
||||
from django.utils.html import mark_safe
|
||||
from django.utils.six.moves.urllib import parse as urlparse
|
||||
|
||||
from rest_framework import VERSION, exceptions, serializers, status
|
||||
from rest_framework.compat import (
|
||||
INDENT_SEPARATORS, LONG_SEPARATORS, SHORT_SEPARATORS, coreapi, coreschema,
|
||||
pygments_css, urlparse, yaml
|
||||
pygments_css, yaml
|
||||
)
|
||||
from rest_framework.exceptions import ParseError
|
||||
from rest_framework.request import is_form_media_type, override_method
|
||||
|
|
|
@ -25,7 +25,9 @@ from django.urls import NoReverseMatch
|
|||
from django.utils import six
|
||||
from django.utils.deprecation import RenameMethodsBase
|
||||
|
||||
from rest_framework import views
|
||||
from rest_framework import (
|
||||
RemovedInDRF310Warning, RemovedInDRF311Warning, views
|
||||
)
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.reverse import reverse
|
||||
from rest_framework.schemas import SchemaGenerator
|
||||
|
@ -43,7 +45,7 @@ class DynamicDetailRoute(object):
|
|||
"`DynamicDetailRoute` is deprecated and will be removed in 3.10 "
|
||||
"in favor of `DynamicRoute`, which accepts a `detail` boolean. Use "
|
||||
"`DynamicRoute(url, name, True, initkwargs)` instead.",
|
||||
DeprecationWarning, stacklevel=2
|
||||
RemovedInDRF310Warning, stacklevel=2
|
||||
)
|
||||
return DynamicRoute(url, name, True, initkwargs)
|
||||
|
||||
|
@ -54,7 +56,7 @@ class DynamicListRoute(object):
|
|||
"`DynamicListRoute` is deprecated and will be removed in 3.10 in "
|
||||
"favor of `DynamicRoute`, which accepts a `detail` boolean. Use "
|
||||
"`DynamicRoute(url, name, False, initkwargs)` instead.",
|
||||
DeprecationWarning, stacklevel=2
|
||||
RemovedInDRF310Warning, stacklevel=2
|
||||
)
|
||||
return DynamicRoute(url, name, False, initkwargs)
|
||||
|
||||
|
@ -77,7 +79,7 @@ def flatten(list_of_lists):
|
|||
|
||||
class RenameRouterMethods(RenameMethodsBase):
|
||||
renamed_methods = (
|
||||
('get_default_base_name', 'get_default_basename', PendingDeprecationWarning),
|
||||
('get_default_base_name', 'get_default_basename', RemovedInDRF311Warning),
|
||||
)
|
||||
|
||||
|
||||
|
@ -88,7 +90,7 @@ class BaseRouter(six.with_metaclass(RenameRouterMethods)):
|
|||
def register(self, prefix, viewset, basename=None, base_name=None):
|
||||
if base_name is not None:
|
||||
msg = "The `base_name` argument is pending deprecation in favor of `basename`."
|
||||
warnings.warn(msg, PendingDeprecationWarning, 2)
|
||||
warnings.warn(msg, RemovedInDRF311Warning, 2)
|
||||
|
||||
assert not (basename and base_name), (
|
||||
"Do not provide both the `basename` and `base_name` arguments.")
|
||||
|
@ -100,6 +102,10 @@ class BaseRouter(six.with_metaclass(RenameRouterMethods)):
|
|||
basename = self.get_default_basename(viewset)
|
||||
self.registry.append((prefix, viewset, basename))
|
||||
|
||||
# invalidate the urls cache
|
||||
if hasattr(self, '_urls'):
|
||||
del self._urls
|
||||
|
||||
def get_default_basename(self, viewset):
|
||||
"""
|
||||
If `basename` is not specified, attempt to automatically determine
|
||||
|
|
|
@ -51,8 +51,10 @@ def field_to_schema(field):
|
|||
description=description
|
||||
)
|
||||
elif isinstance(field, serializers.ManyRelatedField):
|
||||
related_field_schema = field_to_schema(field.child_relation)
|
||||
|
||||
return coreschema.Array(
|
||||
items=coreschema.String(),
|
||||
items=related_field_schema,
|
||||
title=title,
|
||||
description=description
|
||||
)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -393,7 +393,7 @@ class Serializer(BaseSerializer):
|
|||
# Used by the lazily-evaluated `validators` property.
|
||||
meta = getattr(self, 'Meta', None)
|
||||
validators = getattr(meta, 'validators', None)
|
||||
return validators[:] if validators else []
|
||||
return list(validators) if validators else []
|
||||
|
||||
def get_initial(self):
|
||||
if hasattr(self, 'initial_data'):
|
||||
|
@ -461,8 +461,11 @@ class Serializer(BaseSerializer):
|
|||
"""
|
||||
Add read_only fields with defaults to value before running validators.
|
||||
"""
|
||||
to_validate = self._read_only_defaults()
|
||||
to_validate.update(value)
|
||||
if isinstance(value, dict):
|
||||
to_validate = self._read_only_defaults()
|
||||
to_validate.update(value)
|
||||
else:
|
||||
to_validate = value
|
||||
super(Serializer, self).run_validators(to_validate)
|
||||
|
||||
def to_internal_value(self, data):
|
||||
|
@ -1477,7 +1480,7 @@ class ModelSerializer(Serializer):
|
|||
# If the validators have been declared explicitly then use that.
|
||||
validators = getattr(getattr(self, 'Meta', None), 'validators', None)
|
||||
if validators is not None:
|
||||
return validators[:]
|
||||
return list(validators)
|
||||
|
||||
# Otherwise use the default set of validators.
|
||||
return (
|
||||
|
|
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
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>
|
||||
|
||||
|
@ -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>
|
||||
|
|
|
@ -336,6 +336,12 @@ def urlize_quoted_links(text, trim_url_limit=None, nofollow=True, autoescape=Tru
|
|||
return limit is not None and (len(x) > limit and ('%s...' % x[:max(0, limit - 3)])) or x
|
||||
|
||||
safe_input = isinstance(text, SafeData)
|
||||
|
||||
# Unfortunately, Django built-in cannot be used here, because escaping
|
||||
# is to be performed on words, which have been forcibly coerced to text
|
||||
def conditional_escape(text):
|
||||
return escape(text) if autoescape and not safe_input else text
|
||||
|
||||
words = word_split_re.split(force_text(text))
|
||||
for i, word in enumerate(words):
|
||||
if '.' in word or '@' in word or ':' in word:
|
||||
|
@ -376,21 +382,15 @@ def urlize_quoted_links(text, trim_url_limit=None, nofollow=True, autoescape=Tru
|
|||
# Make link.
|
||||
if url:
|
||||
trimmed = trim_url(middle)
|
||||
if autoescape and not safe_input:
|
||||
lead, trail = escape(lead), escape(trail)
|
||||
url, trimmed = escape(url), escape(trimmed)
|
||||
lead, trail = conditional_escape(lead), conditional_escape(trail)
|
||||
url, trimmed = conditional_escape(url), conditional_escape(trimmed)
|
||||
middle = '<a href="%s"%s>%s</a>' % (url, nofollow_attr, trimmed)
|
||||
words[i] = mark_safe('%s%s%s' % (lead, middle, trail))
|
||||
words[i] = '%s%s%s' % (lead, middle, trail)
|
||||
else:
|
||||
if safe_input:
|
||||
words[i] = mark_safe(word)
|
||||
elif autoescape:
|
||||
words[i] = escape(word)
|
||||
elif safe_input:
|
||||
words[i] = mark_safe(word)
|
||||
elif autoescape:
|
||||
words[i] = escape(word)
|
||||
return ''.join(words)
|
||||
words[i] = conditional_escape(word)
|
||||
else:
|
||||
words[i] = conditional_escape(word)
|
||||
return mark_safe(''.join(words))
|
||||
|
||||
|
||||
@register.filter
|
||||
|
|
|
@ -106,8 +106,7 @@ def get_field_kwargs(field_name, model_field):
|
|||
if model_field.null and not isinstance(model_field, models.NullBooleanField):
|
||||
kwargs['allow_null'] = True
|
||||
|
||||
if model_field.blank and (isinstance(model_field, models.CharField) or
|
||||
isinstance(model_field, models.TextField)):
|
||||
if model_field.blank and (isinstance(model_field, (models.CharField, models.TextField))):
|
||||
kwargs['allow_blank'] = True
|
||||
|
||||
if isinstance(model_field, models.FilePathField):
|
||||
|
@ -193,9 +192,7 @@ def get_field_kwargs(field_name, model_field):
|
|||
# Ensure that max_length is passed explicitly as a keyword arg,
|
||||
# rather than as a validator.
|
||||
max_length = getattr(model_field, 'max_length', None)
|
||||
if max_length is not None and (isinstance(model_field, models.CharField) or
|
||||
isinstance(model_field, models.TextField) or
|
||||
isinstance(model_field, models.FileField)):
|
||||
if max_length is not None and (isinstance(model_field, (models.CharField, models.TextField, models.FileField))):
|
||||
kwargs['max_length'] = max_length
|
||||
validator_kwarg = [
|
||||
validator for validator in validator_kwarg
|
||||
|
@ -249,6 +246,12 @@ def get_relation_kwargs(field_name, relation_info):
|
|||
if to_field:
|
||||
kwargs['to_field'] = to_field
|
||||
|
||||
limit_choices_to = model_field and model_field.get_limit_choices_to()
|
||||
if limit_choices_to:
|
||||
if not isinstance(limit_choices_to, models.Q):
|
||||
limit_choices_to = models.Q(**limit_choices_to)
|
||||
kwargs['queryset'] = kwargs['queryset'].filter(limit_choices_to)
|
||||
|
||||
if has_through_model:
|
||||
kwargs['read_only'] = True
|
||||
kwargs.pop('queryset', None)
|
||||
|
|
|
@ -1,11 +1,10 @@
|
|||
from __future__ import unicode_literals
|
||||
|
||||
import collections
|
||||
from collections import OrderedDict
|
||||
|
||||
from django.utils.encoding import force_text
|
||||
|
||||
from rest_framework.compat import unicode_to_repr
|
||||
from rest_framework.compat import MutableMapping, unicode_to_repr
|
||||
from rest_framework.utils import json
|
||||
|
||||
|
||||
|
@ -130,7 +129,7 @@ class NestedBoundField(BoundField):
|
|||
return self.__class__(self._field, values, self.errors, self._prefix)
|
||||
|
||||
|
||||
class BindingDict(collections.MutableMapping):
|
||||
class BindingDict(MutableMapping):
|
||||
"""
|
||||
This dict-like object is used to store fields on a serializer.
|
||||
|
||||
|
|
|
@ -75,6 +75,9 @@ class URLPathVersioning(BaseVersioning):
|
|||
|
||||
def determine_version(self, request, *args, **kwargs):
|
||||
version = kwargs.get(self.version_param, self.default_version)
|
||||
if version is None:
|
||||
version = self.default_version
|
||||
|
||||
if not self.is_allowed_version(version):
|
||||
raise exceptions.NotFound(self.invalid_version_message)
|
||||
return version
|
||||
|
|
|
@ -463,7 +463,7 @@ class APIView(View):
|
|||
renderer_format = getattr(request.accepted_renderer, 'format')
|
||||
use_plaintext_traceback = renderer_format not in ('html', 'api', 'admin')
|
||||
request.force_plaintext_errors(use_plaintext_traceback)
|
||||
raise
|
||||
raise exc
|
||||
|
||||
# Note: Views are made CSRF exempt from within `as_view` as to prevent
|
||||
# accidental removal of this exemption in cases where `dispatch` needs to
|
||||
|
|
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)
|
|
@ -8,7 +8,6 @@ import pytest
|
|||
from django.conf import settings
|
||||
from django.conf.urls import include, url
|
||||
from django.contrib.auth.models import User
|
||||
from django.db import models
|
||||
from django.http import HttpResponse
|
||||
from django.test import TestCase, override_settings
|
||||
from django.utils import six
|
||||
|
@ -26,14 +25,11 @@ from rest_framework.response import Response
|
|||
from rest_framework.test import APIClient, APIRequestFactory
|
||||
from rest_framework.views import APIView
|
||||
|
||||
from .models import CustomToken
|
||||
|
||||
factory = APIRequestFactory()
|
||||
|
||||
|
||||
class CustomToken(models.Model):
|
||||
key = models.CharField(max_length=40, primary_key=True)
|
||||
user = models.OneToOneField(User, on_delete=models.CASCADE)
|
||||
|
||||
|
||||
class CustomTokenAuthentication(TokenAuthentication):
|
||||
model = CustomToken
|
||||
|
||||
|
@ -87,7 +83,7 @@ urlpatterns = [
|
|||
]
|
||||
|
||||
|
||||
@override_settings(ROOT_URLCONF='tests.test_authentication')
|
||||
@override_settings(ROOT_URLCONF=__name__)
|
||||
class BasicAuthTests(TestCase):
|
||||
"""Basic authentication"""
|
||||
def setUp(self):
|
||||
|
@ -169,7 +165,7 @@ class BasicAuthTests(TestCase):
|
|||
assert response.status_code == status.HTTP_401_UNAUTHORIZED
|
||||
|
||||
|
||||
@override_settings(ROOT_URLCONF='tests.test_authentication')
|
||||
@override_settings(ROOT_URLCONF=__name__)
|
||||
class SessionAuthTests(TestCase):
|
||||
"""User session authentication"""
|
||||
def setUp(self):
|
||||
|
@ -370,7 +366,7 @@ class BaseTokenAuthTests(object):
|
|||
assert response.status_code == status.HTTP_401_UNAUTHORIZED
|
||||
|
||||
|
||||
@override_settings(ROOT_URLCONF='tests.test_authentication')
|
||||
@override_settings(ROOT_URLCONF=__name__)
|
||||
class TokenAuthTests(BaseTokenAuthTests, TestCase):
|
||||
model = Token
|
||||
path = '/token/'
|
||||
|
@ -429,13 +425,13 @@ class TokenAuthTests(BaseTokenAuthTests, TestCase):
|
|||
assert response.data['token'] == self.key
|
||||
|
||||
|
||||
@override_settings(ROOT_URLCONF='tests.test_authentication')
|
||||
@override_settings(ROOT_URLCONF=__name__)
|
||||
class CustomTokenAuthTests(BaseTokenAuthTests, TestCase):
|
||||
model = CustomToken
|
||||
path = '/customtoken/'
|
||||
|
||||
|
||||
@override_settings(ROOT_URLCONF='tests.test_authentication')
|
||||
@override_settings(ROOT_URLCONF=__name__)
|
||||
class CustomKeywordTokenAuthTests(BaseTokenAuthTests, TestCase):
|
||||
model = Token
|
||||
path = '/customkeywordtoken/'
|
||||
|
@ -549,7 +545,7 @@ class BasicAuthenticationUnitTests(TestCase):
|
|||
authentication.authenticate = old_authenticate
|
||||
|
||||
|
||||
@override_settings(ROOT_URLCONF='tests.test_authentication',
|
||||
@override_settings(ROOT_URLCONF=__name__,
|
||||
AUTHENTICATION_BACKENDS=('django.contrib.auth.backends.RemoteUserBackend',))
|
||||
class RemoteUserAuthenticationUnitTests(TestCase):
|
||||
def setUp(self):
|
|
@ -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
|
|
@ -1,52 +1,10 @@
|
|||
from __future__ import unicode_literals
|
||||
|
||||
from django.contrib.contenttypes.fields import (
|
||||
GenericForeignKey, GenericRelation
|
||||
)
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.db import models
|
||||
from django.test import TestCase
|
||||
from django.utils.encoding import python_2_unicode_compatible
|
||||
|
||||
from rest_framework import serializers
|
||||
|
||||
|
||||
@python_2_unicode_compatible
|
||||
class Tag(models.Model):
|
||||
"""
|
||||
Tags have a descriptive slug, and are attached to an arbitrary object.
|
||||
"""
|
||||
tag = models.SlugField()
|
||||
content_type = models.ForeignKey(ContentType, on_delete=models.CASCADE)
|
||||
object_id = models.PositiveIntegerField()
|
||||
tagged_item = GenericForeignKey('content_type', 'object_id')
|
||||
|
||||
def __str__(self):
|
||||
return self.tag
|
||||
|
||||
|
||||
@python_2_unicode_compatible
|
||||
class Bookmark(models.Model):
|
||||
"""
|
||||
A URL bookmark that may have multiple tags attached.
|
||||
"""
|
||||
url = models.URLField()
|
||||
tags = GenericRelation(Tag)
|
||||
|
||||
def __str__(self):
|
||||
return 'Bookmark: %s' % self.url
|
||||
|
||||
|
||||
@python_2_unicode_compatible
|
||||
class Note(models.Model):
|
||||
"""
|
||||
A textual note that may have multiple tags attached.
|
||||
"""
|
||||
text = models.TextField()
|
||||
tags = GenericRelation(Tag)
|
||||
|
||||
def __str__(self):
|
||||
return 'Note: %s' % self.text
|
||||
from .models import Bookmark, Note, Tag
|
||||
|
||||
|
||||
class TestGenericRelations(TestCase):
|
|
@ -52,6 +52,20 @@ class ForeignKeySource(RESTFrameworkModel):
|
|||
on_delete=models.CASCADE)
|
||||
|
||||
|
||||
class ForeignKeySourceWithLimitedChoices(RESTFrameworkModel):
|
||||
target = models.ForeignKey(ForeignKeyTarget, help_text='Target',
|
||||
verbose_name='Target',
|
||||
limit_choices_to={"name__startswith": "limited-"},
|
||||
on_delete=models.CASCADE)
|
||||
|
||||
|
||||
class ForeignKeySourceWithQLimitedChoices(RESTFrameworkModel):
|
||||
target = models.ForeignKey(ForeignKeyTarget, help_text='Target',
|
||||
verbose_name='Target',
|
||||
limit_choices_to=models.Q(name__startswith="limited-"),
|
||||
on_delete=models.CASCADE)
|
||||
|
||||
|
||||
# Nullable ForeignKey
|
||||
class NullableForeignKeySource(RESTFrameworkModel):
|
||||
name = models.CharField(max_length=100)
|
||||
|
|
|
@ -3,7 +3,7 @@ from __future__ import unicode_literals
|
|||
import pytest
|
||||
from django.test import TestCase
|
||||
|
||||
from rest_framework import status
|
||||
from rest_framework import RemovedInDRF310Warning, status
|
||||
from rest_framework.authentication import BasicAuthentication
|
||||
from rest_framework.decorators import (
|
||||
action, api_view, authentication_classes, detail_route, list_route,
|
||||
|
@ -290,7 +290,7 @@ class ActionDecoratorTestCase(TestCase):
|
|||
raise NotImplementedError
|
||||
|
||||
def test_detail_route_deprecation(self):
|
||||
with pytest.warns(DeprecationWarning) as record:
|
||||
with pytest.warns(RemovedInDRF310Warning) as record:
|
||||
@detail_route()
|
||||
def view(request):
|
||||
raise NotImplementedError
|
||||
|
@ -303,7 +303,7 @@ class ActionDecoratorTestCase(TestCase):
|
|||
)
|
||||
|
||||
def test_list_route_deprecation(self):
|
||||
with pytest.warns(DeprecationWarning) as record:
|
||||
with pytest.warns(RemovedInDRF310Warning) as record:
|
||||
@list_route()
|
||||
def view(request):
|
||||
raise NotImplementedError
|
||||
|
@ -317,7 +317,7 @@ class ActionDecoratorTestCase(TestCase):
|
|||
|
||||
def test_route_url_name_from_path(self):
|
||||
# pre-3.8 behavior was to base the `url_name` off of the `url_path`
|
||||
with pytest.warns(DeprecationWarning):
|
||||
with pytest.warns(RemovedInDRF310Warning):
|
||||
@list_route(url_path='foo_bar')
|
||||
def view(request):
|
||||
raise NotImplementedError
|
||||
|
|
|
@ -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):
|
||||
"""
|
||||
|
|
|
@ -5,6 +5,7 @@ import datetime
|
|||
import pytest
|
||||
from django.core.exceptions import ImproperlyConfigured
|
||||
from django.db import models
|
||||
from django.db.models.functions import Concat, Upper
|
||||
from django.test import TestCase
|
||||
from django.test.utils import override_settings
|
||||
from django.utils.six.moves import reload_module
|
||||
|
@ -156,6 +157,31 @@ class SearchFilterTests(TestCase):
|
|||
|
||||
reload_module(filters)
|
||||
|
||||
def test_search_with_filter_subclass(self):
|
||||
class CustomSearchFilter(filters.SearchFilter):
|
||||
# Filter that dynamically changes search fields
|
||||
def get_search_fields(self, view, request):
|
||||
if request.query_params.get('title_only'):
|
||||
return ('$title',)
|
||||
return super(CustomSearchFilter, self).get_search_fields(view, request)
|
||||
|
||||
class SearchListView(generics.ListAPIView):
|
||||
queryset = SearchFilterModel.objects.all()
|
||||
serializer_class = SearchFilterSerializer
|
||||
filter_backends = (CustomSearchFilter,)
|
||||
search_fields = ('$title', '$text')
|
||||
|
||||
view = SearchListView.as_view()
|
||||
request = factory.get('/', {'search': r'^\w{3}$'})
|
||||
response = view(request)
|
||||
assert len(response.data) == 10
|
||||
|
||||
request = factory.get('/', {'search': r'^\w{3}$', 'title_only': 'true'})
|
||||
response = view(request)
|
||||
assert response.data == [
|
||||
{'id': 3, 'title': 'zzz', 'text': 'cde'}
|
||||
]
|
||||
|
||||
|
||||
class AttributeModel(models.Model):
|
||||
label = models.CharField(max_length=32)
|
||||
|
@ -221,7 +247,7 @@ class SearchFilterM2MTests(TestCase):
|
|||
# ...
|
||||
for idx in range(3):
|
||||
label = 'w' * (idx + 1)
|
||||
AttributeModel(label=label)
|
||||
AttributeModel.objects.create(label=label)
|
||||
|
||||
for idx in range(10):
|
||||
title = 'z' * (idx + 1)
|
||||
|
@ -304,6 +330,38 @@ class SearchFilterToManyTests(TestCase):
|
|||
assert len(response.data) == 1
|
||||
|
||||
|
||||
class SearchFilterAnnotatedSerializer(serializers.ModelSerializer):
|
||||
title_text = serializers.CharField()
|
||||
|
||||
class Meta:
|
||||
model = SearchFilterModel
|
||||
fields = ('title', 'text', 'title_text')
|
||||
|
||||
|
||||
class SearchFilterAnnotatedFieldTests(TestCase):
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
SearchFilterModel.objects.create(title='abc', text='def')
|
||||
SearchFilterModel.objects.create(title='ghi', text='jkl')
|
||||
|
||||
def test_search_in_annotated_field(self):
|
||||
class SearchListView(generics.ListAPIView):
|
||||
queryset = SearchFilterModel.objects.annotate(
|
||||
title_text=Upper(
|
||||
Concat(models.F('title'), models.F('text'))
|
||||
)
|
||||
).all()
|
||||
serializer_class = SearchFilterAnnotatedSerializer
|
||||
filter_backends = (filters.SearchFilter,)
|
||||
search_fields = ('title_text',)
|
||||
|
||||
view = SearchListView.as_view()
|
||||
request = factory.get('/', {'search': 'ABCDEF'})
|
||||
response = view(request)
|
||||
assert len(response.data) == 1
|
||||
assert response.data[0]['title_text'] == 'ABCDEF'
|
||||
|
||||
|
||||
class OrderingFilterModel(models.Model):
|
||||
title = models.CharField(max_length=20, verbose_name='verbose title')
|
||||
text = models.CharField(max_length=100)
|
||||
|
|
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())
|
|
@ -5,6 +5,7 @@ import pytest
|
|||
from django.core.paginator import Paginator as DjangoPaginator
|
||||
from django.db import models
|
||||
from django.test import TestCase
|
||||
from django.utils import six
|
||||
|
||||
from rest_framework import (
|
||||
exceptions, filters, generics, pagination, serializers, status
|
||||
|
@ -207,7 +208,7 @@ class TestPageNumberPagination:
|
|||
]
|
||||
}
|
||||
assert self.pagination.display_page_controls
|
||||
assert isinstance(self.pagination.to_html(), type(''))
|
||||
assert isinstance(self.pagination.to_html(), six.text_type)
|
||||
|
||||
def test_second_page(self):
|
||||
request = Request(factory.get('/', {'page': 2}))
|
||||
|
@ -313,7 +314,7 @@ class TestPageNumberPaginationOverride:
|
|||
]
|
||||
}
|
||||
assert not self.pagination.display_page_controls
|
||||
assert isinstance(self.pagination.to_html(), type(''))
|
||||
assert isinstance(self.pagination.to_html(), six.text_type)
|
||||
|
||||
def test_invalid_page(self):
|
||||
request = Request(factory.get('/', {'page': 'invalid'}))
|
||||
|
@ -368,7 +369,7 @@ class TestLimitOffset:
|
|||
]
|
||||
}
|
||||
assert self.pagination.display_page_controls
|
||||
assert isinstance(self.pagination.to_html(), type(''))
|
||||
assert isinstance(self.pagination.to_html(), six.text_type)
|
||||
|
||||
def test_pagination_not_applied_if_limit_or_default_limit_not_set(self):
|
||||
class MockPagination(pagination.LimitOffsetPagination):
|
||||
|
@ -631,7 +632,7 @@ class CursorPaginationTestsMixin:
|
|||
assert current == [1, 1, 1, 1, 1]
|
||||
assert next == [1, 2, 3, 4, 4]
|
||||
|
||||
assert isinstance(self.pagination.to_html(), type(''))
|
||||
assert isinstance(self.pagination.to_html(), six.text_type)
|
||||
|
||||
def test_cursor_pagination_with_page_size(self):
|
||||
(previous, current, next, previous_url, next_url) = self.get_pages('/?page_size=20')
|
||||
|
|
|
@ -5,16 +5,17 @@ import unittest
|
|||
import warnings
|
||||
|
||||
import django
|
||||
import pytest
|
||||
from django.contrib.auth.models import AnonymousUser, Group, Permission, User
|
||||
from django.db import models
|
||||
from django.test import TestCase
|
||||
from django.urls import ResolverMatch
|
||||
|
||||
from rest_framework import (
|
||||
HTTP_HEADER_ENCODING, authentication, generics, permissions, serializers,
|
||||
status, views
|
||||
HTTP_HEADER_ENCODING, RemovedInDRF310Warning, authentication, generics,
|
||||
permissions, serializers, status, views
|
||||
)
|
||||
from rest_framework.compat import is_guardian_installed
|
||||
from rest_framework.compat import PY36, is_guardian_installed, mock
|
||||
from rest_framework.filters import DjangoObjectPermissionsFilter
|
||||
from rest_framework.routers import DefaultRouter
|
||||
from rest_framework.test import APIRequestFactory
|
||||
|
@ -426,7 +427,7 @@ class ObjectPermissionsIntegrationTests(TestCase):
|
|||
message = ("`DjangoObjectPermissionsFilter` has been deprecated and moved "
|
||||
"to the 3rd-party django-rest-framework-guardian package.")
|
||||
self.assertEqual(len(w), 1)
|
||||
self.assertIs(w[-1].category, DeprecationWarning)
|
||||
self.assertIs(w[-1].category, RemovedInDRF310Warning)
|
||||
self.assertEqual(str(w[-1].message), message)
|
||||
|
||||
def test_can_read_list_permissions(self):
|
||||
|
@ -579,7 +580,19 @@ class PermissionsCompositionTests(TestCase):
|
|||
composed_perm = permissions.IsAuthenticated | permissions.AllowAny
|
||||
assert composed_perm().has_permission(request, None) is True
|
||||
|
||||
def test_several_levels(self):
|
||||
def test_not_false(self):
|
||||
request = factory.get('/1', format='json')
|
||||
request.user = AnonymousUser()
|
||||
composed_perm = ~permissions.IsAuthenticated
|
||||
assert composed_perm().has_permission(request, None) is True
|
||||
|
||||
def test_not_true(self):
|
||||
request = factory.get('/1', format='json')
|
||||
request.user = self.user
|
||||
composed_perm = ~permissions.AllowAny
|
||||
assert composed_perm().has_permission(request, None) is False
|
||||
|
||||
def test_several_levels_without_negation(self):
|
||||
request = factory.get('/1', format='json')
|
||||
request.user = self.user
|
||||
composed_perm = (
|
||||
|
@ -590,6 +603,17 @@ class PermissionsCompositionTests(TestCase):
|
|||
)
|
||||
assert composed_perm().has_permission(request, None) is True
|
||||
|
||||
def test_several_levels_and_precedence_with_negation(self):
|
||||
request = factory.get('/1', format='json')
|
||||
request.user = self.user
|
||||
composed_perm = (
|
||||
permissions.IsAuthenticated &
|
||||
~ permissions.IsAdminUser &
|
||||
permissions.IsAuthenticated &
|
||||
~(permissions.IsAdminUser & permissions.IsAdminUser)
|
||||
)
|
||||
assert composed_perm().has_permission(request, None) is True
|
||||
|
||||
def test_several_levels_and_precedence(self):
|
||||
request = factory.get('/1', format='json')
|
||||
request.user = self.user
|
||||
|
@ -600,3 +624,87 @@ class PermissionsCompositionTests(TestCase):
|
|||
permissions.IsAuthenticated
|
||||
)
|
||||
assert composed_perm().has_permission(request, None) is True
|
||||
|
||||
@pytest.mark.skipif(not PY36, reason="assert_called_once() not available")
|
||||
def test_or_lazyness(self):
|
||||
request = factory.get('/1', format='json')
|
||||
request.user = AnonymousUser()
|
||||
|
||||
with mock.patch.object(permissions.AllowAny, 'has_permission', return_value=True) as mock_allow:
|
||||
with mock.patch.object(permissions.IsAuthenticated, 'has_permission', return_value=False) as mock_deny:
|
||||
composed_perm = (permissions.AllowAny | permissions.IsAuthenticated)
|
||||
hasperm = composed_perm().has_permission(request, None)
|
||||
self.assertIs(hasperm, True)
|
||||
mock_allow.assert_called_once()
|
||||
mock_deny.assert_not_called()
|
||||
|
||||
with mock.patch.object(permissions.AllowAny, 'has_permission', return_value=True) as mock_allow:
|
||||
with mock.patch.object(permissions.IsAuthenticated, 'has_permission', return_value=False) as mock_deny:
|
||||
composed_perm = (permissions.IsAuthenticated | permissions.AllowAny)
|
||||
hasperm = composed_perm().has_permission(request, None)
|
||||
self.assertIs(hasperm, True)
|
||||
mock_deny.assert_called_once()
|
||||
mock_allow.assert_called_once()
|
||||
|
||||
@pytest.mark.skipif(not PY36, reason="assert_called_once() not available")
|
||||
def test_object_or_lazyness(self):
|
||||
request = factory.get('/1', format='json')
|
||||
request.user = AnonymousUser()
|
||||
|
||||
with mock.patch.object(permissions.AllowAny, 'has_object_permission', return_value=True) as mock_allow:
|
||||
with mock.patch.object(permissions.IsAuthenticated, 'has_object_permission', return_value=False) as mock_deny:
|
||||
composed_perm = (permissions.AllowAny | permissions.IsAuthenticated)
|
||||
hasperm = composed_perm().has_object_permission(request, None, None)
|
||||
self.assertIs(hasperm, True)
|
||||
mock_allow.assert_called_once()
|
||||
mock_deny.assert_not_called()
|
||||
|
||||
with mock.patch.object(permissions.AllowAny, 'has_object_permission', return_value=True) as mock_allow:
|
||||
with mock.patch.object(permissions.IsAuthenticated, 'has_object_permission', return_value=False) as mock_deny:
|
||||
composed_perm = (permissions.IsAuthenticated | permissions.AllowAny)
|
||||
hasperm = composed_perm().has_object_permission(request, None, None)
|
||||
self.assertIs(hasperm, True)
|
||||
mock_deny.assert_called_once()
|
||||
mock_allow.assert_called_once()
|
||||
|
||||
@pytest.mark.skipif(not PY36, reason="assert_called_once() not available")
|
||||
def test_and_lazyness(self):
|
||||
request = factory.get('/1', format='json')
|
||||
request.user = AnonymousUser()
|
||||
|
||||
with mock.patch.object(permissions.AllowAny, 'has_permission', return_value=True) as mock_allow:
|
||||
with mock.patch.object(permissions.IsAuthenticated, 'has_permission', return_value=False) as mock_deny:
|
||||
composed_perm = (permissions.AllowAny & permissions.IsAuthenticated)
|
||||
hasperm = composed_perm().has_permission(request, None)
|
||||
self.assertIs(hasperm, False)
|
||||
mock_allow.assert_called_once()
|
||||
mock_deny.assert_called_once()
|
||||
|
||||
with mock.patch.object(permissions.AllowAny, 'has_permission', return_value=True) as mock_allow:
|
||||
with mock.patch.object(permissions.IsAuthenticated, 'has_permission', return_value=False) as mock_deny:
|
||||
composed_perm = (permissions.IsAuthenticated & permissions.AllowAny)
|
||||
hasperm = composed_perm().has_permission(request, None)
|
||||
self.assertIs(hasperm, False)
|
||||
mock_allow.assert_not_called()
|
||||
mock_deny.assert_called_once()
|
||||
|
||||
@pytest.mark.skipif(not PY36, reason="assert_called_once() not available")
|
||||
def test_object_and_lazyness(self):
|
||||
request = factory.get('/1', format='json')
|
||||
request.user = AnonymousUser()
|
||||
|
||||
with mock.patch.object(permissions.AllowAny, 'has_object_permission', return_value=True) as mock_allow:
|
||||
with mock.patch.object(permissions.IsAuthenticated, 'has_object_permission', return_value=False) as mock_deny:
|
||||
composed_perm = (permissions.AllowAny & permissions.IsAuthenticated)
|
||||
hasperm = composed_perm().has_object_permission(request, None, None)
|
||||
self.assertIs(hasperm, False)
|
||||
mock_allow.assert_called_once()
|
||||
mock_deny.assert_called_once()
|
||||
|
||||
with mock.patch.object(permissions.AllowAny, 'has_object_permission', return_value=True) as mock_allow:
|
||||
with mock.patch.object(permissions.IsAuthenticated, 'has_object_permission', return_value=False) as mock_deny:
|
||||
composed_perm = (permissions.IsAuthenticated & permissions.AllowAny)
|
||||
hasperm = composed_perm().has_object_permission(request, None, None)
|
||||
self.assertIs(hasperm, False)
|
||||
mock_allow.assert_not_called()
|
||||
mock_deny.assert_called_once()
|
||||
|
|
|
@ -5,8 +5,9 @@ from django.utils import six
|
|||
|
||||
from rest_framework import serializers
|
||||
from tests.models import (
|
||||
ForeignKeySource, ForeignKeyTarget, ManyToManySource, ManyToManyTarget,
|
||||
NullableForeignKeySource, NullableOneToOneSource,
|
||||
ForeignKeySource, ForeignKeySourceWithLimitedChoices,
|
||||
ForeignKeySourceWithQLimitedChoices, ForeignKeyTarget, ManyToManySource,
|
||||
ManyToManyTarget, NullableForeignKeySource, NullableOneToOneSource,
|
||||
NullableUUIDForeignKeySource, OneToOnePKSource, OneToOneTarget,
|
||||
UUIDForeignKeyTarget
|
||||
)
|
||||
|
@ -38,6 +39,12 @@ class ForeignKeySourceSerializer(serializers.ModelSerializer):
|
|||
fields = ('id', 'name', 'target')
|
||||
|
||||
|
||||
class ForeignKeySourceWithLimitedChoicesSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = ForeignKeySourceWithLimitedChoices
|
||||
fields = ("id", "target")
|
||||
|
||||
|
||||
# Nullable ForeignKey
|
||||
class NullableForeignKeySourceSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
|
@ -360,6 +367,30 @@ class PKForeignKeyTests(TestCase):
|
|||
serializer.is_valid(raise_exception=True)
|
||||
assert 'target' not in serializer.validated_data
|
||||
|
||||
def test_queryset_size_without_limited_choices(self):
|
||||
limited_target = ForeignKeyTarget(name="limited-target")
|
||||
limited_target.save()
|
||||
queryset = ForeignKeySourceSerializer().fields["target"].get_queryset()
|
||||
assert len(queryset) == 3
|
||||
|
||||
def test_queryset_size_with_limited_choices(self):
|
||||
limited_target = ForeignKeyTarget(name="limited-target")
|
||||
limited_target.save()
|
||||
queryset = ForeignKeySourceWithLimitedChoicesSerializer().fields["target"].get_queryset()
|
||||
assert len(queryset) == 1
|
||||
|
||||
def test_queryset_size_with_Q_limited_choices(self):
|
||||
limited_target = ForeignKeyTarget(name="limited-target")
|
||||
limited_target.save()
|
||||
|
||||
class QLimitedChoicesSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = ForeignKeySourceWithQLimitedChoices
|
||||
fields = ("id", "target")
|
||||
|
||||
queryset = QLimitedChoicesSerializer().fields["target"].get_queryset()
|
||||
assert len(queryset) == 1
|
||||
|
||||
|
||||
class PKNullableForeignKeyTests(TestCase):
|
||||
def setUp(self):
|
||||
|
|
|
@ -2,20 +2,21 @@
|
|||
from __future__ import unicode_literals
|
||||
|
||||
import re
|
||||
from collections import MutableMapping, OrderedDict
|
||||
from collections import OrderedDict
|
||||
|
||||
import pytest
|
||||
from django.conf.urls import include, url
|
||||
from django.core.cache import cache
|
||||
from django.db import models
|
||||
from django.http.request import HttpRequest
|
||||
from django.template import loader
|
||||
from django.test import TestCase, override_settings
|
||||
from django.utils import six
|
||||
from django.utils.safestring import SafeText
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
from rest_framework import permissions, serializers, status
|
||||
from rest_framework.compat import coreapi
|
||||
from rest_framework.compat import MutableMapping, coreapi
|
||||
from rest_framework.decorators import action
|
||||
from rest_framework.renderers import (
|
||||
AdminRenderer, BaseRenderer, BrowsableAPIRenderer, DocumentationRenderer,
|
||||
|
@ -635,7 +636,9 @@ class BrowsableAPIRendererTests(URLPatternsTestCase):
|
|||
raise NotImplementedError
|
||||
|
||||
router = SimpleRouter()
|
||||
router.register(r'examples/', ExampleViewSet, basename='example')
|
||||
|
||||
router.register('examples', ExampleViewSet, basename='example')
|
||||
|
||||
urlpatterns = [url(r'^api/', include(router.urls))]
|
||||
|
||||
def setUp(self):
|
||||
|
@ -827,6 +830,16 @@ class TestDocumentationRenderer(TestCase):
|
|||
html = renderer.render(document, accepted_media_type="text/html", renderer_context={"request": request})
|
||||
assert '<h1>Data Endpoint API</h1>' in html
|
||||
|
||||
def test_shell_code_example_rendering(self):
|
||||
template = loader.get_template('rest_framework/docs/langs/shell.html')
|
||||
context = {
|
||||
'document': coreapi.Document(url='https://api.example.org/'),
|
||||
'link_key': 'testcases > list',
|
||||
'link': coreapi.Link(url='/data/', action='get', fields=[]),
|
||||
}
|
||||
html = template.render(context)
|
||||
assert 'testcases list' in html
|
||||
|
||||
|
||||
@pytest.mark.skipif(not coreapi, reason='coreapi is not installed')
|
||||
class TestSchemaJSRenderer(TestCase):
|
||||
|
|
|
@ -10,7 +10,9 @@ from django.db import models
|
|||
from django.test import TestCase, override_settings
|
||||
from django.urls import resolve, reverse
|
||||
|
||||
from rest_framework import permissions, serializers, viewsets
|
||||
from rest_framework import (
|
||||
RemovedInDRF311Warning, permissions, serializers, viewsets
|
||||
)
|
||||
from rest_framework.compat import get_regex_pattern
|
||||
from rest_framework.decorators import action
|
||||
from rest_framework.response import Response
|
||||
|
@ -119,6 +121,7 @@ class BasicViewSet(viewsets.ViewSet):
|
|||
|
||||
class TestSimpleRouter(URLPatternsTestCase, TestCase):
|
||||
router = SimpleRouter()
|
||||
|
||||
router.register(r'basics/', BasicViewSet, basename='basic')
|
||||
|
||||
urlpatterns = [
|
||||
|
@ -158,6 +161,12 @@ class TestSimpleRouter(URLPatternsTestCase, TestCase):
|
|||
response = self.client.delete(reverse('basic-action3', args=[1]))
|
||||
assert response.data == {'delete': '1'}
|
||||
|
||||
def test_register_after_accessing_urls(self):
|
||||
self.router.register(r'notes', NoteViewSet)
|
||||
assert len(self.router.urls) == 2 # list and detail
|
||||
self.router.register(r'notes_bis', NoteViewSet)
|
||||
assert len(self.router.urls) == 4
|
||||
|
||||
|
||||
class TestRootView(URLPatternsTestCase, TestCase):
|
||||
urlpatterns = [
|
||||
|
@ -502,7 +511,7 @@ class TestBaseNameRename(TestCase):
|
|||
def test_base_name_argument_deprecation(self):
|
||||
router = SimpleRouter()
|
||||
|
||||
with pytest.warns(PendingDeprecationWarning) as w:
|
||||
with pytest.warns(RemovedInDRF311Warning) as w:
|
||||
warnings.simplefilter('always')
|
||||
router.register('mock', MockViewSet, base_name='mock')
|
||||
|
||||
|
@ -529,7 +538,7 @@ class TestBaseNameRename(TestCase):
|
|||
msg = "`CustomRouter.get_default_base_name` method should be renamed `get_default_basename`."
|
||||
|
||||
# Class definition should raise a warning
|
||||
with pytest.warns(PendingDeprecationWarning) as w:
|
||||
with pytest.warns(RemovedInDRF311Warning) as w:
|
||||
warnings.simplefilter('always')
|
||||
|
||||
class CustomRouter(SimpleRouter):
|
||||
|
|
|
@ -24,7 +24,7 @@ from rest_framework.utils import formatting
|
|||
from rest_framework.views import APIView
|
||||
from rest_framework.viewsets import GenericViewSet, ModelViewSet
|
||||
|
||||
from .models import BasicModel, ForeignKeySource
|
||||
from .models import BasicModel, ForeignKeySource, ManyToManySource
|
||||
|
||||
factory = APIRequestFactory()
|
||||
|
||||
|
@ -701,6 +701,51 @@ class TestSchemaGeneratorWithForeignKey(TestCase):
|
|||
assert schema == expected
|
||||
|
||||
|
||||
class ManyToManySourceSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = ManyToManySource
|
||||
fields = ('id', 'name', 'targets')
|
||||
|
||||
|
||||
class ManyToManySourceView(generics.CreateAPIView):
|
||||
queryset = ManyToManySource.objects.all()
|
||||
serializer_class = ManyToManySourceSerializer
|
||||
|
||||
|
||||
@unittest.skipUnless(coreapi, 'coreapi is not installed')
|
||||
class TestSchemaGeneratorWithManyToMany(TestCase):
|
||||
def setUp(self):
|
||||
self.patterns = [
|
||||
url(r'^example/?$', ManyToManySourceView.as_view()),
|
||||
]
|
||||
|
||||
def test_schema_for_regular_views(self):
|
||||
"""
|
||||
Ensure that AutoField many to many fields are output as Integer.
|
||||
"""
|
||||
generator = SchemaGenerator(title='Example API', patterns=self.patterns)
|
||||
schema = generator.get_schema()
|
||||
|
||||
expected = coreapi.Document(
|
||||
url='',
|
||||
title='Example API',
|
||||
content={
|
||||
'example': {
|
||||
'create': coreapi.Link(
|
||||
url='/example/',
|
||||
action='post',
|
||||
encoding='application/json',
|
||||
fields=[
|
||||
coreapi.Field('name', required=True, location='form', schema=coreschema.String(title='Name')),
|
||||
coreapi.Field('targets', required=True, location='form', schema=coreschema.Array(title='Targets', items=coreschema.Integer())),
|
||||
]
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
assert schema == expected
|
||||
|
||||
|
||||
@unittest.skipUnless(coreapi, 'coreapi is not installed')
|
||||
class Test4605Regression(TestCase):
|
||||
def test_4605_regression(self):
|
||||
|
@ -1304,3 +1349,13 @@ class TestAutoSchemaAllowsFilters(object):
|
|||
|
||||
def test_FOO(self):
|
||||
assert not self._test('FOO')
|
||||
|
||||
|
||||
@pytest.mark.skipif(not coreapi, reason='coreapi is not installed')
|
||||
def test_schema_handles_exception():
|
||||
schema_view = get_schema_view(permission_classes=[DenyAllUsingPermissionDenied])
|
||||
request = factory.get('/')
|
||||
response = schema_view(request)
|
||||
response.render()
|
||||
assert response.status_code == 403
|
||||
assert "You do not have permission to perform this action." in str(response.content)
|
||||
|
|
|
@ -5,13 +5,12 @@ import inspect
|
|||
import pickle
|
||||
import re
|
||||
import unittest
|
||||
from collections import Mapping
|
||||
|
||||
import pytest
|
||||
from django.db import models
|
||||
|
||||
from rest_framework import fields, relations, serializers
|
||||
from rest_framework.compat import unicode_repr
|
||||
from rest_framework import exceptions, fields, relations, serializers
|
||||
from rest_framework.compat import Mapping, unicode_repr
|
||||
from rest_framework.fields import Field
|
||||
|
||||
from .models import (
|
||||
|
@ -156,6 +155,65 @@ class TestSerializer:
|
|||
assert serializer.validated_data == {'char': 'abc', 'integer': 123}
|
||||
assert serializer.errors == {}
|
||||
|
||||
def test_custom_to_internal_value(self):
|
||||
"""
|
||||
to_internal_value() is expected to return a dict, but subclasses may
|
||||
return application specific type.
|
||||
"""
|
||||
class Point(object):
|
||||
def __init__(self, srid, x, y):
|
||||
self.srid = srid
|
||||
self.coords = (x, y)
|
||||
|
||||
# Declares a serializer that converts data into an object
|
||||
class NestedPointSerializer(serializers.Serializer):
|
||||
longitude = serializers.FloatField(source='x')
|
||||
latitude = serializers.FloatField(source='y')
|
||||
|
||||
def to_internal_value(self, data):
|
||||
kwargs = super(NestedPointSerializer, self).to_internal_value(data)
|
||||
return Point(srid=4326, **kwargs)
|
||||
|
||||
serializer = NestedPointSerializer(data={'longitude': 6.958307, 'latitude': 50.941357})
|
||||
assert serializer.is_valid()
|
||||
assert isinstance(serializer.validated_data, Point)
|
||||
assert serializer.validated_data.srid == 4326
|
||||
assert serializer.validated_data.coords[0] == 6.958307
|
||||
assert serializer.validated_data.coords[1] == 50.941357
|
||||
assert serializer.errors == {}
|
||||
|
||||
def test_iterable_validators(self):
|
||||
"""
|
||||
Ensure `validators` parameter is compatible with reasonable iterables.
|
||||
"""
|
||||
data = {'char': 'abc', 'integer': 123}
|
||||
|
||||
for validators in ([], (), set()):
|
||||
class ExampleSerializer(serializers.Serializer):
|
||||
char = serializers.CharField(validators=validators)
|
||||
integer = serializers.IntegerField()
|
||||
|
||||
serializer = ExampleSerializer(data=data)
|
||||
assert serializer.is_valid()
|
||||
assert serializer.validated_data == data
|
||||
assert serializer.errors == {}
|
||||
|
||||
def raise_exception(value):
|
||||
raise exceptions.ValidationError('Raised error')
|
||||
|
||||
for validators in ([raise_exception], (raise_exception,), set([raise_exception])):
|
||||
class ExampleSerializer(serializers.Serializer):
|
||||
char = serializers.CharField(validators=validators)
|
||||
integer = serializers.IntegerField()
|
||||
|
||||
serializer = ExampleSerializer(data=data)
|
||||
assert not serializer.is_valid()
|
||||
assert serializer.data == data
|
||||
assert serializer.validated_data == {}
|
||||
assert serializer.errors == {'char': [
|
||||
exceptions.ErrorDetail(string='Raised error', code='invalid')
|
||||
]}
|
||||
|
||||
|
||||
class TestValidateMethod:
|
||||
def test_non_field_error_validate_method(self):
|
||||
|
|
|
@ -1,7 +1,17 @@
|
|||
import re
|
||||
|
||||
from django.shortcuts import render
|
||||
|
||||
|
||||
def test_base_template_with_context():
|
||||
context = {'request': True, 'csrf_token': 'TOKEN'}
|
||||
result = render({}, 'rest_framework/base.html', context=context)
|
||||
assert re.search(r'\bcsrfToken: "TOKEN"', result.content.decode('utf-8'))
|
||||
|
||||
|
||||
def test_base_template_with_no_context():
|
||||
# base.html should be renderable with no context,
|
||||
# so it can be easily extended.
|
||||
render({}, 'rest_framework/base.html')
|
||||
result = render({}, 'rest_framework/base.html')
|
||||
# note that this response will not include a valid CSRF token
|
||||
assert re.search(r'\bcsrfToken: ""', result.content.decode('utf-8'))
|
||||
|
|
|
@ -305,6 +305,15 @@ class URLizerTests(TestCase):
|
|||
'"foo_set": [\n "<a href="http://api/foos/1/">http://api/foos/1/</a>"\n], '
|
||||
self._urlize_dict_check(data)
|
||||
|
||||
def test_template_render_with_autoescape(self):
|
||||
"""
|
||||
Test that HTML is correctly escaped in Browsable API views.
|
||||
"""
|
||||
template = Template("{% load rest_framework %}{{ content|urlize_quoted_links }}")
|
||||
rendered = template.render(Context({'content': '<script>alert()</script> http://example.com'}))
|
||||
assert rendered == '<script>alert()</script>' \
|
||||
' <a href="http://example.com" rel="nofollow">http://example.com</a>'
|
||||
|
||||
def test_template_render_with_noautoescape(self):
|
||||
"""
|
||||
Test if the autoescape value is getting passed to urlize_quoted_links filter.
|
||||
|
@ -312,8 +321,8 @@ class URLizerTests(TestCase):
|
|||
template = Template("{% load rest_framework %}"
|
||||
"{% autoescape off %}{{ content|urlize_quoted_links }}"
|
||||
"{% endautoescape %}")
|
||||
rendered = template.render(Context({'content': '"http://example.com"'}))
|
||||
assert rendered == '"<a href="http://example.com" rel="nofollow">http://example.com</a>"'
|
||||
rendered = template.render(Context({'content': '<b> "http://example.com" </b>'}))
|
||||
assert rendered == '<b> "<a href="http://example.com" rel="nofollow">http://example.com</a>" </b>'
|
||||
|
||||
|
||||
@unittest.skipUnless(coreapi, 'coreapi is not installed')
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user