mirror of
https://github.com/encode/django-rest-framework.git
synced 2025-08-05 04:50:12 +03:00
Merge branch 'master' into document-how-to-trans
This commit is contained in:
commit
21ab6ff3ea
|
@ -156,7 +156,7 @@ Send a description of the issue via email to [rest-framework-security@googlegrou
|
|||
|
||||
# License
|
||||
|
||||
Copyright (c) 2011-2014, Tom Christie
|
||||
Copyright (c) 2011-2015, Tom Christie
|
||||
All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
|
|
|
@ -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.
|
||||
|
||||
### 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
|
||||
|
|
|
@ -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 can be imported from `rest_framework.mixins`.
|
||||
|
||||
## ListModelMixin
|
||||
|
||||
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 view classes can be imported from `rest_framework.generics`.
|
||||
|
||||
## CreateAPIView
|
||||
|
||||
Used for **create-only** endpoints.
|
||||
|
|
|
@ -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
|
||||
|
||||
Any methods on the viewset decorated with `@detail_route` or `@list_route` will also be routed.
|
||||
|
|
|
@ -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']
|
||||
)
|
||||
|
||||
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
|
||||
|
||||
|
|
|
@ -235,7 +235,7 @@ Send a description of the issue via email to [rest-framework-security@googlegrou
|
|||
|
||||
## License
|
||||
|
||||
Copyright (c) 2011-2014, Tom Christie
|
||||
Copyright (c) 2011-2015, Tom Christie
|
||||
All rights reserved.
|
||||
|
||||
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.
|
||||
* 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.
|
||||
* `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.
|
||||
|
||||
---
|
||||
|
||||
|
|
|
@ -11,7 +11,7 @@ __title__ = 'Django REST framework'
|
|||
__version__ = '3.0.2'
|
||||
__author__ = 'Tom Christie'
|
||||
__license__ = 'BSD 2-Clause'
|
||||
__copyright__ = 'Copyright 2011-2014 Tom Christie'
|
||||
__copyright__ = 'Copyright 2011-2015 Tom Christie'
|
||||
|
||||
# Version synonym
|
||||
VERSION = __version__
|
||||
|
|
|
@ -50,6 +50,16 @@ except ImportError:
|
|||
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
|
||||
try:
|
||||
import django_filters
|
||||
|
|
|
@ -77,6 +77,7 @@ class BasePaginationSerializer(serializers.Serializer):
|
|||
child=object_serializer(),
|
||||
source='object_list'
|
||||
)
|
||||
self.fields[results_field].bind(field_name=results_field, parent=self)
|
||||
|
||||
|
||||
class PaginationSerializer(BasePaginationSerializer):
|
||||
|
|
|
@ -21,7 +21,7 @@ from django.conf.urls import patterns, url
|
|||
from django.core.exceptions import ImproperlyConfigured
|
||||
from django.core.urlresolvers import NoReverseMatch
|
||||
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.reverse import reverse
|
||||
from rest_framework.urlpatterns import format_suffix_patterns
|
||||
|
@ -292,7 +292,10 @@ class DefaultRouter(SimpleRouter):
|
|||
|
||||
def get(self, request, *args, **kwargs):
|
||||
ret = OrderedDict()
|
||||
namespace = get_resolver_match(request).namespace
|
||||
for key, url_name in api_root_dict.items():
|
||||
if namespace:
|
||||
url_name = namespace + ':' + url_name
|
||||
try:
|
||||
ret[key] = reverse(
|
||||
url_name,
|
||||
|
|
|
@ -106,7 +106,7 @@ def get_field_kwargs(field_name, model_field):
|
|||
# Ensure that max_length is passed explicitly as a keyword arg,
|
||||
# rather than as a validator.
|
||||
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
|
||||
validator_kwarg = [
|
||||
validator for validator in validator_kwarg
|
||||
|
|
|
@ -35,7 +35,7 @@ def _resolve_model(obj):
|
|||
Resolve supplied `obj` to a Django model class.
|
||||
|
||||
`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
|
||||
another model's foreign key definition.
|
||||
|
||||
|
@ -56,23 +56,44 @@ def _resolve_model(obj):
|
|||
|
||||
def get_field_info(model):
|
||||
"""
|
||||
Given a model class, returns a `FieldInfo` instance containing metadata
|
||||
about the various field types on the model.
|
||||
Given a model class, returns a `FieldInfo` instance, which is a
|
||||
`namedtuple`, containing metadata about the various field types on the model
|
||||
including information about their relationships.
|
||||
"""
|
||||
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
|
||||
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
|
||||
|
||||
# Deal with regular fields.
|
||||
return pk
|
||||
|
||||
|
||||
def _get_fields(opts):
|
||||
fields = OrderedDict()
|
||||
for field in [field for field in opts.fields if field.serialize and not field.rel]:
|
||||
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()
|
||||
for field in [field for field in opts.fields if field.serialize and field.rel]:
|
||||
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()
|
||||
for relation in opts.get_all_related_objects():
|
||||
accessor_name = relation.get_accessor_name()
|
||||
|
@ -117,18 +144,20 @@ def get_field_info(model):
|
|||
)
|
||||
)
|
||||
|
||||
# Shortcut that merges both regular fields and the pk,
|
||||
# for simplifying regular field lookup.
|
||||
return reverse_relations
|
||||
|
||||
|
||||
def _merge_fields_and_pk(pk, fields):
|
||||
fields_and_pk = OrderedDict()
|
||||
fields_and_pk['pk'] = pk
|
||||
fields_and_pk[pk.name] = pk
|
||||
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(reverse_relations.items())
|
||||
)
|
||||
|
||||
return FieldInfo(pk, fields, forward_relations, reverse_relations, fields_and_pk, relations)
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
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.test import TestCase
|
||||
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.response import Response
|
||||
from rest_framework.routers import SimpleRouter, DefaultRouter
|
||||
|
@ -12,7 +12,42 @@ from collections import namedtuple
|
|||
|
||||
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):
|
||||
|
@ -64,9 +99,26 @@ class TestSimpleRouter(TestCase):
|
|||
self.assertEqual(route.mapping[method], endpoint)
|
||||
|
||||
|
||||
class RouterTestModel(models.Model):
|
||||
uuid = models.CharField(max_length=20)
|
||||
text = models.CharField(max_length=200)
|
||||
class TestRootView(TestCase):
|
||||
urls = 'tests.test_routers'
|
||||
|
||||
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):
|
||||
|
@ -76,51 +128,29 @@ class TestCustomLookupFields(TestCase):
|
|||
urls = 'tests.test_routers'
|
||||
|
||||
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')
|
||||
|
||||
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
|
||||
self.assertIn('<uuid>', detail_url_pattern)
|
||||
|
||||
def test_retrieve_lookup_field_list_view(self):
|
||||
response = self.client.get('/notes/')
|
||||
response = self.client.get('/example/notes/')
|
||||
self.assertEqual(
|
||||
response.data,
|
||||
[{
|
||||
"url": "http://testserver/notes/123/",
|
||||
"url": "http://testserver/example/notes/123/",
|
||||
"uuid": "123", "text": "foo bar"
|
||||
}]
|
||||
)
|
||||
|
||||
def test_retrieve_lookup_field_detail_view(self):
|
||||
response = self.client.get('/notes/123/')
|
||||
response = self.client.get('/example/notes/123/')
|
||||
self.assertEqual(
|
||||
response.data,
|
||||
{
|
||||
"url": "http://testserver/notes/123/",
|
||||
"url": "http://testserver/example/notes/123/",
|
||||
"uuid": "123", "text": "foo bar"
|
||||
}
|
||||
)
|
||||
|
@ -305,19 +335,3 @@ class TestDynamicListAndDetailRouter(TestCase):
|
|||
else:
|
||||
method_map = 'get'
|
||||
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