Merge branch 'master' into document-how-to-trans

This commit is contained in:
Craig Blaszczyk 2014-12-31 12:51:39 +00:00
commit 21ab6ff3ea
14 changed files with 175 additions and 69 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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):

View File

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

View File

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

View File

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

View File

@ -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, {})