Commit 1aa77830 by Eleni Lixourioti

Merge branch 'version-3.1' of github.com:tomchristie/django-rest-framework into oauth_as_package

Conflicts:
	.travis.yml
parents afaa52a3 88008c0a
language: python language: python
python: python: 2.7
- "2.6"
- "2.7"
- "3.2"
- "3.3"
- "3.4"
env: env:
- DJANGO="django==1.7" - TOX_ENV=flake8
- DJANGO="django==1.6.5" - TOX_ENV=py3.4-django1.7
- DJANGO="django==1.5.8" - TOX_ENV=py3.3-django1.7
- DJANGO="django==1.4.13" - TOX_ENV=py3.2-django1.7
- TOX_ENV=py2.7-django1.7
- TOX_ENV=py3.4-django1.6
- TOX_ENV=py3.3-django1.6
- TOX_ENV=py3.2-django1.6
- TOX_ENV=py2.7-django1.6
- TOX_ENV=py2.6-django1.6
- TOX_ENV=py3.4-django1.5
- TOX_ENV=py3.3-django1.5
- TOX_ENV=py3.2-django1.5
- TOX_ENV=py2.7-django1.5
- TOX_ENV=py2.6-django1.5
- TOX_ENV=py2.7-django1.4
- TOX_ENV=py2.6-django1.4
install: install:
- pip install $DJANGO - "pip install tox --download-cache $HOME/.pip-cache"
- pip install defusedxml==0.3
- pip install Pillow==2.3.0
- pip install django-guardian==1.2.3
- pip install pytest-django==2.6.1
- pip install flake8==2.2.2
- "if [[ ${DJANGO::11} == 'django==1.3' ]]; then pip install django-filter==0.5.4; fi"
- "if [[ ${DJANGO::11} != 'django==1.3' ]]; then pip install django-filter==0.7; fi"
- "if [[ ${DJANGO} == 'django==1.7' ]]; then pip install -e git+https://github.com/linovia/django-guardian.git@feature/django_1_7#egg=django-guardian-1.2.0; fi"
- export PYTHONPATH=.
script: script:
- ./runtests.py - tox -e $TOX_ENV
matrix:
exclude:
- python: "2.6"
env: DJANGO="django==1.7"
- python: "3.2"
env: DJANGO="django==1.4.13"
- python: "3.3"
env: DJANGO="django==1.4.13"
- python: "3.4"
env: DJANGO="django==1.4.13"
...@@ -84,7 +84,7 @@ Note that the exception handler will only be called for responses generated by r ...@@ -84,7 +84,7 @@ Note that the exception handler will only be called for responses generated by r
**Signature:** `APIException()` **Signature:** `APIException()`
The **base class** for all exceptions raised inside REST framework. The **base class** for all exceptions raised inside an `APIView` class or `@api_view`.
To provide a custom exception, subclass `APIException` and set the `.status_code` and `.default_detail` properties on the class. To provide a custom exception, subclass `APIException` and set the `.status_code` and `.default_detail` properties on the class.
......
...@@ -274,7 +274,27 @@ Corresponds to `django.db.models.fields.FloatField`. ...@@ -274,7 +274,27 @@ Corresponds to `django.db.models.fields.FloatField`.
## DecimalField ## DecimalField
A decimal representation. A decimal representation, represented in Python by a Decimal instance.
Has two required arguments:
- `max_digits` The maximum number of digits allowed in the number. Note that this number must be greater than or equal to decimal_places.
- `decimal_places` The number of decimal places to store with the number.
For example, to validate numbers up to 999 with a resolution of 2 decimal places, you would use:
serializers.DecimalField(max_digits=5, decimal_places=2)
And to validate numbers up to anything lesss than one billion with a resolution of 10 decimal places:
serializers.DecimalField(max_digits=19, decimal_places=10)
This field also takes an optional argument, `coerce_to_string`. If set to `True` the representation will be output as a string. If set to `False` the representation will be left as a `Decimal` instance and the final representation will be determined by the renderer.
If unset, this will default to the same value as the `COERCE_DECIMAL_TO_STRING` setting, which is `True` unless set otherwise.
**Signature:** `DecimalField(max_digits, decimal_places, coerce_to_string=None)`
Corresponds to `django.db.models.fields.DecimalField`. Corresponds to `django.db.models.fields.DecimalField`.
......
...@@ -193,7 +193,7 @@ filters using `Manufacturer` name. For example: ...@@ -193,7 +193,7 @@ filters using `Manufacturer` name. For example:
class ProductFilter(django_filters.FilterSet): class ProductFilter(django_filters.FilterSet):
class Meta: class Meta:
model = Product model = Product
fields = ['category', 'in_stock', 'manufacturer__name`] fields = ['category', 'in_stock', 'manufacturer__name']
This enables us to make queries like: This enables us to make queries like:
...@@ -211,7 +211,7 @@ This is nice, but it exposes the Django's double underscore convention as part o ...@@ -211,7 +211,7 @@ This is nice, but it exposes the Django's double underscore convention as part o
class Meta: class Meta:
model = Product model = Product
fields = ['category', 'in_stock', 'manufacturer`] fields = ['category', 'in_stock', 'manufacturer']
And now you can execute: And now you can execute:
......
...@@ -212,8 +212,6 @@ Provides a `.list(request, *args, **kwargs)` method, that implements listing a q ...@@ -212,8 +212,6 @@ Provides a `.list(request, *args, **kwargs)` method, that implements listing a q
If the queryset is populated, this returns a `200 OK` response, with a serialized representation of the queryset as the body of the response. The response data may optionally be paginated. If the queryset is populated, this returns a `200 OK` response, with a serialized representation of the queryset as the body of the response. The response data may optionally be paginated.
If the queryset is empty this returns a `200 OK` response, unless the `.allow_empty` attribute on the view is set to `False`, in which case it will return a `404 Not Found`.
## CreateModelMixin ## CreateModelMixin
Provides a `.create(request, *args, **kwargs)` method, that implements creating and saving a new model instance. Provides a `.create(request, *args, **kwargs)` method, that implements creating and saving a new model instance.
......
...@@ -74,37 +74,18 @@ If your API includes views that can serve both regular webpages and API response ...@@ -74,37 +74,18 @@ If your API includes views that can serve both regular webpages and API response
Renders the request data into `JSON`, using utf-8 encoding. Renders the request data into `JSON`, using utf-8 encoding.
Note that non-ascii characters will be rendered using JSON's `\uXXXX` character escape. For example: Note that the default style is to include unicode characters, and render the response using a compact style with no uneccessary whitespace:
{"unicode black star": "\u2605"} {"unicode black star":"★","value":999}
The client may additionally include an `'indent'` media type parameter, in which case the returned `JSON` will be indented. For example `Accept: application/json; indent=4`. The client may additionally include an `'indent'` media type parameter, in which case the returned `JSON` will be indented. For example `Accept: application/json; indent=4`.
{ {
"unicode black star": "\u2605" "unicode black star": "★",
"value": 999
} }
**.media_type**: `application/json` The default JSON encoding style can be altered using the `UNICODE_JSON` and `COMPACT_JSON` settings keys.
**.format**: `'.json'`
**.charset**: `None`
## UnicodeJSONRenderer
Renders the request data into `JSON`, using utf-8 encoding.
Note that non-ascii characters will not be character escaped. For example:
{"unicode black star": "★"}
The client may additionally include an `'indent'` media type parameter, in which case the returned `JSON` will be indented. For example `Accept: application/json; indent=4`.
{
"unicode black star": "★"
}
Both the `JSONRenderer` and `UnicodeJSONRenderer` styles conform to [RFC 4627][rfc4627], and are syntactically valid JSON.
**.media_type**: `application/json` **.media_type**: `application/json`
...@@ -444,6 +425,11 @@ Comma-separated values are a plain-text tabular data format, that can be easily ...@@ -444,6 +425,11 @@ Comma-separated values are a plain-text tabular data format, that can be easily
[djangorestframework-camel-case] provides camel case JSON renderers and parsers for REST framework. This allows serializers to use Python-style underscored field names, but be exposed in the API as Javascript-style camel case field names. It is maintained by [Vitaly Babiy][vbabiy]. [djangorestframework-camel-case] provides camel case JSON renderers and parsers for REST framework. This allows serializers to use Python-style underscored field names, but be exposed in the API as Javascript-style camel case field names. It is maintained by [Vitaly Babiy][vbabiy].
## Pandas (CSV, Excel, PNG)
[Django REST Pandas] provides a serializer and renderers that support additional data processing and output via the [Pandas] DataFrame API. Django REST Pandas includes renderers for Pandas-style CSV files, Excel workbooks (both `.xls` and `.xlsx`), and a number of [other formats]. It is maintained by [S. Andrew Sheppard][sheppard] as part of the [wq Project][wq].
[cite]: https://docs.djangoproject.com/en/dev/ref/template-response/#the-rendering-process [cite]: https://docs.djangoproject.com/en/dev/ref/template-response/#the-rendering-process
[conneg]: content-negotiation.md [conneg]: content-negotiation.md
[browser-accept-headers]: http://www.gethifi.com/blog/browser-rest-http-accept-headers [browser-accept-headers]: http://www.gethifi.com/blog/browser-rest-http-accept-headers
...@@ -467,3 +453,8 @@ Comma-separated values are a plain-text tabular data format, that can be easily ...@@ -467,3 +453,8 @@ Comma-separated values are a plain-text tabular data format, that can be easily
[hzy]: https://github.com/hzy [hzy]: https://github.com/hzy
[drf-ujson-renderer]: https://github.com/gizmag/drf-ujson-renderer [drf-ujson-renderer]: https://github.com/gizmag/drf-ujson-renderer
[djangorestframework-camel-case]: https://github.com/vbabiy/djangorestframework-camel-case [djangorestframework-camel-case]: https://github.com/vbabiy/djangorestframework-camel-case
[Django REST Pandas]: https://github.com/wq/django-rest-pandas
[Pandas]: http://pandas.pydata.org/
[other formats]: https://github.com/wq/django-rest-pandas#supported-formats
[sheppard]: https://github.com/sheppard
[wq]: https://github.com/wq
...@@ -265,7 +265,7 @@ A format string that should be used by default for rendering the output of `Date ...@@ -265,7 +265,7 @@ A format string that should be used by default for rendering the output of `Date
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: `'iso-8601'`
#### DATETIME_INPUT_FORMATS #### DATETIME_INPUT_FORMATS
...@@ -281,7 +281,7 @@ A format string that should be used by default for rendering the output of `Date ...@@ -281,7 +281,7 @@ A format string that should be used by default for rendering the output of `Date
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: `'iso-8601'`
#### DATE_INPUT_FORMATS #### DATE_INPUT_FORMATS
...@@ -297,7 +297,7 @@ A format string that should be used by default for rendering the output of `Time ...@@ -297,7 +297,7 @@ A format string that should be used by default for rendering the output of `Time
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: `'iso-8601'`
#### TIME_INPUT_FORMATS #### TIME_INPUT_FORMATS
...@@ -309,6 +309,46 @@ Default: `['iso-8601']` ...@@ -309,6 +309,46 @@ Default: `['iso-8601']`
--- ---
## Encodings
#### UNICODE_JSON
When set to `True`, JSON responses will allow unicode characters in responses. For example:
{"unicode black star":"★"}
When set to `False`, JSON responses will escape non-ascii characters, like so:
{"unicode black star":"\u2605"}
Both styles conform to [RFC 4627][rfc4627], and are syntactically valid JSON. The unicode style is prefered as being more user-friendly when inspecting API responses.
Default: `True`
#### COMPACT_JSON
When set to `True`, JSON responses will return compact representations, with no spacing after `':'` and `','` characters. For example:
{"is_admin":false,"email":"jane@example"}
When set to `False`, JSON responses will return slightly more verbose representations, like so:
{"is_admin": false, "email": "jane@example"}
The default style is to return minified responses, in line with [Heroku's API design guidelines][heroku-minified-json].
Default: `True`
#### COERCE_DECIMAL_TO_STRING
When returning decimal objects in API representations that do not support a native decimal type, it is normally best to return the value as a string. This avoids the loss of precision that occurs with binary floating point implementations.
When set to `True`, the serializer `DecimalField` class will return strings instead of `Decimal` objects. When set to `False`, serializers will return `Decimal` objects, which the default JSON encoder will return as floats.
Default: `True`
---
## View names and descriptions ## View names and descriptions
**The following settings are used to generate the view names and descriptions, as used in responses to `OPTIONS` requests, and as used in the browsable API.** **The following settings are used to generate the view names and descriptions, as used in responses to `OPTIONS` requests, and as used in the browsable API.**
...@@ -378,4 +418,6 @@ An integer of 0 or more, that may be used to specify the number of application p ...@@ -378,4 +418,6 @@ An integer of 0 or more, that may be used to specify the number of application p
Default: `None` Default: `None`
[cite]: http://www.python.org/dev/peps/pep-0020/ [cite]: http://www.python.org/dev/peps/pep-0020/
[rfc4627]: http://www.ietf.org/rfc/rfc4627.txt
[heroku-minified-json]: https://github.com/interagent/http-api-design#keep-json-minified-in-all-responses
[strftime]: http://docs.python.org/2/library/time.html#time.strftime [strftime]: http://docs.python.org/2/library/time.html#time.strftime
...@@ -178,6 +178,8 @@ To create a custom throttle, override `BaseThrottle` and implement `.allow_reque ...@@ -178,6 +178,8 @@ To create a custom throttle, override `BaseThrottle` and implement `.allow_reque
Optionally you may also override the `.wait()` method. If implemented, `.wait()` should return a recommended number of seconds to wait before attempting the next request, or `None`. The `.wait()` method will only be called if `.allow_request()` has previously returned `False`. Optionally you may also override the `.wait()` method. If implemented, `.wait()` should return a recommended number of seconds to wait before attempting the next request, or `None`. The `.wait()` method will only be called if `.allow_request()` has previously returned `False`.
If the `.wait()` method is implemented and the request is throttled, then a `Retry-After` header will be included in the response.
## Example ## Example
The following is an example of a rate throttle, that will randomly throttle 1 in every 10 requests. The following is an example of a rate throttle, that will randomly throttle 1 in every 10 requests.
......
...@@ -192,6 +192,7 @@ General guides to using REST framework. ...@@ -192,6 +192,7 @@ General guides to using REST framework.
* [Browser enhancements][browser-enhancements] * [Browser enhancements][browser-enhancements]
* [The Browsable API][browsableapi] * [The Browsable API][browsableapi]
* [REST, Hypermedia & HATEOAS][rest-hypermedia-hateoas] * [REST, Hypermedia & HATEOAS][rest-hypermedia-hateoas]
* [Third Party Resources][third-party-resources]
* [Contributing to REST framework][contributing] * [Contributing to REST framework][contributing]
* [2.0 Announcement][rest-framework-2-announcement] * [2.0 Announcement][rest-framework-2-announcement]
* [2.2 Announcement][2.2-announcement] * [2.2 Announcement][2.2-announcement]
...@@ -307,6 +308,7 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ...@@ -307,6 +308,7 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
[browsableapi]: topics/browsable-api.md [browsableapi]: topics/browsable-api.md
[rest-hypermedia-hateoas]: topics/rest-hypermedia-hateoas.md [rest-hypermedia-hateoas]: topics/rest-hypermedia-hateoas.md
[contributing]: topics/contributing.md [contributing]: topics/contributing.md
[third-party-resources]: topics/third-party-resources.md
[rest-framework-2-announcement]: topics/rest-framework-2-announcement.md [rest-framework-2-announcement]: topics/rest-framework-2-announcement.md
[2.2-announcement]: topics/2.2-announcement.md [2.2-announcement]: topics/2.2-announcement.md
[2.3-announcement]: topics/2.3-announcement.md [2.3-announcement]: topics/2.3-announcement.md
......
...@@ -117,6 +117,7 @@ a.fusion-poweredby { ...@@ -117,6 +117,7 @@ a.fusion-poweredby {
<li><a href="{{ base_url }}/topics/browser-enhancements{{ suffix }}">Browser enhancements</a></li> <li><a href="{{ base_url }}/topics/browser-enhancements{{ suffix }}">Browser enhancements</a></li>
<li><a href="{{ base_url }}/topics/browsable-api{{ suffix }}">The Browsable API</a></li> <li><a href="{{ base_url }}/topics/browsable-api{{ suffix }}">The Browsable API</a></li>
<li><a href="{{ base_url }}/topics/rest-hypermedia-hateoas{{ suffix }}">REST, Hypermedia & HATEOAS</a></li> <li><a href="{{ base_url }}/topics/rest-hypermedia-hateoas{{ suffix }}">REST, Hypermedia & HATEOAS</a></li>
<li><a href="{{ base_url }}/topics/third-party-resources{{ suffix }}">Third Party Resources</a></li>
<li><a href="{{ base_url }}/topics/contributing{{ suffix }}">Contributing to REST framework</a></li> <li><a href="{{ base_url }}/topics/contributing{{ suffix }}">Contributing to REST framework</a></li>
<li><a href="{{ base_url }}/topics/rest-framework-2-announcement{{ suffix }}">2.0 Announcement</a></li> <li><a href="{{ base_url }}/topics/rest-framework-2-announcement{{ suffix }}">2.0 Announcement</a></li>
<li><a href="{{ base_url }}/topics/2.2-announcement{{ suffix }}">2.2 Announcement</a></li> <li><a href="{{ base_url }}/topics/2.2-announcement{{ suffix }}">2.2 Announcement</a></li>
......
**THIS DOCUMENT IS CURRENTLY A WORK IN PROGRESS**
See the [Version 3.0 GitHub issue](https://github.com/tomchristie/django-rest-framework/pull/1800) for more details.
# REST framework 3.0
**Note incremental nature, discuss upgrading.**
## Motivation
**TODO**
---
## Request objects
#### The `request.data` property.
**TODO**
#### The parser API.
**TODO**
## Serializers
#### Single-step object creation.
**TODO**: Drop `.restore_object()`, use `.create()` and `.update()` which should save the instance.
**TODO**: Drop`.object`, use `.validated_data` or get the instance with `.save()`.
#### Always use `fields`, not `exclude`.
The `exclude` option is no longer available. You should use the more explicit `fields` option instead.
#### The `extra_kwargs` option.
The `read_only_fields` and `write_only_fields` options have been removed and replaced with a more generic `extra_kwargs`.
class MySerializer(serializer.ModelSerializer):
class Meta:
model = MyModel
fields = ('id', 'email', 'notes', 'is_admin')
extra_kwargs = {
'is_admin': {'read_only': True}
}
Alternatively, specify the field explicitly on the serializer class:
class MySerializer(serializer.ModelSerializer):
is_admin = serializers.BooleanField(read_only=True)
class Meta:
model = MyModel
fields = ('id', 'email', 'notes', 'is_admin')
#### Changes to `HyperlinkedModelSerializer`.
The `view_name` and `lookup_field` options have been removed. They are no longer required, as you can use the `extra_kwargs` argument instead:
class MySerializer(serializer.HyperlinkedModelSerializer):
class Meta:
model = MyModel
fields = ('url', 'email', 'notes', 'is_admin')
extra_kwargs = {
'url': {'lookup_field': 'uuid'}
}
Alternatively, specify the field explicitly on the serializer class:
class MySerializer(serializer.HyperlinkedModelSerializer):
url = serializers.HyperlinkedIdentityField(
view_name='mymodel-detail',
lookup_field='uuid'
)
class Meta:
model = MyModel
fields = ('url', 'email', 'notes', 'is_admin')
#### Fields for model methods and properties.
You can now specify field names in the `fields` option that refer to model methods or properties. For example, suppose you have the following model:
class Invitation(models.Model):
created = models.DateTimeField()
to_email = models.EmailField()
message = models.CharField(max_length=1000)
def expiry_date(self):
return self.created + datetime.timedelta(days=30)
You can include `expiry_date` as a field option on a `ModelSerializer` class.
class InvitationSerializer(serializers.ModelSerializer):
class Meta:
model = Invitation
fields = ('to_email', 'message', 'expiry_date')
These fields will be mapped to `serializers.ReadOnlyField()` instances.
>>> serializer = InvitationSerializer()
>>> print repr(serializer)
InvitationSerializer():
to_email = EmailField(max_length=75)
message = CharField(max_length=1000)
expiry_date = ReadOnlyField()
## Serializer fields
#### The `Field` and `ReadOnly` field classes.
**TODO**
#### Coercing output types.
**TODO**
#### The `ListSerializer` class.
**TODO**
#### The `MultipleChoiceField` class.
**TODO**
#### Changes to the custom field API.
**TODO** `to_representation`, `to_internal_value`.
#### Explicit `querysets` required on relational fields.
**TODO**
#### Optional argument to `SerializerMethodField`.
**TODO**
## Generic views
#### Simplification of view logic.
**TODO**
#### Removal of pre/post save hooks.
The following method hooks no longer exist on the new, simplified, generic views: `pre_save`, `post_save`, `pre_delete`, `post_delete`.
If you do need custom behavior, you might choose to instead override the `.save()` method on your serializer class. For example:
def save(self, *args, **kwargs):
instance = super(MySerializer).save(*args, **kwarg)
send_email(instance.to_email, instance.message)
return instance
Alternatively write your view logic exlpicitly, or tie your pre/post save behavior into the model class or model manager.
#### Removal of view attributes.
The `.object` and `.object_list` attributes are no longer set on the view instance. Treating views as mutable object instances that store state during the processing of the view tends to be poor design, and can lead to obscure flow logic.
I would personally recommend that developers treat view instances as immutable objects in their application code.
#### PUT as create.
**TODO**
## API style
There are some improvements in the default style we use in our API responses.
#### Unicode JSON by default.
Unicode JSON is now the default. The `UnicodeJSONRenderer` class no longer exists, and the `UNICODE_JSON` setting has been added. To revert this behavior use the new setting:
REST_FRAMEWORK = {
'UNICODE_JSON': False
}
#### Compact JSON by default.
We now output compact JSON in responses by default. For example, we return:
{"email":"amy@example.com","is_admin":true}
Instead of the following:
{"email": "amy@example.com", "is_admin": true}
The `COMPACT_JSON` setting has been added, and can be used to revert this behavior if needed:
REST_FRAMEWORK = {
'COMPACT_JSON': False
}
#### Throttle headers using `Retry-After`.
The custom `X-Throttle-Wait-Second` header has now been dropped in favor of the standard `Retry-After` header. You can revert this behavior if needed by writing a custom exception handler for your application.
#### Date and time objects as ISO-8859-1 strings in serializer data.
Date and Time objects are now coerced to strings by default in the serializer output. Previously they were returned as `Date`, `Time` and `DateTime` objects, and later coerced to strings by the renderer.
You can modify this behavior globally by settings the existing `DATE_FORMAT`, `DATETIME_FORMAT` and `TIME_FORMAT` settings keys. Setting these values to `None` instead of their default value of `'iso-8859-1'` will result in native objects being returned in serializer data.
REST_FRAMEWORK = {
# Return native `Date` and `Time` objects in `serializer.data`
'DATETIME_FORMAT': None
'DATE_FORMAT': None
'TIME_FORMAT': None
}
You can also modify serializer fields individually, using the `date_format`, `time_format` and `datetime_format` arguments:
# Return `DateTime` instances in `serializer.data`, not strings.
created = serializers.DateTimeField(format=None)
#### Decimals as strings in serializer data.
Decimals are now coerced to strings by default in the serializer output. Previously they were returned as `Decimal` objects, and later coerced to strings by the renderer.
You can modify this behavior globally by using the `COERCE_DECIMAL_TO_STRING` settings key.
REST_FRAMEWORK = {
'COERCE_DECIMAL_TO_STRING': False
}
Or modify it on an individual serializer field, using the `corece_to_string` keyword argument.
# Return `Decimal` instances in `serializer.data`, not strings.
amount = serializers.DecimalField(
max_digits=10,
decimal_places=2,
coerce_to_string=False
)
The default JSON renderer will return float objects for uncoerced `Decimal` instances. This allows you to easily switch between string or float representations for decimals depending on your API design needs.
# Third Party Resources
Django REST Framework has a growing community of developers, packages, and resources.
Check out a grid detailing all the packages and ecosystem around Django REST Framework at [Django Packages](https://www.djangopackages.com/grids/g/django-rest-framework/).
To submit new content, [open an issue](https://github.com/tomchristie/django-rest-framework/issues/new) or [create a pull request](https://github.com/tomchristie/django-rest-framework/).
## Libraries and Extensions
### Authentication
* [djangorestframework-digestauth](https://github.com/juanriaza/django-rest-framework-digestauth) - Provides Digest Access Authentication support.
* [django-oauth-toolkit](https://github.com/evonove/django-oauth-toolkit) - Provides OAuth 2.0 support.
* [doac](https://github.com/Rediker-Software/doac) - Provides OAuth 2.0 support.
* [djangorestframework-jwt](https://github.com/GetBlimp/django-rest-framework-jwt) - Provides JSON Web Token Authentication support.
* [hawkrest](https://github.com/kumar303/hawkrest) - Provides Hawk HTTP Authorization.
* [djangorestframework-httpsignature](https://github.com/etoccalino/django-rest-framework-httpsignature) - Provides an easy to use HTTP Signature Authentication mechanism.
### Permissions
* [drf-any-permissions](https://github.com/kevin-brown/drf-any-permissions) - Provides alternative permission handling.
* [djangorestframework-composed-permissions](https://github.com/niwibe/djangorestframework-composed-permissions) - Provides a simple way to define complex permissions.
* [rest_condition](https://github.com/caxap/rest_condition) - Another extension for building complex permissions in a simple and convenient way.
### Serializers
* [django-rest-framework-mongoengine](https://github.com/umutbozkurt/django-rest-framework-mongoengine) - Serializer class that supports using MongoDB as the storage layer for Django REST framework.
* [djangorestframework-gis](https://github.com/djangonauts/django-rest-framework-gis) - Geographic add-ons
* [djangorestframework-hstore](https://github.com/djangonauts/django-rest-framework-hstore) - Serializer class to support django-hstore DictionaryField model field and its schema-mode feature.
### Serializer fields
* [drf-compound-fields](https://github.com/estebistec/drf-compound-fields) - Provides "compound" serializer fields, such as lists of simple values.
* [django-extra-fields](https://github.com/Hipo/drf-extra-fields) - Provides extra serializer fields.
### Views
* [djangorestframework-bulk](https://github.com/miki725/django-rest-framework-bulk) - Implements generic view mixins as well as some common concrete generic views to allow to apply bulk operations via API requests.
### Routers
* [drf-nested-routers](https://github.com/alanjds/drf-nested-routers) - Provides routers and relationship fields for working with nested resources.
* [wq.db.rest](http://wq.io/docs/about-rest) - Provides an admin-style model registration API with reasonable default URLs and viewsets.
### Parsers
* [djangorestframework-msgpack](https://github.com/juanriaza/django-rest-framework-msgpack) - Provides MessagePack renderer and parser support.
* [djangorestframework-camel-case](https://github.com/vbabiy/djangorestframework-camel-case) - Provides camel case JSON renderers and parsers.
### Renderers
* [djangorestframework-csv](https://github.com/mjumbewu/django-rest-framework-csv) - Provides CSV renderer support.
* [drf_ujson](https://github.com/gizmag/drf-ujson-renderer) - Implements JSON rendering using the UJSON package.
* [Django REST Pandas](https://github.com/wq/django-rest-pandas) - Pandas DataFrame-powered renderers including Excel, CSV, and SVG formats.
### Filtering
* [djangorestframework-chain](https://github.com/philipn/django-rest-framework-chain) - Allows arbitrary chaining of both relations and lookup filters.
### Misc
* [djangorestrelationalhyperlink](https://github.com/fredkingham/django_rest_model_hyperlink_serializers_project) - A hyperlinked serialiser that can can be used to alter relationships via hyperlinks, but otherwise like a hyperlink model serializer.
* [django-rest-swagger](https://github.com/marcgibbons/django-rest-swagger) - An API documentation generator for Swagger UI.
* [django-rest-framework-proxy ](https://github.com/eofs/django-rest-framework-proxy) - Proxy to redirect incoming request to another API server.
* [gaiarestframework](https://github.com/AppsFuel/gaiarestframework) - Utils for django-rest-framewok
* [drf-extensions](https://github.com/chibisov/drf-extensions) - A collection of custom extensions
* [ember-data-django-rest-adapter](https://github.com/toranb/ember-data-django-rest-adapter) - An ember-data adapter
## Tutorials
* [Beginner's Guide to the Django Rest Framework](http://code.tutsplus.com/tutorials/beginners-guide-to-the-django-rest-framework--cms-19786)
* [Getting Started with Django Rest Framework and AngularJS](http://blog.kevinastone.com/getting-started-with-django-rest-framework-and-angularjs.html)
* [End to end web app with Django-Rest-Framework & AngularJS](http://blog.mourafiq.com/post/55034504632/end-to-end-web-app-with-django-rest-framework)
* [Start Your API - django-rest-framework part 1](https://godjango.com/41-start-your-api-django-rest-framework-part-1/)
* [Permissions & Authentication - django-rest-framework part 2](https://godjango.com/43-permissions-authentication-django-rest-framework-part-2/)
* [ViewSets and Routers - django-rest-framework part 3](https://godjango.com/45-viewsets-and-routers-django-rest-framework-part-3/)
* [Django Rest Framework User Endpoint](http://richardtier.com/2014/02/25/django-rest-framework-user-endpoint/)
* [Check credentials using Django Rest Framework](http://richardtier.com/2014/03/06/110/)
## Videos
* [Ember and Django Part 1 (Video)](http://www.neckbeardrepublic.com/screencasts/ember-and-django-part-1)
* [Django Rest Framework Part 1 (Video)](http://www.neckbeardrepublic.com/screencasts/django-rest-framework-part-1)
* [Pyowa July 2013 - Django Rest Framework (Video)](http://www.youtube.com/watch?v=E1ZrehVxpBo)
* [django-rest-framework and angularjs (Video)](http://www.youtube.com/watch?v=Q8FRBGTJ020)
## Articles
* [Web API performance: profiling Django REST framework](http://dabapps.com/blog/api-performance-profiling-django-rest-framework/)
* [API Development with Django and Django REST Framework](https://bnotions.com/api-development-with-django-and-django-rest-framework/)
...@@ -76,6 +76,7 @@ path_list = [ ...@@ -76,6 +76,7 @@ path_list = [
'topics/browser-enhancements.md', 'topics/browser-enhancements.md',
'topics/browsable-api.md', 'topics/browsable-api.md',
'topics/rest-hypermedia-hateoas.md', 'topics/rest-hypermedia-hateoas.md',
'topics/third-party-resources.md',
'topics/contributing.md', 'topics/contributing.md',
'topics/rest-framework-2-announcement.md', 'topics/rest-framework-2-announcement.md',
'topics/2.2-announcement.md', 'topics/2.2-announcement.md',
......
...@@ -8,5 +8,6 @@ flake8==2.2.2 ...@@ -8,5 +8,6 @@ flake8==2.2.2
markdown>=2.1.0 markdown>=2.1.0
PyYAML>=3.10 PyYAML>=3.10
defusedxml>=0.3 defusedxml>=0.3
django-guardian==1.2.4
django-filter>=0.5.4 django-filter>=0.5.4
Pillow==2.3.0 Pillow==2.3.0
# encoding: utf8 # -*- coding: utf-8 -*-
from __future__ import unicode_literals from __future__ import unicode_literals
from django.db import models, migrations from django.db import models, migrations
...@@ -15,12 +15,11 @@ class Migration(migrations.Migration): ...@@ -15,12 +15,11 @@ class Migration(migrations.Migration):
migrations.CreateModel( migrations.CreateModel(
name='Token', name='Token',
fields=[ fields=[
('key', models.CharField(max_length=40, serialize=False, primary_key=True)), ('key', models.CharField(primary_key=True, serialize=False, max_length=40)),
('user', models.OneToOneField(to=settings.AUTH_USER_MODEL, to_field='id')),
('created', models.DateTimeField(auto_now_add=True)), ('created', models.DateTimeField(auto_now_add=True)),
('user', models.OneToOneField(to=settings.AUTH_USER_MODEL, related_name='auth_token')),
], ],
options={ options={
'abstract': False,
}, },
bases=(models.Model,), bases=(models.Model,),
), ),
......
...@@ -19,11 +19,12 @@ class AuthTokenSerializer(serializers.Serializer): ...@@ -19,11 +19,12 @@ class AuthTokenSerializer(serializers.Serializer):
if not user.is_active: if not user.is_active:
msg = _('User account is disabled.') msg = _('User account is disabled.')
raise serializers.ValidationError(msg) raise serializers.ValidationError(msg)
attrs['user'] = user
return attrs
else: else:
msg = _('Unable to login with provided credentials.') msg = _('Unable to log in with provided credentials.')
raise serializers.ValidationError(msg) raise serializers.ValidationError(msg)
else: else:
msg = _('Must include "username" and "password"') msg = _('Must include "username" and "password"')
raise serializers.ValidationError(msg) raise serializers.ValidationError(msg)
attrs['user'] = user
return attrs
...@@ -18,7 +18,8 @@ class ObtainAuthToken(APIView): ...@@ -18,7 +18,8 @@ class ObtainAuthToken(APIView):
def post(self, request): def post(self, request):
serializer = self.serializer_class(data=request.DATA) serializer = self.serializer_class(data=request.DATA)
if serializer.is_valid(): if serializer.is_valid():
token, created = Token.objects.get_or_create(user=serializer.object['user']) user = serializer.validated_data['user']
token, created = Token.objects.get_or_create(user=user)
return Response({'token': token.key}) return Response({'token': token.key})
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
......
...@@ -39,6 +39,17 @@ except ImportError: ...@@ -39,6 +39,17 @@ except ImportError:
django_filters = None django_filters = None
if django.VERSION >= (1, 6):
def clean_manytomany_helptext(text):
return text
else:
# Up to version 1.5 many to many fields automatically suffix
# the `help_text` attribute with hardcoded text.
def clean_manytomany_helptext(text):
if text.endswith(' Hold down "Control", or "Command" on a Mac, to select more than one.'):
text = text[:-69]
return text
# Django-guardian is optional. Import only if guardian is in INSTALLED_APPS # Django-guardian is optional. Import only if guardian is in INSTALLED_APPS
# Fixes (#1712). We keep the try/except for the test suite. # Fixes (#1712). We keep the try/except for the test suite.
guardian = None guardian = None
......
...@@ -10,7 +10,6 @@ from __future__ import unicode_literals ...@@ -10,7 +10,6 @@ from __future__ import unicode_literals
from django.utils import six from django.utils import six
from rest_framework.views import APIView from rest_framework.views import APIView
import types import types
import warnings
def api_view(http_method_names): def api_view(http_method_names):
...@@ -130,37 +129,3 @@ def list_route(methods=['get'], **kwargs): ...@@ -130,37 +129,3 @@ def list_route(methods=['get'], **kwargs):
func.kwargs = kwargs func.kwargs = kwargs
return func return func
return decorator return decorator
# These are now pending deprecation, in favor of `detail_route` and `list_route`.
def link(**kwargs):
"""
Used to mark a method on a ViewSet that should be routed for detail GET requests.
"""
msg = 'link is pending deprecation. Use detail_route instead.'
warnings.warn(msg, PendingDeprecationWarning, stacklevel=2)
def decorator(func):
func.bind_to_methods = ['get']
func.detail = True
func.kwargs = kwargs
return func
return decorator
def action(methods=['post'], **kwargs):
"""
Used to mark a method on a ViewSet that should be routed for detail POST requests.
"""
msg = 'action is pending deprecation. Use detail_route instead.'
warnings.warn(msg, PendingDeprecationWarning, stacklevel=2)
def decorator(func):
func.bind_to_methods = methods
func.detail = True
func.kwargs = kwargs
return func
return decorator
...@@ -15,7 +15,7 @@ class APIException(Exception): ...@@ -15,7 +15,7 @@ class APIException(Exception):
Subclasses should provide `.status_code` and `.default_detail` properties. Subclasses should provide `.status_code` and `.default_detail` properties.
""" """
status_code = status.HTTP_500_INTERNAL_SERVER_ERROR status_code = status.HTTP_500_INTERNAL_SERVER_ERROR
default_detail = '' default_detail = 'A server error occured'
def __init__(self, detail=None): def __init__(self, detail=None):
self.detail = detail or self.default_detail self.detail = detail or self.default_detail
...@@ -54,7 +54,7 @@ class MethodNotAllowed(APIException): ...@@ -54,7 +54,7 @@ class MethodNotAllowed(APIException):
class NotAcceptable(APIException): class NotAcceptable(APIException):
status_code = status.HTTP_406_NOT_ACCEPTABLE status_code = status.HTTP_406_NOT_ACCEPTABLE
default_detail = "Could not satisfy the request's Accept header" default_detail = "Could not satisfy the request Accept header"
def __init__(self, detail=None, available_renderers=None): def __init__(self, detail=None, available_renderers=None):
self.detail = detail or self.default_detail self.detail = detail or self.default_detail
......
...@@ -6,40 +6,11 @@ which allows mixin classes to be composed in interesting ways. ...@@ -6,40 +6,11 @@ which allows mixin classes to be composed in interesting ways.
""" """
from __future__ import unicode_literals from __future__ import unicode_literals
from django.core.exceptions import ValidationError
from django.http import Http404 from django.http import Http404
from rest_framework import status from rest_framework import status
from rest_framework.response import Response from rest_framework.response import Response
from rest_framework.request import clone_request from rest_framework.request import clone_request
from rest_framework.settings import api_settings from rest_framework.settings import api_settings
import warnings
def _get_validation_exclusions(obj, pk=None, slug_field=None, lookup_field=None):
"""
Given a model instance, and an optional pk and slug field,
return the full list of all other field names on that model.
For use when performing full_clean on a model instance,
so we only clean the required fields.
"""
include = []
if pk:
# Deprecated
pk_field = obj._meta.pk
while pk_field.rel:
pk_field = pk_field.rel.to._meta.pk
include.append(pk_field.name)
if slug_field:
# Deprecated
include.append(slug_field)
if lookup_field and lookup_field != 'pk':
include.append(lookup_field)
return [field.name for field in obj._meta.fields if field.name not in include]
class CreateModelMixin(object): class CreateModelMixin(object):
...@@ -47,17 +18,11 @@ class CreateModelMixin(object): ...@@ -47,17 +18,11 @@ class CreateModelMixin(object):
Create a model instance. Create a model instance.
""" """
def create(self, request, *args, **kwargs): def create(self, request, *args, **kwargs):
serializer = self.get_serializer(data=request.DATA, files=request.FILES) serializer = self.get_serializer(data=request.DATA)
serializer.is_valid(raise_exception=True)
if serializer.is_valid(): serializer.save()
self.pre_save(serializer.object)
self.object = serializer.save(force_insert=True)
self.post_save(self.object, created=True)
headers = self.get_success_headers(serializer.data) headers = self.get_success_headers(serializer.data)
return Response(serializer.data, status=status.HTTP_201_CREATED, return Response(serializer.data, status=status.HTTP_201_CREATED, headers=headers)
headers=headers)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
def get_success_headers(self, data): def get_success_headers(self, data):
try: try:
...@@ -70,31 +35,13 @@ class ListModelMixin(object): ...@@ -70,31 +35,13 @@ class ListModelMixin(object):
""" """
List a queryset. List a queryset.
""" """
empty_error = "Empty list and '%(class_name)s.allow_empty' is False."
def list(self, request, *args, **kwargs): def list(self, request, *args, **kwargs):
self.object_list = self.filter_queryset(self.get_queryset()) instance = self.filter_queryset(self.get_queryset())
page = self.paginate_queryset(instance)
# Default is to allow empty querysets. This can be altered by setting
# `.allow_empty = False`, to raise 404 errors on empty querysets.
if not self.allow_empty and not self.object_list:
warnings.warn(
'The `allow_empty` parameter is deprecated. '
'To use `allow_empty=False` style behavior, You should override '
'`get_queryset()` and explicitly raise a 404 on empty querysets.',
DeprecationWarning
)
class_name = self.__class__.__name__
error_msg = self.empty_error % {'class_name': class_name}
raise Http404(error_msg)
# Switch between paginated or standard style responses
page = self.paginate_queryset(self.object_list)
if page is not None: if page is not None:
serializer = self.get_pagination_serializer(page) serializer = self.get_pagination_serializer(page)
else: else:
serializer = self.get_serializer(self.object_list, many=True) serializer = self.get_serializer(instance, many=True)
return Response(serializer.data) return Response(serializer.data)
...@@ -103,8 +50,8 @@ class RetrieveModelMixin(object): ...@@ -103,8 +50,8 @@ class RetrieveModelMixin(object):
Retrieve a model instance. Retrieve a model instance.
""" """
def retrieve(self, request, *args, **kwargs): def retrieve(self, request, *args, **kwargs):
self.object = self.get_object() instance = self.get_object()
serializer = self.get_serializer(self.object) serializer = self.get_serializer(instance)
return Response(serializer.data) return Response(serializer.data)
...@@ -114,29 +61,52 @@ class UpdateModelMixin(object): ...@@ -114,29 +61,52 @@ class UpdateModelMixin(object):
""" """
def update(self, request, *args, **kwargs): def update(self, request, *args, **kwargs):
partial = kwargs.pop('partial', False) partial = kwargs.pop('partial', False)
self.object = self.get_object_or_none() instance = self.get_object()
serializer = self.get_serializer(instance, data=request.DATA, partial=partial)
serializer.is_valid(raise_exception=True)
serializer.save()
return Response(serializer.data)
serializer = self.get_serializer(self.object, data=request.DATA, def partial_update(self, request, *args, **kwargs):
files=request.FILES, partial=partial) kwargs['partial'] = True
return self.update(request, *args, **kwargs)
if not serializer.is_valid():
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
try: class DestroyModelMixin(object):
self.pre_save(serializer.object) """
except ValidationError as err: Destroy a model instance.
# full_clean on model instance may be called in pre_save, """
# so we have to handle eventual errors. def destroy(self, request, *args, **kwargs):
return Response(err.message_dict, status=status.HTTP_400_BAD_REQUEST) instance = self.get_object()
instance.delete()
if self.object is None: return Response(status=status.HTTP_204_NO_CONTENT)
self.object = serializer.save(force_insert=True)
self.post_save(self.object, created=True)
# The AllowPUTAsCreateMixin was previously the default behaviour
# for PUT requests. This has now been removed and must be *explictly*
# included if it is the behavior that you want.
# For more info see: ...
class AllowPUTAsCreateMixin(object):
"""
The following mixin class may be used in order to support PUT-as-create
behavior for incoming requests.
"""
def update(self, request, *args, **kwargs):
partial = kwargs.pop('partial', False)
instance = self.get_object_or_none()
serializer = self.get_serializer(instance, data=request.DATA, partial=partial)
serializer.is_valid(raise_exception=True)
if instance is None:
lookup_url_kwarg = self.lookup_url_kwarg or self.lookup_field
lookup_value = self.kwargs[lookup_url_kwarg]
extras = {self.lookup_field: lookup_value}
serializer.save(extras=extras)
return Response(serializer.data, status=status.HTTP_201_CREATED) return Response(serializer.data, status=status.HTTP_201_CREATED)
self.object = serializer.save(force_update=True) serializer.save()
self.post_save(self.object, created=False) return Response(serializer.data)
return Response(serializer.data, status=status.HTTP_200_OK)
def partial_update(self, request, *args, **kwargs): def partial_update(self, request, *args, **kwargs):
kwargs['partial'] = True kwargs['partial'] = True
...@@ -156,41 +126,3 @@ class UpdateModelMixin(object): ...@@ -156,41 +126,3 @@ class UpdateModelMixin(object):
# PATCH requests where the object does not exist should still # PATCH requests where the object does not exist should still
# return a 404 response. # return a 404 response.
raise raise
def pre_save(self, obj):
"""
Set any attributes on the object that are implicit in the request.
"""
# pk and/or slug attributes are implicit in the URL.
lookup_url_kwarg = self.lookup_url_kwarg or self.lookup_field
lookup = self.kwargs.get(lookup_url_kwarg, None)
pk = self.kwargs.get(self.pk_url_kwarg, None)
slug = self.kwargs.get(self.slug_url_kwarg, None)
slug_field = slug and self.slug_field or None
if lookup:
setattr(obj, self.lookup_field, lookup)
if pk:
setattr(obj, 'pk', pk)
if slug:
setattr(obj, slug_field, slug)
# Ensure we clean the attributes so that we don't eg return integer
# pk using a string representation, as provided by the url conf kwarg.
if hasattr(obj, 'full_clean'):
exclude = _get_validation_exclusions(obj, pk, slug_field, self.lookup_field)
obj.full_clean(exclude)
class DestroyModelMixin(object):
"""
Destroy a model instance.
"""
def destroy(self, request, *args, **kwargs):
obj = self.get_object()
self.pre_delete(obj)
obj.delete()
self.post_delete(obj)
return Response(status=status.HTTP_204_NO_CONTENT)
...@@ -13,7 +13,7 @@ class NextPageField(serializers.Field): ...@@ -13,7 +13,7 @@ class NextPageField(serializers.Field):
""" """
page_field = 'page' page_field = 'page'
def to_native(self, value): def to_representation(self, value):
if not value.has_next(): if not value.has_next():
return None return None
page = value.next_page_number() page = value.next_page_number()
...@@ -28,7 +28,7 @@ class PreviousPageField(serializers.Field): ...@@ -28,7 +28,7 @@ class PreviousPageField(serializers.Field):
""" """
page_field = 'page' page_field = 'page'
def to_native(self, value): def to_representation(self, value):
if not value.has_previous(): if not value.has_previous():
return None return None
page = value.previous_page_number() page = value.previous_page_number()
...@@ -37,7 +37,7 @@ class PreviousPageField(serializers.Field): ...@@ -37,7 +37,7 @@ class PreviousPageField(serializers.Field):
return replace_query_param(url, self.page_field, page) return replace_query_param(url, self.page_field, page)
class DefaultObjectSerializer(serializers.Field): class DefaultObjectSerializer(serializers.ReadOnlyField):
""" """
If no object serializer is specified, then this serializer will be applied If no object serializer is specified, then this serializer will be applied
as the default. as the default.
...@@ -49,25 +49,11 @@ class DefaultObjectSerializer(serializers.Field): ...@@ -49,25 +49,11 @@ class DefaultObjectSerializer(serializers.Field):
super(DefaultObjectSerializer, self).__init__(source=source) super(DefaultObjectSerializer, self).__init__(source=source)
class PaginationSerializerOptions(serializers.SerializerOptions):
"""
An object that stores the options that may be provided to a
pagination serializer by using the inner `Meta` class.
Accessible on the instance as `serializer.opts`.
"""
def __init__(self, meta):
super(PaginationSerializerOptions, self).__init__(meta)
self.object_serializer_class = getattr(meta, 'object_serializer_class',
DefaultObjectSerializer)
class BasePaginationSerializer(serializers.Serializer): class BasePaginationSerializer(serializers.Serializer):
""" """
A base class for pagination serializers to inherit from, A base class for pagination serializers to inherit from,
to make implementing custom serializers more easy. to make implementing custom serializers more easy.
""" """
_options_class = PaginationSerializerOptions
results_field = 'results' results_field = 'results'
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
...@@ -76,22 +62,23 @@ class BasePaginationSerializer(serializers.Serializer): ...@@ -76,22 +62,23 @@ class BasePaginationSerializer(serializers.Serializer):
""" """
super(BasePaginationSerializer, self).__init__(*args, **kwargs) super(BasePaginationSerializer, self).__init__(*args, **kwargs)
results_field = self.results_field results_field = self.results_field
object_serializer = self.opts.object_serializer_class
if 'context' in kwargs: try:
context_kwarg = {'context': kwargs['context']} object_serializer = self.Meta.object_serializer_class
else: except AttributeError:
context_kwarg = {} object_serializer = DefaultObjectSerializer
self.fields[results_field] = object_serializer(source='object_list', self.fields[results_field] = serializers.ListSerializer(
many=True, child=object_serializer(),
**context_kwarg) source='object_list'
)
self.fields[results_field].bind(results_field, self, self)
class PaginationSerializer(BasePaginationSerializer): class PaginationSerializer(BasePaginationSerializer):
""" """
A default implementation of a pagination serializer. A default implementation of a pagination serializer.
""" """
count = serializers.Field(source='paginator.count') count = serializers.ReadOnlyField(source='paginator.count')
next = NextPageField(source='*') next = NextPageField(source='*')
previous = PreviousPageField(source='*') previous = PreviousPageField(source='*')
...@@ -11,7 +11,7 @@ from django.http import QueryDict ...@@ -11,7 +11,7 @@ from django.http import QueryDict
from django.http.multipartparser import MultiPartParser as DjangoMultiPartParser from django.http.multipartparser import MultiPartParser as DjangoMultiPartParser
from django.http.multipartparser import MultiPartParserError, parse_header, ChunkIter from django.http.multipartparser import MultiPartParserError, parse_header, ChunkIter
from django.utils import six from django.utils import six
from rest_framework.compat import etree, yaml, force_text from rest_framework.compat import etree, yaml, force_text, urlparse
from rest_framework.exceptions import ParseError from rest_framework.exceptions import ParseError
from rest_framework import renderers from rest_framework import renderers
import json import json
...@@ -48,7 +48,7 @@ class JSONParser(BaseParser): ...@@ -48,7 +48,7 @@ class JSONParser(BaseParser):
""" """
media_type = 'application/json' media_type = 'application/json'
renderer_class = renderers.UnicodeJSONRenderer renderer_class = renderers.JSONRenderer
def parse(self, stream, media_type=None, parser_context=None): def parse(self, stream, media_type=None, parser_context=None):
""" """
...@@ -290,6 +290,22 @@ class FileUploadParser(BaseParser): ...@@ -290,6 +290,22 @@ class FileUploadParser(BaseParser):
try: try:
meta = parser_context['request'].META meta = parser_context['request'].META
disposition = parse_header(meta['HTTP_CONTENT_DISPOSITION'].encode('utf-8')) disposition = parse_header(meta['HTTP_CONTENT_DISPOSITION'].encode('utf-8'))
return force_text(disposition[1]['filename']) filename_parm = disposition[1]
if 'filename*' in filename_parm:
return self.get_encoded_filename(filename_parm)
return force_text(filename_parm['filename'])
except (AttributeError, KeyError): except (AttributeError, KeyError):
pass pass
def get_encoded_filename(self, filename_parm):
"""
Handle encoded filenames per RFC6266. See also:
http://tools.ietf.org/html/rfc2231#section-4
"""
encoded_filename = force_text(filename_parm['filename*'])
try:
charset, lang, filename = encoded_filename.split('\'', 2)
filename = urlparse.unquote(filename)
except (ValueError, LookupError):
filename = force_text(filename_parm['filename'])
return filename
...@@ -26,6 +26,10 @@ from rest_framework.utils.breadcrumbs import get_breadcrumbs ...@@ -26,6 +26,10 @@ from rest_framework.utils.breadcrumbs import get_breadcrumbs
from rest_framework import exceptions, status, VERSION from rest_framework import exceptions, status, VERSION
def zero_as_none(value):
return None if value == 0 else value
class BaseRenderer(object): class BaseRenderer(object):
""" """
All renderers should extend this class, setting the `media_type` All renderers should extend this class, setting the `media_type`
...@@ -44,13 +48,13 @@ class BaseRenderer(object): ...@@ -44,13 +48,13 @@ class BaseRenderer(object):
class JSONRenderer(BaseRenderer): class JSONRenderer(BaseRenderer):
""" """
Renderer which serializes to JSON. Renderer which serializes to JSON.
Applies JSON's backslash-u character escaping for non-ascii characters.
""" """
media_type = 'application/json' media_type = 'application/json'
format = 'json' format = 'json'
encoder_class = encoders.JSONEncoder encoder_class = encoders.JSONEncoder
ensure_ascii = True ensure_ascii = not api_settings.UNICODE_JSON
compact = api_settings.COMPACT_JSON
# We don't set a charset because JSON is a binary encoding, # We don't set a charset because JSON is a binary encoding,
# that can be encoded as utf-8, utf-16 or utf-32. # that can be encoded as utf-8, utf-16 or utf-32.
...@@ -62,9 +66,10 @@ class JSONRenderer(BaseRenderer): ...@@ -62,9 +66,10 @@ class JSONRenderer(BaseRenderer):
if accepted_media_type: if accepted_media_type:
# If the media type looks like 'application/json; indent=4', # If the media type looks like 'application/json; indent=4',
# then pretty print the result. # then pretty print the result.
# Note that we coerce `indent=0` into `indent=None`.
base_media_type, params = parse_header(accepted_media_type.encode('ascii')) base_media_type, params = parse_header(accepted_media_type.encode('ascii'))
try: try:
return max(min(int(params['indent']), 8), 0) return zero_as_none(max(min(int(params['indent']), 8), 0))
except (KeyError, ValueError, TypeError): except (KeyError, ValueError, TypeError):
pass pass
...@@ -81,10 +86,12 @@ class JSONRenderer(BaseRenderer): ...@@ -81,10 +86,12 @@ class JSONRenderer(BaseRenderer):
renderer_context = renderer_context or {} renderer_context = renderer_context or {}
indent = self.get_indent(accepted_media_type, renderer_context) indent = self.get_indent(accepted_media_type, renderer_context)
separators = (',', ':') if (indent is None and self.compact) else (', ', ': ')
ret = json.dumps( ret = json.dumps(
data, cls=self.encoder_class, data, cls=self.encoder_class,
indent=indent, ensure_ascii=self.ensure_ascii indent=indent, ensure_ascii=self.ensure_ascii,
separators=separators
) )
# On python 2.x json.dumps() returns bytestrings if ensure_ascii=True, # On python 2.x json.dumps() returns bytestrings if ensure_ascii=True,
...@@ -96,14 +103,6 @@ class JSONRenderer(BaseRenderer): ...@@ -96,14 +103,6 @@ class JSONRenderer(BaseRenderer):
return ret return ret
class UnicodeJSONRenderer(JSONRenderer):
ensure_ascii = False
"""
Renderer which serializes to JSON.
Does *not* apply JSON's character escaping for non-ascii characters.
"""
class JSONPRenderer(JSONRenderer): class JSONPRenderer(JSONRenderer):
""" """
Renderer which serializes to json, Renderer which serializes to json,
...@@ -196,7 +195,7 @@ class YAMLRenderer(BaseRenderer): ...@@ -196,7 +195,7 @@ class YAMLRenderer(BaseRenderer):
format = 'yaml' format = 'yaml'
encoder = encoders.SafeDumper encoder = encoders.SafeDumper
charset = 'utf-8' charset = 'utf-8'
ensure_ascii = True ensure_ascii = False
def render(self, data, accepted_media_type=None, renderer_context=None): def render(self, data, accepted_media_type=None, renderer_context=None):
""" """
...@@ -210,14 +209,6 @@ class YAMLRenderer(BaseRenderer): ...@@ -210,14 +209,6 @@ class YAMLRenderer(BaseRenderer):
return yaml.dump(data, stream=None, encoding=self.charset, Dumper=self.encoder, allow_unicode=not self.ensure_ascii) return yaml.dump(data, stream=None, encoding=self.charset, Dumper=self.encoder, allow_unicode=not self.ensure_ascii)
class UnicodeYAMLRenderer(YAMLRenderer):
"""
Renderer which serializes to YAML.
Does *not* apply character escaping for non-ascii characters.
"""
ensure_ascii = False
class TemplateHTMLRenderer(BaseRenderer): class TemplateHTMLRenderer(BaseRenderer):
""" """
An HTML renderer for use with templates. An HTML renderer for use with templates.
...@@ -436,13 +427,13 @@ class BrowsableAPIRenderer(BaseRenderer): ...@@ -436,13 +427,13 @@ class BrowsableAPIRenderer(BaseRenderer):
if request.method == method: if request.method == method:
try: try:
data = request.DATA data = request.DATA
files = request.FILES # files = request.FILES
except ParseError: except ParseError:
data = None data = None
files = None # files = None
else: else:
data = None data = None
files = None # files = None
with override_method(view, request, method) as request: with override_method(view, request, method) as request:
obj = getattr(view, 'object', None) obj = getattr(view, 'object', None)
...@@ -458,7 +449,7 @@ class BrowsableAPIRenderer(BaseRenderer): ...@@ -458,7 +449,7 @@ class BrowsableAPIRenderer(BaseRenderer):
): ):
return return
serializer = view.get_serializer(instance=obj, data=data, files=files) serializer = view.get_serializer(instance=obj, data=data)
serializer.is_valid() serializer.is_valid()
data = serializer.data data = serializer.data
...@@ -579,10 +570,10 @@ class BrowsableAPIRenderer(BaseRenderer): ...@@ -579,10 +570,10 @@ class BrowsableAPIRenderer(BaseRenderer):
'available_formats': [renderer_cls.format for renderer_cls in view.renderer_classes], 'available_formats': [renderer_cls.format for renderer_cls in view.renderer_classes],
'response_headers': response_headers, 'response_headers': response_headers,
'put_form': self.get_rendered_html_form(view, 'PUT', request), # 'put_form': self.get_rendered_html_form(view, 'PUT', request),
'post_form': self.get_rendered_html_form(view, 'POST', request), # 'post_form': self.get_rendered_html_form(view, 'POST', request),
'delete_form': self.get_rendered_html_form(view, 'DELETE', request), # 'delete_form': self.get_rendered_html_form(view, 'DELETE', request),
'options_form': self.get_rendered_html_form(view, 'OPTIONS', request), # 'options_form': self.get_rendered_html_form(view, 'OPTIONS', request),
'raw_data_put_form': raw_data_put_form, 'raw_data_put_form': raw_data_put_form,
'raw_data_post_form': raw_data_post_form, 'raw_data_post_form': raw_data_post_form,
......
...@@ -19,6 +19,7 @@ import itertools ...@@ -19,6 +19,7 @@ import itertools
from collections import namedtuple from collections import namedtuple
from django.conf.urls import patterns, url from django.conf.urls import patterns, url
from django.core.exceptions import ImproperlyConfigured from django.core.exceptions import ImproperlyConfigured
from django.core.urlresolvers import NoReverseMatch
from rest_framework import views from rest_framework import views
from rest_framework.response import Response from rest_framework.response import Response
from rest_framework.reverse import reverse from rest_framework.reverse import reverse
...@@ -284,10 +285,19 @@ class DefaultRouter(SimpleRouter): ...@@ -284,10 +285,19 @@ class DefaultRouter(SimpleRouter):
class APIRoot(views.APIView): class APIRoot(views.APIView):
_ignore_model_permissions = True _ignore_model_permissions = True
def get(self, request, format=None): def get(self, request, *args, **kwargs):
ret = {} ret = {}
for key, url_name in api_root_dict.items(): for key, url_name in api_root_dict.items():
ret[key] = reverse(url_name, request=request, format=format) try:
ret[key] = reverse(
url_name,
request=request,
format=kwargs.get('format', None)
)
except NoReverseMatch:
# Don't bail out if eg. no list routes exist, only detail routes.
continue
return Response(ret) return Response(ret)
return APIRoot.as_view() return APIRoot.as_view()
......
...@@ -77,6 +77,7 @@ DEFAULTS = { ...@@ -77,6 +77,7 @@ DEFAULTS = {
# Exception handling # Exception handling
'EXCEPTION_HANDLER': 'rest_framework.views.exception_handler', 'EXCEPTION_HANDLER': 'rest_framework.views.exception_handler',
'NON_FIELD_ERRORS_KEY': 'non_field_errors',
# Testing # Testing
'TEST_REQUEST_RENDERER_CLASSES': ( 'TEST_REQUEST_RENDERER_CLASSES': (
...@@ -96,24 +97,19 @@ DEFAULTS = { ...@@ -96,24 +97,19 @@ DEFAULTS = {
'URL_FIELD_NAME': 'url', 'URL_FIELD_NAME': 'url',
# Input and output formats # Input and output formats
'DATE_INPUT_FORMATS': ( 'DATE_FORMAT': ISO_8601,
ISO_8601, 'DATE_INPUT_FORMATS': (ISO_8601,),
),
'DATE_FORMAT': None,
'DATETIME_INPUT_FORMATS': ( 'DATETIME_FORMAT': ISO_8601,
ISO_8601, 'DATETIME_INPUT_FORMATS': (ISO_8601,),
),
'DATETIME_FORMAT': None,
'TIME_INPUT_FORMATS': ( 'TIME_FORMAT': ISO_8601,
ISO_8601, 'TIME_INPUT_FORMATS': (ISO_8601,),
),
'TIME_FORMAT': None,
# Pending deprecation
'FILTER_BACKEND': None,
# Encoding
'UNICODE_JSON': True,
'COMPACT_JSON': True,
'COERCE_DECIMAL_TO_STRING': True
} }
...@@ -129,7 +125,6 @@ IMPORT_STRINGS = ( ...@@ -129,7 +125,6 @@ IMPORT_STRINGS = (
'DEFAULT_PAGINATION_SERIALIZER_CLASS', 'DEFAULT_PAGINATION_SERIALIZER_CLASS',
'DEFAULT_FILTER_BACKENDS', 'DEFAULT_FILTER_BACKENDS',
'EXCEPTION_HANDLER', 'EXCEPTION_HANDLER',
'FILTER_BACKEND',
'TEST_REQUEST_RENDERER_CLASSES', 'TEST_REQUEST_RENDERER_CLASSES',
'UNAUTHENTICATED_USER', 'UNAUTHENTICATED_USER',
'UNAUTHENTICATED_TOKEN', 'UNAUTHENTICATED_TOKEN',
...@@ -196,15 +191,9 @@ class APISettings(object): ...@@ -196,15 +191,9 @@ class APISettings(object):
if val and attr in self.import_strings: if val and attr in self.import_strings:
val = perform_import(val, attr) val = perform_import(val, attr)
self.validate_setting(attr, val)
# Cache the result # Cache the result
setattr(self, attr, val) setattr(self, attr, val)
return val return val
def validate_setting(self, attr, val):
if attr == 'FILTER_BACKEND' and val is not None:
# Make sure we can initialize the class
val()
api_settings = APISettings(USER_SETTINGS, DEFAULTS, IMPORT_STRINGS) api_settings = APISettings(USER_SETTINGS, DEFAULTS, IMPORT_STRINGS)
...@@ -36,7 +36,7 @@ class APIRequestFactory(DjangoRequestFactory): ...@@ -36,7 +36,7 @@ class APIRequestFactory(DjangoRequestFactory):
Encode the data returning a two tuple of (bytes, content_type) Encode the data returning a two tuple of (bytes, content_type)
""" """
if not data: if data is None:
return ('', content_type) return ('', content_type)
assert format is None or content_type is None, ( assert format is None or content_type is None, (
......
...@@ -7,7 +7,6 @@ from django.db.models.query import QuerySet ...@@ -7,7 +7,6 @@ from django.db.models.query import QuerySet
from django.utils.datastructures import SortedDict from django.utils.datastructures import SortedDict
from django.utils.functional import Promise from django.utils.functional import Promise
from rest_framework.compat import force_text from rest_framework.compat import force_text
from rest_framework.serializers import DictWithMetadata, SortedDictWithMetadata
import datetime import datetime
import decimal import decimal
import types import types
...@@ -17,45 +16,47 @@ import json ...@@ -17,45 +16,47 @@ import json
class JSONEncoder(json.JSONEncoder): class JSONEncoder(json.JSONEncoder):
""" """
JSONEncoder subclass that knows how to encode date/time/timedelta, JSONEncoder subclass that knows how to encode date/time/timedelta,
decimal types, and generators. decimal types, generators and other basic python objects.
""" """
def default(self, o): def default(self, obj):
# For Date Time string spec, see ECMA 262 # For Date Time string spec, see ECMA 262
# http://ecma-international.org/ecma-262/5.1/#sec-15.9.1.15 # http://ecma-international.org/ecma-262/5.1/#sec-15.9.1.15
if isinstance(o, Promise): if isinstance(obj, Promise):
return force_text(o) return force_text(obj)
elif isinstance(o, datetime.datetime): elif isinstance(obj, datetime.datetime):
r = o.isoformat() representation = obj.isoformat()
if o.microsecond: if obj.microsecond:
r = r[:23] + r[26:] representation = representation[:23] + representation[26:]
if r.endswith('+00:00'): if representation.endswith('+00:00'):
r = r[:-6] + 'Z' representation = representation[:-6] + 'Z'
return r return representation
elif isinstance(o, datetime.date): elif isinstance(obj, datetime.date):
return o.isoformat() return obj.isoformat()
elif isinstance(o, datetime.time): elif isinstance(obj, datetime.time):
if timezone and timezone.is_aware(o): if timezone and timezone.is_aware(obj):
raise ValueError("JSON can't represent timezone-aware times.") raise ValueError("JSON can't represent timezone-aware times.")
r = o.isoformat() representation = obj.isoformat()
if o.microsecond: if obj.microsecond:
r = r[:12] representation = representation[:12]
return r return representation
elif isinstance(o, datetime.timedelta): elif isinstance(obj, datetime.timedelta):
return str(o.total_seconds()) return str(obj.total_seconds())
elif isinstance(o, decimal.Decimal): elif isinstance(obj, decimal.Decimal):
return str(o) # Serializers will coerce decimals to strings by default.
elif isinstance(o, QuerySet): return float(obj)
return list(o) elif isinstance(obj, QuerySet):
elif hasattr(o, 'tolist'): return list(obj)
return o.tolist() elif hasattr(obj, 'tolist'):
elif hasattr(o, '__getitem__'): # Numpy arrays and array scalars.
return obj.tolist()
elif hasattr(obj, '__getitem__'):
try: try:
return dict(o) return dict(obj)
except: except:
pass pass
elif hasattr(o, '__iter__'): elif hasattr(obj, '__iter__'):
return [i for i in o] return [item for item in obj]
return super(JSONEncoder, self).default(o) return super(JSONEncoder, self).default(obj)
try: try:
...@@ -106,14 +107,14 @@ else: ...@@ -106,14 +107,14 @@ else:
SortedDict, SortedDict,
yaml.representer.SafeRepresenter.represent_dict yaml.representer.SafeRepresenter.represent_dict
) )
SafeDumper.add_representer( # SafeDumper.add_representer(
DictWithMetadata, # DictWithMetadata,
yaml.representer.SafeRepresenter.represent_dict # yaml.representer.SafeRepresenter.represent_dict
) # )
SafeDumper.add_representer( # SafeDumper.add_representer(
SortedDictWithMetadata, # SortedDictWithMetadata,
yaml.representer.SafeRepresenter.represent_dict # yaml.representer.SafeRepresenter.represent_dict
) # )
SafeDumper.add_representer( SafeDumper.add_representer(
types.GeneratorType, types.GeneratorType,
yaml.representer.SafeRepresenter.represent_list yaml.representer.SafeRepresenter.represent_list
......
"""
Helper functions for mapping model fields to a dictionary of default
keyword arguments that should be used for their equivelent serializer fields.
"""
from django.core import validators
from django.db import models
from django.utils.text import capfirst
from rest_framework.compat import clean_manytomany_helptext
import inspect
def lookup_class(mapping, instance):
"""
Takes a dictionary with classes as keys, and an object.
Traverses the object's inheritance hierarchy in method
resolution order, and returns the first matching value
from the dictionary or raises a KeyError if nothing matches.
"""
for cls in inspect.getmro(instance.__class__):
if cls in mapping:
return mapping[cls]
raise KeyError('Class %s not found in lookup.', cls.__name__)
def needs_label(model_field, field_name):
"""
Returns `True` if the label based on the model's verbose name
is not equal to the default label it would have based on it's field name.
"""
default_label = field_name.replace('_', ' ').capitalize()
return capfirst(model_field.verbose_name) != default_label
def get_detail_view_name(model):
"""
Given a model class, return the view name to use for URL relationships
that refer to instances of the model.
"""
return '%(model_name)s-detail' % {
'app_label': model._meta.app_label,
'model_name': model._meta.object_name.lower()
}
def get_field_kwargs(field_name, model_field):
"""
Creates a default instance of a basic non-relational field.
"""
kwargs = {}
validator_kwarg = model_field.validators
if model_field.null or model_field.blank:
kwargs['required'] = False
if model_field.verbose_name and needs_label(model_field, field_name):
kwargs['label'] = capfirst(model_field.verbose_name)
if model_field.help_text:
kwargs['help_text'] = model_field.help_text
if isinstance(model_field, models.AutoField) or not model_field.editable:
kwargs['read_only'] = True
# Read only implies that the field is not required.
# We have a cleaner repr on the instance if we don't set it.
kwargs.pop('required', None)
if model_field.has_default():
kwargs['default'] = model_field.get_default()
# Having a default implies that the field is not required.
# We have a cleaner repr on the instance if we don't set it.
kwargs.pop('required', None)
if model_field.flatchoices:
# If this model field contains choices, then return now,
# any further keyword arguments are not valid.
kwargs['choices'] = model_field.flatchoices
return kwargs
# Ensure that max_length is passed explicitly as a keyword arg,
# rather than as a validator.
max_length = getattr(model_field, 'max_length', None)
if max_length is not None:
kwargs['max_length'] = max_length
validator_kwarg = [
validator for validator in validator_kwarg
if not isinstance(validator, validators.MaxLengthValidator)
]
# Ensure that min_length is passed explicitly as a keyword arg,
# rather than as a validator.
min_length = getattr(model_field, 'min_length', None)
if min_length is not None:
kwargs['min_length'] = min_length
validator_kwarg = [
validator for validator in validator_kwarg
if not isinstance(validator, validators.MinLengthValidator)
]
# Ensure that max_value is passed explicitly as a keyword arg,
# rather than as a validator.
max_value = next((
validator.limit_value for validator in validator_kwarg
if isinstance(validator, validators.MaxValueValidator)
), None)
if max_value is not None:
kwargs['max_value'] = max_value
validator_kwarg = [
validator for validator in validator_kwarg
if not isinstance(validator, validators.MaxValueValidator)
]
# Ensure that max_value is passed explicitly as a keyword arg,
# rather than as a validator.
min_value = next((
validator.limit_value for validator in validator_kwarg
if isinstance(validator, validators.MinValueValidator)
), None)
if min_value is not None:
kwargs['min_value'] = min_value
validator_kwarg = [
validator for validator in validator_kwarg
if not isinstance(validator, validators.MinValueValidator)
]
# URLField does not need to include the URLValidator argument,
# as it is explicitly added in.
if isinstance(model_field, models.URLField):
validator_kwarg = [
validator for validator in validator_kwarg
if not isinstance(validator, validators.URLValidator)
]
# EmailField does not need to include the validate_email argument,
# as it is explicitly added in.
if isinstance(model_field, models.EmailField):
validator_kwarg = [
validator for validator in validator_kwarg
if validator is not validators.validate_email
]
# SlugField do not need to include the 'validate_slug' argument,
if isinstance(model_field, models.SlugField):
validator_kwarg = [
validator for validator in validator_kwarg
if validator is not validators.validate_slug
]
max_digits = getattr(model_field, 'max_digits', None)
if max_digits is not None:
kwargs['max_digits'] = max_digits
decimal_places = getattr(model_field, 'decimal_places', None)
if decimal_places is not None:
kwargs['decimal_places'] = decimal_places
if isinstance(model_field, models.BooleanField):
# models.BooleanField has `blank=True`, but *is* actually
# required *unless* a default is provided.
# Also note that Django<1.6 uses `default=False` for
# models.BooleanField, but Django>=1.6 uses `default=None`.
kwargs.pop('required', None)
if validator_kwarg:
kwargs['validators'] = validator_kwarg
# The following will only be used by ModelField classes.
# Gets removed for everything else.
kwargs['model_field'] = model_field
return kwargs
def get_relation_kwargs(field_name, relation_info):
"""
Creates a default instance of a flat relational field.
"""
model_field, related_model, to_many, has_through_model = relation_info
kwargs = {
'queryset': related_model._default_manager,
'view_name': get_detail_view_name(related_model)
}
if to_many:
kwargs['many'] = True
if has_through_model:
kwargs['read_only'] = True
kwargs.pop('queryset', None)
if model_field:
if model_field.null or model_field.blank:
kwargs['required'] = False
if model_field.verbose_name and needs_label(model_field, field_name):
kwargs['label'] = capfirst(model_field.verbose_name)
if not model_field.editable:
kwargs['read_only'] = True
kwargs.pop('queryset', None)
help_text = clean_manytomany_helptext(model_field.help_text)
if help_text:
kwargs['help_text'] = help_text
return kwargs
def get_nested_relation_kwargs(relation_info):
kwargs = {'read_only': True}
if relation_info.to_many:
kwargs['many'] = True
return kwargs
def get_url_kwargs(model_field):
return {
'view_name': get_detail_view_name(model_field)
}
...@@ -2,11 +2,12 @@ ...@@ -2,11 +2,12 @@
Utility functions to return a formatted name and description for a given view. Utility functions to return a formatted name and description for a given view.
""" """
from __future__ import unicode_literals from __future__ import unicode_literals
import re
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
import re from rest_framework.compat import apply_markdown, force_text
def remove_trailing_string(content, trailing): def remove_trailing_string(content, trailing):
...@@ -28,6 +29,7 @@ def dedent(content): ...@@ -28,6 +29,7 @@ def dedent(content):
as it fails to dedent multiline docstrings that include as it fails to dedent multiline docstrings that include
unindented text on the initial line. unindented text on the initial line.
""" """
content = force_text(content)
whitespace_counts = [len(line) - len(line.lstrip(' ')) whitespace_counts = [len(line) - len(line.lstrip(' '))
for line in content.splitlines()[1:] if line.lstrip()] for line in content.splitlines()[1:] if line.lstrip()]
......
"""
Helpers for dealing with HTML input.
"""
import re
def is_html_input(dictionary):
# MultiDict type datastructures are used to represent HTML form input,
# which may have more than one value for each key.
return hasattr(dictionary, 'getlist')
def parse_html_list(dictionary, prefix=''):
"""
Used to suport list values in HTML forms.
Supports lists of primitives and/or dictionaries.
* List of primitives.
{
'[0]': 'abc',
'[1]': 'def',
'[2]': 'hij'
}
-->
[
'abc',
'def',
'hij'
]
* List of dictionaries.
{
'[0]foo': 'abc',
'[0]bar': 'def',
'[1]foo': 'hij',
'[2]bar': 'klm',
}
-->
[
{'foo': 'abc', 'bar': 'def'},
{'foo': 'hij', 'bar': 'klm'}
]
"""
Dict = type(dictionary)
ret = {}
regex = re.compile(r'^%s\[([0-9]+)\](.*)$' % re.escape(prefix))
for field, value in dictionary.items():
match = regex.match(field)
if not match:
continue
index, key = match.groups()
index = int(index)
if not key:
ret[index] = value
elif isinstance(ret.get(index), dict):
ret[index][key] = value
else:
ret[index] = Dict({key: value})
return [ret[item] for item in sorted(ret.keys())]
def parse_html_dict(dictionary, prefix):
"""
Used to support dictionary values in HTML forms.
{
'profile.username': 'example',
'profile.email': 'example@example.com',
}
-->
{
'profile': {
'username': 'example,
'email': 'example@example.com'
}
}
"""
ret = {}
regex = re.compile(r'^%s\.(.+)$' % re.escape(prefix))
for field, value in dictionary.items():
match = regex.match(field)
if not match:
continue
key = match.groups()[0]
ret[key] = value
return ret
"""
Helper functions that convert strftime formats into more readable representations.
"""
from rest_framework import ISO_8601
def datetime_formats(formats):
format = ', '.join(formats).replace(
ISO_8601,
'YYYY-MM-DDThh:mm[:ss[.uuuuuu]][+HH:MM|-HH:MM|Z]'
)
return humanize_strptime(format)
def date_formats(formats):
format = ', '.join(formats).replace(ISO_8601, 'YYYY[-MM[-DD]]')
return humanize_strptime(format)
def time_formats(formats):
format = ', '.join(formats).replace(ISO_8601, 'hh:mm[:ss[.uuuuuu]]')
return humanize_strptime(format)
def humanize_strptime(format_string):
# Note that we're missing some of the locale specific mappings that
# don't really make sense.
mapping = {
"%Y": "YYYY",
"%y": "YY",
"%m": "MM",
"%b": "[Jan-Dec]",
"%B": "[January-December]",
"%d": "DD",
"%H": "hh",
"%I": "hh", # Requires '%p' to differentiate from '%H'.
"%M": "mm",
"%S": "ss",
"%f": "uuuuuu",
"%a": "[Mon-Sun]",
"%A": "[Monday-Sunday]",
"%p": "[AM|PM]",
"%z": "[+HHMM|-HHMM]"
}
for key, val in mapping.items():
format_string = format_string.replace(key, val)
return format_string
"""
Helper function for returning the field information that is associated
with a model class. This includes returning all the forward and reverse
relationships and their associated metadata.
Usage: `get_field_info(model)` returns a `FieldInfo` instance.
"""
from collections import namedtuple
from django.db import models
from django.utils import six
from django.utils.datastructures import SortedDict
import inspect
FieldInfo = namedtuple('FieldResult', [
'pk', # Model field instance
'fields', # Dict of field name -> model field instance
'forward_relations', # Dict of field name -> RelationInfo
'reverse_relations', # Dict of field name -> RelationInfo
'fields_and_pk', # Shortcut for 'pk' + 'fields'
'relations' # Shortcut for 'forward_relations' + 'reverse_relations'
])
RelationInfo = namedtuple('RelationInfo', [
'model_field',
'related',
'to_many',
'has_through_model'
])
def _resolve_model(obj):
"""
Resolve supplied `obj` to a Django model class.
`obj` must be a Django model class itself, or a string
representation of one. Useful in situtations like GH #1225 where
Django may not have resolved a string-based reference to a model in
another model's foreign key definition.
String representations should have the format:
'appname.ModelName'
"""
if isinstance(obj, six.string_types) and len(obj.split('.')) == 2:
app_name, model_name = obj.split('.')
return models.get_model(app_name, model_name)
elif inspect.isclass(obj) and issubclass(obj, models.Model):
return obj
raise ValueError("{0} is not a Django model".format(obj))
def get_field_info(model):
"""
Given a model class, returns a `FieldInfo` instance containing metadata
about the various field types on the model.
"""
opts = model._meta.concrete_model._meta
# Deal with the primary key.
pk = opts.pk
while pk.rel and pk.rel.parent_link:
# If model is a child via multitable inheritance, use parent's pk.
pk = pk.rel.to._meta.pk
# Deal with regular fields.
fields = SortedDict()
for field in [field for field in opts.fields if field.serialize and not field.rel]:
fields[field.name] = field
# Deal with forward relationships.
forward_relations = SortedDict()
for field in [field for field in opts.fields if field.serialize and field.rel]:
forward_relations[field.name] = RelationInfo(
model_field=field,
related=_resolve_model(field.rel.to),
to_many=False,
has_through_model=False
)
# Deal with forward many-to-many relationships.
for field in [field for field in opts.many_to_many if field.serialize]:
forward_relations[field.name] = RelationInfo(
model_field=field,
related=_resolve_model(field.rel.to),
to_many=True,
has_through_model=(
not field.rel.through._meta.auto_created
)
)
# Deal with reverse relationships.
reverse_relations = SortedDict()
for relation in opts.get_all_related_objects():
accessor_name = relation.get_accessor_name()
reverse_relations[accessor_name] = RelationInfo(
model_field=None,
related=relation.model,
to_many=relation.field.rel.multiple,
has_through_model=False
)
# Deal with reverse many-to-many relationships.
for relation in opts.get_all_related_many_to_many_objects():
accessor_name = relation.get_accessor_name()
reverse_relations[accessor_name] = RelationInfo(
model_field=None,
related=relation.model,
to_many=True,
has_through_model=(
hasattr(relation.field.rel, 'through') and
not relation.field.rel.through._meta.auto_created
)
)
# Shortcut that merges both regular fields and the pk,
# for simplifying regular field lookup.
fields_and_pk = SortedDict()
fields_and_pk['pk'] = pk
fields_and_pk[pk.name] = pk
fields_and_pk.update(fields)
# Shortcut that merges both forward and reverse relationships
relations = SortedDict(
list(forward_relations.items()) +
list(reverse_relations.items())
)
return FieldInfo(pk, fields, forward_relations, reverse_relations, fields_and_pk, relations)
"""
Helper functions for creating user-friendly representations
of serializer classes and serializer fields.
"""
from django.db import models
import re
def manager_repr(value):
model = value.model
opts = model._meta
for _, name, manager in opts.concrete_managers + opts.abstract_managers:
if manager == value:
return '%s.%s.all()' % (model._meta.object_name, name)
return repr(value)
def smart_repr(value):
if isinstance(value, models.Manager):
return manager_repr(value)
value = repr(value)
# Representations like u'help text'
# should simply be presented as 'help text'
if value.startswith("u'") and value.endswith("'"):
return value[1:]
# Representations like
# <django.core.validators.RegexValidator object at 0x1047af050>
# Should be presented as
# <django.core.validators.RegexValidator object>
value = re.sub(' at 0x[0-9a-f]{4,32}>', '>', value)
return value
def field_repr(field, force_many=False):
kwargs = field._kwargs
if force_many:
kwargs = kwargs.copy()
kwargs['many'] = True
kwargs.pop('child', None)
arg_string = ', '.join([smart_repr(val) for val in field._args])
kwarg_string = ', '.join([
'%s=%s' % (key, smart_repr(val))
for key, val in sorted(kwargs.items())
])
if arg_string and kwarg_string:
arg_string += ', '
if force_many:
class_name = force_many.__class__.__name__
else:
class_name = field.__class__.__name__
return "%s(%s%s)" % (class_name, arg_string, kwarg_string)
def serializer_repr(serializer, indent, force_many=None):
ret = field_repr(serializer, force_many) + ':'
indent_str = ' ' * indent
if force_many:
fields = force_many.fields
else:
fields = serializer.fields
for field_name, field in fields.items():
ret += '\n' + indent_str + field_name + ' = '
if hasattr(field, 'fields'):
ret += serializer_repr(field, indent + 1)
elif hasattr(field, 'child'):
ret += list_repr(field, indent + 1)
elif hasattr(field, 'child_relation'):
ret += field_repr(field.child_relation, force_many=field.child_relation)
else:
ret += field_repr(field)
return ret
def list_repr(serializer, indent):
child = serializer.child
if hasattr(child, 'fields'):
return serializer_repr(serializer, indent, force_many=child)
return field_repr(serializer)
...@@ -3,7 +3,7 @@ Provides an APIView class that is the base of all views in REST framework. ...@@ -3,7 +3,7 @@ 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, ValidationError, NON_FIELD_ERRORS
from django.http import Http404 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
...@@ -51,7 +51,8 @@ def exception_handler(exc): ...@@ -51,7 +51,8 @@ def exception_handler(exc):
Returns the response that should be used for any given exception. Returns the response that should be used for any given exception.
By default we handle the REST framework `APIException`, and also By default we handle the REST framework `APIException`, and also
Django's builtin `Http404` and `PermissionDenied` exceptions. Django's built-in `ValidationError`, `Http404` and `PermissionDenied`
exceptions.
Any unhandled exceptions may return `None`, which will cause a 500 error Any unhandled exceptions may return `None`, which will cause a 500 error
to be raised. to be raised.
...@@ -61,13 +62,22 @@ def exception_handler(exc): ...@@ -61,13 +62,22 @@ def exception_handler(exc):
if getattr(exc, 'auth_header', None): if getattr(exc, 'auth_header', None):
headers['WWW-Authenticate'] = exc.auth_header headers['WWW-Authenticate'] = exc.auth_header
if getattr(exc, 'wait', None): if getattr(exc, 'wait', None):
headers['X-Throttle-Wait-Seconds'] = '%d' % exc.wait
headers['Retry-After'] = '%d' % exc.wait headers['Retry-After'] = '%d' % exc.wait
return Response({'detail': exc.detail}, return Response({'detail': exc.detail},
status=exc.status_code, status=exc.status_code,
headers=headers) headers=headers)
elif isinstance(exc, ValidationError):
# ValidationErrors may include the non-field key named '__all__'.
# When returning a response we map this to a key name that can be
# modified in settings.
if NON_FIELD_ERRORS in exc.message_dict:
errors = exc.message_dict.pop(NON_FIELD_ERRORS)
exc.message_dict[api_settings.NON_FIELD_ERRORS_KEY] = errors
return Response(exc.message_dict,
status=status.HTTP_400_BAD_REQUEST)
elif isinstance(exc, Http404): elif isinstance(exc, Http404):
return Response({'detail': 'Not found'}, return Response({'detail': 'Not found'},
status=status.HTTP_404_NOT_FOUND) status=status.HTTP_404_NOT_FOUND)
......
...@@ -20,6 +20,7 @@ from __future__ import unicode_literals ...@@ -20,6 +20,7 @@ from __future__ import unicode_literals
from functools import update_wrapper from functools import update_wrapper
from django.utils.decorators import classonlymethod from django.utils.decorators import classonlymethod
from django.views.decorators.csrf import csrf_exempt
from rest_framework import views, generics, mixins from rest_framework import views, generics, mixins
...@@ -89,7 +90,7 @@ class ViewSetMixin(object): ...@@ -89,7 +90,7 @@ class ViewSetMixin(object):
# resolved URL. # resolved URL.
view.cls = cls view.cls = cls
view.suffix = initkwargs.get('suffix', None) view.suffix = initkwargs.get('suffix', None)
return view return csrf_exempt(view)
def initialize_request(self, request, *args, **kargs): def initialize_request(self, request, *args, **kargs):
""" """
......
from __future__ import unicode_literals from __future__ import unicode_literals
from django.db import models from django.db import models
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from rest_framework import serializers
def foobar(): def foobar():
...@@ -178,9 +177,3 @@ class NullableOneToOneSource(RESTFrameworkModel): ...@@ -178,9 +177,3 @@ class NullableOneToOneSource(RESTFrameworkModel):
name = models.CharField(max_length=100) name = models.CharField(max_length=100)
target = models.OneToOneField(OneToOneTarget, null=True, blank=True, target = models.OneToOneField(OneToOneTarget, null=True, blank=True,
related_name='nullable_source') related_name='nullable_source')
# Serializer used to test BasicModel
class BasicModelSerializer(serializers.ModelSerializer):
class Meta:
model = BasicModel
# From test_validation...
class TestPreSaveValidationExclusions(TestCase):
def test_pre_save_validation_exclusions(self):
"""
Somewhat weird test case to ensure that we don't perform model
validation on read only fields.
"""
obj = ValidationModel.objects.create(blank_validated_field='')
request = factory.put('/', {}, format='json')
view = UpdateValidationModel().as_view()
response = view(request, pk=obj.pk).render()
self.assertEqual(response.status_code, status.HTTP_200_OK)
# From test_permissions...
class ModelPermissionsIntegrationTests(TestCase):
def setUp(...):
...
def test_has_put_as_create_permissions(self):
# User only has update permissions - should be able to update an entity.
request = factory.put('/1', {'text': 'foobar'}, format='json',
HTTP_AUTHORIZATION=self.updateonly_credentials)
response = instance_view(request, pk='1')
self.assertEqual(response.status_code, status.HTTP_200_OK)
# But if PUTing to a new entity, permission should be denied.
request = factory.put('/2', {'text': 'foobar'}, format='json',
HTTP_AUTHORIZATION=self.updateonly_credentials)
response = instance_view(request, pk='2')
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
from rest_framework import serializers
from tests.models import NullableForeignKeySource
class NullableFKSourceSerializer(serializers.ModelSerializer):
class Meta:
model = NullableForeignKeySource
...@@ -98,6 +98,30 @@ class TestViewNamesAndDescriptions(TestCase): ...@@ -98,6 +98,30 @@ class TestViewNamesAndDescriptions(TestCase):
pass pass
self.assertEqual(MockView().get_view_description(), '') self.assertEqual(MockView().get_view_description(), '')
def test_view_description_can_be_promise(self):
"""
Ensure a view may have a docstring that is actually a lazily evaluated
class that can be converted to a string.
See: https://github.com/tomchristie/django-rest-framework/issues/1708
"""
# use a mock object instead of gettext_lazy to ensure that we can't end
# up with a test case string in our l10n catalog
class MockLazyStr(object):
def __init__(self, string):
self.s = string
def __str__(self):
return self.s
def __unicode__(self):
return self.s
class MockView(APIView):
__doc__ = MockLazyStr("a gettext string")
self.assertEqual(MockView().get_view_description(), 'a gettext string')
def test_markdown(self): def test_markdown(self):
""" """
Ensure markdown to HTML works as expected. Ensure markdown to HTML works as expected.
......
from __future__ import unicode_literals # from __future__ import unicode_literals
from django.test import TestCase # from django.test import TestCase
from django.utils import six # from django.utils import six
from rest_framework import serializers # from rest_framework import serializers
from rest_framework.compat import BytesIO # from rest_framework.compat import BytesIO
import datetime # import datetime
class UploadedFile(object): # class UploadedFile(object):
def __init__(self, file=None, created=None): # def __init__(self, file=None, created=None):
self.file = file # self.file = file
self.created = created or datetime.datetime.now() # self.created = created or datetime.datetime.now()
class UploadedFileSerializer(serializers.Serializer): # class UploadedFileSerializer(serializers.Serializer):
file = serializers.FileField(required=False) # file = serializers.FileField(required=False)
created = serializers.DateTimeField() # created = serializers.DateTimeField()
def restore_object(self, attrs, instance=None): # def restore_object(self, attrs, instance=None):
if instance: # if instance:
instance.file = attrs['file'] # instance.file = attrs['file']
instance.created = attrs['created'] # instance.created = attrs['created']
return instance # return instance
return UploadedFile(**attrs) # return UploadedFile(**attrs)
class FileSerializerTests(TestCase): # class FileSerializerTests(TestCase):
def test_create(self): # def test_create(self):
now = datetime.datetime.now() # now = datetime.datetime.now()
file = BytesIO(six.b('stuff')) # file = BytesIO(six.b('stuff'))
file.name = 'stuff.txt' # file.name = 'stuff.txt'
file.size = len(file.getvalue()) # file.size = len(file.getvalue())
serializer = UploadedFileSerializer(data={'created': now}, files={'file': file}) # serializer = UploadedFileSerializer(data={'created': now}, files={'file': file})
uploaded_file = UploadedFile(file=file, created=now) # uploaded_file = UploadedFile(file=file, created=now)
self.assertTrue(serializer.is_valid()) # self.assertTrue(serializer.is_valid())
self.assertEqual(serializer.object.created, uploaded_file.created) # self.assertEqual(serializer.object.created, uploaded_file.created)
self.assertEqual(serializer.object.file, uploaded_file.file) # self.assertEqual(serializer.object.file, uploaded_file.file)
self.assertFalse(serializer.object is uploaded_file) # self.assertFalse(serializer.object is uploaded_file)
def test_creation_failure(self): # def test_creation_failure(self):
""" # """
Passing files=None should result in an ValidationError # Passing files=None should result in an ValidationError
Regression test for: # Regression test for:
https://github.com/tomchristie/django-rest-framework/issues/542 # https://github.com/tomchristie/django-rest-framework/issues/542
""" # """
now = datetime.datetime.now() # now = datetime.datetime.now()
serializer = UploadedFileSerializer(data={'created': now}) # serializer = UploadedFileSerializer(data={'created': now})
self.assertTrue(serializer.is_valid()) # self.assertTrue(serializer.is_valid())
self.assertEqual(serializer.object.created, now) # self.assertEqual(serializer.object.created, now)
self.assertIsNone(serializer.object.file) # self.assertIsNone(serializer.object.file)
def test_remove_with_empty_string(self): # def test_remove_with_empty_string(self):
""" # """
Passing empty string as data should cause file to be removed # Passing empty string as data should cause file to be removed
Test for: # Test for:
https://github.com/tomchristie/django-rest-framework/issues/937 # https://github.com/tomchristie/django-rest-framework/issues/937
""" # """
now = datetime.datetime.now() # now = datetime.datetime.now()
file = BytesIO(six.b('stuff')) # file = BytesIO(six.b('stuff'))
file.name = 'stuff.txt' # file.name = 'stuff.txt'
file.size = len(file.getvalue()) # file.size = len(file.getvalue())
uploaded_file = UploadedFile(file=file, created=now) # uploaded_file = UploadedFile(file=file, created=now)
serializer = UploadedFileSerializer(instance=uploaded_file, data={'created': now, 'file': ''}) # serializer = UploadedFileSerializer(instance=uploaded_file, data={'created': now, 'file': ''})
self.assertTrue(serializer.is_valid()) # self.assertTrue(serializer.is_valid())
self.assertEqual(serializer.object.created, uploaded_file.created) # self.assertEqual(serializer.object.created, uploaded_file.created)
self.assertIsNone(serializer.object.file) # self.assertIsNone(serializer.object.file)
def test_validation_error_with_non_file(self): # def test_validation_error_with_non_file(self):
""" # """
Passing non-files should raise a validation error. # Passing non-files should raise a validation error.
""" # """
now = datetime.datetime.now() # now = datetime.datetime.now()
errmsg = 'No file was submitted. Check the encoding type on the form.' # errmsg = 'No file was submitted. Check the encoding type on the form.'
serializer = UploadedFileSerializer(data={'created': now, 'file': 'abc'}) # serializer = UploadedFileSerializer(data={'created': now, 'file': 'abc'})
self.assertFalse(serializer.is_valid()) # self.assertFalse(serializer.is_valid())
self.assertEqual(serializer.errors, {'file': [errmsg]}) # self.assertEqual(serializer.errors, {'file': [errmsg]})
def test_validation_with_no_data(self): # def test_validation_with_no_data(self):
""" # """
Validation should still function when no data dictionary is provided. # Validation should still function when no data dictionary is provided.
""" # """
uploaded_file = BytesIO(six.b('stuff')) # uploaded_file = BytesIO(six.b('stuff'))
uploaded_file.name = 'stuff.txt' # uploaded_file.name = 'stuff.txt'
uploaded_file.size = len(uploaded_file.getvalue()) # uploaded_file.size = len(uploaded_file.getvalue())
serializer = UploadedFileSerializer(files={'file': uploaded_file}) # serializer = UploadedFileSerializer(files={'file': uploaded_file})
self.assertFalse(serializer.is_valid()) # self.assertFalse(serializer.is_valid())
from __future__ import unicode_literals # from __future__ import unicode_literals
from django.contrib.contenttypes.models import ContentType # from django.contrib.contenttypes.models import ContentType
from django.contrib.contenttypes.generic import GenericRelation, GenericForeignKey # from django.contrib.contenttypes.generic import GenericRelation, GenericForeignKey
from django.db import models # from django.db import models
from django.test import TestCase # from django.test import TestCase
from rest_framework import serializers # from rest_framework import serializers
from rest_framework.compat import python_2_unicode_compatible # from rest_framework.compat import python_2_unicode_compatible
@python_2_unicode_compatible # @python_2_unicode_compatible
class Tag(models.Model): # class Tag(models.Model):
""" # """
Tags have a descriptive slug, and are attached to an arbitrary object. # Tags have a descriptive slug, and are attached to an arbitrary object.
""" # """
tag = models.SlugField() # tag = models.SlugField()
content_type = models.ForeignKey(ContentType) # content_type = models.ForeignKey(ContentType)
object_id = models.PositiveIntegerField() # object_id = models.PositiveIntegerField()
tagged_item = GenericForeignKey('content_type', 'object_id') # tagged_item = GenericForeignKey('content_type', 'object_id')
def __str__(self): # def __str__(self):
return self.tag # return self.tag
@python_2_unicode_compatible # @python_2_unicode_compatible
class Bookmark(models.Model): # class Bookmark(models.Model):
""" # """
A URL bookmark that may have multiple tags attached. # A URL bookmark that may have multiple tags attached.
""" # """
url = models.URLField() # url = models.URLField()
tags = GenericRelation(Tag) # tags = GenericRelation(Tag)
def __str__(self): # def __str__(self):
return 'Bookmark: %s' % self.url # return 'Bookmark: %s' % self.url
@python_2_unicode_compatible # @python_2_unicode_compatible
class Note(models.Model): # class Note(models.Model):
""" # """
A textual note that may have multiple tags attached. # A textual note that may have multiple tags attached.
""" # """
text = models.TextField() # text = models.TextField()
tags = GenericRelation(Tag) # tags = GenericRelation(Tag)
def __str__(self): # def __str__(self):
return 'Note: %s' % self.text # return 'Note: %s' % self.text
class TestGenericRelations(TestCase): # class TestGenericRelations(TestCase):
def setUp(self): # def setUp(self):
self.bookmark = Bookmark.objects.create(url='https://www.djangoproject.com/') # self.bookmark = Bookmark.objects.create(url='https://www.djangoproject.com/')
Tag.objects.create(tagged_item=self.bookmark, tag='django') # Tag.objects.create(tagged_item=self.bookmark, tag='django')
Tag.objects.create(tagged_item=self.bookmark, tag='python') # Tag.objects.create(tagged_item=self.bookmark, tag='python')
self.note = Note.objects.create(text='Remember the milk') # self.note = Note.objects.create(text='Remember the milk')
Tag.objects.create(tagged_item=self.note, tag='reminder') # Tag.objects.create(tagged_item=self.note, tag='reminder')
def test_generic_relation(self): # def test_generic_relation(self):
""" # """
Test a relationship that spans a GenericRelation field. # Test a relationship that spans a GenericRelation field.
IE. A reverse generic relationship. # IE. A reverse generic relationship.
""" # """
class BookmarkSerializer(serializers.ModelSerializer): # class BookmarkSerializer(serializers.ModelSerializer):
tags = serializers.RelatedField(many=True) # tags = serializers.RelatedField(many=True)
class Meta: # class Meta:
model = Bookmark # model = Bookmark
exclude = ('id',) # exclude = ('id',)
serializer = BookmarkSerializer(self.bookmark) # serializer = BookmarkSerializer(self.bookmark)
expected = { # expected = {
'tags': ['django', 'python'], # 'tags': ['django', 'python'],
'url': 'https://www.djangoproject.com/' # 'url': 'https://www.djangoproject.com/'
} # }
self.assertEqual(serializer.data, expected) # self.assertEqual(serializer.data, expected)
def test_generic_nested_relation(self): # def test_generic_nested_relation(self):
""" # """
Test saving a GenericRelation field via a nested serializer. # Test saving a GenericRelation field via a nested serializer.
""" # """
class TagSerializer(serializers.ModelSerializer): # class TagSerializer(serializers.ModelSerializer):
class Meta: # class Meta:
model = Tag # model = Tag
exclude = ('content_type', 'object_id') # exclude = ('content_type', 'object_id')
class BookmarkSerializer(serializers.ModelSerializer): # class BookmarkSerializer(serializers.ModelSerializer):
tags = TagSerializer(many=True) # tags = TagSerializer(many=True)
class Meta: # class Meta:
model = Bookmark # model = Bookmark
exclude = ('id',) # exclude = ('id',)
data = { # data = {
'url': 'https://docs.djangoproject.com/', # 'url': 'https://docs.djangoproject.com/',
'tags': [ # 'tags': [
{'tag': 'contenttypes'}, # {'tag': 'contenttypes'},
{'tag': 'genericrelations'}, # {'tag': 'genericrelations'},
] # ]
} # }
serializer = BookmarkSerializer(data=data) # serializer = BookmarkSerializer(data=data)
self.assertTrue(serializer.is_valid()) # self.assertTrue(serializer.is_valid())
serializer.save() # serializer.save()
self.assertEqual(serializer.object.tags.count(), 2) # self.assertEqual(serializer.object.tags.count(), 2)
def test_generic_fk(self): # def test_generic_fk(self):
""" # """
Test a relationship that spans a GenericForeignKey field. # Test a relationship that spans a GenericForeignKey field.
IE. A forward generic relationship. # IE. A forward generic relationship.
""" # """
class TagSerializer(serializers.ModelSerializer): # class TagSerializer(serializers.ModelSerializer):
tagged_item = serializers.RelatedField() # tagged_item = serializers.RelatedField()
class Meta: # class Meta:
model = Tag # model = Tag
exclude = ('id', 'content_type', 'object_id') # exclude = ('id', 'content_type', 'object_id')
serializer = TagSerializer(Tag.objects.all(), many=True) # serializer = TagSerializer(Tag.objects.all(), many=True)
expected = [ # expected = [
{ # {
'tag': 'django', # 'tag': 'django',
'tagged_item': 'Bookmark: https://www.djangoproject.com/' # 'tagged_item': 'Bookmark: https://www.djangoproject.com/'
}, # },
{ # {
'tag': 'python', # 'tag': 'python',
'tagged_item': 'Bookmark: https://www.djangoproject.com/' # 'tagged_item': 'Bookmark: https://www.djangoproject.com/'
}, # },
{ # {
'tag': 'reminder', # 'tag': 'reminder',
'tagged_item': 'Note: Remember the milk' # 'tagged_item': 'Note: Remember the milk'
} # }
] # ]
self.assertEqual(serializer.data, expected) # self.assertEqual(serializer.data, expected)
def test_restore_object_generic_fk(self): # def test_restore_object_generic_fk(self):
""" # """
Ensure an object with a generic foreign key can be restored. # Ensure an object with a generic foreign key can be restored.
""" # """
class TagSerializer(serializers.ModelSerializer): # class TagSerializer(serializers.ModelSerializer):
class Meta: # class Meta:
model = Tag # model = Tag
exclude = ('content_type', 'object_id') # exclude = ('content_type', 'object_id')
serializer = TagSerializer() # serializer = TagSerializer()
bookmark = Bookmark(url='http://example.com') # bookmark = Bookmark(url='http://example.com')
attrs = {'tagged_item': bookmark, 'tag': 'example'} # attrs = {'tagged_item': bookmark, 'tag': 'example'}
tag = serializer.restore_object(attrs) # tag = serializer.restore_object(attrs)
self.assertEqual(tag.tagged_item, bookmark) # self.assertEqual(tag.tagged_item, bookmark)
from django.test import TestCase from django.test import TestCase
from django.utils import six from django.utils import six
from rest_framework.serializers import _resolve_model from rest_framework.utils.model_meta import _resolve_model
from tests.models import BasicModel from tests.models import BasicModel
......
from django.core.urlresolvers import reverse # from django.core.urlresolvers import reverse
from django.conf.urls import patterns, url # from django.conf.urls import patterns, url
from rest_framework.test import APITestCase # from rest_framework import serializers, generics
from tests.models import NullableForeignKeySource # from rest_framework.test import APITestCase
from tests.serializers import NullableFKSourceSerializer # from tests.models import NullableForeignKeySource
from tests.views import NullableFKSourceDetail
urlpatterns = patterns( # class NullableFKSourceSerializer(serializers.ModelSerializer):
'', # class Meta:
url(r'^objects/(?P<pk>\d+)/$', NullableFKSourceDetail.as_view(), name='object-detail'), # model = NullableForeignKeySource
)
class NullableForeignKeyTests(APITestCase): # class NullableFKSourceDetail(generics.RetrieveUpdateDestroyAPIView):
""" # queryset = NullableForeignKeySource.objects.all()
DRF should be able to handle nullable foreign keys when a test # serializer_class = NullableFKSourceSerializer
Client POST/PUT request is made with its own serialized object.
"""
urls = 'tests.test_nullable_fields'
def test_updating_object_with_null_fk(self):
obj = NullableForeignKeySource(name='example', target=None)
obj.save()
serialized_data = NullableFKSourceSerializer(obj).data
response = self.client.put(reverse('object-detail', args=[obj.pk]), serialized_data) # urlpatterns = patterns(
# '',
# url(r'^objects/(?P<pk>\d+)/$', NullableFKSourceDetail.as_view(), name='object-detail'),
# )
self.assertEqual(response.data, serialized_data)
# class NullableForeignKeyTests(APITestCase):
# """
# DRF should be able to handle nullable foreign keys when a test
# Client POST/PUT request is made with its own serialized object.
# """
# urls = 'tests.test_nullable_fields'
# def test_updating_object_with_null_fk(self):
# obj = NullableForeignKeySource(name='example', target=None)
# obj.save()
# serialized_data = NullableFKSourceSerializer(obj).data
# response = self.client.put(reverse('object-detail', args=[obj.pk]), serialized_data)
# self.assertEqual(response.data, serialized_data)
...@@ -4,7 +4,7 @@ from decimal import Decimal ...@@ -4,7 +4,7 @@ from decimal import Decimal
from django.core.paginator import Paginator from django.core.paginator import Paginator
from django.test import TestCase from django.test import TestCase
from django.utils import unittest from django.utils import unittest
from rest_framework import generics, status, pagination, filters, serializers from rest_framework import generics, serializers, status, pagination, filters
from rest_framework.compat import django_filters from rest_framework.compat import django_filters
from rest_framework.test import APIRequestFactory from rest_framework.test import APIRequestFactory
from .models import BasicModel, FilterableItem from .models import BasicModel, FilterableItem
...@@ -22,11 +22,22 @@ def split_arguments_from_url(url): ...@@ -22,11 +22,22 @@ def split_arguments_from_url(url):
return path, args return path, args
class BasicSerializer(serializers.ModelSerializer):
class Meta:
model = BasicModel
class FilterableItemSerializer(serializers.ModelSerializer):
class Meta:
model = FilterableItem
class RootView(generics.ListCreateAPIView): class RootView(generics.ListCreateAPIView):
""" """
Example description for OPTIONS. Example description for OPTIONS.
""" """
model = BasicModel queryset = BasicModel.objects.all()
serializer_class = BasicSerializer
paginate_by = 10 paginate_by = 10
...@@ -34,14 +45,16 @@ class DefaultPageSizeKwargView(generics.ListAPIView): ...@@ -34,14 +45,16 @@ class DefaultPageSizeKwargView(generics.ListAPIView):
""" """
View for testing default paginate_by_param usage View for testing default paginate_by_param usage
""" """
model = BasicModel queryset = BasicModel.objects.all()
serializer_class = BasicSerializer
class PaginateByParamView(generics.ListAPIView): class PaginateByParamView(generics.ListAPIView):
""" """
View for testing custom paginate_by_param usage View for testing custom paginate_by_param usage
""" """
model = BasicModel queryset = BasicModel.objects.all()
serializer_class = BasicSerializer
paginate_by_param = 'page_size' paginate_by_param = 'page_size'
...@@ -49,7 +62,8 @@ class MaxPaginateByView(generics.ListAPIView): ...@@ -49,7 +62,8 @@ class MaxPaginateByView(generics.ListAPIView):
""" """
View for testing custom max_paginate_by usage View for testing custom max_paginate_by usage
""" """
model = BasicModel queryset = BasicModel.objects.all()
serializer_class = BasicSerializer
paginate_by = 3 paginate_by = 3
max_paginate_by = 5 max_paginate_by = 5
paginate_by_param = 'page_size' paginate_by_param = 'page_size'
...@@ -121,7 +135,7 @@ class IntegrationTestPaginationAndFiltering(TestCase): ...@@ -121,7 +135,7 @@ class IntegrationTestPaginationAndFiltering(TestCase):
self.objects = FilterableItem.objects self.objects = FilterableItem.objects
self.data = [ self.data = [
{'id': obj.id, 'text': obj.text, 'decimal': obj.decimal, 'date': obj.date} {'id': obj.id, 'text': obj.text, 'decimal': str(obj.decimal), 'date': obj.date.isoformat()}
for obj in self.objects.all() for obj in self.objects.all()
] ]
...@@ -140,7 +154,8 @@ class IntegrationTestPaginationAndFiltering(TestCase): ...@@ -140,7 +154,8 @@ class IntegrationTestPaginationAndFiltering(TestCase):
fields = ['text', 'decimal', 'date'] fields = ['text', 'decimal', 'date']
class FilterFieldsRootView(generics.ListCreateAPIView): class FilterFieldsRootView(generics.ListCreateAPIView):
model = FilterableItem queryset = FilterableItem.objects.all()
serializer_class = FilterableItemSerializer
paginate_by = 10 paginate_by = 10
filter_class = DecimalFilter filter_class = DecimalFilter
filter_backends = (filters.DjangoFilterBackend,) filter_backends = (filters.DjangoFilterBackend,)
...@@ -188,7 +203,8 @@ class IntegrationTestPaginationAndFiltering(TestCase): ...@@ -188,7 +203,8 @@ class IntegrationTestPaginationAndFiltering(TestCase):
return queryset.filter(decimal__lt=Decimal(request.GET['decimal'])) return queryset.filter(decimal__lt=Decimal(request.GET['decimal']))
class BasicFilterFieldsRootView(generics.ListCreateAPIView): class BasicFilterFieldsRootView(generics.ListCreateAPIView):
model = FilterableItem queryset = FilterableItem.objects.all()
serializer_class = FilterableItemSerializer
paginate_by = 10 paginate_by = 10
filter_backends = (DecimalFilterBackend,) filter_backends = (DecimalFilterBackend,)
...@@ -365,7 +381,7 @@ class TestMaxPaginateByParam(TestCase): ...@@ -365,7 +381,7 @@ class TestMaxPaginateByParam(TestCase):
# Tests for context in pagination serializers # Tests for context in pagination serializers
class CustomField(serializers.Field): class CustomField(serializers.ReadOnlyField):
def to_native(self, value): def to_native(self, value):
if 'view' not in self.context: if 'view' not in self.context:
raise RuntimeError("context isn't getting passed into custom field") raise RuntimeError("context isn't getting passed into custom field")
...@@ -375,10 +391,10 @@ class CustomField(serializers.Field): ...@@ -375,10 +391,10 @@ class CustomField(serializers.Field):
class BasicModelSerializer(serializers.Serializer): class BasicModelSerializer(serializers.Serializer):
text = CustomField() text = CustomField()
def __init__(self, *args, **kwargs): def to_native(self, value):
super(BasicModelSerializer, self).__init__(*args, **kwargs)
if 'view' not in self.context: if 'view' not in self.context:
raise RuntimeError("context isn't getting passed into serializer init") raise RuntimeError("context isn't getting passed into serializer")
return super(BasicSerializer, self).to_native(value)
class TestContextPassedToCustomField(TestCase): class TestContextPassedToCustomField(TestCase):
...@@ -387,7 +403,7 @@ class TestContextPassedToCustomField(TestCase): ...@@ -387,7 +403,7 @@ class TestContextPassedToCustomField(TestCase):
def test_with_pagination(self): def test_with_pagination(self):
class ListView(generics.ListCreateAPIView): class ListView(generics.ListCreateAPIView):
model = BasicModel queryset = BasicModel.objects.all()
serializer_class = BasicModelSerializer serializer_class = BasicModelSerializer
paginate_by = 1 paginate_by = 1
...@@ -407,7 +423,7 @@ class LinksSerializer(serializers.Serializer): ...@@ -407,7 +423,7 @@ class LinksSerializer(serializers.Serializer):
class CustomPaginationSerializer(pagination.BasePaginationSerializer): class CustomPaginationSerializer(pagination.BasePaginationSerializer):
links = LinksSerializer(source='*') # Takes the page object as the source links = LinksSerializer(source='*') # Takes the page object as the source
total_results = serializers.Field(source='paginator.count') total_results = serializers.ReadOnlyField(source='paginator.count')
results_field = 'objects' results_field = 'objects'
......
# -*- coding: utf-8 -*-
from __future__ import unicode_literals from __future__ import unicode_literals
from rest_framework.compat import StringIO from rest_framework.compat import StringIO
from django import forms from django import forms
...@@ -113,3 +115,25 @@ class TestFileUploadParser(TestCase): ...@@ -113,3 +115,25 @@ class TestFileUploadParser(TestCase):
parser = FileUploadParser() parser = FileUploadParser()
filename = parser.get_filename(self.stream, None, self.parser_context) filename = parser.get_filename(self.stream, None, self.parser_context)
self.assertEqual(filename, 'file.txt') self.assertEqual(filename, 'file.txt')
def test_get_encoded_filename(self):
parser = FileUploadParser()
self.__replace_content_disposition('inline; filename*=utf-8\'\'ÀĥƦ.txt')
filename = parser.get_filename(self.stream, None, self.parser_context)
self.assertEqual(filename, 'ÀĥƦ.txt')
self.__replace_content_disposition('inline; filename=fallback.txt; filename*=utf-8\'\'ÀĥƦ.txt')
filename = parser.get_filename(self.stream, None, self.parser_context)
self.assertEqual(filename, 'ÀĥƦ.txt')
self.__replace_content_disposition('inline; filename=fallback.txt; filename*=utf-8\'en-us\'ÀĥƦ.txt')
filename = parser.get_filename(self.stream, None, self.parser_context)
self.assertEqual(filename, 'ÀĥƦ.txt')
self.__replace_content_disposition('inline; filename=fallback.txt; filename*=utf-8--ÀĥƦ.txt')
filename = parser.get_filename(self.stream, None, self.parser_context)
self.assertEqual(filename, 'fallback.txt')
def __replace_content_disposition(self, disposition):
self.parser_context['request'].META['HTTP_CONTENT_DISPOSITION'] = disposition
...@@ -3,7 +3,7 @@ from django.contrib.auth.models import User, Permission, Group ...@@ -3,7 +3,7 @@ from django.contrib.auth.models import User, Permission, Group
from django.db import models from django.db import models
from django.test import TestCase from django.test import TestCase
from django.utils import unittest from django.utils import unittest
from rest_framework import generics, status, permissions, authentication, HTTP_HEADER_ENCODING from rest_framework import generics, serializers, status, permissions, authentication, HTTP_HEADER_ENCODING
from rest_framework.compat import guardian, get_model_name from rest_framework.compat import guardian, get_model_name
from rest_framework.filters import DjangoObjectPermissionsFilter from rest_framework.filters import DjangoObjectPermissionsFilter
from rest_framework.test import APIRequestFactory from rest_framework.test import APIRequestFactory
...@@ -13,14 +13,21 @@ import base64 ...@@ -13,14 +13,21 @@ import base64
factory = APIRequestFactory() factory = APIRequestFactory()
class RootView(generics.ListCreateAPIView): class BasicSerializer(serializers.ModelSerializer):
class Meta:
model = BasicModel model = BasicModel
class RootView(generics.ListCreateAPIView):
queryset = BasicModel.objects.all()
serializer_class = BasicSerializer
authentication_classes = [authentication.BasicAuthentication] authentication_classes = [authentication.BasicAuthentication]
permission_classes = [permissions.DjangoModelPermissions] permission_classes = [permissions.DjangoModelPermissions]
class InstanceView(generics.RetrieveUpdateDestroyAPIView): class InstanceView(generics.RetrieveUpdateDestroyAPIView):
model = BasicModel queryset = BasicModel.objects.all()
serializer_class = BasicSerializer
authentication_classes = [authentication.BasicAuthentication] authentication_classes = [authentication.BasicAuthentication]
permission_classes = [permissions.DjangoModelPermissions] permission_classes = [permissions.DjangoModelPermissions]
...@@ -88,72 +95,59 @@ class ModelPermissionsIntegrationTests(TestCase): ...@@ -88,72 +95,59 @@ class ModelPermissionsIntegrationTests(TestCase):
response = instance_view(request, pk=1) response = instance_view(request, pk=1)
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
def test_has_put_as_create_permissions(self): # def test_options_permitted(self):
# User only has update permissions - should be able to update an entity. # request = factory.options(
request = factory.put('/1', {'text': 'foobar'}, format='json', # '/',
HTTP_AUTHORIZATION=self.updateonly_credentials) # HTTP_AUTHORIZATION=self.permitted_credentials
response = instance_view(request, pk='1') # )
self.assertEqual(response.status_code, status.HTTP_200_OK) # response = root_view(request, pk='1')
# self.assertEqual(response.status_code, status.HTTP_200_OK)
# But if PUTing to a new entity, permission should be denied. # self.assertIn('actions', response.data)
request = factory.put('/2', {'text': 'foobar'}, format='json', # self.assertEqual(list(response.data['actions'].keys()), ['POST'])
HTTP_AUTHORIZATION=self.updateonly_credentials)
response = instance_view(request, pk='2') # request = factory.options(
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) # '/1',
# HTTP_AUTHORIZATION=self.permitted_credentials
def test_options_permitted(self): # )
request = factory.options( # response = instance_view(request, pk='1')
'/', # self.assertEqual(response.status_code, status.HTTP_200_OK)
HTTP_AUTHORIZATION=self.permitted_credentials # self.assertIn('actions', response.data)
) # self.assertEqual(list(response.data['actions'].keys()), ['PUT'])
response = root_view(request, pk='1')
self.assertEqual(response.status_code, status.HTTP_200_OK) # def test_options_disallowed(self):
self.assertIn('actions', response.data) # request = factory.options(
self.assertEqual(list(response.data['actions'].keys()), ['POST']) # '/',
# HTTP_AUTHORIZATION=self.disallowed_credentials
request = factory.options( # )
'/1', # response = root_view(request, pk='1')
HTTP_AUTHORIZATION=self.permitted_credentials # self.assertEqual(response.status_code, status.HTTP_200_OK)
) # self.assertNotIn('actions', response.data)
response = instance_view(request, pk='1')
self.assertEqual(response.status_code, status.HTTP_200_OK) # request = factory.options(
self.assertIn('actions', response.data) # '/1',
self.assertEqual(list(response.data['actions'].keys()), ['PUT']) # HTTP_AUTHORIZATION=self.disallowed_credentials
# )
def test_options_disallowed(self): # response = instance_view(request, pk='1')
request = factory.options( # self.assertEqual(response.status_code, status.HTTP_200_OK)
'/', # self.assertNotIn('actions', response.data)
HTTP_AUTHORIZATION=self.disallowed_credentials
) # def test_options_updateonly(self):
response = root_view(request, pk='1') # request = factory.options(
self.assertEqual(response.status_code, status.HTTP_200_OK) # '/',
self.assertNotIn('actions', response.data) # HTTP_AUTHORIZATION=self.updateonly_credentials
# )
request = factory.options( # response = root_view(request, pk='1')
'/1', # self.assertEqual(response.status_code, status.HTTP_200_OK)
HTTP_AUTHORIZATION=self.disallowed_credentials # self.assertNotIn('actions', response.data)
)
response = instance_view(request, pk='1') # request = factory.options(
self.assertEqual(response.status_code, status.HTTP_200_OK) # '/1',
self.assertNotIn('actions', response.data) # HTTP_AUTHORIZATION=self.updateonly_credentials
# )
def test_options_updateonly(self): # response = instance_view(request, pk='1')
request = factory.options( # self.assertEqual(response.status_code, status.HTTP_200_OK)
'/', # self.assertIn('actions', response.data)
HTTP_AUTHORIZATION=self.updateonly_credentials # self.assertEqual(list(response.data['actions'].keys()), ['PUT'])
)
response = root_view(request, pk='1')
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertNotIn('actions', response.data)
request = factory.options(
'/1',
HTTP_AUTHORIZATION=self.updateonly_credentials
)
response = instance_view(request, pk='1')
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertIn('actions', response.data)
self.assertEqual(list(response.data['actions'].keys()), ['PUT'])
class BasicPermModel(models.Model): class BasicPermModel(models.Model):
...@@ -167,6 +161,11 @@ class BasicPermModel(models.Model): ...@@ -167,6 +161,11 @@ class BasicPermModel(models.Model):
) )
class BasicPermSerializer(serializers.ModelSerializer):
class Meta:
model = BasicPermModel
# Custom object-level permission, that includes 'view' permissions # Custom object-level permission, that includes 'view' permissions
class ViewObjectPermissions(permissions.DjangoObjectPermissions): class ViewObjectPermissions(permissions.DjangoObjectPermissions):
perms_map = { perms_map = {
...@@ -181,7 +180,8 @@ class ViewObjectPermissions(permissions.DjangoObjectPermissions): ...@@ -181,7 +180,8 @@ class ViewObjectPermissions(permissions.DjangoObjectPermissions):
class ObjectPermissionInstanceView(generics.RetrieveUpdateDestroyAPIView): class ObjectPermissionInstanceView(generics.RetrieveUpdateDestroyAPIView):
model = BasicPermModel queryset = BasicPermModel.objects.all()
serializer_class = BasicPermSerializer
authentication_classes = [authentication.BasicAuthentication] authentication_classes = [authentication.BasicAuthentication]
permission_classes = [ViewObjectPermissions] permission_classes = [ViewObjectPermissions]
...@@ -189,7 +189,8 @@ object_permissions_view = ObjectPermissionInstanceView.as_view() ...@@ -189,7 +189,8 @@ object_permissions_view = ObjectPermissionInstanceView.as_view()
class ObjectPermissionListView(generics.ListAPIView): class ObjectPermissionListView(generics.ListAPIView):
model = BasicPermModel queryset = BasicPermModel.objects.all()
serializer_class = BasicPermSerializer
authentication_classes = [authentication.BasicAuthentication] authentication_classes = [authentication.BasicAuthentication]
permission_classes = [ViewObjectPermissions] permission_classes = [ViewObjectPermissions]
......
...@@ -13,7 +13,7 @@ from rest_framework.compat import yaml, etree, StringIO ...@@ -13,7 +13,7 @@ from rest_framework.compat import yaml, etree, StringIO
from rest_framework.response import Response from rest_framework.response import Response
from rest_framework.views import APIView from rest_framework.views import APIView
from rest_framework.renderers import BaseRenderer, JSONRenderer, YAMLRenderer, \ from rest_framework.renderers import BaseRenderer, JSONRenderer, YAMLRenderer, \
XMLRenderer, JSONPRenderer, BrowsableAPIRenderer, UnicodeJSONRenderer, UnicodeYAMLRenderer XMLRenderer, JSONPRenderer, BrowsableAPIRenderer
from rest_framework.parsers import YAMLParser, XMLParser from rest_framework.parsers import YAMLParser, XMLParser
from rest_framework.settings import api_settings from rest_framework.settings import api_settings
from rest_framework.test import APIRequestFactory from rest_framework.test import APIRequestFactory
...@@ -32,7 +32,7 @@ RENDERER_B_SERIALIZER = lambda x: ('Renderer B: %s' % x).encode('ascii') ...@@ -32,7 +32,7 @@ RENDERER_B_SERIALIZER = lambda x: ('Renderer B: %s' % x).encode('ascii')
expected_results = [ expected_results = [
((elem for elem in [1, 2, 3]), JSONRenderer, b'[1, 2, 3]') # Generator ((elem for elem in [1, 2, 3]), JSONRenderer, b'[1,2,3]') # Generator
] ]
...@@ -270,7 +270,7 @@ class RendererEndToEndTests(TestCase): ...@@ -270,7 +270,7 @@ class RendererEndToEndTests(TestCase):
self.assertNotContains(resp, '>text/html; charset=utf-8<') self.assertNotContains(resp, '>text/html; charset=utf-8<')
_flat_repr = '{"foo": ["bar", "baz"]}' _flat_repr = '{"foo":["bar","baz"]}'
_indented_repr = '{\n "foo": [\n "bar",\n "baz"\n ]\n}' _indented_repr = '{\n "foo": [\n "bar",\n "baz"\n ]\n}'
...@@ -373,22 +373,29 @@ class JSONRendererTests(TestCase): ...@@ -373,22 +373,29 @@ class JSONRendererTests(TestCase):
content = renderer.render(obj, 'application/json; indent=2') content = renderer.render(obj, 'application/json; indent=2')
self.assertEqual(strip_trailing_whitespace(content.decode('utf-8')), _indented_repr) self.assertEqual(strip_trailing_whitespace(content.decode('utf-8')), _indented_repr)
def test_check_ascii(self):
class UnicodeJSONRendererTests(TestCase):
"""
Tests specific for the Unicode JSON Renderer
"""
def test_proper_encoding(self):
obj = {'countries': ['United Kingdom', 'France', 'España']} obj = {'countries': ['United Kingdom', 'France', 'España']}
renderer = JSONRenderer() renderer = JSONRenderer()
content = renderer.render(obj, 'application/json') content = renderer.render(obj, 'application/json')
self.assertEqual(content, '{"countries": ["United Kingdom", "France", "Espa\\u00f1a"]}'.encode('utf-8')) self.assertEqual(content, '{"countries":["United Kingdom","France","España"]}'.encode('utf-8'))
class UnicodeJSONRendererTests(TestCase): class AsciiJSONRendererTests(TestCase):
""" """
Tests specific for the Unicode JSON Renderer Tests specific for the Unicode JSON Renderer
""" """
def test_proper_encoding(self): def test_proper_encoding(self):
class AsciiJSONRenderer(JSONRenderer):
ensure_ascii = True
obj = {'countries': ['United Kingdom', 'France', 'España']} obj = {'countries': ['United Kingdom', 'France', 'España']}
renderer = UnicodeJSONRenderer() renderer = AsciiJSONRenderer()
content = renderer.render(obj, 'application/json') content = renderer.render(obj, 'application/json')
self.assertEqual(content, '{"countries": ["United Kingdom", "France", "España"]}'.encode('utf-8')) self.assertEqual(content, '{"countries":["United Kingdom","France","Espa\\u00f1a"]}'.encode('utf-8'))
class JSONPRendererTests(TestCase): class JSONPRendererTests(TestCase):
...@@ -487,13 +494,9 @@ if yaml: ...@@ -487,13 +494,9 @@ if yaml:
def assertYAMLContains(self, content, string): def assertYAMLContains(self, content, string):
self.assertTrue(string in content, '%r not in %r' % (string, content)) self.assertTrue(string in content, '%r not in %r' % (string, content))
class UnicodeYAMLRendererTests(TestCase):
"""
Tests specific for the Unicode YAML Renderer
"""
def test_proper_encoding(self): def test_proper_encoding(self):
obj = {'countries': ['United Kingdom', 'France', 'España']} obj = {'countries': ['United Kingdom', 'France', 'España']}
renderer = UnicodeYAMLRenderer() renderer = YAMLRenderer()
content = renderer.render(obj, 'application/yaml') content = renderer.render(obj, 'application/yaml')
self.assertEqual(content.strip(), 'countries: [United Kingdom, France, España]'.encode('utf-8')) self.assertEqual(content.strip(), 'countries: [United Kingdom, France, España]'.encode('utf-8'))
......
...@@ -2,11 +2,12 @@ from __future__ import unicode_literals ...@@ -2,11 +2,12 @@ from __future__ import unicode_literals
from django.conf.urls import patterns, url, include from django.conf.urls import patterns, url, include
from django.test import TestCase from django.test import TestCase
from django.utils import six from django.utils import six
from tests.models import BasicModel, BasicModelSerializer from tests.models import BasicModel
from rest_framework.response import Response from rest_framework.response import Response
from rest_framework.views import APIView from rest_framework.views import APIView
from rest_framework import generics from rest_framework import generics
from rest_framework import routers from rest_framework import routers
from rest_framework import serializers
from rest_framework import status from rest_framework import status
from rest_framework.renderers import ( from rest_framework.renderers import (
BaseRenderer, BaseRenderer,
...@@ -17,6 +18,12 @@ from rest_framework import viewsets ...@@ -17,6 +18,12 @@ from rest_framework import viewsets
from rest_framework.settings import api_settings from rest_framework.settings import api_settings
# Serializer used to test BasicModel
class BasicModelSerializer(serializers.ModelSerializer):
class Meta:
model = BasicModel
class MockPickleRenderer(BaseRenderer): class MockPickleRenderer(BaseRenderer):
media_type = 'application/pickle' media_type = 'application/pickle'
...@@ -86,14 +93,15 @@ class HTMLView1(APIView): ...@@ -86,14 +93,15 @@ class HTMLView1(APIView):
class HTMLNewModelViewSet(viewsets.ModelViewSet): class HTMLNewModelViewSet(viewsets.ModelViewSet):
model = BasicModel serializer_class = BasicModelSerializer
queryset = BasicModel.objects.all()
class HTMLNewModelView(generics.ListCreateAPIView): class HTMLNewModelView(generics.ListCreateAPIView):
renderer_classes = (BrowsableAPIRenderer,) renderer_classes = (BrowsableAPIRenderer,)
permission_classes = [] permission_classes = []
serializer_class = BasicModelSerializer serializer_class = BasicModelSerializer
model = BasicModel queryset = BasicModel.objects.all()
new_model_viewset_router = routers.DefaultRouter() new_model_viewset_router = routers.DefaultRouter()
...@@ -224,8 +232,8 @@ class Issue467Tests(TestCase): ...@@ -224,8 +232,8 @@ class Issue467Tests(TestCase):
def test_form_has_label_and_help_text(self): def test_form_has_label_and_help_text(self):
resp = self.client.get('/html_new_model') resp = self.client.get('/html_new_model')
self.assertEqual(resp['Content-Type'], 'text/html; charset=utf-8') self.assertEqual(resp['Content-Type'], 'text/html; charset=utf-8')
self.assertContains(resp, 'Text comes here') # self.assertContains(resp, 'Text comes here')
self.assertContains(resp, 'Text description.') # self.assertContains(resp, 'Text description.')
class Issue807Tests(TestCase): class Issue807Tests(TestCase):
...@@ -269,11 +277,11 @@ class Issue807Tests(TestCase): ...@@ -269,11 +277,11 @@ class Issue807Tests(TestCase):
) )
resp = self.client.get('/html_new_model_viewset/' + param) resp = self.client.get('/html_new_model_viewset/' + param)
self.assertEqual(resp['Content-Type'], 'text/html; charset=utf-8') self.assertEqual(resp['Content-Type'], 'text/html; charset=utf-8')
self.assertContains(resp, 'Text comes here') # self.assertContains(resp, 'Text comes here')
self.assertContains(resp, 'Text description.') # self.assertContains(resp, 'Text description.')
def test_form_has_label_and_help_text(self): def test_form_has_label_and_help_text(self):
resp = self.client.get('/html_new_model') resp = self.client.get('/html_new_model')
self.assertEqual(resp['Content-Type'], 'text/html; charset=utf-8') self.assertEqual(resp['Content-Type'], 'text/html; charset=utf-8')
self.assertContains(resp, 'Text comes here') # self.assertContains(resp, 'Text comes here')
self.assertContains(resp, 'Text description.') # self.assertContains(resp, 'Text description.')
...@@ -3,7 +3,7 @@ from django.conf.urls import patterns, url, include ...@@ -3,7 +3,7 @@ from django.conf.urls import patterns, url, include
from django.db import models from django.db import models
from django.test import TestCase from django.test import TestCase
from django.core.exceptions import ImproperlyConfigured from django.core.exceptions import ImproperlyConfigured
from rest_framework import serializers, viewsets, permissions from rest_framework import serializers, viewsets, mixins, permissions
from rest_framework.decorators import detail_route, list_route from rest_framework.decorators import detail_route, list_route
from rest_framework.response import Response from rest_framework.response import Response
from rest_framework.routers import SimpleRouter, DefaultRouter from rest_framework.routers import SimpleRouter, DefaultRouter
...@@ -76,9 +76,10 @@ class TestCustomLookupFields(TestCase): ...@@ -76,9 +76,10 @@ class TestCustomLookupFields(TestCase):
def setUp(self): def setUp(self):
class NoteSerializer(serializers.HyperlinkedModelSerializer): class NoteSerializer(serializers.HyperlinkedModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='routertestmodel-detail', lookup_field='uuid')
class Meta: class Meta:
model = RouterTestModel model = RouterTestModel
lookup_field = 'uuid'
fields = ('url', 'uuid', 'text') fields = ('url', 'uuid', 'text')
class NoteViewSet(viewsets.ModelViewSet): class NoteViewSet(viewsets.ModelViewSet):
...@@ -86,8 +87,6 @@ class TestCustomLookupFields(TestCase): ...@@ -86,8 +87,6 @@ class TestCustomLookupFields(TestCase):
serializer_class = NoteSerializer serializer_class = NoteSerializer
lookup_field = 'uuid' lookup_field = 'uuid'
RouterTestModel.objects.create(uuid='123', text='foo bar')
self.router = SimpleRouter() self.router = SimpleRouter()
self.router.register(r'notes', NoteViewSet) self.router.register(r'notes', NoteViewSet)
...@@ -98,6 +97,8 @@ class TestCustomLookupFields(TestCase): ...@@ -98,6 +97,8 @@ class TestCustomLookupFields(TestCase):
url(r'^', include(self.router.urls)), url(r'^', include(self.router.urls)),
) )
RouterTestModel.objects.create(uuid='123', text='foo bar')
def test_custom_lookup_field_route(self): def test_custom_lookup_field_route(self):
detail_route = self.router.urls[-1] detail_route = self.router.urls[-1]
detail_url_pattern = detail_route.regex.pattern detail_url_pattern = detail_route.regex.pattern
...@@ -284,3 +285,19 @@ class TestDynamicListAndDetailRouter(TestCase): ...@@ -284,3 +285,19 @@ class TestDynamicListAndDetailRouter(TestCase):
else: else:
method_map = 'get' method_map = 'get'
self.assertEqual(route.mapping[method_map], endpoint) self.assertEqual(route.mapping[method_map], endpoint)
class TestRootWithAListlessViewset(TestCase):
def setUp(self):
class NoteViewSet(mixins.RetrieveModelMixin,
viewsets.GenericViewSet):
model = RouterTestModel
self.router = DefaultRouter()
self.router.register(r'notes', NoteViewSet)
self.view = self.router.urls[0].callback
def test_api_root(self):
request = factory.get('/')
response = self.view(request)
self.assertEqual(response.data, {})
This source diff could not be displayed because it is too large. You can view the blob instead.
from django.test import TestCase # from django.test import TestCase
from rest_framework import serializers # from rest_framework import serializers
class EmptySerializerTestCase(TestCase): # class EmptySerializerTestCase(TestCase):
def test_empty_serializer(self): # def test_empty_serializer(self):
class FooBarSerializer(serializers.Serializer): # class FooBarSerializer(serializers.Serializer):
foo = serializers.IntegerField() # foo = serializers.IntegerField()
bar = serializers.SerializerMethodField('get_bar') # bar = serializers.SerializerMethodField()
def get_bar(self, obj): # def get_bar(self, obj):
return 'bar' # return 'bar'
serializer = FooBarSerializer() # serializer = FooBarSerializer()
self.assertEquals(serializer.data, {'foo': 0}) # self.assertEquals(serializer.data, {'foo': 0})
from django.test import TestCase # from django.test import TestCase
from rest_framework import serializers # from rest_framework import serializers
from tests.accounts.serializers import AccountSerializer # from tests.accounts.serializers import AccountSerializer
class ImportingModelSerializerTests(TestCase): # class ImportingModelSerializerTests(TestCase):
""" # """
In some situations like, GH #1225, it is possible, especially in # In some situations like, GH #1225, it is possible, especially in
testing, to import a serializer who's related models have not yet # testing, to import a serializer who's related models have not yet
been resolved by Django. `AccountSerializer` is an example of such # been resolved by Django. `AccountSerializer` is an example of such
a serializer (imported at the top of this file). # a serializer (imported at the top of this file).
""" # """
def test_import_model_serializer(self): # def test_import_model_serializer(self):
""" # """
The serializer at the top of this file should have been # The serializer at the top of this file should have been
imported successfully, and we should be able to instantiate it. # imported successfully, and we should be able to instantiate it.
""" # """
self.assertIsInstance(AccountSerializer(), serializers.ModelSerializer) # self.assertIsInstance(AccountSerializer(), serializers.ModelSerializer)
...@@ -109,7 +109,7 @@ class ThrottlingTests(TestCase): ...@@ -109,7 +109,7 @@ class ThrottlingTests(TestCase):
def ensure_response_header_contains_proper_throttle_field(self, view, expected_headers): def ensure_response_header_contains_proper_throttle_field(self, view, expected_headers):
""" """
Ensure the response returns an X-Throttle field with status and next attributes Ensure the response returns an Retry-After field with status and next attributes
set properly. set properly.
""" """
request = self.factory.get('/') request = self.factory.get('/')
...@@ -117,10 +117,8 @@ class ThrottlingTests(TestCase): ...@@ -117,10 +117,8 @@ class ThrottlingTests(TestCase):
self.set_throttle_timer(view, timer) self.set_throttle_timer(view, timer)
response = view.as_view()(request) response = view.as_view()(request)
if expect is not None: if expect is not None:
self.assertEqual(response['X-Throttle-Wait-Seconds'], expect)
self.assertEqual(response['Retry-After'], expect) self.assertEqual(response['Retry-After'], expect)
else: else:
self.assertFalse('X-Throttle-Wait-Seconds' in response)
self.assertFalse('Retry-After' in response) self.assertFalse('Retry-After' in response)
def test_seconds_fields(self): def test_seconds_fields(self):
...@@ -173,13 +171,11 @@ class ThrottlingTests(TestCase): ...@@ -173,13 +171,11 @@ class ThrottlingTests(TestCase):
self.assertFalse(hasattr(MockView_NonTimeThrottling.throttle_classes[0], 'called')) self.assertFalse(hasattr(MockView_NonTimeThrottling.throttle_classes[0], 'called'))
response = MockView_NonTimeThrottling.as_view()(request) response = MockView_NonTimeThrottling.as_view()(request)
self.assertFalse('X-Throttle-Wait-Seconds' in response)
self.assertFalse('Retry-After' in response) self.assertFalse('Retry-After' in response)
self.assertTrue(MockView_NonTimeThrottling.throttle_classes[0].called) self.assertTrue(MockView_NonTimeThrottling.throttle_classes[0].called)
response = MockView_NonTimeThrottling.as_view()(request) response = MockView_NonTimeThrottling.as_view()(request)
self.assertFalse('X-Throttle-Wait-Seconds' in response)
self.assertFalse('Retry-After' in response) self.assertFalse('Retry-After' in response)
......
from __future__ import unicode_literals from __future__ import unicode_literals
from django.core.validators import MaxValueValidator from django.core.validators import MaxValueValidator
from django.core.exceptions import ValidationError
from django.db import models from django.db import models
from django.test import TestCase from django.test import TestCase
from rest_framework import generics, serializers, status from rest_framework import generics, serializers, status
...@@ -22,23 +23,10 @@ class ValidationModelSerializer(serializers.ModelSerializer): ...@@ -22,23 +23,10 @@ class ValidationModelSerializer(serializers.ModelSerializer):
class UpdateValidationModel(generics.RetrieveUpdateDestroyAPIView): class UpdateValidationModel(generics.RetrieveUpdateDestroyAPIView):
model = ValidationModel queryset = ValidationModel.objects.all()
serializer_class = ValidationModelSerializer serializer_class = ValidationModelSerializer
class TestPreSaveValidationExclusions(TestCase):
def test_pre_save_validation_exclusions(self):
"""
Somewhat weird test case to ensure that we don't perform model
validation on read only fields.
"""
obj = ValidationModel.objects.create(blank_validated_field='')
request = factory.put('/', {}, format='json')
view = UpdateValidationModel().as_view()
response = view(request, pk=obj.pk).render()
self.assertEqual(response.status_code, status.HTTP_200_OK)
# Regression for #653 # Regression for #653
class ShouldValidateModel(models.Model): class ShouldValidateModel(models.Model):
...@@ -48,11 +36,10 @@ class ShouldValidateModel(models.Model): ...@@ -48,11 +36,10 @@ class ShouldValidateModel(models.Model):
class ShouldValidateModelSerializer(serializers.ModelSerializer): class ShouldValidateModelSerializer(serializers.ModelSerializer):
renamed = serializers.CharField(source='should_validate_field', required=False) renamed = serializers.CharField(source='should_validate_field', required=False)
def validate_renamed(self, attrs, source): def validate_renamed(self, value):
value = attrs[source]
if len(value) < 3: if len(value) < 3:
raise serializers.ValidationError('Minimum 3 characters.') raise serializers.ValidationError('Minimum 3 characters.')
return attrs return value
class Meta: class Meta:
model = ShouldValidateModel model = ShouldValidateModel
...@@ -117,7 +104,7 @@ class ValidationMaxValueValidatorModelSerializer(serializers.ModelSerializer): ...@@ -117,7 +104,7 @@ class ValidationMaxValueValidatorModelSerializer(serializers.ModelSerializer):
class UpdateMaxValueValidationModel(generics.RetrieveUpdateDestroyAPIView): class UpdateMaxValueValidationModel(generics.RetrieveUpdateDestroyAPIView):
model = ValidationMaxValueValidatorModel queryset = ValidationMaxValueValidatorModel.objects.all()
serializer_class = ValidationMaxValueValidatorModelSerializer serializer_class = ValidationMaxValueValidatorModelSerializer
...@@ -144,5 +131,44 @@ class TestMaxValueValidatorValidation(TestCase): ...@@ -144,5 +131,44 @@ class TestMaxValueValidatorValidation(TestCase):
request = factory.patch('/{0}'.format(obj.pk), {'number_value': 101}, format='json') request = factory.patch('/{0}'.format(obj.pk), {'number_value': 101}, format='json')
view = UpdateMaxValueValidationModel().as_view() view = UpdateMaxValueValidationModel().as_view()
response = view(request, pk=obj.pk).render() response = view(request, pk=obj.pk).render()
self.assertEqual(response.content, b'{"number_value": ["Ensure this value is less than or equal to 100."]}') self.assertEqual(response.content, b'{"number_value":["Ensure this value is less than or equal to 100."]}')
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
class TestChoiceFieldChoicesValidate(TestCase):
CHOICES = [
(0, 'Small'),
(1, 'Medium'),
(2, 'Large'),
]
CHOICES_NESTED = [
('Category', (
(1, 'First'),
(2, 'Second'),
(3, 'Third'),
)),
(4, 'Fourth'),
]
def test_choices(self):
"""
Make sure a value for choices works as expected.
"""
f = serializers.ChoiceField(choices=self.CHOICES)
value = self.CHOICES[0][0]
try:
f.to_internal_value(value)
except ValidationError:
self.fail("Value %s does not validate" % str(value))
# def test_nested_choices(self):
# """
# Make sure a nested value for choices works as expected.
# """
# f = serializers.ChoiceField(choices=self.CHOICES_NESTED)
# value = self.CHOICES_NESTED[0][1][0][0]
# try:
# f.to_native(value)
# except ValidationError:
# self.fail("Value %s does not validate" % str(value))
from django.db import models
from django.test import TestCase from django.test import TestCase
from rest_framework import serializers from rest_framework import serializers
class ExampleModel(models.Model):
email = models.EmailField(max_length=100)
password = models.CharField(max_length=100)
class WriteOnlyFieldTests(TestCase): class WriteOnlyFieldTests(TestCase):
def test_write_only_fields(self): def setUp(self):
class ExampleSerializer(serializers.Serializer): class ExampleSerializer(serializers.Serializer):
email = serializers.EmailField() email = serializers.EmailField()
password = serializers.CharField(write_only=True) password = serializers.CharField(write_only=True)
def create(self, attrs):
return attrs
self.Serializer = ExampleSerializer
def write_only_fields_are_present_on_input(self):
data = { data = {
'email': 'foo@example.com', 'email': 'foo@example.com',
'password': '123' 'password': '123'
} }
serializer = ExampleSerializer(data=data) serializer = self.Serializer(data=data)
self.assertTrue(serializer.is_valid()) self.assertTrue(serializer.is_valid())
self.assertEquals(serializer.object, data) self.assertEquals(serializer.validated_data, data)
self.assertEquals(serializer.data, {'email': 'foo@example.com'})
def test_write_only_fields_meta(self):
class ExampleSerializer(serializers.ModelSerializer):
class Meta:
model = ExampleModel
fields = ('email', 'password')
write_only_fields = ('password',)
data = { def write_only_fields_are_not_present_on_output(self):
instance = {
'email': 'foo@example.com', 'email': 'foo@example.com',
'password': '123' 'password': '123'
} }
serializer = ExampleSerializer(data=data) serializer = self.Serializer(instance)
self.assertTrue(serializer.is_valid())
self.assertTrue(isinstance(serializer.object, ExampleModel))
self.assertEquals(serializer.object.email, data['email'])
self.assertEquals(serializer.object.password, data['password'])
self.assertEquals(serializer.data, {'email': 'foo@example.com'}) self.assertEquals(serializer.data, {'email': 'foo@example.com'})
from contextlib import contextmanager from contextlib import contextmanager
from django.core.exceptions import ObjectDoesNotExist
from django.core.urlresolvers import NoReverseMatch
from django.utils import six from django.utils import six
from rest_framework.settings import api_settings from rest_framework.settings import api_settings
...@@ -23,3 +25,54 @@ def temporary_setting(setting, value, module=None): ...@@ -23,3 +25,54 @@ def temporary_setting(setting, value, module=None):
if module is not None: if module is not None:
six.moves.reload_module(module) six.moves.reload_module(module)
class MockObject(object):
def __init__(self, **kwargs):
self._kwargs = kwargs
for key, val in kwargs.items():
setattr(self, key, val)
def __str__(self):
kwargs_str = ', '.join([
'%s=%s' % (key, value)
for key, value in sorted(self._kwargs.items())
])
return '<MockObject %s>' % kwargs_str
class MockQueryset(object):
def __init__(self, iterable):
self.items = iterable
def get(self, **lookup):
for item in self.items:
if all([
getattr(item, key, None) == value
for key, value in lookup.items()
]):
return item
raise ObjectDoesNotExist()
class BadType(object):
"""
When used as a lookup with a `MockQueryset`, these objects
will raise a `TypeError`, as occurs in Django when making
queryset lookups with an incorrect type for the lookup value.
"""
def __eq__(self):
raise TypeError()
def mock_reverse(view_name, args=None, kwargs=None, request=None, format=None):
args = args or []
kwargs = kwargs or {}
value = (args + list(kwargs.values()) + ['-'])[0]
prefix = 'http://example.org' if request else ''
suffix = ('.' + format) if (format is not None) else ''
return '%s/%s/%s%s/' % (prefix, view_name, value, suffix)
def fail_reverse(view_name, args=None, kwargs=None, request=None, format=None):
raise NoReverseMatch()
from rest_framework import generics
from .models import NullableForeignKeySource
from .serializers import NullableFKSourceSerializer
class NullableFKSourceDetail(generics.RetrieveUpdateDestroyAPIView):
model = NullableForeignKeySource
model_serializer_class = NullableFKSourceSerializer
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment