mirror of
https://github.com/encode/django-rest-framework.git
synced 2025-08-05 04:50:12 +03:00
Merge 361c9f4f3c
into 836e49b535
This commit is contained in:
commit
a264c8ca15
|
@ -83,18 +83,6 @@ def field_to_schema(field):
|
||||||
return coreschema.String(title=title, description=description)
|
return coreschema.String(title=title, description=description)
|
||||||
|
|
||||||
|
|
||||||
def common_path(paths):
|
|
||||||
split_paths = [path.strip('/').split('/') for path in paths]
|
|
||||||
s1 = min(split_paths)
|
|
||||||
s2 = max(split_paths)
|
|
||||||
common = s1
|
|
||||||
for i, c in enumerate(s1):
|
|
||||||
if c != s2[i]:
|
|
||||||
common = s1[:i]
|
|
||||||
break
|
|
||||||
return '/' + '/'.join(common)
|
|
||||||
|
|
||||||
|
|
||||||
def get_pk_name(model):
|
def get_pk_name(model):
|
||||||
meta = model._meta.concrete_model._meta
|
meta = model._meta.concrete_model._meta
|
||||||
return _get_pk(meta).name
|
return _get_pk(meta).name
|
||||||
|
@ -336,49 +324,24 @@ class SchemaGenerator(object):
|
||||||
# Only generate the path prefix for paths that will be included
|
# Only generate the path prefix for paths that will be included
|
||||||
if not paths:
|
if not paths:
|
||||||
return None
|
return None
|
||||||
prefix = self.determine_path_prefix(paths)
|
|
||||||
|
|
||||||
for path, method, view in view_endpoints:
|
for path, method, view in view_endpoints:
|
||||||
if not self.has_view_permissions(path, method, view):
|
if not self.has_view_permissions(path, method, view):
|
||||||
continue
|
continue
|
||||||
link = self.get_link(path, method, view)
|
link = self.get_link(path, method, view)
|
||||||
subpath = path[len(prefix):]
|
overall = self.make_overall(path)
|
||||||
keys = self.get_keys(subpath, method, view)
|
keys = self.get_keys(path, overall, method, view)
|
||||||
insert_into(links, keys, link)
|
insert_into(links, keys, link)
|
||||||
return links
|
return links
|
||||||
|
|
||||||
# Methods used when we generate a view instance from the raw callback...
|
# Methods used when we generate a view instance from the raw callback...
|
||||||
|
|
||||||
def determine_path_prefix(self, paths):
|
def make_overall(self, path):
|
||||||
"""
|
"""
|
||||||
Given a list of all paths, return the common prefix which should be
|
Removes '/' from path and returns a linked value using '-'.
|
||||||
discounted when generating a schema structure.
|
|
||||||
|
|
||||||
This will be the longest common string that does not include that last
|
|
||||||
component of the URL, or the last component before a path parameter.
|
|
||||||
|
|
||||||
For example:
|
|
||||||
|
|
||||||
/api/v1/users/
|
|
||||||
/api/v1/users/{pk}/
|
|
||||||
|
|
||||||
The path prefix is '/api/v1/'
|
|
||||||
"""
|
"""
|
||||||
prefixes = []
|
ret = [partial for partial in path.split('/') if partial and '{' not in partial]
|
||||||
for path in paths:
|
return '-'.join(ret)
|
||||||
components = path.strip('/').split('/')
|
|
||||||
initial_components = []
|
|
||||||
for component in components:
|
|
||||||
if '{' in component:
|
|
||||||
break
|
|
||||||
initial_components.append(component)
|
|
||||||
prefix = '/'.join(initial_components[:-1])
|
|
||||||
if not prefix:
|
|
||||||
# We can just break early in the case that there's at least
|
|
||||||
# one URL that doesn't have a path prefix.
|
|
||||||
return '/'
|
|
||||||
prefixes.append('/' + prefix + '/')
|
|
||||||
return common_path(prefixes)
|
|
||||||
|
|
||||||
def create_view(self, callback, method, request=None):
|
def create_view(self, callback, method, request=None):
|
||||||
"""
|
"""
|
||||||
|
@ -626,7 +589,7 @@ class SchemaGenerator(object):
|
||||||
|
|
||||||
# Method for generating the link layout....
|
# Method for generating the link layout....
|
||||||
|
|
||||||
def get_keys(self, subpath, method, view):
|
def get_keys(self, path, overall, method, view):
|
||||||
"""
|
"""
|
||||||
Return a list of keys that should be used to layout a link within
|
Return a list of keys that should be used to layout a link within
|
||||||
the schema document.
|
the schema document.
|
||||||
|
@ -643,14 +606,14 @@ class SchemaGenerator(object):
|
||||||
action = view.action
|
action = view.action
|
||||||
else:
|
else:
|
||||||
# Views have no associated action, so we determine one from the method.
|
# Views have no associated action, so we determine one from the method.
|
||||||
if is_list_view(subpath, method, view):
|
if is_list_view(path, method, view):
|
||||||
action = 'list'
|
action = 'list'
|
||||||
else:
|
else:
|
||||||
action = self.default_mapping[method.lower()]
|
action = self.default_mapping[method.lower()]
|
||||||
|
|
||||||
named_path_components = [
|
named_path_components = [
|
||||||
component for component
|
component for component
|
||||||
in subpath.strip('/').split('/')
|
in overall.strip('/').split('/')
|
||||||
if '{' not in component
|
if '{' not in component
|
||||||
]
|
]
|
||||||
|
|
||||||
|
@ -662,8 +625,8 @@ class SchemaGenerator(object):
|
||||||
action = self.coerce_method_names[action]
|
action = self.coerce_method_names[action]
|
||||||
return named_path_components + [action]
|
return named_path_components + [action]
|
||||||
else:
|
else:
|
||||||
|
named_path_components = overall.split('-')
|
||||||
return named_path_components[:-1] + [action]
|
return named_path_components[:-1] + [action]
|
||||||
|
|
||||||
if action in self.coerce_method_names:
|
if action in self.coerce_method_names:
|
||||||
action = self.coerce_method_names[action]
|
action = self.coerce_method_names[action]
|
||||||
|
|
||||||
|
|
|
@ -16,7 +16,7 @@
|
||||||
|
|
||||||
{% for section_key, section in document.data|items %}
|
{% for section_key, section in document.data|items %}
|
||||||
{% if section_key %}
|
{% if section_key %}
|
||||||
<h2 id="{{ section_key }}" class="coredocs-section-title">{{ section_key }} <a href="#{{ section_key }}"><i class="fa fa-link" aria-hidden="true"></i>
|
<h2 id="{{ section_key }}" class="coredocs-section-title">{{ section_key | symbolize }} <a href="#{{ section_key }}"><i class="fa fa-link" aria-hidden="true"></i>
|
||||||
</a></h2>
|
</a></h2>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
|
|
|
@ -362,3 +362,8 @@ def break_long_headers(header):
|
||||||
if len(header) > 160 and ',' in header:
|
if len(header) > 160 and ',' in header:
|
||||||
header = mark_safe('<br> ' + ', <br>'.join(header.split(',')))
|
header = mark_safe('<br> ' + ', <br>'.join(header.split(',')))
|
||||||
return header
|
return header
|
||||||
|
|
||||||
|
|
||||||
|
@register.filter
|
||||||
|
def symbolize(word):
|
||||||
|
return word.replace('-', ' ').upper()
|
||||||
|
|
|
@ -45,6 +45,7 @@ class NonAtomicAPIExceptionView(APIView):
|
||||||
BasicModel.objects.all()
|
BasicModel.objects.all()
|
||||||
raise Http404
|
raise Http404
|
||||||
|
|
||||||
|
|
||||||
urlpatterns = (
|
urlpatterns = (
|
||||||
url(r'^$', NonAtomicAPIExceptionView.as_view()),
|
url(r'^$', NonAtomicAPIExceptionView.as_view()),
|
||||||
)
|
)
|
||||||
|
|
|
@ -1132,9 +1132,6 @@ class TestDateTimeField(FieldValues):
|
||||||
valid_inputs = {
|
valid_inputs = {
|
||||||
'2001-01-01 13:00': datetime.datetime(2001, 1, 1, 13, 00, tzinfo=utc),
|
'2001-01-01 13:00': datetime.datetime(2001, 1, 1, 13, 00, tzinfo=utc),
|
||||||
'2001-01-01T13:00': datetime.datetime(2001, 1, 1, 13, 00, tzinfo=utc),
|
'2001-01-01T13:00': datetime.datetime(2001, 1, 1, 13, 00, tzinfo=utc),
|
||||||
'2001-01-01T13:00Z': datetime.datetime(2001, 1, 1, 13, 00, tzinfo=utc),
|
|
||||||
datetime.datetime(2001, 1, 1, 13, 00): datetime.datetime(2001, 1, 1, 13, 00, tzinfo=utc),
|
|
||||||
datetime.datetime(2001, 1, 1, 13, 00, tzinfo=utc): datetime.datetime(2001, 1, 1, 13, 00, tzinfo=utc),
|
|
||||||
# Django 1.4 does not support timezone string parsing.
|
# Django 1.4 does not support timezone string parsing.
|
||||||
'2001-01-01T13:00Z': datetime.datetime(2001, 1, 1, 13, 00, tzinfo=utc)
|
'2001-01-01T13:00Z': datetime.datetime(2001, 1, 1, 13, 00, tzinfo=utc)
|
||||||
}
|
}
|
||||||
|
|
|
@ -246,6 +246,7 @@ class ObjectPermissionInstanceView(generics.RetrieveUpdateDestroyAPIView):
|
||||||
authentication_classes = [authentication.BasicAuthentication]
|
authentication_classes = [authentication.BasicAuthentication]
|
||||||
permission_classes = [ViewObjectPermissions]
|
permission_classes = [ViewObjectPermissions]
|
||||||
|
|
||||||
|
|
||||||
object_permissions_view = ObjectPermissionInstanceView.as_view()
|
object_permissions_view = ObjectPermissionInstanceView.as_view()
|
||||||
|
|
||||||
|
|
||||||
|
@ -255,6 +256,7 @@ class ObjectPermissionListView(generics.ListAPIView):
|
||||||
authentication_classes = [authentication.BasicAuthentication]
|
authentication_classes = [authentication.BasicAuthentication]
|
||||||
permission_classes = [ViewObjectPermissions]
|
permission_classes = [ViewObjectPermissions]
|
||||||
|
|
||||||
|
|
||||||
object_permissions_list_view = ObjectPermissionListView.as_view()
|
object_permissions_list_view = ObjectPermissionListView.as_view()
|
||||||
|
|
||||||
|
|
||||||
|
@ -443,6 +445,7 @@ class DeniedObjectView(PermissionInstanceView):
|
||||||
class DeniedObjectViewWithDetail(PermissionInstanceView):
|
class DeniedObjectViewWithDetail(PermissionInstanceView):
|
||||||
permission_classes = (BasicObjectPermWithDetail,)
|
permission_classes = (BasicObjectPermWithDetail,)
|
||||||
|
|
||||||
|
|
||||||
denied_view = DeniedView.as_view()
|
denied_view = DeniedView.as_view()
|
||||||
|
|
||||||
denied_view_with_detail = DeniedViewWithDetail.as_view()
|
denied_view_with_detail = DeniedViewWithDetail.as_view()
|
||||||
|
|
|
@ -108,6 +108,7 @@ class HTMLView1(APIView):
|
||||||
def get(self, request, **kwargs):
|
def get(self, request, **kwargs):
|
||||||
return Response('text')
|
return Response('text')
|
||||||
|
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
url(r'^.*\.(?P<format>.+)$', MockView.as_view(renderer_classes=[RendererA, RendererB])),
|
url(r'^.*\.(?P<format>.+)$', MockView.as_view(renderer_classes=[RendererA, RendererB])),
|
||||||
url(r'^$', MockView.as_view(renderer_classes=[RendererA, RendererB])),
|
url(r'^$', MockView.as_view(renderer_classes=[RendererA, RendererB])),
|
||||||
|
|
|
@ -120,6 +120,7 @@ class MockView(APIView):
|
||||||
|
|
||||||
return Response(status=status.HTTP_500_INTERNAL_SERVER_ERROR)
|
return Response(status=status.HTTP_500_INTERNAL_SERVER_ERROR)
|
||||||
|
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
url(r'^$', MockView.as_view()),
|
url(r'^$', MockView.as_view()),
|
||||||
]
|
]
|
||||||
|
|
|
@ -32,6 +32,7 @@ class MockJsonRenderer(BaseRenderer):
|
||||||
class MockTextMediaRenderer(BaseRenderer):
|
class MockTextMediaRenderer(BaseRenderer):
|
||||||
media_type = 'text/html'
|
media_type = 'text/html'
|
||||||
|
|
||||||
|
|
||||||
DUMMYSTATUS = status.HTTP_200_OK
|
DUMMYSTATUS = status.HTTP_200_OK
|
||||||
DUMMYCONTENT = 'dummycontent'
|
DUMMYCONTENT = 'dummycontent'
|
||||||
|
|
||||||
|
|
|
@ -13,6 +13,7 @@ factory = APIRequestFactory()
|
||||||
def null_view(request):
|
def null_view(request):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
url(r'^view$', null_view, name='view'),
|
url(r'^view$', null_view, name='view'),
|
||||||
]
|
]
|
||||||
|
|
|
@ -70,6 +70,7 @@ class ExampleViewSet(ModelViewSet):
|
||||||
assert self.action
|
assert self.action
|
||||||
return super(ExampleViewSet, self).get_serializer(*args, **kwargs)
|
return super(ExampleViewSet, self).get_serializer(*args, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
if coreapi:
|
if coreapi:
|
||||||
schema_view = get_schema_view(title='Example API')
|
schema_view = get_schema_view(title='Example API')
|
||||||
else:
|
else:
|
||||||
|
@ -109,12 +110,6 @@ class TestRouterGeneratedSchema(TestCase):
|
||||||
url='/example/custom_list_action/',
|
url='/example/custom_list_action/',
|
||||||
action='get'
|
action='get'
|
||||||
),
|
),
|
||||||
'custom_list_action_multiple_methods': {
|
|
||||||
'read': coreapi.Link(
|
|
||||||
url='/example/custom_list_action_multiple_methods/',
|
|
||||||
action='get'
|
|
||||||
)
|
|
||||||
},
|
|
||||||
'read': coreapi.Link(
|
'read': coreapi.Link(
|
||||||
url='/example/{id}/',
|
url='/example/{id}/',
|
||||||
action='get',
|
action='get',
|
||||||
|
@ -122,6 +117,12 @@ class TestRouterGeneratedSchema(TestCase):
|
||||||
coreapi.Field('id', required=True, location='path', schema=coreschema.String())
|
coreapi.Field('id', required=True, location='path', schema=coreschema.String())
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
},
|
||||||
|
'example-custom_list_action_multiple_methods': {
|
||||||
|
'read': coreapi.Link(
|
||||||
|
url='/example/custom_list_action_multiple_methods/',
|
||||||
|
action='get'
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
@ -162,31 +163,6 @@ class TestRouterGeneratedSchema(TestCase):
|
||||||
coreapi.Field('id', required=True, location='path', schema=coreschema.String())
|
coreapi.Field('id', required=True, location='path', schema=coreschema.String())
|
||||||
]
|
]
|
||||||
),
|
),
|
||||||
'custom_action': coreapi.Link(
|
|
||||||
url='/example/{id}/custom_action/',
|
|
||||||
action='post',
|
|
||||||
encoding='application/json',
|
|
||||||
description='A description of custom action.',
|
|
||||||
fields=[
|
|
||||||
coreapi.Field('id', required=True, location='path', schema=coreschema.String()),
|
|
||||||
coreapi.Field('c', required=True, location='form', schema=coreschema.String(title='C')),
|
|
||||||
coreapi.Field('d', required=False, location='form', schema=coreschema.String(title='D')),
|
|
||||||
]
|
|
||||||
),
|
|
||||||
'custom_list_action': coreapi.Link(
|
|
||||||
url='/example/custom_list_action/',
|
|
||||||
action='get'
|
|
||||||
),
|
|
||||||
'custom_list_action_multiple_methods': {
|
|
||||||
'read': coreapi.Link(
|
|
||||||
url='/example/custom_list_action_multiple_methods/',
|
|
||||||
action='get'
|
|
||||||
),
|
|
||||||
'create': coreapi.Link(
|
|
||||||
url='/example/custom_list_action_multiple_methods/',
|
|
||||||
action='post'
|
|
||||||
)
|
|
||||||
},
|
|
||||||
'update': coreapi.Link(
|
'update': coreapi.Link(
|
||||||
url='/example/{id}/',
|
url='/example/{id}/',
|
||||||
action='put',
|
action='put',
|
||||||
|
@ -213,8 +189,33 @@ class TestRouterGeneratedSchema(TestCase):
|
||||||
fields=[
|
fields=[
|
||||||
coreapi.Field('id', required=True, location='path', schema=coreschema.String())
|
coreapi.Field('id', required=True, location='path', schema=coreschema.String())
|
||||||
]
|
]
|
||||||
|
),
|
||||||
|
'custom_list_action': coreapi.Link(
|
||||||
|
url='/example/custom_list_action/',
|
||||||
|
action='get'
|
||||||
|
),
|
||||||
|
'custom_action': coreapi.Link(
|
||||||
|
url='/example/{id}/custom_action/',
|
||||||
|
action='post',
|
||||||
|
encoding='application/json',
|
||||||
|
description='A description of custom action.',
|
||||||
|
fields=[
|
||||||
|
coreapi.Field('id', required=True, location='path', schema=coreschema.String()),
|
||||||
|
coreapi.Field('c', required=True, location='form', schema=coreschema.String(title='C')),
|
||||||
|
coreapi.Field('d', required=False, location='form', schema=coreschema.String(title='D')),
|
||||||
|
]
|
||||||
)
|
)
|
||||||
}
|
},
|
||||||
|
'example-custom_list_action_multiple_methods': {
|
||||||
|
'read': coreapi.Link(
|
||||||
|
url='/example/custom_list_action_multiple_methods/',
|
||||||
|
action='get'
|
||||||
|
),
|
||||||
|
'create': coreapi.Link(
|
||||||
|
url='/example/custom_list_action_multiple_methods/',
|
||||||
|
action='post'
|
||||||
|
)
|
||||||
|
},
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
assert response.data == expected
|
assert response.data == expected
|
||||||
|
@ -304,8 +305,8 @@ class TestSchemaGenerator(TestCase):
|
||||||
fields=[
|
fields=[
|
||||||
coreapi.Field('id', required=True, location='path', schema=coreschema.String())
|
coreapi.Field('id', required=True, location='path', schema=coreschema.String())
|
||||||
]
|
]
|
||||||
),
|
)},
|
||||||
'sub': {
|
'example-sub': {
|
||||||
'list': coreapi.Link(
|
'list': coreapi.Link(
|
||||||
url='/example/{id}/sub/',
|
url='/example/{id}/sub/',
|
||||||
action='get',
|
action='get',
|
||||||
|
@ -315,7 +316,6 @@ class TestSchemaGenerator(TestCase):
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
)
|
)
|
||||||
assert schema == expected
|
assert schema == expected
|
||||||
|
|
||||||
|
@ -340,7 +340,7 @@ class TestSchemaGeneratorNotAtRoot(TestCase):
|
||||||
url='',
|
url='',
|
||||||
title='Example API',
|
title='Example API',
|
||||||
content={
|
content={
|
||||||
'example': {
|
'api-v1-example': {
|
||||||
'create': coreapi.Link(
|
'create': coreapi.Link(
|
||||||
url='/api/v1/example/',
|
url='/api/v1/example/',
|
||||||
action='post',
|
action='post',
|
||||||
|
@ -357,8 +357,8 @@ class TestSchemaGeneratorNotAtRoot(TestCase):
|
||||||
fields=[
|
fields=[
|
||||||
coreapi.Field('id', required=True, location='path', schema=coreschema.String())
|
coreapi.Field('id', required=True, location='path', schema=coreschema.String())
|
||||||
]
|
]
|
||||||
),
|
)},
|
||||||
'sub': {
|
'api-v1-example-sub': {
|
||||||
'list': coreapi.Link(
|
'list': coreapi.Link(
|
||||||
url='/api/v1/example/{id}/sub/',
|
url='/api/v1/example/{id}/sub/',
|
||||||
action='get',
|
action='get',
|
||||||
|
@ -368,7 +368,6 @@ class TestSchemaGeneratorNotAtRoot(TestCase):
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
)
|
)
|
||||||
assert schema == expected
|
assert schema == expected
|
||||||
|
|
||||||
|
@ -467,8 +466,11 @@ class TestSchemaGeneratorWithRestrictedViewSets(TestCase):
|
||||||
class Test4605Regression(TestCase):
|
class Test4605Regression(TestCase):
|
||||||
def test_4605_regression(self):
|
def test_4605_regression(self):
|
||||||
generator = SchemaGenerator()
|
generator = SchemaGenerator()
|
||||||
prefix = generator.determine_path_prefix([
|
overall = generator.make_overall(
|
||||||
'/api/v1/items/',
|
'/api/',
|
||||||
|
)
|
||||||
|
assert overall == 'api'
|
||||||
|
overall = generator.make_overall(
|
||||||
'/auth/convert-token/'
|
'/auth/convert-token/'
|
||||||
])
|
)
|
||||||
assert prefix == '/'
|
assert overall == 'auth-convert-token'
|
||||||
|
|
|
@ -243,6 +243,7 @@ class RegexSerializer(serializers.Serializer):
|
||||||
validators=[RegexValidator(regex=re.compile('^[0-9]{4,6}$'),
|
validators=[RegexValidator(regex=re.compile('^[0-9]{4,6}$'),
|
||||||
message='A PIN is 4-6 digits')])
|
message='A PIN is 4-6 digits')])
|
||||||
|
|
||||||
|
|
||||||
expected_repr = """
|
expected_repr = """
|
||||||
RegexSerializer():
|
RegexSerializer():
|
||||||
pin = CharField(validators=[<django.core.validators.RegexValidator object>])
|
pin = CharField(validators=[<django.core.validators.RegexValidator object>])
|
||||||
|
|
Loading…
Reference in New Issue
Block a user