mirror of
https://github.com/encode/django-rest-framework.git
synced 2025-01-24 08:14:16 +03:00
Merge branch 'master' into writable-nested-modelserializer
This commit is contained in:
commit
58d38d694e
23
README.md
23
README.md
|
@ -12,11 +12,11 @@ Django REST framework is a powerful and flexible toolkit that makes it easy to b
|
||||||
|
|
||||||
Some reasons you might want to use REST framework:
|
Some reasons you might want to use REST framework:
|
||||||
|
|
||||||
* The Web browseable API is a huge useability win for your developers.
|
* The [Web browseable API][sandbox] is a huge useability win for your developers.
|
||||||
* Authentication policies including OAuth1a and OAuth2 out of the box.
|
* [Authentication policies][authentication] including [OAuth1a][oauth1-section] and [OAuth2][oauth2-section] out of the box.
|
||||||
* Serialization that supports both ORM and non-ORM data sources.
|
* [Serialization][serializers] that supports both [ORM][modelserializer-section] and [non-ORM][serializer-section] data sources.
|
||||||
* Customizable all the way down - just use regular function-based views if you don't need the more powerful features.
|
* Customizable all the way down - just use [regular function-based views][functionview-section] if you don't need the [more][generic-views] [powerful][viewsets] [features][routers].
|
||||||
* Extensive documentation, and great community support.
|
* [Extensive documentation][index], and [great community support][group].
|
||||||
|
|
||||||
There is a live example API for testing purposes, [available here][sandbox].
|
There is a live example API for testing purposes, [available here][sandbox].
|
||||||
|
|
||||||
|
@ -139,6 +139,19 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||||
[group]: https://groups.google.com/forum/?fromgroups#!forum/django-rest-framework
|
[group]: https://groups.google.com/forum/?fromgroups#!forum/django-rest-framework
|
||||||
[0.4]: https://github.com/tomchristie/django-rest-framework/tree/0.4.X
|
[0.4]: https://github.com/tomchristie/django-rest-framework/tree/0.4.X
|
||||||
[sandbox]: http://restframework.herokuapp.com/
|
[sandbox]: http://restframework.herokuapp.com/
|
||||||
|
|
||||||
|
[index]: http://django-rest-framework.org/
|
||||||
|
[oauth1-section]: http://django-rest-framework.org/api-guide/authentication.html#oauthauthentication
|
||||||
|
[oauth2-section]: http://django-rest-framework.org/api-guide/authentication.html#oauth2authentication
|
||||||
|
[serializer-section]: http://django-rest-framework.org/api-guide/serializers.html#serializers
|
||||||
|
[modelserializer-section]: http://django-rest-framework.org/api-guide/serializers.html#modelserializer
|
||||||
|
[functionview-section]: http://django-rest-framework.org/api-guide/views.html#function-based-views
|
||||||
|
[generic-views]: http://django-rest-framework.org/api-guide/generic-views.html
|
||||||
|
[viewsets]: http://django-rest-framework.org/api-guide/viewsets.html
|
||||||
|
[routers]: http://django-rest-framework.org/api-guide/routers.html
|
||||||
|
[serializers]: http://django-rest-framework.org/api-guide/serializers.html
|
||||||
|
[authentication]: http://django-rest-framework.org/api-guide/authentication.html
|
||||||
|
|
||||||
[rest-framework-2-announcement]: http://django-rest-framework.org/topics/rest-framework-2-announcement.html
|
[rest-framework-2-announcement]: http://django-rest-framework.org/topics/rest-framework-2-announcement.html
|
||||||
[2.1.0-notes]: https://groups.google.com/d/topic/django-rest-framework/Vv2M0CMY9bg/discussion
|
[2.1.0-notes]: https://groups.google.com/d/topic/django-rest-framework/Vv2M0CMY9bg/discussion
|
||||||
[image]: http://django-rest-framework.org/img/quickstart.png
|
[image]: http://django-rest-framework.org/img/quickstart.png
|
||||||
|
|
|
@ -303,6 +303,10 @@ The command line to test the authentication looks like:
|
||||||
|
|
||||||
curl -H "Authorization: Bearer <your-access-token>" http://localhost:8000/api/
|
curl -H "Authorization: Bearer <your-access-token>" http://localhost:8000/api/
|
||||||
|
|
||||||
|
### Alternative OAuth 2 implementations
|
||||||
|
|
||||||
|
Note that [Django OAuth Toolkit][django-oauth-toolkit] is an alternative external package that also includes OAuth 2.0 support for REST framework.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
# Custom authentication
|
# Custom authentication
|
||||||
|
@ -347,6 +351,10 @@ The following third party packages are also available.
|
||||||
|
|
||||||
HTTP digest authentication is a widely implemented scheme that was intended to replace HTTP basic authentication, and which provides a simple encrypted authentication mechanism. [Juan Riaza][juanriaza] maintains the [djangorestframework-digestauth][djangorestframework-digestauth] package which provides HTTP digest authentication support for REST framework.
|
HTTP digest authentication is a widely implemented scheme that was intended to replace HTTP basic authentication, and which provides a simple encrypted authentication mechanism. [Juan Riaza][juanriaza] maintains the [djangorestframework-digestauth][djangorestframework-digestauth] package which provides HTTP digest authentication support for REST framework.
|
||||||
|
|
||||||
|
## Django OAuth Toolkit
|
||||||
|
|
||||||
|
The [Django OAuth Toolkit][django-oauth-toolkit] package provides OAuth 2.0 support, and works with Python 2.7 and Python 3.3+. The package is maintained by [Evonove][evonove] and uses the excelllent [OAuthLib][oauthlib]. The package is well documented, and comes as a recommended alternative for OAuth 2.0 support.
|
||||||
|
|
||||||
[cite]: http://jacobian.org/writing/rest-worst-practices/
|
[cite]: http://jacobian.org/writing/rest-worst-practices/
|
||||||
[http401]: http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.4.2
|
[http401]: http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.4.2
|
||||||
[http403]: http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.4.4
|
[http403]: http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.4.4
|
||||||
|
@ -365,3 +373,6 @@ HTTP digest authentication is a widely implemented scheme that was intended to r
|
||||||
[django-oauth2-provider]: https://github.com/caffeinehit/django-oauth2-provider
|
[django-oauth2-provider]: https://github.com/caffeinehit/django-oauth2-provider
|
||||||
[django-oauth2-provider-docs]: https://django-oauth2-provider.readthedocs.org/en/latest/
|
[django-oauth2-provider-docs]: https://django-oauth2-provider.readthedocs.org/en/latest/
|
||||||
[rfc6749]: http://tools.ietf.org/html/rfc6749
|
[rfc6749]: http://tools.ietf.org/html/rfc6749
|
||||||
|
[django-oauth-toolkit]: https://github.com/evonove/django-oauth-toolkit
|
||||||
|
[evonove]: https://github.com/evonove/
|
||||||
|
[oauthlib]: https://github.com/idan/oauthlib
|
||||||
|
|
|
@ -214,10 +214,10 @@ In the case of JSON this means the default datetime representation uses the [ECM
|
||||||
|
|
||||||
**Signature:** `DateTimeField(format=None, input_formats=None)`
|
**Signature:** `DateTimeField(format=None, input_formats=None)`
|
||||||
|
|
||||||
* `format` - A string representing the output format. If not specified, this defaults to `None`, which indicates that python `datetime` objects should be returned by `to_native`. In this case the datetime encoding will be determined by the renderer.
|
* `format` - A string representing the output format. If not specified, this defaults to `None`, which indicates that Python `datetime` objects should be returned by `to_native`. 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']`.
|
* `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']`.
|
||||||
|
|
||||||
DateTime format strings may either be [python strftime formats][strftime] which explicitly specify the format, or the special string `'iso-8601'`, which indicates that [ISO 8601][iso8601] style datetimes should be used. (eg `'2013-01-29T12:34:56.000000Z'`)
|
DateTime format strings may either be [Python strftime formats][strftime] which explicitly specify the format, or the special string `'iso-8601'`, which indicates that [ISO 8601][iso8601] style datetimes should be used. (eg `'2013-01-29T12:34:56.000000Z'`)
|
||||||
|
|
||||||
## DateField
|
## DateField
|
||||||
|
|
||||||
|
@ -227,10 +227,10 @@ Corresponds to `django.db.models.fields.DateField`
|
||||||
|
|
||||||
**Signature:** `DateField(format=None, input_formats=None)`
|
**Signature:** `DateField(format=None, input_formats=None)`
|
||||||
|
|
||||||
* `format` - A string representing the output format. If not specified, this defaults to `None`, which indicates that python `date` objects should be returned by `to_native`. In this case the date encoding will be determined by the renderer.
|
* `format` - A string representing the output format. If not specified, this defaults to `None`, which indicates that Python `date` objects should be returned by `to_native`. In this case the date 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 `DATE_INPUT_FORMATS` setting will be used, which defaults to `['iso-8601']`.
|
* `input_formats` - A list of strings representing the input formats which may be used to parse the date. If not specified, the `DATE_INPUT_FORMATS` setting will be used, which defaults to `['iso-8601']`.
|
||||||
|
|
||||||
Date format strings may either be [python strftime formats][strftime] which explicitly specify the format, or the special string `'iso-8601'`, which indicates that [ISO 8601][iso8601] style dates should be used. (eg `'2013-01-29'`)
|
Date format strings may either be [Python strftime formats][strftime] which explicitly specify the format, or the special string `'iso-8601'`, which indicates that [ISO 8601][iso8601] style dates should be used. (eg `'2013-01-29'`)
|
||||||
|
|
||||||
## TimeField
|
## TimeField
|
||||||
|
|
||||||
|
@ -242,10 +242,10 @@ Corresponds to `django.db.models.fields.TimeField`
|
||||||
|
|
||||||
**Signature:** `TimeField(format=None, input_formats=None)`
|
**Signature:** `TimeField(format=None, input_formats=None)`
|
||||||
|
|
||||||
* `format` - A string representing the output format. If not specified, this defaults to `None`, which indicates that python `time` objects should be returned by `to_native`. In this case the time encoding will be determined by the renderer.
|
* `format` - A string representing the output format. If not specified, this defaults to `None`, which indicates that Python `time` objects should be returned by `to_native`. In this case the time 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 `TIME_INPUT_FORMATS` setting will be used, which defaults to `['iso-8601']`.
|
* `input_formats` - A list of strings representing the input formats which may be used to parse the date. If not specified, the `TIME_INPUT_FORMATS` setting will be used, which defaults to `['iso-8601']`.
|
||||||
|
|
||||||
Time format strings may either be [python strftime formats][strftime] which explicitly specify the format, or the special string `'iso-8601'`, which indicates that [ISO 8601][iso8601] style times should be used. (eg `'12:34:56.000000'`)
|
Time format strings may either be [Python strftime formats][strftime] which explicitly specify the format, or the special string `'iso-8601'`, which indicates that [ISO 8601][iso8601] style times should be used. (eg `'12:34:56.000000'`)
|
||||||
|
|
||||||
## IntegerField
|
## IntegerField
|
||||||
|
|
||||||
|
|
|
@ -60,7 +60,7 @@ The following attributes control the basic view behavior.
|
||||||
|
|
||||||
* `queryset` - The queryset that should be used for returning objects from this view. Typically, you must either set this attribute, or override the `get_queryset()` method.
|
* `queryset` - The queryset that should be used for returning objects from this view. Typically, you must either set this attribute, or override the `get_queryset()` method.
|
||||||
* `serializer_class` - The serializer class that should be used for validating and deserializing input, and for serializing output. Typically, you must either set this attribute, or override the `get_serializer_class()` method.
|
* `serializer_class` - The serializer class that should be used for validating and deserializing input, and for serializing output. Typically, you must either set this attribute, or override the `get_serializer_class()` method.
|
||||||
* `lookup_field` - The field that should be used to lookup individual model instances. Defaults to `'pk'`. The URL conf should include a keyword argument corresponding to this value. More complex lookup styles can be supported by overriding the `get_object()` method.
|
* `lookup_field` - The field that should be used to lookup individual model instances. Defaults to `'pk'`. The URL conf should include a keyword argument corresponding to this value. More complex lookup styles can be supported by overriding the `get_object()` method. Note that when using hyperlinked APIs you'll need to ensure that *both* the API views *and* the serializer classes use lookup fields that correctly correspond with the URL conf.
|
||||||
|
|
||||||
**Shortcuts**:
|
**Shortcuts**:
|
||||||
|
|
||||||
|
@ -92,7 +92,8 @@ May be overridden to provide dynamic behavior such as returning a queryset that
|
||||||
For example:
|
For example:
|
||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
return self.user.accounts.all()
|
user = self.request.user
|
||||||
|
return user.accounts.all()
|
||||||
|
|
||||||
#### `get_object(self)`
|
#### `get_object(self)`
|
||||||
|
|
||||||
|
@ -131,7 +132,7 @@ You may want to override this method to provide more complex behavior such as mo
|
||||||
For example:
|
For example:
|
||||||
|
|
||||||
def get_paginate_by(self):
|
def get_paginate_by(self):
|
||||||
self.request.accepted_renderer.format == 'html':
|
if self.request.accepted_renderer.format == 'html':
|
||||||
return 20
|
return 20
|
||||||
return 100
|
return 100
|
||||||
|
|
||||||
|
|
|
@ -8,9 +8,9 @@
|
||||||
|
|
||||||
REST framework supports HTTP content negotiation by providing a `Response` class which allows you to return content that can be rendered into multiple content types, depending on the client request.
|
REST framework supports HTTP content negotiation by providing a `Response` class which allows you to return content that can be rendered into multiple content types, depending on the client request.
|
||||||
|
|
||||||
The `Response` class subclasses Django's `SimpleTemplateResponse`. `Response` objects are initialised with data, which should consist of native python primatives. REST framework then uses standard HTTP content negotiation to determine how it should render the final response content.
|
The `Response` class subclasses Django's `SimpleTemplateResponse`. `Response` objects are initialised with data, which should consist of native Python primitives. REST framework then uses standard HTTP content negotiation to determine how it should render the final response content.
|
||||||
|
|
||||||
There's no requirement for you to use the `Response` class, you can also return regular `HttpResponse` objects from your views if you want, but it provides a nicer interface for returning Web API responses.
|
There's no requirement for you to use the `Response` class, you can also return regular `HttpResponse` or `StreamingHttpResponse` objects from your views if required. Using the `Response` class simply provides a nicer interface for returning content-negotiated Web API responses, that can be rendered to multiple formats.
|
||||||
|
|
||||||
Unless you want to heavily customize REST framework for some reason, you should always use an `APIView` class or `@api_view` function for views that return `Response` objects. Doing so ensures that the view can perform content negotiation and select the appropriate renderer for the response, before it is returned from the view.
|
Unless you want to heavily customize REST framework for some reason, you should always use an `APIView` class or `@api_view` function for views that return `Response` objects. Doing so ensures that the view can perform content negotiation and select the appropriate renderer for the response, before it is returned from the view.
|
||||||
|
|
||||||
|
@ -22,7 +22,7 @@ Unless you want to heavily customize REST framework for some reason, you should
|
||||||
|
|
||||||
**Signature:** `Response(data, status=None, template_name=None, headers=None, content_type=None)`
|
**Signature:** `Response(data, status=None, template_name=None, headers=None, content_type=None)`
|
||||||
|
|
||||||
Unlike regular `HttpResponse` objects, you do not instantiate `Response` objects with rendered content. Instead you pass in unrendered data, which may consist of any python primatives.
|
Unlike regular `HttpResponse` objects, you do not instantiate `Response` objects with rendered content. Instead you pass in unrendered data, which may consist of any Python primitives.
|
||||||
|
|
||||||
The renderers used by the `Response` class cannot natively handle complex datatypes such as Django model instances, so you need to serialize the data into primative datatypes before creating the `Response` object.
|
The renderers used by the `Response` class cannot natively handle complex datatypes such as Django model instances, so you need to serialize the data into primative datatypes before creating the `Response` object.
|
||||||
|
|
||||||
|
|
|
@ -26,7 +26,7 @@ There are two mandatory arguments to the `register()` method:
|
||||||
|
|
||||||
Optionally, you may also specify an additional argument:
|
Optionally, you may also specify an additional argument:
|
||||||
|
|
||||||
* `base_name` - The base to use for the URL names that are created. If unset the basename will be automatically generated based on the `model` or `queryset` attribute on the viewset, if it has one.
|
* `base_name` - The base to use for the URL names that are created. If unset the basename will be automatically generated based on the `model` or `queryset` attribute on the viewset, if it has one. Note that if the viewset does not include a `model` or `queryset` attribute then you must set `base_name` when registering the viewset.
|
||||||
|
|
||||||
The example above would generate the following URL patterns:
|
The example above would generate the following URL patterns:
|
||||||
|
|
||||||
|
|
|
@ -8,7 +8,7 @@ will take some serious design work.
|
||||||
>
|
>
|
||||||
> — Russell Keith-Magee, [Django users group][cite]
|
> — Russell Keith-Magee, [Django users group][cite]
|
||||||
|
|
||||||
Serializers allow complex data such as querysets and model instances to be converted to native python datatypes that can then be easily rendered into `JSON`, `XML` or other content types. Serializers also provide deserialization, allowing parsed data to be converted back into complex types, after first validating the incoming data.
|
Serializers allow complex data such as querysets and model instances to be converted to native Python datatypes that can then be easily rendered into `JSON`, `XML` or other content types. Serializers also provide deserialization, allowing parsed data to be converted back into complex types, after first validating the incoming data.
|
||||||
|
|
||||||
REST framework's serializers work very similarly to Django's `Form` and `ModelForm` classes. It provides a `Serializer` class which gives you a powerful, generic way to control the output of your responses, as well as a `ModelSerializer` class which provides a useful shortcut for creating serializers that deal with model instances and querysets.
|
REST framework's serializers work very similarly to Django's `Form` and `ModelForm` classes. It provides a `Serializer` class which gives you a powerful, generic way to control the output of your responses, as well as a `ModelSerializer` class which provides a useful shortcut for creating serializers that deal with model instances and querysets.
|
||||||
|
|
||||||
|
@ -39,7 +39,7 @@ Declaring a serializer looks very similar to declaring a form:
|
||||||
an existing model instance, or create a new model instance.
|
an existing model instance, or create a new model instance.
|
||||||
"""
|
"""
|
||||||
if instance is not None:
|
if instance is not None:
|
||||||
instance.title = attrs.get('title', instance.title)
|
instance.email = attrs.get('email', instance.email)
|
||||||
instance.content = attrs.get('content', instance.content)
|
instance.content = attrs.get('content', instance.content)
|
||||||
instance.created = attrs.get('created', instance.created)
|
instance.created = attrs.get('created', instance.created)
|
||||||
return instance
|
return instance
|
||||||
|
@ -57,7 +57,7 @@ We can now use `CommentSerializer` to serialize a comment, or list of comments.
|
||||||
serializer.data
|
serializer.data
|
||||||
# {'email': u'leila@example.com', 'content': u'foo bar', 'created': datetime.datetime(2012, 8, 22, 16, 20, 9, 822774)}
|
# {'email': u'leila@example.com', 'content': u'foo bar', 'created': datetime.datetime(2012, 8, 22, 16, 20, 9, 822774)}
|
||||||
|
|
||||||
At this point we've translated the model instance into python native datatypes. To finalise the serialization process we render the data into `json`.
|
At this point we've translated the model instance into Python native datatypes. To finalise the serialization process we render the data into `json`.
|
||||||
|
|
||||||
json = JSONRenderer().render(serializer.data)
|
json = JSONRenderer().render(serializer.data)
|
||||||
json
|
json
|
||||||
|
@ -65,7 +65,7 @@ At this point we've translated the model instance into python native datatypes.
|
||||||
|
|
||||||
## Deserializing objects
|
## Deserializing objects
|
||||||
|
|
||||||
Deserialization is similar. First we parse a stream into python native datatypes...
|
Deserialization is similar. First we parse a stream into Python native datatypes...
|
||||||
|
|
||||||
stream = StringIO(json)
|
stream = StringIO(json)
|
||||||
data = JSONParser().parse(stream)
|
data = JSONParser().parse(stream)
|
||||||
|
@ -387,7 +387,7 @@ There needs to be a way of determining which views should be used for hyperlinki
|
||||||
|
|
||||||
By default hyperlinks are expected to correspond to a view name that matches the style `'{model_name}-detail'`, and looks up the instance by a `pk` keyword argument.
|
By default hyperlinks are expected to correspond to a view name that matches the style `'{model_name}-detail'`, and looks up the instance by a `pk` keyword argument.
|
||||||
|
|
||||||
You can change the field that is used for object lookups by setting the `lookup_field` option. The value of this option should correspond both with a kwarg in the URL conf, and with an field on the model. For example:
|
You can change the field that is used for object lookups by setting the `lookup_field` option. The value of this option should correspond both with a kwarg in the URL conf, and with a field on the model. For example:
|
||||||
|
|
||||||
class AccountSerializer(serializers.HyperlinkedModelSerializer):
|
class AccountSerializer(serializers.HyperlinkedModelSerializer):
|
||||||
class Meta:
|
class Meta:
|
||||||
|
@ -395,6 +395,8 @@ You can change the field that is used for object lookups by setting the `lookup_
|
||||||
fields = ('url', 'account_name', 'users', 'created')
|
fields = ('url', 'account_name', 'users', 'created')
|
||||||
lookup_field = 'slug'
|
lookup_field = 'slug'
|
||||||
|
|
||||||
|
Not that the `lookup_field` will be used as the default on *all* hyperlinked fields, including both the URL identity, and any hyperlinked relationships.
|
||||||
|
|
||||||
For more specfic requirements such as specifying a different lookup for each field, you'll want to set the fields on the serializer explicitly. For example:
|
For more specfic requirements such as specifying a different lookup for each field, you'll want to set the fields on the serializer explicitly. For example:
|
||||||
|
|
||||||
class AccountSerializer(serializers.HyperlinkedModelSerializer):
|
class AccountSerializer(serializers.HyperlinkedModelSerializer):
|
||||||
|
|
|
@ -199,9 +199,9 @@ Default: `'format'`
|
||||||
|
|
||||||
#### DATETIME_FORMAT
|
#### DATETIME_FORMAT
|
||||||
|
|
||||||
A format string that should be used by default for rendering the output of `DateTimeField` serializer fields. If `None`, then `DateTimeField` serializer fields will return python `datetime` objects, and the datetime encoding will be determined by the renderer.
|
A format string that should be used by default for rendering the output of `DateTimeField` serializer fields. If `None`, then `DateTimeField` serializer fields will return Python `datetime` objects, and the datetime encoding will be determined by the renderer.
|
||||||
|
|
||||||
May be any of `None`, `'iso-8601'` or a python [strftime format][strftime] string.
|
May be any of `None`, `'iso-8601'` or a Python [strftime format][strftime] string.
|
||||||
|
|
||||||
Default: `None`
|
Default: `None`
|
||||||
|
|
||||||
|
@ -209,15 +209,15 @@ Default: `None`
|
||||||
|
|
||||||
A list of format strings that should be used by default for parsing inputs to `DateTimeField` serializer fields.
|
A list of format strings that should be used by default for parsing inputs to `DateTimeField` serializer fields.
|
||||||
|
|
||||||
May be a list including the string `'iso-8601'` or python [strftime format][strftime] strings.
|
May be a list including the string `'iso-8601'` or Python [strftime format][strftime] strings.
|
||||||
|
|
||||||
Default: `['iso-8601']`
|
Default: `['iso-8601']`
|
||||||
|
|
||||||
#### DATE_FORMAT
|
#### DATE_FORMAT
|
||||||
|
|
||||||
A format string that should be used by default for rendering the output of `DateField` serializer fields. If `None`, then `DateField` serializer fields will return python `date` objects, and the date encoding will be determined by the renderer.
|
A format string that should be used by default for rendering the output of `DateField` serializer fields. If `None`, then `DateField` serializer fields will return Python `date` objects, and the date encoding will be determined by the renderer.
|
||||||
|
|
||||||
May be any of `None`, `'iso-8601'` or a python [strftime format][strftime] string.
|
May be any of `None`, `'iso-8601'` or a Python [strftime format][strftime] string.
|
||||||
|
|
||||||
Default: `None`
|
Default: `None`
|
||||||
|
|
||||||
|
@ -225,15 +225,15 @@ Default: `None`
|
||||||
|
|
||||||
A list of format strings that should be used by default for parsing inputs to `DateField` serializer fields.
|
A list of format strings that should be used by default for parsing inputs to `DateField` serializer fields.
|
||||||
|
|
||||||
May be a list including the string `'iso-8601'` or python [strftime format][strftime] strings.
|
May be a list including the string `'iso-8601'` or Python [strftime format][strftime] strings.
|
||||||
|
|
||||||
Default: `['iso-8601']`
|
Default: `['iso-8601']`
|
||||||
|
|
||||||
#### TIME_FORMAT
|
#### TIME_FORMAT
|
||||||
|
|
||||||
A format string that should be used by default for rendering the output of `TimeField` serializer fields. If `None`, then `TimeField` serializer fields will return python `time` objects, and the time encoding will be determined by the renderer.
|
A format string that should be used by default for rendering the output of `TimeField` serializer fields. If `None`, then `TimeField` serializer fields will return Python `time` objects, and the time encoding will be determined by the renderer.
|
||||||
|
|
||||||
May be any of `None`, `'iso-8601'` or a python [strftime format][strftime] string.
|
May be any of `None`, `'iso-8601'` or a Python [strftime format][strftime] string.
|
||||||
|
|
||||||
Default: `None`
|
Default: `None`
|
||||||
|
|
||||||
|
@ -241,7 +241,7 @@ Default: `None`
|
||||||
|
|
||||||
A list of format strings that should be used by default for parsing inputs to `TimeField` serializer fields.
|
A list of format strings that should be used by default for parsing inputs to `TimeField` serializer fields.
|
||||||
|
|
||||||
May be a list including the string `'iso-8601'` or python [strftime format][strftime] strings.
|
May be a list including the string `'iso-8601'` or Python [strftime format][strftime] strings.
|
||||||
|
|
||||||
Default: `['iso-8601']`
|
Default: `['iso-8601']`
|
||||||
|
|
||||||
|
|
|
@ -108,7 +108,7 @@ For example:
|
||||||
queryset = User.objects.all()
|
queryset = User.objects.all()
|
||||||
serializer_class = UserSerializer
|
serializer_class = UserSerializer
|
||||||
|
|
||||||
@action
|
@action()
|
||||||
def set_password(self, request, pk=None):
|
def set_password(self, request, pk=None):
|
||||||
user = self.get_object()
|
user = self.get_object()
|
||||||
serializer = PasswordSerializer(data=request.DATA)
|
serializer = PasswordSerializer(data=request.DATA)
|
||||||
|
@ -209,8 +209,6 @@ To create a base viewset class that provides `create`, `list` and `retrieve` ope
|
||||||
mixins.ListMixin,
|
mixins.ListMixin,
|
||||||
mixins.RetrieveMixin,
|
mixins.RetrieveMixin,
|
||||||
viewsets.GenericViewSet):
|
viewsets.GenericViewSet):
|
||||||
pass
|
|
||||||
|
|
||||||
"""
|
"""
|
||||||
A viewset that provides `retrieve`, `update`, and `list` actions.
|
A viewset that provides `retrieve`, `update`, and `list` actions.
|
||||||
|
|
||||||
|
|
|
@ -303,3 +303,7 @@ table {
|
||||||
border-color: white;
|
border-color: white;
|
||||||
margin-bottom: 0.6em;
|
margin-bottom: 0.6em;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.side-nav {
|
||||||
|
overflow-y: scroll;
|
||||||
|
}
|
||||||
|
|
|
@ -15,11 +15,11 @@ Django REST framework is a powerful and flexible toolkit that makes it easy to b
|
||||||
|
|
||||||
Some reasons you might want to use REST framework:
|
Some reasons you might want to use REST framework:
|
||||||
|
|
||||||
* The Web browseable API is a huge usability win for your developers.
|
* The [Web browseable API][sandbox] is a huge usability win for your developers.
|
||||||
* Authentication policies including OAuth1a and OAuth2 out of the box.
|
* [Authentication policies][authentication] including [OAuth1a][oauth1-section] and [OAuth2][oauth2-section] out of the box.
|
||||||
* Serialization that supports both ORM and non-ORM data sources.
|
* [Serialization][serializers] that supports both [ORM][modelserializer-section] and [non-ORM][serializer-section] data sources.
|
||||||
* Customizable all the way down - just use regular function-based views if you don't need the more powerful features.
|
* Customizable all the way down - just use [regular function-based views][functionview-section] if you don't need the [more][generic-views] [powerful][viewsets] [features][routers].
|
||||||
* Extensive documentation, and great community support.
|
* [Extensive documentation][index], and [great community support][group].
|
||||||
|
|
||||||
There is a live example API for testing purposes, [available here][sandbox].
|
There is a live example API for testing purposes, [available here][sandbox].
|
||||||
|
|
||||||
|
@ -43,7 +43,7 @@ The following packages are optional:
|
||||||
* [django-oauth-plus][django-oauth-plus] (2.0+) and [oauth2][oauth2] (1.5.211+) - OAuth 1.0a support.
|
* [django-oauth-plus][django-oauth-plus] (2.0+) and [oauth2][oauth2] (1.5.211+) - OAuth 1.0a support.
|
||||||
* [django-oauth2-provider][django-oauth2-provider] (0.2.3+) - OAuth 2.0 support.
|
* [django-oauth2-provider][django-oauth2-provider] (0.2.3+) - OAuth 2.0 support.
|
||||||
|
|
||||||
**Note**: The `oauth2` python package is badly misnamed, and actually provides OAuth 1.0a support. Also note that packages required for both OAuth 1.0a, and OAuth 2.0 are not yet Python 3 compatible.
|
**Note**: The `oauth2` Python package is badly misnamed, and actually provides OAuth 1.0a support. Also note that packages required for both OAuth 1.0a, and OAuth 2.0 are not yet Python 3 compatible.
|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
|
|
||||||
|
@ -250,6 +250,12 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||||
[django-oauth2-provider]: https://github.com/caffeinehit/django-oauth2-provider
|
[django-oauth2-provider]: https://github.com/caffeinehit/django-oauth2-provider
|
||||||
[0.4]: https://github.com/tomchristie/django-rest-framework/tree/0.4.X
|
[0.4]: https://github.com/tomchristie/django-rest-framework/tree/0.4.X
|
||||||
[image]: img/quickstart.png
|
[image]: img/quickstart.png
|
||||||
|
[index]: .
|
||||||
|
[oauth1-section]: api-guide/authentication.html#oauthauthentication
|
||||||
|
[oauth2-section]: api-guide/authentication.html#oauth2authentication
|
||||||
|
[serializer-section]: api-guide/serializers.html#serializers
|
||||||
|
[modelserializer-section]: api-guide/serializers.html#modelserializer
|
||||||
|
[functionview-section]: api-guide/views.html#function-based-views
|
||||||
[sandbox]: http://restframework.herokuapp.com/
|
[sandbox]: http://restframework.herokuapp.com/
|
||||||
|
|
||||||
[quickstart]: tutorial/quickstart.md
|
[quickstart]: tutorial/quickstart.md
|
||||||
|
|
|
@ -198,5 +198,14 @@
|
||||||
$('.dropdown-menu').on('click touchstart', function(event) {
|
$('.dropdown-menu').on('click touchstart', function(event) {
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Dynamically force sidenav to no higher than browser window
|
||||||
|
$('.side-nav').css('max-height', window.innerHeight - 130);
|
||||||
|
|
||||||
|
$(function(){
|
||||||
|
$(window).resize(function(){
|
||||||
|
$('.side-nav').css('max-height', window.innerHeight - 130);
|
||||||
|
});
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
</body></html>
|
</body></html>
|
||||||
|
|
|
@ -139,6 +139,11 @@ The following people have helped make REST framework great.
|
||||||
* Pascal Borreli - [pborreli]
|
* Pascal Borreli - [pborreli]
|
||||||
* Alex Burgel - [aburgel]
|
* Alex Burgel - [aburgel]
|
||||||
* David Medina - [copitux]
|
* David Medina - [copitux]
|
||||||
|
* Areski Belaid - [areski]
|
||||||
|
* Ethan Freman - [mindlace]
|
||||||
|
* David Sanders - [davesque]
|
||||||
|
* Philip Douglas - [freakydug]
|
||||||
|
* Igor Kalat - [trwired]
|
||||||
|
|
||||||
Many thanks to everyone who's contributed to the project.
|
Many thanks to everyone who's contributed to the project.
|
||||||
|
|
||||||
|
@ -314,3 +319,8 @@ You can also contact [@_tomchristie][twitter] directly on twitter.
|
||||||
[pborreli]: https://github.com/pborreli
|
[pborreli]: https://github.com/pborreli
|
||||||
[aburgel]: https://github.com/aburgel
|
[aburgel]: https://github.com/aburgel
|
||||||
[copitux]: https://github.com/copitux
|
[copitux]: https://github.com/copitux
|
||||||
|
[areski]: https://github.com/areski
|
||||||
|
[mindlace]: https://github.com/mindlace
|
||||||
|
[davesque]: https://github.com/davesque
|
||||||
|
[freakydug]: https://github.com/freakydug
|
||||||
|
[trwired]: https://github.com/trwired
|
||||||
|
|
|
@ -40,6 +40,19 @@ You can determine your currently installed version using `pip freeze`:
|
||||||
|
|
||||||
## 2.3.x series
|
## 2.3.x series
|
||||||
|
|
||||||
|
### 2.3.6
|
||||||
|
|
||||||
|
**Date**: 27th June 2013
|
||||||
|
|
||||||
|
* Added `trailing_slash` option to routers.
|
||||||
|
* Include support for `HttpStreamingResponse`.
|
||||||
|
* Support wider range of default serializer validation when used with custom model fields.
|
||||||
|
* UTF-8 Support for browsable API descriptions.
|
||||||
|
* OAuth2 provider uses timezone aware datetimes when supported.
|
||||||
|
* Bugfix: Return error correctly when OAuth non-existent consumer occurs.
|
||||||
|
* Bugfix: Allow `FileUploadParser` to correctly filename if provided as URL kwarg.
|
||||||
|
* Bugfix: Fix `ScopedRateThrottle`.
|
||||||
|
|
||||||
### 2.3.5
|
### 2.3.5
|
||||||
|
|
||||||
**Date**: 3rd June 2013
|
**Date**: 3rd June 2013
|
||||||
|
|
|
@ -146,7 +146,7 @@ The first thing we need to get started on our Web API is provide a way of serial
|
||||||
|
|
||||||
The first part of serializer class defines the fields that get serialized/deserialized. The `restore_object` method defines how fully fledged instances get created when deserializing data.
|
The first part of serializer class defines the fields that get serialized/deserialized. The `restore_object` method defines how fully fledged instances get created when deserializing data.
|
||||||
|
|
||||||
Notice that we can also use various attributes that would typically be used on form fields, such as `widget=widgets.Testarea`. These can be used to control how the serializer should render when displayed as an HTML form. This is particularly useful for controlling how the browsable API should be displayed, as we'll see later in the tutorial.
|
Notice that we can also use various attributes that would typically be used on form fields, such as `widget=widgets.Textarea`. These can be used to control how the serializer should render when displayed as an HTML form. This is particularly useful for controlling how the browsable API should be displayed, as we'll see later in the tutorial.
|
||||||
|
|
||||||
We can actually also save ourselves some time by using the `ModelSerializer` class, as we'll see later, but for now we'll keep our serializer definition explicit.
|
We can actually also save ourselves some time by using the `ModelSerializer` class, as we'll see later, but for now we'll keep our serializer definition explicit.
|
||||||
|
|
||||||
|
@ -175,13 +175,13 @@ We've now got a few snippet instances to play with. Let's take a look at serial
|
||||||
serializer.data
|
serializer.data
|
||||||
# {'pk': 2, 'title': u'', 'code': u'print "hello, world"\n', 'linenos': False, 'language': u'python', 'style': u'friendly'}
|
# {'pk': 2, 'title': u'', 'code': u'print "hello, world"\n', 'linenos': False, 'language': u'python', 'style': u'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`.
|
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 = JSONRenderer().render(serializer.data)
|
||||||
content
|
content
|
||||||
# '{"pk": 2, "title": "", "code": "print \\"hello, world\\"\\n", "linenos": false, "language": "python", "style": "friendly"}'
|
# '{"pk": 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...
|
Deserialization is similar. First we parse a stream into Python native datatypes...
|
||||||
|
|
||||||
import StringIO
|
import StringIO
|
||||||
|
|
||||||
|
|
|
@ -10,7 +10,9 @@ A `ViewSet` class is only bound to a set of method handlers at the last moment,
|
||||||
|
|
||||||
Let's take our current set of views, and refactor them into view sets.
|
Let's take our current set of views, and refactor them into view sets.
|
||||||
|
|
||||||
First of all let's refactor our `UserListView` and `UserDetailView` views into a single `UserViewSet`. We can remove the two views, and replace then with a single class:
|
First of all let's refactor our `UserList` and `UserDetail` views into a single `UserViewSet`. We can remove the two views, and replace then with a single class:
|
||||||
|
|
||||||
|
from rest_framework import viewsets
|
||||||
|
|
||||||
class UserViewSet(viewsets.ReadOnlyModelViewSet):
|
class UserViewSet(viewsets.ReadOnlyModelViewSet):
|
||||||
"""
|
"""
|
||||||
|
@ -23,7 +25,6 @@ Here we've used `ReadOnlyModelViewSet` class to automatically provide the defaul
|
||||||
|
|
||||||
Next we're going to replace the `SnippetList`, `SnippetDetail` and `SnippetHighlight` view classes. We can remove the three views, and again replace them with a single class.
|
Next we're going to replace the `SnippetList`, `SnippetDetail` and `SnippetHighlight` view classes. We can remove the three views, and again replace them with a single class.
|
||||||
|
|
||||||
from rest_framework import viewsets
|
|
||||||
from rest_framework.decorators import link
|
from rest_framework.decorators import link
|
||||||
|
|
||||||
class SnippetViewSet(viewsets.ModelViewSet):
|
class SnippetViewSet(viewsets.ModelViewSet):
|
||||||
|
@ -73,7 +74,7 @@ In the `urls.py` file we bind our `ViewSet` classes into a set of concrete views
|
||||||
})
|
})
|
||||||
snippet_highlight = SnippetViewSet.as_view({
|
snippet_highlight = SnippetViewSet.as_view({
|
||||||
'get': 'highlight'
|
'get': 'highlight'
|
||||||
})
|
}, renderer_classes=[renderers.StaticHTMLRenderer])
|
||||||
user_list = UserViewSet.as_view({
|
user_list = UserViewSet.as_view({
|
||||||
'get': 'list'
|
'get': 'list'
|
||||||
})
|
})
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
__version__ = '2.3.5'
|
__version__ = '2.3.6'
|
||||||
|
|
||||||
VERSION = __version__ # synonym
|
VERSION = __version__ # synonym
|
||||||
|
|
||||||
|
|
|
@ -3,14 +3,13 @@ Provides various authentication policies.
|
||||||
"""
|
"""
|
||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
import base64
|
import base64
|
||||||
from datetime import datetime
|
|
||||||
|
|
||||||
from django.contrib.auth import authenticate
|
from django.contrib.auth import authenticate
|
||||||
from django.core.exceptions import ImproperlyConfigured
|
from django.core.exceptions import ImproperlyConfigured
|
||||||
from rest_framework import exceptions, HTTP_HEADER_ENCODING
|
from rest_framework import exceptions, HTTP_HEADER_ENCODING
|
||||||
from rest_framework.compat import CsrfViewMiddleware
|
from rest_framework.compat import CsrfViewMiddleware
|
||||||
from rest_framework.compat import oauth, oauth_provider, oauth_provider_store
|
from rest_framework.compat import oauth, oauth_provider, oauth_provider_store
|
||||||
from rest_framework.compat import oauth2_provider
|
from rest_framework.compat import oauth2_provider, provider_now
|
||||||
from rest_framework.authtoken.models import Token
|
from rest_framework.authtoken.models import Token
|
||||||
|
|
||||||
|
|
||||||
|
@ -230,8 +229,9 @@ class OAuthAuthentication(BaseAuthentication):
|
||||||
try:
|
try:
|
||||||
consumer_key = oauth_request.get_parameter('oauth_consumer_key')
|
consumer_key = oauth_request.get_parameter('oauth_consumer_key')
|
||||||
consumer = oauth_provider_store.get_consumer(request, oauth_request, consumer_key)
|
consumer = oauth_provider_store.get_consumer(request, oauth_request, consumer_key)
|
||||||
except oauth_provider.store.InvalidConsumerError as err:
|
except oauth_provider.store.InvalidConsumerError:
|
||||||
raise exceptions.AuthenticationFailed(err)
|
msg = 'Invalid consumer token: %s' % oauth_request.get_parameter('oauth_consumer_key')
|
||||||
|
raise exceptions.AuthenticationFailed(msg)
|
||||||
|
|
||||||
if consumer.status != oauth_provider.consts.ACCEPTED:
|
if consumer.status != oauth_provider.consts.ACCEPTED:
|
||||||
msg = 'Invalid consumer key status: %s' % consumer.get_status_display()
|
msg = 'Invalid consumer key status: %s' % consumer.get_status_display()
|
||||||
|
@ -319,9 +319,9 @@ class OAuth2Authentication(BaseAuthentication):
|
||||||
|
|
||||||
try:
|
try:
|
||||||
token = oauth2_provider.models.AccessToken.objects.select_related('user')
|
token = oauth2_provider.models.AccessToken.objects.select_related('user')
|
||||||
# TODO: Change to timezone aware datetime when oauth2_provider add
|
# provider_now switches to timezone aware datetime when
|
||||||
# support to it.
|
# the oauth2_provider version supports to it.
|
||||||
token = token.get(token=access_token, expires__gt=datetime.now())
|
token = token.get(token=access_token, expires__gt=provider_now())
|
||||||
except oauth2_provider.models.AccessToken.DoesNotExist:
|
except oauth2_provider.models.AccessToken.DoesNotExist:
|
||||||
raise exceptions.AuthenticationFailed('Invalid token')
|
raise exceptions.AuthenticationFailed('Invalid token')
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import uuid
|
import uuid
|
||||||
import hmac
|
import hmac
|
||||||
from hashlib import sha1
|
from hashlib import sha1
|
||||||
from rest_framework.compat import User
|
from rest_framework.compat import AUTH_USER_MODEL
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.db import models
|
from django.db import models
|
||||||
|
|
||||||
|
@ -11,7 +11,7 @@ class Token(models.Model):
|
||||||
The default authorization token model.
|
The default authorization token model.
|
||||||
"""
|
"""
|
||||||
key = models.CharField(max_length=40, primary_key=True)
|
key = models.CharField(max_length=40, primary_key=True)
|
||||||
user = models.OneToOneField(User, related_name='auth_token')
|
user = models.OneToOneField(AUTH_USER_MODEL, related_name='auth_token')
|
||||||
created = models.DateTimeField(auto_now_add=True)
|
created = models.DateTimeField(auto_now_add=True)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
|
|
|
@ -2,6 +2,7 @@
|
||||||
The `compat` module provides support for backwards compatibility with older
|
The `compat` module provides support for backwards compatibility with older
|
||||||
versions of django/python, and compatibility wrappers around optional packages.
|
versions of django/python, and compatibility wrappers around optional packages.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# flake8: noqa
|
# flake8: noqa
|
||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
@ -33,6 +34,12 @@ except ImportError:
|
||||||
from django.utils.encoding import force_unicode as force_text
|
from django.utils.encoding import force_unicode as force_text
|
||||||
|
|
||||||
|
|
||||||
|
# HttpResponseBase only exists from 1.5 onwards
|
||||||
|
try:
|
||||||
|
from django.http.response import HttpResponseBase
|
||||||
|
except ImportError:
|
||||||
|
from django.http import HttpResponse as HttpResponseBase
|
||||||
|
|
||||||
# django-filter is optional
|
# django-filter is optional
|
||||||
try:
|
try:
|
||||||
import django_filters
|
import django_filters
|
||||||
|
@ -77,15 +84,9 @@ def get_concrete_model(model_cls):
|
||||||
# Django 1.5 add support for custom auth user model
|
# Django 1.5 add support for custom auth user model
|
||||||
if django.VERSION >= (1, 5):
|
if django.VERSION >= (1, 5):
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
if hasattr(settings, 'AUTH_USER_MODEL'):
|
AUTH_USER_MODEL = settings.AUTH_USER_MODEL
|
||||||
User = settings.AUTH_USER_MODEL
|
|
||||||
else:
|
|
||||||
from django.contrib.auth.models import User
|
|
||||||
else:
|
else:
|
||||||
try:
|
AUTH_USER_MODEL = 'auth.User'
|
||||||
from django.contrib.auth.models import User
|
|
||||||
except ImportError:
|
|
||||||
raise ImportError("User model is not to be found.")
|
|
||||||
|
|
||||||
|
|
||||||
if django.VERSION >= (1, 5):
|
if django.VERSION >= (1, 5):
|
||||||
|
@ -489,12 +490,21 @@ try:
|
||||||
from provider.oauth2 import forms as oauth2_provider_forms
|
from provider.oauth2 import forms as oauth2_provider_forms
|
||||||
from provider import scope as oauth2_provider_scope
|
from provider import scope as oauth2_provider_scope
|
||||||
from provider import constants as oauth2_constants
|
from provider import constants as oauth2_constants
|
||||||
|
from provider import __version__ as provider_version
|
||||||
|
if provider_version in ('0.2.3', '0.2.4'):
|
||||||
|
# 0.2.3 and 0.2.4 are supported version that do not support
|
||||||
|
# timezone aware datetimes
|
||||||
|
from datetime.datetime import now as provider_now
|
||||||
|
else:
|
||||||
|
# Any other supported version does use timezone aware datetimes
|
||||||
|
from django.utils.timezone import now as provider_now
|
||||||
except ImportError:
|
except ImportError:
|
||||||
oauth2_provider = None
|
oauth2_provider = None
|
||||||
oauth2_provider_models = None
|
oauth2_provider_models = None
|
||||||
oauth2_provider_forms = None
|
oauth2_provider_forms = None
|
||||||
oauth2_provider_scope = None
|
oauth2_provider_scope = None
|
||||||
oauth2_constants = None
|
oauth2_constants = None
|
||||||
|
provider_now = None
|
||||||
|
|
||||||
# Handle lazy strings
|
# Handle lazy strings
|
||||||
from django.utils.functional import Promise
|
from django.utils.functional import Promise
|
||||||
|
|
|
@ -86,10 +86,3 @@ class Throttled(APIException):
|
||||||
self.detail = format % (self.wait, self.wait != 1 and 's' or '')
|
self.detail = format % (self.wait, self.wait != 1 and 's' or '')
|
||||||
else:
|
else:
|
||||||
self.detail = detail or self.default_detail
|
self.detail = detail or self.default_detail
|
||||||
|
|
||||||
|
|
||||||
class ConfigurationError(Exception):
|
|
||||||
"""
|
|
||||||
Indicates an internal server error.
|
|
||||||
"""
|
|
||||||
pass
|
|
||||||
|
|
|
@ -7,25 +7,24 @@ from __future__ import unicode_literals
|
||||||
|
|
||||||
import copy
|
import copy
|
||||||
import datetime
|
import datetime
|
||||||
from decimal import Decimal, DecimalException
|
|
||||||
import inspect
|
import inspect
|
||||||
import re
|
import re
|
||||||
import warnings
|
import warnings
|
||||||
|
from decimal import Decimal, DecimalException
|
||||||
|
from django import forms
|
||||||
from django.core import validators
|
from django.core import validators
|
||||||
from django.core.exceptions import ValidationError
|
from django.core.exceptions import ValidationError
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.db.models.fields import BLANK_CHOICE_DASH
|
from django.db.models.fields import BLANK_CHOICE_DASH
|
||||||
from django import forms
|
|
||||||
from django.forms import widgets
|
from django.forms import widgets
|
||||||
from django.utils.encoding import is_protected_type
|
from django.utils.encoding import is_protected_type
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import ugettext_lazy as _
|
||||||
from django.utils.datastructures import SortedDict
|
from django.utils.datastructures import SortedDict
|
||||||
from rest_framework import ISO_8601
|
from rest_framework import ISO_8601
|
||||||
from rest_framework.compat import (timezone, parse_date, parse_datetime,
|
from rest_framework.compat import (
|
||||||
parse_time)
|
timezone, parse_date, parse_datetime, parse_time, BytesIO, six, smart_text,
|
||||||
from rest_framework.compat import BytesIO
|
force_text, is_non_str_iterable
|
||||||
from rest_framework.compat import six
|
)
|
||||||
from rest_framework.compat import smart_text, force_text, is_non_str_iterable
|
|
||||||
from rest_framework.settings import api_settings
|
from rest_framework.settings import api_settings
|
||||||
|
|
||||||
|
|
||||||
|
@ -256,6 +255,12 @@ class WritableField(Field):
|
||||||
widget = widget()
|
widget = widget()
|
||||||
self.widget = widget
|
self.widget = widget
|
||||||
|
|
||||||
|
def __deepcopy__(self, memo):
|
||||||
|
result = copy.copy(self)
|
||||||
|
memo[id(self)] = result
|
||||||
|
result.validators = self.validators[:]
|
||||||
|
return result
|
||||||
|
|
||||||
def validate(self, value):
|
def validate(self, value):
|
||||||
if value in validators.EMPTY_VALUES and self.required:
|
if value in validators.EMPTY_VALUES and self.required:
|
||||||
raise ValidationError(self.error_messages['required'])
|
raise ValidationError(self.error_messages['required'])
|
||||||
|
@ -331,9 +336,13 @@ class ModelField(WritableField):
|
||||||
raise ValueError("ModelField requires 'model_field' kwarg")
|
raise ValueError("ModelField requires 'model_field' kwarg")
|
||||||
|
|
||||||
self.min_length = kwargs.pop('min_length',
|
self.min_length = kwargs.pop('min_length',
|
||||||
getattr(self.model_field, 'min_length', None))
|
getattr(self.model_field, 'min_length', None))
|
||||||
self.max_length = kwargs.pop('max_length',
|
self.max_length = kwargs.pop('max_length',
|
||||||
getattr(self.model_field, 'max_length', None))
|
getattr(self.model_field, 'max_length', None))
|
||||||
|
self.min_value = kwargs.pop('min_value',
|
||||||
|
getattr(self.model_field, 'min_value', None))
|
||||||
|
self.max_value = kwargs.pop('max_value',
|
||||||
|
getattr(self.model_field, 'max_value', None))
|
||||||
|
|
||||||
super(ModelField, self).__init__(*args, **kwargs)
|
super(ModelField, self).__init__(*args, **kwargs)
|
||||||
|
|
||||||
|
@ -341,6 +350,10 @@ class ModelField(WritableField):
|
||||||
self.validators.append(validators.MinLengthValidator(self.min_length))
|
self.validators.append(validators.MinLengthValidator(self.min_length))
|
||||||
if self.max_length is not None:
|
if self.max_length is not None:
|
||||||
self.validators.append(validators.MaxLengthValidator(self.max_length))
|
self.validators.append(validators.MaxLengthValidator(self.max_length))
|
||||||
|
if self.min_value is not None:
|
||||||
|
self.validators.append(validators.MinValueValidator(self.min_value))
|
||||||
|
if self.max_value is not None:
|
||||||
|
self.validators.append(validators.MaxValueValidator(self.max_value))
|
||||||
|
|
||||||
def from_native(self, value):
|
def from_native(self, value):
|
||||||
rel = getattr(self.model_field, "rel", None)
|
rel = getattr(self.model_field, "rel", None)
|
||||||
|
@ -428,13 +441,6 @@ class SlugField(CharField):
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
super(SlugField, self).__init__(*args, **kwargs)
|
super(SlugField, self).__init__(*args, **kwargs)
|
||||||
|
|
||||||
def __deepcopy__(self, memo):
|
|
||||||
result = copy.copy(self)
|
|
||||||
memo[id(self)] = result
|
|
||||||
#result.widget = copy.deepcopy(self.widget, memo)
|
|
||||||
result.validators = self.validators[:]
|
|
||||||
return result
|
|
||||||
|
|
||||||
|
|
||||||
class ChoiceField(WritableField):
|
class ChoiceField(WritableField):
|
||||||
type_name = 'ChoiceField'
|
type_name = 'ChoiceField'
|
||||||
|
@ -503,13 +509,6 @@ class EmailField(CharField):
|
||||||
return None
|
return None
|
||||||
return ret.strip()
|
return ret.strip()
|
||||||
|
|
||||||
def __deepcopy__(self, memo):
|
|
||||||
result = copy.copy(self)
|
|
||||||
memo[id(self)] = result
|
|
||||||
#result.widget = copy.deepcopy(self.widget, memo)
|
|
||||||
result.validators = self.validators[:]
|
|
||||||
return result
|
|
||||||
|
|
||||||
|
|
||||||
class RegexField(CharField):
|
class RegexField(CharField):
|
||||||
type_name = 'RegexField'
|
type_name = 'RegexField'
|
||||||
|
@ -534,12 +533,6 @@ class RegexField(CharField):
|
||||||
|
|
||||||
regex = property(_get_regex, _set_regex)
|
regex = property(_get_regex, _set_regex)
|
||||||
|
|
||||||
def __deepcopy__(self, memo):
|
|
||||||
result = copy.copy(self)
|
|
||||||
memo[id(self)] = result
|
|
||||||
result.validators = self.validators[:]
|
|
||||||
return result
|
|
||||||
|
|
||||||
|
|
||||||
class DateField(WritableField):
|
class DateField(WritableField):
|
||||||
type_name = 'DateField'
|
type_name = 'DateField'
|
||||||
|
|
|
@ -212,7 +212,7 @@ class GenericAPIView(views.APIView):
|
||||||
You may want to override this if you need to provide different
|
You may want to override this if you need to provide different
|
||||||
serializations depending on the incoming request.
|
serializations depending on the incoming request.
|
||||||
|
|
||||||
(Eg. admins get full serialization, others get basic serilization)
|
(Eg. admins get full serialization, others get basic serialization)
|
||||||
"""
|
"""
|
||||||
serializer_class = self.serializer_class
|
serializer_class = self.serializer_class
|
||||||
if serializer_class is not None:
|
if serializer_class is not None:
|
||||||
|
@ -285,7 +285,7 @@ class GenericAPIView(views.APIView):
|
||||||
)
|
)
|
||||||
filter_kwargs = {self.slug_field: slug}
|
filter_kwargs = {self.slug_field: slug}
|
||||||
else:
|
else:
|
||||||
raise exceptions.ConfigurationError(
|
raise ImproperlyConfigured(
|
||||||
'Expected view %s to be called with a URL keyword argument '
|
'Expected view %s to be called with a URL keyword argument '
|
||||||
'named "%s". Fix your URL conf, or set the `.lookup_field` '
|
'named "%s". Fix your URL conf, or set the `.lookup_field` '
|
||||||
'attribute on the view correctly.' %
|
'attribute on the view correctly.' %
|
||||||
|
|
|
@ -128,7 +128,7 @@ class DjangoModelPermissions(BasePermission):
|
||||||
|
|
||||||
# Workaround to ensure DjangoModelPermissions are not applied
|
# Workaround to ensure DjangoModelPermissions are not applied
|
||||||
# to the root view when using DefaultRouter.
|
# to the root view when using DefaultRouter.
|
||||||
if model_cls is None and getattr(view, '_ignore_model_permissions'):
|
if model_cls is None and getattr(view, '_ignore_model_permissions', False):
|
||||||
return True
|
return True
|
||||||
|
|
||||||
assert model_cls, ('Cannot apply DjangoModelPermissions on a view that'
|
assert model_cls, ('Cannot apply DjangoModelPermissions on a view that'
|
||||||
|
|
|
@ -11,6 +11,7 @@ from __future__ import unicode_literals
|
||||||
import copy
|
import copy
|
||||||
import json
|
import json
|
||||||
from django import forms
|
from django import forms
|
||||||
|
from django.core.exceptions import ImproperlyConfigured
|
||||||
from django.http.multipartparser import parse_header
|
from django.http.multipartparser import parse_header
|
||||||
from django.template import RequestContext, loader, Template
|
from django.template import RequestContext, loader, Template
|
||||||
from django.utils.xmlutils import SimplerXMLGenerator
|
from django.utils.xmlutils import SimplerXMLGenerator
|
||||||
|
@ -18,7 +19,6 @@ from rest_framework.compat import StringIO
|
||||||
from rest_framework.compat import six
|
from rest_framework.compat import six
|
||||||
from rest_framework.compat import smart_text
|
from rest_framework.compat import smart_text
|
||||||
from rest_framework.compat import yaml
|
from rest_framework.compat import yaml
|
||||||
from rest_framework.exceptions import ConfigurationError
|
|
||||||
from rest_framework.settings import api_settings
|
from rest_framework.settings import api_settings
|
||||||
from rest_framework.request import clone_request
|
from rest_framework.request import clone_request
|
||||||
from rest_framework.utils import encoders
|
from rest_framework.utils import encoders
|
||||||
|
@ -270,7 +270,7 @@ class TemplateHTMLRenderer(BaseRenderer):
|
||||||
return [self.template_name]
|
return [self.template_name]
|
||||||
elif hasattr(view, 'get_template_names'):
|
elif hasattr(view, 'get_template_names'):
|
||||||
return view.get_template_names()
|
return view.get_template_names()
|
||||||
raise ConfigurationError('Returned a template response with no template_name')
|
raise ImproperlyConfigured('Returned a template response with no template_name')
|
||||||
|
|
||||||
def get_exception_template(self, response):
|
def get_exception_template(self, response):
|
||||||
template_names = [name % {'status_code': response.status_code}
|
template_names = [name % {'status_code': response.status_code}
|
||||||
|
|
|
@ -15,7 +15,9 @@ For example, you might have a `urls.py` that looks something like this:
|
||||||
"""
|
"""
|
||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
import itertools
|
||||||
from collections import namedtuple
|
from collections import namedtuple
|
||||||
|
from django.core.exceptions import ImproperlyConfigured
|
||||||
from rest_framework import views
|
from rest_framework import views
|
||||||
from rest_framework.compat import patterns, url
|
from rest_framework.compat import patterns, url
|
||||||
from rest_framework.response import Response
|
from rest_framework.response import Response
|
||||||
|
@ -38,6 +40,13 @@ def replace_methodname(format_string, methodname):
|
||||||
return ret
|
return ret
|
||||||
|
|
||||||
|
|
||||||
|
def flatten(list_of_lists):
|
||||||
|
"""
|
||||||
|
Takes an iterable of iterables, returns a single iterable containing all items
|
||||||
|
"""
|
||||||
|
return itertools.chain(*list_of_lists)
|
||||||
|
|
||||||
|
|
||||||
class BaseRouter(object):
|
class BaseRouter(object):
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self.registry = []
|
self.registry = []
|
||||||
|
@ -117,7 +126,7 @@ class SimpleRouter(BaseRouter):
|
||||||
if model_cls is None and queryset is not None:
|
if model_cls is None and queryset is not None:
|
||||||
model_cls = queryset.model
|
model_cls = queryset.model
|
||||||
|
|
||||||
assert model_cls, '`name` not argument not specified, and could ' \
|
assert model_cls, '`base_name` argument not specified, and could ' \
|
||||||
'not automatically determine the name from the viewset, as ' \
|
'not automatically determine the name from the viewset, as ' \
|
||||||
'it does not have a `.model` or `.queryset` attribute.'
|
'it does not have a `.model` or `.queryset` attribute.'
|
||||||
|
|
||||||
|
@ -130,12 +139,18 @@ class SimpleRouter(BaseRouter):
|
||||||
Returns a list of the Route namedtuple.
|
Returns a list of the Route namedtuple.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
known_actions = flatten([route.mapping.values() for route in self.routes])
|
||||||
|
|
||||||
# Determine any `@action` or `@link` decorated methods on the viewset
|
# Determine any `@action` or `@link` decorated methods on the viewset
|
||||||
dynamic_routes = []
|
dynamic_routes = []
|
||||||
for methodname in dir(viewset):
|
for methodname in dir(viewset):
|
||||||
attr = getattr(viewset, methodname)
|
attr = getattr(viewset, methodname)
|
||||||
httpmethods = getattr(attr, 'bind_to_methods', None)
|
httpmethods = getattr(attr, 'bind_to_methods', None)
|
||||||
if httpmethods:
|
if httpmethods:
|
||||||
|
if methodname in known_actions:
|
||||||
|
raise ImproperlyConfigured('Cannot use @action or @link decorator on '
|
||||||
|
'method "%s" as it is an existing route' % methodname)
|
||||||
|
httpmethods = [method.lower() for method in httpmethods]
|
||||||
dynamic_routes.append((httpmethods, methodname))
|
dynamic_routes.append((httpmethods, methodname))
|
||||||
|
|
||||||
ret = []
|
ret = []
|
||||||
|
@ -215,6 +230,7 @@ class DefaultRouter(SimpleRouter):
|
||||||
"""
|
"""
|
||||||
include_root_view = True
|
include_root_view = True
|
||||||
include_format_suffixes = True
|
include_format_suffixes = True
|
||||||
|
root_view_name = 'api-root'
|
||||||
|
|
||||||
def get_api_root_view(self):
|
def get_api_root_view(self):
|
||||||
"""
|
"""
|
||||||
|
@ -244,7 +260,7 @@ class DefaultRouter(SimpleRouter):
|
||||||
urls = []
|
urls = []
|
||||||
|
|
||||||
if self.include_root_view:
|
if self.include_root_view:
|
||||||
root_url = url(r'^$', self.get_api_root_view(), name='api-root')
|
root_url = url(r'^$', self.get_api_root_view(), name=self.root_view_name)
|
||||||
urls.append(root_url)
|
urls.append(root_url)
|
||||||
|
|
||||||
default_urls = super(DefaultRouter, self).get_urls()
|
default_urls = super(DefaultRouter, self).get_urls()
|
||||||
|
|
|
@ -134,6 +134,8 @@ PASSWORD_HASHERS = (
|
||||||
'django.contrib.auth.hashers.CryptPasswordHasher',
|
'django.contrib.auth.hashers.CryptPasswordHasher',
|
||||||
)
|
)
|
||||||
|
|
||||||
|
AUTH_USER_MODEL = 'auth.User'
|
||||||
|
|
||||||
import django
|
import django
|
||||||
|
|
||||||
if django.VERSION < (1, 3):
|
if django.VERSION < (1, 3):
|
||||||
|
|
|
@ -944,34 +944,23 @@ class HyperlinkedModelSerializer(ModelSerializer):
|
||||||
_default_view_name = '%(model_name)s-detail'
|
_default_view_name = '%(model_name)s-detail'
|
||||||
_hyperlink_field_class = HyperlinkedRelatedField
|
_hyperlink_field_class = HyperlinkedRelatedField
|
||||||
|
|
||||||
# Just a placeholder to ensure 'url' is the first field
|
def get_default_fields(self):
|
||||||
# The field itself is actually created on initialization,
|
fields = super(HyperlinkedModelSerializer, self).get_default_fields()
|
||||||
# when the view_name and lookup_field arguments are available.
|
|
||||||
url = Field()
|
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
|
||||||
super(HyperlinkedModelSerializer, self).__init__(*args, **kwargs)
|
|
||||||
|
|
||||||
if self.opts.view_name is None:
|
if self.opts.view_name is None:
|
||||||
self.opts.view_name = self._get_default_view_name(self.opts.model)
|
self.opts.view_name = self._get_default_view_name(self.opts.model)
|
||||||
|
|
||||||
url_field = HyperlinkedIdentityField(
|
if 'url' not in fields:
|
||||||
view_name=self.opts.view_name,
|
url_field = HyperlinkedIdentityField(
|
||||||
lookup_field=self.opts.lookup_field
|
view_name=self.opts.view_name,
|
||||||
)
|
lookup_field=self.opts.lookup_field
|
||||||
url_field.initialize(self, 'url')
|
)
|
||||||
self.fields['url'] = url_field
|
ret = self._dict_class()
|
||||||
|
ret['url'] = url_field
|
||||||
|
ret.update(fields)
|
||||||
|
fields = ret
|
||||||
|
|
||||||
def _get_default_view_name(self, model):
|
return fields
|
||||||
"""
|
|
||||||
Return the view name to use if 'view_name' is not specified in 'Meta'
|
|
||||||
"""
|
|
||||||
model_meta = model._meta
|
|
||||||
format_kwargs = {
|
|
||||||
'app_label': model_meta.app_label,
|
|
||||||
'model_name': model_meta.object_name.lower()
|
|
||||||
}
|
|
||||||
return self._default_view_name % format_kwargs
|
|
||||||
|
|
||||||
def get_pk_field(self, model_field):
|
def get_pk_field(self, model_field):
|
||||||
if self.opts.fields and model_field.name in self.opts.fields:
|
if self.opts.fields and model_field.name in self.opts.fields:
|
||||||
|
@ -1006,3 +995,14 @@ class HyperlinkedModelSerializer(ModelSerializer):
|
||||||
return data.get('url', None)
|
return data.get('url', None)
|
||||||
except AttributeError:
|
except AttributeError:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
def _get_default_view_name(self, model):
|
||||||
|
"""
|
||||||
|
Return the view name to use if 'view_name' is not specified in 'Meta'
|
||||||
|
"""
|
||||||
|
model_meta = model._meta
|
||||||
|
format_kwargs = {
|
||||||
|
'app_label': model_meta.app_label,
|
||||||
|
'model_name': model_meta.object_name.lower()
|
||||||
|
}
|
||||||
|
return self._default_view_name % format_kwargs
|
||||||
|
|
26
rest_framework/tests/description.py
Normal file
26
rest_framework/tests/description.py
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
# -- coding: utf-8 --
|
||||||
|
|
||||||
|
# Apparently there is a python 2.6 issue where docstrings of imported view classes
|
||||||
|
# do not retain their encoding information even if a module has a proper
|
||||||
|
# encoding declaration at the top of its source file. Therefore for tests
|
||||||
|
# to catch unicode related errors, a mock view has to be declared in a separate
|
||||||
|
# module.
|
||||||
|
|
||||||
|
from rest_framework.views import APIView
|
||||||
|
|
||||||
|
|
||||||
|
# test strings snatched from http://www.columbia.edu/~fdc/utf8/,
|
||||||
|
# http://winrus.com/utf8-jap.htm and memory
|
||||||
|
UTF8_TEST_DOCSTRING = (
|
||||||
|
'zażółć gęślą jaźń'
|
||||||
|
'Sîne klâwen durh die wolken sint geslagen'
|
||||||
|
'Τη γλώσσα μου έδωσαν ελληνική'
|
||||||
|
'யாமறிந்த மொழிகளிலே தமிழ்மொழி'
|
||||||
|
'На берегу пустынных волн'
|
||||||
|
'てすと'
|
||||||
|
'アイウエオカキクケコサシスセソタチツテ'
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class ViewWithNonASCIICharactersInDocstring(APIView):
|
||||||
|
__doc__ = UTF8_TEST_DOCSTRING
|
|
@ -428,6 +428,47 @@ class OAuthTests(TestCase):
|
||||||
response = self.csrf_client.post('/oauth-with-scope/', params)
|
response = self.csrf_client.post('/oauth-with-scope/', params)
|
||||||
self.assertEqual(response.status_code, 200)
|
self.assertEqual(response.status_code, 200)
|
||||||
|
|
||||||
|
@unittest.skipUnless(oauth_provider, 'django-oauth-plus not installed')
|
||||||
|
@unittest.skipUnless(oauth, 'oauth2 not installed')
|
||||||
|
def test_bad_consumer_key(self):
|
||||||
|
"""Ensure POSTing using HMAC_SHA1 signature method passes"""
|
||||||
|
params = {
|
||||||
|
'oauth_version': "1.0",
|
||||||
|
'oauth_nonce': oauth.generate_nonce(),
|
||||||
|
'oauth_timestamp': int(time.time()),
|
||||||
|
'oauth_token': self.token.key,
|
||||||
|
'oauth_consumer_key': 'badconsumerkey'
|
||||||
|
}
|
||||||
|
|
||||||
|
req = oauth.Request(method="POST", url="http://testserver/oauth/", parameters=params)
|
||||||
|
|
||||||
|
signature_method = oauth.SignatureMethod_HMAC_SHA1()
|
||||||
|
req.sign_request(signature_method, self.consumer, self.token)
|
||||||
|
auth = req.to_header()["Authorization"]
|
||||||
|
|
||||||
|
response = self.csrf_client.post('/oauth/', HTTP_AUTHORIZATION=auth)
|
||||||
|
self.assertEqual(response.status_code, 401)
|
||||||
|
|
||||||
|
@unittest.skipUnless(oauth_provider, 'django-oauth-plus not installed')
|
||||||
|
@unittest.skipUnless(oauth, 'oauth2 not installed')
|
||||||
|
def test_bad_token_key(self):
|
||||||
|
"""Ensure POSTing using HMAC_SHA1 signature method passes"""
|
||||||
|
params = {
|
||||||
|
'oauth_version': "1.0",
|
||||||
|
'oauth_nonce': oauth.generate_nonce(),
|
||||||
|
'oauth_timestamp': int(time.time()),
|
||||||
|
'oauth_token': 'badtokenkey',
|
||||||
|
'oauth_consumer_key': self.consumer.key
|
||||||
|
}
|
||||||
|
|
||||||
|
req = oauth.Request(method="POST", url="http://testserver/oauth/", parameters=params)
|
||||||
|
|
||||||
|
signature_method = oauth.SignatureMethod_HMAC_SHA1()
|
||||||
|
req.sign_request(signature_method, self.consumer, self.token)
|
||||||
|
auth = req.to_header()["Authorization"]
|
||||||
|
|
||||||
|
response = self.csrf_client.post('/oauth/', HTTP_AUTHORIZATION=auth)
|
||||||
|
self.assertEqual(response.status_code, 401)
|
||||||
|
|
||||||
class OAuth2Tests(TestCase):
|
class OAuth2Tests(TestCase):
|
||||||
"""OAuth 2.0 authentication"""
|
"""OAuth 2.0 authentication"""
|
||||||
|
|
|
@ -2,8 +2,10 @@
|
||||||
|
|
||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
from django.test import TestCase
|
from django.test import TestCase
|
||||||
|
from rest_framework.compat import apply_markdown, smart_text
|
||||||
from rest_framework.views import APIView
|
from rest_framework.views import APIView
|
||||||
from rest_framework.compat import apply_markdown
|
from rest_framework.tests.description import ViewWithNonASCIICharactersInDocstring
|
||||||
|
from rest_framework.tests.description import UTF8_TEST_DOCSTRING
|
||||||
from rest_framework.utils.formatting import get_view_name, get_view_description
|
from rest_framework.utils.formatting import get_view_name, get_view_description
|
||||||
|
|
||||||
# We check that docstrings get nicely un-indented.
|
# We check that docstrings get nicely un-indented.
|
||||||
|
@ -83,11 +85,10 @@ class TestViewNamesAndDescriptions(TestCase):
|
||||||
Unicode in docstrings should be respected.
|
Unicode in docstrings should be respected.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
class MockView(APIView):
|
self.assertEqual(
|
||||||
"""Проверка"""
|
get_view_description(ViewWithNonASCIICharactersInDocstring),
|
||||||
pass
|
smart_text(UTF8_TEST_DOCSTRING)
|
||||||
|
)
|
||||||
self.assertEqual(get_view_description(MockView), "Проверка")
|
|
||||||
|
|
||||||
def test_view_description_can_be_empty(self):
|
def test_view_description_can_be_empty(self):
|
||||||
"""
|
"""
|
||||||
|
|
|
@ -866,3 +866,33 @@ class FieldCallableDefault(TestCase):
|
||||||
into = {}
|
into = {}
|
||||||
field.field_from_native({}, {}, 'field', into)
|
field.field_from_native({}, {}, 'field', into)
|
||||||
self.assertEqual(into, {'field': 'foo bar'})
|
self.assertEqual(into, {'field': 'foo bar'})
|
||||||
|
|
||||||
|
|
||||||
|
class CustomIntegerField(TestCase):
|
||||||
|
"""
|
||||||
|
Test that custom fields apply min_value and max_value constraints
|
||||||
|
"""
|
||||||
|
def test_custom_fields_can_be_validated_for_value(self):
|
||||||
|
|
||||||
|
class MoneyField(models.PositiveIntegerField):
|
||||||
|
pass
|
||||||
|
|
||||||
|
class EntryModel(models.Model):
|
||||||
|
bank = MoneyField(validators=[validators.MaxValueValidator(100)])
|
||||||
|
|
||||||
|
class EntrySerializer(serializers.ModelSerializer):
|
||||||
|
class Meta:
|
||||||
|
model = EntryModel
|
||||||
|
|
||||||
|
entry = EntryModel(bank=1)
|
||||||
|
|
||||||
|
serializer = EntrySerializer(entry, data={"bank": 11})
|
||||||
|
self.assertTrue(serializer.is_valid())
|
||||||
|
|
||||||
|
serializer = EntrySerializer(entry, data={"bank": -1})
|
||||||
|
self.assertFalse(serializer.is_valid())
|
||||||
|
|
||||||
|
serializer = EntrySerializer(entry, data={"bank": 101})
|
||||||
|
self.assertFalse(serializer.is_valid())
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -301,3 +301,30 @@ class TestOptionalRelationHyperlinkedView(TestCase):
|
||||||
data=json.dumps(self.data),
|
data=json.dumps(self.data),
|
||||||
content_type='application/json')
|
content_type='application/json')
|
||||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||||
|
|
||||||
|
|
||||||
|
class TestOverriddenURLField(TestCase):
|
||||||
|
def setUp(self):
|
||||||
|
class OverriddenURLSerializer(serializers.HyperlinkedModelSerializer):
|
||||||
|
url = serializers.SerializerMethodField('get_url')
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = BlogPost
|
||||||
|
fields = ('title', 'url')
|
||||||
|
|
||||||
|
def get_url(self, obj):
|
||||||
|
return 'foo bar'
|
||||||
|
|
||||||
|
self.Serializer = OverriddenURLSerializer
|
||||||
|
self.obj = BlogPost.objects.create(title='New blog post')
|
||||||
|
|
||||||
|
def test_overridden_url_field(self):
|
||||||
|
"""
|
||||||
|
The 'url' field should respect overriding.
|
||||||
|
Regression test for #936.
|
||||||
|
"""
|
||||||
|
serializer = self.Serializer(self.obj)
|
||||||
|
self.assertEqual(
|
||||||
|
serializer.data,
|
||||||
|
{'title': 'New blog post', 'url': 'foo bar'}
|
||||||
|
)
|
||||||
|
|
|
@ -2,11 +2,12 @@ from __future__ import unicode_literals
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.test import TestCase
|
from django.test import TestCase
|
||||||
from django.test.client import RequestFactory
|
from django.test.client import RequestFactory
|
||||||
from rest_framework import serializers, viewsets
|
from django.core.exceptions import ImproperlyConfigured
|
||||||
|
from rest_framework import serializers, viewsets, permissions
|
||||||
from rest_framework.compat import include, patterns, url
|
from rest_framework.compat import include, patterns, url
|
||||||
from rest_framework.decorators import link, action
|
from rest_framework.decorators import link, action
|
||||||
from rest_framework.response import Response
|
from rest_framework.response import Response
|
||||||
from rest_framework.routers import SimpleRouter
|
from rest_framework.routers import SimpleRouter, DefaultRouter
|
||||||
|
|
||||||
factory = RequestFactory()
|
factory = RequestFactory()
|
||||||
|
|
||||||
|
@ -120,7 +121,7 @@ class TestCustomLookupFields(TestCase):
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class TestTrailingSlash(TestCase):
|
class TestTrailingSlashIncluded(TestCase):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
class NoteViewSet(viewsets.ModelViewSet):
|
class NoteViewSet(viewsets.ModelViewSet):
|
||||||
model = RouterTestModel
|
model = RouterTestModel
|
||||||
|
@ -135,7 +136,7 @@ class TestTrailingSlash(TestCase):
|
||||||
self.assertEqual(expected[idx], self.urls[idx].regex.pattern)
|
self.assertEqual(expected[idx], self.urls[idx].regex.pattern)
|
||||||
|
|
||||||
|
|
||||||
class TestTrailingSlash(TestCase):
|
class TestTrailingSlashRemoved(TestCase):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
class NoteViewSet(viewsets.ModelViewSet):
|
class NoteViewSet(viewsets.ModelViewSet):
|
||||||
model = RouterTestModel
|
model = RouterTestModel
|
||||||
|
@ -148,3 +149,67 @@ class TestTrailingSlash(TestCase):
|
||||||
expected = ['^notes$', '^notes/(?P<pk>[^/]+)$']
|
expected = ['^notes$', '^notes/(?P<pk>[^/]+)$']
|
||||||
for idx in range(len(expected)):
|
for idx in range(len(expected)):
|
||||||
self.assertEqual(expected[idx], self.urls[idx].regex.pattern)
|
self.assertEqual(expected[idx], self.urls[idx].regex.pattern)
|
||||||
|
|
||||||
|
|
||||||
|
class TestNameableRoot(TestCase):
|
||||||
|
def setUp(self):
|
||||||
|
class NoteViewSet(viewsets.ModelViewSet):
|
||||||
|
model = RouterTestModel
|
||||||
|
self.router = DefaultRouter()
|
||||||
|
self.router.root_view_name = 'nameable-root'
|
||||||
|
self.router.register(r'notes', NoteViewSet)
|
||||||
|
self.urls = self.router.urls
|
||||||
|
|
||||||
|
def test_router_has_custom_name(self):
|
||||||
|
expected = 'nameable-root'
|
||||||
|
self.assertEqual(expected, self.urls[0].name)
|
||||||
|
|
||||||
|
|
||||||
|
class TestActionKeywordArgs(TestCase):
|
||||||
|
"""
|
||||||
|
Ensure keyword arguments passed in the `@action` decorator
|
||||||
|
are properly handled. Refs #940.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
class TestViewSet(viewsets.ModelViewSet):
|
||||||
|
permission_classes = []
|
||||||
|
|
||||||
|
@action(permission_classes=[permissions.AllowAny])
|
||||||
|
def custom(self, request, *args, **kwargs):
|
||||||
|
return Response({
|
||||||
|
'permission_classes': self.permission_classes
|
||||||
|
})
|
||||||
|
|
||||||
|
self.router = SimpleRouter()
|
||||||
|
self.router.register(r'test', TestViewSet, base_name='test')
|
||||||
|
self.view = self.router.urls[-1].callback
|
||||||
|
|
||||||
|
def test_action_kwargs(self):
|
||||||
|
request = factory.post('/test/0/custom/')
|
||||||
|
response = self.view(request)
|
||||||
|
self.assertEqual(
|
||||||
|
response.data,
|
||||||
|
{'permission_classes': [permissions.AllowAny]}
|
||||||
|
)
|
||||||
|
|
||||||
|
class TestActionAppliedToExistingRoute(TestCase):
|
||||||
|
"""
|
||||||
|
Ensure `@action` decorator raises an except when applied
|
||||||
|
to an existing route
|
||||||
|
"""
|
||||||
|
|
||||||
|
def test_exception_raised_when_action_applied_to_existing_route(self):
|
||||||
|
class TestViewSet(viewsets.ModelViewSet):
|
||||||
|
|
||||||
|
@action()
|
||||||
|
def retrieve(self, request, *args, **kwargs):
|
||||||
|
return Response({
|
||||||
|
'hello': 'world'
|
||||||
|
})
|
||||||
|
|
||||||
|
self.router = SimpleRouter()
|
||||||
|
self.router.register(r'test', TestViewSet, base_name='test')
|
||||||
|
|
||||||
|
with self.assertRaises(ImproperlyConfigured):
|
||||||
|
self.router.urls
|
||||||
|
|
|
@ -7,7 +7,7 @@ from django.contrib.auth.models import User
|
||||||
from django.core.cache import cache
|
from django.core.cache import cache
|
||||||
from django.test.client import RequestFactory
|
from django.test.client import RequestFactory
|
||||||
from rest_framework.views import APIView
|
from rest_framework.views import APIView
|
||||||
from rest_framework.throttling import UserRateThrottle
|
from rest_framework.throttling import UserRateThrottle, ScopedRateThrottle
|
||||||
from rest_framework.response import Response
|
from rest_framework.response import Response
|
||||||
|
|
||||||
|
|
||||||
|
@ -36,8 +36,6 @@ class MockView_MinuteThrottling(APIView):
|
||||||
|
|
||||||
|
|
||||||
class ThrottlingTests(TestCase):
|
class ThrottlingTests(TestCase):
|
||||||
urls = 'rest_framework.tests.test_throttling'
|
|
||||||
|
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
"""
|
"""
|
||||||
Reset the cache so that no throttles will be active
|
Reset the cache so that no throttles will be active
|
||||||
|
@ -141,3 +139,108 @@ class ThrottlingTests(TestCase):
|
||||||
(60, None),
|
(60, None),
|
||||||
(80, None)
|
(80, None)
|
||||||
))
|
))
|
||||||
|
|
||||||
|
|
||||||
|
class ScopedRateThrottleTests(TestCase):
|
||||||
|
"""
|
||||||
|
Tests for ScopedRateThrottle.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
class XYScopedRateThrottle(ScopedRateThrottle):
|
||||||
|
TIMER_SECONDS = 0
|
||||||
|
THROTTLE_RATES = {'x': '3/min', 'y': '1/min'}
|
||||||
|
timer = lambda self: self.TIMER_SECONDS
|
||||||
|
|
||||||
|
class XView(APIView):
|
||||||
|
throttle_classes = (XYScopedRateThrottle,)
|
||||||
|
throttle_scope = 'x'
|
||||||
|
|
||||||
|
def get(self, request):
|
||||||
|
return Response('x')
|
||||||
|
|
||||||
|
class YView(APIView):
|
||||||
|
throttle_classes = (XYScopedRateThrottle,)
|
||||||
|
throttle_scope = 'y'
|
||||||
|
|
||||||
|
def get(self, request):
|
||||||
|
return Response('y')
|
||||||
|
|
||||||
|
class UnscopedView(APIView):
|
||||||
|
throttle_classes = (XYScopedRateThrottle,)
|
||||||
|
|
||||||
|
def get(self, request):
|
||||||
|
return Response('y')
|
||||||
|
|
||||||
|
self.throttle_class = XYScopedRateThrottle
|
||||||
|
self.factory = RequestFactory()
|
||||||
|
self.x_view = XView.as_view()
|
||||||
|
self.y_view = YView.as_view()
|
||||||
|
self.unscoped_view = UnscopedView.as_view()
|
||||||
|
|
||||||
|
def increment_timer(self, seconds=1):
|
||||||
|
self.throttle_class.TIMER_SECONDS += seconds
|
||||||
|
|
||||||
|
def test_scoped_rate_throttle(self):
|
||||||
|
request = self.factory.get('/')
|
||||||
|
|
||||||
|
# Should be able to hit x view 3 times per minute.
|
||||||
|
response = self.x_view(request)
|
||||||
|
self.assertEqual(200, response.status_code)
|
||||||
|
|
||||||
|
self.increment_timer()
|
||||||
|
response = self.x_view(request)
|
||||||
|
self.assertEqual(200, response.status_code)
|
||||||
|
|
||||||
|
self.increment_timer()
|
||||||
|
response = self.x_view(request)
|
||||||
|
self.assertEqual(200, response.status_code)
|
||||||
|
|
||||||
|
self.increment_timer()
|
||||||
|
response = self.x_view(request)
|
||||||
|
self.assertEqual(429, response.status_code)
|
||||||
|
|
||||||
|
# Should be able to hit y view 1 time per minute.
|
||||||
|
self.increment_timer()
|
||||||
|
response = self.y_view(request)
|
||||||
|
self.assertEqual(200, response.status_code)
|
||||||
|
|
||||||
|
self.increment_timer()
|
||||||
|
response = self.y_view(request)
|
||||||
|
self.assertEqual(429, response.status_code)
|
||||||
|
|
||||||
|
# Ensure throttles properly reset by advancing the rest of the minute
|
||||||
|
self.increment_timer(55)
|
||||||
|
|
||||||
|
# Should still be able to hit x view 3 times per minute.
|
||||||
|
response = self.x_view(request)
|
||||||
|
self.assertEqual(200, response.status_code)
|
||||||
|
|
||||||
|
self.increment_timer()
|
||||||
|
response = self.x_view(request)
|
||||||
|
self.assertEqual(200, response.status_code)
|
||||||
|
|
||||||
|
self.increment_timer()
|
||||||
|
response = self.x_view(request)
|
||||||
|
self.assertEqual(200, response.status_code)
|
||||||
|
|
||||||
|
self.increment_timer()
|
||||||
|
response = self.x_view(request)
|
||||||
|
self.assertEqual(429, response.status_code)
|
||||||
|
|
||||||
|
# Should still be able to hit y view 1 time per minute.
|
||||||
|
self.increment_timer()
|
||||||
|
response = self.y_view(request)
|
||||||
|
self.assertEqual(200, response.status_code)
|
||||||
|
|
||||||
|
self.increment_timer()
|
||||||
|
response = self.y_view(request)
|
||||||
|
self.assertEqual(429, response.status_code)
|
||||||
|
|
||||||
|
def test_unscoped_view_not_throttled(self):
|
||||||
|
request = self.factory.get('/')
|
||||||
|
|
||||||
|
for idx in range(10):
|
||||||
|
self.increment_timer()
|
||||||
|
response = self.unscoped_view(request)
|
||||||
|
self.assertEqual(200, response.status_code)
|
||||||
|
|
|
@ -3,7 +3,7 @@ Provides various throttling policies.
|
||||||
"""
|
"""
|
||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
from django.core.cache import cache
|
from django.core.cache import cache
|
||||||
from rest_framework import exceptions
|
from django.core.exceptions import ImproperlyConfigured
|
||||||
from rest_framework.settings import api_settings
|
from rest_framework.settings import api_settings
|
||||||
import time
|
import time
|
||||||
|
|
||||||
|
@ -40,9 +40,9 @@ class SimpleRateThrottle(BaseThrottle):
|
||||||
"""
|
"""
|
||||||
|
|
||||||
timer = time.time
|
timer = time.time
|
||||||
settings = api_settings
|
|
||||||
cache_format = 'throtte_%(scope)s_%(ident)s'
|
cache_format = 'throtte_%(scope)s_%(ident)s'
|
||||||
scope = None
|
scope = None
|
||||||
|
THROTTLE_RATES = api_settings.DEFAULT_THROTTLE_RATES
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
if not getattr(self, 'rate', None):
|
if not getattr(self, 'rate', None):
|
||||||
|
@ -65,13 +65,13 @@ class SimpleRateThrottle(BaseThrottle):
|
||||||
if not getattr(self, 'scope', None):
|
if not getattr(self, 'scope', None):
|
||||||
msg = ("You must set either `.scope` or `.rate` for '%s' throttle" %
|
msg = ("You must set either `.scope` or `.rate` for '%s' throttle" %
|
||||||
self.__class__.__name__)
|
self.__class__.__name__)
|
||||||
raise exceptions.ConfigurationError(msg)
|
raise ImproperlyConfigured(msg)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
return self.settings.DEFAULT_THROTTLE_RATES[self.scope]
|
return self.THROTTLE_RATES[self.scope]
|
||||||
except KeyError:
|
except KeyError:
|
||||||
msg = "No default throttle rate set for '%s' scope" % self.scope
|
msg = "No default throttle rate set for '%s' scope" % self.scope
|
||||||
raise exceptions.ConfigurationError(msg)
|
raise ImproperlyConfigured(msg)
|
||||||
|
|
||||||
def parse_rate(self, rate):
|
def parse_rate(self, rate):
|
||||||
"""
|
"""
|
||||||
|
@ -187,6 +187,27 @@ class ScopedRateThrottle(SimpleRateThrottle):
|
||||||
"""
|
"""
|
||||||
scope_attr = 'throttle_scope'
|
scope_attr = 'throttle_scope'
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
# Override the usual SimpleRateThrottle, because we can't determine
|
||||||
|
# the rate until called by the view.
|
||||||
|
pass
|
||||||
|
|
||||||
|
def allow_request(self, request, view):
|
||||||
|
# We can only determine the scope once we're called by the view.
|
||||||
|
self.scope = getattr(view, self.scope_attr, None)
|
||||||
|
|
||||||
|
# If a view does not have a `throttle_scope` always allow the request
|
||||||
|
if not self.scope:
|
||||||
|
return True
|
||||||
|
|
||||||
|
# Determine the allowed request rate as we normally would during
|
||||||
|
# the `__init__` call.
|
||||||
|
self.rate = self.get_rate()
|
||||||
|
self.num_requests, self.duration = self.parse_rate(self.rate)
|
||||||
|
|
||||||
|
# We can now proceed as normal.
|
||||||
|
return super(ScopedRateThrottle, self).allow_request(request, view)
|
||||||
|
|
||||||
def get_cache_key(self, request, view):
|
def get_cache_key(self, request, view):
|
||||||
"""
|
"""
|
||||||
If `view.throttle_scope` is not set, don't apply this throttle.
|
If `view.throttle_scope` is not set, don't apply this throttle.
|
||||||
|
@ -194,18 +215,12 @@ class ScopedRateThrottle(SimpleRateThrottle):
|
||||||
Otherwise generate the unique cache key by concatenating the user id
|
Otherwise generate the unique cache key by concatenating the user id
|
||||||
with the '.throttle_scope` property of the view.
|
with the '.throttle_scope` property of the view.
|
||||||
"""
|
"""
|
||||||
scope = getattr(view, self.scope_attr, None)
|
|
||||||
|
|
||||||
if not scope:
|
|
||||||
# Only throttle views if `.throttle_scope` is set on the view.
|
|
||||||
return None
|
|
||||||
|
|
||||||
if request.user.is_authenticated():
|
if request.user.is_authenticated():
|
||||||
ident = request.user.id
|
ident = request.user.id
|
||||||
else:
|
else:
|
||||||
ident = request.META.get('REMOTE_ADDR', None)
|
ident = request.META.get('REMOTE_ADDR', None)
|
||||||
|
|
||||||
return self.cache_format % {
|
return self.cache_format % {
|
||||||
'scope': scope,
|
'scope': self.scope,
|
||||||
'ident': ident
|
'ident': ident
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,7 +5,7 @@ from __future__ import unicode_literals
|
||||||
|
|
||||||
from django.utils.html import escape
|
from django.utils.html import escape
|
||||||
from django.utils.safestring import mark_safe
|
from django.utils.safestring import mark_safe
|
||||||
from rest_framework.compat import apply_markdown
|
from rest_framework.compat import apply_markdown, smart_text
|
||||||
import re
|
import re
|
||||||
|
|
||||||
|
|
||||||
|
@ -63,7 +63,7 @@ def get_view_description(cls, html=False):
|
||||||
Return a description for an `APIView` class or `@api_view` function.
|
Return a description for an `APIView` class or `@api_view` function.
|
||||||
"""
|
"""
|
||||||
description = cls.__doc__ or ''
|
description = cls.__doc__ or ''
|
||||||
description = _remove_leading_indent(description)
|
description = _remove_leading_indent(smart_text(description))
|
||||||
if html:
|
if html:
|
||||||
return markup_description(description)
|
return markup_description(description)
|
||||||
return description
|
return description
|
||||||
|
|
|
@ -4,11 +4,11 @@ Provides an APIView class that is the base of all views in REST framework.
|
||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
from django.core.exceptions import PermissionDenied
|
from django.core.exceptions import PermissionDenied
|
||||||
from django.http import Http404, HttpResponse
|
from django.http import Http404
|
||||||
from django.utils.datastructures import SortedDict
|
from django.utils.datastructures import SortedDict
|
||||||
from django.views.decorators.csrf import csrf_exempt
|
from django.views.decorators.csrf import csrf_exempt
|
||||||
from rest_framework import status, exceptions
|
from rest_framework import status, exceptions
|
||||||
from rest_framework.compat import View
|
from rest_framework.compat import View, HttpResponseBase
|
||||||
from rest_framework.request import Request
|
from rest_framework.request import Request
|
||||||
from rest_framework.response import Response
|
from rest_framework.response import Response
|
||||||
from rest_framework.settings import api_settings
|
from rest_framework.settings import api_settings
|
||||||
|
@ -244,9 +244,10 @@ class APIView(View):
|
||||||
Returns the final response object.
|
Returns the final response object.
|
||||||
"""
|
"""
|
||||||
# Make the error obvious if a proper response is not returned
|
# Make the error obvious if a proper response is not returned
|
||||||
assert isinstance(response, HttpResponse), (
|
assert isinstance(response, HttpResponseBase), (
|
||||||
'Expected a `Response` to be returned from the view, '
|
'Expected a `Response`, `HttpResponse` or `HttpStreamingResponse` '
|
||||||
'but received a `%s`' % type(response)
|
'to be returned from the view, but received a `%s`'
|
||||||
|
% type(response)
|
||||||
)
|
)
|
||||||
|
|
||||||
if isinstance(response, Response):
|
if isinstance(response, Response):
|
||||||
|
@ -304,10 +305,10 @@ class APIView(View):
|
||||||
`.dispatch()` is pretty much the same as Django's regular dispatch,
|
`.dispatch()` is pretty much the same as Django's regular dispatch,
|
||||||
but with extra hooks for startup, finalize, and exception handling.
|
but with extra hooks for startup, finalize, and exception handling.
|
||||||
"""
|
"""
|
||||||
request = self.initialize_request(request, *args, **kwargs)
|
|
||||||
self.request = request
|
|
||||||
self.args = args
|
self.args = args
|
||||||
self.kwargs = kwargs
|
self.kwargs = kwargs
|
||||||
|
request = self.initialize_request(request, *args, **kwargs)
|
||||||
|
self.request = request
|
||||||
self.headers = self.default_response_headers # deprecate?
|
self.headers = self.default_response_headers # deprecate?
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
@ -341,8 +342,15 @@ class APIView(View):
|
||||||
Return a dictionary of metadata about the view.
|
Return a dictionary of metadata about the view.
|
||||||
Used to return responses for OPTIONS requests.
|
Used to return responses for OPTIONS requests.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
# This is used by ViewSets to disambiguate instance vs list views
|
||||||
|
view_name_suffix = getattr(self, 'suffix', None)
|
||||||
|
|
||||||
|
# By default we can't provide any form-like information, however the
|
||||||
|
# generic views override this implementation and add additional
|
||||||
|
# information for POST and PUT methods, based on the serializer.
|
||||||
ret = SortedDict()
|
ret = SortedDict()
|
||||||
ret['name'] = get_view_name(self.__class__)
|
ret['name'] = get_view_name(self.__class__, view_name_suffix)
|
||||||
ret['description'] = get_view_description(self.__class__)
|
ret['description'] = get_view_description(self.__class__)
|
||||||
ret['renders'] = [renderer.media_type for renderer in self.renderer_classes]
|
ret['renders'] = [renderer.media_type for renderer in self.renderer_classes]
|
||||||
ret['parses'] = [parser.media_type for parser in self.parser_classes]
|
ret['parses'] = [parser.media_type for parser in self.parser_classes]
|
||||||
|
|
Loading…
Reference in New Issue
Block a user