mirror of
https://github.com/encode/django-rest-framework.git
synced 2025-08-04 12:30:11 +03:00
Merge branch 'master' into localizedfloatfield
This commit is contained in:
commit
c737fa0f8c
|
@ -300,7 +300,7 @@ Add the package to your `INSTALLED_APPS` and modify your REST framework settings
|
||||||
|
|
||||||
REST_FRAMEWORK = {
|
REST_FRAMEWORK = {
|
||||||
'DEFAULT_AUTHENTICATION_CLASSES': (
|
'DEFAULT_AUTHENTICATION_CLASSES': (
|
||||||
'oauth2_provider.ext.rest_framework.OAuth2Authentication',
|
'oauth2_provider.contrib.rest_framework.OAuth2Authentication',
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -230,5 +230,5 @@ The generic views use the `raise_exception=True` flag, which means that you can
|
||||||
|
|
||||||
By default this exception results in a response with the HTTP status code "400 Bad Request".
|
By default this exception results in a response with the HTTP status code "400 Bad Request".
|
||||||
|
|
||||||
[cite]: http://www.doughellmann.com/articles/how-tos/python-exception-handling/index.html
|
[cite]: https://doughellmann.com/blog/2009/06/19/python-exception-handling-techniques/
|
||||||
[authentication]: authentication.md
|
[authentication]: authentication.md
|
||||||
|
|
|
@ -390,4 +390,4 @@ The [django-rest-framework-bulk package][django-rest-framework-bulk] implements
|
||||||
[UpdateModelMixin]: #updatemodelmixin
|
[UpdateModelMixin]: #updatemodelmixin
|
||||||
[DestroyModelMixin]: #destroymodelmixin
|
[DestroyModelMixin]: #destroymodelmixin
|
||||||
[django-rest-framework-bulk]: https://github.com/miki725/django-rest-framework-bulk
|
[django-rest-framework-bulk]: https://github.com/miki725/django-rest-framework-bulk
|
||||||
[django-rest-multiple-models]: https://github.com/Axiologue/DjangoRestMultipleModels
|
[django-rest-multiple-models]: https://github.com/MattBroach/DjangoRestMultipleModels
|
||||||
|
|
|
@ -194,7 +194,7 @@ As with `SimpleRouter` the trailing slashes on the URL routes can be removed by
|
||||||
|
|
||||||
# Custom Routers
|
# Custom Routers
|
||||||
|
|
||||||
Implementing a custom router isn't something you'd need to do very often, but it can be useful if you have specific requirements about how the your URLs for your API are structured. Doing so allows you to encapsulate the URL structure in a reusable way that ensures you don't have to write your URL patterns explicitly for each new view.
|
Implementing a custom router isn't something you'd need to do very often, but it can be useful if you have specific requirements about how the URLs for your API are structured. Doing so allows you to encapsulate the URL structure in a reusable way that ensures you don't have to write your URL patterns explicitly for each new view.
|
||||||
|
|
||||||
The simplest way to implement a custom router is to subclass one of the existing router classes. The `.routes` attribute is used to template the URL patterns that will be mapped to each viewset. The `.routes` attribute is a list of `Route` named tuples.
|
The simplest way to implement a custom router is to subclass one of the existing router classes. The `.routes` attribute is used to template the URL patterns that will be mapped to each viewset. The `.routes` attribute is a list of `Route` named tuples.
|
||||||
|
|
||||||
|
|
|
@ -107,6 +107,8 @@ add a schema to your API, depending on exactly what you need.
|
||||||
The simplest way to include a schema in your project is to use the
|
The simplest way to include a schema in your project is to use the
|
||||||
`get_schema_view()` function.
|
`get_schema_view()` function.
|
||||||
|
|
||||||
|
from rest_framework.schemas import get_schema_view
|
||||||
|
|
||||||
schema_view = get_schema_view(title="Server Monitoring API")
|
schema_view = get_schema_view(title="Server Monitoring API")
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
|
@ -161,6 +163,7 @@ ROOT_URLCONF setting.
|
||||||
|
|
||||||
May be used to pass the set of renderer classes that can be used to render the API root endpoint.
|
May be used to pass the set of renderer classes that can be used to render the API root endpoint.
|
||||||
|
|
||||||
|
from rest_framework.schemas import get_schema_view
|
||||||
from rest_framework.renderers import CoreJSONRenderer
|
from rest_framework.renderers import CoreJSONRenderer
|
||||||
from my_custom_package import APIBlueprintRenderer
|
from my_custom_package import APIBlueprintRenderer
|
||||||
|
|
||||||
|
@ -185,6 +188,12 @@ to be exposed in the schema:
|
||||||
patterns=schema_url_patterns,
|
patterns=schema_url_patterns,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
#### `generator_class`
|
||||||
|
|
||||||
|
May be used to specify a `SchemaGenerator` subclass to be passed to the
|
||||||
|
`SchemaView`.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
## Using an explicit schema view
|
## Using an explicit schema view
|
||||||
|
|
||||||
|
|
|
@ -329,7 +329,7 @@ For further enquires please contact <a href=mailto:funding@django-rest-framework
|
||||||
|
|
||||||
## Accountability
|
## Accountability
|
||||||
|
|
||||||
In an effort to keep the project as transparent as possible, we are releasing [monthly progress reports](http://www.encode.io/reports/march-2017) and regularly include financial reports and cost breakdowns.
|
In an effort to keep the project as transparent as possible, we are releasing [monthly progress reports](http://www.encode.io/reports/may-2017) and regularly include financial reports and cost breakdowns.
|
||||||
|
|
||||||
<!-- Begin MailChimp Signup Form -->
|
<!-- Begin MailChimp Signup Form -->
|
||||||
<link href="//cdn-images.mailchimp.com/embedcode/classic-10_7.css" rel="stylesheet" type="text/css">
|
<link href="//cdn-images.mailchimp.com/embedcode/classic-10_7.css" rel="stylesheet" type="text/css">
|
||||||
|
@ -360,6 +360,25 @@ In an effort to keep the project as transparent as possible, we are releasing [m
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## Frequently asked questions
|
||||||
|
|
||||||
|
**Q: Can you issue monthly invoices?**
|
||||||
|
A: Yes, we are happy to issue monthly invoices. Please just <a href=mailto:funding@django-rest-framework.org>email us</a> and let us know who to issue the invoice to (name and address) and which email address to send it to each month.
|
||||||
|
|
||||||
|
**Q: Does sponsorship include VAT?**
|
||||||
|
A: Sponsorship is VAT exempt.
|
||||||
|
|
||||||
|
**Q: Do I have to sign up for a certain time period?**
|
||||||
|
A: No, we appreciate your support for any time period that is convenient for you. Also, you can cancel your sponsorship anytime.
|
||||||
|
|
||||||
|
**Q: Can I pay yearly? Can I pay upfront fox X amount of months at a time?**
|
||||||
|
A: We are currently only set up to accept monthly payments. However, if you'd like to support Django REST framework and you can only do yearly/upfront payments, we are happy to work with you and figure out a convenient solution.
|
||||||
|
|
||||||
|
**Q: Are you only looking for corporate sponsors?**
|
||||||
|
A: No, we value individual sponsors just as much as corporate sponsors and appreciate any kind of support.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## Our sponsors
|
## Our sponsors
|
||||||
|
|
||||||
<div id="fundingInclude"></div>
|
<div id="fundingInclude"></div>
|
||||||
|
|
|
@ -296,7 +296,7 @@ To submit new content, [open an issue][drf-create-issue] or [create a pull reque
|
||||||
[drf-compound-fields]: https://github.com/estebistec/drf-compound-fields
|
[drf-compound-fields]: https://github.com/estebistec/drf-compound-fields
|
||||||
[django-extra-fields]: https://github.com/Hipo/drf-extra-fields
|
[django-extra-fields]: https://github.com/Hipo/drf-extra-fields
|
||||||
[djangorestframework-bulk]: https://github.com/miki725/django-rest-framework-bulk
|
[djangorestframework-bulk]: https://github.com/miki725/django-rest-framework-bulk
|
||||||
[django-rest-multiple-models]: https://github.com/Axiologue/DjangoRestMultipleModels
|
[django-rest-multiple-models]: https://github.com/MattBroach/DjangoRestMultipleModels
|
||||||
[drf-nested-routers]: https://github.com/alanjds/drf-nested-routers
|
[drf-nested-routers]: https://github.com/alanjds/drf-nested-routers
|
||||||
[wq.db.rest]: http://wq.io/docs/about-rest
|
[wq.db.rest]: http://wq.io/docs/about-rest
|
||||||
[djangorestframework-msgpack]: https://github.com/juanriaza/django-rest-framework-msgpack
|
[djangorestframework-msgpack]: https://github.com/juanriaza/django-rest-framework-msgpack
|
||||||
|
|
|
@ -35,6 +35,7 @@ There are a wide range of resources available for learning and using Django REST
|
||||||
|
|
||||||
### Talks
|
### Talks
|
||||||
|
|
||||||
|
* [Level Up! Rethinking the Web API Framework][pycon-us-2017]
|
||||||
* [How to Make a Full Fledged REST API with Django OAuth Toolkit][full-fledged-rest-api-with-django-oauth-tookit]
|
* [How to Make a Full Fledged REST API with Django OAuth Toolkit][full-fledged-rest-api-with-django-oauth-tookit]
|
||||||
* [Django REST API - So Easy You Can Learn It in 25 Minutes][django-rest-api-so-easy]
|
* [Django REST API - So Easy You Can Learn It in 25 Minutes][django-rest-api-so-easy]
|
||||||
* [Tom Christie about Django Rest Framework at Django: Under The Hood][django-under-hood-2014]
|
* [Tom Christie about Django Rest Framework at Django: Under The Hood][django-under-hood-2014]
|
||||||
|
@ -114,3 +115,4 @@ Want your Django REST Framework talk/tutorial/article to be added to our website
|
||||||
[building-a-restful-api-with-drf]: http://agiliq.com/blog/2014/12/building-a-restful-api-with-django-rest-framework/
|
[building-a-restful-api-with-drf]: http://agiliq.com/blog/2014/12/building-a-restful-api-with-django-rest-framework/
|
||||||
[submit-pr]: https://github.com/encode/django-rest-framework
|
[submit-pr]: https://github.com/encode/django-rest-framework
|
||||||
[anna-email]: mailto:anna@django-rest-framework.org
|
[anna-email]: mailto:anna@django-rest-framework.org
|
||||||
|
[pycon-us-2017]: https://www.youtube.com/watch?v=Rk6MHZdust4
|
||||||
|
|
|
@ -48,7 +48,7 @@ if django_filters:
|
||||||
warnings.warn(
|
warnings.warn(
|
||||||
"The built in 'rest_framework.filters.FilterSet' is deprecated. "
|
"The built in 'rest_framework.filters.FilterSet' is deprecated. "
|
||||||
"You should use 'django_filters.rest_framework.FilterSet' instead.",
|
"You should use 'django_filters.rest_framework.FilterSet' instead.",
|
||||||
DeprecationWarning
|
DeprecationWarning, stacklevel=2
|
||||||
)
|
)
|
||||||
return super(FilterSet, self).__init__(*args, **kwargs)
|
return super(FilterSet, self).__init__(*args, **kwargs)
|
||||||
|
|
||||||
|
@ -72,7 +72,7 @@ class DjangoFilterBackend(DFBase):
|
||||||
warnings.warn(
|
warnings.warn(
|
||||||
"The built in 'rest_framework.filters.DjangoFilterBackend' is deprecated. "
|
"The built in 'rest_framework.filters.DjangoFilterBackend' is deprecated. "
|
||||||
"You should use 'django_filters.rest_framework.DjangoFilterBackend' instead.",
|
"You should use 'django_filters.rest_framework.DjangoFilterBackend' instead.",
|
||||||
DeprecationWarning
|
DeprecationWarning, stacklevel=2
|
||||||
)
|
)
|
||||||
|
|
||||||
return super(DjangoFilterBackend, cls).__new__(cls, *args, **kwargs)
|
return super(DjangoFilterBackend, cls).__new__(cls, *args, **kwargs)
|
||||||
|
|
|
@ -6,6 +6,7 @@ on the request, such as form content or json encoded data.
|
||||||
"""
|
"""
|
||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
import codecs
|
||||||
import json
|
import json
|
||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
|
@ -61,8 +62,8 @@ class JSONParser(BaseParser):
|
||||||
encoding = parser_context.get('encoding', settings.DEFAULT_CHARSET)
|
encoding = parser_context.get('encoding', settings.DEFAULT_CHARSET)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
data = stream.read().decode(encoding)
|
decoded_stream = codecs.getreader(encoding)(stream)
|
||||||
return json.loads(data)
|
return json.load(decoded_stream)
|
||||||
except ValueError as exc:
|
except ValueError as exc:
|
||||||
raise ParseError('JSON parse error - %s' % six.text_type(exc))
|
raise ParseError('JSON parse error - %s' % six.text_type(exc))
|
||||||
|
|
||||||
|
|
|
@ -75,7 +75,8 @@ class PKOnlyObject(object):
|
||||||
# rather than the parent serializer.
|
# rather than the parent serializer.
|
||||||
MANY_RELATION_KWARGS = (
|
MANY_RELATION_KWARGS = (
|
||||||
'read_only', 'write_only', 'required', 'default', 'initial', 'source',
|
'read_only', 'write_only', 'required', 'default', 'initial', 'source',
|
||||||
'label', 'help_text', 'style', 'error_messages', 'allow_empty'
|
'label', 'help_text', 'style', 'error_messages', 'allow_empty',
|
||||||
|
'html_cutoff', 'html_cutoff_text'
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@ -86,10 +87,12 @@ class RelatedField(Field):
|
||||||
|
|
||||||
def __init__(self, **kwargs):
|
def __init__(self, **kwargs):
|
||||||
self.queryset = kwargs.pop('queryset', self.queryset)
|
self.queryset = kwargs.pop('queryset', self.queryset)
|
||||||
self.html_cutoff = kwargs.pop(
|
|
||||||
'html_cutoff',
|
cutoff_from_settings = api_settings.HTML_SELECT_CUTOFF
|
||||||
self.html_cutoff or int(api_settings.HTML_SELECT_CUTOFF)
|
if cutoff_from_settings is not None:
|
||||||
)
|
cutoff_from_settings = int(cutoff_from_settings)
|
||||||
|
self.html_cutoff = kwargs.pop('html_cutoff', cutoff_from_settings)
|
||||||
|
|
||||||
self.html_cutoff_text = kwargs.pop(
|
self.html_cutoff_text = kwargs.pop(
|
||||||
'html_cutoff_text',
|
'html_cutoff_text',
|
||||||
self.html_cutoff_text or _(api_settings.HTML_SELECT_CUTOFF_TEXT)
|
self.html_cutoff_text or _(api_settings.HTML_SELECT_CUTOFF_TEXT)
|
||||||
|
@ -466,10 +469,12 @@ class ManyRelatedField(Field):
|
||||||
def __init__(self, child_relation=None, *args, **kwargs):
|
def __init__(self, child_relation=None, *args, **kwargs):
|
||||||
self.child_relation = child_relation
|
self.child_relation = child_relation
|
||||||
self.allow_empty = kwargs.pop('allow_empty', True)
|
self.allow_empty = kwargs.pop('allow_empty', True)
|
||||||
self.html_cutoff = kwargs.pop(
|
|
||||||
'html_cutoff',
|
cutoff_from_settings = api_settings.HTML_SELECT_CUTOFF
|
||||||
self.html_cutoff or int(api_settings.HTML_SELECT_CUTOFF)
|
if cutoff_from_settings is not None:
|
||||||
)
|
cutoff_from_settings = int(cutoff_from_settings)
|
||||||
|
self.html_cutoff = kwargs.pop('html_cutoff', cutoff_from_settings)
|
||||||
|
|
||||||
self.html_cutoff_text = kwargs.pop(
|
self.html_cutoff_text = kwargs.pop(
|
||||||
'html_cutoff_text',
|
'html_cutoff_text',
|
||||||
self.html_cutoff_text or _(api_settings.HTML_SELECT_CUTOFF_TEXT)
|
self.html_cutoff_text or _(api_settings.HTML_SELECT_CUTOFF_TEXT)
|
||||||
|
|
|
@ -335,7 +335,6 @@ class Request(object):
|
||||||
"""
|
"""
|
||||||
Attempt to authenticate the request using each authentication instance
|
Attempt to authenticate the request using each authentication instance
|
||||||
in turn.
|
in turn.
|
||||||
Returns a three-tuple of (authenticator, user, authtoken).
|
|
||||||
"""
|
"""
|
||||||
for authenticator in self.authenticators:
|
for authenticator in self.authenticators:
|
||||||
try:
|
try:
|
||||||
|
|
|
@ -35,6 +35,15 @@ DynamicDetailRoute = namedtuple('DynamicDetailRoute', ['url', 'name', 'initkwarg
|
||||||
DynamicListRoute = namedtuple('DynamicListRoute', ['url', 'name', 'initkwargs'])
|
DynamicListRoute = namedtuple('DynamicListRoute', ['url', 'name', 'initkwargs'])
|
||||||
|
|
||||||
|
|
||||||
|
def escape_curly_brackets(url_path):
|
||||||
|
"""
|
||||||
|
Double brackets in regex of url_path for escape string formatting
|
||||||
|
"""
|
||||||
|
if ('{' and '}') in url_path:
|
||||||
|
url_path = url_path.replace('{', '{{').replace('}', '}}')
|
||||||
|
return url_path
|
||||||
|
|
||||||
|
|
||||||
def replace_methodname(format_string, methodname):
|
def replace_methodname(format_string, methodname):
|
||||||
"""
|
"""
|
||||||
Partially format a format_string, swapping out any
|
Partially format a format_string, swapping out any
|
||||||
|
@ -178,6 +187,7 @@ class SimpleRouter(BaseRouter):
|
||||||
initkwargs = route.initkwargs.copy()
|
initkwargs = route.initkwargs.copy()
|
||||||
initkwargs.update(method_kwargs)
|
initkwargs.update(method_kwargs)
|
||||||
url_path = initkwargs.pop("url_path", None) or methodname
|
url_path = initkwargs.pop("url_path", None) or methodname
|
||||||
|
url_path = escape_curly_brackets(url_path)
|
||||||
url_name = initkwargs.pop("url_name", None) or url_path
|
url_name = initkwargs.pop("url_name", None) or url_path
|
||||||
ret.append(Route(
|
ret.append(Route(
|
||||||
url=replace_methodname(route.url, url_path),
|
url=replace_methodname(route.url, url_path),
|
||||||
|
@ -323,7 +333,7 @@ class DefaultRouter(SimpleRouter):
|
||||||
warnings.warn(
|
warnings.warn(
|
||||||
"Including a schema directly via a router is now deprecated. "
|
"Including a schema directly via a router is now deprecated. "
|
||||||
"Use `get_schema_view()` instead.",
|
"Use `get_schema_view()` instead.",
|
||||||
DeprecationWarning
|
DeprecationWarning, stacklevel=2
|
||||||
)
|
)
|
||||||
if 'schema_renderers' in kwargs:
|
if 'schema_renderers' in kwargs:
|
||||||
assert 'schema_title' in kwargs, 'Missing "schema_title" argument.'
|
assert 'schema_title' in kwargs, 'Missing "schema_title" argument.'
|
||||||
|
|
|
@ -1159,6 +1159,11 @@ class ModelSerializer(Serializer):
|
||||||
field_class = field_mapping[model_field]
|
field_class = field_mapping[model_field]
|
||||||
field_kwargs = get_field_kwargs(field_name, model_field)
|
field_kwargs = get_field_kwargs(field_name, model_field)
|
||||||
|
|
||||||
|
# Special case to handle when a OneToOneField is also the primary key
|
||||||
|
if model_field.one_to_one and model_field.primary_key:
|
||||||
|
field_class = self.serializer_related_field
|
||||||
|
field_kwargs['queryset'] = model_field.related_model.objects
|
||||||
|
|
||||||
if 'choices' in field_kwargs:
|
if 'choices' in field_kwargs:
|
||||||
# Fields with choices get coerced into `ChoiceField`
|
# Fields with choices get coerced into `ChoiceField`
|
||||||
# instead of using their regular typed field.
|
# instead of using their regular typed field.
|
||||||
|
|
|
@ -102,7 +102,7 @@ $(function () {
|
||||||
var entry = entries[i]
|
var entry = entries[i]
|
||||||
var paramKey = entry[0]
|
var paramKey = entry[0]
|
||||||
var paramValue = entry[1]
|
var paramValue = entry[1]
|
||||||
var $elem = $form.find('[name=' + paramKey + ']')
|
var $elem = $form.find('[name="' + paramKey + '"]')
|
||||||
var dataType = $elem.data('type') || 'string'
|
var dataType = $elem.data('type') || 'string'
|
||||||
|
|
||||||
if (dataType === 'integer' && paramValue) {
|
if (dataType === 'integer' && paramValue) {
|
||||||
|
|
|
@ -37,6 +37,21 @@ function doAjaxSubmit(e) {
|
||||||
|
|
||||||
if (contentType) {
|
if (contentType) {
|
||||||
data = form.find('[data-override="content"]').val() || ''
|
data = form.find('[data-override="content"]').val() || ''
|
||||||
|
|
||||||
|
if (contentType === 'multipart/form-data') {
|
||||||
|
// We need to add a boundary parameter to the header
|
||||||
|
// We assume the first valid-looking boundary line in the body is correct
|
||||||
|
// regex is from RFC 2046 appendix A
|
||||||
|
var boundaryCharNoSpace = "0-9A-Z'()+_,-./:=?";
|
||||||
|
var boundaryChar = boundaryCharNoSpace + ' ';
|
||||||
|
var re = new RegExp('^--([' + boundaryChar + ']{0,69}[' + boundaryCharNoSpace + '])[\\s]*?$', 'im');
|
||||||
|
var boundary = data.match(re);
|
||||||
|
if (boundary !== null) {
|
||||||
|
contentType += '; boundary="' + boundary[1] + '"';
|
||||||
|
}
|
||||||
|
// Fix textarea.value EOL normalisation (multipart/form-data should use CR+NL, not NL)
|
||||||
|
data = data.replace(/\n/g, '\r\n');
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
contentType = form.attr('enctype') || form.attr('encoding')
|
contentType = form.attr('enctype') || form.attr('encoding')
|
||||||
|
|
||||||
|
|
|
@ -32,7 +32,8 @@
|
||||||
|
|
||||||
<div class="wrapper">
|
<div class="wrapper">
|
||||||
{% block navbar %}
|
{% block navbar %}
|
||||||
<div class="navbar navbar-static-top {% block bootstrap_navbar_variant %}navbar-inverse{% endblock %}">
|
<div class="navbar navbar-static-top {% block bootstrap_navbar_variant %}navbar-inverse{% endblock %}"
|
||||||
|
role="navigation" aria-label="{% trans "navbar" %}">
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<span>
|
<span>
|
||||||
{% block branding %}
|
{% block branding %}
|
||||||
|
@ -70,9 +71,10 @@
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
<!-- Content -->
|
<!-- Content -->
|
||||||
<div id="content">
|
<div id="content" role="main" aria-label="{% trans "content" %}">
|
||||||
{% block content %}
|
{% block content %}
|
||||||
|
|
||||||
|
<div class="region" aria-label="{% trans "request form" %}">
|
||||||
{% if 'GET' in allowed_methods %}
|
{% if 'GET' in allowed_methods %}
|
||||||
<form id="get-form" class="pull-right">
|
<form id="get-form" class="pull-right">
|
||||||
<fieldset>
|
<fieldset>
|
||||||
|
@ -131,8 +133,9 @@
|
||||||
{% trans "Filters" %}
|
{% trans "Filters" %}
|
||||||
</button>
|
</button>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="content-main">
|
<div class="content-main" role="main" aria-label="{% trans "main content" %}">
|
||||||
<div class="page-header">
|
<div class="page-header">
|
||||||
<h1>{{ name }}</h1>
|
<h1>{{ name }}</h1>
|
||||||
</div>
|
</div>
|
||||||
|
@ -148,11 +151,11 @@
|
||||||
</nav>
|
</nav>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
<div class="request-info" style="clear: both" >
|
<div class="request-info" style="clear: both" aria-label="{% trans "request info" %}">
|
||||||
<pre class="prettyprint"><b>{{ request.method }}</b> {{ request.get_full_path }}</pre>
|
<pre class="prettyprint"><b>{{ request.method }}</b> {{ request.get_full_path }}</pre>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="response-info">
|
<div class="response-info" aria-label="{% trans "response info" %}">
|
||||||
<pre class="prettyprint"><span class="meta nocode"><b>HTTP {{ response.status_code }} {{ response.status_text }}</b>{% autoescape off %}{% for key, val in response_headers|items %}
|
<pre class="prettyprint"><span class="meta nocode"><b>HTTP {{ response.status_code }} {{ response.status_text }}</b>{% autoescape off %}{% for key, val in response_headers|items %}
|
||||||
<b>{{ key }}:</b> <span class="lit">{{ val|break_long_headers|urlize_quoted_links }}</span>{% endfor %}
|
<b>{{ key }}:</b> <span class="lit">{{ val|break_long_headers|urlize_quoted_links }}</span>{% endfor %}
|
||||||
|
|
||||||
|
|
|
@ -192,7 +192,8 @@ def get_field_kwargs(field_name, model_field):
|
||||||
# rather than as a validator.
|
# rather than as a validator.
|
||||||
max_length = getattr(model_field, 'max_length', None)
|
max_length = getattr(model_field, 'max_length', None)
|
||||||
if max_length is not None and (isinstance(model_field, models.CharField) or
|
if max_length is not None and (isinstance(model_field, models.CharField) or
|
||||||
isinstance(model_field, models.TextField)):
|
isinstance(model_field, models.TextField) or
|
||||||
|
isinstance(model_field, models.FileField)):
|
||||||
kwargs['max_length'] = max_length
|
kwargs['max_length'] = max_length
|
||||||
validator_kwarg = [
|
validator_kwarg = [
|
||||||
validator for validator in validator_kwarg
|
validator for validator in validator_kwarg
|
||||||
|
|
|
@ -88,3 +88,11 @@ class NullableOneToOneSource(RESTFrameworkModel):
|
||||||
target = models.OneToOneField(
|
target = models.OneToOneField(
|
||||||
OneToOneTarget, null=True, blank=True,
|
OneToOneTarget, null=True, blank=True,
|
||||||
related_name='nullable_source', on_delete=models.CASCADE)
|
related_name='nullable_source', on_delete=models.CASCADE)
|
||||||
|
|
||||||
|
|
||||||
|
class OneToOnePKSource(RESTFrameworkModel):
|
||||||
|
""" Test model where the primary key is a OneToOneField with another model. """
|
||||||
|
name = models.CharField(max_length=100)
|
||||||
|
target = models.OneToOneField(
|
||||||
|
OneToOneTarget, primary_key=True,
|
||||||
|
related_name='required_source', on_delete=models.CASCADE)
|
||||||
|
|
|
@ -63,6 +63,7 @@ class RegularFieldsModel(models.Model):
|
||||||
slug_field = models.SlugField(max_length=100)
|
slug_field = models.SlugField(max_length=100)
|
||||||
small_integer_field = models.SmallIntegerField()
|
small_integer_field = models.SmallIntegerField()
|
||||||
text_field = models.TextField(max_length=100)
|
text_field = models.TextField(max_length=100)
|
||||||
|
file_field = models.FileField(max_length=100)
|
||||||
time_field = models.TimeField()
|
time_field = models.TimeField()
|
||||||
url_field = models.URLField(max_length=100)
|
url_field = models.URLField(max_length=100)
|
||||||
custom_field = CustomField()
|
custom_field = CustomField()
|
||||||
|
@ -181,6 +182,7 @@ class TestRegularFieldMappings(TestCase):
|
||||||
slug_field = SlugField(max_length=100)
|
slug_field = SlugField(max_length=100)
|
||||||
small_integer_field = IntegerField()
|
small_integer_field = IntegerField()
|
||||||
text_field = CharField(max_length=100, style={'base_template': 'textarea.html'})
|
text_field = CharField(max_length=100, style={'base_template': 'textarea.html'})
|
||||||
|
file_field = FileField(max_length=100)
|
||||||
time_field = TimeField()
|
time_field = TimeField()
|
||||||
url_field = URLField(max_length=100)
|
url_field = URLField(max_length=100)
|
||||||
custom_field = ModelField(model_field=<tests.test_model_serializer.CustomField: custom_field>)
|
custom_field = ModelField(model_field=<tests.test_model_serializer.CustomField: custom_field>)
|
||||||
|
|
|
@ -1,12 +1,13 @@
|
||||||
import uuid
|
import uuid
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
from _pytest.monkeypatch import MonkeyPatch
|
||||||
from django.conf.urls import url
|
from django.conf.urls import url
|
||||||
from django.core.exceptions import ImproperlyConfigured
|
from django.core.exceptions import ImproperlyConfigured
|
||||||
from django.test import override_settings
|
from django.test import override_settings
|
||||||
from django.utils.datastructures import MultiValueDict
|
from django.utils.datastructures import MultiValueDict
|
||||||
|
|
||||||
from rest_framework import serializers
|
from rest_framework import relations, serializers
|
||||||
from rest_framework.fields import empty
|
from rest_framework.fields import empty
|
||||||
from rest_framework.test import APISimpleTestCase
|
from rest_framework.test import APISimpleTestCase
|
||||||
|
|
||||||
|
@ -25,6 +26,61 @@ class TestStringRelatedField(APISimpleTestCase):
|
||||||
assert representation == '<MockObject name=foo, pk=1>'
|
assert representation == '<MockObject name=foo, pk=1>'
|
||||||
|
|
||||||
|
|
||||||
|
class MockApiSettings(object):
|
||||||
|
def __init__(self, cutoff, cutoff_text):
|
||||||
|
self.HTML_SELECT_CUTOFF = cutoff
|
||||||
|
self.HTML_SELECT_CUTOFF_TEXT = cutoff_text
|
||||||
|
|
||||||
|
|
||||||
|
class TestRelatedFieldHTMLCutoff(APISimpleTestCase):
|
||||||
|
def setUp(self):
|
||||||
|
self.queryset = MockQueryset([
|
||||||
|
MockObject(pk=i, name=str(i)) for i in range(0, 1100)
|
||||||
|
])
|
||||||
|
self.monkeypatch = MonkeyPatch()
|
||||||
|
|
||||||
|
def test_no_settings(self):
|
||||||
|
# The default is 1,000, so sans settings it should be 1,000 plus one.
|
||||||
|
for many in (False, True):
|
||||||
|
field = serializers.PrimaryKeyRelatedField(queryset=self.queryset,
|
||||||
|
many=many)
|
||||||
|
options = list(field.iter_options())
|
||||||
|
assert len(options) == 1001
|
||||||
|
assert options[-1].display_text == "More than 1000 items..."
|
||||||
|
|
||||||
|
def test_settings_cutoff(self):
|
||||||
|
self.monkeypatch.setattr(relations, "api_settings",
|
||||||
|
MockApiSettings(2, "Cut Off"))
|
||||||
|
for many in (False, True):
|
||||||
|
field = serializers.PrimaryKeyRelatedField(queryset=self.queryset,
|
||||||
|
many=many)
|
||||||
|
options = list(field.iter_options())
|
||||||
|
assert len(options) == 3 # 2 real items plus the 'Cut Off' item.
|
||||||
|
assert options[-1].display_text == "Cut Off"
|
||||||
|
|
||||||
|
def test_settings_cutoff_none(self):
|
||||||
|
# Setting it to None should mean no limit; the default limit is 1,000.
|
||||||
|
self.monkeypatch.setattr(relations, "api_settings",
|
||||||
|
MockApiSettings(None, "Cut Off"))
|
||||||
|
for many in (False, True):
|
||||||
|
field = serializers.PrimaryKeyRelatedField(queryset=self.queryset,
|
||||||
|
many=many)
|
||||||
|
options = list(field.iter_options())
|
||||||
|
assert len(options) == 1100
|
||||||
|
|
||||||
|
def test_settings_kwargs_cutoff(self):
|
||||||
|
# The explicit argument should override the settings.
|
||||||
|
self.monkeypatch.setattr(relations, "api_settings",
|
||||||
|
MockApiSettings(2, "Cut Off"))
|
||||||
|
for many in (False, True):
|
||||||
|
field = serializers.PrimaryKeyRelatedField(queryset=self.queryset,
|
||||||
|
many=many,
|
||||||
|
html_cutoff=100)
|
||||||
|
options = list(field.iter_options())
|
||||||
|
assert len(options) == 101
|
||||||
|
assert options[-1].display_text == "Cut Off"
|
||||||
|
|
||||||
|
|
||||||
class TestPrimaryKeyRelatedField(APISimpleTestCase):
|
class TestPrimaryKeyRelatedField(APISimpleTestCase):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
self.queryset = MockQueryset([
|
self.queryset = MockQueryset([
|
||||||
|
@ -96,7 +152,7 @@ class TestHyperlinkedRelatedField(APISimpleTestCase):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
self.queryset = MockQueryset([
|
self.queryset = MockQueryset([
|
||||||
MockObject(pk=1, name='foobar'),
|
MockObject(pk=1, name='foobar'),
|
||||||
MockObject(pk=2, name='baz qux'),
|
MockObject(pk=2, name='bazABCqux'),
|
||||||
])
|
])
|
||||||
self.field = serializers.HyperlinkedRelatedField(
|
self.field = serializers.HyperlinkedRelatedField(
|
||||||
view_name='example',
|
view_name='example',
|
||||||
|
@ -116,7 +172,7 @@ class TestHyperlinkedRelatedField(APISimpleTestCase):
|
||||||
assert instance is self.queryset.items[0]
|
assert instance is self.queryset.items[0]
|
||||||
|
|
||||||
def test_hyperlinked_related_lookup_url_encoded_exists(self):
|
def test_hyperlinked_related_lookup_url_encoded_exists(self):
|
||||||
instance = self.field.to_internal_value('http://example.org/example/baz%20qux/')
|
instance = self.field.to_internal_value('http://example.org/example/baz%41%42%43qux/')
|
||||||
assert instance is self.queryset.items[1]
|
assert instance is self.queryset.items[1]
|
||||||
|
|
||||||
def test_hyperlinked_related_lookup_does_not_exist(self):
|
def test_hyperlinked_related_lookup_does_not_exist(self):
|
||||||
|
|
|
@ -6,8 +6,8 @@ from django.utils import six
|
||||||
from rest_framework import serializers
|
from rest_framework import serializers
|
||||||
from tests.models import (
|
from tests.models import (
|
||||||
ForeignKeySource, ForeignKeyTarget, ManyToManySource, ManyToManyTarget,
|
ForeignKeySource, ForeignKeyTarget, ManyToManySource, ManyToManyTarget,
|
||||||
NullableForeignKeySource, NullableOneToOneSource,
|
NullableForeignKeySource, NullableOneToOneSource, NullableUUIDForeignKeySource,
|
||||||
NullableUUIDForeignKeySource, OneToOneTarget, UUIDForeignKeyTarget
|
OneToOnePKSource, OneToOneTarget, UUIDForeignKeyTarget
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@ -63,6 +63,13 @@ class NullableOneToOneTargetSerializer(serializers.ModelSerializer):
|
||||||
fields = ('id', 'name', 'nullable_source')
|
fields = ('id', 'name', 'nullable_source')
|
||||||
|
|
||||||
|
|
||||||
|
class OneToOnePKSourceSerializer(serializers.ModelSerializer):
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = OneToOnePKSource
|
||||||
|
fields = '__all__'
|
||||||
|
|
||||||
|
|
||||||
# TODO: Add test that .data cannot be accessed prior to .is_valid
|
# TODO: Add test that .data cannot be accessed prior to .is_valid
|
||||||
|
|
||||||
class PKManyToManyTests(TestCase):
|
class PKManyToManyTests(TestCase):
|
||||||
|
@ -486,3 +493,51 @@ class PKNullableOneToOneTests(TestCase):
|
||||||
{'id': 2, 'name': 'target-2', 'nullable_source': 1},
|
{'id': 2, 'name': 'target-2', 'nullable_source': 1},
|
||||||
]
|
]
|
||||||
assert serializer.data == expected
|
assert serializer.data == expected
|
||||||
|
|
||||||
|
|
||||||
|
class OneToOnePrimaryKeyTests(TestCase):
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
# Given: Some target models already exist
|
||||||
|
self.target = target = OneToOneTarget(name='target-1')
|
||||||
|
target.save()
|
||||||
|
self.alt_target = alt_target = OneToOneTarget(name='target-2')
|
||||||
|
alt_target.save()
|
||||||
|
|
||||||
|
def test_one_to_one_when_primary_key(self):
|
||||||
|
# When: Creating a Source pointing at the id of the second Target
|
||||||
|
target_pk = self.alt_target.id
|
||||||
|
source = OneToOnePKSourceSerializer(data={'name': 'source-2', 'target': target_pk})
|
||||||
|
# Then: The source is valid with the serializer
|
||||||
|
if not source.is_valid():
|
||||||
|
self.fail("Expected OneToOnePKTargetSerializer to be valid but had errors: {}".format(source.errors))
|
||||||
|
# Then: Saving the serializer creates a new object
|
||||||
|
new_source = source.save()
|
||||||
|
# Then: The new object has the same pk as the target object
|
||||||
|
self.assertEqual(new_source.pk, target_pk)
|
||||||
|
|
||||||
|
def test_one_to_one_when_primary_key_no_duplicates(self):
|
||||||
|
# When: Creating a Source pointing at the id of the second Target
|
||||||
|
target_pk = self.target.id
|
||||||
|
data = {'name': 'source-1', 'target': target_pk}
|
||||||
|
source = OneToOnePKSourceSerializer(data=data)
|
||||||
|
# Then: The source is valid with the serializer
|
||||||
|
self.assertTrue(source.is_valid())
|
||||||
|
# Then: Saving the serializer creates a new object
|
||||||
|
new_source = source.save()
|
||||||
|
# Then: The new object has the same pk as the target object
|
||||||
|
self.assertEqual(new_source.pk, target_pk)
|
||||||
|
# When: Trying to create a second object
|
||||||
|
second_source = OneToOnePKSourceSerializer(data=data)
|
||||||
|
self.assertFalse(second_source.is_valid())
|
||||||
|
expected = {'target': [u'one to one pk source with this target already exists.']}
|
||||||
|
self.assertDictEqual(second_source.errors, expected)
|
||||||
|
|
||||||
|
def test_one_to_one_when_primary_key_does_not_exist(self):
|
||||||
|
# Given: a target PK that does not exist
|
||||||
|
target_pk = self.target.pk + self.alt_target.pk
|
||||||
|
source = OneToOnePKSourceSerializer(data={'name': 'source-2', 'target': target_pk})
|
||||||
|
# Then: The source is not valid with the serializer
|
||||||
|
self.assertFalse(source.is_valid())
|
||||||
|
self.assertIn("Invalid pk", source.errors['target'][0])
|
||||||
|
self.assertIn("object does not exist", source.errors['target'][0])
|
||||||
|
|
|
@ -65,6 +65,19 @@ class EmptyPrefixViewSet(viewsets.ModelViewSet):
|
||||||
return self.queryset[index]
|
return self.queryset[index]
|
||||||
|
|
||||||
|
|
||||||
|
class RegexUrlPathViewSet(viewsets.ViewSet):
|
||||||
|
@list_route(url_path='list/(?P<kwarg>[0-9]{4})')
|
||||||
|
def regex_url_path_list(self, request, *args, **kwargs):
|
||||||
|
kwarg = self.kwargs.get('kwarg', '')
|
||||||
|
return Response({'kwarg': kwarg})
|
||||||
|
|
||||||
|
@detail_route(url_path='detail/(?P<kwarg>[0-9]{4})')
|
||||||
|
def regex_url_path_detail(self, request, *args, **kwargs):
|
||||||
|
pk = self.kwargs.get('pk', '')
|
||||||
|
kwarg = self.kwargs.get('kwarg', '')
|
||||||
|
return Response({'pk': pk, 'kwarg': kwarg})
|
||||||
|
|
||||||
|
|
||||||
notes_router = SimpleRouter()
|
notes_router = SimpleRouter()
|
||||||
notes_router.register(r'notes', NoteViewSet)
|
notes_router.register(r'notes', NoteViewSet)
|
||||||
|
|
||||||
|
@ -80,6 +93,9 @@ empty_prefix_urls = [
|
||||||
url(r'^', include(empty_prefix_router.urls)),
|
url(r'^', include(empty_prefix_router.urls)),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
regex_url_path_router = SimpleRouter()
|
||||||
|
regex_url_path_router.register(r'', RegexUrlPathViewSet, base_name='regex')
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
url(r'^non-namespaced/', include(namespaced_router.urls)),
|
url(r'^non-namespaced/', include(namespaced_router.urls)),
|
||||||
url(r'^namespaced/', include(namespaced_router.urls, namespace='example', app_name='example')),
|
url(r'^namespaced/', include(namespaced_router.urls, namespace='example', app_name='example')),
|
||||||
|
@ -87,6 +103,7 @@ urlpatterns = [
|
||||||
url(r'^example2/', include(kwarged_notes_router.urls)),
|
url(r'^example2/', include(kwarged_notes_router.urls)),
|
||||||
|
|
||||||
url(r'^empty-prefix/', include(empty_prefix_urls)),
|
url(r'^empty-prefix/', include(empty_prefix_urls)),
|
||||||
|
url(r'^regex/', include(regex_url_path_router.urls))
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
@ -402,3 +419,19 @@ class TestEmptyPrefix(TestCase):
|
||||||
response = self.client.get('/empty-prefix/1/')
|
response = self.client.get('/empty-prefix/1/')
|
||||||
assert response.status_code == 200
|
assert response.status_code == 200
|
||||||
assert json.loads(response.content.decode('utf-8')) == {'uuid': '111', 'text': 'First'}
|
assert json.loads(response.content.decode('utf-8')) == {'uuid': '111', 'text': 'First'}
|
||||||
|
|
||||||
|
|
||||||
|
@override_settings(ROOT_URLCONF='tests.test_routers')
|
||||||
|
class TestRegexUrlPath(TestCase):
|
||||||
|
def test_regex_url_path_list(self):
|
||||||
|
kwarg = '1234'
|
||||||
|
response = self.client.get('/regex/list/{}/'.format(kwarg))
|
||||||
|
assert response.status_code == 200
|
||||||
|
assert json.loads(response.content.decode('utf-8')) == {'kwarg': kwarg}
|
||||||
|
|
||||||
|
def test_regex_url_path_detail(self):
|
||||||
|
pk = '1'
|
||||||
|
kwarg = '1234'
|
||||||
|
response = self.client.get('/regex/{}/detail/{}/'.format(pk, kwarg))
|
||||||
|
assert response.status_code == 200
|
||||||
|
assert json.loads(response.content.decode('utf-8')) == {'pk': pk, 'kwarg': kwarg}
|
||||||
|
|
|
@ -20,6 +20,9 @@ class MockQueryset(object):
|
||||||
def __init__(self, iterable):
|
def __init__(self, iterable):
|
||||||
self.items = iterable
|
self.items = iterable
|
||||||
|
|
||||||
|
def __getitem__(self, val):
|
||||||
|
return self.items[val]
|
||||||
|
|
||||||
def get(self, **lookup):
|
def get(self, **lookup):
|
||||||
for item in self.items:
|
for item in self.items:
|
||||||
if all([
|
if all([
|
||||||
|
|
Loading…
Reference in New Issue
Block a user