Added SCHEMA_COERCE_PATH_PK and SCHEMA_COERCE_METHOD_NAMES

This commit is contained in:
Tom Christie 2016-10-10 11:48:24 +01:00
parent fcf932f118
commit 18aebbbe01
11 changed files with 102 additions and 55 deletions

View File

@ -234,6 +234,28 @@ Default:
---
## Schema generation controls
#### SCHEMA_COERCE_PATH_PK
If set, this maps the `'pk'` identifier in the URL conf onto the actual field
name when generating a schema path parameter. Typically this will be `'id'`.
This gives a more suitable representation as "primary key" is an implementation
detail, wheras "identifier" is a more general concept.
Default: `True`
#### SCHEMA_COERCE_METHOD_NAMES
If set, this is used to map internal viewset method names onto external action
names used in the schema generation. This allows us to generate names that
are more suitable for an external representation than those that are used
internally in the codebase.
Default: `{'retrieve': 'read', 'destroy': 'delete'}`
---
## Content type controls
#### URL_FORMAT_OVERRIDE

View File

@ -88,7 +88,7 @@ The first thing we need to get started on our Web API is to provide a way of ser
class SnippetSerializer(serializers.Serializer):
pk = serializers.IntegerField(read_only=True)
id = serializers.IntegerField(read_only=True)
title = serializers.CharField(required=False, allow_blank=True, max_length=100)
code = serializers.CharField(style={'base_template': 'textarea.html'})
linenos = serializers.BooleanField(required=False)
@ -144,13 +144,13 @@ We've now got a few snippet instances to play with. Let's take a look at serial
serializer = SnippetSerializer(snippet)
serializer.data
# {'pk': 2, 'title': u'', 'code': u'print "hello, world"\n', 'linenos': False, 'language': u'python', 'style': u'friendly'}
# {'id': 2, 'title': u'', 'code': u'print "hello, world"\n', 'linenos': False, 'language': u'python', 'style': u'friendly'}
At this point we've translated the model instance into Python native datatypes. To finalize the serialization process we render the data into `json`.
content = JSONRenderer().render(serializer.data)
content
# '{"pk": 2, "title": "", "code": "print \\"hello, world\\"\\n", "linenos": false, "language": "python", "style": "friendly"}'
# '{"id": 2, "title": "", "code": "print \\"hello, world\\"\\n", "linenos": false, "language": "python", "style": "friendly"}'
Deserialization is similar. First we parse a stream into Python native datatypes...
@ -175,7 +175,7 @@ We can also serialize querysets instead of model instances. To do so we simply
serializer = SnippetSerializer(Snippet.objects.all(), many=True)
serializer.data
# [OrderedDict([('pk', 1), ('title', u''), ('code', u'foo = "bar"\n'), ('linenos', False), ('language', 'python'), ('style', 'friendly')]), OrderedDict([('pk', 2), ('title', u''), ('code', u'print "hello, world"\n'), ('linenos', False), ('language', 'python'), ('style', 'friendly')]), OrderedDict([('pk', 3), ('title', u''), ('code', u'print "hello, world"'), ('linenos', False), ('language', 'python'), ('style', 'friendly')])]
# [OrderedDict([('id', 1), ('title', u''), ('code', u'foo = "bar"\n'), ('linenos', False), ('language', 'python'), ('style', 'friendly')]), OrderedDict([('id', 2), ('title', u''), ('code', u'print "hello, world"\n'), ('linenos', False), ('language', 'python'), ('style', 'friendly')]), OrderedDict([('id', 3), ('title', u''), ('code', u'print "hello, world"'), ('linenos', False), ('language', 'python'), ('style', 'friendly')])]
## Using ModelSerializers
@ -259,12 +259,12 @@ Note that because we want to be able to POST to this view from clients that won'
We'll also need a view which corresponds to an individual snippet, and can be used to retrieve, update or delete the snippet.
@csrf_exempt
def snippet_detail(request, pk):
def snippet_detail(request, id):
"""
Retrieve, update or delete a code snippet.
"""
try:
snippet = Snippet.objects.get(pk=pk)
snippet = Snippet.objects.get(id=id)
except Snippet.DoesNotExist:
return HttpResponse(status=404)
@ -291,7 +291,7 @@ Finally we need to wire these views up. Create the `snippets/urls.py` file:
urlpatterns = [
url(r'^snippets/$', views.snippet_list),
url(r'^snippets/(?P<pk>[0-9]+)/$', views.snippet_detail),
url(r'^snippets/(?P<id>[0-9]+)/$', views.snippet_detail),
]
We also need to wire up the root urlconf, in the `tutorial/urls.py` file, to include our snippet app's URLs.

View File

@ -66,12 +66,12 @@ Our instance view is an improvement over the previous example. It's a little mo
Here is the view for an individual snippet, in the `views.py` module.
@api_view(['GET', 'PUT', 'DELETE'])
def snippet_detail(request, pk):
def snippet_detail(request, id):
"""
Retrieve, update or delete a snippet instance.
"""
try:
snippet = Snippet.objects.get(pk=pk)
snippet = Snippet.objects.get(id=id)
except Snippet.DoesNotExist:
return Response(status=status.HTTP_404_NOT_FOUND)
@ -104,7 +104,7 @@ Start by adding a `format` keyword argument to both of the views, like so.
and
def snippet_detail(request, pk, format=None):
def snippet_detail(request, id, format=None):
Now update the `urls.py` file slightly, to append a set of `format_suffix_patterns` in addition to the existing URLs.
@ -114,7 +114,7 @@ Now update the `urls.py` file slightly, to append a set of `format_suffix_patter
urlpatterns = [
url(r'^snippets/$', views.snippet_list),
url(r'^snippets/(?P<pk>[0-9]+)$', views.snippet_detail),
url(r'^snippets/(?P<id>[0-9]+)$', views.snippet_detail),
]
urlpatterns = format_suffix_patterns(urlpatterns)

View File

@ -36,27 +36,27 @@ So far, so good. It looks pretty similar to the previous case, but we've got be
"""
Retrieve, update or delete a snippet instance.
"""
def get_object(self, pk):
def get_object(self, id):
try:
return Snippet.objects.get(pk=pk)
return Snippet.objects.get(id=id)
except Snippet.DoesNotExist:
raise Http404
def get(self, request, pk, format=None):
snippet = self.get_object(pk)
def get(self, request, id, format=None):
snippet = self.get_object(id)
serializer = SnippetSerializer(snippet)
return Response(serializer.data)
def put(self, request, pk, format=None):
snippet = self.get_object(pk)
def put(self, request, id, format=None):
snippet = self.get_object(id)
serializer = SnippetSerializer(snippet, data=request.data)
if serializer.is_valid():
serializer.save()
return Response(serializer.data)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
def delete(self, request, pk, format=None):
snippet = self.get_object(pk)
def delete(self, request, id, format=None):
snippet = self.get_object(id)
snippet.delete()
return Response(status=status.HTTP_204_NO_CONTENT)
@ -70,7 +70,7 @@ We'll also need to refactor our `urls.py` slightly now we're using class-based v
urlpatterns = [
url(r'^snippets/$', views.SnippetList.as_view()),
url(r'^snippets/(?P<pk>[0-9]+)/$', views.SnippetDetail.as_view()),
url(r'^snippets/(?P<id>[0-9]+)/$', views.SnippetDetail.as_view()),
]
urlpatterns = format_suffix_patterns(urlpatterns)

View File

@ -88,7 +88,7 @@ Make sure to also import the `UserSerializer` class
Finally we need to add those views into the API, by referencing them from the URL conf. Add the following to the patterns in `urls.py`.
url(r'^users/$', views.UserList.as_view()),
url(r'^users/(?P<pk>[0-9]+)/$', views.UserDetail.as_view()),
url(r'^users/(?P<id>[0-9]+)/$', views.UserDetail.as_view()),
## Associating Snippets with Users
@ -150,7 +150,7 @@ The `r'^api-auth/'` part of pattern can actually be whatever URL you want to use
Now if you open up the browser again and refresh the page you'll see a 'Login' link in the top right of the page. If you log in as one of the users you created earlier, you'll be able to create code snippets again.
Once you've created a few code snippets, navigate to the '/users/' endpoint, and notice that the representation includes a list of the snippet pks that are associated with each user, in each user's 'snippets' field.
Once you've created a few code snippets, navigate to the '/users/' endpoint, and notice that the representation includes a list of the snippet ids that are associated with each user, in each user's 'snippets' field.
## Object level permissions

View File

@ -48,7 +48,7 @@ We'll add a url pattern for our new API root in `snippets/urls.py`:
And then add a url pattern for the snippet highlights:
url(r'^snippets/(?P<pk>[0-9]+)/highlight/$', views.SnippetHighlight.as_view()),
url(r'^snippets/(?P<id>[0-9]+)/highlight/$', views.SnippetHighlight.as_view()),
## Hyperlinking our API
@ -67,7 +67,7 @@ In this case we'd like to use a hyperlinked style between entities. In order to
The `HyperlinkedModelSerializer` has the following differences from `ModelSerializer`:
* It does not include the `pk` field by default.
* It does not include the `id` field by default.
* It includes a `url` field, using `HyperlinkedIdentityField`.
* Relationships use `HyperlinkedRelatedField`,
instead of `PrimaryKeyRelatedField`.
@ -80,7 +80,7 @@ We can easily re-write our existing serializers to use hyperlinking. In your `sn
class Meta:
model = Snippet
fields = ('url', 'pk', 'highlight', 'owner',
fields = ('url', 'id', 'highlight', 'owner',
'title', 'code', 'linenos', 'language', 'style')
@ -89,7 +89,7 @@ We can easily re-write our existing serializers to use hyperlinking. In your `sn
class Meta:
model = User
fields = ('url', 'pk', 'username', 'snippets')
fields = ('url', 'id', 'username', 'snippets')
Notice that we've also added a new `'highlight'` field. This field is of the same type as the `url` field, except that it points to the `'snippet-highlight'` url pattern, instead of the `'snippet-detail'` url pattern.
@ -116,16 +116,16 @@ After adding all those names into our URLconf, our final `snippets/urls.py` file
url(r'^snippets/$',
views.SnippetList.as_view(),
name='snippet-list'),
url(r'^snippets/(?P<pk>[0-9]+)/$',
url(r'^snippets/(?P<id>[0-9]+)/$',
views.SnippetDetail.as_view(),
name='snippet-detail'),
url(r'^snippets/(?P<pk>[0-9]+)/highlight/$',
url(r'^snippets/(?P<id>[0-9]+)/highlight/$',
views.SnippetHighlight.as_view(),
name='snippet-highlight'),
url(r'^users/$',
views.UserList.as_view(),
name='user-list'),
url(r'^users/(?P<pk>[0-9]+)/$',
url(r'^users/(?P<id>[0-9]+)/$',
views.UserDetail.as_view(),
name='user-detail')
])

View File

@ -92,10 +92,10 @@ Now that we've bound our resources into concrete views, we can register the view
urlpatterns = format_suffix_patterns([
url(r'^$', api_root),
url(r'^snippets/$', snippet_list, name='snippet-list'),
url(r'^snippets/(?P<pk>[0-9]+)/$', snippet_detail, name='snippet-detail'),
url(r'^snippets/(?P<pk>[0-9]+)/highlight/$', snippet_highlight, name='snippet-highlight'),
url(r'^snippets/(?P<id>[0-9]+)/$', snippet_detail, name='snippet-detail'),
url(r'^snippets/(?P<id>[0-9]+)/highlight/$', snippet_highlight, name='snippet-highlight'),
url(r'^users/$', user_list, name='user-list'),
url(r'^users/(?P<pk>[0-9]+)/$', user_detail, name='user-detail')
url(r'^users/(?P<id>[0-9]+)/$', user_detail, name='user-detail')
])
## Using Routers

View File

@ -101,13 +101,13 @@ First we'll load the API schema using the command line client.
$ coreapi get http://127.0.0.1:8000/schema/
<Pastebin API "http://127.0.0.1:8000/schema/">
snippets: {
highlight(pk)
highlight(id)
list()
retrieve(pk)
read(id)
}
users: {
list()
retrieve(pk)
read(id)
}
We haven't authenticated yet, so right now we're only able to see the read only
@ -119,7 +119,7 @@ Let's try listing the existing snippets, using the command line client:
[
{
"url": "http://127.0.0.1:8000/snippets/1/",
"pk": 1,
"id": 1,
"highlight": "http://127.0.0.1:8000/snippets/1/highlight/",
"owner": "lucy",
"title": "Example",
@ -133,7 +133,7 @@ Let's try listing the existing snippets, using the command line client:
Some of the API endpoints require named parameters. For example, to get back
the highlight HTML for a particular snippet we need to provide an id.
$ coreapi action snippets highlight --param pk=1
$ coreapi action snippets highlight --param id=1
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">
<html>
@ -160,16 +160,16 @@ set of available interactions.
Pastebin API "http://127.0.0.1:8000/schema/">
snippets: {
create(code, [title], [linenos], [language], [style])
destroy(pk)
highlight(pk)
delete(id)
highlight(id)
list()
partial_update(pk, [title], [code], [linenos], [language], [style])
retrieve(pk)
update(pk, code, [title], [linenos], [language], [style])
partial_update(id, [title], [code], [linenos], [language], [style])
read(id)
update(id, code, [title], [linenos], [language], [style])
}
users: {
list()
retrieve(pk)
read(id)
}
We're now able to interact with these endpoints. For example, to create a new
@ -178,7 +178,7 @@ snippet:
$ coreapi action snippets create --param title="Example" --param code="print('hello, world')"
{
"url": "http://127.0.0.1:8000/snippets/7/",
"pk": 7,
"id": 7,
"highlight": "http://127.0.0.1:8000/snippets/7/highlight/",
"owner": "lucy",
"title": "Example",
@ -190,7 +190,7 @@ snippet:
And to delete a snippet:
$ coreapi action snippets destroy --param pk=7
$ coreapi action snippets delete --param id=7
As well as the command line client, developers can also interact with your
API using client libraries. The Python client library is the first of these

View File

@ -194,7 +194,7 @@ class EndpointInspector(object):
class SchemaGenerator(object):
# Map methods onto 'actions' that are the names used in the link layout.
# Map HTTP methods onto actions.
default_mapping = {
'get': 'retrieve',
'post': 'create',
@ -203,9 +203,16 @@ class SchemaGenerator(object):
'delete': 'destroy',
}
endpoint_inspector_cls = EndpointInspector
# Map the method names we use for viewset actions onto external schema names.
# These give us names that are more suitable for the external representation.
# Set by 'SCHEMA_COERCE_METHOD_NAMES'.
coerce_method_names = None
# 'pk' isn't great as an externally exposed name for an identifier,
# so by default we prefer to use the actual model field name for schemas.
coerce_pk = True
# Set by 'SCHEMA_COERCE_PATH_PK'.
coerce_path_pk = None
def __init__(self, title=None, url=None, patterns=None, urlconf=None):
assert coreapi, '`coreapi` must be installed for schema support.'
@ -213,6 +220,9 @@ class SchemaGenerator(object):
if url and not url.endswith('/'):
url += '/'
self.coerce_method_names = api_settings.SCHEMA_COERCE_METHOD_NAMES
self.coerce_path_pk = api_settings.SCHEMA_COERCE_PATH_PK
self.patterns = patterns
self.urlconf = urlconf
self.title = title
@ -339,7 +349,7 @@ class SchemaGenerator(object):
where possible. This is cleaner for an external representation.
(Ie. "this is an identifier", not "this is a database primary key")
"""
if not self.coerce_pk or '{pk}' not in path:
if not self.coerce_path_pk or '{pk}' not in path:
return path
model = getattr(getattr(view, 'queryset', None), 'model', None)
if model:
@ -405,6 +415,9 @@ class SchemaGenerator(object):
header = getattr(view, 'action', method.lower())
if header in sections:
return sections[header].strip()
if header in self.coerce_method_names:
if self.coerce_method_names[header] in sections:
return sections[self.coerce_method_names[header]].strip()
return sections[''].strip()
def get_encoding(self, path, method, view):
@ -541,10 +554,15 @@ class SchemaGenerator(object):
# Custom action, eg "/users/{pk}/activate/", "/users/active/"
if len(view.action_map) > 1:
action = self.default_mapping[method.lower()]
if action in self.coerce_method_names:
action = self.coerce_method_names[action]
return named_path_components + [action]
else:
return named_path_components[:-1] + [action]
if action in self.coerce_method_names:
action = self.coerce_method_names[action]
# Default action, eg "/users/", "/users/{pk}/"
return named_path_components + [action]

View File

@ -115,6 +115,13 @@ DEFAULTS = {
# Browseable API
'HTML_SELECT_CUTOFF': 1000,
'HTML_SELECT_CUTOFF_TEXT': "More than {count} items...",
# Schemas
'SCHEMA_COERCE_PATH_PK': True,
'SCHEMA_COERCE_METHOD_NAMES': {
'retrieve': 'read',
'destroy': 'delete'
},
}

View File

@ -104,12 +104,12 @@ class TestRouterGeneratedSchema(TestCase):
action='get'
),
'custom_list_action_multiple_methods': {
'retrieve': coreapi.Link(
'read': coreapi.Link(
url='/example/custom_list_action_multiple_methods/',
action='get'
)
},
'retrieve': coreapi.Link(
'read': coreapi.Link(
url='/example/{id}/',
action='get',
fields=[
@ -148,7 +148,7 @@ class TestRouterGeneratedSchema(TestCase):
coreapi.Field('b', required=False, location='form', type='string')
]
),
'retrieve': coreapi.Link(
'read': coreapi.Link(
url='/example/{id}/',
action='get',
fields=[
@ -171,7 +171,7 @@ class TestRouterGeneratedSchema(TestCase):
action='get'
),
'custom_list_action_multiple_methods': {
'retrieve': coreapi.Link(
'read': coreapi.Link(
url='/example/custom_list_action_multiple_methods/',
action='get'
),
@ -200,7 +200,7 @@ class TestRouterGeneratedSchema(TestCase):
coreapi.Field('b', required=False, location='form', type='string')
]
),
'destroy': coreapi.Link(
'delete': coreapi.Link(
url='/example/{id}/',
action='delete',
fields=[
@ -260,7 +260,7 @@ class TestSchemaGenerator(TestCase):
action='get',
fields=[]
),
'retrieve': coreapi.Link(
'read': coreapi.Link(
url='/example/{id}/',
action='get',
fields=[
@ -313,7 +313,7 @@ class TestSchemaGeneratorNotAtRoot(TestCase):
action='get',
fields=[]
),
'retrieve': coreapi.Link(
'read': coreapi.Link(
url='/api/v1/example/{id}/',
action='get',
fields=[