mirror of
https://github.com/encode/django-rest-framework.git
synced 2024-11-29 13:04:03 +03:00
Merge branch 'master' into version-3.1
This commit is contained in:
commit
6168f60ba8
|
@ -156,7 +156,7 @@ Send a description of the issue via email to [rest-framework-security@googlegrou
|
||||||
|
|
||||||
# License
|
# License
|
||||||
|
|
||||||
Copyright (c) 2011-2014, Tom Christie
|
Copyright (c) 2011-2015, Tom Christie
|
||||||
All rights reserved.
|
All rights reserved.
|
||||||
|
|
||||||
Redistribution and use in source and binary forms, with or without
|
Redistribution and use in source and binary forms, with or without
|
||||||
|
|
|
@ -480,7 +480,7 @@ Let's look at an example of serializing a class that represents an RGB color val
|
||||||
|
|
||||||
class ColorField(serializers.Field):
|
class ColorField(serializers.Field):
|
||||||
"""
|
"""
|
||||||
Color objects are serialized into "rgb(#, #, #)" notation.
|
Color objects are serialized into 'rgb(#, #, #)' notation.
|
||||||
"""
|
"""
|
||||||
def to_representation(self, obj):
|
def to_representation(self, obj):
|
||||||
return "rgb(%d, %d, %d)" % (obj.red, obj.green, obj.blue)
|
return "rgb(%d, %d, %d)" % (obj.red, obj.green, obj.blue)
|
||||||
|
|
|
@ -55,6 +55,18 @@ The name of the kwarg used may be modified by using the `FORMAT_SUFFIX_KWARG` se
|
||||||
|
|
||||||
Also note that `format_suffix_patterns` does not support descending into `include` URL patterns.
|
Also note that `format_suffix_patterns` does not support descending into `include` URL patterns.
|
||||||
|
|
||||||
|
### Using with `i18n_patterns`
|
||||||
|
|
||||||
|
If using the `i18n_patterns` function provided by Django, as well as `format_suffix_patterns` you should make sure that the `i18n_patterns` function is applied as the final, or outermost function. For example:
|
||||||
|
|
||||||
|
url patterns = [
|
||||||
|
…
|
||||||
|
]
|
||||||
|
|
||||||
|
urlpatterns = i18n_patterns(
|
||||||
|
format_suffix_patterns(urlpatterns, allowed=['json', 'html'])
|
||||||
|
)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Accept headers vs. format suffixes
|
## Accept headers vs. format suffixes
|
||||||
|
|
|
@ -214,6 +214,8 @@ You won't typically need to override the following methods, although you might n
|
||||||
|
|
||||||
The mixin classes provide the actions that are used to provide the basic view behavior. Note that the mixin classes provide action methods rather than defining the handler methods, such as `.get()` and `.post()`, directly. This allows for more flexible composition of behavior.
|
The mixin classes provide the actions that are used to provide the basic view behavior. Note that the mixin classes provide action methods rather than defining the handler methods, such as `.get()` and `.post()`, directly. This allows for more flexible composition of behavior.
|
||||||
|
|
||||||
|
The mixin classes can be imported from `rest_framework.mixins`.
|
||||||
|
|
||||||
## ListModelMixin
|
## ListModelMixin
|
||||||
|
|
||||||
Provides a `.list(request, *args, **kwargs)` method, that implements listing a queryset.
|
Provides a `.list(request, *args, **kwargs)` method, that implements listing a queryset.
|
||||||
|
@ -258,6 +260,8 @@ If an object is deleted this returns a `204 No Content` response, otherwise it w
|
||||||
|
|
||||||
The following classes are the concrete generic views. If you're using generic views this is normally the level you'll be working at unless you need heavily customized behavior.
|
The following classes are the concrete generic views. If you're using generic views this is normally the level you'll be working at unless you need heavily customized behavior.
|
||||||
|
|
||||||
|
The view classes can be imported from `rest_framework.generics`.
|
||||||
|
|
||||||
## CreateAPIView
|
## CreateAPIView
|
||||||
|
|
||||||
Used for **create-only** endpoints.
|
Used for **create-only** endpoints.
|
||||||
|
|
|
@ -231,6 +231,7 @@ This field is always read-only.
|
||||||
|
|
||||||
* `view_name` - The view name that should be used as the target of the relationship. If you're using [the standard router classes][routers] this will be a string with the format `<model_name>-detail`. **required**.
|
* `view_name` - The view name that should be used as the target of the relationship. If you're using [the standard router classes][routers] this will be a string with the format `<model_name>-detail`. **required**.
|
||||||
* `lookup_field` - The field on the target that should be used for the lookup. Should correspond to a URL keyword argument on the referenced view. Default is `'pk'`.
|
* `lookup_field` - The field on the target that should be used for the lookup. Should correspond to a URL keyword argument on the referenced view. Default is `'pk'`.
|
||||||
|
* `lookup_url_kwarg` - The name of the keyword argument defined in the URL conf that corresponds to the lookup field. Defaults to using the same value as `lookup_field`.
|
||||||
* `format` - If using format suffixes, hyperlinked fields will use the same format suffix for the target unless overridden by using the `format` argument.
|
* `format` - If using format suffixes, hyperlinked fields will use the same format suffix for the target unless overridden by using the `format` argument.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
|
@ -49,6 +49,38 @@ This means you'll need to explicitly set the `base_name` argument when registeri
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
### Using `include` with routers
|
||||||
|
|
||||||
|
The `.urls` attribute on a router instance is simply a standard list of URL patterns. There are a number of different styles for how you can include these URLs.
|
||||||
|
|
||||||
|
For example, you can append `router.urls` to a list of existing views…
|
||||||
|
|
||||||
|
router = routers.SimpleRouter()
|
||||||
|
router.register(r'users', UserViewSet)
|
||||||
|
router.register(r'accounts', AccountViewSet)
|
||||||
|
|
||||||
|
urlpatterns = [
|
||||||
|
url(r'^forgot-password/$, ForgotPasswordFormView.as_view(),
|
||||||
|
]
|
||||||
|
|
||||||
|
urlpatterns += router.urls
|
||||||
|
|
||||||
|
Alternatively you can use Django's `include` function, like so…
|
||||||
|
|
||||||
|
urlpatterns = [
|
||||||
|
url(r'^forgot-password/$, ForgotPasswordFormView.as_view(),
|
||||||
|
url(r'^', include(router.urls))
|
||||||
|
]
|
||||||
|
|
||||||
|
Router URL patterns can also be namespaces.
|
||||||
|
|
||||||
|
urlpatterns = [
|
||||||
|
url(r'^forgot-password/$, ForgotPasswordFormView.as_view(),
|
||||||
|
url(r'^api/', include(router.urls, namespace='api'))
|
||||||
|
]
|
||||||
|
|
||||||
|
If using namespacing with hyperlinked serializers you'll also need to ensure that any `view_name` parameters on the serializers correctly reflect the namespace. In the example above you'd need to include a parameter such as `view_name='api:user-detail'` for serializer fields hyperlinked to the user detail view.
|
||||||
|
|
||||||
### Extra link and actions
|
### Extra link and actions
|
||||||
|
|
||||||
Any methods on the viewset decorated with `@detail_route` or `@list_route` will also be routed.
|
Any methods on the viewset decorated with `@detail_route` or `@list_route` will also be routed.
|
||||||
|
@ -68,6 +100,24 @@ The following URL pattern would additionally be generated:
|
||||||
|
|
||||||
* URL pattern: `^users/{pk}/set_password/$` Name: `'user-set-password'`
|
* URL pattern: `^users/{pk}/set_password/$` Name: `'user-set-password'`
|
||||||
|
|
||||||
|
If you do not want to use the default URL generated for your custom action, you can instead use the url_path parameter to customize it.
|
||||||
|
|
||||||
|
For example, if you want to change the URL for our custom action to `^users/{pk}/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_path='change-password')
|
||||||
|
def set_password(self, request, pk=None):
|
||||||
|
...
|
||||||
|
|
||||||
|
The above example would now generate the following URL pattern:
|
||||||
|
|
||||||
|
* URL pattern: `^users/{pk}/change-password/$` Name: `'user-change-password'`
|
||||||
|
|
||||||
For more information see the viewset documentation on [marking extra actions for routing][route-decorators].
|
For more information see the viewset documentation on [marking extra actions for routing][route-decorators].
|
||||||
|
|
||||||
# API Guide
|
# API Guide
|
||||||
|
|
|
@ -384,7 +384,7 @@ This manager class now more nicely encapsulates that user instances and profile
|
||||||
has_support_contract=validated_data['profile']['has_support_contract']
|
has_support_contract=validated_data['profile']['has_support_contract']
|
||||||
)
|
)
|
||||||
|
|
||||||
For more details on this approach see the Django documentation on [model managers](model-managers), and [this blogpost on using model and manger classes](encapsulation-blogpost).
|
For more details on this approach see the Django documentation on [model managers](model-managers), and [this blogpost on using model and manager classes](encapsulation-blogpost).
|
||||||
|
|
||||||
## Dealing with multiple objects
|
## Dealing with multiple objects
|
||||||
|
|
||||||
|
|
|
@ -231,7 +231,7 @@ Send a description of the issue via email to [rest-framework-security@googlegrou
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
Copyright (c) 2011-2014, Tom Christie
|
Copyright (c) 2011-2015, Tom Christie
|
||||||
All rights reserved.
|
All rights reserved.
|
||||||
|
|
||||||
Redistribution and use in source and binary forms, with or without
|
Redistribution and use in source and binary forms, with or without
|
||||||
|
|
|
@ -940,6 +940,7 @@ The default JSON renderer will return float objects for un-coerced `Decimal` ins
|
||||||
* The serializer `ChoiceField` does not currently display nested choices, as was the case in 2.4. This will be address as part of 3.1.
|
* The serializer `ChoiceField` does not currently display nested choices, as was the case in 2.4. This will be address as part of 3.1.
|
||||||
* Due to the new templated form rendering, the 'widget' option is no longer valid. This means there's no easy way of using third party "autocomplete" widgets for rendering select inputs that contain a large number of choices. You'll either need to use a regular select or a plain text input. We may consider addressing this in 3.1 or 3.2 if there's sufficient demand.
|
* Due to the new templated form rendering, the 'widget' option is no longer valid. This means there's no easy way of using third party "autocomplete" widgets for rendering select inputs that contain a large number of choices. You'll either need to use a regular select or a plain text input. We may consider addressing this in 3.1 or 3.2 if there's sufficient demand.
|
||||||
* Some of the default validation error messages were rewritten and might no longer be pre-translated. You can still [create language files with Django][django-localization] if you wish to localize them.
|
* Some of the default validation error messages were rewritten and might no longer be pre-translated. You can still [create language files with Django][django-localization] if you wish to localize them.
|
||||||
|
* `APIException` subclasses could previously take could previously take any arbitrary type in the `detail` argument. These exceptions now use translatable text strings, and as a result call `force_text` on the `detail` argument, which *must be a string*. If you need complex arguments to an `APIException` class, you should subclass it and override the `__init__()` method. Typically you'll instead want to use a custom exception handler to provide for non-standard error responses.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
|
@ -124,7 +124,7 @@ The first part of the serializer class defines the fields that get serialized/de
|
||||||
|
|
||||||
A serializer class is very similar to a Django `Form` class, and includes similar validation flags on the various fields, such as `required`, `max_length` and `default`.
|
A serializer class is very similar to a Django `Form` class, and includes similar validation flags on the various fields, such as `required`, `max_length` and `default`.
|
||||||
|
|
||||||
The field flags can also control how the serializer should be displayed in certain circumstances, such as when rendering to HTML. The `style={'type': 'textarea'}` flag above is equivelent to using `widget=widgets.Textarea` on a Django `Form` class. This is particularly useful for controlling how the browsable API should be displayed, as we'll see later in the tutorial.
|
The field flags can also control how the serializer should be displayed in certain circumstances, such as when rendering to HTML. The `{'base_template': 'textarea.html'}` flag above is equivelent to using `widget=widgets.Textarea` on a Django `Form` class. This is particularly useful for controlling how the browsable API should be displayed, as we'll see later in the tutorial.
|
||||||
|
|
||||||
We can actually also save ourselves some time by using the `ModelSerializer` class, as we'll see later, but for now we'll keep our serializer definition explicit.
|
We can actually also save ourselves some time by using the `ModelSerializer` class, as we'll see later, but for now we'll keep our serializer definition explicit.
|
||||||
|
|
||||||
|
@ -206,7 +206,7 @@ One nice property that serializers have is that you can inspect all the fields i
|
||||||
SnippetSerializer():
|
SnippetSerializer():
|
||||||
id = IntegerField(label='ID', read_only=True)
|
id = IntegerField(label='ID', read_only=True)
|
||||||
title = CharField(allow_blank=True, max_length=100, required=False)
|
title = CharField(allow_blank=True, max_length=100, required=False)
|
||||||
code = CharField(style={'type': 'textarea'})
|
code = CharField(style={'base_template': 'textarea.html'})
|
||||||
linenos = BooleanField(required=False)
|
linenos = BooleanField(required=False)
|
||||||
language = ChoiceField(choices=[('Clipper', 'FoxPro'), ('Cucumber', 'Gherkin'), ('RobotFramework', 'RobotFramework'), ('abap', 'ABAP'), ('ada', 'Ada')...
|
language = ChoiceField(choices=[('Clipper', 'FoxPro'), ('Cucumber', 'Gherkin'), ('RobotFramework', 'RobotFramework'), ('abap', 'ABAP'), ('ada', 'Ada')...
|
||||||
style = ChoiceField(choices=[('autumn', 'autumn'), ('borland', 'borland'), ('bw', 'bw'), ('colorful', 'colorful')...
|
style = ChoiceField(choices=[('autumn', 'autumn'), ('borland', 'borland'), ('bw', 'bw'), ('colorful', 'colorful')...
|
||||||
|
|
|
@ -53,6 +53,8 @@ Notice that we've also used the `@detail_route` decorator to create a custom act
|
||||||
|
|
||||||
Custom actions which use the `@detail_route` decorator will respond to `GET` requests. We can use the `methods` argument if we wanted an action that responded to `POST` requests.
|
Custom actions which use the `@detail_route` decorator will respond to `GET` requests. We can use the `methods` argument if we wanted an action that responded to `POST` requests.
|
||||||
|
|
||||||
|
The URLs for custom actions by default depend on the method name itself. If you want to change the way url should be constructed, you can include url_path as a decorator keyword argument.
|
||||||
|
|
||||||
## Binding ViewSets to URLs explicitly
|
## Binding ViewSets to URLs explicitly
|
||||||
|
|
||||||
The handler methods only get bound to the actions when we define the URLConf.
|
The handler methods only get bound to the actions when we define the URLConf.
|
||||||
|
|
|
@ -11,7 +11,7 @@ __title__ = 'Django REST framework'
|
||||||
__version__ = '3.0.2'
|
__version__ = '3.0.2'
|
||||||
__author__ = 'Tom Christie'
|
__author__ = 'Tom Christie'
|
||||||
__license__ = 'BSD 2-Clause'
|
__license__ = 'BSD 2-Clause'
|
||||||
__copyright__ = 'Copyright 2011-2014 Tom Christie'
|
__copyright__ = 'Copyright 2011-2015 Tom Christie'
|
||||||
|
|
||||||
# Version synonym
|
# Version synonym
|
||||||
VERSION = __version__
|
VERSION = __version__
|
||||||
|
|
|
@ -55,6 +55,16 @@ except ImportError:
|
||||||
from django.http import HttpResponse as HttpResponseBase
|
from django.http import HttpResponse as HttpResponseBase
|
||||||
|
|
||||||
|
|
||||||
|
# request only provides `resolver_match` from 1.5 onwards.
|
||||||
|
def get_resolver_match(request):
|
||||||
|
try:
|
||||||
|
return request.resolver_match
|
||||||
|
except AttributeError:
|
||||||
|
# Django < 1.5
|
||||||
|
from django.core.urlresolvers import resolve
|
||||||
|
return resolve(request.path_info)
|
||||||
|
|
||||||
|
|
||||||
# django-filter is optional
|
# django-filter is optional
|
||||||
try:
|
try:
|
||||||
import django_filters
|
import django_filters
|
||||||
|
|
|
@ -273,7 +273,11 @@ class Field(object):
|
||||||
return empty
|
return empty
|
||||||
return self.default_empty_html
|
return self.default_empty_html
|
||||||
ret = dictionary[self.field_name]
|
ret = dictionary[self.field_name]
|
||||||
return self.default_empty_html if (ret == '') else ret
|
if ret == '' and self.allow_null:
|
||||||
|
# If the field is blank, and null is a valid value then
|
||||||
|
# determine if we should use null instead.
|
||||||
|
return '' if getattr(self, 'allow_blank', False) else None
|
||||||
|
return ret
|
||||||
return dictionary.get(self.field_name, empty)
|
return dictionary.get(self.field_name, empty)
|
||||||
|
|
||||||
def get_attribute(self, instance):
|
def get_attribute(self, instance):
|
||||||
|
@ -545,8 +549,6 @@ class CharField(Field):
|
||||||
'min_length': _('Ensure this field has at least {min_length} characters.')
|
'min_length': _('Ensure this field has at least {min_length} characters.')
|
||||||
}
|
}
|
||||||
initial = ''
|
initial = ''
|
||||||
coerce_blank_to_null = False
|
|
||||||
default_empty_html = ''
|
|
||||||
|
|
||||||
def __init__(self, **kwargs):
|
def __init__(self, **kwargs):
|
||||||
self.allow_blank = kwargs.pop('allow_blank', False)
|
self.allow_blank = kwargs.pop('allow_blank', False)
|
||||||
|
@ -560,11 +562,6 @@ class CharField(Field):
|
||||||
message = self.error_messages['min_length'].format(min_length=min_length)
|
message = self.error_messages['min_length'].format(min_length=min_length)
|
||||||
self.validators.append(MinLengthValidator(min_length, message=message))
|
self.validators.append(MinLengthValidator(min_length, message=message))
|
||||||
|
|
||||||
if self.allow_null and (not self.allow_blank) and (self.default is empty):
|
|
||||||
# HTML input cannot represent `None` values, so we need to
|
|
||||||
# forcibly coerce empty HTML values to `None` if `allow_null=True`.
|
|
||||||
self.default_empty_html = None
|
|
||||||
|
|
||||||
def run_validation(self, data=empty):
|
def run_validation(self, data=empty):
|
||||||
# Test for the empty string here so that it does not get validated,
|
# Test for the empty string here so that it does not get validated,
|
||||||
# and so that subclasses do not need to handle it explicitly
|
# and so that subclasses do not need to handle it explicitly
|
||||||
|
|
|
@ -68,10 +68,16 @@ class BasePaginationSerializer(serializers.Serializer):
|
||||||
except AttributeError:
|
except AttributeError:
|
||||||
object_serializer = DefaultObjectSerializer
|
object_serializer = DefaultObjectSerializer
|
||||||
|
|
||||||
self.fields[results_field] = serializers.ListSerializer(
|
try:
|
||||||
|
list_serializer_class = object_serializer.Meta.list_serializer_class
|
||||||
|
except AttributeError:
|
||||||
|
list_serializer_class = serializers.ListSerializer
|
||||||
|
|
||||||
|
self.fields[results_field] = list_serializer_class(
|
||||||
child=object_serializer(),
|
child=object_serializer(),
|
||||||
source='object_list'
|
source='object_list'
|
||||||
)
|
)
|
||||||
|
self.fields[results_field].bind(field_name=results_field, parent=self)
|
||||||
|
|
||||||
|
|
||||||
class PaginationSerializer(BasePaginationSerializer):
|
class PaginationSerializer(BasePaginationSerializer):
|
||||||
|
|
|
@ -21,7 +21,7 @@ from django.conf.urls import patterns, url
|
||||||
from django.core.exceptions import ImproperlyConfigured
|
from django.core.exceptions import ImproperlyConfigured
|
||||||
from django.core.urlresolvers import NoReverseMatch
|
from django.core.urlresolvers import NoReverseMatch
|
||||||
from rest_framework import views
|
from rest_framework import views
|
||||||
from rest_framework.compat import OrderedDict
|
from rest_framework.compat import get_resolver_match, OrderedDict
|
||||||
from rest_framework.response import Response
|
from rest_framework.response import Response
|
||||||
from rest_framework.reverse import reverse
|
from rest_framework.reverse import reverse
|
||||||
from rest_framework.urlpatterns import format_suffix_patterns
|
from rest_framework.urlpatterns import format_suffix_patterns
|
||||||
|
@ -176,23 +176,27 @@ class SimpleRouter(BaseRouter):
|
||||||
if isinstance(route, DynamicDetailRoute):
|
if isinstance(route, DynamicDetailRoute):
|
||||||
# Dynamic detail routes (@detail_route decorator)
|
# Dynamic detail routes (@detail_route decorator)
|
||||||
for httpmethods, methodname in detail_routes:
|
for httpmethods, methodname in detail_routes:
|
||||||
|
method_kwargs = getattr(viewset, methodname).kwargs
|
||||||
|
url_path = method_kwargs.pop("url_path", None) or methodname
|
||||||
initkwargs = route.initkwargs.copy()
|
initkwargs = route.initkwargs.copy()
|
||||||
initkwargs.update(getattr(viewset, methodname).kwargs)
|
initkwargs.update(method_kwargs)
|
||||||
ret.append(Route(
|
ret.append(Route(
|
||||||
url=replace_methodname(route.url, methodname),
|
url=replace_methodname(route.url, url_path),
|
||||||
mapping=dict((httpmethod, methodname) for httpmethod in httpmethods),
|
mapping=dict((httpmethod, methodname) for httpmethod in httpmethods),
|
||||||
name=replace_methodname(route.name, methodname),
|
name=replace_methodname(route.name, url_path),
|
||||||
initkwargs=initkwargs,
|
initkwargs=initkwargs,
|
||||||
))
|
))
|
||||||
elif isinstance(route, DynamicListRoute):
|
elif isinstance(route, DynamicListRoute):
|
||||||
# Dynamic list routes (@list_route decorator)
|
# Dynamic list routes (@list_route decorator)
|
||||||
for httpmethods, methodname in list_routes:
|
for httpmethods, methodname in list_routes:
|
||||||
|
method_kwargs = getattr(viewset, methodname).kwargs
|
||||||
|
url_path = method_kwargs.pop("url_path", None) or methodname
|
||||||
initkwargs = route.initkwargs.copy()
|
initkwargs = route.initkwargs.copy()
|
||||||
initkwargs.update(getattr(viewset, methodname).kwargs)
|
initkwargs.update(method_kwargs)
|
||||||
ret.append(Route(
|
ret.append(Route(
|
||||||
url=replace_methodname(route.url, methodname),
|
url=replace_methodname(route.url, url_path),
|
||||||
mapping=dict((httpmethod, methodname) for httpmethod in httpmethods),
|
mapping=dict((httpmethod, methodname) for httpmethod in httpmethods),
|
||||||
name=replace_methodname(route.name, methodname),
|
name=replace_methodname(route.name, url_path),
|
||||||
initkwargs=initkwargs,
|
initkwargs=initkwargs,
|
||||||
))
|
))
|
||||||
else:
|
else:
|
||||||
|
@ -288,7 +292,10 @@ class DefaultRouter(SimpleRouter):
|
||||||
|
|
||||||
def get(self, request, *args, **kwargs):
|
def get(self, request, *args, **kwargs):
|
||||||
ret = OrderedDict()
|
ret = OrderedDict()
|
||||||
|
namespace = get_resolver_match(request).namespace
|
||||||
for key, url_name in api_root_dict.items():
|
for key, url_name in api_root_dict.items():
|
||||||
|
if namespace:
|
||||||
|
url_name = namespace + ':' + url_name
|
||||||
try:
|
try:
|
||||||
ret[key] = reverse(
|
ret[key] = reverse(
|
||||||
url_name,
|
url_name,
|
||||||
|
|
|
@ -5,7 +5,7 @@
|
||||||
{% for key, text in field.choices.items %}
|
{% for key, text in field.choices.items %}
|
||||||
<div class="checkbox">
|
<div class="checkbox">
|
||||||
<label>
|
<label>
|
||||||
<input type="checkbox" name="{{ rest_framework/field.name }}" value="{{ key }}" {% if key in field.value %}checked{% endif %}>
|
<input type="checkbox" name="{{ field.name }}" value="{{ key }}" {% if key in field.value %}checked{% endif %}>
|
||||||
{{ text }}
|
{{ text }}
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,8 +1,4 @@
|
||||||
<fieldset>
|
<fieldset>
|
||||||
{% if field.label %}<legend {% if style.hide_label %}class="sr-only"{% endif %}>{{ field.label }}</legend>{% endif %}
|
{% if field.label %}<legend {% if style.hide_label %}class="sr-only"{% endif %}>{{ field.label }}</legend>{% endif %}
|
||||||
<!-- {% if field.label %}<legend {% if style.hide_label %}class="sr-only"{% endif %}>{{ field.label }}</legend>{% endif %}
|
|
||||||
{% for field_item in field.value.field_items.values() %}
|
|
||||||
{{ renderer.render_field(field_item, layout=layout) }}
|
|
||||||
{% endfor %} -->
|
|
||||||
<p>Lists are not currently supported in HTML input.</p>
|
<p>Lists are not currently supported in HTML input.</p>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
|
|
|
@ -10,6 +10,11 @@ from rest_framework.validators import UniqueValidator
|
||||||
import inspect
|
import inspect
|
||||||
|
|
||||||
|
|
||||||
|
NUMERIC_FIELD_TYPES = (
|
||||||
|
models.IntegerField, models.FloatField, models.DecimalField
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class ClassLookupDict(object):
|
class ClassLookupDict(object):
|
||||||
"""
|
"""
|
||||||
Takes a dictionary with classes as keys.
|
Takes a dictionary with classes as keys.
|
||||||
|
@ -80,7 +85,7 @@ def get_field_kwargs(field_name, model_field):
|
||||||
kwargs['decimal_places'] = decimal_places
|
kwargs['decimal_places'] = decimal_places
|
||||||
|
|
||||||
if isinstance(model_field, models.TextField):
|
if isinstance(model_field, models.TextField):
|
||||||
kwargs['style'] = {'type': 'textarea'}
|
kwargs['style'] = {'base_template': 'textarea.html'}
|
||||||
|
|
||||||
if isinstance(model_field, models.AutoField) or not model_field.editable:
|
if isinstance(model_field, models.AutoField) or not model_field.editable:
|
||||||
# If this field is read-only, then return early.
|
# If this field is read-only, then return early.
|
||||||
|
@ -106,7 +111,7 @@ def get_field_kwargs(field_name, model_field):
|
||||||
# Ensure that max_length is passed explicitly as a keyword arg,
|
# Ensure that max_length is passed explicitly as a keyword arg,
|
||||||
# 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:
|
if max_length is not None and isinstance(model_field, models.CharField):
|
||||||
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
|
||||||
|
@ -119,7 +124,7 @@ def get_field_kwargs(field_name, model_field):
|
||||||
validator.limit_value for validator in validator_kwarg
|
validator.limit_value for validator in validator_kwarg
|
||||||
if isinstance(validator, validators.MinLengthValidator)
|
if isinstance(validator, validators.MinLengthValidator)
|
||||||
), None)
|
), None)
|
||||||
if min_length is not None:
|
if min_length is not None and isinstance(model_field, models.CharField):
|
||||||
kwargs['min_length'] = min_length
|
kwargs['min_length'] = min_length
|
||||||
validator_kwarg = [
|
validator_kwarg = [
|
||||||
validator for validator in validator_kwarg
|
validator for validator in validator_kwarg
|
||||||
|
@ -132,7 +137,7 @@ def get_field_kwargs(field_name, model_field):
|
||||||
validator.limit_value for validator in validator_kwarg
|
validator.limit_value for validator in validator_kwarg
|
||||||
if isinstance(validator, validators.MaxValueValidator)
|
if isinstance(validator, validators.MaxValueValidator)
|
||||||
), None)
|
), None)
|
||||||
if max_value is not None:
|
if max_value is not None and isinstance(model_field, NUMERIC_FIELD_TYPES):
|
||||||
kwargs['max_value'] = max_value
|
kwargs['max_value'] = max_value
|
||||||
validator_kwarg = [
|
validator_kwarg = [
|
||||||
validator for validator in validator_kwarg
|
validator for validator in validator_kwarg
|
||||||
|
@ -145,7 +150,7 @@ def get_field_kwargs(field_name, model_field):
|
||||||
validator.limit_value for validator in validator_kwarg
|
validator.limit_value for validator in validator_kwarg
|
||||||
if isinstance(validator, validators.MinValueValidator)
|
if isinstance(validator, validators.MinValueValidator)
|
||||||
), None)
|
), None)
|
||||||
if min_value is not None:
|
if min_value is not None and isinstance(model_field, NUMERIC_FIELD_TYPES):
|
||||||
kwargs['min_value'] = min_value
|
kwargs['min_value'] = min_value
|
||||||
validator_kwarg = [
|
validator_kwarg = [
|
||||||
validator for validator in validator_kwarg
|
validator for validator in validator_kwarg
|
||||||
|
|
|
@ -35,7 +35,7 @@ def _resolve_model(obj):
|
||||||
Resolve supplied `obj` to a Django model class.
|
Resolve supplied `obj` to a Django model class.
|
||||||
|
|
||||||
`obj` must be a Django model class itself, or a string
|
`obj` must be a Django model class itself, or a string
|
||||||
representation of one. Useful in situtations like GH #1225 where
|
representation of one. Useful in situations like GH #1225 where
|
||||||
Django may not have resolved a string-based reference to a model in
|
Django may not have resolved a string-based reference to a model in
|
||||||
another model's foreign key definition.
|
another model's foreign key definition.
|
||||||
|
|
||||||
|
@ -56,23 +56,44 @@ def _resolve_model(obj):
|
||||||
|
|
||||||
def get_field_info(model):
|
def get_field_info(model):
|
||||||
"""
|
"""
|
||||||
Given a model class, returns a `FieldInfo` instance containing metadata
|
Given a model class, returns a `FieldInfo` instance, which is a
|
||||||
about the various field types on the model.
|
`namedtuple`, containing metadata about the various field types on the model
|
||||||
|
including information about their relationships.
|
||||||
"""
|
"""
|
||||||
opts = model._meta.concrete_model._meta
|
opts = model._meta.concrete_model._meta
|
||||||
|
|
||||||
# Deal with the primary key.
|
pk = _get_pk(opts)
|
||||||
|
fields = _get_fields(opts)
|
||||||
|
forward_relations = _get_forward_relationships(opts)
|
||||||
|
reverse_relations = _get_reverse_relationships(opts)
|
||||||
|
fields_and_pk = _merge_fields_and_pk(pk, fields)
|
||||||
|
relationships = _merge_relationships(forward_relations, reverse_relations)
|
||||||
|
|
||||||
|
return FieldInfo(pk, fields, forward_relations, reverse_relations,
|
||||||
|
fields_and_pk, relationships)
|
||||||
|
|
||||||
|
|
||||||
|
def _get_pk(opts):
|
||||||
pk = opts.pk
|
pk = opts.pk
|
||||||
while pk.rel and pk.rel.parent_link:
|
while pk.rel and pk.rel.parent_link:
|
||||||
# If model is a child via multitable inheritance, use parent's pk.
|
# If model is a child via multi-table inheritance, use parent's pk.
|
||||||
pk = pk.rel.to._meta.pk
|
pk = pk.rel.to._meta.pk
|
||||||
|
|
||||||
# Deal with regular fields.
|
return pk
|
||||||
|
|
||||||
|
|
||||||
|
def _get_fields(opts):
|
||||||
fields = OrderedDict()
|
fields = OrderedDict()
|
||||||
for field in [field for field in opts.fields if field.serialize and not field.rel]:
|
for field in [field for field in opts.fields if field.serialize and not field.rel]:
|
||||||
fields[field.name] = field
|
fields[field.name] = field
|
||||||
|
|
||||||
# Deal with forward relationships.
|
return fields
|
||||||
|
|
||||||
|
|
||||||
|
def _get_forward_relationships(opts):
|
||||||
|
"""
|
||||||
|
Returns an `OrderedDict` of field names to `RelationInfo`.
|
||||||
|
"""
|
||||||
forward_relations = OrderedDict()
|
forward_relations = OrderedDict()
|
||||||
for field in [field for field in opts.fields if field.serialize and field.rel]:
|
for field in [field for field in opts.fields if field.serialize and field.rel]:
|
||||||
forward_relations[field.name] = RelationInfo(
|
forward_relations[field.name] = RelationInfo(
|
||||||
|
@ -93,7 +114,13 @@ def get_field_info(model):
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
# Deal with reverse relationships.
|
return forward_relations
|
||||||
|
|
||||||
|
|
||||||
|
def _get_reverse_relationships(opts):
|
||||||
|
"""
|
||||||
|
Returns an `OrderedDict` of field names to `RelationInfo`.
|
||||||
|
"""
|
||||||
reverse_relations = OrderedDict()
|
reverse_relations = OrderedDict()
|
||||||
for relation in opts.get_all_related_objects():
|
for relation in opts.get_all_related_objects():
|
||||||
accessor_name = relation.get_accessor_name()
|
accessor_name = relation.get_accessor_name()
|
||||||
|
@ -117,18 +144,20 @@ def get_field_info(model):
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
# Shortcut that merges both regular fields and the pk,
|
return reverse_relations
|
||||||
# for simplifying regular field lookup.
|
|
||||||
|
|
||||||
|
def _merge_fields_and_pk(pk, fields):
|
||||||
fields_and_pk = OrderedDict()
|
fields_and_pk = OrderedDict()
|
||||||
fields_and_pk['pk'] = pk
|
fields_and_pk['pk'] = pk
|
||||||
fields_and_pk[pk.name] = pk
|
fields_and_pk[pk.name] = pk
|
||||||
fields_and_pk.update(fields)
|
fields_and_pk.update(fields)
|
||||||
|
|
||||||
# Shortcut that merges both forward and reverse relationships
|
return fields_and_pk
|
||||||
|
|
||||||
relations = OrderedDict(
|
|
||||||
|
def _merge_relationships(forward_relations, reverse_relations):
|
||||||
|
return OrderedDict(
|
||||||
list(forward_relations.items()) +
|
list(forward_relations.items()) +
|
||||||
list(reverse_relations.items())
|
list(reverse_relations.items())
|
||||||
)
|
)
|
||||||
|
|
||||||
return FieldInfo(pk, fields, forward_relations, reverse_relations, fields_and_pk, relations)
|
|
||||||
|
|
|
@ -223,8 +223,8 @@ class MockHTMLDict(dict):
|
||||||
getlist = None
|
getlist = None
|
||||||
|
|
||||||
|
|
||||||
class TestCharHTMLInput:
|
class TestHTMLInput:
|
||||||
def test_empty_html_checkbox(self):
|
def test_empty_html_charfield(self):
|
||||||
class TestSerializer(serializers.Serializer):
|
class TestSerializer(serializers.Serializer):
|
||||||
message = serializers.CharField(default='happy')
|
message = serializers.CharField(default='happy')
|
||||||
|
|
||||||
|
@ -232,23 +232,31 @@ class TestCharHTMLInput:
|
||||||
assert serializer.is_valid()
|
assert serializer.is_valid()
|
||||||
assert serializer.validated_data == {'message': 'happy'}
|
assert serializer.validated_data == {'message': 'happy'}
|
||||||
|
|
||||||
def test_empty_html_checkbox_allow_null(self):
|
def test_empty_html_charfield_allow_null(self):
|
||||||
class TestSerializer(serializers.Serializer):
|
class TestSerializer(serializers.Serializer):
|
||||||
message = serializers.CharField(allow_null=True)
|
message = serializers.CharField(allow_null=True)
|
||||||
|
|
||||||
serializer = TestSerializer(data=MockHTMLDict())
|
serializer = TestSerializer(data=MockHTMLDict({'message': ''}))
|
||||||
assert serializer.is_valid()
|
assert serializer.is_valid()
|
||||||
assert serializer.validated_data == {'message': None}
|
assert serializer.validated_data == {'message': None}
|
||||||
|
|
||||||
def test_empty_html_checkbox_allow_null_allow_blank(self):
|
def test_empty_html_datefield_allow_null(self):
|
||||||
|
class TestSerializer(serializers.Serializer):
|
||||||
|
expiry = serializers.DateField(allow_null=True)
|
||||||
|
|
||||||
|
serializer = TestSerializer(data=MockHTMLDict({'expiry': ''}))
|
||||||
|
assert serializer.is_valid()
|
||||||
|
assert serializer.validated_data == {'expiry': None}
|
||||||
|
|
||||||
|
def test_empty_html_charfield_allow_null_allow_blank(self):
|
||||||
class TestSerializer(serializers.Serializer):
|
class TestSerializer(serializers.Serializer):
|
||||||
message = serializers.CharField(allow_null=True, allow_blank=True)
|
message = serializers.CharField(allow_null=True, allow_blank=True)
|
||||||
|
|
||||||
serializer = TestSerializer(data=MockHTMLDict({}))
|
serializer = TestSerializer(data=MockHTMLDict({'message': ''}))
|
||||||
assert serializer.is_valid()
|
assert serializer.is_valid()
|
||||||
assert serializer.validated_data == {'message': ''}
|
assert serializer.validated_data == {'message': ''}
|
||||||
|
|
||||||
def test_empty_html_required_false(self):
|
def test_empty_html_charfield_required_false(self):
|
||||||
class TestSerializer(serializers.Serializer):
|
class TestSerializer(serializers.Serializer):
|
||||||
message = serializers.CharField(required=False)
|
message = serializers.CharField(required=False)
|
||||||
|
|
||||||
|
|
|
@ -119,7 +119,7 @@ class TestRegularFieldMappings(TestCase):
|
||||||
positive_small_integer_field = IntegerField()
|
positive_small_integer_field = IntegerField()
|
||||||
slug_field = SlugField(max_length=100)
|
slug_field = SlugField(max_length=100)
|
||||||
small_integer_field = IntegerField()
|
small_integer_field = IntegerField()
|
||||||
text_field = CharField(style={'type': 'textarea'})
|
text_field = CharField(style={'base_template': 'textarea.html'})
|
||||||
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,17 +1,53 @@
|
||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
from django.conf.urls import patterns, url, include
|
from django.conf.urls import url, include
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.test import TestCase
|
from django.test import TestCase
|
||||||
from django.core.exceptions import ImproperlyConfigured
|
from django.core.exceptions import ImproperlyConfigured
|
||||||
from rest_framework import serializers, viewsets, mixins, permissions
|
from rest_framework import serializers, viewsets, permissions
|
||||||
from rest_framework.decorators import detail_route, list_route
|
from rest_framework.decorators import detail_route, list_route
|
||||||
from rest_framework.response import Response
|
from rest_framework.response import Response
|
||||||
from rest_framework.routers import SimpleRouter, DefaultRouter
|
from rest_framework.routers import SimpleRouter, DefaultRouter
|
||||||
from rest_framework.test import APIRequestFactory
|
from rest_framework.test import APIRequestFactory
|
||||||
|
from collections import namedtuple
|
||||||
|
|
||||||
factory = APIRequestFactory()
|
factory = APIRequestFactory()
|
||||||
|
|
||||||
urlpatterns = patterns('',)
|
|
||||||
|
class RouterTestModel(models.Model):
|
||||||
|
uuid = models.CharField(max_length=20)
|
||||||
|
text = models.CharField(max_length=200)
|
||||||
|
|
||||||
|
|
||||||
|
class NoteSerializer(serializers.HyperlinkedModelSerializer):
|
||||||
|
url = serializers.HyperlinkedIdentityField(view_name='routertestmodel-detail', lookup_field='uuid')
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = RouterTestModel
|
||||||
|
fields = ('url', 'uuid', 'text')
|
||||||
|
|
||||||
|
|
||||||
|
class NoteViewSet(viewsets.ModelViewSet):
|
||||||
|
queryset = RouterTestModel.objects.all()
|
||||||
|
serializer_class = NoteSerializer
|
||||||
|
lookup_field = 'uuid'
|
||||||
|
|
||||||
|
|
||||||
|
class MockViewSet(viewsets.ModelViewSet):
|
||||||
|
queryset = None
|
||||||
|
serializer_class = None
|
||||||
|
|
||||||
|
|
||||||
|
notes_router = SimpleRouter()
|
||||||
|
notes_router.register(r'notes', NoteViewSet)
|
||||||
|
|
||||||
|
namespaced_router = DefaultRouter()
|
||||||
|
namespaced_router.register(r'example', MockViewSet, base_name='example')
|
||||||
|
|
||||||
|
urlpatterns = [
|
||||||
|
url(r'^non-namespaced/', include(namespaced_router.urls)),
|
||||||
|
url(r'^namespaced/', include(namespaced_router.urls, namespace='example')),
|
||||||
|
url(r'^example/', include(notes_router.urls)),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
class BasicViewSet(viewsets.ViewSet):
|
class BasicViewSet(viewsets.ViewSet):
|
||||||
|
@ -63,9 +99,26 @@ class TestSimpleRouter(TestCase):
|
||||||
self.assertEqual(route.mapping[method], endpoint)
|
self.assertEqual(route.mapping[method], endpoint)
|
||||||
|
|
||||||
|
|
||||||
class RouterTestModel(models.Model):
|
class TestRootView(TestCase):
|
||||||
uuid = models.CharField(max_length=20)
|
urls = 'tests.test_routers'
|
||||||
text = models.CharField(max_length=200)
|
|
||||||
|
def test_retrieve_namespaced_root(self):
|
||||||
|
response = self.client.get('/namespaced/')
|
||||||
|
self.assertEqual(
|
||||||
|
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/",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class TestCustomLookupFields(TestCase):
|
class TestCustomLookupFields(TestCase):
|
||||||
|
@ -75,51 +128,29 @@ class TestCustomLookupFields(TestCase):
|
||||||
urls = 'tests.test_routers'
|
urls = 'tests.test_routers'
|
||||||
|
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
class NoteSerializer(serializers.HyperlinkedModelSerializer):
|
|
||||||
url = serializers.HyperlinkedIdentityField(view_name='routertestmodel-detail', lookup_field='uuid')
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
model = RouterTestModel
|
|
||||||
fields = ('url', 'uuid', 'text')
|
|
||||||
|
|
||||||
class NoteViewSet(viewsets.ModelViewSet):
|
|
||||||
queryset = RouterTestModel.objects.all()
|
|
||||||
serializer_class = NoteSerializer
|
|
||||||
lookup_field = 'uuid'
|
|
||||||
|
|
||||||
self.router = SimpleRouter()
|
|
||||||
self.router.register(r'notes', NoteViewSet)
|
|
||||||
|
|
||||||
from tests import test_routers
|
|
||||||
urls = getattr(test_routers, 'urlpatterns')
|
|
||||||
urls += patterns(
|
|
||||||
'',
|
|
||||||
url(r'^', include(self.router.urls)),
|
|
||||||
)
|
|
||||||
|
|
||||||
RouterTestModel.objects.create(uuid='123', text='foo bar')
|
RouterTestModel.objects.create(uuid='123', text='foo bar')
|
||||||
|
|
||||||
def test_custom_lookup_field_route(self):
|
def test_custom_lookup_field_route(self):
|
||||||
detail_route = self.router.urls[-1]
|
detail_route = notes_router.urls[-1]
|
||||||
detail_url_pattern = detail_route.regex.pattern
|
detail_url_pattern = detail_route.regex.pattern
|
||||||
self.assertIn('<uuid>', detail_url_pattern)
|
self.assertIn('<uuid>', detail_url_pattern)
|
||||||
|
|
||||||
def test_retrieve_lookup_field_list_view(self):
|
def test_retrieve_lookup_field_list_view(self):
|
||||||
response = self.client.get('/notes/')
|
response = self.client.get('/example/notes/')
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
response.data,
|
response.data,
|
||||||
[{
|
[{
|
||||||
"url": "http://testserver/notes/123/",
|
"url": "http://testserver/example/notes/123/",
|
||||||
"uuid": "123", "text": "foo bar"
|
"uuid": "123", "text": "foo bar"
|
||||||
}]
|
}]
|
||||||
)
|
)
|
||||||
|
|
||||||
def test_retrieve_lookup_field_detail_view(self):
|
def test_retrieve_lookup_field_detail_view(self):
|
||||||
response = self.client.get('/notes/123/')
|
response = self.client.get('/example/notes/123/')
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
response.data,
|
response.data,
|
||||||
{
|
{
|
||||||
"url": "http://testserver/notes/123/",
|
"url": "http://testserver/example/notes/123/",
|
||||||
"uuid": "123", "text": "foo bar"
|
"uuid": "123", "text": "foo bar"
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
@ -261,6 +292,14 @@ class DynamicListAndDetailViewSet(viewsets.ViewSet):
|
||||||
def detail_route_get(self, request, *args, **kwargs):
|
def detail_route_get(self, request, *args, **kwargs):
|
||||||
return Response({'method': 'link2'})
|
return Response({'method': 'link2'})
|
||||||
|
|
||||||
|
@list_route(url_path="list_custom-route")
|
||||||
|
def list_custom_route_get(self, request, *args, **kwargs):
|
||||||
|
return Response({'method': 'link1'})
|
||||||
|
|
||||||
|
@detail_route(url_path="detail_custom-route")
|
||||||
|
def detail_custom_route_get(self, request, *args, **kwargs):
|
||||||
|
return Response({'method': 'link2'})
|
||||||
|
|
||||||
|
|
||||||
class TestDynamicListAndDetailRouter(TestCase):
|
class TestDynamicListAndDetailRouter(TestCase):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
|
@ -269,35 +308,30 @@ class TestDynamicListAndDetailRouter(TestCase):
|
||||||
def test_list_and_detail_route_decorators(self):
|
def test_list_and_detail_route_decorators(self):
|
||||||
routes = self.router.get_routes(DynamicListAndDetailViewSet)
|
routes = self.router.get_routes(DynamicListAndDetailViewSet)
|
||||||
decorator_routes = [r for r in routes if not (r.name.endswith('-list') or r.name.endswith('-detail'))]
|
decorator_routes = [r for r in routes if not (r.name.endswith('-list') or r.name.endswith('-detail'))]
|
||||||
|
|
||||||
|
MethodNamesMap = namedtuple('MethodNamesMap', 'method_name url_path')
|
||||||
# Make sure all these endpoints exist and none have been clobbered
|
# Make sure all these endpoints exist and none have been clobbered
|
||||||
for i, endpoint in enumerate(['list_route_get', 'list_route_post', 'detail_route_get', 'detail_route_post']):
|
for i, endpoint in enumerate([MethodNamesMap('list_custom_route_get', 'list_custom-route'),
|
||||||
|
MethodNamesMap('list_route_get', 'list_route_get'),
|
||||||
|
MethodNamesMap('list_route_post', 'list_route_post'),
|
||||||
|
MethodNamesMap('detail_custom_route_get', 'detail_custom-route'),
|
||||||
|
MethodNamesMap('detail_route_get', 'detail_route_get'),
|
||||||
|
MethodNamesMap('detail_route_post', 'detail_route_post')
|
||||||
|
]):
|
||||||
route = decorator_routes[i]
|
route = decorator_routes[i]
|
||||||
# check url listing
|
# check url listing
|
||||||
if endpoint.startswith('list_'):
|
method_name = endpoint.method_name
|
||||||
|
url_path = endpoint.url_path
|
||||||
|
|
||||||
|
if method_name.startswith('list_'):
|
||||||
self.assertEqual(route.url,
|
self.assertEqual(route.url,
|
||||||
'^{{prefix}}/{0}{{trailing_slash}}$'.format(endpoint))
|
'^{{prefix}}/{0}{{trailing_slash}}$'.format(url_path))
|
||||||
else:
|
else:
|
||||||
self.assertEqual(route.url,
|
self.assertEqual(route.url,
|
||||||
'^{{prefix}}/{{lookup}}/{0}{{trailing_slash}}$'.format(endpoint))
|
'^{{prefix}}/{{lookup}}/{0}{{trailing_slash}}$'.format(url_path))
|
||||||
# check method to function mapping
|
# check method to function mapping
|
||||||
if endpoint.endswith('_post'):
|
if method_name.endswith('_post'):
|
||||||
method_map = 'post'
|
method_map = 'post'
|
||||||
else:
|
else:
|
||||||
method_map = 'get'
|
method_map = 'get'
|
||||||
self.assertEqual(route.mapping[method_map], endpoint)
|
self.assertEqual(route.mapping[method_map], method_name)
|
||||||
|
|
||||||
|
|
||||||
class TestRootWithAListlessViewset(TestCase):
|
|
||||||
def setUp(self):
|
|
||||||
class NoteViewSet(mixins.RetrieveModelMixin,
|
|
||||||
viewsets.GenericViewSet):
|
|
||||||
model = RouterTestModel
|
|
||||||
|
|
||||||
self.router = DefaultRouter()
|
|
||||||
self.router.register(r'notes', NoteViewSet)
|
|
||||||
self.view = self.router.urls[0].callback
|
|
||||||
|
|
||||||
def test_api_root(self):
|
|
||||||
request = factory.get('/')
|
|
||||||
response = self.view(request)
|
|
||||||
self.assertEqual(response.data, {})
|
|
||||||
|
|
Loading…
Reference in New Issue
Block a user