Merge remote-tracking branch 'upstream/master'
|
@ -25,9 +25,10 @@ The initial aim is to provide a single full-time position on REST framework.
|
|||
<a href="https://getsentry.com/welcome/"><img src="https://raw.githubusercontent.com/tomchristie/django-rest-framework/master/docs/img/premium/sentry-readme.png"/></a>
|
||||
<a href="https://getstream.io/try-the-api/?utm_source=drf&utm_medium=banner&utm_campaign=drf"><img src="https://raw.githubusercontent.com/tomchristie/django-rest-framework/master/docs/img/premium/stream-readme.png"/></a>
|
||||
<a href="https://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](https://hello.machinalis.co.uk/).*
|
||||
*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).*
|
||||
|
||||
---
|
||||
|
||||
|
|
|
@ -303,8 +303,6 @@ Format strings may either be [Python strftime formats][strftime] which explicitl
|
|||
|
||||
When a value of `None` is used for the format `datetime` objects will be returned by `to_representation` and the final output representation will determined by the renderer class.
|
||||
|
||||
In the case of JSON this means the default datetime representation uses the [ECMA 262 date time string specification][ecma262]. This is a subset of ISO 8601 which uses millisecond precision, and includes the 'Z' suffix for the UTC timezone, for example: `2013-01-29T12:34:56.123Z`.
|
||||
|
||||
#### `auto_now` and `auto_now_add` model fields.
|
||||
|
||||
When using `ModelSerializer` or `HyperlinkedModelSerializer`, note that any model fields with `auto_now=True` or `auto_now_add=True` will use serializer fields that are `read_only=True` by default.
|
||||
|
@ -434,9 +432,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:
|
||||
|
||||
|
|
|
@ -700,7 +700,7 @@ You can override a URL field view name and lookup field by using either, or both
|
|||
model = Account
|
||||
fields = ('account_url', 'account_name', 'users', 'created')
|
||||
extra_kwargs = {
|
||||
'url': {'view_name': 'accounts', 'lookup_field': 'account_name'}
|
||||
'url': {'view_name': 'accounts', 'lookup_field': 'account_name'},
|
||||
'users': {'lookup_field': 'username'}
|
||||
}
|
||||
|
||||
|
@ -1100,8 +1100,6 @@ This API included the `.get_field()`, `.get_pk_field()` and other methods.
|
|||
|
||||
Because the serializers have been fundamentally redesigned with 3.0 this API no longer exists. You can still modify the fields that get created but you'll need to refer to the source code, and be aware that if the changes you make are against private bits of API then they may be subject to change.
|
||||
|
||||
A new interface for controlling this behavior is currently planned for REST framework 3.1.
|
||||
|
||||
---
|
||||
|
||||
# Third party packages
|
||||
|
|
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 13 KiB |
BIN
docs/img/premium/rollbar-readme.png
Normal file
After Width: | Height: | Size: 12 KiB |
Before Width: | Height: | Size: 54 KiB After Width: | Height: | Size: 52 KiB |
Before Width: | Height: | Size: 26 KiB After Width: | Height: | Size: 24 KiB |
Before Width: | Height: | Size: 20 KiB After Width: | Height: | Size: 19 KiB |
|
@ -75,10 +75,11 @@ The initial aim is to provide a single full-time position on REST framework.
|
|||
<li><a href="https://getsentry.com/welcome/" style="background-image: url(https://fund-rest-framework.s3.amazonaws.com/sentry130.png)">Sentry</a></li>
|
||||
<li><a href="https://getstream.io/try-the-api/?utm_source=drf&utm_medium=banner&utm_campaign=drf" style="background-image: url(https://fund-rest-framework.s3.amazonaws.com/stream-130.png)">Stream</a></li>
|
||||
<li><a href="https://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](https://hello.machinalis.co.uk/).*
|
||||
*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).*
|
||||
|
||||
---
|
||||
|
||||
|
|
|
@ -40,6 +40,24 @@ You can determine your currently installed version using `pip freeze`:
|
|||
|
||||
## 3.5.x series
|
||||
|
||||
### 3.5.4
|
||||
|
||||
**Date**: [10th February 2017][3.5.4-milestone]
|
||||
|
||||
* Add max_length and min_length arguments for ListField. ([#4877][gh4877])
|
||||
* Add per-view custom exception handler support. ([#4753][gh4753])
|
||||
* Support disabling of declared fields on serializer subclasses. ([#4764][gh4764])
|
||||
* Support custom view names on `@list_route` and `@detail_route` endpoints. ([#4821][gh4821])
|
||||
* Correct labels for fields in login template when custom user model is used. ([#4841][gh4841])
|
||||
* Whitespace fixes for descriptions generated from docstrings. ([#4759][gh4759], [#4869][gh4869], [#4870][gh4870])
|
||||
* Better error reporting when schemas are returned by views without a schema renderer. ([#4790][gh4790])
|
||||
* Fix for returned response of `PUT` requests when `prefetch_related` is used. ([#4661][gh4661], [#4668][gh4668])
|
||||
* Fix for breadcrumb view names. ([#4750][gh4750])
|
||||
* Fix for RequestsClient ensuring fully qualified URLs. ([#4678][gh4678])
|
||||
* Fix for incorrect behavior of writable-nested fields check in some cases. ([#4634][gh4634], [#4669][gh4669])
|
||||
* Resolve Django deprecation warnings. ([#4712][gh4712])
|
||||
* Various cleanup of test cases.
|
||||
|
||||
### 3.5.3
|
||||
|
||||
**Date**: [7th November 2016][3.5.3-milestone]
|
||||
|
@ -639,6 +657,7 @@ For older release notes, [please see the version 2.x documentation][old-release-
|
|||
[3.5.1-milestone]: https://github.com/tomchristie/django-rest-framework/issues?q=milestone%3A%223.5.1+Release%22
|
||||
[3.5.2-milestone]: https://github.com/tomchristie/django-rest-framework/issues?q=milestone%3A%223.5.2+Release%22
|
||||
[3.5.3-milestone]: https://github.com/tomchristie/django-rest-framework/issues?q=milestone%3A%223.5.3+Release%22
|
||||
[3.5.4-milestone]: https://github.com/tomchristie/django-rest-framework/issues?q=milestone%3A%223.5.4+Release%22
|
||||
|
||||
<!-- 3.0.1 -->
|
||||
[gh2013]: https://github.com/tomchristie/django-rest-framework/issues/2013
|
||||
|
@ -1216,3 +1235,22 @@ For older release notes, [please see the version 2.x documentation][old-release-
|
|||
[gh4645]: https://github.com/tomchristie/django-rest-framework/issues/4645
|
||||
[gh4646]: https://github.com/tomchristie/django-rest-framework/issues/4646
|
||||
[gh4650]: https://github.com/tomchristie/django-rest-framework/issues/4650
|
||||
|
||||
<!-- 3.5.4 -->
|
||||
|
||||
[gh4877]: https://github.com/tomchristie/django-rest-framework/issues/4877
|
||||
[gh4753]: https://github.com/tomchristie/django-rest-framework/issues/4753
|
||||
[gh4764]: https://github.com/tomchristie/django-rest-framework/issues/4764
|
||||
[gh4821]: https://github.com/tomchristie/django-rest-framework/issues/4821
|
||||
[gh4841]: https://github.com/tomchristie/django-rest-framework/issues/4841
|
||||
[gh4759]: https://github.com/tomchristie/django-rest-framework/issues/4759
|
||||
[gh4869]: https://github.com/tomchristie/django-rest-framework/issues/4869
|
||||
[gh4870]: https://github.com/tomchristie/django-rest-framework/issues/4870
|
||||
[gh4790]: https://github.com/tomchristie/django-rest-framework/issues/4790
|
||||
[gh4661]: https://github.com/tomchristie/django-rest-framework/issues/4661
|
||||
[gh4668]: https://github.com/tomchristie/django-rest-framework/issues/4668
|
||||
[gh4750]: https://github.com/tomchristie/django-rest-framework/issues/4750
|
||||
[gh4678]: https://github.com/tomchristie/django-rest-framework/issues/4678
|
||||
[gh4634]: https://github.com/tomchristie/django-rest-framework/issues/4634
|
||||
[gh4669]: https://github.com/tomchristie/django-rest-framework/issues/4669
|
||||
[gh4712]: https://github.com/tomchristie/django-rest-framework/issues/4712
|
||||
|
|
|
@ -8,7 +8,7 @@ ______ _____ _____ _____ __
|
|||
"""
|
||||
|
||||
__title__ = 'Django REST framework'
|
||||
__version__ = '3.5.3'
|
||||
__version__ = '3.5.4'
|
||||
__author__ = 'Tom Christie'
|
||||
__license__ = 'BSD 2-Clause'
|
||||
__copyright__ = 'Copyright 2011-2016 Tom Christie'
|
||||
|
|
|
@ -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 (
|
||||
|
@ -312,3 +307,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)
|
||||
|
|
|
@ -1505,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, (
|
||||
|
@ -1520,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:
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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
|
||||
|
||||
|
||||
|
|
|
@ -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 whitespace_counts:
|
||||
whitespace_pattern = '^' + (' ' * min(whitespace_counts))
|
||||
content = re.sub(re.compile(whitespace_pattern, re.MULTILINE), '', content)
|
||||
elif tab_counts:
|
||||
whitespace_pattern = '^' + ('\t' * min(whitespace_counts))
|
||||
content = re.sub(re.compile(whitespace_pattern, re.MULTILINE), '', content)
|
||||
|
||||
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 = '^' + (' ' * whitespace_counts)
|
||||
content = re.sub(re.compile(whitespace_pattern, re.MULTILINE), '', content)
|
||||
elif tab_counts:
|
||||
whitespace_pattern = '^' + ('\t' * tab_counts)
|
||||
content = re.sub(re.compile(whitespace_pattern, re.MULTILINE), '', content)
|
||||
return content.strip()
|
||||
|
||||
|
||||
|
|
|
@ -76,12 +76,7 @@ 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) 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').
|
||||
]:
|
||||
for field in [field for field in opts.fields if field.serialize and get_remote_field(field)]:
|
||||
forward_relations[field.name] = RelationInfo(
|
||||
model_field=field,
|
||||
related_model=get_related_model(field),
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -1,14 +1,20 @@
|
|||
from datetime import date, datetime, timedelta, tzinfo
|
||||
from datetime import date, datetime, timedelta
|
||||
from decimal import Decimal
|
||||
from uuid import uuid4
|
||||
|
||||
import pytest
|
||||
from django.test import TestCase
|
||||
from django.utils.timezone import utc
|
||||
|
||||
from rest_framework.compat import coreapi
|
||||
from rest_framework.utils.encoders import JSONEncoder
|
||||
|
||||
|
||||
class MockList(object):
|
||||
def tolist(self):
|
||||
return [1, 2, 3]
|
||||
|
||||
|
||||
class JSONEncoderTests(TestCase):
|
||||
"""
|
||||
Tests the JSONEncoder method
|
||||
|
@ -22,7 +28,7 @@ class JSONEncoderTests(TestCase):
|
|||
Tests encoding a decimal
|
||||
"""
|
||||
d = Decimal(3.14)
|
||||
assert d == float(d)
|
||||
assert self.encoder.default(d) == float(d)
|
||||
|
||||
def test_encode_datetime(self):
|
||||
"""
|
||||
|
@ -30,6 +36,8 @@ class JSONEncoderTests(TestCase):
|
|||
"""
|
||||
current_time = datetime.now()
|
||||
assert self.encoder.default(current_time) == current_time.isoformat()
|
||||
current_time_utc = current_time.replace(tzinfo=utc)
|
||||
assert self.encoder.default(current_time_utc) == current_time.isoformat() + 'Z'
|
||||
|
||||
def test_encode_time(self):
|
||||
"""
|
||||
|
@ -42,22 +50,8 @@ class JSONEncoderTests(TestCase):
|
|||
"""
|
||||
Tests encoding a timezone aware timestamp
|
||||
"""
|
||||
|
||||
class UTC(tzinfo):
|
||||
"""
|
||||
Class extending tzinfo to mimic UTC time
|
||||
"""
|
||||
def utcoffset(self, dt):
|
||||
return timedelta(0)
|
||||
|
||||
def tzname(self, dt):
|
||||
return "UTC"
|
||||
|
||||
def dst(self, dt):
|
||||
return timedelta(0)
|
||||
|
||||
current_time = datetime.now().time()
|
||||
current_time = current_time.replace(tzinfo=UTC())
|
||||
current_time = current_time.replace(tzinfo=utc)
|
||||
with pytest.raises(ValueError):
|
||||
self.encoder.default(current_time)
|
||||
|
||||
|
@ -91,3 +85,10 @@ class JSONEncoderTests(TestCase):
|
|||
|
||||
with pytest.raises(RuntimeError):
|
||||
self.encoder.default(coreapi.Error())
|
||||
|
||||
def test_encode_object_with_tolist(self):
|
||||
"""
|
||||
Tests encoding a object with tolist method
|
||||
"""
|
||||
foo = MockList()
|
||||
assert self.encoder.default(foo) == [1, 2, 3]
|
||||
|
|
|
@ -1668,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.
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -4,12 +4,13 @@ import json
|
|||
from collections import namedtuple
|
||||
|
||||
import pytest
|
||||
from django.conf.urls import include, url
|
||||
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
|
||||
|
@ -81,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)),
|
||||
|
||||
|
|
|
@ -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 = [
|
||||
|
|