Merge branch 'master' into docs
51
.travis.yml
|
@ -1,44 +1,51 @@
|
||||||
language: python
|
language: python
|
||||||
|
|
||||||
python:
|
python:
|
||||||
|
- "2.7"
|
||||||
|
- "3.4"
|
||||||
- "3.5"
|
- "3.5"
|
||||||
|
|
||||||
sudo: false
|
sudo: false
|
||||||
|
|
||||||
env:
|
env:
|
||||||
- TOX_ENV=py27-lint
|
- DJANGO=1.8
|
||||||
- TOX_ENV=py27-docs
|
- DJANGO=1.9
|
||||||
- TOX_ENV=py35-django19
|
- DJANGO=1.10
|
||||||
- TOX_ENV=py34-django19
|
- DJANGO=1.11
|
||||||
- TOX_ENV=py27-django19
|
- DJANGO=master
|
||||||
- TOX_ENV=py35-django18
|
|
||||||
- TOX_ENV=py34-django18
|
|
||||||
- TOX_ENV=py33-django18
|
|
||||||
- TOX_ENV=py27-django18
|
|
||||||
- TOX_ENV=py27-django110
|
|
||||||
- TOX_ENV=py35-django110
|
|
||||||
- TOX_ENV=py34-django110
|
|
||||||
- TOX_ENV=py27-djangomaster
|
|
||||||
- TOX_ENV=py34-djangomaster
|
|
||||||
- TOX_ENV=py35-djangomaster
|
|
||||||
|
|
||||||
matrix:
|
matrix:
|
||||||
fast_finish: true
|
fast_finish: true
|
||||||
|
include:
|
||||||
|
- python: "3.6"
|
||||||
|
env: DJANGO=master
|
||||||
|
- python: "3.6"
|
||||||
|
env: DJANGO=1.11
|
||||||
|
- python: "3.3"
|
||||||
|
env: DJANGO=1.8
|
||||||
|
- python: "2.7"
|
||||||
|
env: TOXENV="lint"
|
||||||
|
- python: "2.7"
|
||||||
|
env: TOXENV="docs"
|
||||||
|
exclude:
|
||||||
|
- python: "2.7"
|
||||||
|
env: DJANGO=master
|
||||||
|
- python: "3.4"
|
||||||
|
env: DJANGO=master
|
||||||
|
|
||||||
allow_failures:
|
allow_failures:
|
||||||
- env: TOX_ENV=py27-djangomaster
|
- env: DJANGO=master
|
||||||
- env: TOX_ENV=py34-djangomaster
|
- env: DJANGO=1.11
|
||||||
- env: TOX_ENV=py35-djangomaster
|
|
||||||
|
|
||||||
install:
|
install:
|
||||||
# Virtualenv < 14 is required to keep the Python 3.2 builds running.
|
- pip install tox tox-travis
|
||||||
- pip install tox "virtualenv<14"
|
|
||||||
|
|
||||||
script:
|
script:
|
||||||
- tox -e $TOX_ENV
|
- tox
|
||||||
|
|
||||||
after_success:
|
after_success:
|
||||||
- pip install codecov
|
- pip install codecov
|
||||||
- codecov -e TOX_ENV
|
- codecov -e TOXENV,DJANGO
|
||||||
|
|
||||||
notifications:
|
notifications:
|
||||||
email: false
|
email: false
|
||||||
|
|
|
@ -24,10 +24,11 @@ The initial aim is to provide a single full-time position on REST framework.
|
||||||
<a href="http://jobs.rover.com/"><img src="https://raw.githubusercontent.com/tomchristie/django-rest-framework/master/docs/img/premium/rover-readme.png"/></a>
|
<a href="http://jobs.rover.com/"><img src="https://raw.githubusercontent.com/tomchristie/django-rest-framework/master/docs/img/premium/rover-readme.png"/></a>
|
||||||
<a href="https://getsentry.com/welcome/"><img src="https://raw.githubusercontent.com/tomchristie/django-rest-framework/master/docs/img/premium/sentry-readme.png"/></a>
|
<a href="https://getsentry.com/welcome/"><img src="https://raw.githubusercontent.com/tomchristie/django-rest-framework/master/docs/img/premium/sentry-readme.png"/></a>
|
||||||
<a href="https://getstream.io/try-the-api/?utm_source=drf&utm_medium=banner&utm_campaign=drf"><img src="https://raw.githubusercontent.com/tomchristie/django-rest-framework/master/docs/img/premium/stream-readme.png"/></a>
|
<a href="https://getstream.io/try-the-api/?utm_source=drf&utm_medium=banner&utm_campaign=drf"><img src="https://raw.githubusercontent.com/tomchristie/django-rest-framework/master/docs/img/premium/stream-readme.png"/></a>
|
||||||
<a href="http://www.machinalis.com/#services"><img src="https://raw.githubusercontent.com/tomchristie/django-rest-framework/master/docs/img/premium/machinalis-readme.png"/></a>
|
<a href="https://hello.machinalis.co.uk/"><img src="https://raw.githubusercontent.com/tomchristie/django-rest-framework/master/docs/img/premium/machinalis-readme.png"/></a>
|
||||||
|
<a href="https://rollbar.com/"><img src="https://raw.githubusercontent.com/tomchristie/django-rest-framework/master/docs/img/premium/rollbar-readme.png"/></a>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
*Many thanks to all our [wonderful sponsors][sponsors], and in particular to our premium backers, [Rover](http://jobs.rover.com/), [Sentry](https://getsentry.com/welcome/), [Stream](https://getstream.io/?utm_source=drf&utm_medium=banner&utm_campaign=drf), and [Machinalis](http://www.machinalis.com/#services).*
|
*Many thanks to all our [wonderful sponsors][sponsors], and in particular to our premium backers, [Rover](http://jobs.rover.com/), [Sentry](https://getsentry.com/welcome/), [Stream](https://getstream.io/?utm_source=drf&utm_medium=banner&utm_campaign=drf), [Machinalis](https://hello.machinalis.co.uk/), and [Rollbar](https://rollbar.com).*
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
|
@ -434,9 +434,11 @@ Requires either the `Pillow` package or `PIL` package. The `Pillow` package is
|
||||||
|
|
||||||
A field class that validates a list of objects.
|
A field class that validates a list of objects.
|
||||||
|
|
||||||
**Signature**: `ListField(child)`
|
**Signature**: `ListField(child, min_length=None, max_length=None)`
|
||||||
|
|
||||||
- `child` - A field instance that should be used for validating the objects in the list. If this argument is not provided then objects in the list will not be validated.
|
- `child` - A field instance that should be used for validating the objects in the list. If this argument is not provided then objects in the list will not be validated.
|
||||||
|
- `min_length` - Validates that the list contains no fewer than this number of elements.
|
||||||
|
- `max_length` - Validates that the list contains no more than this number of elements.
|
||||||
|
|
||||||
For example, to validate a list of integers you might use something like the following:
|
For example, to validate a list of integers you might use something like the following:
|
||||||
|
|
||||||
|
|
|
@ -113,7 +113,7 @@ The following third party packages provide additional metadata implementations.
|
||||||
[drf-schema-adapter][drf-schema-adapter] is a set of tools that makes it easier to provide schema information to frontend frameworks and libraries. It provides a metadata mixin as well as 2 metadata classes and several adapters suitable to generate [json-schema][json-schema] as well as schema information readable by various libraries.
|
[drf-schema-adapter][drf-schema-adapter] is a set of tools that makes it easier to provide schema information to frontend frameworks and libraries. It provides a metadata mixin as well as 2 metadata classes and several adapters suitable to generate [json-schema][json-schema] as well as schema information readable by various libraries.
|
||||||
|
|
||||||
You can also write your own adapter to work with your specific frontend.
|
You can also write your own adapter to work with your specific frontend.
|
||||||
If you whish to do so, it also provides an exporter that can export those schema information to json files.
|
If you wish to do so, it also provides an exporter that can export those schema information to json files.
|
||||||
|
|
||||||
[cite]: http://tools.ietf.org/html/rfc7231#section-4.3.7
|
[cite]: http://tools.ietf.org/html/rfc7231#section-4.3.7
|
||||||
[no-options]: https://www.mnot.net/blog/2012/10/29/NO_OPTIONS
|
[no-options]: https://www.mnot.net/blog/2012/10/29/NO_OPTIONS
|
||||||
|
|
|
@ -259,12 +259,16 @@ The [REST Condition][rest-condition] package is another extension for building c
|
||||||
|
|
||||||
## DRY Rest Permissions
|
## DRY Rest Permissions
|
||||||
|
|
||||||
The [DRY Rest Permissions][dry-rest-permissions] package provides the ability to define different permissions for individual default and custom actions. This package is made for apps with permissions that are derived from relationships defined in the app's data model. It also supports permission checks being returned to a client app through the API's serializer. Additionally it supports adding permissions to the default and custom list actions to restrict the data they retrive per user.
|
The [DRY Rest Permissions][dry-rest-permissions] package provides the ability to define different permissions for individual default and custom actions. This package is made for apps with permissions that are derived from relationships defined in the app's data model. It also supports permission checks being returned to a client app through the API's serializer. Additionally it supports adding permissions to the default and custom list actions to restrict the data they retrieve per user.
|
||||||
|
|
||||||
## Django Rest Framework Roles
|
## Django Rest Framework Roles
|
||||||
|
|
||||||
The [Django Rest Framework Roles][django-rest-framework-roles] package makes it easier to parameterize your API over multiple types of users.
|
The [Django Rest Framework Roles][django-rest-framework-roles] package makes it easier to parameterize your API over multiple types of users.
|
||||||
|
|
||||||
|
## Django Rest Framework API Key
|
||||||
|
|
||||||
|
The [Django Rest Framework API Key][django-rest-framework-api-key] package allows you to ensure that every request made to the server requires an API key header. You can generate one from the django admin interface.
|
||||||
|
|
||||||
[cite]: https://developer.apple.com/library/mac/#documentation/security/Conceptual/AuthenticationAndAuthorizationGuide/Authorization/Authorization.html
|
[cite]: https://developer.apple.com/library/mac/#documentation/security/Conceptual/AuthenticationAndAuthorizationGuide/Authorization/Authorization.html
|
||||||
[authentication]: authentication.md
|
[authentication]: authentication.md
|
||||||
[throttling]: throttling.md
|
[throttling]: throttling.md
|
||||||
|
@ -280,3 +284,4 @@ The [Django Rest Framework Roles][django-rest-framework-roles] package makes it
|
||||||
[rest-condition]: https://github.com/caxap/rest_condition
|
[rest-condition]: https://github.com/caxap/rest_condition
|
||||||
[dry-rest-permissions]: https://github.com/Helioscene/dry-rest-permissions
|
[dry-rest-permissions]: https://github.com/Helioscene/dry-rest-permissions
|
||||||
[django-rest-framework-roles]: https://github.com/computer-lab/django-rest-framework-roles
|
[django-rest-framework-roles]: https://github.com/computer-lab/django-rest-framework-roles
|
||||||
|
[django-rest-framework-api-key]: https://github.com/manosim/django-rest-framework-api-key
|
||||||
|
|
|
@ -118,6 +118,26 @@ The above example would now generate the following URL pattern:
|
||||||
|
|
||||||
* URL pattern: `^users/{pk}/change-password/$` Name: `'user-change-password'`
|
* URL pattern: `^users/{pk}/change-password/$` Name: `'user-change-password'`
|
||||||
|
|
||||||
|
In the case you do not want to use the default name generated for your custom action, you can use the url_name parameter to customize it.
|
||||||
|
|
||||||
|
For example, if you want to change the name of our custom action to `'user-change-password'`, you could write:
|
||||||
|
|
||||||
|
from myapp.permissions import IsAdminOrIsSelf
|
||||||
|
from rest_framework.decorators import detail_route
|
||||||
|
|
||||||
|
class UserViewSet(ModelViewSet):
|
||||||
|
...
|
||||||
|
|
||||||
|
@detail_route(methods=['post'], permission_classes=[IsAdminOrIsSelf], url_name='change-password')
|
||||||
|
def set_password(self, request, pk=None):
|
||||||
|
...
|
||||||
|
|
||||||
|
The above example would now generate the following URL pattern:
|
||||||
|
|
||||||
|
* URL pattern: `^users/{pk}/set_password/$` Name: `'user-change-password'`
|
||||||
|
|
||||||
|
You can also use url_path and url_name parameters together to obtain extra control on URL generation for custom views.
|
||||||
|
|
||||||
For more information see the viewset documentation on [marking extra actions for routing][route-decorators].
|
For more information see the viewset documentation on [marking extra actions for routing][route-decorators].
|
||||||
|
|
||||||
# API Guide
|
# API Guide
|
||||||
|
|
|
@ -145,6 +145,18 @@ May be used to pass a canonical URL for the schema.
|
||||||
url='https://www.example.org/api/'
|
url='https://www.example.org/api/'
|
||||||
)
|
)
|
||||||
|
|
||||||
|
#### `urlconf`
|
||||||
|
|
||||||
|
A string representing the import path to the URL conf that you want
|
||||||
|
to generate an API schema for. This defaults to the value of Django's
|
||||||
|
ROOT_URLCONF setting.
|
||||||
|
|
||||||
|
schema_view = get_schema_view(
|
||||||
|
title='Server Monitoring API',
|
||||||
|
url='https://www.example.org/api/',
|
||||||
|
urlconf='myproject.urls'
|
||||||
|
)
|
||||||
|
|
||||||
#### `renderer_classes`
|
#### `renderer_classes`
|
||||||
|
|
||||||
May be used to pass the set of renderer classes that can be used to render the API root endpoint.
|
May be used to pass the set of renderer classes that can be used to render the API root endpoint.
|
||||||
|
|
|
@ -1157,7 +1157,7 @@ The [html-json-forms][html-json-forms] package provides an algorithm and seriali
|
||||||
|
|
||||||
## QueryFields
|
## QueryFields
|
||||||
|
|
||||||
[djangorestframework-queryfields][djangorestframework-queryfields] allows API clients to specify which fields will be sent in the response via inclusion or exclusion query paramaters.
|
[djangorestframework-queryfields][djangorestframework-queryfields] allows API clients to specify which fields will be sent in the response via inclusion/exclusion query parameters.
|
||||||
|
|
||||||
|
|
||||||
[cite]: https://groups.google.com/d/topic/django-users/sVFaOfQi4wY/discussion
|
[cite]: https://groups.google.com/d/topic/django-users/sVFaOfQi4wY/discussion
|
||||||
|
|
|
@ -241,7 +241,7 @@ Default:
|
||||||
If set, this maps the `'pk'` identifier in the URL conf onto the actual field
|
If set, this maps the `'pk'` identifier in the URL conf onto the actual field
|
||||||
name when generating a schema path parameter. Typically this will be `'id'`.
|
name when generating a schema path parameter. Typically this will be `'id'`.
|
||||||
This gives a more suitable representation as "primary key" is an implementation
|
This gives a more suitable representation as "primary key" is an implementation
|
||||||
detail, wheras "identifier" is a more general concept.
|
detail, whereas "identifier" is a more general concept.
|
||||||
|
|
||||||
Default: `True`
|
Default: `True`
|
||||||
|
|
||||||
|
|
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 13 KiB |
BIN
docs/img/premium/rollbar-readme.png
Normal file
After Width: | Height: | Size: 12 KiB |
Before Width: | Height: | Size: 54 KiB After Width: | Height: | Size: 52 KiB |
Before Width: | Height: | Size: 26 KiB After Width: | Height: | Size: 24 KiB |
Before Width: | Height: | Size: 20 KiB After Width: | Height: | Size: 19 KiB |
|
@ -74,11 +74,12 @@ The initial aim is to provide a single full-time position on REST framework.
|
||||||
<li><a href="http://jobs.rover.com/" style="background-image: url(https://fund-rest-framework.s3.amazonaws.com/rover_130x130.png)">Rover.com</a></li>
|
<li><a href="http://jobs.rover.com/" style="background-image: url(https://fund-rest-framework.s3.amazonaws.com/rover_130x130.png)">Rover.com</a></li>
|
||||||
<li><a href="https://getsentry.com/welcome/" style="background-image: url(https://fund-rest-framework.s3.amazonaws.com/sentry130.png)">Sentry</a></li>
|
<li><a href="https://getsentry.com/welcome/" style="background-image: url(https://fund-rest-framework.s3.amazonaws.com/sentry130.png)">Sentry</a></li>
|
||||||
<li><a href="https://getstream.io/try-the-api/?utm_source=drf&utm_medium=banner&utm_campaign=drf" style="background-image: url(https://fund-rest-framework.s3.amazonaws.com/stream-130.png)">Stream</a></li>
|
<li><a href="https://getstream.io/try-the-api/?utm_source=drf&utm_medium=banner&utm_campaign=drf" style="background-image: url(https://fund-rest-framework.s3.amazonaws.com/stream-130.png)">Stream</a></li>
|
||||||
<li><a href="http://www.machinalis.com/#services" style="background-image: url(https://fund-rest-framework.s3.amazonaws.com/Machinalis130.png)">Machinalis</a></li>
|
<li><a href="https://hello.machinalis.co.uk/" style="background-image: url(https://fund-rest-framework.s3.amazonaws.com/Machinalis130.png)">Machinalis</a></li>
|
||||||
|
<li><a href="https://rollbar.com" style="background-image: url(https://fund-rest-framework.s3.amazonaws.com/rollbar.png)">Rollbar</a></li>
|
||||||
</ul>
|
</ul>
|
||||||
<div style="clear: both; padding-bottom: 20px;"></div>
|
<div style="clear: both; padding-bottom: 20px;"></div>
|
||||||
|
|
||||||
*Many thanks to all our [wonderful sponsors][sponsors], and in particular to our premium backers, [Rover](http://jobs.rover.com/), [Sentry](https://getsentry.com/welcome/), [Stream](https://getstream.io/?utm_source=drf&utm_medium=banner&utm_campaign=drf), and [Machinalis](http://www.machinalis.com/#services).*
|
*Many thanks to all our [wonderful sponsors][sponsors], and in particular to our premium backers, [Rover](http://jobs.rover.com/), [Sentry](https://getsentry.com/welcome/), [Stream](https://getstream.io/?utm_source=drf&utm_medium=banner&utm_campaign=drf), [Machinalis](https://hello.machinalis.co.uk/), and [Rollbar](https://rollbar.com).*
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
@ -260,7 +261,7 @@ Framework.
|
||||||
|
|
||||||
For support please see the [REST framework discussion group][group], try the `#restframework` channel on `irc.freenode.net`, search [the IRC archives][botbot], or raise a question on [Stack Overflow][stack-overflow], making sure to include the ['django-rest-framework'][django-rest-framework-tag] tag.
|
For support please see the [REST framework discussion group][group], try the `#restframework` channel on `irc.freenode.net`, search [the IRC archives][botbot], or raise a question on [Stack Overflow][stack-overflow], making sure to include the ['django-rest-framework'][django-rest-framework-tag] tag.
|
||||||
|
|
||||||
[Paid support is available][paid-support] from [DabApps][dabapps], and can include work on REST framework core, or support with building your REST framework API. Please [contact DabApps][contact-dabapps] if you'd like to discuss commercial support options.
|
For priority support please sign up for a [professional or premium sponsorship plan](https://fund.django-rest-framework.org/topics/funding/).
|
||||||
|
|
||||||
For updates on REST framework development, you may also want to follow [the author][twitter] on Twitter.
|
For updates on REST framework development, you may also want to follow [the author][twitter] on Twitter.
|
||||||
|
|
||||||
|
|
|
@ -153,7 +153,7 @@ For more information, see the documentation on [customizing field mappings][cust
|
||||||
|
|
||||||
We've now moved a number of packages out of the core of REST framework, and into separately installable packages. If you're currently using these you don't need to worry, you simply need to `pip install` the new packages, and change any import paths.
|
We've now moved a number of packages out of the core of REST framework, and into separately installable packages. If you're currently using these you don't need to worry, you simply need to `pip install` the new packages, and change any import paths.
|
||||||
|
|
||||||
We're making this change in order to help distribute the maintainance workload, and keep better focus of the core essentials of the framework.
|
We're making this change in order to help distribute the maintenance workload, and keep better focus of the core essentials of the framework.
|
||||||
|
|
||||||
The change also means we can be more flexible with which external packages we recommend. For example, the excellently maintained [Django OAuth toolkit](https://github.com/evonove/django-oauth-toolkit) has now been promoted as our recommended option for integrating OAuth support.
|
The change also means we can be more flexible with which external packages we recommend. For example, the excellently maintained [Django OAuth toolkit](https://github.com/evonove/django-oauth-toolkit) has now been promoted as our recommended option for integrating OAuth support.
|
||||||
|
|
||||||
|
|
|
@ -89,7 +89,7 @@ Name | Support | PyPI pa
|
||||||
---------------------------------|-------------------------------------|--------------------------------
|
---------------------------------|-------------------------------------|--------------------------------
|
||||||
[Core JSON][core-json] | Schema generation & client support. | Built-in support in `coreapi`.
|
[Core JSON][core-json] | Schema generation & client support. | Built-in support in `coreapi`.
|
||||||
[Swagger / OpenAPI][swagger] | Schema generation & client support. | The `openapi-codec` package.
|
[Swagger / OpenAPI][swagger] | Schema generation & client support. | The `openapi-codec` package.
|
||||||
[JSON Hyper-Schema][hyperschema] | Currrently client support only. | The `hyperschema-codec` package.
|
[JSON Hyper-Schema][hyperschema] | Currently client support only. | The `hyperschema-codec` package.
|
||||||
[API Blueprint][api-blueprint] | Not yet available. | Not yet available.
|
[API Blueprint][api-blueprint] | Not yet available. | Not yet available.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
|
@ -217,7 +217,7 @@ credentials, headers and bookmarks:
|
||||||
|
|
||||||
# Python client library
|
# Python client library
|
||||||
|
|
||||||
The `coreapi` Python package allows you to programatically interact with any
|
The `coreapi` Python package allows you to programmatically interact with any
|
||||||
API that exposes a supported schema format.
|
API that exposes a supported schema format.
|
||||||
|
|
||||||
## Getting started
|
## Getting started
|
||||||
|
|
|
@ -105,7 +105,7 @@ If the python `markdown` library is installed, then [markdown syntax][markdown]
|
||||||
[ref]: http://example.com/activating-accounts
|
[ref]: http://example.com/activating-accounts
|
||||||
"""
|
"""
|
||||||
|
|
||||||
Note that one constraint of using viewsets is that any documentation be used for all generated views, so for example, you cannot have differing documentation for the generated list view and detail view.
|
Note that when using viewsets the basic docstring is used for all generated views. To provide descriptions for each view, such as for the the list and retrieve views, use docstring sections as described in [Schemas as documentation: Examples][schemas-examples].
|
||||||
|
|
||||||
#### The `OPTIONS` method
|
#### The `OPTIONS` method
|
||||||
|
|
||||||
|
@ -148,3 +148,4 @@ To implement a hypermedia API you'll need to decide on an appropriate media type
|
||||||
[image-django-rest-swagger]: ../img/django-rest-swagger.png
|
[image-django-rest-swagger]: ../img/django-rest-swagger.png
|
||||||
[image-apiary]: ../img/apiary.png
|
[image-apiary]: ../img/apiary.png
|
||||||
[image-self-describing-api]: ../img/self-describing.png
|
[image-self-describing-api]: ../img/self-describing.png
|
||||||
|
[schemas-examples]: api-guide/schemas/#examples
|
||||||
|
|
|
@ -308,7 +308,7 @@ Our professional and premium plans also include **priority support**. At any tim
|
||||||
|
|
||||||
Once you've signed up I'll contact you via email and arrange your ad placements on the site.
|
Once you've signed up I'll contact you via email and arrange your ad placements on the site.
|
||||||
|
|
||||||
For further enquires please contact <a href=mailto:tom@tomchristie.com>tom@tomchristie.com</a>.
|
For further enquires please contact <a href=mailto:funding@django-rest-framework.org>funding@django-rest-framework.org</a>.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
|
@ -102,7 +102,7 @@ Our gold sponsors include companies large and small. Many thanks for their signi
|
||||||
|
|
||||||
### Silver sponsors
|
### Silver sponsors
|
||||||
|
|
||||||
The serious financial contribution that our silver sponsors have made is very much appreciated. I'd like to say a particular thank you to individuals who have choosen to privately support the project at this level.
|
The serious financial contribution that our silver sponsors have made is very much appreciated. I'd like to say a particular thank you to individuals who have chosen to privately support the project at this level.
|
||||||
|
|
||||||
<ul class="sponsor silver">
|
<ul class="sponsor silver">
|
||||||
<li><a href="http://www.imtapps.com/" rel="nofollow" style="background-image:url(../../img/sponsors/3-imt_computer_services.png);">IMT Computer Services</a></li>
|
<li><a href="http://www.imtapps.com/" rel="nofollow" style="background-image:url(../../img/sponsors/3-imt_computer_services.png);">IMT Computer Services</a></li>
|
||||||
|
|
|
@ -228,7 +228,7 @@ You can determine your currently installed version using `pip freeze`:
|
||||||
* Fixed use of deprecated Query.aggregates. ([#4003][gh4003])
|
* Fixed use of deprecated Query.aggregates. ([#4003][gh4003])
|
||||||
* Fix blank lines around docstrings. ([#4002][gh4002])
|
* Fix blank lines around docstrings. ([#4002][gh4002])
|
||||||
* Fixed admin pagination when limit is 0. ([#3990][gh3990])
|
* Fixed admin pagination when limit is 0. ([#3990][gh3990])
|
||||||
* OrderingFilter adjustements. ([#3983][gh3983])
|
* OrderingFilter adjustments. ([#3983][gh3983])
|
||||||
* Non-required serializer related fields. ([#3976][gh3976])
|
* Non-required serializer related fields. ([#3976][gh3976])
|
||||||
* Using safer calling way of "@api_view" in tutorial. ([#3971][gh3971])
|
* Using safer calling way of "@api_view" in tutorial. ([#3971][gh3971])
|
||||||
* ListSerializer doesn't handle unique_together constraints. ([#3970][gh3970])
|
* ListSerializer doesn't handle unique_together constraints. ([#3970][gh3970])
|
||||||
|
|
|
@ -62,7 +62,7 @@ So far, so good. It looks pretty similar to the previous case, but we've got be
|
||||||
|
|
||||||
That's looking good. Again, it's still pretty similar to the function based view right now.
|
That's looking good. Again, it's still pretty similar to the function based view right now.
|
||||||
|
|
||||||
We'll also need to refactor our `urls.py` slightly now we're using class-based views.
|
We'll also need to refactor our `urls.py` slightly now that we're using class-based views.
|
||||||
|
|
||||||
from django.conf.urls import url
|
from django.conf.urls import url
|
||||||
from rest_framework.urlpatterns import format_suffix_patterns
|
from rest_framework.urlpatterns import format_suffix_patterns
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
# PyTest for running the tests.
|
# PyTest for running the tests.
|
||||||
pytest==2.9.1
|
pytest==3.0.5
|
||||||
pytest-django==2.9.1
|
pytest-django==3.1.2
|
||||||
pytest-cov==1.8.1
|
pytest-cov==2.4.0
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
"""
|
r"""
|
||||||
______ _____ _____ _____ __
|
______ _____ _____ _____ __
|
||||||
| ___ \ ___/ ___|_ _| / _| | |
|
| ___ \ ___/ ___|_ _| / _| | |
|
||||||
| |_/ / |__ \ `--. | | | |_ _ __ __ _ _ __ ___ _____ _____ _ __| |__
|
| |_/ / |__ \ `--. | | | |_ _ __ __ _ _ __ ___ _____ _____ _ __| |__
|
||||||
|
|
|
@ -17,11 +17,6 @@ from django.template import Context, RequestContext, Template
|
||||||
from django.utils import six
|
from django.utils import six
|
||||||
from django.views.generic import View
|
from django.views.generic import View
|
||||||
|
|
||||||
try:
|
|
||||||
import importlib # Available in Python 3.1+
|
|
||||||
except ImportError:
|
|
||||||
from django.utils import importlib # Will be removed in Django 1.9
|
|
||||||
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
from django.urls import (
|
from django.urls import (
|
||||||
|
@ -319,3 +314,10 @@ def set_many(instance, field, value):
|
||||||
else:
|
else:
|
||||||
field = getattr(instance, field)
|
field = getattr(instance, field)
|
||||||
field.set(value)
|
field.set(value)
|
||||||
|
|
||||||
|
def include(module, namespace=None, app_name=None):
|
||||||
|
from django.conf.urls import include
|
||||||
|
if django.VERSION < (1,9):
|
||||||
|
return include(module, namespace, app_name)
|
||||||
|
else:
|
||||||
|
return include((module, app_name), namespace)
|
||||||
|
|
|
@ -28,6 +28,7 @@ from django.utils.encoding import is_protected_type, smart_text
|
||||||
from django.utils.formats import localize_input, sanitize_separators
|
from django.utils.formats import localize_input, sanitize_separators
|
||||||
from django.utils.functional import cached_property
|
from django.utils.functional import cached_property
|
||||||
from django.utils.ipv6 import clean_ipv6_address
|
from django.utils.ipv6 import clean_ipv6_address
|
||||||
|
from django.utils.timezone import utc
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import ugettext_lazy as _
|
||||||
|
|
||||||
from rest_framework import ISO_8601
|
from rest_framework import ISO_8601
|
||||||
|
@ -149,7 +150,7 @@ def to_choices_dict(choices):
|
||||||
# choices = [('Category', ((1, 'First'), (2, 'Second'))), (3, 'Third')]
|
# choices = [('Category', ((1, 'First'), (2, 'Second'))), (3, 'Third')]
|
||||||
ret = OrderedDict()
|
ret = OrderedDict()
|
||||||
for choice in choices:
|
for choice in choices:
|
||||||
if (not isinstance(choice, (list, tuple))):
|
if not isinstance(choice, (list, tuple)):
|
||||||
# single choice
|
# single choice
|
||||||
ret[choice] = choice
|
ret[choice] = choice
|
||||||
else:
|
else:
|
||||||
|
@ -1104,7 +1105,7 @@ class DateTimeField(Field):
|
||||||
if (field_timezone is not None) and not timezone.is_aware(value):
|
if (field_timezone is not None) and not timezone.is_aware(value):
|
||||||
return timezone.make_aware(value, field_timezone)
|
return timezone.make_aware(value, field_timezone)
|
||||||
elif (field_timezone is None) and timezone.is_aware(value):
|
elif (field_timezone is None) and timezone.is_aware(value):
|
||||||
return timezone.make_naive(value, timezone.UTC())
|
return timezone.make_naive(value, utc)
|
||||||
return value
|
return value
|
||||||
|
|
||||||
def default_timezone(self):
|
def default_timezone(self):
|
||||||
|
@ -1504,12 +1505,16 @@ class ListField(Field):
|
||||||
initial = []
|
initial = []
|
||||||
default_error_messages = {
|
default_error_messages = {
|
||||||
'not_a_list': _('Expected a list of items but got type "{input_type}".'),
|
'not_a_list': _('Expected a list of items but got type "{input_type}".'),
|
||||||
'empty': _('This list may not be empty.')
|
'empty': _('This list may not be empty.'),
|
||||||
|
'min_length': _('Ensure this field has at least {min_length} elements.'),
|
||||||
|
'max_length': _('Ensure this field has no more than {max_length} elements.')
|
||||||
}
|
}
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
self.child = kwargs.pop('child', copy.deepcopy(self.child))
|
self.child = kwargs.pop('child', copy.deepcopy(self.child))
|
||||||
self.allow_empty = kwargs.pop('allow_empty', True)
|
self.allow_empty = kwargs.pop('allow_empty', True)
|
||||||
|
self.max_length = kwargs.pop('max_length', None)
|
||||||
|
self.min_length = kwargs.pop('min_length', None)
|
||||||
|
|
||||||
assert not inspect.isclass(self.child), '`child` has not been instantiated.'
|
assert not inspect.isclass(self.child), '`child` has not been instantiated.'
|
||||||
assert self.child.source is None, (
|
assert self.child.source is None, (
|
||||||
|
@ -1519,6 +1524,12 @@ class ListField(Field):
|
||||||
|
|
||||||
super(ListField, self).__init__(*args, **kwargs)
|
super(ListField, self).__init__(*args, **kwargs)
|
||||||
self.child.bind(field_name='', parent=self)
|
self.child.bind(field_name='', parent=self)
|
||||||
|
if self.max_length is not None:
|
||||||
|
message = self.error_messages['max_length'].format(max_length=self.max_length)
|
||||||
|
self.validators.append(MaxLengthValidator(self.max_length, message=message))
|
||||||
|
if self.min_length is not None:
|
||||||
|
message = self.error_messages['min_length'].format(min_length=self.min_length)
|
||||||
|
self.validators.append(MinLengthValidator(self.min_length, message=message))
|
||||||
|
|
||||||
def get_value(self, dictionary):
|
def get_value(self, dictionary):
|
||||||
if self.field_name not in dictionary:
|
if self.field_name not in dictionary:
|
||||||
|
@ -1614,7 +1625,7 @@ class JSONField(Field):
|
||||||
def get_value(self, dictionary):
|
def get_value(self, dictionary):
|
||||||
if html.is_html_input(dictionary) and self.field_name in dictionary:
|
if html.is_html_input(dictionary) and self.field_name in dictionary:
|
||||||
# When HTML form input is used, mark up the input
|
# When HTML form input is used, mark up the input
|
||||||
# as being a JSON string, rather than a JSON primative.
|
# as being a JSON string, rather than a JSON primitive.
|
||||||
class JSONString(six.text_type):
|
class JSONString(six.text_type):
|
||||||
def __new__(self, value):
|
def __new__(self, value):
|
||||||
ret = six.text_type.__new__(self, value)
|
ret = six.text_type.__new__(self, value)
|
||||||
|
|
|
@ -132,7 +132,7 @@ def _reverse_ordering(ordering_tuple):
|
||||||
ordering and return a new tuple, eg. `('created', '-uuid')`.
|
ordering and return a new tuple, eg. `('created', '-uuid')`.
|
||||||
"""
|
"""
|
||||||
def invert(x):
|
def invert(x):
|
||||||
return x[1:] if (x.startswith('-')) else '-' + x
|
return x[1:] if x.startswith('-') else '-' + x
|
||||||
|
|
||||||
return tuple([invert(item) for item in ordering_tuple])
|
return tuple([invert(item) for item in ordering_tuple])
|
||||||
|
|
||||||
|
|
|
@ -503,7 +503,7 @@ class ManyRelatedField(Field):
|
||||||
return []
|
return []
|
||||||
|
|
||||||
relationship = get_attribute(instance, self.source_attrs)
|
relationship = get_attribute(instance, self.source_attrs)
|
||||||
return relationship.all() if (hasattr(relationship, 'all')) else relationship
|
return relationship.all() if hasattr(relationship, 'all') else relationship
|
||||||
|
|
||||||
def to_representation(self, iterable):
|
def to_representation(self, iterable):
|
||||||
return [
|
return [
|
||||||
|
|
|
@ -228,7 +228,7 @@ class StaticHTMLRenderer(TemplateHTMLRenderer):
|
||||||
|
|
||||||
def render(self, data, accepted_media_type=None, renderer_context=None):
|
def render(self, data, accepted_media_type=None, renderer_context=None):
|
||||||
renderer_context = renderer_context or {}
|
renderer_context = renderer_context or {}
|
||||||
response = renderer_context['response']
|
response = renderer_context.get('response')
|
||||||
|
|
||||||
if response and response.exception:
|
if response and response.exception:
|
||||||
request = renderer_context['request']
|
request = renderer_context['request']
|
||||||
|
@ -540,7 +540,7 @@ class BrowsableAPIRenderer(BaseRenderer):
|
||||||
# If possible, serialize the initial content for the generic form
|
# If possible, serialize the initial content for the generic form
|
||||||
default_parser = view.parser_classes[0]
|
default_parser = view.parser_classes[0]
|
||||||
renderer_class = getattr(default_parser, 'renderer_class', None)
|
renderer_class = getattr(default_parser, 'renderer_class', None)
|
||||||
if (hasattr(view, 'get_serializer') and renderer_class):
|
if hasattr(view, 'get_serializer') and renderer_class:
|
||||||
# View has a serializer defined and parser class has a
|
# View has a serializer defined and parser class has a
|
||||||
# corresponding renderer that can be used to render the data.
|
# corresponding renderer that can be used to render the data.
|
||||||
|
|
||||||
|
@ -598,7 +598,7 @@ class BrowsableAPIRenderer(BaseRenderer):
|
||||||
paginator = getattr(view, 'paginator', None)
|
paginator = getattr(view, 'paginator', None)
|
||||||
if isinstance(data, list):
|
if isinstance(data, list):
|
||||||
pass
|
pass
|
||||||
elif (paginator is not None and data is not None):
|
elif paginator is not None and data is not None:
|
||||||
try:
|
try:
|
||||||
paginator.get_results(data)
|
paginator.get_results(data)
|
||||||
except (TypeError, KeyError):
|
except (TypeError, KeyError):
|
||||||
|
@ -738,7 +738,7 @@ class AdminRenderer(BrowsableAPIRenderer):
|
||||||
ret = template_render(template, context, request=renderer_context['request'])
|
ret = template_render(template, context, request=renderer_context['request'])
|
||||||
|
|
||||||
# Creation and deletion should use redirects in the admin style.
|
# Creation and deletion should use redirects in the admin style.
|
||||||
if (response.status_code == status.HTTP_201_CREATED) and ('Location' in response):
|
if response.status_code == status.HTTP_201_CREATED and 'Location' in response:
|
||||||
response.status_code = status.HTTP_303_SEE_OTHER
|
response.status_code = status.HTTP_303_SEE_OTHER
|
||||||
response['Location'] = request.build_absolute_uri()
|
response['Location'] = request.build_absolute_uri()
|
||||||
ret = ''
|
ret = ''
|
||||||
|
@ -764,7 +764,7 @@ class AdminRenderer(BrowsableAPIRenderer):
|
||||||
)
|
)
|
||||||
|
|
||||||
paginator = getattr(context['view'], 'paginator', None)
|
paginator = getattr(context['view'], 'paginator', None)
|
||||||
if (paginator is not None and data is not None):
|
if paginator is not None and data is not None:
|
||||||
try:
|
try:
|
||||||
results = paginator.get_results(data)
|
results = paginator.get_results(data)
|
||||||
except (TypeError, KeyError):
|
except (TypeError, KeyError):
|
||||||
|
|
|
@ -152,7 +152,7 @@ class Request(object):
|
||||||
|
|
||||||
force_user = getattr(request, '_force_auth_user', None)
|
force_user = getattr(request, '_force_auth_user', None)
|
||||||
force_token = getattr(request, '_force_auth_token', None)
|
force_token = getattr(request, '_force_auth_token', None)
|
||||||
if (force_user is not None or force_token is not None):
|
if force_user is not None or force_token is not None:
|
||||||
forced_auth = ForcedAuthentication(force_user, force_token)
|
forced_auth = ForcedAuthentication(force_user, force_token)
|
||||||
self.authenticators = (forced_auth,)
|
self.authenticators = (forced_auth,)
|
||||||
|
|
||||||
|
|
|
@ -179,10 +179,11 @@ class SimpleRouter(BaseRouter):
|
||||||
initkwargs = route.initkwargs.copy()
|
initkwargs = route.initkwargs.copy()
|
||||||
initkwargs.update(method_kwargs)
|
initkwargs.update(method_kwargs)
|
||||||
url_path = initkwargs.pop("url_path", None) or methodname
|
url_path = initkwargs.pop("url_path", None) or methodname
|
||||||
|
url_name = initkwargs.pop("url_name", None) or url_path
|
||||||
ret.append(Route(
|
ret.append(Route(
|
||||||
url=replace_methodname(route.url, url_path),
|
url=replace_methodname(route.url, url_path),
|
||||||
mapping={httpmethod: methodname for httpmethod in httpmethods},
|
mapping={httpmethod: methodname for httpmethod in httpmethods},
|
||||||
name=replace_methodname(route.name, url_path),
|
name=replace_methodname(route.name, url_name),
|
||||||
initkwargs=initkwargs,
|
initkwargs=initkwargs,
|
||||||
))
|
))
|
||||||
|
|
||||||
|
|
|
@ -661,11 +661,11 @@ class SchemaGenerator(object):
|
||||||
return named_path_components + [action]
|
return named_path_components + [action]
|
||||||
|
|
||||||
|
|
||||||
def get_schema_view(title=None, url=None, renderer_classes=None):
|
def get_schema_view(title=None, url=None, urlconf=None, renderer_classes=None):
|
||||||
"""
|
"""
|
||||||
Return a schema view.
|
Return a schema view.
|
||||||
"""
|
"""
|
||||||
generator = SchemaGenerator(title=title, url=url)
|
generator = SchemaGenerator(title=title, url=url, urlconf=urlconf)
|
||||||
if renderer_classes is None:
|
if renderer_classes is None:
|
||||||
if renderers.BrowsableAPIRenderer in api_settings.DEFAULT_RENDERER_CLASSES:
|
if renderers.BrowsableAPIRenderer in api_settings.DEFAULT_RENDERER_CLASSES:
|
||||||
rclasses = [renderers.CoreJSONRenderer, renderers.BrowsableAPIRenderer]
|
rclasses = [renderers.CoreJSONRenderer, renderers.BrowsableAPIRenderer]
|
||||||
|
|
|
@ -1177,7 +1177,7 @@ class ModelSerializer(Serializer):
|
||||||
|
|
||||||
if postgres_fields and isinstance(model_field, postgres_fields.ArrayField):
|
if postgres_fields and isinstance(model_field, postgres_fields.ArrayField):
|
||||||
# Populate the `child` argument on `ListField` instances generated
|
# Populate the `child` argument on `ListField` instances generated
|
||||||
# for the PostgrSQL specfic `ArrayField`.
|
# for the PostgreSQL specific `ArrayField`.
|
||||||
child_model_field = model_field.base_field
|
child_model_field = model_field.base_field
|
||||||
child_field_class, child_field_kwargs = self.build_standard_field(
|
child_field_class, child_field_kwargs = self.build_standard_field(
|
||||||
'child', child_model_field
|
'child', child_model_field
|
||||||
|
|
|
@ -18,13 +18,13 @@ REST framework settings, checking for user settings first, then falling
|
||||||
back to the defaults.
|
back to the defaults.
|
||||||
"""
|
"""
|
||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
|
from importlib import import_module
|
||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.test.signals import setting_changed
|
from django.test.signals import setting_changed
|
||||||
from django.utils import six
|
from django.utils import six
|
||||||
|
|
||||||
from rest_framework import ISO_8601
|
from rest_framework import ISO_8601
|
||||||
from rest_framework.compat import importlib
|
|
||||||
|
|
||||||
DEFAULTS = {
|
DEFAULTS = {
|
||||||
# Base API policies
|
# Base API policies
|
||||||
|
@ -174,7 +174,7 @@ def import_from_string(val, setting_name):
|
||||||
# Nod to tastypie's use of importlib.
|
# Nod to tastypie's use of importlib.
|
||||||
parts = val.split('.')
|
parts = val.split('.')
|
||||||
module_path, class_name = '.'.join(parts[:-1]), parts[-1]
|
module_path, class_name = '.'.join(parts[:-1]), parts[-1]
|
||||||
module = importlib.import_module(module_path)
|
module = import_module(module_path)
|
||||||
return getattr(module, class_name)
|
return getattr(module, class_name)
|
||||||
except (ImportError, AttributeError) as e:
|
except (ImportError, AttributeError) as e:
|
||||||
msg = "Could not import '%s' for API setting '%s'. %s: %s." % (val, setting_name, e.__class__.__name__, e)
|
msg = "Could not import '%s' for API setting '%s'. %s: %s." % (val, setting_name, e.__class__.__name__, e)
|
||||||
|
|
|
@ -21,7 +21,7 @@
|
||||||
|
|
||||||
<div id="div_id_username" class="clearfix control-group {% if form.username.errors %}error{% endif %}">
|
<div id="div_id_username" class="clearfix control-group {% if form.username.errors %}error{% endif %}">
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="id_username">Username:</label>
|
<label for="id_username">{{ form.username.label }}:</label>
|
||||||
<input type="text" name="username" maxlength="100"
|
<input type="text" name="username" maxlength="100"
|
||||||
autocapitalize="off"
|
autocapitalize="off"
|
||||||
autocorrect="off" class="form-control textinput textInput"
|
autocorrect="off" class="form-control textinput textInput"
|
||||||
|
@ -37,7 +37,7 @@
|
||||||
|
|
||||||
<div id="div_id_password" class="clearfix control-group {% if form.password.errors %}error{% endif %}">
|
<div id="div_id_password" class="clearfix control-group {% if form.password.errors %}error{% endif %}">
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="id_password">Password:</label>
|
<label for="id_password">{{ form.password.label }}:</label>
|
||||||
<input type="password" name="password" maxlength="100" autocapitalize="off" autocorrect="off" class="form-control textinput textInput" id="id_password" required>
|
<input type="password" name="password" maxlength="100" autocapitalize="off" autocorrect="off" class="form-control textinput textInput" id="id_password" required>
|
||||||
{% if form.password.errors %}
|
{% if form.password.errors %}
|
||||||
<p class="text-error">
|
<p class="text-error">
|
||||||
|
|
|
@ -114,7 +114,7 @@ if requests is not None:
|
||||||
self.mount('https://', adapter)
|
self.mount('https://', adapter)
|
||||||
|
|
||||||
def request(self, method, url, *args, **kwargs):
|
def request(self, method, url, *args, **kwargs):
|
||||||
if ':' not in url:
|
if not url.startswith('http'):
|
||||||
raise ValueError('Missing "http:" or "https:". Use a fully qualified URL, eg "http://testserver%s"' % url)
|
raise ValueError('Missing "http:" or "https:". Use a fully qualified URL, eg "http://testserver%s"' % url)
|
||||||
return super(RequestsClient, self).request(method, url, *args, **kwargs)
|
return super(RequestsClient, self).request(method, url, *args, **kwargs)
|
||||||
|
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
from django.conf.urls import include, url
|
from django.conf.urls import url
|
||||||
|
|
||||||
from rest_framework.compat import RegexURLResolver
|
from rest_framework.compat import RegexURLResolver, include
|
||||||
from rest_framework.settings import api_settings
|
from rest_framework.settings import api_settings
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -55,6 +55,11 @@ class JSONEncoder(json.JSONEncoder):
|
||||||
elif hasattr(obj, 'tolist'):
|
elif hasattr(obj, 'tolist'):
|
||||||
# Numpy arrays and array scalars.
|
# Numpy arrays and array scalars.
|
||||||
return obj.tolist()
|
return obj.tolist()
|
||||||
|
elif (coreapi is not None) and isinstance(obj, (coreapi.Document, coreapi.Error)):
|
||||||
|
raise RuntimeError(
|
||||||
|
'Cannot return a coreapi object from a JSON view. '
|
||||||
|
'You should be using a schema renderer instead for this view.'
|
||||||
|
)
|
||||||
elif hasattr(obj, '__getitem__'):
|
elif hasattr(obj, '__getitem__'):
|
||||||
try:
|
try:
|
||||||
return dict(obj)
|
return dict(obj)
|
||||||
|
@ -62,9 +67,4 @@ class JSONEncoder(json.JSONEncoder):
|
||||||
pass
|
pass
|
||||||
elif hasattr(obj, '__iter__'):
|
elif hasattr(obj, '__iter__'):
|
||||||
return tuple(item for item in obj)
|
return tuple(item for item in obj)
|
||||||
elif (coreapi is not None) and isinstance(obj, (coreapi.Document, coreapi.Error)):
|
|
||||||
raise RuntimeError(
|
|
||||||
'Cannot return a coreapi object from a JSON view. '
|
|
||||||
'You should be using a schema renderer instead for this view.'
|
|
||||||
)
|
|
||||||
return super(JSONEncoder, self).default(obj)
|
return super(JSONEncoder, self).default(obj)
|
||||||
|
|
|
@ -32,23 +32,18 @@ def dedent(content):
|
||||||
unindented text on the initial line.
|
unindented text on the initial line.
|
||||||
"""
|
"""
|
||||||
content = force_text(content)
|
content = force_text(content)
|
||||||
whitespace_counts = [
|
lines = [line for line in content.splitlines()[1:] if line.lstrip()]
|
||||||
len(line) - len(line.lstrip(' '))
|
|
||||||
for line in content.splitlines()[1:] if line.lstrip()
|
|
||||||
]
|
|
||||||
tab_counts = [
|
|
||||||
len(line) - len(line.lstrip('\t'))
|
|
||||||
for line in content.splitlines()[1:] if line.lstrip()
|
|
||||||
]
|
|
||||||
|
|
||||||
# unindent the content if needed
|
# unindent the content if needed
|
||||||
if whitespace_counts:
|
if lines:
|
||||||
whitespace_pattern = '^' + (' ' * min(whitespace_counts))
|
whitespace_counts = min([len(line) - len(line.lstrip(' ')) for line in lines])
|
||||||
content = re.sub(re.compile(whitespace_pattern, re.MULTILINE), '', content)
|
tab_counts = min([len(line) - len(line.lstrip('\t')) for line in lines])
|
||||||
elif tab_counts:
|
if whitespace_counts:
|
||||||
whitespace_pattern = '^' + ('\t' * min(whitespace_counts))
|
whitespace_pattern = '^' + (' ' * whitespace_counts)
|
||||||
content = re.sub(re.compile(whitespace_pattern, re.MULTILINE), '', content)
|
content = re.sub(re.compile(whitespace_pattern, re.MULTILINE), '', content)
|
||||||
|
elif tab_counts:
|
||||||
|
whitespace_pattern = '^' + ('\t' * tab_counts)
|
||||||
|
content = re.sub(re.compile(whitespace_pattern, re.MULTILINE), '', content)
|
||||||
return content.strip()
|
return content.strip()
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -49,10 +49,8 @@ def order_by_precedence(media_type_lst):
|
||||||
@python_2_unicode_compatible
|
@python_2_unicode_compatible
|
||||||
class _MediaType(object):
|
class _MediaType(object):
|
||||||
def __init__(self, media_type_str):
|
def __init__(self, media_type_str):
|
||||||
if media_type_str is None:
|
self.orig = '' if (media_type_str is None) else media_type_str
|
||||||
media_type_str = ''
|
self.full_type, self.params = parse_header(self.orig.encode(HTTP_HEADER_ENCODING))
|
||||||
self.orig = media_type_str
|
|
||||||
self.full_type, self.params = parse_header(media_type_str.encode(HTTP_HEADER_ENCODING))
|
|
||||||
self.main_type, sep, self.sub_type = self.full_type.partition('/')
|
self.main_type, sep, self.sub_type = self.full_type.partition('/')
|
||||||
|
|
||||||
def match(self, other):
|
def match(self, other):
|
||||||
|
|
|
@ -76,7 +76,12 @@ def _get_forward_relationships(opts):
|
||||||
Returns an `OrderedDict` of field names to `RelationInfo`.
|
Returns an `OrderedDict` of field names to `RelationInfo`.
|
||||||
"""
|
"""
|
||||||
forward_relations = OrderedDict()
|
forward_relations = OrderedDict()
|
||||||
for field in [field for field in opts.fields if field.serialize and get_remote_field(field)]:
|
for field in [
|
||||||
|
field for field in opts.fields
|
||||||
|
if field.serialize and get_remote_field(field) and not (field.primary_key and field.one_to_one)
|
||||||
|
# If the field is a OneToOneField and it's been marked as PK, then this
|
||||||
|
# is a multi-table inheritance auto created PK ('%_ptr').
|
||||||
|
]:
|
||||||
forward_relations[field.name] = RelationInfo(
|
forward_relations[field.name] = RelationInfo(
|
||||||
model_field=field,
|
model_field=field,
|
||||||
related_model=get_related_model(field),
|
related_model=get_related_model(field),
|
||||||
|
|
|
@ -117,7 +117,7 @@ class NamespaceVersioning(BaseVersioning):
|
||||||
|
|
||||||
def determine_version(self, request, *args, **kwargs):
|
def determine_version(self, request, *args, **kwargs):
|
||||||
resolver_match = getattr(request, 'resolver_match', None)
|
resolver_match = getattr(request, 'resolver_match', None)
|
||||||
if (resolver_match is None or not resolver_match.namespace):
|
if resolver_match is None or not resolver_match.namespace:
|
||||||
return self.default_version
|
return self.default_version
|
||||||
|
|
||||||
# Allow for possibly nested namespaces.
|
# Allow for possibly nested namespaces.
|
||||||
|
|
|
@ -4,6 +4,7 @@ from __future__ import unicode_literals
|
||||||
|
|
||||||
import base64
|
import base64
|
||||||
|
|
||||||
|
import pytest
|
||||||
from django.conf.urls import include, url
|
from django.conf.urls import include, url
|
||||||
from django.contrib.auth.models import User
|
from django.contrib.auth.models import User
|
||||||
from django.db import models
|
from django.db import models
|
||||||
|
@ -151,6 +152,18 @@ class BasicAuthTests(TestCase):
|
||||||
assert response.status_code == status.HTTP_401_UNAUTHORIZED
|
assert response.status_code == status.HTTP_401_UNAUTHORIZED
|
||||||
assert response['WWW-Authenticate'] == 'Basic realm="api"'
|
assert response['WWW-Authenticate'] == 'Basic realm="api"'
|
||||||
|
|
||||||
|
def test_fail_post_if_credentials_are_missing(self):
|
||||||
|
response = self.csrf_client.post(
|
||||||
|
'/basic/', {'example': 'example'}, HTTP_AUTHORIZATION='Basic ')
|
||||||
|
assert response.status_code == status.HTTP_401_UNAUTHORIZED
|
||||||
|
|
||||||
|
def test_fail_post_if_credentials_contain_spaces(self):
|
||||||
|
response = self.csrf_client.post(
|
||||||
|
'/basic/', {'example': 'example'},
|
||||||
|
HTTP_AUTHORIZATION='Basic foo bar'
|
||||||
|
)
|
||||||
|
assert response.status_code == status.HTTP_401_UNAUTHORIZED
|
||||||
|
|
||||||
|
|
||||||
@override_settings(ROOT_URLCONF='tests.test_authentication')
|
@override_settings(ROOT_URLCONF='tests.test_authentication')
|
||||||
class SessionAuthTests(TestCase):
|
class SessionAuthTests(TestCase):
|
||||||
|
@ -249,6 +262,17 @@ class BaseTokenAuthTests(object):
|
||||||
)
|
)
|
||||||
assert response.status_code == status.HTTP_200_OK
|
assert response.status_code == status.HTTP_200_OK
|
||||||
|
|
||||||
|
def test_fail_authentication_if_user_is_not_active(self):
|
||||||
|
user = User.objects.create_user('foo', 'bar', 'baz')
|
||||||
|
user.is_active = False
|
||||||
|
user.save()
|
||||||
|
self.model.objects.create(key='foobar_token', user=user)
|
||||||
|
response = self.csrf_client.post(
|
||||||
|
self.path, {'example': 'example'},
|
||||||
|
HTTP_AUTHORIZATION=self.header_prefix + 'foobar_token'
|
||||||
|
)
|
||||||
|
assert response.status_code == status.HTTP_401_UNAUTHORIZED
|
||||||
|
|
||||||
def test_fail_post_form_passing_nonexistent_token_auth(self):
|
def test_fail_post_form_passing_nonexistent_token_auth(self):
|
||||||
# use a nonexistent token key
|
# use a nonexistent token key
|
||||||
auth = self.header_prefix + 'wxyz6789'
|
auth = self.header_prefix + 'wxyz6789'
|
||||||
|
@ -257,6 +281,19 @@ class BaseTokenAuthTests(object):
|
||||||
)
|
)
|
||||||
assert response.status_code == status.HTTP_401_UNAUTHORIZED
|
assert response.status_code == status.HTTP_401_UNAUTHORIZED
|
||||||
|
|
||||||
|
def test_fail_post_if_token_is_missing(self):
|
||||||
|
response = self.csrf_client.post(
|
||||||
|
self.path, {'example': 'example'},
|
||||||
|
HTTP_AUTHORIZATION=self.header_prefix)
|
||||||
|
assert response.status_code == status.HTTP_401_UNAUTHORIZED
|
||||||
|
|
||||||
|
def test_fail_post_if_token_contains_spaces(self):
|
||||||
|
response = self.csrf_client.post(
|
||||||
|
self.path, {'example': 'example'},
|
||||||
|
HTTP_AUTHORIZATION=self.header_prefix + 'foo bar'
|
||||||
|
)
|
||||||
|
assert response.status_code == status.HTTP_401_UNAUTHORIZED
|
||||||
|
|
||||||
def test_fail_post_form_passing_invalid_token_auth(self):
|
def test_fail_post_form_passing_invalid_token_auth(self):
|
||||||
# add an 'invalid' unicode character
|
# add an 'invalid' unicode character
|
||||||
auth = self.header_prefix + self.key + "¸"
|
auth = self.header_prefix + self.key + "¸"
|
||||||
|
@ -461,3 +498,28 @@ class NoAuthenticationClassesTests(TestCase):
|
||||||
response = view(request)
|
response = view(request)
|
||||||
assert response.status_code == status.HTTP_403_FORBIDDEN
|
assert response.status_code == status.HTTP_403_FORBIDDEN
|
||||||
assert response.data == {'detail': 'Dummy permission message'}
|
assert response.data == {'detail': 'Dummy permission message'}
|
||||||
|
|
||||||
|
|
||||||
|
class BasicAuthenticationUnitTests(TestCase):
|
||||||
|
|
||||||
|
def test_base_authentication_abstract_method(self):
|
||||||
|
with pytest.raises(NotImplementedError):
|
||||||
|
BaseAuthentication().authenticate({})
|
||||||
|
|
||||||
|
def test_basic_authentication_raises_error_if_user_not_found(self):
|
||||||
|
auth = BasicAuthentication()
|
||||||
|
with pytest.raises(exceptions.AuthenticationFailed):
|
||||||
|
auth.authenticate_credentials('invalid id', 'invalid password')
|
||||||
|
|
||||||
|
def test_basic_authentication_raises_error_if_user_not_active(self):
|
||||||
|
from rest_framework import authentication
|
||||||
|
|
||||||
|
class MockUser(object):
|
||||||
|
is_active = False
|
||||||
|
old_authenticate = authentication.authenticate
|
||||||
|
authentication.authenticate = lambda **kwargs: MockUser()
|
||||||
|
auth = authentication.BasicAuthentication()
|
||||||
|
with pytest.raises(exceptions.AuthenticationFailed) as error:
|
||||||
|
auth.authenticate_credentials('foo', 'bar')
|
||||||
|
assert 'User inactive or deleted.' in str(error)
|
||||||
|
authentication.authenticate = old_authenticate
|
||||||
|
|
29
tests/test_authtoken.py
Normal file
|
@ -0,0 +1,29 @@
|
||||||
|
import pytest
|
||||||
|
from django.contrib.admin import site
|
||||||
|
from django.contrib.auth.models import User
|
||||||
|
from django.test import TestCase
|
||||||
|
|
||||||
|
from rest_framework.authtoken.admin import TokenAdmin
|
||||||
|
from rest_framework.authtoken.models import Token
|
||||||
|
from rest_framework.authtoken.serializers import AuthTokenSerializer
|
||||||
|
from rest_framework.exceptions import ValidationError
|
||||||
|
|
||||||
|
|
||||||
|
class AuthTokenTests(TestCase):
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
self.site = site
|
||||||
|
self.user = User.objects.create_user(username='test_user')
|
||||||
|
self.token = Token.objects.create(key='test token', user=self.user)
|
||||||
|
|
||||||
|
def test_model_admin_displayed_fields(self):
|
||||||
|
mock_request = object()
|
||||||
|
token_admin = TokenAdmin(self.token, self.site)
|
||||||
|
assert token_admin.get_fields(mock_request) == ('user',)
|
||||||
|
|
||||||
|
def test_token_string_representation(self):
|
||||||
|
assert str(self.token) == 'test token'
|
||||||
|
|
||||||
|
def test_validate_raise_error_if_no_credentials_provided(self):
|
||||||
|
with pytest.raises(ValidationError):
|
||||||
|
AuthTokenSerializer().validate({})
|
|
@ -45,6 +45,15 @@ class TestSimpleBoundField:
|
||||||
assert serializer['amount'].errors is None
|
assert serializer['amount'].errors is None
|
||||||
assert serializer['amount'].name == 'amount'
|
assert serializer['amount'].name == 'amount'
|
||||||
|
|
||||||
|
def test_delete_field(self):
|
||||||
|
class ExampleSerializer(serializers.Serializer):
|
||||||
|
text = serializers.CharField(max_length=100)
|
||||||
|
amount = serializers.IntegerField()
|
||||||
|
|
||||||
|
serializer = ExampleSerializer()
|
||||||
|
del serializer.fields['text']
|
||||||
|
assert 'text' not in serializer.fields.keys()
|
||||||
|
|
||||||
def test_as_form_fields(self):
|
def test_as_form_fields(self):
|
||||||
class ExampleSerializer(serializers.Serializer):
|
class ExampleSerializer(serializers.Serializer):
|
||||||
bool_field = serializers.BooleanField()
|
bool_field = serializers.BooleanField()
|
||||||
|
|
67
tests/test_compat.py
Normal file
|
@ -0,0 +1,67 @@
|
||||||
|
from django.test import TestCase
|
||||||
|
|
||||||
|
from rest_framework import compat
|
||||||
|
|
||||||
|
|
||||||
|
class CompatTests(TestCase):
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
self.original_django_version = compat.django.VERSION
|
||||||
|
self.original_transaction = compat.transaction
|
||||||
|
|
||||||
|
def tearDown(self):
|
||||||
|
compat.django.VERSION = self.original_django_version
|
||||||
|
compat.transaction = self.original_transaction
|
||||||
|
|
||||||
|
def test_total_seconds(self):
|
||||||
|
class MockTimedelta(object):
|
||||||
|
days = 1
|
||||||
|
seconds = 1
|
||||||
|
microseconds = 100
|
||||||
|
timedelta = MockTimedelta()
|
||||||
|
expected = (timedelta.days * 86400.0) + float(timedelta.seconds) + (timedelta.microseconds / 1000000.0)
|
||||||
|
assert compat.total_seconds(timedelta) == expected
|
||||||
|
|
||||||
|
def test_get_remote_field_with_old_django_version(self):
|
||||||
|
class MockField(object):
|
||||||
|
rel = 'example_rel'
|
||||||
|
compat.django.VERSION = (1, 8)
|
||||||
|
assert compat.get_remote_field(MockField(), default='default_value') == 'example_rel'
|
||||||
|
assert compat.get_remote_field(object(), default='default_value') == 'default_value'
|
||||||
|
|
||||||
|
def test_get_remote_field_with_new_django_version(self):
|
||||||
|
class MockField(object):
|
||||||
|
remote_field = 'example_remote_field'
|
||||||
|
compat.django.VERSION = (1, 10)
|
||||||
|
assert compat.get_remote_field(MockField(), default='default_value') == 'example_remote_field'
|
||||||
|
assert compat.get_remote_field(object(), default='default_value') == 'default_value'
|
||||||
|
|
||||||
|
def test_set_rollback_for_transaction_in_managed_mode(self):
|
||||||
|
class MockTransaction(object):
|
||||||
|
called_rollback = False
|
||||||
|
called_leave_transaction_management = False
|
||||||
|
|
||||||
|
def is_managed(self):
|
||||||
|
return True
|
||||||
|
|
||||||
|
def is_dirty(self):
|
||||||
|
return True
|
||||||
|
|
||||||
|
def rollback(self):
|
||||||
|
self.called_rollback = True
|
||||||
|
|
||||||
|
def leave_transaction_management(self):
|
||||||
|
self.called_leave_transaction_management = True
|
||||||
|
|
||||||
|
dirty_mock_transaction = MockTransaction()
|
||||||
|
compat.transaction = dirty_mock_transaction
|
||||||
|
compat.set_rollback()
|
||||||
|
assert dirty_mock_transaction.called_rollback is True
|
||||||
|
assert dirty_mock_transaction.called_leave_transaction_management is True
|
||||||
|
|
||||||
|
clean_mock_transaction = MockTransaction()
|
||||||
|
clean_mock_transaction.is_dirty = lambda: False
|
||||||
|
compat.transaction = clean_mock_transaction
|
||||||
|
compat.set_rollback()
|
||||||
|
assert clean_mock_transaction.called_rollback is False
|
||||||
|
assert clean_mock_transaction.called_leave_transaction_management is True
|
|
@ -124,4 +124,8 @@ class TestViewNamesAndDescriptions(TestCase):
|
||||||
|
|
||||||
|
|
||||||
def test_dedent_tabs():
|
def test_dedent_tabs():
|
||||||
assert dedent("\tfirst string\n\n\tsecond string") == 'first string\n\n\tsecond string'
|
result = 'first string\n\nsecond string'
|
||||||
|
assert dedent(" first string\n\n second string") == result
|
||||||
|
assert dedent("first string\n\n second string") == result
|
||||||
|
assert dedent("\tfirst string\n\n\tsecond string") == result
|
||||||
|
assert dedent("first string\n\n\tsecond string") == result
|
||||||
|
|
|
@ -2,8 +2,10 @@ from datetime import date, datetime, timedelta, tzinfo
|
||||||
from decimal import Decimal
|
from decimal import Decimal
|
||||||
from uuid import uuid4
|
from uuid import uuid4
|
||||||
|
|
||||||
|
import pytest
|
||||||
from django.test import TestCase
|
from django.test import TestCase
|
||||||
|
|
||||||
|
from rest_framework.compat import coreapi
|
||||||
from rest_framework.utils.encoders import JSONEncoder
|
from rest_framework.utils.encoders import JSONEncoder
|
||||||
|
|
||||||
|
|
||||||
|
@ -56,7 +58,7 @@ class JSONEncoderTests(TestCase):
|
||||||
|
|
||||||
current_time = datetime.now().time()
|
current_time = datetime.now().time()
|
||||||
current_time = current_time.replace(tzinfo=UTC())
|
current_time = current_time.replace(tzinfo=UTC())
|
||||||
with self.assertRaises(ValueError):
|
with pytest.raises(ValueError):
|
||||||
self.encoder.default(current_time)
|
self.encoder.default(current_time)
|
||||||
|
|
||||||
def test_encode_date(self):
|
def test_encode_date(self):
|
||||||
|
@ -79,3 +81,13 @@ class JSONEncoderTests(TestCase):
|
||||||
"""
|
"""
|
||||||
unique_id = uuid4()
|
unique_id = uuid4()
|
||||||
assert self.encoder.default(unique_id) == str(unique_id)
|
assert self.encoder.default(unique_id) == str(unique_id)
|
||||||
|
|
||||||
|
def test_encode_coreapi_raises_error(self):
|
||||||
|
"""
|
||||||
|
Tests encoding a coreapi objects raises proper error
|
||||||
|
"""
|
||||||
|
with pytest.raises(RuntimeError):
|
||||||
|
self.encoder.default(coreapi.Document())
|
||||||
|
|
||||||
|
with pytest.raises(RuntimeError):
|
||||||
|
self.encoder.default(coreapi.Error())
|
||||||
|
|
|
@ -8,7 +8,8 @@ from decimal import Decimal
|
||||||
import pytest
|
import pytest
|
||||||
from django.http import QueryDict
|
from django.http import QueryDict
|
||||||
from django.test import TestCase, override_settings
|
from django.test import TestCase, override_settings
|
||||||
from django.utils import six, timezone
|
from django.utils import six
|
||||||
|
from django.utils.timezone import utc
|
||||||
|
|
||||||
import rest_framework
|
import rest_framework
|
||||||
from rest_framework import serializers
|
from rest_framework import serializers
|
||||||
|
@ -1129,13 +1130,13 @@ class TestDateTimeField(FieldValues):
|
||||||
Valid and invalid values for `DateTimeField`.
|
Valid and invalid values for `DateTimeField`.
|
||||||
"""
|
"""
|
||||||
valid_inputs = {
|
valid_inputs = {
|
||||||
'2001-01-01 13:00': datetime.datetime(2001, 1, 1, 13, 00, tzinfo=timezone.UTC()),
|
'2001-01-01 13:00': datetime.datetime(2001, 1, 1, 13, 00, tzinfo=utc),
|
||||||
'2001-01-01T13:00': datetime.datetime(2001, 1, 1, 13, 00, tzinfo=timezone.UTC()),
|
'2001-01-01T13:00': datetime.datetime(2001, 1, 1, 13, 00, tzinfo=utc),
|
||||||
'2001-01-01T13:00Z': datetime.datetime(2001, 1, 1, 13, 00, tzinfo=timezone.UTC()),
|
'2001-01-01T13:00Z': datetime.datetime(2001, 1, 1, 13, 00, tzinfo=utc),
|
||||||
datetime.datetime(2001, 1, 1, 13, 00): datetime.datetime(2001, 1, 1, 13, 00, tzinfo=timezone.UTC()),
|
datetime.datetime(2001, 1, 1, 13, 00): datetime.datetime(2001, 1, 1, 13, 00, tzinfo=utc),
|
||||||
datetime.datetime(2001, 1, 1, 13, 00, tzinfo=timezone.UTC()): datetime.datetime(2001, 1, 1, 13, 00, tzinfo=timezone.UTC()),
|
datetime.datetime(2001, 1, 1, 13, 00, tzinfo=utc): datetime.datetime(2001, 1, 1, 13, 00, tzinfo=utc),
|
||||||
# Django 1.4 does not support timezone string parsing.
|
# Django 1.4 does not support timezone string parsing.
|
||||||
'2001-01-01T13:00Z': datetime.datetime(2001, 1, 1, 13, 00, tzinfo=timezone.UTC())
|
'2001-01-01T13:00Z': datetime.datetime(2001, 1, 1, 13, 00, tzinfo=utc)
|
||||||
}
|
}
|
||||||
invalid_inputs = {
|
invalid_inputs = {
|
||||||
'abc': ['Datetime has wrong format. Use one of these formats instead: YYYY-MM-DDThh:mm[:ss[.uuuuuu]][+HH:MM|-HH:MM|Z].'],
|
'abc': ['Datetime has wrong format. Use one of these formats instead: YYYY-MM-DDThh:mm[:ss[.uuuuuu]][+HH:MM|-HH:MM|Z].'],
|
||||||
|
@ -1144,13 +1145,13 @@ class TestDateTimeField(FieldValues):
|
||||||
}
|
}
|
||||||
outputs = {
|
outputs = {
|
||||||
datetime.datetime(2001, 1, 1, 13, 00): '2001-01-01T13:00:00',
|
datetime.datetime(2001, 1, 1, 13, 00): '2001-01-01T13:00:00',
|
||||||
datetime.datetime(2001, 1, 1, 13, 00, tzinfo=timezone.UTC()): '2001-01-01T13:00:00Z',
|
datetime.datetime(2001, 1, 1, 13, 00, tzinfo=utc): '2001-01-01T13:00:00Z',
|
||||||
'2001-01-01T00:00:00': '2001-01-01T00:00:00',
|
'2001-01-01T00:00:00': '2001-01-01T00:00:00',
|
||||||
six.text_type('2016-01-10T00:00:00'): '2016-01-10T00:00:00',
|
six.text_type('2016-01-10T00:00:00'): '2016-01-10T00:00:00',
|
||||||
None: None,
|
None: None,
|
||||||
'': None,
|
'': None,
|
||||||
}
|
}
|
||||||
field = serializers.DateTimeField(default_timezone=timezone.UTC())
|
field = serializers.DateTimeField(default_timezone=utc)
|
||||||
|
|
||||||
|
|
||||||
class TestCustomInputFormatDateTimeField(FieldValues):
|
class TestCustomInputFormatDateTimeField(FieldValues):
|
||||||
|
@ -1158,13 +1159,13 @@ class TestCustomInputFormatDateTimeField(FieldValues):
|
||||||
Valid and invalid values for `DateTimeField` with a custom input format.
|
Valid and invalid values for `DateTimeField` with a custom input format.
|
||||||
"""
|
"""
|
||||||
valid_inputs = {
|
valid_inputs = {
|
||||||
'1:35pm, 1 Jan 2001': datetime.datetime(2001, 1, 1, 13, 35, tzinfo=timezone.UTC()),
|
'1:35pm, 1 Jan 2001': datetime.datetime(2001, 1, 1, 13, 35, tzinfo=utc),
|
||||||
}
|
}
|
||||||
invalid_inputs = {
|
invalid_inputs = {
|
||||||
'2001-01-01T20:50': ['Datetime has wrong format. Use one of these formats instead: hh:mm[AM|PM], DD [Jan-Dec] YYYY.']
|
'2001-01-01T20:50': ['Datetime has wrong format. Use one of these formats instead: hh:mm[AM|PM], DD [Jan-Dec] YYYY.']
|
||||||
}
|
}
|
||||||
outputs = {}
|
outputs = {}
|
||||||
field = serializers.DateTimeField(default_timezone=timezone.UTC(), input_formats=['%I:%M%p, %d %b %Y'])
|
field = serializers.DateTimeField(default_timezone=utc, input_formats=['%I:%M%p, %d %b %Y'])
|
||||||
|
|
||||||
|
|
||||||
class TestCustomOutputFormatDateTimeField(FieldValues):
|
class TestCustomOutputFormatDateTimeField(FieldValues):
|
||||||
|
@ -1196,7 +1197,7 @@ class TestNaiveDateTimeField(FieldValues):
|
||||||
Valid and invalid values for `DateTimeField` with naive datetimes.
|
Valid and invalid values for `DateTimeField` with naive datetimes.
|
||||||
"""
|
"""
|
||||||
valid_inputs = {
|
valid_inputs = {
|
||||||
datetime.datetime(2001, 1, 1, 13, 00, tzinfo=timezone.UTC()): datetime.datetime(2001, 1, 1, 13, 00),
|
datetime.datetime(2001, 1, 1, 13, 00, tzinfo=utc): datetime.datetime(2001, 1, 1, 13, 00),
|
||||||
'2001-01-01 13:00': datetime.datetime(2001, 1, 1, 13, 00),
|
'2001-01-01 13:00': datetime.datetime(2001, 1, 1, 13, 00),
|
||||||
}
|
}
|
||||||
invalid_inputs = {}
|
invalid_inputs = {}
|
||||||
|
@ -1667,6 +1668,16 @@ class TestEmptyListField(FieldValues):
|
||||||
field = serializers.ListField(child=serializers.IntegerField(), allow_empty=False)
|
field = serializers.ListField(child=serializers.IntegerField(), allow_empty=False)
|
||||||
|
|
||||||
|
|
||||||
|
class TestListFieldLengthLimit(FieldValues):
|
||||||
|
valid_inputs = ()
|
||||||
|
invalid_inputs = [
|
||||||
|
((0, 1), ['Ensure this field has at least 3 elements.']),
|
||||||
|
((0, 1, 2, 3, 4, 5), ['Ensure this field has no more than 4 elements.']),
|
||||||
|
]
|
||||||
|
outputs = ()
|
||||||
|
field = serializers.ListField(child=serializers.IntegerField(), min_length=3, max_length=4)
|
||||||
|
|
||||||
|
|
||||||
class TestUnvalidatedListField(FieldValues):
|
class TestUnvalidatedListField(FieldValues):
|
||||||
"""
|
"""
|
||||||
Values for `ListField` with no `child` argument.
|
Values for `ListField` with no `child` argument.
|
||||||
|
|
|
@ -5,6 +5,7 @@ import unittest
|
||||||
import warnings
|
import warnings
|
||||||
from decimal import Decimal
|
from decimal import Decimal
|
||||||
|
|
||||||
|
import pytest
|
||||||
from django.conf.urls import url
|
from django.conf.urls import url
|
||||||
from django.core.exceptions import ImproperlyConfigured
|
from django.core.exceptions import ImproperlyConfigured
|
||||||
from django.db import models
|
from django.db import models
|
||||||
|
@ -119,6 +120,27 @@ if django_filters:
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
class BaseFilterTests(TestCase):
|
||||||
|
def setUp(self):
|
||||||
|
self.original_coreapi = filters.coreapi
|
||||||
|
filters.coreapi = True # mock it, because not None value needed
|
||||||
|
self.filter_backend = filters.BaseFilterBackend()
|
||||||
|
|
||||||
|
def tearDown(self):
|
||||||
|
filters.coreapi = self.original_coreapi
|
||||||
|
|
||||||
|
def test_filter_queryset_raises_error(self):
|
||||||
|
with pytest.raises(NotImplementedError):
|
||||||
|
self.filter_backend.filter_queryset(None, None, None)
|
||||||
|
|
||||||
|
def test_get_schema_fields_checks_for_coreapi(self):
|
||||||
|
filters.coreapi = None
|
||||||
|
with pytest.raises(AssertionError):
|
||||||
|
self.filter_backend.get_schema_fields({})
|
||||||
|
filters.coreapi = True
|
||||||
|
assert self.filter_backend.get_schema_fields({}) == []
|
||||||
|
|
||||||
|
|
||||||
class CommonFilteringTestCase(TestCase):
|
class CommonFilteringTestCase(TestCase):
|
||||||
def _serialize_object(self, obj):
|
def _serialize_object(self, obj):
|
||||||
return {'id': obj.id, 'text': obj.text, 'decimal': str(obj.decimal), 'date': obj.date.isoformat()}
|
return {'id': obj.id, 'text': obj.text, 'decimal': str(obj.decimal), 'date': obj.date.isoformat()}
|
||||||
|
@ -429,6 +451,19 @@ class SearchFilterTests(TestCase):
|
||||||
{'id': 2, 'title': 'zz', 'text': 'bcd'}
|
{'id': 2, 'title': 'zz', 'text': 'bcd'}
|
||||||
]
|
]
|
||||||
|
|
||||||
|
def test_search_returns_same_queryset_if_no_search_fields_or_terms_provided(self):
|
||||||
|
class SearchListView(generics.ListAPIView):
|
||||||
|
queryset = SearchFilterModel.objects.all()
|
||||||
|
serializer_class = SearchFilterSerializer
|
||||||
|
filter_backends = (filters.SearchFilter,)
|
||||||
|
|
||||||
|
view = SearchListView.as_view()
|
||||||
|
request = factory.get('/')
|
||||||
|
response = view(request)
|
||||||
|
expected = SearchFilterSerializer(SearchFilterModel.objects.all(),
|
||||||
|
many=True).data
|
||||||
|
assert response.data == expected
|
||||||
|
|
||||||
def test_exact_search(self):
|
def test_exact_search(self):
|
||||||
class SearchListView(generics.ListAPIView):
|
class SearchListView(generics.ListAPIView):
|
||||||
queryset = SearchFilterModel.objects.all()
|
queryset = SearchFilterModel.objects.all()
|
||||||
|
|
|
@ -547,3 +547,94 @@ class TestGuardedQueryset(TestCase):
|
||||||
request = factory.get('/')
|
request = factory.get('/')
|
||||||
with pytest.raises(RuntimeError):
|
with pytest.raises(RuntimeError):
|
||||||
view(request).render()
|
view(request).render()
|
||||||
|
|
||||||
|
|
||||||
|
class ApiViewsTests(TestCase):
|
||||||
|
|
||||||
|
def test_create_api_view_post(self):
|
||||||
|
class MockCreateApiView(generics.CreateAPIView):
|
||||||
|
def create(self, request, *args, **kwargs):
|
||||||
|
self.called = True
|
||||||
|
self.call_args = (request, args, kwargs)
|
||||||
|
view = MockCreateApiView()
|
||||||
|
data = ('test request', ('test arg',), {'test_kwarg': 'test'})
|
||||||
|
view.post('test request', 'test arg', test_kwarg='test')
|
||||||
|
assert view.called is True
|
||||||
|
assert view.call_args == data
|
||||||
|
|
||||||
|
def test_destroy_api_view_delete(self):
|
||||||
|
class MockDestroyApiView(generics.DestroyAPIView):
|
||||||
|
def destroy(self, request, *args, **kwargs):
|
||||||
|
self.called = True
|
||||||
|
self.call_args = (request, args, kwargs)
|
||||||
|
view = MockDestroyApiView()
|
||||||
|
data = ('test request', ('test arg',), {'test_kwarg': 'test'})
|
||||||
|
view.delete('test request', 'test arg', test_kwarg='test')
|
||||||
|
assert view.called is True
|
||||||
|
assert view.call_args == data
|
||||||
|
|
||||||
|
def test_update_api_view_partial_update(self):
|
||||||
|
class MockUpdateApiView(generics.UpdateAPIView):
|
||||||
|
def partial_update(self, request, *args, **kwargs):
|
||||||
|
self.called = True
|
||||||
|
self.call_args = (request, args, kwargs)
|
||||||
|
view = MockUpdateApiView()
|
||||||
|
data = ('test request', ('test arg',), {'test_kwarg': 'test'})
|
||||||
|
view.patch('test request', 'test arg', test_kwarg='test')
|
||||||
|
assert view.called is True
|
||||||
|
assert view.call_args == data
|
||||||
|
|
||||||
|
def test_retrieve_update_api_view_get(self):
|
||||||
|
class MockRetrieveUpdateApiView(generics.RetrieveUpdateAPIView):
|
||||||
|
def retrieve(self, request, *args, **kwargs):
|
||||||
|
self.called = True
|
||||||
|
self.call_args = (request, args, kwargs)
|
||||||
|
view = MockRetrieveUpdateApiView()
|
||||||
|
data = ('test request', ('test arg',), {'test_kwarg': 'test'})
|
||||||
|
view.get('test request', 'test arg', test_kwarg='test')
|
||||||
|
assert view.called is True
|
||||||
|
assert view.call_args == data
|
||||||
|
|
||||||
|
def test_retrieve_update_api_view_put(self):
|
||||||
|
class MockRetrieveUpdateApiView(generics.RetrieveUpdateAPIView):
|
||||||
|
def update(self, request, *args, **kwargs):
|
||||||
|
self.called = True
|
||||||
|
self.call_args = (request, args, kwargs)
|
||||||
|
view = MockRetrieveUpdateApiView()
|
||||||
|
data = ('test request', ('test arg',), {'test_kwarg': 'test'})
|
||||||
|
view.put('test request', 'test arg', test_kwarg='test')
|
||||||
|
assert view.called is True
|
||||||
|
assert view.call_args == data
|
||||||
|
|
||||||
|
def test_retrieve_update_api_view_patch(self):
|
||||||
|
class MockRetrieveUpdateApiView(generics.RetrieveUpdateAPIView):
|
||||||
|
def partial_update(self, request, *args, **kwargs):
|
||||||
|
self.called = True
|
||||||
|
self.call_args = (request, args, kwargs)
|
||||||
|
view = MockRetrieveUpdateApiView()
|
||||||
|
data = ('test request', ('test arg',), {'test_kwarg': 'test'})
|
||||||
|
view.patch('test request', 'test arg', test_kwarg='test')
|
||||||
|
assert view.called is True
|
||||||
|
assert view.call_args == data
|
||||||
|
|
||||||
|
def test_retrieve_destroy_api_view_get(self):
|
||||||
|
class MockRetrieveDestroyUApiView(generics.RetrieveDestroyAPIView):
|
||||||
|
def retrieve(self, request, *args, **kwargs):
|
||||||
|
self.called = True
|
||||||
|
self.call_args = (request, args, kwargs)
|
||||||
|
view = MockRetrieveDestroyUApiView()
|
||||||
|
data = ('test request', ('test arg',), {'test_kwarg': 'test'})
|
||||||
|
view.get('test request', 'test arg', test_kwarg='test')
|
||||||
|
assert view.called is True
|
||||||
|
assert view.call_args == data
|
||||||
|
|
||||||
|
def test_retrieve_destroy_api_view_delete(self):
|
||||||
|
class MockRetrieveDestroyUApiView(generics.RetrieveDestroyAPIView):
|
||||||
|
def destroy(self, request, *args, **kwargs):
|
||||||
|
self.called = True
|
||||||
|
self.call_args = (request, args, kwargs)
|
||||||
|
view = MockRetrieveDestroyUApiView()
|
||||||
|
data = ('test request', ('test arg',), {'test_kwarg': 'test'})
|
||||||
|
view.delete('test request', 'test arg', test_kwarg='test')
|
||||||
|
assert view.called is True
|
||||||
|
assert view.call_args == data
|
||||||
|
|
|
@ -1,8 +1,9 @@
|
||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
import django.template.loader
|
import django.template.loader
|
||||||
|
import pytest
|
||||||
from django.conf.urls import url
|
from django.conf.urls import url
|
||||||
from django.core.exceptions import PermissionDenied
|
from django.core.exceptions import ImproperlyConfigured, PermissionDenied
|
||||||
from django.http import Http404
|
from django.http import Http404
|
||||||
from django.template import Template, TemplateDoesNotExist
|
from django.template import Template, TemplateDoesNotExist
|
||||||
from django.test import TestCase, override_settings
|
from django.test import TestCase, override_settings
|
||||||
|
@ -46,6 +47,12 @@ urlpatterns = [
|
||||||
@override_settings(ROOT_URLCONF='tests.test_htmlrenderer')
|
@override_settings(ROOT_URLCONF='tests.test_htmlrenderer')
|
||||||
class TemplateHTMLRendererTests(TestCase):
|
class TemplateHTMLRendererTests(TestCase):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
|
class MockResponse(object):
|
||||||
|
template_name = None
|
||||||
|
self.mock_response = MockResponse()
|
||||||
|
self._monkey_patch_get_template()
|
||||||
|
|
||||||
|
def _monkey_patch_get_template(self):
|
||||||
"""
|
"""
|
||||||
Monkeypatch get_template
|
Monkeypatch get_template
|
||||||
"""
|
"""
|
||||||
|
@ -87,6 +94,40 @@ class TemplateHTMLRendererTests(TestCase):
|
||||||
self.assertEqual(response.content, six.b("403 Forbidden"))
|
self.assertEqual(response.content, six.b("403 Forbidden"))
|
||||||
self.assertEqual(response['Content-Type'], 'text/html; charset=utf-8')
|
self.assertEqual(response['Content-Type'], 'text/html; charset=utf-8')
|
||||||
|
|
||||||
|
# 2 tests below are based on order of if statements in corresponding method
|
||||||
|
# of TemplateHTMLRenderer
|
||||||
|
def test_get_template_names_returns_own_template_name(self):
|
||||||
|
renderer = TemplateHTMLRenderer()
|
||||||
|
renderer.template_name = 'test_template'
|
||||||
|
template_name = renderer.get_template_names(self.mock_response, view={})
|
||||||
|
assert template_name == ['test_template']
|
||||||
|
|
||||||
|
def test_get_template_names_returns_view_template_name(self):
|
||||||
|
renderer = TemplateHTMLRenderer()
|
||||||
|
|
||||||
|
class MockResponse(object):
|
||||||
|
template_name = None
|
||||||
|
|
||||||
|
class MockView(object):
|
||||||
|
def get_template_names(self):
|
||||||
|
return ['template from get_template_names method']
|
||||||
|
|
||||||
|
class MockView2(object):
|
||||||
|
template_name = 'template from template_name attribute'
|
||||||
|
|
||||||
|
template_name = renderer.get_template_names(self.mock_response,
|
||||||
|
MockView())
|
||||||
|
assert template_name == ['template from get_template_names method']
|
||||||
|
|
||||||
|
template_name = renderer.get_template_names(self.mock_response,
|
||||||
|
MockView2())
|
||||||
|
assert template_name == ['template from template_name attribute']
|
||||||
|
|
||||||
|
def test_get_template_names_raises_error_if_no_template_found(self):
|
||||||
|
renderer = TemplateHTMLRenderer()
|
||||||
|
with pytest.raises(ImproperlyConfigured):
|
||||||
|
renderer.get_template_names(self.mock_response, view=object())
|
||||||
|
|
||||||
|
|
||||||
@override_settings(ROOT_URLCONF='tests.test_htmlrenderer')
|
@override_settings(ROOT_URLCONF='tests.test_htmlrenderer')
|
||||||
class TemplateHTMLRendererExceptionTests(TestCase):
|
class TemplateHTMLRendererExceptionTests(TestCase):
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
import pytest
|
||||||
from django.core.validators import MaxValueValidator, MinValueValidator
|
from django.core.validators import MaxValueValidator, MinValueValidator
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.test import TestCase
|
from django.test import TestCase
|
||||||
|
@ -17,6 +18,11 @@ request = Request(APIRequestFactory().options('/'))
|
||||||
|
|
||||||
|
|
||||||
class TestMetadata:
|
class TestMetadata:
|
||||||
|
|
||||||
|
def test_determine_metadata_abstract_method_raises_proper_error(self):
|
||||||
|
with pytest.raises(NotImplementedError):
|
||||||
|
metadata.BaseMetadata().determine_metadata(None, None)
|
||||||
|
|
||||||
def test_metadata(self):
|
def test_metadata(self):
|
||||||
"""
|
"""
|
||||||
OPTIONS requests to views should return a valid 200 response.
|
OPTIONS requests to views should return a valid 200 response.
|
||||||
|
@ -263,12 +269,25 @@ class TestMetadata:
|
||||||
view = ExampleView.as_view(versioning_class=scheme)
|
view = ExampleView.as_view(versioning_class=scheme)
|
||||||
view(request=request)
|
view(request=request)
|
||||||
|
|
||||||
|
def test_list_serializer_metadata_returns_info_about_fields_of_child_serializer(self):
|
||||||
|
class ExampleSerializer(serializers.Serializer):
|
||||||
|
integer_field = serializers.IntegerField(max_value=10)
|
||||||
|
char_field = serializers.CharField(required=False)
|
||||||
|
|
||||||
|
class ExampleListSerializer(serializers.ListSerializer):
|
||||||
|
pass
|
||||||
|
|
||||||
|
options = metadata.SimpleMetadata()
|
||||||
|
child_serializer = ExampleSerializer()
|
||||||
|
list_serializer = ExampleListSerializer(child=child_serializer)
|
||||||
|
assert options.get_serializer_info(list_serializer) == options.get_serializer_info(child_serializer)
|
||||||
|
|
||||||
|
|
||||||
class TestSimpleMetadataFieldInfo(TestCase):
|
class TestSimpleMetadataFieldInfo(TestCase):
|
||||||
def test_null_boolean_field_info_type(self):
|
def test_null_boolean_field_info_type(self):
|
||||||
options = metadata.SimpleMetadata()
|
options = metadata.SimpleMetadata()
|
||||||
field_info = options.get_field_info(serializers.NullBooleanField())
|
field_info = options.get_field_info(serializers.NullBooleanField())
|
||||||
self.assertEqual(field_info['type'], 'boolean')
|
assert field_info['type'] == 'boolean'
|
||||||
|
|
||||||
def test_related_field_choices(self):
|
def test_related_field_choices(self):
|
||||||
options = metadata.SimpleMetadata()
|
options = metadata.SimpleMetadata()
|
||||||
|
@ -277,7 +296,7 @@ class TestSimpleMetadataFieldInfo(TestCase):
|
||||||
field_info = options.get_field_info(
|
field_info = options.get_field_info(
|
||||||
serializers.RelatedField(queryset=BasicModel.objects.all())
|
serializers.RelatedField(queryset=BasicModel.objects.all())
|
||||||
)
|
)
|
||||||
self.assertNotIn('choices', field_info)
|
assert 'choices' not in field_info
|
||||||
|
|
||||||
|
|
||||||
class TestModelSerializerMetadata(TestCase):
|
class TestModelSerializerMetadata(TestCase):
|
||||||
|
|
|
@ -1,11 +1,16 @@
|
||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
from django.http import Http404
|
||||||
from django.test import TestCase
|
from django.test import TestCase
|
||||||
|
|
||||||
from rest_framework.negotiation import DefaultContentNegotiation
|
from rest_framework.negotiation import (
|
||||||
|
BaseContentNegotiation, DefaultContentNegotiation
|
||||||
|
)
|
||||||
from rest_framework.renderers import BaseRenderer
|
from rest_framework.renderers import BaseRenderer
|
||||||
from rest_framework.request import Request
|
from rest_framework.request import Request
|
||||||
from rest_framework.test import APIRequestFactory
|
from rest_framework.test import APIRequestFactory
|
||||||
|
from rest_framework.utils.mediatypes import _MediaType
|
||||||
|
|
||||||
factory = APIRequestFactory()
|
factory = APIRequestFactory()
|
||||||
|
|
||||||
|
@ -55,3 +60,46 @@ class TestAcceptedMediaType(TestCase):
|
||||||
accepted_renderer, accepted_media_type = self.select_renderer(request)
|
accepted_renderer, accepted_media_type = self.select_renderer(request)
|
||||||
assert accepted_media_type == 'application/openapi+json;version=2.0'
|
assert accepted_media_type == 'application/openapi+json;version=2.0'
|
||||||
assert accepted_renderer.format == 'swagger'
|
assert accepted_renderer.format == 'swagger'
|
||||||
|
|
||||||
|
def test_match_is_false_if_main_types_not_match(self):
|
||||||
|
mediatype = _MediaType('test_1')
|
||||||
|
anoter_mediatype = _MediaType('test_2')
|
||||||
|
assert mediatype.match(anoter_mediatype) is False
|
||||||
|
|
||||||
|
def test_mediatype_match_is_false_if_keys_not_match(self):
|
||||||
|
mediatype = _MediaType(';test_param=foo')
|
||||||
|
another_mediatype = _MediaType(';test_param=bar')
|
||||||
|
assert mediatype.match(another_mediatype) is False
|
||||||
|
|
||||||
|
def test_mediatype_precedence_with_wildcard_subtype(self):
|
||||||
|
mediatype = _MediaType('test/*')
|
||||||
|
assert mediatype.precedence == 1
|
||||||
|
|
||||||
|
def test_mediatype_string_representation(self):
|
||||||
|
mediatype = _MediaType('test/*; foo=bar')
|
||||||
|
params_str = ''
|
||||||
|
for key, val in mediatype.params.items():
|
||||||
|
params_str += '; %s=%s' % (key, val)
|
||||||
|
expected = 'test/*' + params_str
|
||||||
|
assert str(mediatype) == expected
|
||||||
|
|
||||||
|
def test_raise_error_if_no_suitable_renderers_found(self):
|
||||||
|
class MockRenderer(object):
|
||||||
|
format = 'xml'
|
||||||
|
renderers = [MockRenderer()]
|
||||||
|
with pytest.raises(Http404):
|
||||||
|
self.negotiator.filter_renderers(renderers, format='json')
|
||||||
|
|
||||||
|
|
||||||
|
class BaseContentNegotiationTests(TestCase):
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
self.negotiator = BaseContentNegotiation()
|
||||||
|
|
||||||
|
def test_raise_error_for_abstract_select_parser_method(self):
|
||||||
|
with pytest.raises(NotImplementedError):
|
||||||
|
self.negotiator.select_parser(None, None)
|
||||||
|
|
||||||
|
def test_raise_error_for_abstract_select_renderer_method(self):
|
||||||
|
with pytest.raises(NotImplementedError):
|
||||||
|
self.negotiator.select_renderer(None, None)
|
||||||
|
|
|
@ -14,7 +14,7 @@ from tests.test_multitable_inheritance import ChildModel
|
||||||
# Regression test for #4290
|
# Regression test for #4290
|
||||||
|
|
||||||
class ChildAssociatedModel(RESTFrameworkModel):
|
class ChildAssociatedModel(RESTFrameworkModel):
|
||||||
child_model = models.OneToOneField(ChildModel)
|
child_model = models.OneToOneField(ChildModel, on_delete=models.CASCADE)
|
||||||
child_name = models.CharField(max_length=100)
|
child_name = models.CharField(max_length=100)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -370,6 +370,13 @@ class TestLimitOffset:
|
||||||
assert self.pagination.display_page_controls
|
assert self.pagination.display_page_controls
|
||||||
assert isinstance(self.pagination.to_html(), type(''))
|
assert isinstance(self.pagination.to_html(), type(''))
|
||||||
|
|
||||||
|
def test_pagination_not_applied_if_limit_or_default_limit_not_set(self):
|
||||||
|
class MockPagination(pagination.LimitOffsetPagination):
|
||||||
|
default_limit = None
|
||||||
|
request = Request(factory.get('/'))
|
||||||
|
queryset = MockPagination().paginate_queryset(self.queryset, request)
|
||||||
|
assert queryset is None
|
||||||
|
|
||||||
def test_single_offset(self):
|
def test_single_offset(self):
|
||||||
"""
|
"""
|
||||||
When the offset is not a multiple of the limit we get some edge cases:
|
When the offset is not a multiple of the limit we get some edge cases:
|
||||||
|
|
|
@ -35,7 +35,7 @@ class TestFormParser(TestCase):
|
||||||
stream = StringIO(self.string)
|
stream = StringIO(self.string)
|
||||||
data = parser.parse(stream)
|
data = parser.parse(stream)
|
||||||
|
|
||||||
self.assertEqual(Form(data).is_valid(), True)
|
assert Form(data).is_valid() is True
|
||||||
|
|
||||||
|
|
||||||
class TestFileUploadParser(TestCase):
|
class TestFileUploadParser(TestCase):
|
||||||
|
@ -62,7 +62,7 @@ class TestFileUploadParser(TestCase):
|
||||||
self.stream.seek(0)
|
self.stream.seek(0)
|
||||||
data_and_files = parser.parse(self.stream, None, self.parser_context)
|
data_and_files = parser.parse(self.stream, None, self.parser_context)
|
||||||
file_obj = data_and_files.files['file']
|
file_obj = data_and_files.files['file']
|
||||||
self.assertEqual(file_obj._size, 14)
|
assert file_obj._size == 14
|
||||||
|
|
||||||
def test_parse_missing_filename(self):
|
def test_parse_missing_filename(self):
|
||||||
"""
|
"""
|
||||||
|
@ -108,22 +108,22 @@ class TestFileUploadParser(TestCase):
|
||||||
def test_get_filename(self):
|
def test_get_filename(self):
|
||||||
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')
|
assert filename == 'file.txt'
|
||||||
|
|
||||||
def test_get_encoded_filename(self):
|
def test_get_encoded_filename(self):
|
||||||
parser = FileUploadParser()
|
parser = FileUploadParser()
|
||||||
|
|
||||||
self.__replace_content_disposition('inline; filename*=utf-8\'\'ÀĥƦ.txt')
|
self.__replace_content_disposition('inline; filename*=utf-8\'\'ÀĥƦ.txt')
|
||||||
filename = parser.get_filename(self.stream, None, self.parser_context)
|
filename = parser.get_filename(self.stream, None, self.parser_context)
|
||||||
self.assertEqual(filename, 'ÀĥƦ.txt')
|
assert filename == 'ÀĥƦ.txt'
|
||||||
|
|
||||||
self.__replace_content_disposition('inline; filename=fallback.txt; filename*=utf-8\'\'ÀĥƦ.txt')
|
self.__replace_content_disposition('inline; filename=fallback.txt; filename*=utf-8\'\'ÀĥƦ.txt')
|
||||||
filename = parser.get_filename(self.stream, None, self.parser_context)
|
filename = parser.get_filename(self.stream, None, self.parser_context)
|
||||||
self.assertEqual(filename, 'ÀĥƦ.txt')
|
assert filename == 'ÀĥƦ.txt'
|
||||||
|
|
||||||
self.__replace_content_disposition('inline; filename=fallback.txt; filename*=utf-8\'en-us\'ÀĥƦ.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)
|
filename = parser.get_filename(self.stream, None, self.parser_context)
|
||||||
self.assertEqual(filename, 'ÀĥƦ.txt')
|
assert filename == 'ÀĥƦ.txt'
|
||||||
|
|
||||||
def __replace_content_disposition(self, disposition):
|
def __replace_content_disposition(self, disposition):
|
||||||
self.parser_context['request'].META['HTTP_CONTENT_DISPOSITION'] = disposition
|
self.parser_context['request'].META['HTTP_CONTENT_DISPOSITION'] = disposition
|
||||||
|
|
|
@ -91,7 +91,7 @@ class HyperlinkedManyToManyTests(TestCase):
|
||||||
{'url': '/manytomanysource/3/', 'name': 'source-3', 'targets': ['/manytomanytarget/1/', '/manytomanytarget/2/', '/manytomanytarget/3/']}
|
{'url': '/manytomanysource/3/', 'name': 'source-3', 'targets': ['/manytomanytarget/1/', '/manytomanytarget/2/', '/manytomanytarget/3/']}
|
||||||
]
|
]
|
||||||
with self.assertNumQueries(4):
|
with self.assertNumQueries(4):
|
||||||
self.assertEqual(serializer.data, expected)
|
assert serializer.data == expected
|
||||||
|
|
||||||
def test_many_to_many_retrieve(self):
|
def test_many_to_many_retrieve(self):
|
||||||
queryset = ManyToManySource.objects.all()
|
queryset = ManyToManySource.objects.all()
|
||||||
|
@ -102,7 +102,7 @@ class HyperlinkedManyToManyTests(TestCase):
|
||||||
{'url': 'http://testserver/manytomanysource/3/', 'name': 'source-3', 'targets': ['http://testserver/manytomanytarget/1/', 'http://testserver/manytomanytarget/2/', 'http://testserver/manytomanytarget/3/']}
|
{'url': 'http://testserver/manytomanysource/3/', 'name': 'source-3', 'targets': ['http://testserver/manytomanytarget/1/', 'http://testserver/manytomanytarget/2/', 'http://testserver/manytomanytarget/3/']}
|
||||||
]
|
]
|
||||||
with self.assertNumQueries(4):
|
with self.assertNumQueries(4):
|
||||||
self.assertEqual(serializer.data, expected)
|
assert serializer.data == expected
|
||||||
|
|
||||||
def test_many_to_many_retrieve_prefetch_related(self):
|
def test_many_to_many_retrieve_prefetch_related(self):
|
||||||
queryset = ManyToManySource.objects.all().prefetch_related('targets')
|
queryset = ManyToManySource.objects.all().prefetch_related('targets')
|
||||||
|
@ -119,15 +119,15 @@ class HyperlinkedManyToManyTests(TestCase):
|
||||||
{'url': 'http://testserver/manytomanytarget/3/', 'name': 'target-3', 'sources': ['http://testserver/manytomanysource/3/']}
|
{'url': 'http://testserver/manytomanytarget/3/', 'name': 'target-3', 'sources': ['http://testserver/manytomanysource/3/']}
|
||||||
]
|
]
|
||||||
with self.assertNumQueries(4):
|
with self.assertNumQueries(4):
|
||||||
self.assertEqual(serializer.data, expected)
|
assert serializer.data == expected
|
||||||
|
|
||||||
def test_many_to_many_update(self):
|
def test_many_to_many_update(self):
|
||||||
data = {'url': 'http://testserver/manytomanysource/1/', 'name': 'source-1', 'targets': ['http://testserver/manytomanytarget/1/', 'http://testserver/manytomanytarget/2/', 'http://testserver/manytomanytarget/3/']}
|
data = {'url': 'http://testserver/manytomanysource/1/', 'name': 'source-1', 'targets': ['http://testserver/manytomanytarget/1/', 'http://testserver/manytomanytarget/2/', 'http://testserver/manytomanytarget/3/']}
|
||||||
instance = ManyToManySource.objects.get(pk=1)
|
instance = ManyToManySource.objects.get(pk=1)
|
||||||
serializer = ManyToManySourceSerializer(instance, data=data, context={'request': request})
|
serializer = ManyToManySourceSerializer(instance, data=data, context={'request': request})
|
||||||
self.assertTrue(serializer.is_valid())
|
assert serializer.is_valid()
|
||||||
serializer.save()
|
serializer.save()
|
||||||
self.assertEqual(serializer.data, data)
|
assert serializer.data == data
|
||||||
|
|
||||||
# Ensure source 1 is updated, and everything else is as expected
|
# Ensure source 1 is updated, and everything else is as expected
|
||||||
queryset = ManyToManySource.objects.all()
|
queryset = ManyToManySource.objects.all()
|
||||||
|
@ -137,16 +137,15 @@ class HyperlinkedManyToManyTests(TestCase):
|
||||||
{'url': 'http://testserver/manytomanysource/2/', 'name': 'source-2', 'targets': ['http://testserver/manytomanytarget/1/', 'http://testserver/manytomanytarget/2/']},
|
{'url': 'http://testserver/manytomanysource/2/', 'name': 'source-2', 'targets': ['http://testserver/manytomanytarget/1/', 'http://testserver/manytomanytarget/2/']},
|
||||||
{'url': 'http://testserver/manytomanysource/3/', 'name': 'source-3', 'targets': ['http://testserver/manytomanytarget/1/', 'http://testserver/manytomanytarget/2/', 'http://testserver/manytomanytarget/3/']}
|
{'url': 'http://testserver/manytomanysource/3/', 'name': 'source-3', 'targets': ['http://testserver/manytomanytarget/1/', 'http://testserver/manytomanytarget/2/', 'http://testserver/manytomanytarget/3/']}
|
||||||
]
|
]
|
||||||
self.assertEqual(serializer.data, expected)
|
assert serializer.data == expected
|
||||||
|
|
||||||
def test_reverse_many_to_many_update(self):
|
def test_reverse_many_to_many_update(self):
|
||||||
data = {'url': 'http://testserver/manytomanytarget/1/', 'name': 'target-1', 'sources': ['http://testserver/manytomanysource/1/']}
|
data = {'url': 'http://testserver/manytomanytarget/1/', 'name': 'target-1', 'sources': ['http://testserver/manytomanysource/1/']}
|
||||||
instance = ManyToManyTarget.objects.get(pk=1)
|
instance = ManyToManyTarget.objects.get(pk=1)
|
||||||
serializer = ManyToManyTargetSerializer(instance, data=data, context={'request': request})
|
serializer = ManyToManyTargetSerializer(instance, data=data, context={'request': request})
|
||||||
self.assertTrue(serializer.is_valid())
|
assert serializer.is_valid()
|
||||||
serializer.save()
|
serializer.save()
|
||||||
self.assertEqual(serializer.data, data)
|
assert serializer.data == data
|
||||||
|
|
||||||
# Ensure target 1 is updated, and everything else is as expected
|
# Ensure target 1 is updated, and everything else is as expected
|
||||||
queryset = ManyToManyTarget.objects.all()
|
queryset = ManyToManyTarget.objects.all()
|
||||||
serializer = ManyToManyTargetSerializer(queryset, many=True, context={'request': request})
|
serializer = ManyToManyTargetSerializer(queryset, many=True, context={'request': request})
|
||||||
|
@ -156,15 +155,15 @@ class HyperlinkedManyToManyTests(TestCase):
|
||||||
{'url': 'http://testserver/manytomanytarget/3/', 'name': 'target-3', 'sources': ['http://testserver/manytomanysource/3/']}
|
{'url': 'http://testserver/manytomanytarget/3/', 'name': 'target-3', 'sources': ['http://testserver/manytomanysource/3/']}
|
||||||
|
|
||||||
]
|
]
|
||||||
self.assertEqual(serializer.data, expected)
|
assert serializer.data == expected
|
||||||
|
|
||||||
def test_many_to_many_create(self):
|
def test_many_to_many_create(self):
|
||||||
data = {'url': 'http://testserver/manytomanysource/4/', 'name': 'source-4', 'targets': ['http://testserver/manytomanytarget/1/', 'http://testserver/manytomanytarget/3/']}
|
data = {'url': 'http://testserver/manytomanysource/4/', 'name': 'source-4', 'targets': ['http://testserver/manytomanytarget/1/', 'http://testserver/manytomanytarget/3/']}
|
||||||
serializer = ManyToManySourceSerializer(data=data, context={'request': request})
|
serializer = ManyToManySourceSerializer(data=data, context={'request': request})
|
||||||
self.assertTrue(serializer.is_valid())
|
assert serializer.is_valid()
|
||||||
obj = serializer.save()
|
obj = serializer.save()
|
||||||
self.assertEqual(serializer.data, data)
|
assert serializer.data == data
|
||||||
self.assertEqual(obj.name, 'source-4')
|
assert obj.name == 'source-4'
|
||||||
|
|
||||||
# Ensure source 4 is added, and everything else is as expected
|
# Ensure source 4 is added, and everything else is as expected
|
||||||
queryset = ManyToManySource.objects.all()
|
queryset = ManyToManySource.objects.all()
|
||||||
|
@ -175,15 +174,15 @@ class HyperlinkedManyToManyTests(TestCase):
|
||||||
{'url': 'http://testserver/manytomanysource/3/', 'name': 'source-3', 'targets': ['http://testserver/manytomanytarget/1/', 'http://testserver/manytomanytarget/2/', 'http://testserver/manytomanytarget/3/']},
|
{'url': 'http://testserver/manytomanysource/3/', 'name': 'source-3', 'targets': ['http://testserver/manytomanytarget/1/', 'http://testserver/manytomanytarget/2/', 'http://testserver/manytomanytarget/3/']},
|
||||||
{'url': 'http://testserver/manytomanysource/4/', 'name': 'source-4', 'targets': ['http://testserver/manytomanytarget/1/', 'http://testserver/manytomanytarget/3/']}
|
{'url': 'http://testserver/manytomanysource/4/', 'name': 'source-4', 'targets': ['http://testserver/manytomanytarget/1/', 'http://testserver/manytomanytarget/3/']}
|
||||||
]
|
]
|
||||||
self.assertEqual(serializer.data, expected)
|
assert serializer.data == expected
|
||||||
|
|
||||||
def test_reverse_many_to_many_create(self):
|
def test_reverse_many_to_many_create(self):
|
||||||
data = {'url': 'http://testserver/manytomanytarget/4/', 'name': 'target-4', 'sources': ['http://testserver/manytomanysource/1/', 'http://testserver/manytomanysource/3/']}
|
data = {'url': 'http://testserver/manytomanytarget/4/', 'name': 'target-4', 'sources': ['http://testserver/manytomanysource/1/', 'http://testserver/manytomanysource/3/']}
|
||||||
serializer = ManyToManyTargetSerializer(data=data, context={'request': request})
|
serializer = ManyToManyTargetSerializer(data=data, context={'request': request})
|
||||||
self.assertTrue(serializer.is_valid())
|
assert serializer.is_valid()
|
||||||
obj = serializer.save()
|
obj = serializer.save()
|
||||||
self.assertEqual(serializer.data, data)
|
assert serializer.data == data
|
||||||
self.assertEqual(obj.name, 'target-4')
|
assert obj.name == 'target-4'
|
||||||
|
|
||||||
# Ensure target 4 is added, and everything else is as expected
|
# Ensure target 4 is added, and everything else is as expected
|
||||||
queryset = ManyToManyTarget.objects.all()
|
queryset = ManyToManyTarget.objects.all()
|
||||||
|
@ -194,7 +193,7 @@ class HyperlinkedManyToManyTests(TestCase):
|
||||||
{'url': 'http://testserver/manytomanytarget/3/', 'name': 'target-3', 'sources': ['http://testserver/manytomanysource/3/']},
|
{'url': 'http://testserver/manytomanytarget/3/', 'name': 'target-3', 'sources': ['http://testserver/manytomanysource/3/']},
|
||||||
{'url': 'http://testserver/manytomanytarget/4/', 'name': 'target-4', 'sources': ['http://testserver/manytomanysource/1/', 'http://testserver/manytomanysource/3/']}
|
{'url': 'http://testserver/manytomanytarget/4/', 'name': 'target-4', 'sources': ['http://testserver/manytomanysource/1/', 'http://testserver/manytomanysource/3/']}
|
||||||
]
|
]
|
||||||
self.assertEqual(serializer.data, expected)
|
assert serializer.data == expected
|
||||||
|
|
||||||
|
|
||||||
@override_settings(ROOT_URLCONF='tests.test_relations_hyperlink')
|
@override_settings(ROOT_URLCONF='tests.test_relations_hyperlink')
|
||||||
|
@ -217,7 +216,7 @@ class HyperlinkedForeignKeyTests(TestCase):
|
||||||
{'url': 'http://testserver/foreignkeysource/3/', 'name': 'source-3', 'target': 'http://testserver/foreignkeytarget/1/'}
|
{'url': 'http://testserver/foreignkeysource/3/', 'name': 'source-3', 'target': 'http://testserver/foreignkeytarget/1/'}
|
||||||
]
|
]
|
||||||
with self.assertNumQueries(1):
|
with self.assertNumQueries(1):
|
||||||
self.assertEqual(serializer.data, expected)
|
assert serializer.data == expected
|
||||||
|
|
||||||
def test_reverse_foreign_key_retrieve(self):
|
def test_reverse_foreign_key_retrieve(self):
|
||||||
queryset = ForeignKeyTarget.objects.all()
|
queryset = ForeignKeyTarget.objects.all()
|
||||||
|
@ -227,15 +226,15 @@ class HyperlinkedForeignKeyTests(TestCase):
|
||||||
{'url': 'http://testserver/foreignkeytarget/2/', 'name': 'target-2', 'sources': []},
|
{'url': 'http://testserver/foreignkeytarget/2/', 'name': 'target-2', 'sources': []},
|
||||||
]
|
]
|
||||||
with self.assertNumQueries(3):
|
with self.assertNumQueries(3):
|
||||||
self.assertEqual(serializer.data, expected)
|
assert serializer.data == expected
|
||||||
|
|
||||||
def test_foreign_key_update(self):
|
def test_foreign_key_update(self):
|
||||||
data = {'url': 'http://testserver/foreignkeysource/1/', 'name': 'source-1', 'target': 'http://testserver/foreignkeytarget/2/'}
|
data = {'url': 'http://testserver/foreignkeysource/1/', 'name': 'source-1', 'target': 'http://testserver/foreignkeytarget/2/'}
|
||||||
instance = ForeignKeySource.objects.get(pk=1)
|
instance = ForeignKeySource.objects.get(pk=1)
|
||||||
serializer = ForeignKeySourceSerializer(instance, data=data, context={'request': request})
|
serializer = ForeignKeySourceSerializer(instance, data=data, context={'request': request})
|
||||||
self.assertTrue(serializer.is_valid())
|
assert serializer.is_valid()
|
||||||
serializer.save()
|
serializer.save()
|
||||||
self.assertEqual(serializer.data, data)
|
assert serializer.data == data
|
||||||
|
|
||||||
# Ensure source 1 is updated, and everything else is as expected
|
# Ensure source 1 is updated, and everything else is as expected
|
||||||
queryset = ForeignKeySource.objects.all()
|
queryset = ForeignKeySource.objects.all()
|
||||||
|
@ -245,20 +244,20 @@ class HyperlinkedForeignKeyTests(TestCase):
|
||||||
{'url': 'http://testserver/foreignkeysource/2/', 'name': 'source-2', 'target': 'http://testserver/foreignkeytarget/1/'},
|
{'url': 'http://testserver/foreignkeysource/2/', 'name': 'source-2', 'target': 'http://testserver/foreignkeytarget/1/'},
|
||||||
{'url': 'http://testserver/foreignkeysource/3/', 'name': 'source-3', 'target': 'http://testserver/foreignkeytarget/1/'}
|
{'url': 'http://testserver/foreignkeysource/3/', 'name': 'source-3', 'target': 'http://testserver/foreignkeytarget/1/'}
|
||||||
]
|
]
|
||||||
self.assertEqual(serializer.data, expected)
|
assert serializer.data == expected
|
||||||
|
|
||||||
def test_foreign_key_update_incorrect_type(self):
|
def test_foreign_key_update_incorrect_type(self):
|
||||||
data = {'url': 'http://testserver/foreignkeysource/1/', 'name': 'source-1', 'target': 2}
|
data = {'url': 'http://testserver/foreignkeysource/1/', 'name': 'source-1', 'target': 2}
|
||||||
instance = ForeignKeySource.objects.get(pk=1)
|
instance = ForeignKeySource.objects.get(pk=1)
|
||||||
serializer = ForeignKeySourceSerializer(instance, data=data, context={'request': request})
|
serializer = ForeignKeySourceSerializer(instance, data=data, context={'request': request})
|
||||||
self.assertFalse(serializer.is_valid())
|
assert not serializer.is_valid()
|
||||||
self.assertEqual(serializer.errors, {'target': ['Incorrect type. Expected URL string, received int.']})
|
assert serializer.errors == {'target': ['Incorrect type. Expected URL string, received int.']}
|
||||||
|
|
||||||
def test_reverse_foreign_key_update(self):
|
def test_reverse_foreign_key_update(self):
|
||||||
data = {'url': 'http://testserver/foreignkeytarget/2/', 'name': 'target-2', 'sources': ['http://testserver/foreignkeysource/1/', 'http://testserver/foreignkeysource/3/']}
|
data = {'url': 'http://testserver/foreignkeytarget/2/', 'name': 'target-2', 'sources': ['http://testserver/foreignkeysource/1/', 'http://testserver/foreignkeysource/3/']}
|
||||||
instance = ForeignKeyTarget.objects.get(pk=2)
|
instance = ForeignKeyTarget.objects.get(pk=2)
|
||||||
serializer = ForeignKeyTargetSerializer(instance, data=data, context={'request': request})
|
serializer = ForeignKeyTargetSerializer(instance, data=data, context={'request': request})
|
||||||
self.assertTrue(serializer.is_valid())
|
assert serializer.is_valid()
|
||||||
# We shouldn't have saved anything to the db yet since save
|
# We shouldn't have saved anything to the db yet since save
|
||||||
# hasn't been called.
|
# hasn't been called.
|
||||||
queryset = ForeignKeyTarget.objects.all()
|
queryset = ForeignKeyTarget.objects.all()
|
||||||
|
@ -267,10 +266,10 @@ class HyperlinkedForeignKeyTests(TestCase):
|
||||||
{'url': 'http://testserver/foreignkeytarget/1/', 'name': 'target-1', 'sources': ['http://testserver/foreignkeysource/1/', 'http://testserver/foreignkeysource/2/', 'http://testserver/foreignkeysource/3/']},
|
{'url': 'http://testserver/foreignkeytarget/1/', 'name': 'target-1', 'sources': ['http://testserver/foreignkeysource/1/', 'http://testserver/foreignkeysource/2/', 'http://testserver/foreignkeysource/3/']},
|
||||||
{'url': 'http://testserver/foreignkeytarget/2/', 'name': 'target-2', 'sources': []},
|
{'url': 'http://testserver/foreignkeytarget/2/', 'name': 'target-2', 'sources': []},
|
||||||
]
|
]
|
||||||
self.assertEqual(new_serializer.data, expected)
|
assert new_serializer.data == expected
|
||||||
|
|
||||||
serializer.save()
|
serializer.save()
|
||||||
self.assertEqual(serializer.data, data)
|
assert serializer.data == data
|
||||||
|
|
||||||
# Ensure target 2 is update, and everything else is as expected
|
# Ensure target 2 is update, and everything else is as expected
|
||||||
queryset = ForeignKeyTarget.objects.all()
|
queryset = ForeignKeyTarget.objects.all()
|
||||||
|
@ -279,15 +278,15 @@ class HyperlinkedForeignKeyTests(TestCase):
|
||||||
{'url': 'http://testserver/foreignkeytarget/1/', 'name': 'target-1', 'sources': ['http://testserver/foreignkeysource/2/']},
|
{'url': 'http://testserver/foreignkeytarget/1/', 'name': 'target-1', 'sources': ['http://testserver/foreignkeysource/2/']},
|
||||||
{'url': 'http://testserver/foreignkeytarget/2/', 'name': 'target-2', 'sources': ['http://testserver/foreignkeysource/1/', 'http://testserver/foreignkeysource/3/']},
|
{'url': 'http://testserver/foreignkeytarget/2/', 'name': 'target-2', 'sources': ['http://testserver/foreignkeysource/1/', 'http://testserver/foreignkeysource/3/']},
|
||||||
]
|
]
|
||||||
self.assertEqual(serializer.data, expected)
|
assert serializer.data == expected
|
||||||
|
|
||||||
def test_foreign_key_create(self):
|
def test_foreign_key_create(self):
|
||||||
data = {'url': 'http://testserver/foreignkeysource/4/', 'name': 'source-4', 'target': 'http://testserver/foreignkeytarget/2/'}
|
data = {'url': 'http://testserver/foreignkeysource/4/', 'name': 'source-4', 'target': 'http://testserver/foreignkeytarget/2/'}
|
||||||
serializer = ForeignKeySourceSerializer(data=data, context={'request': request})
|
serializer = ForeignKeySourceSerializer(data=data, context={'request': request})
|
||||||
self.assertTrue(serializer.is_valid())
|
assert serializer.is_valid()
|
||||||
obj = serializer.save()
|
obj = serializer.save()
|
||||||
self.assertEqual(serializer.data, data)
|
assert serializer.data == data
|
||||||
self.assertEqual(obj.name, 'source-4')
|
assert obj.name == 'source-4'
|
||||||
|
|
||||||
# Ensure source 1 is updated, and everything else is as expected
|
# Ensure source 1 is updated, and everything else is as expected
|
||||||
queryset = ForeignKeySource.objects.all()
|
queryset = ForeignKeySource.objects.all()
|
||||||
|
@ -298,15 +297,15 @@ class HyperlinkedForeignKeyTests(TestCase):
|
||||||
{'url': 'http://testserver/foreignkeysource/3/', 'name': 'source-3', 'target': 'http://testserver/foreignkeytarget/1/'},
|
{'url': 'http://testserver/foreignkeysource/3/', 'name': 'source-3', 'target': 'http://testserver/foreignkeytarget/1/'},
|
||||||
{'url': 'http://testserver/foreignkeysource/4/', 'name': 'source-4', 'target': 'http://testserver/foreignkeytarget/2/'},
|
{'url': 'http://testserver/foreignkeysource/4/', 'name': 'source-4', 'target': 'http://testserver/foreignkeytarget/2/'},
|
||||||
]
|
]
|
||||||
self.assertEqual(serializer.data, expected)
|
assert serializer.data == expected
|
||||||
|
|
||||||
def test_reverse_foreign_key_create(self):
|
def test_reverse_foreign_key_create(self):
|
||||||
data = {'url': 'http://testserver/foreignkeytarget/3/', 'name': 'target-3', 'sources': ['http://testserver/foreignkeysource/1/', 'http://testserver/foreignkeysource/3/']}
|
data = {'url': 'http://testserver/foreignkeytarget/3/', 'name': 'target-3', 'sources': ['http://testserver/foreignkeysource/1/', 'http://testserver/foreignkeysource/3/']}
|
||||||
serializer = ForeignKeyTargetSerializer(data=data, context={'request': request})
|
serializer = ForeignKeyTargetSerializer(data=data, context={'request': request})
|
||||||
self.assertTrue(serializer.is_valid())
|
assert serializer.is_valid()
|
||||||
obj = serializer.save()
|
obj = serializer.save()
|
||||||
self.assertEqual(serializer.data, data)
|
assert serializer.data == data
|
||||||
self.assertEqual(obj.name, 'target-3')
|
assert obj.name == 'target-3'
|
||||||
|
|
||||||
# Ensure target 4 is added, and everything else is as expected
|
# Ensure target 4 is added, and everything else is as expected
|
||||||
queryset = ForeignKeyTarget.objects.all()
|
queryset = ForeignKeyTarget.objects.all()
|
||||||
|
@ -316,14 +315,14 @@ class HyperlinkedForeignKeyTests(TestCase):
|
||||||
{'url': 'http://testserver/foreignkeytarget/2/', 'name': 'target-2', 'sources': []},
|
{'url': 'http://testserver/foreignkeytarget/2/', 'name': 'target-2', 'sources': []},
|
||||||
{'url': 'http://testserver/foreignkeytarget/3/', 'name': 'target-3', 'sources': ['http://testserver/foreignkeysource/1/', 'http://testserver/foreignkeysource/3/']},
|
{'url': 'http://testserver/foreignkeytarget/3/', 'name': 'target-3', 'sources': ['http://testserver/foreignkeysource/1/', 'http://testserver/foreignkeysource/3/']},
|
||||||
]
|
]
|
||||||
self.assertEqual(serializer.data, expected)
|
assert serializer.data == expected
|
||||||
|
|
||||||
def test_foreign_key_update_with_invalid_null(self):
|
def test_foreign_key_update_with_invalid_null(self):
|
||||||
data = {'url': 'http://testserver/foreignkeysource/1/', 'name': 'source-1', 'target': None}
|
data = {'url': 'http://testserver/foreignkeysource/1/', 'name': 'source-1', 'target': None}
|
||||||
instance = ForeignKeySource.objects.get(pk=1)
|
instance = ForeignKeySource.objects.get(pk=1)
|
||||||
serializer = ForeignKeySourceSerializer(instance, data=data, context={'request': request})
|
serializer = ForeignKeySourceSerializer(instance, data=data, context={'request': request})
|
||||||
self.assertFalse(serializer.is_valid())
|
assert not serializer.is_valid()
|
||||||
self.assertEqual(serializer.errors, {'target': ['This field may not be null.']})
|
assert serializer.errors == {'target': ['This field may not be null.']}
|
||||||
|
|
||||||
|
|
||||||
@override_settings(ROOT_URLCONF='tests.test_relations_hyperlink')
|
@override_settings(ROOT_URLCONF='tests.test_relations_hyperlink')
|
||||||
|
@ -345,15 +344,15 @@ class HyperlinkedNullableForeignKeyTests(TestCase):
|
||||||
{'url': 'http://testserver/nullableforeignkeysource/2/', 'name': 'source-2', 'target': 'http://testserver/foreignkeytarget/1/'},
|
{'url': 'http://testserver/nullableforeignkeysource/2/', 'name': 'source-2', 'target': 'http://testserver/foreignkeytarget/1/'},
|
||||||
{'url': 'http://testserver/nullableforeignkeysource/3/', 'name': 'source-3', 'target': None},
|
{'url': 'http://testserver/nullableforeignkeysource/3/', 'name': 'source-3', 'target': None},
|
||||||
]
|
]
|
||||||
self.assertEqual(serializer.data, expected)
|
assert serializer.data == expected
|
||||||
|
|
||||||
def test_foreign_key_create_with_valid_null(self):
|
def test_foreign_key_create_with_valid_null(self):
|
||||||
data = {'url': 'http://testserver/nullableforeignkeysource/4/', 'name': 'source-4', 'target': None}
|
data = {'url': 'http://testserver/nullableforeignkeysource/4/', 'name': 'source-4', 'target': None}
|
||||||
serializer = NullableForeignKeySourceSerializer(data=data, context={'request': request})
|
serializer = NullableForeignKeySourceSerializer(data=data, context={'request': request})
|
||||||
self.assertTrue(serializer.is_valid())
|
assert serializer.is_valid()
|
||||||
obj = serializer.save()
|
obj = serializer.save()
|
||||||
self.assertEqual(serializer.data, data)
|
assert serializer.data == data
|
||||||
self.assertEqual(obj.name, 'source-4')
|
assert obj.name == 'source-4'
|
||||||
|
|
||||||
# Ensure source 4 is created, and everything else is as expected
|
# Ensure source 4 is created, and everything else is as expected
|
||||||
queryset = NullableForeignKeySource.objects.all()
|
queryset = NullableForeignKeySource.objects.all()
|
||||||
|
@ -364,7 +363,7 @@ class HyperlinkedNullableForeignKeyTests(TestCase):
|
||||||
{'url': 'http://testserver/nullableforeignkeysource/3/', 'name': 'source-3', 'target': None},
|
{'url': 'http://testserver/nullableforeignkeysource/3/', 'name': 'source-3', 'target': None},
|
||||||
{'url': 'http://testserver/nullableforeignkeysource/4/', 'name': 'source-4', 'target': None}
|
{'url': 'http://testserver/nullableforeignkeysource/4/', 'name': 'source-4', 'target': None}
|
||||||
]
|
]
|
||||||
self.assertEqual(serializer.data, expected)
|
assert serializer.data == expected
|
||||||
|
|
||||||
def test_foreign_key_create_with_valid_emptystring(self):
|
def test_foreign_key_create_with_valid_emptystring(self):
|
||||||
"""
|
"""
|
||||||
|
@ -374,10 +373,10 @@ class HyperlinkedNullableForeignKeyTests(TestCase):
|
||||||
data = {'url': 'http://testserver/nullableforeignkeysource/4/', 'name': 'source-4', 'target': ''}
|
data = {'url': 'http://testserver/nullableforeignkeysource/4/', 'name': 'source-4', 'target': ''}
|
||||||
expected_data = {'url': 'http://testserver/nullableforeignkeysource/4/', 'name': 'source-4', 'target': None}
|
expected_data = {'url': 'http://testserver/nullableforeignkeysource/4/', 'name': 'source-4', 'target': None}
|
||||||
serializer = NullableForeignKeySourceSerializer(data=data, context={'request': request})
|
serializer = NullableForeignKeySourceSerializer(data=data, context={'request': request})
|
||||||
self.assertTrue(serializer.is_valid())
|
assert serializer.is_valid()
|
||||||
obj = serializer.save()
|
obj = serializer.save()
|
||||||
self.assertEqual(serializer.data, expected_data)
|
assert serializer.data == expected_data
|
||||||
self.assertEqual(obj.name, 'source-4')
|
assert obj.name == 'source-4'
|
||||||
|
|
||||||
# Ensure source 4 is created, and everything else is as expected
|
# Ensure source 4 is created, and everything else is as expected
|
||||||
queryset = NullableForeignKeySource.objects.all()
|
queryset = NullableForeignKeySource.objects.all()
|
||||||
|
@ -388,15 +387,15 @@ class HyperlinkedNullableForeignKeyTests(TestCase):
|
||||||
{'url': 'http://testserver/nullableforeignkeysource/3/', 'name': 'source-3', 'target': None},
|
{'url': 'http://testserver/nullableforeignkeysource/3/', 'name': 'source-3', 'target': None},
|
||||||
{'url': 'http://testserver/nullableforeignkeysource/4/', 'name': 'source-4', 'target': None}
|
{'url': 'http://testserver/nullableforeignkeysource/4/', 'name': 'source-4', 'target': None}
|
||||||
]
|
]
|
||||||
self.assertEqual(serializer.data, expected)
|
assert serializer.data == expected
|
||||||
|
|
||||||
def test_foreign_key_update_with_valid_null(self):
|
def test_foreign_key_update_with_valid_null(self):
|
||||||
data = {'url': 'http://testserver/nullableforeignkeysource/1/', 'name': 'source-1', 'target': None}
|
data = {'url': 'http://testserver/nullableforeignkeysource/1/', 'name': 'source-1', 'target': None}
|
||||||
instance = NullableForeignKeySource.objects.get(pk=1)
|
instance = NullableForeignKeySource.objects.get(pk=1)
|
||||||
serializer = NullableForeignKeySourceSerializer(instance, data=data, context={'request': request})
|
serializer = NullableForeignKeySourceSerializer(instance, data=data, context={'request': request})
|
||||||
self.assertTrue(serializer.is_valid())
|
assert serializer.is_valid()
|
||||||
serializer.save()
|
serializer.save()
|
||||||
self.assertEqual(serializer.data, data)
|
assert serializer.data == data
|
||||||
|
|
||||||
# Ensure source 1 is updated, and everything else is as expected
|
# Ensure source 1 is updated, and everything else is as expected
|
||||||
queryset = NullableForeignKeySource.objects.all()
|
queryset = NullableForeignKeySource.objects.all()
|
||||||
|
@ -406,7 +405,7 @@ class HyperlinkedNullableForeignKeyTests(TestCase):
|
||||||
{'url': 'http://testserver/nullableforeignkeysource/2/', 'name': 'source-2', 'target': 'http://testserver/foreignkeytarget/1/'},
|
{'url': 'http://testserver/nullableforeignkeysource/2/', 'name': 'source-2', 'target': 'http://testserver/foreignkeytarget/1/'},
|
||||||
{'url': 'http://testserver/nullableforeignkeysource/3/', 'name': 'source-3', 'target': None},
|
{'url': 'http://testserver/nullableforeignkeysource/3/', 'name': 'source-3', 'target': None},
|
||||||
]
|
]
|
||||||
self.assertEqual(serializer.data, expected)
|
assert serializer.data == expected
|
||||||
|
|
||||||
def test_foreign_key_update_with_valid_emptystring(self):
|
def test_foreign_key_update_with_valid_emptystring(self):
|
||||||
"""
|
"""
|
||||||
|
@ -417,9 +416,9 @@ class HyperlinkedNullableForeignKeyTests(TestCase):
|
||||||
expected_data = {'url': 'http://testserver/nullableforeignkeysource/1/', 'name': 'source-1', 'target': None}
|
expected_data = {'url': 'http://testserver/nullableforeignkeysource/1/', 'name': 'source-1', 'target': None}
|
||||||
instance = NullableForeignKeySource.objects.get(pk=1)
|
instance = NullableForeignKeySource.objects.get(pk=1)
|
||||||
serializer = NullableForeignKeySourceSerializer(instance, data=data, context={'request': request})
|
serializer = NullableForeignKeySourceSerializer(instance, data=data, context={'request': request})
|
||||||
self.assertTrue(serializer.is_valid())
|
assert serializer.is_valid()
|
||||||
serializer.save()
|
serializer.save()
|
||||||
self.assertEqual(serializer.data, expected_data)
|
assert serializer.data == expected_data
|
||||||
|
|
||||||
# Ensure source 1 is updated, and everything else is as expected
|
# Ensure source 1 is updated, and everything else is as expected
|
||||||
queryset = NullableForeignKeySource.objects.all()
|
queryset = NullableForeignKeySource.objects.all()
|
||||||
|
@ -429,7 +428,7 @@ class HyperlinkedNullableForeignKeyTests(TestCase):
|
||||||
{'url': 'http://testserver/nullableforeignkeysource/2/', 'name': 'source-2', 'target': 'http://testserver/foreignkeytarget/1/'},
|
{'url': 'http://testserver/nullableforeignkeysource/2/', 'name': 'source-2', 'target': 'http://testserver/foreignkeytarget/1/'},
|
||||||
{'url': 'http://testserver/nullableforeignkeysource/3/', 'name': 'source-3', 'target': None},
|
{'url': 'http://testserver/nullableforeignkeysource/3/', 'name': 'source-3', 'target': None},
|
||||||
]
|
]
|
||||||
self.assertEqual(serializer.data, expected)
|
assert serializer.data == expected
|
||||||
|
|
||||||
|
|
||||||
@override_settings(ROOT_URLCONF='tests.test_relations_hyperlink')
|
@override_settings(ROOT_URLCONF='tests.test_relations_hyperlink')
|
||||||
|
@ -449,4 +448,4 @@ class HyperlinkedNullableOneToOneTests(TestCase):
|
||||||
{'url': 'http://testserver/onetoonetarget/1/', 'name': 'target-1', 'nullable_source': 'http://testserver/nullableonetoonesource/1/'},
|
{'url': 'http://testserver/onetoonetarget/1/', 'name': 'target-1', 'nullable_source': 'http://testserver/nullableonetoonesource/1/'},
|
||||||
{'url': 'http://testserver/onetoonetarget/2/', 'name': 'target-2', 'nullable_source': None},
|
{'url': 'http://testserver/onetoonetarget/2/', 'name': 'target-2', 'nullable_source': None},
|
||||||
]
|
]
|
||||||
self.assertEqual(serializer.data, expected)
|
assert serializer.data == expected
|
||||||
|
|
|
@ -61,7 +61,7 @@ class SlugForeignKeyTests(TestCase):
|
||||||
{'id': 3, 'name': 'source-3', 'target': 'target-1'}
|
{'id': 3, 'name': 'source-3', 'target': 'target-1'}
|
||||||
]
|
]
|
||||||
with self.assertNumQueries(4):
|
with self.assertNumQueries(4):
|
||||||
self.assertEqual(serializer.data, expected)
|
assert serializer.data == expected
|
||||||
|
|
||||||
def test_foreign_key_retrieve_select_related(self):
|
def test_foreign_key_retrieve_select_related(self):
|
||||||
queryset = ForeignKeySource.objects.all().select_related('target')
|
queryset = ForeignKeySource.objects.all().select_related('target')
|
||||||
|
@ -76,7 +76,7 @@ class SlugForeignKeyTests(TestCase):
|
||||||
{'id': 1, 'name': 'target-1', 'sources': ['source-1', 'source-2', 'source-3']},
|
{'id': 1, 'name': 'target-1', 'sources': ['source-1', 'source-2', 'source-3']},
|
||||||
{'id': 2, 'name': 'target-2', 'sources': []},
|
{'id': 2, 'name': 'target-2', 'sources': []},
|
||||||
]
|
]
|
||||||
self.assertEqual(serializer.data, expected)
|
assert serializer.data == expected
|
||||||
|
|
||||||
def test_reverse_foreign_key_retrieve_prefetch_related(self):
|
def test_reverse_foreign_key_retrieve_prefetch_related(self):
|
||||||
queryset = ForeignKeyTarget.objects.all().prefetch_related('sources')
|
queryset = ForeignKeyTarget.objects.all().prefetch_related('sources')
|
||||||
|
@ -88,9 +88,9 @@ class SlugForeignKeyTests(TestCase):
|
||||||
data = {'id': 1, 'name': 'source-1', 'target': 'target-2'}
|
data = {'id': 1, 'name': 'source-1', 'target': 'target-2'}
|
||||||
instance = ForeignKeySource.objects.get(pk=1)
|
instance = ForeignKeySource.objects.get(pk=1)
|
||||||
serializer = ForeignKeySourceSerializer(instance, data=data)
|
serializer = ForeignKeySourceSerializer(instance, data=data)
|
||||||
self.assertTrue(serializer.is_valid())
|
assert serializer.is_valid()
|
||||||
serializer.save()
|
serializer.save()
|
||||||
self.assertEqual(serializer.data, data)
|
assert serializer.data == data
|
||||||
|
|
||||||
# Ensure source 1 is updated, and everything else is as expected
|
# Ensure source 1 is updated, and everything else is as expected
|
||||||
queryset = ForeignKeySource.objects.all()
|
queryset = ForeignKeySource.objects.all()
|
||||||
|
@ -100,20 +100,20 @@ class SlugForeignKeyTests(TestCase):
|
||||||
{'id': 2, 'name': 'source-2', 'target': 'target-1'},
|
{'id': 2, 'name': 'source-2', 'target': 'target-1'},
|
||||||
{'id': 3, 'name': 'source-3', 'target': 'target-1'}
|
{'id': 3, 'name': 'source-3', 'target': 'target-1'}
|
||||||
]
|
]
|
||||||
self.assertEqual(serializer.data, expected)
|
assert serializer.data == expected
|
||||||
|
|
||||||
def test_foreign_key_update_incorrect_type(self):
|
def test_foreign_key_update_incorrect_type(self):
|
||||||
data = {'id': 1, 'name': 'source-1', 'target': 123}
|
data = {'id': 1, 'name': 'source-1', 'target': 123}
|
||||||
instance = ForeignKeySource.objects.get(pk=1)
|
instance = ForeignKeySource.objects.get(pk=1)
|
||||||
serializer = ForeignKeySourceSerializer(instance, data=data)
|
serializer = ForeignKeySourceSerializer(instance, data=data)
|
||||||
self.assertFalse(serializer.is_valid())
|
assert not serializer.is_valid()
|
||||||
self.assertEqual(serializer.errors, {'target': ['Object with name=123 does not exist.']})
|
assert serializer.errors == {'target': ['Object with name=123 does not exist.']}
|
||||||
|
|
||||||
def test_reverse_foreign_key_update(self):
|
def test_reverse_foreign_key_update(self):
|
||||||
data = {'id': 2, 'name': 'target-2', 'sources': ['source-1', 'source-3']}
|
data = {'id': 2, 'name': 'target-2', 'sources': ['source-1', 'source-3']}
|
||||||
instance = ForeignKeyTarget.objects.get(pk=2)
|
instance = ForeignKeyTarget.objects.get(pk=2)
|
||||||
serializer = ForeignKeyTargetSerializer(instance, data=data)
|
serializer = ForeignKeyTargetSerializer(instance, data=data)
|
||||||
self.assertTrue(serializer.is_valid())
|
assert serializer.is_valid()
|
||||||
# We shouldn't have saved anything to the db yet since save
|
# We shouldn't have saved anything to the db yet since save
|
||||||
# hasn't been called.
|
# hasn't been called.
|
||||||
queryset = ForeignKeyTarget.objects.all()
|
queryset = ForeignKeyTarget.objects.all()
|
||||||
|
@ -122,10 +122,10 @@ class SlugForeignKeyTests(TestCase):
|
||||||
{'id': 1, 'name': 'target-1', 'sources': ['source-1', 'source-2', 'source-3']},
|
{'id': 1, 'name': 'target-1', 'sources': ['source-1', 'source-2', 'source-3']},
|
||||||
{'id': 2, 'name': 'target-2', 'sources': []},
|
{'id': 2, 'name': 'target-2', 'sources': []},
|
||||||
]
|
]
|
||||||
self.assertEqual(new_serializer.data, expected)
|
assert new_serializer.data == expected
|
||||||
|
|
||||||
serializer.save()
|
serializer.save()
|
||||||
self.assertEqual(serializer.data, data)
|
assert serializer.data == data
|
||||||
|
|
||||||
# Ensure target 2 is update, and everything else is as expected
|
# Ensure target 2 is update, and everything else is as expected
|
||||||
queryset = ForeignKeyTarget.objects.all()
|
queryset = ForeignKeyTarget.objects.all()
|
||||||
|
@ -134,16 +134,16 @@ class SlugForeignKeyTests(TestCase):
|
||||||
{'id': 1, 'name': 'target-1', 'sources': ['source-2']},
|
{'id': 1, 'name': 'target-1', 'sources': ['source-2']},
|
||||||
{'id': 2, 'name': 'target-2', 'sources': ['source-1', 'source-3']},
|
{'id': 2, 'name': 'target-2', 'sources': ['source-1', 'source-3']},
|
||||||
]
|
]
|
||||||
self.assertEqual(serializer.data, expected)
|
assert serializer.data == expected
|
||||||
|
|
||||||
def test_foreign_key_create(self):
|
def test_foreign_key_create(self):
|
||||||
data = {'id': 4, 'name': 'source-4', 'target': 'target-2'}
|
data = {'id': 4, 'name': 'source-4', 'target': 'target-2'}
|
||||||
serializer = ForeignKeySourceSerializer(data=data)
|
serializer = ForeignKeySourceSerializer(data=data)
|
||||||
serializer.is_valid()
|
serializer.is_valid()
|
||||||
self.assertTrue(serializer.is_valid())
|
assert serializer.is_valid()
|
||||||
obj = serializer.save()
|
obj = serializer.save()
|
||||||
self.assertEqual(serializer.data, data)
|
assert serializer.data == data
|
||||||
self.assertEqual(obj.name, 'source-4')
|
assert obj.name == 'source-4'
|
||||||
|
|
||||||
# Ensure source 4 is added, and everything else is as expected
|
# Ensure source 4 is added, and everything else is as expected
|
||||||
queryset = ForeignKeySource.objects.all()
|
queryset = ForeignKeySource.objects.all()
|
||||||
|
@ -154,15 +154,15 @@ class SlugForeignKeyTests(TestCase):
|
||||||
{'id': 3, 'name': 'source-3', 'target': 'target-1'},
|
{'id': 3, 'name': 'source-3', 'target': 'target-1'},
|
||||||
{'id': 4, 'name': 'source-4', 'target': 'target-2'},
|
{'id': 4, 'name': 'source-4', 'target': 'target-2'},
|
||||||
]
|
]
|
||||||
self.assertEqual(serializer.data, expected)
|
assert serializer.data == expected
|
||||||
|
|
||||||
def test_reverse_foreign_key_create(self):
|
def test_reverse_foreign_key_create(self):
|
||||||
data = {'id': 3, 'name': 'target-3', 'sources': ['source-1', 'source-3']}
|
data = {'id': 3, 'name': 'target-3', 'sources': ['source-1', 'source-3']}
|
||||||
serializer = ForeignKeyTargetSerializer(data=data)
|
serializer = ForeignKeyTargetSerializer(data=data)
|
||||||
self.assertTrue(serializer.is_valid())
|
assert serializer.is_valid()
|
||||||
obj = serializer.save()
|
obj = serializer.save()
|
||||||
self.assertEqual(serializer.data, data)
|
assert serializer.data == data
|
||||||
self.assertEqual(obj.name, 'target-3')
|
assert obj.name == 'target-3'
|
||||||
|
|
||||||
# Ensure target 3 is added, and everything else is as expected
|
# Ensure target 3 is added, and everything else is as expected
|
||||||
queryset = ForeignKeyTarget.objects.all()
|
queryset = ForeignKeyTarget.objects.all()
|
||||||
|
@ -172,14 +172,14 @@ class SlugForeignKeyTests(TestCase):
|
||||||
{'id': 2, 'name': 'target-2', 'sources': []},
|
{'id': 2, 'name': 'target-2', 'sources': []},
|
||||||
{'id': 3, 'name': 'target-3', 'sources': ['source-1', 'source-3']},
|
{'id': 3, 'name': 'target-3', 'sources': ['source-1', 'source-3']},
|
||||||
]
|
]
|
||||||
self.assertEqual(serializer.data, expected)
|
assert serializer.data == expected
|
||||||
|
|
||||||
def test_foreign_key_update_with_invalid_null(self):
|
def test_foreign_key_update_with_invalid_null(self):
|
||||||
data = {'id': 1, 'name': 'source-1', 'target': None}
|
data = {'id': 1, 'name': 'source-1', 'target': None}
|
||||||
instance = ForeignKeySource.objects.get(pk=1)
|
instance = ForeignKeySource.objects.get(pk=1)
|
||||||
serializer = ForeignKeySourceSerializer(instance, data=data)
|
serializer = ForeignKeySourceSerializer(instance, data=data)
|
||||||
self.assertFalse(serializer.is_valid())
|
assert not serializer.is_valid()
|
||||||
self.assertEqual(serializer.errors, {'target': ['This field may not be null.']})
|
assert serializer.errors == {'target': ['This field may not be null.']}
|
||||||
|
|
||||||
|
|
||||||
class SlugNullableForeignKeyTests(TestCase):
|
class SlugNullableForeignKeyTests(TestCase):
|
||||||
|
@ -200,15 +200,15 @@ class SlugNullableForeignKeyTests(TestCase):
|
||||||
{'id': 2, 'name': 'source-2', 'target': 'target-1'},
|
{'id': 2, 'name': 'source-2', 'target': 'target-1'},
|
||||||
{'id': 3, 'name': 'source-3', 'target': None},
|
{'id': 3, 'name': 'source-3', 'target': None},
|
||||||
]
|
]
|
||||||
self.assertEqual(serializer.data, expected)
|
assert serializer.data == expected
|
||||||
|
|
||||||
def test_foreign_key_create_with_valid_null(self):
|
def test_foreign_key_create_with_valid_null(self):
|
||||||
data = {'id': 4, 'name': 'source-4', 'target': None}
|
data = {'id': 4, 'name': 'source-4', 'target': None}
|
||||||
serializer = NullableForeignKeySourceSerializer(data=data)
|
serializer = NullableForeignKeySourceSerializer(data=data)
|
||||||
self.assertTrue(serializer.is_valid())
|
assert serializer.is_valid()
|
||||||
obj = serializer.save()
|
obj = serializer.save()
|
||||||
self.assertEqual(serializer.data, data)
|
assert serializer.data == data
|
||||||
self.assertEqual(obj.name, 'source-4')
|
assert obj.name == 'source-4'
|
||||||
|
|
||||||
# Ensure source 4 is created, and everything else is as expected
|
# Ensure source 4 is created, and everything else is as expected
|
||||||
queryset = NullableForeignKeySource.objects.all()
|
queryset = NullableForeignKeySource.objects.all()
|
||||||
|
@ -219,7 +219,7 @@ class SlugNullableForeignKeyTests(TestCase):
|
||||||
{'id': 3, 'name': 'source-3', 'target': None},
|
{'id': 3, 'name': 'source-3', 'target': None},
|
||||||
{'id': 4, 'name': 'source-4', 'target': None}
|
{'id': 4, 'name': 'source-4', 'target': None}
|
||||||
]
|
]
|
||||||
self.assertEqual(serializer.data, expected)
|
assert serializer.data == expected
|
||||||
|
|
||||||
def test_foreign_key_create_with_valid_emptystring(self):
|
def test_foreign_key_create_with_valid_emptystring(self):
|
||||||
"""
|
"""
|
||||||
|
@ -229,10 +229,10 @@ class SlugNullableForeignKeyTests(TestCase):
|
||||||
data = {'id': 4, 'name': 'source-4', 'target': ''}
|
data = {'id': 4, 'name': 'source-4', 'target': ''}
|
||||||
expected_data = {'id': 4, 'name': 'source-4', 'target': None}
|
expected_data = {'id': 4, 'name': 'source-4', 'target': None}
|
||||||
serializer = NullableForeignKeySourceSerializer(data=data)
|
serializer = NullableForeignKeySourceSerializer(data=data)
|
||||||
self.assertTrue(serializer.is_valid())
|
assert serializer.is_valid()
|
||||||
obj = serializer.save()
|
obj = serializer.save()
|
||||||
self.assertEqual(serializer.data, expected_data)
|
assert serializer.data == expected_data
|
||||||
self.assertEqual(obj.name, 'source-4')
|
assert obj.name == 'source-4'
|
||||||
|
|
||||||
# Ensure source 4 is created, and everything else is as expected
|
# Ensure source 4 is created, and everything else is as expected
|
||||||
queryset = NullableForeignKeySource.objects.all()
|
queryset = NullableForeignKeySource.objects.all()
|
||||||
|
@ -243,15 +243,15 @@ class SlugNullableForeignKeyTests(TestCase):
|
||||||
{'id': 3, 'name': 'source-3', 'target': None},
|
{'id': 3, 'name': 'source-3', 'target': None},
|
||||||
{'id': 4, 'name': 'source-4', 'target': None}
|
{'id': 4, 'name': 'source-4', 'target': None}
|
||||||
]
|
]
|
||||||
self.assertEqual(serializer.data, expected)
|
assert serializer.data == expected
|
||||||
|
|
||||||
def test_foreign_key_update_with_valid_null(self):
|
def test_foreign_key_update_with_valid_null(self):
|
||||||
data = {'id': 1, 'name': 'source-1', 'target': None}
|
data = {'id': 1, 'name': 'source-1', 'target': None}
|
||||||
instance = NullableForeignKeySource.objects.get(pk=1)
|
instance = NullableForeignKeySource.objects.get(pk=1)
|
||||||
serializer = NullableForeignKeySourceSerializer(instance, data=data)
|
serializer = NullableForeignKeySourceSerializer(instance, data=data)
|
||||||
self.assertTrue(serializer.is_valid())
|
assert serializer.is_valid()
|
||||||
serializer.save()
|
serializer.save()
|
||||||
self.assertEqual(serializer.data, data)
|
assert serializer.data == data
|
||||||
|
|
||||||
# Ensure source 1 is updated, and everything else is as expected
|
# Ensure source 1 is updated, and everything else is as expected
|
||||||
queryset = NullableForeignKeySource.objects.all()
|
queryset = NullableForeignKeySource.objects.all()
|
||||||
|
@ -261,7 +261,7 @@ class SlugNullableForeignKeyTests(TestCase):
|
||||||
{'id': 2, 'name': 'source-2', 'target': 'target-1'},
|
{'id': 2, 'name': 'source-2', 'target': 'target-1'},
|
||||||
{'id': 3, 'name': 'source-3', 'target': None}
|
{'id': 3, 'name': 'source-3', 'target': None}
|
||||||
]
|
]
|
||||||
self.assertEqual(serializer.data, expected)
|
assert serializer.data == expected
|
||||||
|
|
||||||
def test_foreign_key_update_with_valid_emptystring(self):
|
def test_foreign_key_update_with_valid_emptystring(self):
|
||||||
"""
|
"""
|
||||||
|
@ -272,9 +272,9 @@ class SlugNullableForeignKeyTests(TestCase):
|
||||||
expected_data = {'id': 1, 'name': 'source-1', 'target': None}
|
expected_data = {'id': 1, 'name': 'source-1', 'target': None}
|
||||||
instance = NullableForeignKeySource.objects.get(pk=1)
|
instance = NullableForeignKeySource.objects.get(pk=1)
|
||||||
serializer = NullableForeignKeySourceSerializer(instance, data=data)
|
serializer = NullableForeignKeySourceSerializer(instance, data=data)
|
||||||
self.assertTrue(serializer.is_valid())
|
assert serializer.is_valid()
|
||||||
serializer.save()
|
serializer.save()
|
||||||
self.assertEqual(serializer.data, expected_data)
|
assert serializer.data == expected_data
|
||||||
|
|
||||||
# Ensure source 1 is updated, and everything else is as expected
|
# Ensure source 1 is updated, and everything else is as expected
|
||||||
queryset = NullableForeignKeySource.objects.all()
|
queryset = NullableForeignKeySource.objects.all()
|
||||||
|
@ -284,4 +284,4 @@ class SlugNullableForeignKeyTests(TestCase):
|
||||||
{'id': 2, 'name': 'source-2', 'target': 'target-1'},
|
{'id': 2, 'name': 'source-2', 'target': 'target-1'},
|
||||||
{'id': 3, 'name': 'source-3', 'target': None}
|
{'id': 3, 'name': 'source-3', 'target': None}
|
||||||
]
|
]
|
||||||
self.assertEqual(serializer.data, expected)
|
assert serializer.data == expected
|
||||||
|
|
|
@ -5,9 +5,11 @@ import json
|
||||||
import re
|
import re
|
||||||
from collections import MutableMapping, OrderedDict
|
from collections import MutableMapping, OrderedDict
|
||||||
|
|
||||||
|
import pytest
|
||||||
from django.conf.urls import include, url
|
from django.conf.urls import include, url
|
||||||
from django.core.cache import cache
|
from django.core.cache import cache
|
||||||
from django.db import models
|
from django.db import models
|
||||||
|
from django.http.request import HttpRequest
|
||||||
from django.test import TestCase, override_settings
|
from django.test import TestCase, override_settings
|
||||||
from django.utils import six
|
from django.utils import six
|
||||||
from django.utils.safestring import SafeText
|
from django.utils.safestring import SafeText
|
||||||
|
@ -15,8 +17,10 @@ from django.utils.translation import ugettext_lazy as _
|
||||||
|
|
||||||
from rest_framework import permissions, serializers, status
|
from rest_framework import permissions, serializers, status
|
||||||
from rest_framework.renderers import (
|
from rest_framework.renderers import (
|
||||||
BaseRenderer, BrowsableAPIRenderer, HTMLFormRenderer, JSONRenderer
|
AdminRenderer, BaseRenderer, BrowsableAPIRenderer,
|
||||||
|
HTMLFormRenderer, JSONRenderer, StaticHTMLRenderer
|
||||||
)
|
)
|
||||||
|
from rest_framework.request import Request
|
||||||
from rest_framework.response import Response
|
from rest_framework.response import Response
|
||||||
from rest_framework.settings import api_settings
|
from rest_framework.settings import api_settings
|
||||||
from rest_framework.test import APIRequestFactory
|
from rest_framework.test import APIRequestFactory
|
||||||
|
@ -269,6 +273,18 @@ def strip_trailing_whitespace(content):
|
||||||
return re.sub(' +\n', '\n', content)
|
return re.sub(' +\n', '\n', content)
|
||||||
|
|
||||||
|
|
||||||
|
class BaseRendererTests(TestCase):
|
||||||
|
"""
|
||||||
|
Tests BaseRenderer
|
||||||
|
"""
|
||||||
|
def test_render_raise_error(self):
|
||||||
|
"""
|
||||||
|
BaseRenderer.render should raise NotImplementedError
|
||||||
|
"""
|
||||||
|
with pytest.raises(NotImplementedError):
|
||||||
|
BaseRenderer().render('test')
|
||||||
|
|
||||||
|
|
||||||
class JSONRendererTests(TestCase):
|
class JSONRendererTests(TestCase):
|
||||||
"""
|
"""
|
||||||
Tests specific to the JSON Renderer
|
Tests specific to the JSON Renderer
|
||||||
|
@ -568,3 +584,67 @@ class TestMultipleChoiceFieldHTMLFormRenderer(TestCase):
|
||||||
result)
|
result)
|
||||||
self.assertInHTML('<option value="1">Option1</option>', result)
|
self.assertInHTML('<option value="1">Option1</option>', result)
|
||||||
self.assertInHTML('<option value="2">Option2</option>', result)
|
self.assertInHTML('<option value="2">Option2</option>', result)
|
||||||
|
|
||||||
|
|
||||||
|
class StaticHTMLRendererTests(TestCase):
|
||||||
|
"""
|
||||||
|
Tests specific for Static HTML Renderer
|
||||||
|
"""
|
||||||
|
def setUp(self):
|
||||||
|
self.renderer = StaticHTMLRenderer()
|
||||||
|
|
||||||
|
def test_static_renderer(self):
|
||||||
|
data = '<html><body>text</body></html>'
|
||||||
|
result = self.renderer.render(data)
|
||||||
|
assert result == data
|
||||||
|
|
||||||
|
def test_static_renderer_with_exception(self):
|
||||||
|
context = {
|
||||||
|
'response': Response(status=500, exception=True),
|
||||||
|
'request': Request(HttpRequest())
|
||||||
|
}
|
||||||
|
result = self.renderer.render({}, renderer_context=context)
|
||||||
|
assert result == '500 Internal Server Error'
|
||||||
|
|
||||||
|
|
||||||
|
class BrowsableAPIRendererTests(TestCase):
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
self.renderer = BrowsableAPIRenderer()
|
||||||
|
|
||||||
|
def test_get_description_returns_empty_string_for_401_and_403_statuses(self):
|
||||||
|
assert self.renderer.get_description({}, status_code=401) == ''
|
||||||
|
assert self.renderer.get_description({}, status_code=403) == ''
|
||||||
|
|
||||||
|
def test_get_filter_form_returns_none_if_data_is_not_list_instance(self):
|
||||||
|
class DummyView(object):
|
||||||
|
get_queryset = None
|
||||||
|
filter_backends = None
|
||||||
|
|
||||||
|
result = self.renderer.get_filter_form(data='not list',
|
||||||
|
view=DummyView(), request={})
|
||||||
|
assert result is None
|
||||||
|
|
||||||
|
|
||||||
|
class AdminRendererTests(TestCase):
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
self.renderer = AdminRenderer()
|
||||||
|
|
||||||
|
def test_render_when_resource_created(self):
|
||||||
|
class DummyView(APIView):
|
||||||
|
renderer_classes = (AdminRenderer, )
|
||||||
|
request = Request(HttpRequest())
|
||||||
|
request.build_absolute_uri = lambda: 'http://example.com'
|
||||||
|
response = Response(status=201, headers={'Location': '/test'})
|
||||||
|
context = {
|
||||||
|
'view': DummyView(),
|
||||||
|
'request': request,
|
||||||
|
'response': response
|
||||||
|
}
|
||||||
|
|
||||||
|
result = self.renderer.render(data={'test': 'test'},
|
||||||
|
renderer_context=context)
|
||||||
|
assert result == ''
|
||||||
|
assert response.status_code == status.HTTP_303_SEE_OTHER
|
||||||
|
assert response['Location'] == 'http://example.com'
|
||||||
|
|
|
@ -42,14 +42,14 @@ class TestContentParsing(TestCase):
|
||||||
Ensure request.data returns empty QueryDict for GET request.
|
Ensure request.data returns empty QueryDict for GET request.
|
||||||
"""
|
"""
|
||||||
request = Request(factory.get('/'))
|
request = Request(factory.get('/'))
|
||||||
self.assertEqual(request.data, {})
|
assert request.data == {}
|
||||||
|
|
||||||
def test_standard_behaviour_determines_no_content_HEAD(self):
|
def test_standard_behaviour_determines_no_content_HEAD(self):
|
||||||
"""
|
"""
|
||||||
Ensure request.data returns empty QueryDict for HEAD request.
|
Ensure request.data returns empty QueryDict for HEAD request.
|
||||||
"""
|
"""
|
||||||
request = Request(factory.head('/'))
|
request = Request(factory.head('/'))
|
||||||
self.assertEqual(request.data, {})
|
assert request.data == {}
|
||||||
|
|
||||||
def test_request_DATA_with_form_content(self):
|
def test_request_DATA_with_form_content(self):
|
||||||
"""
|
"""
|
||||||
|
@ -58,7 +58,7 @@ class TestContentParsing(TestCase):
|
||||||
data = {'qwerty': 'uiop'}
|
data = {'qwerty': 'uiop'}
|
||||||
request = Request(factory.post('/', data))
|
request = Request(factory.post('/', data))
|
||||||
request.parsers = (FormParser(), MultiPartParser())
|
request.parsers = (FormParser(), MultiPartParser())
|
||||||
self.assertEqual(list(request.data.items()), list(data.items()))
|
assert list(request.data.items()) == list(data.items())
|
||||||
|
|
||||||
def test_request_DATA_with_text_content(self):
|
def test_request_DATA_with_text_content(self):
|
||||||
"""
|
"""
|
||||||
|
@ -69,7 +69,7 @@ class TestContentParsing(TestCase):
|
||||||
content_type = 'text/plain'
|
content_type = 'text/plain'
|
||||||
request = Request(factory.post('/', content, content_type=content_type))
|
request = Request(factory.post('/', content, content_type=content_type))
|
||||||
request.parsers = (PlainTextParser(),)
|
request.parsers = (PlainTextParser(),)
|
||||||
self.assertEqual(request.data, content)
|
assert request.data == content
|
||||||
|
|
||||||
def test_request_POST_with_form_content(self):
|
def test_request_POST_with_form_content(self):
|
||||||
"""
|
"""
|
||||||
|
@ -78,7 +78,7 @@ class TestContentParsing(TestCase):
|
||||||
data = {'qwerty': 'uiop'}
|
data = {'qwerty': 'uiop'}
|
||||||
request = Request(factory.post('/', data))
|
request = Request(factory.post('/', data))
|
||||||
request.parsers = (FormParser(), MultiPartParser())
|
request.parsers = (FormParser(), MultiPartParser())
|
||||||
self.assertEqual(list(request.POST.items()), list(data.items()))
|
assert list(request.POST.items()) == list(data.items())
|
||||||
|
|
||||||
def test_request_POST_with_files(self):
|
def test_request_POST_with_files(self):
|
||||||
"""
|
"""
|
||||||
|
@ -87,8 +87,8 @@ class TestContentParsing(TestCase):
|
||||||
upload = SimpleUploadedFile("file.txt", b"file_content")
|
upload = SimpleUploadedFile("file.txt", b"file_content")
|
||||||
request = Request(factory.post('/', {'upload': upload}))
|
request = Request(factory.post('/', {'upload': upload}))
|
||||||
request.parsers = (FormParser(), MultiPartParser())
|
request.parsers = (FormParser(), MultiPartParser())
|
||||||
self.assertEqual(list(request.POST.keys()), [])
|
assert list(request.POST.keys()) == []
|
||||||
self.assertEqual(list(request.FILES.keys()), ['upload'])
|
assert list(request.FILES.keys()) == ['upload']
|
||||||
|
|
||||||
def test_standard_behaviour_determines_form_content_PUT(self):
|
def test_standard_behaviour_determines_form_content_PUT(self):
|
||||||
"""
|
"""
|
||||||
|
@ -97,7 +97,7 @@ class TestContentParsing(TestCase):
|
||||||
data = {'qwerty': 'uiop'}
|
data = {'qwerty': 'uiop'}
|
||||||
request = Request(factory.put('/', data))
|
request = Request(factory.put('/', data))
|
||||||
request.parsers = (FormParser(), MultiPartParser())
|
request.parsers = (FormParser(), MultiPartParser())
|
||||||
self.assertEqual(list(request.data.items()), list(data.items()))
|
assert list(request.data.items()) == list(data.items())
|
||||||
|
|
||||||
def test_standard_behaviour_determines_non_form_content_PUT(self):
|
def test_standard_behaviour_determines_non_form_content_PUT(self):
|
||||||
"""
|
"""
|
||||||
|
@ -108,7 +108,7 @@ class TestContentParsing(TestCase):
|
||||||
content_type = 'text/plain'
|
content_type = 'text/plain'
|
||||||
request = Request(factory.put('/', content, content_type=content_type))
|
request = Request(factory.put('/', content, content_type=content_type))
|
||||||
request.parsers = (PlainTextParser(), )
|
request.parsers = (PlainTextParser(), )
|
||||||
self.assertEqual(request.data, content)
|
assert request.data == content
|
||||||
|
|
||||||
|
|
||||||
class MockView(APIView):
|
class MockView(APIView):
|
||||||
|
@ -142,10 +142,10 @@ class TestContentParsingWithAuthentication(TestCase):
|
||||||
content = {'example': 'example'}
|
content = {'example': 'example'}
|
||||||
|
|
||||||
response = self.client.post('/', content)
|
response = self.client.post('/', content)
|
||||||
self.assertEqual(status.HTTP_200_OK, response.status_code)
|
assert status.HTTP_200_OK == response.status_code
|
||||||
|
|
||||||
response = self.csrf_client.post('/', content)
|
response = self.csrf_client.post('/', content)
|
||||||
self.assertEqual(status.HTTP_200_OK, response.status_code)
|
assert status.HTTP_200_OK == response.status_code
|
||||||
|
|
||||||
|
|
||||||
class TestUserSetter(TestCase):
|
class TestUserSetter(TestCase):
|
||||||
|
@ -162,11 +162,11 @@ class TestUserSetter(TestCase):
|
||||||
|
|
||||||
def test_user_can_be_set(self):
|
def test_user_can_be_set(self):
|
||||||
self.request.user = self.user
|
self.request.user = self.user
|
||||||
self.assertEqual(self.request.user, self.user)
|
assert self.request.user == self.user
|
||||||
|
|
||||||
def test_user_can_login(self):
|
def test_user_can_login(self):
|
||||||
login(self.request, self.user)
|
login(self.request, self.user)
|
||||||
self.assertEqual(self.request.user, self.user)
|
assert self.request.user == self.user
|
||||||
|
|
||||||
def test_user_can_logout(self):
|
def test_user_can_logout(self):
|
||||||
self.request.user = self.user
|
self.request.user = self.user
|
||||||
|
@ -176,7 +176,7 @@ class TestUserSetter(TestCase):
|
||||||
|
|
||||||
def test_logged_in_user_is_set_on_wrapped_request(self):
|
def test_logged_in_user_is_set_on_wrapped_request(self):
|
||||||
login(self.request, self.user)
|
login(self.request, self.user)
|
||||||
self.assertEqual(self.wrapped_request.user, self.user)
|
assert self.wrapped_request.user == self.user
|
||||||
|
|
||||||
def test_calling_user_fails_when_attribute_error_is_raised(self):
|
def test_calling_user_fails_when_attribute_error_is_raised(self):
|
||||||
"""
|
"""
|
||||||
|
@ -207,15 +207,15 @@ class TestAuthSetter(TestCase):
|
||||||
def test_auth_can_be_set(self):
|
def test_auth_can_be_set(self):
|
||||||
request = Request(factory.get('/'))
|
request = Request(factory.get('/'))
|
||||||
request.auth = 'DUMMY'
|
request.auth = 'DUMMY'
|
||||||
self.assertEqual(request.auth, 'DUMMY')
|
assert request.auth == 'DUMMY'
|
||||||
|
|
||||||
|
|
||||||
class TestSecure(TestCase):
|
class TestSecure(TestCase):
|
||||||
|
|
||||||
def test_default_secure_false(self):
|
def test_default_secure_false(self):
|
||||||
request = Request(factory.get('/', secure=False))
|
request = Request(factory.get('/', secure=False))
|
||||||
self.assertEqual(request.scheme, 'http')
|
assert request.scheme == 'http'
|
||||||
|
|
||||||
def test_default_secure_true(self):
|
def test_default_secure_true(self):
|
||||||
request = Request(factory.get('/', secure=True))
|
request = Request(factory.get('/', secure=True))
|
||||||
self.assertEqual(request.scheme, 'https')
|
assert request.scheme == 'https'
|
||||||
|
|
|
@ -38,18 +38,18 @@ class ReverseTests(TestCase):
|
||||||
def test_reversed_urls_are_fully_qualified(self):
|
def test_reversed_urls_are_fully_qualified(self):
|
||||||
request = factory.get('/view')
|
request = factory.get('/view')
|
||||||
url = reverse('view', request=request)
|
url = reverse('view', request=request)
|
||||||
self.assertEqual(url, 'http://testserver/view')
|
assert url == 'http://testserver/view'
|
||||||
|
|
||||||
def test_reverse_with_versioning_scheme(self):
|
def test_reverse_with_versioning_scheme(self):
|
||||||
request = factory.get('/view')
|
request = factory.get('/view')
|
||||||
request.versioning_scheme = MockVersioningScheme()
|
request.versioning_scheme = MockVersioningScheme()
|
||||||
|
|
||||||
url = reverse('view', request=request)
|
url = reverse('view', request=request)
|
||||||
self.assertEqual(url, 'http://scheme-reversed/view')
|
assert url == 'http://scheme-reversed/view'
|
||||||
|
|
||||||
def test_reverse_with_versioning_scheme_fallback_to_default_on_error(self):
|
def test_reverse_with_versioning_scheme_fallback_to_default_on_error(self):
|
||||||
request = factory.get('/view')
|
request = factory.get('/view')
|
||||||
request.versioning_scheme = MockVersioningScheme(raise_error=True)
|
request.versioning_scheme = MockVersioningScheme(raise_error=True)
|
||||||
|
|
||||||
url = reverse('view', request=request)
|
url = reverse('view', request=request)
|
||||||
self.assertEqual(url, 'http://testserver/view')
|
assert url == 'http://testserver/view'
|
||||||
|
|
|
@ -3,12 +3,14 @@ from __future__ import unicode_literals
|
||||||
import json
|
import json
|
||||||
from collections import namedtuple
|
from collections import namedtuple
|
||||||
|
|
||||||
from django.conf.urls import include, url
|
import pytest
|
||||||
|
from django.conf.urls import url
|
||||||
from django.core.exceptions import ImproperlyConfigured
|
from django.core.exceptions import ImproperlyConfigured
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.test import TestCase, override_settings
|
from django.test import TestCase, override_settings
|
||||||
|
|
||||||
from rest_framework import permissions, serializers, viewsets
|
from rest_framework import permissions, serializers, viewsets
|
||||||
|
from rest_framework.compat import include
|
||||||
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 DefaultRouter, SimpleRouter
|
from rest_framework.routers import DefaultRouter, SimpleRouter
|
||||||
|
@ -80,7 +82,7 @@ empty_prefix_urls = [
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
url(r'^non-namespaced/', include(namespaced_router.urls)),
|
url(r'^non-namespaced/', include(namespaced_router.urls)),
|
||||||
url(r'^namespaced/', include(namespaced_router.urls, namespace='example')),
|
url(r'^namespaced/', include(namespaced_router.urls, namespace='example', app_name='example')),
|
||||||
url(r'^example/', include(notes_router.urls)),
|
url(r'^example/', include(notes_router.urls)),
|
||||||
url(r'^example2/', include(kwarged_notes_router.urls)),
|
url(r'^example2/', include(kwarged_notes_router.urls)),
|
||||||
|
|
||||||
|
@ -124,8 +126,7 @@ class TestSimpleRouter(TestCase):
|
||||||
for i, endpoint in enumerate(['action1', 'action2', 'action3', 'link1', 'link2']):
|
for i, endpoint in enumerate(['action1', 'action2', 'action3', 'link1', 'link2']):
|
||||||
route = decorator_routes[i]
|
route = decorator_routes[i]
|
||||||
# check url listing
|
# check url listing
|
||||||
self.assertEqual(route.url,
|
assert route.url == '^{{prefix}}/{{lookup}}/{0}{{trailing_slash}}$'.format(endpoint)
|
||||||
'^{{prefix}}/{{lookup}}/{0}{{trailing_slash}}$'.format(endpoint))
|
|
||||||
# check method to function mapping
|
# check method to function mapping
|
||||||
if endpoint == 'action3':
|
if endpoint == 'action3':
|
||||||
methods_map = ['post', 'delete']
|
methods_map = ['post', 'delete']
|
||||||
|
@ -134,28 +135,18 @@ class TestSimpleRouter(TestCase):
|
||||||
else:
|
else:
|
||||||
methods_map = ['get']
|
methods_map = ['get']
|
||||||
for method in methods_map:
|
for method in methods_map:
|
||||||
self.assertEqual(route.mapping[method], endpoint)
|
assert route.mapping[method] == endpoint
|
||||||
|
|
||||||
|
|
||||||
@override_settings(ROOT_URLCONF='tests.test_routers')
|
@override_settings(ROOT_URLCONF='tests.test_routers')
|
||||||
class TestRootView(TestCase):
|
class TestRootView(TestCase):
|
||||||
def test_retrieve_namespaced_root(self):
|
def test_retrieve_namespaced_root(self):
|
||||||
response = self.client.get('/namespaced/')
|
response = self.client.get('/namespaced/')
|
||||||
self.assertEqual(
|
assert response.data == {"example": "http://testserver/namespaced/example/"}
|
||||||
response.data,
|
|
||||||
{
|
|
||||||
"example": "http://testserver/namespaced/example/",
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
def test_retrieve_non_namespaced_root(self):
|
def test_retrieve_non_namespaced_root(self):
|
||||||
response = self.client.get('/non-namespaced/')
|
response = self.client.get('/non-namespaced/')
|
||||||
self.assertEqual(
|
assert response.data == {"example": "http://testserver/non-namespaced/example/"}
|
||||||
response.data,
|
|
||||||
{
|
|
||||||
"example": "http://testserver/non-namespaced/example/",
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@override_settings(ROOT_URLCONF='tests.test_routers')
|
@override_settings(ROOT_URLCONF='tests.test_routers')
|
||||||
|
@ -169,27 +160,15 @@ class TestCustomLookupFields(TestCase):
|
||||||
def test_custom_lookup_field_route(self):
|
def test_custom_lookup_field_route(self):
|
||||||
detail_route = notes_router.urls[-1]
|
detail_route = notes_router.urls[-1]
|
||||||
detail_url_pattern = detail_route.regex.pattern
|
detail_url_pattern = detail_route.regex.pattern
|
||||||
self.assertIn('<uuid>', detail_url_pattern)
|
assert '<uuid>' in detail_url_pattern
|
||||||
|
|
||||||
def test_retrieve_lookup_field_list_view(self):
|
def test_retrieve_lookup_field_list_view(self):
|
||||||
response = self.client.get('/example/notes/')
|
response = self.client.get('/example/notes/')
|
||||||
self.assertEqual(
|
assert response.data == [{"url": "http://testserver/example/notes/123/", "uuid": "123", "text": "foo bar"}]
|
||||||
response.data,
|
|
||||||
[{
|
|
||||||
"url": "http://testserver/example/notes/123/",
|
|
||||||
"uuid": "123", "text": "foo bar"
|
|
||||||
}]
|
|
||||||
)
|
|
||||||
|
|
||||||
def test_retrieve_lookup_field_detail_view(self):
|
def test_retrieve_lookup_field_detail_view(self):
|
||||||
response = self.client.get('/example/notes/123/')
|
response = self.client.get('/example/notes/123/')
|
||||||
self.assertEqual(
|
assert response.data == {"url": "http://testserver/example/notes/123/", "uuid": "123", "text": "foo bar"}
|
||||||
response.data,
|
|
||||||
{
|
|
||||||
"url": "http://testserver/example/notes/123/",
|
|
||||||
"uuid": "123", "text": "foo bar"
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class TestLookupValueRegex(TestCase):
|
class TestLookupValueRegex(TestCase):
|
||||||
|
@ -210,7 +189,7 @@ class TestLookupValueRegex(TestCase):
|
||||||
def test_urls_limited_by_lookup_value_regex(self):
|
def test_urls_limited_by_lookup_value_regex(self):
|
||||||
expected = ['^notes/$', '^notes/(?P<uuid>[0-9a-f]{32})/$']
|
expected = ['^notes/$', '^notes/(?P<uuid>[0-9a-f]{32})/$']
|
||||||
for idx in range(len(expected)):
|
for idx in range(len(expected)):
|
||||||
self.assertEqual(expected[idx], self.urls[idx].regex.pattern)
|
assert expected[idx] == self.urls[idx].regex.pattern
|
||||||
|
|
||||||
|
|
||||||
@override_settings(ROOT_URLCONF='tests.test_routers')
|
@override_settings(ROOT_URLCONF='tests.test_routers')
|
||||||
|
@ -226,17 +205,11 @@ class TestLookupUrlKwargs(TestCase):
|
||||||
def test_custom_lookup_url_kwarg_route(self):
|
def test_custom_lookup_url_kwarg_route(self):
|
||||||
detail_route = kwarged_notes_router.urls[-1]
|
detail_route = kwarged_notes_router.urls[-1]
|
||||||
detail_url_pattern = detail_route.regex.pattern
|
detail_url_pattern = detail_route.regex.pattern
|
||||||
self.assertIn('^notes/(?P<text>', detail_url_pattern)
|
assert '^notes/(?P<text>' in detail_url_pattern
|
||||||
|
|
||||||
def test_retrieve_lookup_url_kwarg_detail_view(self):
|
def test_retrieve_lookup_url_kwarg_detail_view(self):
|
||||||
response = self.client.get('/example2/notes/fo/')
|
response = self.client.get('/example2/notes/fo/')
|
||||||
self.assertEqual(
|
assert response.data == {"url": "http://testserver/example/notes/123/", "uuid": "123", "text": "foo bar"}
|
||||||
response.data,
|
|
||||||
{
|
|
||||||
"url": "http://testserver/example/notes/123/",
|
|
||||||
"uuid": "123", "text": "foo bar"
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class TestTrailingSlashIncluded(TestCase):
|
class TestTrailingSlashIncluded(TestCase):
|
||||||
|
@ -251,7 +224,7 @@ class TestTrailingSlashIncluded(TestCase):
|
||||||
def test_urls_have_trailing_slash_by_default(self):
|
def test_urls_have_trailing_slash_by_default(self):
|
||||||
expected = ['^notes/$', '^notes/(?P<pk>[^/.]+)/$']
|
expected = ['^notes/$', '^notes/(?P<pk>[^/.]+)/$']
|
||||||
for idx in range(len(expected)):
|
for idx in range(len(expected)):
|
||||||
self.assertEqual(expected[idx], self.urls[idx].regex.pattern)
|
assert expected[idx] == self.urls[idx].regex.pattern
|
||||||
|
|
||||||
|
|
||||||
class TestTrailingSlashRemoved(TestCase):
|
class TestTrailingSlashRemoved(TestCase):
|
||||||
|
@ -266,7 +239,7 @@ class TestTrailingSlashRemoved(TestCase):
|
||||||
def test_urls_can_have_trailing_slash_removed(self):
|
def test_urls_can_have_trailing_slash_removed(self):
|
||||||
expected = ['^notes$', '^notes/(?P<pk>[^/.]+)$']
|
expected = ['^notes$', '^notes/(?P<pk>[^/.]+)$']
|
||||||
for idx in range(len(expected)):
|
for idx in range(len(expected)):
|
||||||
self.assertEqual(expected[idx], self.urls[idx].regex.pattern)
|
assert expected[idx] == self.urls[idx].regex.pattern
|
||||||
|
|
||||||
|
|
||||||
class TestNameableRoot(TestCase):
|
class TestNameableRoot(TestCase):
|
||||||
|
@ -281,7 +254,7 @@ class TestNameableRoot(TestCase):
|
||||||
|
|
||||||
def test_router_has_custom_name(self):
|
def test_router_has_custom_name(self):
|
||||||
expected = 'nameable-root'
|
expected = 'nameable-root'
|
||||||
self.assertEqual(expected, self.urls[-1].name)
|
assert expected == self.urls[-1].name
|
||||||
|
|
||||||
|
|
||||||
class TestActionKeywordArgs(TestCase):
|
class TestActionKeywordArgs(TestCase):
|
||||||
|
@ -307,10 +280,7 @@ class TestActionKeywordArgs(TestCase):
|
||||||
def test_action_kwargs(self):
|
def test_action_kwargs(self):
|
||||||
request = factory.post('/test/0/custom/')
|
request = factory.post('/test/0/custom/')
|
||||||
response = self.view(request)
|
response = self.view(request)
|
||||||
self.assertEqual(
|
assert response.data == {'permission_classes': [permissions.AllowAny]}
|
||||||
response.data,
|
|
||||||
{'permission_classes': [permissions.AllowAny]}
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class TestActionAppliedToExistingRoute(TestCase):
|
class TestActionAppliedToExistingRoute(TestCase):
|
||||||
|
@ -331,7 +301,7 @@ class TestActionAppliedToExistingRoute(TestCase):
|
||||||
self.router = SimpleRouter()
|
self.router = SimpleRouter()
|
||||||
self.router.register(r'test', TestViewSet, base_name='test')
|
self.router.register(r'test', TestViewSet, base_name='test')
|
||||||
|
|
||||||
with self.assertRaises(ImproperlyConfigured):
|
with pytest.raises(ImproperlyConfigured):
|
||||||
self.router.urls
|
self.router.urls
|
||||||
|
|
||||||
|
|
||||||
|
@ -391,17 +361,15 @@ class TestDynamicListAndDetailRouter(TestCase):
|
||||||
url_path = endpoint.url_path
|
url_path = endpoint.url_path
|
||||||
|
|
||||||
if method_name.startswith('list_'):
|
if method_name.startswith('list_'):
|
||||||
self.assertEqual(route.url,
|
assert route.url == '^{{prefix}}/{0}{{trailing_slash}}$'.format(url_path)
|
||||||
'^{{prefix}}/{0}{{trailing_slash}}$'.format(url_path))
|
|
||||||
else:
|
else:
|
||||||
self.assertEqual(route.url,
|
assert route.url == '^{{prefix}}/{{lookup}}/{0}{{trailing_slash}}$'.format(url_path)
|
||||||
'^{{prefix}}/{{lookup}}/{0}{{trailing_slash}}$'.format(url_path))
|
|
||||||
# check method to function mapping
|
# check method to function mapping
|
||||||
if method_name.endswith('_post'):
|
if method_name.endswith('_post'):
|
||||||
method_map = 'post'
|
method_map = 'post'
|
||||||
else:
|
else:
|
||||||
method_map = 'get'
|
method_map = 'get'
|
||||||
self.assertEqual(route.mapping[method_map], method_name)
|
assert route.mapping[method_map] == method_name
|
||||||
|
|
||||||
def test_list_and_detail_route_decorators(self):
|
def test_list_and_detail_route_decorators(self):
|
||||||
self._test_list_and_detail_route_decorators(DynamicListAndDetailViewSet)
|
self._test_list_and_detail_route_decorators(DynamicListAndDetailViewSet)
|
||||||
|
@ -414,22 +382,11 @@ class TestDynamicListAndDetailRouter(TestCase):
|
||||||
class TestEmptyPrefix(TestCase):
|
class TestEmptyPrefix(TestCase):
|
||||||
def test_empty_prefix_list(self):
|
def test_empty_prefix_list(self):
|
||||||
response = self.client.get('/empty-prefix/')
|
response = self.client.get('/empty-prefix/')
|
||||||
self.assertEqual(200, response.status_code)
|
assert response.status_code == 200
|
||||||
self.assertEqual(
|
assert json.loads(response.content.decode('utf-8')) == [{'uuid': '111', 'text': 'First'},
|
||||||
json.loads(response.content.decode('utf-8')),
|
{'uuid': '222', 'text': 'Second'}]
|
||||||
[
|
|
||||||
{'uuid': '111', 'text': 'First'},
|
|
||||||
{'uuid': '222', 'text': 'Second'}
|
|
||||||
]
|
|
||||||
)
|
|
||||||
|
|
||||||
def test_empty_prefix_detail(self):
|
def test_empty_prefix_detail(self):
|
||||||
response = self.client.get('/empty-prefix/1/')
|
response = self.client.get('/empty-prefix/1/')
|
||||||
self.assertEqual(200, response.status_code)
|
assert response.status_code == 200
|
||||||
self.assertEqual(
|
assert json.loads(response.content.decode('utf-8')) == {'uuid': '111', 'text': 'First'}
|
||||||
json.loads(response.content.decode('utf-8')),
|
|
||||||
{
|
|
||||||
'uuid': '111',
|
|
||||||
'text': 'First'
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
|
@ -92,7 +92,7 @@ class TestRouterGeneratedSchema(TestCase):
|
||||||
def test_anonymous_request(self):
|
def test_anonymous_request(self):
|
||||||
client = APIClient()
|
client = APIClient()
|
||||||
response = client.get('/', HTTP_ACCEPT='application/coreapi+json')
|
response = client.get('/', HTTP_ACCEPT='application/coreapi+json')
|
||||||
self.assertEqual(response.status_code, 200)
|
assert response.status_code == 200
|
||||||
expected = coreapi.Document(
|
expected = coreapi.Document(
|
||||||
url='http://testserver/',
|
url='http://testserver/',
|
||||||
title='Example API',
|
title='Example API',
|
||||||
|
@ -127,14 +127,14 @@ class TestRouterGeneratedSchema(TestCase):
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
self.assertEqual(response.data, expected)
|
assert response.data == expected
|
||||||
|
|
||||||
@unittest.expectedFailure
|
@unittest.expectedFailure
|
||||||
def test_authenticated_request(self):
|
def test_authenticated_request(self):
|
||||||
client = APIClient()
|
client = APIClient()
|
||||||
client.force_authenticate(MockUser())
|
client.force_authenticate(MockUser())
|
||||||
response = client.get('/', HTTP_ACCEPT='application/coreapi+json')
|
response = client.get('/', HTTP_ACCEPT='application/coreapi+json')
|
||||||
self.assertEqual(response.status_code, 200)
|
assert response.status_code == 200
|
||||||
expected = coreapi.Document(
|
expected = coreapi.Document(
|
||||||
url='http://testserver/',
|
url='http://testserver/',
|
||||||
title='Example API',
|
title='Example API',
|
||||||
|
@ -220,7 +220,7 @@ class TestRouterGeneratedSchema(TestCase):
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
self.assertEqual(response.data, expected)
|
assert response.data == expected
|
||||||
|
|
||||||
|
|
||||||
class DenyAllUsingHttp404(permissions.BasePermission):
|
class DenyAllUsingHttp404(permissions.BasePermission):
|
||||||
|
@ -316,7 +316,7 @@ class TestSchemaGenerator(TestCase):
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
self.assertEqual(schema, expected)
|
assert schema == expected
|
||||||
|
|
||||||
|
|
||||||
@unittest.skipUnless(coreapi, 'coreapi is not installed')
|
@unittest.skipUnless(coreapi, 'coreapi is not installed')
|
||||||
|
@ -370,7 +370,7 @@ class TestSchemaGeneratorNotAtRoot(TestCase):
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
self.assertEqual(schema, expected)
|
assert schema == expected
|
||||||
|
|
||||||
|
|
||||||
@unittest.skipUnless(coreapi, 'coreapi is not installed')
|
@unittest.skipUnless(coreapi, 'coreapi is not installed')
|
||||||
|
@ -405,7 +405,7 @@ class TestSchemaGeneratorWithRestrictedViewSets(TestCase):
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
self.assertEqual(schema, expected)
|
assert schema == expected
|
||||||
|
|
||||||
|
|
||||||
@unittest.skipUnless(coreapi, 'coreapi is not installed')
|
@unittest.skipUnless(coreapi, 'coreapi is not installed')
|
||||||
|
|
|
@ -159,6 +159,32 @@ class TestBaseSerializer:
|
||||||
|
|
||||||
self.Serializer = ExampleSerializer
|
self.Serializer = ExampleSerializer
|
||||||
|
|
||||||
|
def test_abstract_methods_raise_proper_errors(self):
|
||||||
|
serializer = serializers.BaseSerializer()
|
||||||
|
with pytest.raises(NotImplementedError):
|
||||||
|
serializer.to_internal_value(None)
|
||||||
|
with pytest.raises(NotImplementedError):
|
||||||
|
serializer.to_representation(None)
|
||||||
|
with pytest.raises(NotImplementedError):
|
||||||
|
serializer.update(None, None)
|
||||||
|
with pytest.raises(NotImplementedError):
|
||||||
|
serializer.create(None)
|
||||||
|
|
||||||
|
def test_access_to_data_attribute_before_validation_raises_error(self):
|
||||||
|
serializer = serializers.BaseSerializer(data={'foo': 'bar'})
|
||||||
|
with pytest.raises(AssertionError):
|
||||||
|
serializer.data
|
||||||
|
|
||||||
|
def test_access_to_errors_attribute_before_validation_raises_error(self):
|
||||||
|
serializer = serializers.BaseSerializer(data={'foo': 'bar'})
|
||||||
|
with pytest.raises(AssertionError):
|
||||||
|
serializer.errors
|
||||||
|
|
||||||
|
def test_access_to_validated_data_attribute_before_validation_raises_error(self):
|
||||||
|
serializer = serializers.BaseSerializer(data={'foo': 'bar'})
|
||||||
|
with pytest.raises(AssertionError):
|
||||||
|
serializer.validated_data
|
||||||
|
|
||||||
def test_serialize_instance(self):
|
def test_serialize_instance(self):
|
||||||
instance = {'id': 1, 'name': 'tom', 'domain': 'example.com'}
|
instance = {'id': 1, 'name': 'tom', 'domain': 'example.com'}
|
||||||
serializer = self.Serializer(instance)
|
serializer = self.Serializer(instance)
|
||||||
|
|
|
@ -44,9 +44,9 @@ class BulkCreateSerializerTests(TestCase):
|
||||||
]
|
]
|
||||||
|
|
||||||
serializer = self.BookSerializer(data=data, many=True)
|
serializer = self.BookSerializer(data=data, many=True)
|
||||||
self.assertEqual(serializer.is_valid(), True)
|
assert serializer.is_valid() is True
|
||||||
self.assertEqual(serializer.validated_data, data)
|
assert serializer.validated_data == data
|
||||||
self.assertEqual(serializer.errors, [])
|
assert serializer.errors == []
|
||||||
|
|
||||||
def test_bulk_create_errors(self):
|
def test_bulk_create_errors(self):
|
||||||
"""
|
"""
|
||||||
|
@ -75,9 +75,9 @@ class BulkCreateSerializerTests(TestCase):
|
||||||
]
|
]
|
||||||
|
|
||||||
serializer = self.BookSerializer(data=data, many=True)
|
serializer = self.BookSerializer(data=data, many=True)
|
||||||
self.assertEqual(serializer.is_valid(), False)
|
assert serializer.is_valid() is False
|
||||||
self.assertEqual(serializer.errors, expected_errors)
|
assert serializer.errors == expected_errors
|
||||||
self.assertEqual(serializer.validated_data, [])
|
assert serializer.validated_data == []
|
||||||
|
|
||||||
def test_invalid_list_datatype(self):
|
def test_invalid_list_datatype(self):
|
||||||
"""
|
"""
|
||||||
|
@ -85,7 +85,7 @@ class BulkCreateSerializerTests(TestCase):
|
||||||
"""
|
"""
|
||||||
data = ['foo', 'bar', 'baz']
|
data = ['foo', 'bar', 'baz']
|
||||||
serializer = self.BookSerializer(data=data, many=True)
|
serializer = self.BookSerializer(data=data, many=True)
|
||||||
self.assertEqual(serializer.is_valid(), False)
|
assert serializer.is_valid() is False
|
||||||
|
|
||||||
text_type_string = six.text_type.__name__
|
text_type_string = six.text_type.__name__
|
||||||
message = 'Invalid data. Expected a dictionary, but got %s.' % text_type_string
|
message = 'Invalid data. Expected a dictionary, but got %s.' % text_type_string
|
||||||
|
@ -95,7 +95,7 @@ class BulkCreateSerializerTests(TestCase):
|
||||||
{'non_field_errors': [message]}
|
{'non_field_errors': [message]}
|
||||||
]
|
]
|
||||||
|
|
||||||
self.assertEqual(serializer.errors, expected_errors)
|
assert serializer.errors == expected_errors
|
||||||
|
|
||||||
def test_invalid_single_datatype(self):
|
def test_invalid_single_datatype(self):
|
||||||
"""
|
"""
|
||||||
|
@ -103,11 +103,11 @@ class BulkCreateSerializerTests(TestCase):
|
||||||
"""
|
"""
|
||||||
data = 123
|
data = 123
|
||||||
serializer = self.BookSerializer(data=data, many=True)
|
serializer = self.BookSerializer(data=data, many=True)
|
||||||
self.assertEqual(serializer.is_valid(), False)
|
assert serializer.is_valid() is False
|
||||||
|
|
||||||
expected_errors = {'non_field_errors': ['Expected a list of items but got type "int".']}
|
expected_errors = {'non_field_errors': ['Expected a list of items but got type "int".']}
|
||||||
|
|
||||||
self.assertEqual(serializer.errors, expected_errors)
|
assert serializer.errors == expected_errors
|
||||||
|
|
||||||
def test_invalid_single_object(self):
|
def test_invalid_single_object(self):
|
||||||
"""
|
"""
|
||||||
|
@ -120,8 +120,8 @@ class BulkCreateSerializerTests(TestCase):
|
||||||
'author': 'Tom Wolfe'
|
'author': 'Tom Wolfe'
|
||||||
}
|
}
|
||||||
serializer = self.BookSerializer(data=data, many=True)
|
serializer = self.BookSerializer(data=data, many=True)
|
||||||
self.assertEqual(serializer.is_valid(), False)
|
assert serializer.is_valid() is False
|
||||||
|
|
||||||
expected_errors = {'non_field_errors': ['Expected a list of items but got type "dict".']}
|
expected_errors = {'non_field_errors': ['Expected a list of items but got type "dict".']}
|
||||||
|
|
||||||
self.assertEqual(serializer.errors, expected_errors)
|
assert serializer.errors == expected_errors
|
||||||
|
|
|
@ -4,8 +4,10 @@ from __future__ import unicode_literals
|
||||||
from django.test import TestCase
|
from django.test import TestCase
|
||||||
|
|
||||||
from rest_framework.relations import Hyperlink
|
from rest_framework.relations import Hyperlink
|
||||||
|
from rest_framework.templatetags import rest_framework
|
||||||
from rest_framework.templatetags.rest_framework import (
|
from rest_framework.templatetags.rest_framework import (
|
||||||
add_nested_class, add_query_param, format_value, urlize_quoted_links
|
add_nested_class, add_query_param, as_string, break_long_headers,
|
||||||
|
format_value, get_pagination_html, urlize_quoted_links
|
||||||
)
|
)
|
||||||
from rest_framework.test import APIRequestFactory
|
from rest_framework.test import APIRequestFactory
|
||||||
|
|
||||||
|
@ -214,6 +216,27 @@ class TemplateTagTests(TestCase):
|
||||||
for case in negative_cases:
|
for case in negative_cases:
|
||||||
self.assertEqual(add_nested_class(case), '')
|
self.assertEqual(add_nested_class(case), '')
|
||||||
|
|
||||||
|
def test_as_string_with_none(self):
|
||||||
|
result = as_string(None)
|
||||||
|
assert result == ''
|
||||||
|
|
||||||
|
def test_get_pagination_html(self):
|
||||||
|
class MockPager(object):
|
||||||
|
def __init__(self):
|
||||||
|
self.called = False
|
||||||
|
|
||||||
|
def to_html(self):
|
||||||
|
self.called = True
|
||||||
|
|
||||||
|
pager = MockPager()
|
||||||
|
get_pagination_html(pager)
|
||||||
|
assert pager.called is True
|
||||||
|
|
||||||
|
def test_break_long_lines(self):
|
||||||
|
header = 'long test header,' * 20
|
||||||
|
expected_header = '<br> ' + ', <br>'.join(header.split(','))
|
||||||
|
assert break_long_headers(header) == expected_header
|
||||||
|
|
||||||
|
|
||||||
class Issue1386Tests(TestCase):
|
class Issue1386Tests(TestCase):
|
||||||
"""
|
"""
|
||||||
|
@ -246,6 +269,15 @@ class Issue1386Tests(TestCase):
|
||||||
# example from issue #1386, this shouldn't raise an exception
|
# example from issue #1386, this shouldn't raise an exception
|
||||||
urlize_quoted_links("asdf:[/p]zxcv.com")
|
urlize_quoted_links("asdf:[/p]zxcv.com")
|
||||||
|
|
||||||
|
def test_smart_urlquote_wrapper_handles_value_error(self):
|
||||||
|
def mock_smart_urlquote(url):
|
||||||
|
raise ValueError
|
||||||
|
|
||||||
|
old = rest_framework.smart_urlquote
|
||||||
|
rest_framework.smart_urlquote = mock_smart_urlquote
|
||||||
|
assert rest_framework.smart_urlquote_wrapper('test') is None
|
||||||
|
rest_framework.smart_urlquote = old
|
||||||
|
|
||||||
|
|
||||||
class URLizerTests(TestCase):
|
class URLizerTests(TestCase):
|
||||||
"""
|
"""
|
||||||
|
|
|
@ -3,15 +3,20 @@ Tests for the throttling implementations in the permissions module.
|
||||||
"""
|
"""
|
||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
import pytest
|
||||||
from django.contrib.auth.models import User
|
from django.contrib.auth.models import User
|
||||||
from django.core.cache import cache
|
from django.core.cache import cache
|
||||||
|
from django.core.exceptions import ImproperlyConfigured
|
||||||
|
from django.http import HttpRequest
|
||||||
from django.test import TestCase
|
from django.test import TestCase
|
||||||
|
|
||||||
|
from rest_framework.request import Request
|
||||||
from rest_framework.response import Response
|
from rest_framework.response import Response
|
||||||
from rest_framework.settings import api_settings
|
from rest_framework.settings import api_settings
|
||||||
from rest_framework.test import APIRequestFactory
|
from rest_framework.test import APIRequestFactory, force_authenticate
|
||||||
from rest_framework.throttling import (
|
from rest_framework.throttling import (
|
||||||
BaseThrottle, ScopedRateThrottle, UserRateThrottle
|
AnonRateThrottle, BaseThrottle, ScopedRateThrottle, SimpleRateThrottle,
|
||||||
|
UserRateThrottle
|
||||||
)
|
)
|
||||||
from rest_framework.views import APIView
|
from rest_framework.views import APIView
|
||||||
|
|
||||||
|
@ -189,6 +194,8 @@ class ScopedRateThrottleTests(TestCase):
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
|
self.throttle = ScopedRateThrottle()
|
||||||
|
|
||||||
class XYScopedRateThrottle(ScopedRateThrottle):
|
class XYScopedRateThrottle(ScopedRateThrottle):
|
||||||
TIMER_SECONDS = 0
|
TIMER_SECONDS = 0
|
||||||
THROTTLE_RATES = {'x': '3/min', 'y': '1/min'}
|
THROTTLE_RATES = {'x': '3/min', 'y': '1/min'}
|
||||||
|
@ -288,6 +295,18 @@ class ScopedRateThrottleTests(TestCase):
|
||||||
response = self.unscoped_view(request)
|
response = self.unscoped_view(request)
|
||||||
assert response.status_code == 200
|
assert response.status_code == 200
|
||||||
|
|
||||||
|
def test_get_cache_key_returns_correct_key_if_user_is_authenticated(self):
|
||||||
|
class DummyView(object):
|
||||||
|
throttle_scope = 'user'
|
||||||
|
|
||||||
|
request = Request(HttpRequest())
|
||||||
|
user = User.objects.create(username='test')
|
||||||
|
force_authenticate(request, user)
|
||||||
|
request.user = user
|
||||||
|
self.throttle.allow_request(request, DummyView())
|
||||||
|
cache_key = self.throttle.get_cache_key(request, view=DummyView())
|
||||||
|
assert cache_key == 'throttle_user_%s' % user.pk
|
||||||
|
|
||||||
|
|
||||||
class XffTestingBase(TestCase):
|
class XffTestingBase(TestCase):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
|
@ -354,3 +373,79 @@ class XffUniqueMachinesTest(XffTestingBase):
|
||||||
self.view(self.request)
|
self.view(self.request)
|
||||||
self.request.META['HTTP_X_FORWARDED_FOR'] = '0.0.0.0, 7.7.7.7, 2.2.2.2'
|
self.request.META['HTTP_X_FORWARDED_FOR'] = '0.0.0.0, 7.7.7.7, 2.2.2.2'
|
||||||
assert self.view(self.request).status_code == 200
|
assert self.view(self.request).status_code == 200
|
||||||
|
|
||||||
|
|
||||||
|
class BaseThrottleTests(TestCase):
|
||||||
|
|
||||||
|
def test_allow_request_raises_not_implemented_error(self):
|
||||||
|
with pytest.raises(NotImplementedError):
|
||||||
|
BaseThrottle().allow_request(request={}, view={})
|
||||||
|
|
||||||
|
|
||||||
|
class SimpleRateThrottleTests(TestCase):
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
SimpleRateThrottle.scope = 'anon'
|
||||||
|
|
||||||
|
def test_get_rate_raises_error_if_scope_is_missing(self):
|
||||||
|
throttle = SimpleRateThrottle()
|
||||||
|
with pytest.raises(ImproperlyConfigured):
|
||||||
|
throttle.scope = None
|
||||||
|
throttle.get_rate()
|
||||||
|
|
||||||
|
def test_throttle_raises_error_if_rate_is_missing(self):
|
||||||
|
SimpleRateThrottle.scope = 'invalid scope'
|
||||||
|
with pytest.raises(ImproperlyConfigured):
|
||||||
|
SimpleRateThrottle()
|
||||||
|
|
||||||
|
def test_parse_rate_returns_tuple_with_none_if_rate_not_provided(self):
|
||||||
|
rate = SimpleRateThrottle().parse_rate(None)
|
||||||
|
assert rate == (None, None)
|
||||||
|
|
||||||
|
def test_allow_request_returns_true_if_rate_is_none(self):
|
||||||
|
assert SimpleRateThrottle().allow_request(request={}, view={}) is True
|
||||||
|
|
||||||
|
def test_get_cache_key_raises_not_implemented_error(self):
|
||||||
|
with pytest.raises(NotImplementedError):
|
||||||
|
SimpleRateThrottle().get_cache_key({}, {})
|
||||||
|
|
||||||
|
def test_allow_request_returns_true_if_key_is_none(self):
|
||||||
|
throttle = SimpleRateThrottle()
|
||||||
|
throttle.rate = 'some rate'
|
||||||
|
throttle.get_cache_key = lambda *args: None
|
||||||
|
assert throttle.allow_request(request={}, view={}) is True
|
||||||
|
|
||||||
|
def test_wait_returns_correct_waiting_time_without_history(self):
|
||||||
|
throttle = SimpleRateThrottle()
|
||||||
|
throttle.num_requests = 1
|
||||||
|
throttle.duration = 60
|
||||||
|
throttle.history = []
|
||||||
|
waiting_time = throttle.wait()
|
||||||
|
assert isinstance(waiting_time, float)
|
||||||
|
assert waiting_time == 30.0
|
||||||
|
|
||||||
|
def test_wait_returns_none_if_there_are_no_available_requests(self):
|
||||||
|
throttle = SimpleRateThrottle()
|
||||||
|
throttle.num_requests = 1
|
||||||
|
throttle.duration = 60
|
||||||
|
throttle.now = throttle.timer()
|
||||||
|
throttle.history = [throttle.timer() for _ in range(3)]
|
||||||
|
assert throttle.wait() is None
|
||||||
|
|
||||||
|
|
||||||
|
class AnonRateThrottleTests(TestCase):
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
self.throttle = AnonRateThrottle()
|
||||||
|
|
||||||
|
def test_authenticated_user_not_affected(self):
|
||||||
|
request = Request(HttpRequest())
|
||||||
|
user = User.objects.create(username='test')
|
||||||
|
force_authenticate(request, user)
|
||||||
|
request.user = user
|
||||||
|
assert self.throttle.get_cache_key(request, view={}) is None
|
||||||
|
|
||||||
|
def test_get_cache_key_returns_correct_value(self):
|
||||||
|
request = Request(HttpRequest())
|
||||||
|
cache_key = self.throttle.get_cache_key(request, view={})
|
||||||
|
assert cache_key == 'throttle_anon_None'
|
||||||
|
|
|
@ -1,10 +1,15 @@
|
||||||
import datetime
|
import datetime
|
||||||
|
|
||||||
from django.db import models
|
import pytest
|
||||||
|
from django.db import DataError, models
|
||||||
from django.test import TestCase
|
from django.test import TestCase
|
||||||
|
|
||||||
from rest_framework import serializers
|
from rest_framework import serializers
|
||||||
from rest_framework.validators import UniqueValidator
|
from rest_framework.exceptions import ValidationError
|
||||||
|
from rest_framework.validators import (
|
||||||
|
BaseUniqueForValidator, UniqueTogetherValidator, UniqueValidator,
|
||||||
|
qs_exists
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def dedent(blocktext):
|
def dedent(blocktext):
|
||||||
|
@ -319,6 +324,23 @@ class TestUniquenessTogetherValidation(TestCase):
|
||||||
serializer = NullUniquenessTogetherSerializer(data=data)
|
serializer = NullUniquenessTogetherSerializer(data=data)
|
||||||
assert not serializer.is_valid()
|
assert not serializer.is_valid()
|
||||||
|
|
||||||
|
def test_filter_queryset_do_not_skip_existing_attribute(self):
|
||||||
|
"""
|
||||||
|
filter_queryset should add value from existing instance attribute
|
||||||
|
if it is not provided in attributes dict
|
||||||
|
"""
|
||||||
|
class MockQueryset(object):
|
||||||
|
def filter(self, **kwargs):
|
||||||
|
self.called_with = kwargs
|
||||||
|
|
||||||
|
data = {'race_name': 'bar'}
|
||||||
|
queryset = MockQueryset()
|
||||||
|
validator = UniqueTogetherValidator(queryset, fields=('race_name',
|
||||||
|
'position'))
|
||||||
|
validator.instance = self.instance
|
||||||
|
validator.filter_queryset(attrs=data, queryset=queryset)
|
||||||
|
assert queryset.called_with == {'race_name': 'bar', 'position': 1}
|
||||||
|
|
||||||
|
|
||||||
# Tests for `UniqueForDateValidator`
|
# Tests for `UniqueForDateValidator`
|
||||||
# ----------------------------------
|
# ----------------------------------
|
||||||
|
@ -389,6 +411,84 @@ class TestUniquenessForDateValidation(TestCase):
|
||||||
'published': datetime.date(2000, 1, 1)
|
'published': datetime.date(2000, 1, 1)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Tests for `UniqueForMonthValidator`
|
||||||
|
# ----------------------------------
|
||||||
|
|
||||||
|
|
||||||
|
class UniqueForMonthModel(models.Model):
|
||||||
|
slug = models.CharField(max_length=100, unique_for_month='published')
|
||||||
|
published = models.DateField()
|
||||||
|
|
||||||
|
|
||||||
|
class UniqueForMonthSerializer(serializers.ModelSerializer):
|
||||||
|
class Meta:
|
||||||
|
model = UniqueForMonthModel
|
||||||
|
fields = '__all__'
|
||||||
|
|
||||||
|
|
||||||
|
class UniqueForMonthTests(TestCase):
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
self.instance = UniqueForMonthModel.objects.create(
|
||||||
|
slug='existing', published='2017-01-01'
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_not_unique_for_month(self):
|
||||||
|
data = {'slug': 'existing', 'published': '2017-01-01'}
|
||||||
|
serializer = UniqueForMonthSerializer(data=data)
|
||||||
|
assert not serializer.is_valid()
|
||||||
|
assert serializer.errors == {
|
||||||
|
'slug': ['This field must be unique for the "published" month.']
|
||||||
|
}
|
||||||
|
|
||||||
|
def test_unique_for_month(self):
|
||||||
|
data = {'slug': 'existing', 'published': '2017-02-01'}
|
||||||
|
serializer = UniqueForMonthSerializer(data=data)
|
||||||
|
assert serializer.is_valid()
|
||||||
|
assert serializer.validated_data == {
|
||||||
|
'slug': 'existing',
|
||||||
|
'published': datetime.date(2017, 2, 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
# Tests for `UniqueForYearValidator`
|
||||||
|
# ----------------------------------
|
||||||
|
|
||||||
|
|
||||||
|
class UniqueForYearModel(models.Model):
|
||||||
|
slug = models.CharField(max_length=100, unique_for_year='published')
|
||||||
|
published = models.DateField()
|
||||||
|
|
||||||
|
|
||||||
|
class UniqueForYearSerializer(serializers.ModelSerializer):
|
||||||
|
class Meta:
|
||||||
|
model = UniqueForYearModel
|
||||||
|
fields = '__all__'
|
||||||
|
|
||||||
|
|
||||||
|
class UniqueForYearTests(TestCase):
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
self.instance = UniqueForYearModel.objects.create(
|
||||||
|
slug='existing', published='2017-01-01'
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_not_unique_for_year(self):
|
||||||
|
data = {'slug': 'existing', 'published': '2017-01-01'}
|
||||||
|
serializer = UniqueForYearSerializer(data=data)
|
||||||
|
assert not serializer.is_valid()
|
||||||
|
assert serializer.errors == {
|
||||||
|
'slug': ['This field must be unique for the "published" year.']
|
||||||
|
}
|
||||||
|
|
||||||
|
def test_unique_for_year(self):
|
||||||
|
data = {'slug': 'existing', 'published': '2018-01-01'}
|
||||||
|
serializer = UniqueForYearSerializer(data=data)
|
||||||
|
assert serializer.is_valid()
|
||||||
|
assert serializer.validated_data == {
|
||||||
|
'slug': 'existing',
|
||||||
|
'published': datetime.date(2018, 1, 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
class HiddenFieldUniqueForDateModel(models.Model):
|
class HiddenFieldUniqueForDateModel(models.Model):
|
||||||
slug = models.CharField(max_length=100, unique_for_date='published')
|
slug = models.CharField(max_length=100, unique_for_date='published')
|
||||||
|
@ -429,3 +529,37 @@ class TestHiddenFieldUniquenessForDateValidation(TestCase):
|
||||||
validators = [<UniqueForDateValidator(queryset=HiddenFieldUniqueForDateModel.objects.all(), field='slug', date_field='published')>]
|
validators = [<UniqueForDateValidator(queryset=HiddenFieldUniqueForDateModel.objects.all(), field='slug', date_field='published')>]
|
||||||
""")
|
""")
|
||||||
assert repr(serializer) == expected
|
assert repr(serializer) == expected
|
||||||
|
|
||||||
|
|
||||||
|
class ValidatorsTests(TestCase):
|
||||||
|
|
||||||
|
def test_qs_exists_handles_type_error(self):
|
||||||
|
class TypeErrorQueryset(object):
|
||||||
|
def exists(self):
|
||||||
|
raise TypeError
|
||||||
|
assert qs_exists(TypeErrorQueryset()) is False
|
||||||
|
|
||||||
|
def test_qs_exists_handles_value_error(self):
|
||||||
|
class ValueErrorQueryset(object):
|
||||||
|
def exists(self):
|
||||||
|
raise ValueError
|
||||||
|
assert qs_exists(ValueErrorQueryset()) is False
|
||||||
|
|
||||||
|
def test_qs_exists_handles_data_error(self):
|
||||||
|
class DataErrorQueryset(object):
|
||||||
|
def exists(self):
|
||||||
|
raise DataError
|
||||||
|
assert qs_exists(DataErrorQueryset()) is False
|
||||||
|
|
||||||
|
def test_validator_raises_error_if_not_all_fields_are_provided(self):
|
||||||
|
validator = BaseUniqueForValidator(queryset=object(), field='foo',
|
||||||
|
date_field='bar')
|
||||||
|
attrs = {'foo': 'baz'}
|
||||||
|
with pytest.raises(ValidationError):
|
||||||
|
validator.enforce_required_fields(attrs)
|
||||||
|
|
||||||
|
def test_validator_raises_error_when_abstract_method_called(self):
|
||||||
|
validator = BaseUniqueForValidator(queryset=object(), field='foo',
|
||||||
|
date_field='bar')
|
||||||
|
with pytest.raises(NotImplementedError):
|
||||||
|
validator.filter_queryset(attrs=None, queryset=None)
|
||||||
|
|
|
@ -1,8 +1,9 @@
|
||||||
import pytest
|
import pytest
|
||||||
from django.conf.urls import include, url
|
from django.conf.urls import url
|
||||||
from django.test import override_settings
|
from django.test import override_settings
|
||||||
|
|
||||||
from rest_framework import serializers, status, versioning
|
from rest_framework import serializers, status, versioning
|
||||||
|
from rest_framework.compat import include
|
||||||
from rest_framework.decorators import APIView
|
from rest_framework.decorators import APIView
|
||||||
from rest_framework.relations import PKOnlyObject
|
from rest_framework.relations import PKOnlyObject
|
||||||
from rest_framework.response import Response
|
from rest_framework.response import Response
|
||||||
|
@ -170,7 +171,7 @@ class TestURLReversing(URLPatternsTestCase):
|
||||||
]
|
]
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
url(r'^v1/', include(included, namespace='v1')),
|
url(r'^v1/', include(included, namespace='v1', app_name='v1')),
|
||||||
url(r'^another/$', dummy_view, name='another'),
|
url(r'^another/$', dummy_view, name='another'),
|
||||||
url(r'^(?P<version>[v1|v2]+)/another/$', dummy_view, name='another'),
|
url(r'^(?P<version>[v1|v2]+)/another/$', dummy_view, name='another'),
|
||||||
]
|
]
|
||||||
|
@ -335,8 +336,8 @@ class TestHyperlinkedRelatedField(URLPatternsTestCase):
|
||||||
]
|
]
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
url(r'^v1/', include(included, namespace='v1')),
|
url(r'^v1/', include(included, namespace='v1', app_name='v1')),
|
||||||
url(r'^v2/', include(included, namespace='v2'))
|
url(r'^v2/', include(included, namespace='v2', app_name='v2'))
|
||||||
]
|
]
|
||||||
|
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
|
@ -367,7 +368,7 @@ class TestNamespaceVersioningHyperlinkedRelatedFieldScheme(URLPatternsTestCase):
|
||||||
]
|
]
|
||||||
included = [
|
included = [
|
||||||
url(r'^namespaced/(?P<pk>\d+)/$', dummy_pk_view, name='namespaced'),
|
url(r'^namespaced/(?P<pk>\d+)/$', dummy_pk_view, name='namespaced'),
|
||||||
url(r'^nested/', include(nested, namespace='nested-namespace'))
|
url(r'^nested/', include(nested, namespace='nested-namespace', app_name='nested-namespace'))
|
||||||
]
|
]
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
|
|
34
tox.ini
|
@ -3,11 +3,19 @@ addopts=--tb=short
|
||||||
|
|
||||||
[tox]
|
[tox]
|
||||||
envlist =
|
envlist =
|
||||||
py27-{lint,docs},
|
|
||||||
{py27,py33,py34,py35}-django18,
|
{py27,py33,py34,py35}-django18,
|
||||||
{py27,py34,py35}-django19,
|
{py27,py34,py35}-django{19,110},
|
||||||
{py27,py34,py35}-django110,
|
{py27,py34,py35,py36}-django111,
|
||||||
{py27,py34,py35}-django{master}
|
{py35,py36}-djangomaster
|
||||||
|
lint,docs
|
||||||
|
|
||||||
|
[travis:env]
|
||||||
|
DJANGO =
|
||||||
|
1.8: django18
|
||||||
|
1.9: django19
|
||||||
|
1.10: django110
|
||||||
|
1.11: django111
|
||||||
|
master: djangomaster
|
||||||
|
|
||||||
[testenv]
|
[testenv]
|
||||||
commands = ./runtests.py --fast {posargs} --coverage -rw
|
commands = ./runtests.py --fast {posargs} --coverage -rw
|
||||||
|
@ -15,25 +23,23 @@ setenv =
|
||||||
PYTHONDONTWRITEBYTECODE=1
|
PYTHONDONTWRITEBYTECODE=1
|
||||||
PYTHONWARNINGS=once
|
PYTHONWARNINGS=once
|
||||||
deps =
|
deps =
|
||||||
django18: Django==1.8.17
|
django18: Django>=1.8,<1.9
|
||||||
django19: Django==1.9.12
|
django19: Django>=1.9,<1.10
|
||||||
django110: Django==1.10.5
|
django110: Django>=1.10,<1.11
|
||||||
|
django111: Django>=1.11a1,<2.0
|
||||||
djangomaster: https://github.com/django/django/archive/master.tar.gz
|
djangomaster: https://github.com/django/django/archive/master.tar.gz
|
||||||
-rrequirements/requirements-testing.txt
|
-rrequirements/requirements-testing.txt
|
||||||
-rrequirements/requirements-optionals.txt
|
-rrequirements/requirements-optionals.txt
|
||||||
basepython =
|
|
||||||
py35: python3.5
|
|
||||||
py34: python3.4
|
|
||||||
py33: python3.3
|
|
||||||
py27: python2.7
|
|
||||||
|
|
||||||
[testenv:py27-lint]
|
[testenv:lint]
|
||||||
|
basepython = python2.7
|
||||||
commands = ./runtests.py --lintonly
|
commands = ./runtests.py --lintonly
|
||||||
deps =
|
deps =
|
||||||
-rrequirements/requirements-codestyle.txt
|
-rrequirements/requirements-codestyle.txt
|
||||||
-rrequirements/requirements-testing.txt
|
-rrequirements/requirements-testing.txt
|
||||||
|
|
||||||
[testenv:py27-docs]
|
[testenv:docs]
|
||||||
|
basepython = python2.7
|
||||||
commands = mkdocs build
|
commands = mkdocs build
|
||||||
deps =
|
deps =
|
||||||
-rrequirements/requirements-testing.txt
|
-rrequirements/requirements-testing.txt
|
||||||
|
|