Merge branch 'master' into docs

This commit is contained in:
Tom Christie 2017-02-08 14:35:26 +00:00
commit 0b8cd3683b
72 changed files with 1174 additions and 348 deletions

View File

@ -1,44 +1,51 @@
language: python
python:
- "2.7"
- "3.4"
- "3.5"
sudo: false
env:
- TOX_ENV=py27-lint
- TOX_ENV=py27-docs
- TOX_ENV=py35-django19
- TOX_ENV=py34-django19
- TOX_ENV=py27-django19
- 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
- DJANGO=1.8
- DJANGO=1.9
- DJANGO=1.10
- DJANGO=1.11
- DJANGO=master
matrix:
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:
- env: TOX_ENV=py27-djangomaster
- env: TOX_ENV=py34-djangomaster
- env: TOX_ENV=py35-djangomaster
- env: DJANGO=master
- env: DJANGO=1.11
install:
# Virtualenv < 14 is required to keep the Python 3.2 builds running.
- pip install tox "virtualenv<14"
- pip install tox tox-travis
script:
- tox -e $TOX_ENV
- tox
after_success:
- pip install codecov
- codecov -e TOX_ENV
- codecov -e TOXENV,DJANGO
notifications:
email: false

View File

@ -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="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="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>
*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).*
---

View File

@ -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.
**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.
- `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:

View File

@ -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.
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
[no-options]: https://www.mnot.net/blog/2012/10/29/NO_OPTIONS

View File

@ -259,12 +259,16 @@ The [REST Condition][rest-condition] package is another extension for building c
## 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
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
[authentication]: authentication.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
[dry-rest-permissions]: https://github.com/Helioscene/dry-rest-permissions
[django-rest-framework-roles]: https://github.com/computer-lab/django-rest-framework-roles
[django-rest-framework-api-key]: https://github.com/manosim/django-rest-framework-api-key

View File

@ -118,6 +118,26 @@ The above example would now generate the following URL pattern:
* 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].
# API Guide

View File

@ -145,6 +145,18 @@ May be used to pass a canonical URL for the schema.
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`
May be used to pass the set of renderer classes that can be used to render the API root endpoint.

View File

@ -1157,7 +1157,7 @@ The [html-json-forms][html-json-forms] package provides an algorithm and seriali
## 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

View File

@ -241,7 +241,7 @@ Default:
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'`.
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`

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 54 KiB

After

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 26 KiB

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

After

Width:  |  Height:  |  Size: 19 KiB

View File

@ -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="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="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>
<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.
[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.

View File

@ -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'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.

View File

@ -89,7 +89,7 @@ Name | Support | PyPI pa
---------------------------------|-------------------------------------|--------------------------------
[Core JSON][core-json] | Schema generation & client support. | Built-in support in `coreapi`.
[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.
---

View File

@ -217,7 +217,7 @@ credentials, headers and bookmarks:
# 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.
## Getting started

View File

@ -105,7 +105,7 @@ If the python `markdown` library is installed, then [markdown syntax][markdown]
[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
@ -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-apiary]: ../img/apiary.png
[image-self-describing-api]: ../img/self-describing.png
[schemas-examples]: api-guide/schemas/#examples

View File

@ -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.
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>.
---

View File

@ -102,7 +102,7 @@ Our gold sponsors include companies large and small. Many thanks for their signi
### Silver sponsors
The serious financial contribution that our silver sponsors have made is very much appreciated. I'd like to say a particular thank&nbsp;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&nbsp;you to individuals who have chosen to privately support the project at this level.
<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>

View File

@ -228,7 +228,7 @@ You can determine your currently installed version using `pip freeze`:
* Fixed use of deprecated Query.aggregates. ([#4003][gh4003])
* Fix blank lines around docstrings. ([#4002][gh4002])
* Fixed admin pagination when limit is 0. ([#3990][gh3990])
* OrderingFilter adjustements. ([#3983][gh3983])
* OrderingFilter adjustments. ([#3983][gh3983])
* Non-required serializer related fields. ([#3976][gh3976])
* Using safer calling way of "@api_view" in tutorial. ([#3971][gh3971])
* ListSerializer doesn't handle unique_together constraints. ([#3970][gh3970])

View File

@ -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.
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 rest_framework.urlpatterns import format_suffix_patterns

View File

@ -1,4 +1,4 @@
# PyTest for running the tests.
pytest==2.9.1
pytest-django==2.9.1
pytest-cov==1.8.1
pytest==3.0.5
pytest-django==3.1.2
pytest-cov==2.4.0

View File

@ -1,4 +1,4 @@
"""
r"""
______ _____ _____ _____ __
| ___ \ ___/ ___|_ _| / _| | |
| |_/ / |__ \ `--. | | | |_ _ __ __ _ _ __ ___ _____ _____ _ __| |__

View File

@ -17,11 +17,6 @@ from django.template import Context, RequestContext, Template
from django.utils import six
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:
from django.urls import (
@ -319,3 +314,10 @@ def set_many(instance, field, value):
else:
field = getattr(instance, field)
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)

View File

@ -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.functional import cached_property
from django.utils.ipv6 import clean_ipv6_address
from django.utils.timezone import utc
from django.utils.translation import ugettext_lazy as _
from rest_framework import ISO_8601
@ -149,7 +150,7 @@ def to_choices_dict(choices):
# choices = [('Category', ((1, 'First'), (2, 'Second'))), (3, 'Third')]
ret = OrderedDict()
for choice in choices:
if (not isinstance(choice, (list, tuple))):
if not isinstance(choice, (list, tuple)):
# single choice
ret[choice] = choice
else:
@ -1104,7 +1105,7 @@ class DateTimeField(Field):
if (field_timezone is not None) and not timezone.is_aware(value):
return timezone.make_aware(value, field_timezone)
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
def default_timezone(self):
@ -1504,12 +1505,16 @@ class ListField(Field):
initial = []
default_error_messages = {
'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):
self.child = kwargs.pop('child', copy.deepcopy(self.child))
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 self.child.source is None, (
@ -1519,6 +1524,12 @@ class ListField(Field):
super(ListField, self).__init__(*args, **kwargs)
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):
if self.field_name not in dictionary:
@ -1614,7 +1625,7 @@ class JSONField(Field):
def get_value(self, dictionary):
if html.is_html_input(dictionary) and self.field_name in dictionary:
# 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):
def __new__(self, value):
ret = six.text_type.__new__(self, value)

View File

@ -132,7 +132,7 @@ def _reverse_ordering(ordering_tuple):
ordering and return a new tuple, eg. `('created', '-uuid')`.
"""
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])

View File

@ -503,7 +503,7 @@ class ManyRelatedField(Field):
return []
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):
return [

View File

@ -228,7 +228,7 @@ class StaticHTMLRenderer(TemplateHTMLRenderer):
def render(self, data, accepted_media_type=None, renderer_context=None):
renderer_context = renderer_context or {}
response = renderer_context['response']
response = renderer_context.get('response')
if response and response.exception:
request = renderer_context['request']
@ -540,7 +540,7 @@ class BrowsableAPIRenderer(BaseRenderer):
# If possible, serialize the initial content for the generic form
default_parser = view.parser_classes[0]
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
# corresponding renderer that can be used to render the data.
@ -598,7 +598,7 @@ class BrowsableAPIRenderer(BaseRenderer):
paginator = getattr(view, 'paginator', None)
if isinstance(data, list):
pass
elif (paginator is not None and data is not None):
elif paginator is not None and data is not None:
try:
paginator.get_results(data)
except (TypeError, KeyError):
@ -738,7 +738,7 @@ class AdminRenderer(BrowsableAPIRenderer):
ret = template_render(template, context, request=renderer_context['request'])
# 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['Location'] = request.build_absolute_uri()
ret = ''
@ -764,7 +764,7 @@ class AdminRenderer(BrowsableAPIRenderer):
)
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:
results = paginator.get_results(data)
except (TypeError, KeyError):

View File

@ -152,7 +152,7 @@ class Request(object):
force_user = getattr(request, '_force_auth_user', 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)
self.authenticators = (forced_auth,)

View File

@ -179,10 +179,11 @@ class SimpleRouter(BaseRouter):
initkwargs = route.initkwargs.copy()
initkwargs.update(method_kwargs)
url_path = initkwargs.pop("url_path", None) or methodname
url_name = initkwargs.pop("url_name", None) or url_path
ret.append(Route(
url=replace_methodname(route.url, url_path),
mapping={httpmethod: methodname for httpmethod in httpmethods},
name=replace_methodname(route.name, url_path),
name=replace_methodname(route.name, url_name),
initkwargs=initkwargs,
))

View File

@ -661,11 +661,11 @@ class SchemaGenerator(object):
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.
"""
generator = SchemaGenerator(title=title, url=url)
generator = SchemaGenerator(title=title, url=url, urlconf=urlconf)
if renderer_classes is None:
if renderers.BrowsableAPIRenderer in api_settings.DEFAULT_RENDERER_CLASSES:
rclasses = [renderers.CoreJSONRenderer, renderers.BrowsableAPIRenderer]

View File

@ -1177,7 +1177,7 @@ class ModelSerializer(Serializer):
if postgres_fields and isinstance(model_field, postgres_fields.ArrayField):
# 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_field_class, child_field_kwargs = self.build_standard_field(
'child', child_model_field

View File

@ -18,13 +18,13 @@ REST framework settings, checking for user settings first, then falling
back to the defaults.
"""
from __future__ import unicode_literals
from importlib import import_module
from django.conf import settings
from django.test.signals import setting_changed
from django.utils import six
from rest_framework import ISO_8601
from rest_framework.compat import importlib
DEFAULTS = {
# Base API policies
@ -174,7 +174,7 @@ def import_from_string(val, setting_name):
# Nod to tastypie's use of importlib.
parts = val.split('.')
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)
except (ImportError, AttributeError) as e:
msg = "Could not import '%s' for API setting '%s'. %s: %s." % (val, setting_name, e.__class__.__name__, e)

View File

@ -21,7 +21,7 @@
<div id="div_id_username" class="clearfix control-group {% if form.username.errors %}error{% endif %}">
<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"
autocapitalize="off"
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 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>
{% if form.password.errors %}
<p class="text-error">

View File

@ -114,7 +114,7 @@ if requests is not None:
self.mount('https://', adapter)
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)
return super(RequestsClient, self).request(method, url, *args, **kwargs)

View File

@ -1,8 +1,8 @@
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

View File

@ -55,6 +55,11 @@ class JSONEncoder(json.JSONEncoder):
elif hasattr(obj, 'tolist'):
# Numpy arrays and array scalars.
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__'):
try:
return dict(obj)
@ -62,9 +67,4 @@ class JSONEncoder(json.JSONEncoder):
pass
elif hasattr(obj, '__iter__'):
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)

View File

@ -32,23 +32,18 @@ def dedent(content):
unindented text on the initial line.
"""
content = force_text(content)
whitespace_counts = [
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()
]
lines = [line for line in content.splitlines()[1:] if line.lstrip()]
# unindent the content if needed
if lines:
whitespace_counts = min([len(line) - len(line.lstrip(' ')) for line in lines])
tab_counts = min([len(line) - len(line.lstrip('\t')) for line in lines])
if whitespace_counts:
whitespace_pattern = '^' + (' ' * min(whitespace_counts))
whitespace_pattern = '^' + (' ' * whitespace_counts)
content = re.sub(re.compile(whitespace_pattern, re.MULTILINE), '', content)
elif tab_counts:
whitespace_pattern = '^' + ('\t' * min(whitespace_counts))
whitespace_pattern = '^' + ('\t' * tab_counts)
content = re.sub(re.compile(whitespace_pattern, re.MULTILINE), '', content)
return content.strip()

View File

@ -49,10 +49,8 @@ def order_by_precedence(media_type_lst):
@python_2_unicode_compatible
class _MediaType(object):
def __init__(self, media_type_str):
if media_type_str is None:
media_type_str = ''
self.orig = media_type_str
self.full_type, self.params = parse_header(media_type_str.encode(HTTP_HEADER_ENCODING))
self.orig = '' if (media_type_str is None) else media_type_str
self.full_type, self.params = parse_header(self.orig.encode(HTTP_HEADER_ENCODING))
self.main_type, sep, self.sub_type = self.full_type.partition('/')
def match(self, other):

View File

@ -76,7 +76,12 @@ def _get_forward_relationships(opts):
Returns an `OrderedDict` of field names to `RelationInfo`.
"""
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(
model_field=field,
related_model=get_related_model(field),

View File

@ -117,7 +117,7 @@ class NamespaceVersioning(BaseVersioning):
def determine_version(self, request, *args, **kwargs):
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
# Allow for possibly nested namespaces.

View File

@ -4,6 +4,7 @@ from __future__ import unicode_literals
import base64
import pytest
from django.conf.urls import include, url
from django.contrib.auth.models import User
from django.db import models
@ -151,6 +152,18 @@ class BasicAuthTests(TestCase):
assert response.status_code == status.HTTP_401_UNAUTHORIZED
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')
class SessionAuthTests(TestCase):
@ -249,6 +262,17 @@ class BaseTokenAuthTests(object):
)
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):
# use a nonexistent token key
auth = self.header_prefix + 'wxyz6789'
@ -257,6 +281,19 @@ class BaseTokenAuthTests(object):
)
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):
# add an 'invalid' unicode character
auth = self.header_prefix + self.key + "¸"
@ -461,3 +498,28 @@ class NoAuthenticationClassesTests(TestCase):
response = view(request)
assert response.status_code == status.HTTP_403_FORBIDDEN
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
View 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({})

View File

@ -45,6 +45,15 @@ class TestSimpleBoundField:
assert serializer['amount'].errors is None
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):
class ExampleSerializer(serializers.Serializer):
bool_field = serializers.BooleanField()

67
tests/test_compat.py Normal file
View 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

View File

@ -124,4 +124,8 @@ class TestViewNamesAndDescriptions(TestCase):
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

View File

@ -2,8 +2,10 @@ from datetime import date, datetime, timedelta, tzinfo
from decimal import Decimal
from uuid import uuid4
import pytest
from django.test import TestCase
from rest_framework.compat import coreapi
from rest_framework.utils.encoders import JSONEncoder
@ -56,7 +58,7 @@ class JSONEncoderTests(TestCase):
current_time = datetime.now().time()
current_time = current_time.replace(tzinfo=UTC())
with self.assertRaises(ValueError):
with pytest.raises(ValueError):
self.encoder.default(current_time)
def test_encode_date(self):
@ -79,3 +81,13 @@ class JSONEncoderTests(TestCase):
"""
unique_id = uuid4()
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())

View File

@ -8,7 +8,8 @@ from decimal import Decimal
import pytest
from django.http import QueryDict
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
from rest_framework import serializers
@ -1129,13 +1130,13 @@ class TestDateTimeField(FieldValues):
Valid and invalid values for `DateTimeField`.
"""
valid_inputs = {
'2001-01-01 13:00': datetime.datetime(2001, 1, 1, 13, 00, tzinfo=timezone.UTC()),
'2001-01-01T13:00': datetime.datetime(2001, 1, 1, 13, 00, tzinfo=timezone.UTC()),
'2001-01-01T13:00Z': 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=timezone.UTC()),
datetime.datetime(2001, 1, 1, 13, 00, tzinfo=timezone.UTC()): 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=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=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.
'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 = {
'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 = {
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',
six.text_type('2016-01-10T00:00:00'): '2016-01-10T00:00:00',
None: None,
'': None,
}
field = serializers.DateTimeField(default_timezone=timezone.UTC())
field = serializers.DateTimeField(default_timezone=utc)
class TestCustomInputFormatDateTimeField(FieldValues):
@ -1158,13 +1159,13 @@ class TestCustomInputFormatDateTimeField(FieldValues):
Valid and invalid values for `DateTimeField` with a custom input format.
"""
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 = {
'2001-01-01T20:50': ['Datetime has wrong format. Use one of these formats instead: hh:mm[AM|PM], DD [Jan-Dec] YYYY.']
}
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):
@ -1196,7 +1197,7 @@ class TestNaiveDateTimeField(FieldValues):
Valid and invalid values for `DateTimeField` with naive datetimes.
"""
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),
}
invalid_inputs = {}
@ -1667,6 +1668,16 @@ class TestEmptyListField(FieldValues):
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):
"""
Values for `ListField` with no `child` argument.

View File

@ -5,6 +5,7 @@ import unittest
import warnings
from decimal import Decimal
import pytest
from django.conf.urls import url
from django.core.exceptions import ImproperlyConfigured
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):
def _serialize_object(self, obj):
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'}
]
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):
class SearchListView(generics.ListAPIView):
queryset = SearchFilterModel.objects.all()

View File

@ -547,3 +547,94 @@ class TestGuardedQueryset(TestCase):
request = factory.get('/')
with pytest.raises(RuntimeError):
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

View File

@ -1,8 +1,9 @@
from __future__ import unicode_literals
import django.template.loader
import pytest
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.template import Template, TemplateDoesNotExist
from django.test import TestCase, override_settings
@ -46,6 +47,12 @@ urlpatterns = [
@override_settings(ROOT_URLCONF='tests.test_htmlrenderer')
class TemplateHTMLRendererTests(TestCase):
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
"""
@ -87,6 +94,40 @@ class TemplateHTMLRendererTests(TestCase):
self.assertEqual(response.content, six.b("403 Forbidden"))
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')
class TemplateHTMLRendererExceptionTests(TestCase):

View File

@ -1,5 +1,6 @@
from __future__ import unicode_literals
import pytest
from django.core.validators import MaxValueValidator, MinValueValidator
from django.db import models
from django.test import TestCase
@ -17,6 +18,11 @@ request = Request(APIRequestFactory().options('/'))
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):
"""
OPTIONS requests to views should return a valid 200 response.
@ -263,12 +269,25 @@ class TestMetadata:
view = ExampleView.as_view(versioning_class=scheme)
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):
def test_null_boolean_field_info_type(self):
options = metadata.SimpleMetadata()
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):
options = metadata.SimpleMetadata()
@ -277,7 +296,7 @@ class TestSimpleMetadataFieldInfo(TestCase):
field_info = options.get_field_info(
serializers.RelatedField(queryset=BasicModel.objects.all())
)
self.assertNotIn('choices', field_info)
assert 'choices' not in field_info
class TestModelSerializerMetadata(TestCase):

View File

@ -1,11 +1,16 @@
from __future__ import unicode_literals
import pytest
from django.http import Http404
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.request import Request
from rest_framework.test import APIRequestFactory
from rest_framework.utils.mediatypes import _MediaType
factory = APIRequestFactory()
@ -55,3 +60,46 @@ class TestAcceptedMediaType(TestCase):
accepted_renderer, accepted_media_type = self.select_renderer(request)
assert accepted_media_type == 'application/openapi+json;version=2.0'
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)

View File

@ -14,7 +14,7 @@ from tests.test_multitable_inheritance import ChildModel
# Regression test for #4290
class ChildAssociatedModel(RESTFrameworkModel):
child_model = models.OneToOneField(ChildModel)
child_model = models.OneToOneField(ChildModel, on_delete=models.CASCADE)
child_name = models.CharField(max_length=100)

View File

@ -370,6 +370,13 @@ class TestLimitOffset:
assert self.pagination.display_page_controls
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):
"""
When the offset is not a multiple of the limit we get some edge cases:

View File

@ -35,7 +35,7 @@ class TestFormParser(TestCase):
stream = StringIO(self.string)
data = parser.parse(stream)
self.assertEqual(Form(data).is_valid(), True)
assert Form(data).is_valid() is True
class TestFileUploadParser(TestCase):
@ -62,7 +62,7 @@ class TestFileUploadParser(TestCase):
self.stream.seek(0)
data_and_files = parser.parse(self.stream, None, self.parser_context)
file_obj = data_and_files.files['file']
self.assertEqual(file_obj._size, 14)
assert file_obj._size == 14
def test_parse_missing_filename(self):
"""
@ -108,22 +108,22 @@ class TestFileUploadParser(TestCase):
def test_get_filename(self):
parser = FileUploadParser()
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):
parser = FileUploadParser()
self.__replace_content_disposition('inline; filename*=utf-8\'\'ÀĥƦ.txt')
filename = parser.get_filename(self.stream, None, self.parser_context)
self.assertEqual(filename, 'ÀĥƦ.txt')
assert filename == 'ÀĥƦ.txt'
self.__replace_content_disposition('inline; filename=fallback.txt; filename*=utf-8\'\'ÀĥƦ.txt')
filename = parser.get_filename(self.stream, None, self.parser_context)
self.assertEqual(filename, 'ÀĥƦ.txt')
assert filename == 'ÀĥƦ.txt'
self.__replace_content_disposition('inline; filename=fallback.txt; filename*=utf-8\'en-us\'ÀĥƦ.txt')
filename = parser.get_filename(self.stream, None, self.parser_context)
self.assertEqual(filename, 'ÀĥƦ.txt')
assert filename == 'ÀĥƦ.txt'
def __replace_content_disposition(self, disposition):
self.parser_context['request'].META['HTTP_CONTENT_DISPOSITION'] = disposition

View File

@ -91,7 +91,7 @@ class HyperlinkedManyToManyTests(TestCase):
{'url': '/manytomanysource/3/', 'name': 'source-3', 'targets': ['/manytomanytarget/1/', '/manytomanytarget/2/', '/manytomanytarget/3/']}
]
with self.assertNumQueries(4):
self.assertEqual(serializer.data, expected)
assert serializer.data == expected
def test_many_to_many_retrieve(self):
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/']}
]
with self.assertNumQueries(4):
self.assertEqual(serializer.data, expected)
assert serializer.data == expected
def test_many_to_many_retrieve_prefetch_related(self):
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/']}
]
with self.assertNumQueries(4):
self.assertEqual(serializer.data, expected)
assert serializer.data == expected
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/']}
instance = ManyToManySource.objects.get(pk=1)
serializer = ManyToManySourceSerializer(instance, data=data, context={'request': request})
self.assertTrue(serializer.is_valid())
assert serializer.is_valid()
serializer.save()
self.assertEqual(serializer.data, data)
assert serializer.data == data
# Ensure source 1 is updated, and everything else is as expected
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/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):
data = {'url': 'http://testserver/manytomanytarget/1/', 'name': 'target-1', 'sources': ['http://testserver/manytomanysource/1/']}
instance = ManyToManyTarget.objects.get(pk=1)
serializer = ManyToManyTargetSerializer(instance, data=data, context={'request': request})
self.assertTrue(serializer.is_valid())
assert serializer.is_valid()
serializer.save()
self.assertEqual(serializer.data, data)
assert serializer.data == data
# Ensure target 1 is updated, and everything else is as expected
queryset = ManyToManyTarget.objects.all()
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/']}
]
self.assertEqual(serializer.data, expected)
assert serializer.data == expected
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/']}
serializer = ManyToManySourceSerializer(data=data, context={'request': request})
self.assertTrue(serializer.is_valid())
assert serializer.is_valid()
obj = serializer.save()
self.assertEqual(serializer.data, data)
self.assertEqual(obj.name, 'source-4')
assert serializer.data == data
assert obj.name == 'source-4'
# Ensure source 4 is added, and everything else is as expected
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/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):
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})
self.assertTrue(serializer.is_valid())
assert serializer.is_valid()
obj = serializer.save()
self.assertEqual(serializer.data, data)
self.assertEqual(obj.name, 'target-4')
assert serializer.data == data
assert obj.name == 'target-4'
# Ensure target 4 is added, and everything else is as expected
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/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')
@ -217,7 +216,7 @@ class HyperlinkedForeignKeyTests(TestCase):
{'url': 'http://testserver/foreignkeysource/3/', 'name': 'source-3', 'target': 'http://testserver/foreignkeytarget/1/'}
]
with self.assertNumQueries(1):
self.assertEqual(serializer.data, expected)
assert serializer.data == expected
def test_reverse_foreign_key_retrieve(self):
queryset = ForeignKeyTarget.objects.all()
@ -227,15 +226,15 @@ class HyperlinkedForeignKeyTests(TestCase):
{'url': 'http://testserver/foreignkeytarget/2/', 'name': 'target-2', 'sources': []},
]
with self.assertNumQueries(3):
self.assertEqual(serializer.data, expected)
assert serializer.data == expected
def test_foreign_key_update(self):
data = {'url': 'http://testserver/foreignkeysource/1/', 'name': 'source-1', 'target': 'http://testserver/foreignkeytarget/2/'}
instance = ForeignKeySource.objects.get(pk=1)
serializer = ForeignKeySourceSerializer(instance, data=data, context={'request': request})
self.assertTrue(serializer.is_valid())
assert serializer.is_valid()
serializer.save()
self.assertEqual(serializer.data, data)
assert serializer.data == data
# Ensure source 1 is updated, and everything else is as expected
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/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):
data = {'url': 'http://testserver/foreignkeysource/1/', 'name': 'source-1', 'target': 2}
instance = ForeignKeySource.objects.get(pk=1)
serializer = ForeignKeySourceSerializer(instance, data=data, context={'request': request})
self.assertFalse(serializer.is_valid())
self.assertEqual(serializer.errors, {'target': ['Incorrect type. Expected URL string, received int.']})
assert not serializer.is_valid()
assert serializer.errors == {'target': ['Incorrect type. Expected URL string, received int.']}
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/']}
instance = ForeignKeyTarget.objects.get(pk=2)
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
# hasn't been called.
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/2/', 'name': 'target-2', 'sources': []},
]
self.assertEqual(new_serializer.data, expected)
assert new_serializer.data == expected
serializer.save()
self.assertEqual(serializer.data, data)
assert serializer.data == data
# Ensure target 2 is update, and everything else is as expected
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/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):
data = {'url': 'http://testserver/foreignkeysource/4/', 'name': 'source-4', 'target': 'http://testserver/foreignkeytarget/2/'}
serializer = ForeignKeySourceSerializer(data=data, context={'request': request})
self.assertTrue(serializer.is_valid())
assert serializer.is_valid()
obj = serializer.save()
self.assertEqual(serializer.data, data)
self.assertEqual(obj.name, 'source-4')
assert serializer.data == data
assert obj.name == 'source-4'
# Ensure source 1 is updated, and everything else is as expected
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/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):
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})
self.assertTrue(serializer.is_valid())
assert serializer.is_valid()
obj = serializer.save()
self.assertEqual(serializer.data, data)
self.assertEqual(obj.name, 'target-3')
assert serializer.data == data
assert obj.name == 'target-3'
# Ensure target 4 is added, and everything else is as expected
queryset = ForeignKeyTarget.objects.all()
@ -316,14 +315,14 @@ class HyperlinkedForeignKeyTests(TestCase):
{'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/']},
]
self.assertEqual(serializer.data, expected)
assert serializer.data == expected
def test_foreign_key_update_with_invalid_null(self):
data = {'url': 'http://testserver/foreignkeysource/1/', 'name': 'source-1', 'target': None}
instance = ForeignKeySource.objects.get(pk=1)
serializer = ForeignKeySourceSerializer(instance, data=data, context={'request': request})
self.assertFalse(serializer.is_valid())
self.assertEqual(serializer.errors, {'target': ['This field may not be null.']})
assert not serializer.is_valid()
assert serializer.errors == {'target': ['This field may not be null.']}
@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/3/', 'name': 'source-3', 'target': None},
]
self.assertEqual(serializer.data, expected)
assert serializer.data == expected
def test_foreign_key_create_with_valid_null(self):
data = {'url': 'http://testserver/nullableforeignkeysource/4/', 'name': 'source-4', 'target': None}
serializer = NullableForeignKeySourceSerializer(data=data, context={'request': request})
self.assertTrue(serializer.is_valid())
assert serializer.is_valid()
obj = serializer.save()
self.assertEqual(serializer.data, data)
self.assertEqual(obj.name, 'source-4')
assert serializer.data == data
assert obj.name == 'source-4'
# Ensure source 4 is created, and everything else is as expected
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/4/', 'name': 'source-4', 'target': None}
]
self.assertEqual(serializer.data, expected)
assert serializer.data == expected
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': ''}
expected_data = {'url': 'http://testserver/nullableforeignkeysource/4/', 'name': 'source-4', 'target': None}
serializer = NullableForeignKeySourceSerializer(data=data, context={'request': request})
self.assertTrue(serializer.is_valid())
assert serializer.is_valid()
obj = serializer.save()
self.assertEqual(serializer.data, expected_data)
self.assertEqual(obj.name, 'source-4')
assert serializer.data == expected_data
assert obj.name == 'source-4'
# Ensure source 4 is created, and everything else is as expected
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/4/', 'name': 'source-4', 'target': None}
]
self.assertEqual(serializer.data, expected)
assert serializer.data == expected
def test_foreign_key_update_with_valid_null(self):
data = {'url': 'http://testserver/nullableforeignkeysource/1/', 'name': 'source-1', 'target': None}
instance = NullableForeignKeySource.objects.get(pk=1)
serializer = NullableForeignKeySourceSerializer(instance, data=data, context={'request': request})
self.assertTrue(serializer.is_valid())
assert serializer.is_valid()
serializer.save()
self.assertEqual(serializer.data, data)
assert serializer.data == data
# Ensure source 1 is updated, and everything else is as expected
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/3/', 'name': 'source-3', 'target': None},
]
self.assertEqual(serializer.data, expected)
assert serializer.data == expected
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}
instance = NullableForeignKeySource.objects.get(pk=1)
serializer = NullableForeignKeySourceSerializer(instance, data=data, context={'request': request})
self.assertTrue(serializer.is_valid())
assert serializer.is_valid()
serializer.save()
self.assertEqual(serializer.data, expected_data)
assert serializer.data == expected_data
# Ensure source 1 is updated, and everything else is as expected
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/3/', 'name': 'source-3', 'target': None},
]
self.assertEqual(serializer.data, expected)
assert serializer.data == expected
@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/2/', 'name': 'target-2', 'nullable_source': None},
]
self.assertEqual(serializer.data, expected)
assert serializer.data == expected

View File

@ -61,7 +61,7 @@ class SlugForeignKeyTests(TestCase):
{'id': 3, 'name': 'source-3', 'target': 'target-1'}
]
with self.assertNumQueries(4):
self.assertEqual(serializer.data, expected)
assert serializer.data == expected
def test_foreign_key_retrieve_select_related(self):
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': 2, 'name': 'target-2', 'sources': []},
]
self.assertEqual(serializer.data, expected)
assert serializer.data == expected
def test_reverse_foreign_key_retrieve_prefetch_related(self):
queryset = ForeignKeyTarget.objects.all().prefetch_related('sources')
@ -88,9 +88,9 @@ class SlugForeignKeyTests(TestCase):
data = {'id': 1, 'name': 'source-1', 'target': 'target-2'}
instance = ForeignKeySource.objects.get(pk=1)
serializer = ForeignKeySourceSerializer(instance, data=data)
self.assertTrue(serializer.is_valid())
assert serializer.is_valid()
serializer.save()
self.assertEqual(serializer.data, data)
assert serializer.data == data
# Ensure source 1 is updated, and everything else is as expected
queryset = ForeignKeySource.objects.all()
@ -100,20 +100,20 @@ class SlugForeignKeyTests(TestCase):
{'id': 2, 'name': 'source-2', '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):
data = {'id': 1, 'name': 'source-1', 'target': 123}
instance = ForeignKeySource.objects.get(pk=1)
serializer = ForeignKeySourceSerializer(instance, data=data)
self.assertFalse(serializer.is_valid())
self.assertEqual(serializer.errors, {'target': ['Object with name=123 does not exist.']})
assert not serializer.is_valid()
assert serializer.errors == {'target': ['Object with name=123 does not exist.']}
def test_reverse_foreign_key_update(self):
data = {'id': 2, 'name': 'target-2', 'sources': ['source-1', 'source-3']}
instance = ForeignKeyTarget.objects.get(pk=2)
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
# hasn't been called.
queryset = ForeignKeyTarget.objects.all()
@ -122,10 +122,10 @@ class SlugForeignKeyTests(TestCase):
{'id': 1, 'name': 'target-1', 'sources': ['source-1', 'source-2', 'source-3']},
{'id': 2, 'name': 'target-2', 'sources': []},
]
self.assertEqual(new_serializer.data, expected)
assert new_serializer.data == expected
serializer.save()
self.assertEqual(serializer.data, data)
assert serializer.data == data
# Ensure target 2 is update, and everything else is as expected
queryset = ForeignKeyTarget.objects.all()
@ -134,16 +134,16 @@ class SlugForeignKeyTests(TestCase):
{'id': 1, 'name': 'target-1', 'sources': ['source-2']},
{'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):
data = {'id': 4, 'name': 'source-4', 'target': 'target-2'}
serializer = ForeignKeySourceSerializer(data=data)
serializer.is_valid()
self.assertTrue(serializer.is_valid())
assert serializer.is_valid()
obj = serializer.save()
self.assertEqual(serializer.data, data)
self.assertEqual(obj.name, 'source-4')
assert serializer.data == data
assert obj.name == 'source-4'
# Ensure source 4 is added, and everything else is as expected
queryset = ForeignKeySource.objects.all()
@ -154,15 +154,15 @@ class SlugForeignKeyTests(TestCase):
{'id': 3, 'name': 'source-3', 'target': 'target-1'},
{'id': 4, 'name': 'source-4', 'target': 'target-2'},
]
self.assertEqual(serializer.data, expected)
assert serializer.data == expected
def test_reverse_foreign_key_create(self):
data = {'id': 3, 'name': 'target-3', 'sources': ['source-1', 'source-3']}
serializer = ForeignKeyTargetSerializer(data=data)
self.assertTrue(serializer.is_valid())
assert serializer.is_valid()
obj = serializer.save()
self.assertEqual(serializer.data, data)
self.assertEqual(obj.name, 'target-3')
assert serializer.data == data
assert obj.name == 'target-3'
# Ensure target 3 is added, and everything else is as expected
queryset = ForeignKeyTarget.objects.all()
@ -172,14 +172,14 @@ class SlugForeignKeyTests(TestCase):
{'id': 2, 'name': 'target-2', 'sources': []},
{'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):
data = {'id': 1, 'name': 'source-1', 'target': None}
instance = ForeignKeySource.objects.get(pk=1)
serializer = ForeignKeySourceSerializer(instance, data=data)
self.assertFalse(serializer.is_valid())
self.assertEqual(serializer.errors, {'target': ['This field may not be null.']})
assert not serializer.is_valid()
assert serializer.errors == {'target': ['This field may not be null.']}
class SlugNullableForeignKeyTests(TestCase):
@ -200,15 +200,15 @@ class SlugNullableForeignKeyTests(TestCase):
{'id': 2, 'name': 'source-2', 'target': 'target-1'},
{'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):
data = {'id': 4, 'name': 'source-4', 'target': None}
serializer = NullableForeignKeySourceSerializer(data=data)
self.assertTrue(serializer.is_valid())
assert serializer.is_valid()
obj = serializer.save()
self.assertEqual(serializer.data, data)
self.assertEqual(obj.name, 'source-4')
assert serializer.data == data
assert obj.name == 'source-4'
# Ensure source 4 is created, and everything else is as expected
queryset = NullableForeignKeySource.objects.all()
@ -219,7 +219,7 @@ class SlugNullableForeignKeyTests(TestCase):
{'id': 3, 'name': 'source-3', '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):
"""
@ -229,10 +229,10 @@ class SlugNullableForeignKeyTests(TestCase):
data = {'id': 4, 'name': 'source-4', 'target': ''}
expected_data = {'id': 4, 'name': 'source-4', 'target': None}
serializer = NullableForeignKeySourceSerializer(data=data)
self.assertTrue(serializer.is_valid())
assert serializer.is_valid()
obj = serializer.save()
self.assertEqual(serializer.data, expected_data)
self.assertEqual(obj.name, 'source-4')
assert serializer.data == expected_data
assert obj.name == 'source-4'
# Ensure source 4 is created, and everything else is as expected
queryset = NullableForeignKeySource.objects.all()
@ -243,15 +243,15 @@ class SlugNullableForeignKeyTests(TestCase):
{'id': 3, 'name': 'source-3', '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):
data = {'id': 1, 'name': 'source-1', 'target': None}
instance = NullableForeignKeySource.objects.get(pk=1)
serializer = NullableForeignKeySourceSerializer(instance, data=data)
self.assertTrue(serializer.is_valid())
assert serializer.is_valid()
serializer.save()
self.assertEqual(serializer.data, data)
assert serializer.data == data
# Ensure source 1 is updated, and everything else is as expected
queryset = NullableForeignKeySource.objects.all()
@ -261,7 +261,7 @@ class SlugNullableForeignKeyTests(TestCase):
{'id': 2, 'name': 'source-2', 'target': 'target-1'},
{'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):
"""
@ -272,9 +272,9 @@ class SlugNullableForeignKeyTests(TestCase):
expected_data = {'id': 1, 'name': 'source-1', 'target': None}
instance = NullableForeignKeySource.objects.get(pk=1)
serializer = NullableForeignKeySourceSerializer(instance, data=data)
self.assertTrue(serializer.is_valid())
assert serializer.is_valid()
serializer.save()
self.assertEqual(serializer.data, expected_data)
assert serializer.data == expected_data
# Ensure source 1 is updated, and everything else is as expected
queryset = NullableForeignKeySource.objects.all()
@ -284,4 +284,4 @@ class SlugNullableForeignKeyTests(TestCase):
{'id': 2, 'name': 'source-2', 'target': 'target-1'},
{'id': 3, 'name': 'source-3', 'target': None}
]
self.assertEqual(serializer.data, expected)
assert serializer.data == expected

View File

@ -5,9 +5,11 @@ import json
import re
from collections import MutableMapping, OrderedDict
import pytest
from django.conf.urls import include, url
from django.core.cache import cache
from django.db import models
from django.http.request import HttpRequest
from django.test import TestCase, override_settings
from django.utils import six
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.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.settings import api_settings
from rest_framework.test import APIRequestFactory
@ -269,6 +273,18 @@ def strip_trailing_whitespace(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):
"""
Tests specific to the JSON Renderer
@ -568,3 +584,67 @@ class TestMultipleChoiceFieldHTMLFormRenderer(TestCase):
result)
self.assertInHTML('<option value="1">Option1</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'

View File

@ -42,14 +42,14 @@ class TestContentParsing(TestCase):
Ensure request.data returns empty QueryDict for GET request.
"""
request = Request(factory.get('/'))
self.assertEqual(request.data, {})
assert request.data == {}
def test_standard_behaviour_determines_no_content_HEAD(self):
"""
Ensure request.data returns empty QueryDict for HEAD request.
"""
request = Request(factory.head('/'))
self.assertEqual(request.data, {})
assert request.data == {}
def test_request_DATA_with_form_content(self):
"""
@ -58,7 +58,7 @@ class TestContentParsing(TestCase):
data = {'qwerty': 'uiop'}
request = Request(factory.post('/', data))
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):
"""
@ -69,7 +69,7 @@ class TestContentParsing(TestCase):
content_type = 'text/plain'
request = Request(factory.post('/', content, content_type=content_type))
request.parsers = (PlainTextParser(),)
self.assertEqual(request.data, content)
assert request.data == content
def test_request_POST_with_form_content(self):
"""
@ -78,7 +78,7 @@ class TestContentParsing(TestCase):
data = {'qwerty': 'uiop'}
request = Request(factory.post('/', data))
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):
"""
@ -87,8 +87,8 @@ class TestContentParsing(TestCase):
upload = SimpleUploadedFile("file.txt", b"file_content")
request = Request(factory.post('/', {'upload': upload}))
request.parsers = (FormParser(), MultiPartParser())
self.assertEqual(list(request.POST.keys()), [])
self.assertEqual(list(request.FILES.keys()), ['upload'])
assert list(request.POST.keys()) == []
assert list(request.FILES.keys()) == ['upload']
def test_standard_behaviour_determines_form_content_PUT(self):
"""
@ -97,7 +97,7 @@ class TestContentParsing(TestCase):
data = {'qwerty': 'uiop'}
request = Request(factory.put('/', data))
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):
"""
@ -108,7 +108,7 @@ class TestContentParsing(TestCase):
content_type = 'text/plain'
request = Request(factory.put('/', content, content_type=content_type))
request.parsers = (PlainTextParser(), )
self.assertEqual(request.data, content)
assert request.data == content
class MockView(APIView):
@ -142,10 +142,10 @@ class TestContentParsingWithAuthentication(TestCase):
content = {'example': 'example'}
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)
self.assertEqual(status.HTTP_200_OK, response.status_code)
assert status.HTTP_200_OK == response.status_code
class TestUserSetter(TestCase):
@ -162,11 +162,11 @@ class TestUserSetter(TestCase):
def test_user_can_be_set(self):
self.request.user = self.user
self.assertEqual(self.request.user, self.user)
assert self.request.user == self.user
def test_user_can_login(self):
login(self.request, self.user)
self.assertEqual(self.request.user, self.user)
assert self.request.user == self.user
def test_user_can_logout(self):
self.request.user = self.user
@ -176,7 +176,7 @@ class TestUserSetter(TestCase):
def test_logged_in_user_is_set_on_wrapped_request(self):
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):
"""
@ -207,15 +207,15 @@ class TestAuthSetter(TestCase):
def test_auth_can_be_set(self):
request = Request(factory.get('/'))
request.auth = 'DUMMY'
self.assertEqual(request.auth, 'DUMMY')
assert request.auth == 'DUMMY'
class TestSecure(TestCase):
def test_default_secure_false(self):
request = Request(factory.get('/', secure=False))
self.assertEqual(request.scheme, 'http')
assert request.scheme == 'http'
def test_default_secure_true(self):
request = Request(factory.get('/', secure=True))
self.assertEqual(request.scheme, 'https')
assert request.scheme == 'https'

View File

@ -38,18 +38,18 @@ class ReverseTests(TestCase):
def test_reversed_urls_are_fully_qualified(self):
request = factory.get('/view')
url = reverse('view', request=request)
self.assertEqual(url, 'http://testserver/view')
assert url == 'http://testserver/view'
def test_reverse_with_versioning_scheme(self):
request = factory.get('/view')
request.versioning_scheme = MockVersioningScheme()
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):
request = factory.get('/view')
request.versioning_scheme = MockVersioningScheme(raise_error=True)
url = reverse('view', request=request)
self.assertEqual(url, 'http://testserver/view')
assert url == 'http://testserver/view'

View File

@ -3,12 +3,14 @@ from __future__ import unicode_literals
import json
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.db import models
from django.test import TestCase, override_settings
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.response import Response
from rest_framework.routers import DefaultRouter, SimpleRouter
@ -80,7 +82,7 @@ empty_prefix_urls = [
urlpatterns = [
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'^example2/', include(kwarged_notes_router.urls)),
@ -124,8 +126,7 @@ class TestSimpleRouter(TestCase):
for i, endpoint in enumerate(['action1', 'action2', 'action3', 'link1', 'link2']):
route = decorator_routes[i]
# check url listing
self.assertEqual(route.url,
'^{{prefix}}/{{lookup}}/{0}{{trailing_slash}}$'.format(endpoint))
assert route.url == '^{{prefix}}/{{lookup}}/{0}{{trailing_slash}}$'.format(endpoint)
# check method to function mapping
if endpoint == 'action3':
methods_map = ['post', 'delete']
@ -134,28 +135,18 @@ class TestSimpleRouter(TestCase):
else:
methods_map = ['get']
for method in methods_map:
self.assertEqual(route.mapping[method], endpoint)
assert route.mapping[method] == endpoint
@override_settings(ROOT_URLCONF='tests.test_routers')
class TestRootView(TestCase):
def test_retrieve_namespaced_root(self):
response = self.client.get('/namespaced/')
self.assertEqual(
response.data,
{
"example": "http://testserver/namespaced/example/",
}
)
assert response.data == {"example": "http://testserver/namespaced/example/"}
def test_retrieve_non_namespaced_root(self):
response = self.client.get('/non-namespaced/')
self.assertEqual(
response.data,
{
"example": "http://testserver/non-namespaced/example/",
}
)
assert response.data == {"example": "http://testserver/non-namespaced/example/"}
@override_settings(ROOT_URLCONF='tests.test_routers')
@ -169,27 +160,15 @@ class TestCustomLookupFields(TestCase):
def test_custom_lookup_field_route(self):
detail_route = notes_router.urls[-1]
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):
response = self.client.get('/example/notes/')
self.assertEqual(
response.data,
[{
"url": "http://testserver/example/notes/123/",
"uuid": "123", "text": "foo bar"
}]
)
assert response.data == [{"url": "http://testserver/example/notes/123/", "uuid": "123", "text": "foo bar"}]
def test_retrieve_lookup_field_detail_view(self):
response = self.client.get('/example/notes/123/')
self.assertEqual(
response.data,
{
"url": "http://testserver/example/notes/123/",
"uuid": "123", "text": "foo bar"
}
)
assert response.data == {"url": "http://testserver/example/notes/123/", "uuid": "123", "text": "foo bar"}
class TestLookupValueRegex(TestCase):
@ -210,7 +189,7 @@ class TestLookupValueRegex(TestCase):
def test_urls_limited_by_lookup_value_regex(self):
expected = ['^notes/$', '^notes/(?P<uuid>[0-9a-f]{32})/$']
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')
@ -226,17 +205,11 @@ class TestLookupUrlKwargs(TestCase):
def test_custom_lookup_url_kwarg_route(self):
detail_route = kwarged_notes_router.urls[-1]
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):
response = self.client.get('/example2/notes/fo/')
self.assertEqual(
response.data,
{
"url": "http://testserver/example/notes/123/",
"uuid": "123", "text": "foo bar"
}
)
assert response.data == {"url": "http://testserver/example/notes/123/", "uuid": "123", "text": "foo bar"}
class TestTrailingSlashIncluded(TestCase):
@ -251,7 +224,7 @@ class TestTrailingSlashIncluded(TestCase):
def test_urls_have_trailing_slash_by_default(self):
expected = ['^notes/$', '^notes/(?P<pk>[^/.]+)/$']
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):
@ -266,7 +239,7 @@ class TestTrailingSlashRemoved(TestCase):
def test_urls_can_have_trailing_slash_removed(self):
expected = ['^notes$', '^notes/(?P<pk>[^/.]+)$']
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):
@ -281,7 +254,7 @@ class TestNameableRoot(TestCase):
def test_router_has_custom_name(self):
expected = 'nameable-root'
self.assertEqual(expected, self.urls[-1].name)
assert expected == self.urls[-1].name
class TestActionKeywordArgs(TestCase):
@ -307,10 +280,7 @@ class TestActionKeywordArgs(TestCase):
def test_action_kwargs(self):
request = factory.post('/test/0/custom/')
response = self.view(request)
self.assertEqual(
response.data,
{'permission_classes': [permissions.AllowAny]}
)
assert response.data == {'permission_classes': [permissions.AllowAny]}
class TestActionAppliedToExistingRoute(TestCase):
@ -331,7 +301,7 @@ class TestActionAppliedToExistingRoute(TestCase):
self.router = SimpleRouter()
self.router.register(r'test', TestViewSet, base_name='test')
with self.assertRaises(ImproperlyConfigured):
with pytest.raises(ImproperlyConfigured):
self.router.urls
@ -391,17 +361,15 @@ class TestDynamicListAndDetailRouter(TestCase):
url_path = endpoint.url_path
if method_name.startswith('list_'):
self.assertEqual(route.url,
'^{{prefix}}/{0}{{trailing_slash}}$'.format(url_path))
assert route.url == '^{{prefix}}/{0}{{trailing_slash}}$'.format(url_path)
else:
self.assertEqual(route.url,
'^{{prefix}}/{{lookup}}/{0}{{trailing_slash}}$'.format(url_path))
assert route.url == '^{{prefix}}/{{lookup}}/{0}{{trailing_slash}}$'.format(url_path)
# check method to function mapping
if method_name.endswith('_post'):
method_map = 'post'
else:
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):
self._test_list_and_detail_route_decorators(DynamicListAndDetailViewSet)
@ -414,22 +382,11 @@ class TestDynamicListAndDetailRouter(TestCase):
class TestEmptyPrefix(TestCase):
def test_empty_prefix_list(self):
response = self.client.get('/empty-prefix/')
self.assertEqual(200, response.status_code)
self.assertEqual(
json.loads(response.content.decode('utf-8')),
[
{'uuid': '111', 'text': 'First'},
{'uuid': '222', 'text': 'Second'}
]
)
assert response.status_code == 200
assert json.loads(response.content.decode('utf-8')) == [{'uuid': '111', 'text': 'First'},
{'uuid': '222', 'text': 'Second'}]
def test_empty_prefix_detail(self):
response = self.client.get('/empty-prefix/1/')
self.assertEqual(200, response.status_code)
self.assertEqual(
json.loads(response.content.decode('utf-8')),
{
'uuid': '111',
'text': 'First'
}
)
assert response.status_code == 200
assert json.loads(response.content.decode('utf-8')) == {'uuid': '111', 'text': 'First'}

View File

@ -92,7 +92,7 @@ class TestRouterGeneratedSchema(TestCase):
def test_anonymous_request(self):
client = APIClient()
response = client.get('/', HTTP_ACCEPT='application/coreapi+json')
self.assertEqual(response.status_code, 200)
assert response.status_code == 200
expected = coreapi.Document(
url='http://testserver/',
title='Example API',
@ -127,14 +127,14 @@ class TestRouterGeneratedSchema(TestCase):
}
}
)
self.assertEqual(response.data, expected)
assert response.data == expected
@unittest.expectedFailure
def test_authenticated_request(self):
client = APIClient()
client.force_authenticate(MockUser())
response = client.get('/', HTTP_ACCEPT='application/coreapi+json')
self.assertEqual(response.status_code, 200)
assert response.status_code == 200
expected = coreapi.Document(
url='http://testserver/',
title='Example API',
@ -220,7 +220,7 @@ class TestRouterGeneratedSchema(TestCase):
}
}
)
self.assertEqual(response.data, expected)
assert response.data == expected
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')
@ -370,7 +370,7 @@ class TestSchemaGeneratorNotAtRoot(TestCase):
}
}
)
self.assertEqual(schema, expected)
assert schema == expected
@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')

View File

@ -159,6 +159,32 @@ class TestBaseSerializer:
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):
instance = {'id': 1, 'name': 'tom', 'domain': 'example.com'}
serializer = self.Serializer(instance)

View File

@ -44,9 +44,9 @@ class BulkCreateSerializerTests(TestCase):
]
serializer = self.BookSerializer(data=data, many=True)
self.assertEqual(serializer.is_valid(), True)
self.assertEqual(serializer.validated_data, data)
self.assertEqual(serializer.errors, [])
assert serializer.is_valid() is True
assert serializer.validated_data == data
assert serializer.errors == []
def test_bulk_create_errors(self):
"""
@ -75,9 +75,9 @@ class BulkCreateSerializerTests(TestCase):
]
serializer = self.BookSerializer(data=data, many=True)
self.assertEqual(serializer.is_valid(), False)
self.assertEqual(serializer.errors, expected_errors)
self.assertEqual(serializer.validated_data, [])
assert serializer.is_valid() is False
assert serializer.errors == expected_errors
assert serializer.validated_data == []
def test_invalid_list_datatype(self):
"""
@ -85,7 +85,7 @@ class BulkCreateSerializerTests(TestCase):
"""
data = ['foo', 'bar', 'baz']
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__
message = 'Invalid data. Expected a dictionary, but got %s.' % text_type_string
@ -95,7 +95,7 @@ class BulkCreateSerializerTests(TestCase):
{'non_field_errors': [message]}
]
self.assertEqual(serializer.errors, expected_errors)
assert serializer.errors == expected_errors
def test_invalid_single_datatype(self):
"""
@ -103,11 +103,11 @@ class BulkCreateSerializerTests(TestCase):
"""
data = 123
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".']}
self.assertEqual(serializer.errors, expected_errors)
assert serializer.errors == expected_errors
def test_invalid_single_object(self):
"""
@ -120,8 +120,8 @@ class BulkCreateSerializerTests(TestCase):
'author': 'Tom Wolfe'
}
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".']}
self.assertEqual(serializer.errors, expected_errors)
assert serializer.errors == expected_errors

View File

@ -4,8 +4,10 @@ from __future__ import unicode_literals
from django.test import TestCase
from rest_framework.relations import Hyperlink
from rest_framework.templatetags import rest_framework
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
@ -214,6 +216,27 @@ class TemplateTagTests(TestCase):
for case in negative_cases:
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):
"""
@ -246,6 +269,15 @@ class Issue1386Tests(TestCase):
# example from issue #1386, this shouldn't raise an exception
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):
"""

View File

@ -3,15 +3,20 @@ Tests for the throttling implementations in the permissions module.
"""
from __future__ import unicode_literals
import pytest
from django.contrib.auth.models import User
from django.core.cache import cache
from django.core.exceptions import ImproperlyConfigured
from django.http import HttpRequest
from django.test import TestCase
from rest_framework.request import Request
from rest_framework.response import Response
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 (
BaseThrottle, ScopedRateThrottle, UserRateThrottle
AnonRateThrottle, BaseThrottle, ScopedRateThrottle, SimpleRateThrottle,
UserRateThrottle
)
from rest_framework.views import APIView
@ -189,6 +194,8 @@ class ScopedRateThrottleTests(TestCase):
"""
def setUp(self):
self.throttle = ScopedRateThrottle()
class XYScopedRateThrottle(ScopedRateThrottle):
TIMER_SECONDS = 0
THROTTLE_RATES = {'x': '3/min', 'y': '1/min'}
@ -288,6 +295,18 @@ class ScopedRateThrottleTests(TestCase):
response = self.unscoped_view(request)
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):
def setUp(self):
@ -354,3 +373,79 @@ class XffUniqueMachinesTest(XffTestingBase):
self.view(self.request)
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
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'

View File

@ -1,10 +1,15 @@
import datetime
from django.db import models
import pytest
from django.db import DataError, models
from django.test import TestCase
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):
@ -319,6 +324,23 @@ class TestUniquenessTogetherValidation(TestCase):
serializer = NullUniquenessTogetherSerializer(data=data)
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`
# ----------------------------------
@ -389,6 +411,84 @@ class TestUniquenessForDateValidation(TestCase):
'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):
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')>]
""")
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)

View File

@ -1,8 +1,9 @@
import pytest
from django.conf.urls import include, url
from django.conf.urls import url
from django.test import override_settings
from rest_framework import serializers, status, versioning
from rest_framework.compat import include
from rest_framework.decorators import APIView
from rest_framework.relations import PKOnlyObject
from rest_framework.response import Response
@ -170,7 +171,7 @@ class TestURLReversing(URLPatternsTestCase):
]
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'^(?P<version>[v1|v2]+)/another/$', dummy_view, name='another'),
]
@ -335,8 +336,8 @@ class TestHyperlinkedRelatedField(URLPatternsTestCase):
]
urlpatterns = [
url(r'^v1/', include(included, namespace='v1')),
url(r'^v2/', include(included, namespace='v2'))
url(r'^v1/', include(included, namespace='v1', app_name='v1')),
url(r'^v2/', include(included, namespace='v2', app_name='v2'))
]
def setUp(self):
@ -367,7 +368,7 @@ class TestNamespaceVersioningHyperlinkedRelatedFieldScheme(URLPatternsTestCase):
]
included = [
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 = [

34
tox.ini
View File

@ -3,11 +3,19 @@ addopts=--tb=short
[tox]
envlist =
py27-{lint,docs},
{py27,py33,py34,py35}-django18,
{py27,py34,py35}-django19,
{py27,py34,py35}-django110,
{py27,py34,py35}-django{master}
{py27,py34,py35}-django{19,110},
{py27,py34,py35,py36}-django111,
{py35,py36}-djangomaster
lint,docs
[travis:env]
DJANGO =
1.8: django18
1.9: django19
1.10: django110
1.11: django111
master: djangomaster
[testenv]
commands = ./runtests.py --fast {posargs} --coverage -rw
@ -15,25 +23,23 @@ setenv =
PYTHONDONTWRITEBYTECODE=1
PYTHONWARNINGS=once
deps =
django18: Django==1.8.17
django19: Django==1.9.12
django110: Django==1.10.5
django18: Django>=1.8,<1.9
django19: Django>=1.9,<1.10
django110: Django>=1.10,<1.11
django111: Django>=1.11a1,<2.0
djangomaster: https://github.com/django/django/archive/master.tar.gz
-rrequirements/requirements-testing.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
deps =
-rrequirements/requirements-codestyle.txt
-rrequirements/requirements-testing.txt
[testenv:py27-docs]
[testenv:docs]
basepython = python2.7
commands = mkdocs build
deps =
-rrequirements/requirements-testing.txt