diff --git a/rest_framework/schemas.py b/rest_framework/schemas.py index 875f9454b..056e6263e 100644 --- a/rest_framework/schemas.py +++ b/rest_framework/schemas.py @@ -83,18 +83,6 @@ def field_to_schema(field): 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): meta = model._meta.concrete_model._meta return _get_pk(meta).name @@ -336,49 +324,24 @@ class SchemaGenerator(object): # Only generate the path prefix for paths that will be included if not paths: return None - prefix = self.determine_path_prefix(paths) for path, method, view in view_endpoints: if not self.has_view_permissions(path, method, view): continue link = self.get_link(path, method, view) - subpath = path[len(prefix):] - keys = self.get_keys(subpath, method, view) + overall = self.make_overall(path) + keys = self.get_keys(path, overall, method, view) insert_into(links, keys, link) return links # 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 - 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/' + Removes '/' from path and returns a linked value using '-'. """ - prefixes = [] - for path in paths: - 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) + ret = [partial for partial in path.split('/') if partial and '{' not in partial] + return '-'.join(ret) def create_view(self, callback, method, request=None): """ @@ -626,7 +589,7 @@ class SchemaGenerator(object): # 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 the schema document. @@ -643,14 +606,14 @@ class SchemaGenerator(object): action = view.action else: # 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' else: action = self.default_mapping[method.lower()] named_path_components = [ component for component - in subpath.strip('/').split('/') + in overall.strip('/').split('/') if '{' not in component ] @@ -662,8 +625,8 @@ class SchemaGenerator(object): action = self.coerce_method_names[action] return named_path_components + [action] else: + named_path_components = overall.split('-') return named_path_components[:-1] + [action] - if action in self.coerce_method_names: action = self.coerce_method_names[action] diff --git a/rest_framework/templates/rest_framework/docs/document.html b/rest_framework/templates/rest_framework/docs/document.html index ef5f5966b..b37fefe65 100644 --- a/rest_framework/templates/rest_framework/docs/document.html +++ b/rest_framework/templates/rest_framework/docs/document.html @@ -16,7 +16,7 @@ {% for section_key, section in document.data|items %} {% if section_key %} -

{{ section_key }} +

{{ section_key | symbolize }}

{% endif %} diff --git a/rest_framework/templatetags/rest_framework.py b/rest_framework/templatetags/rest_framework.py index 4e69b62c9..7693793c9 100644 --- a/rest_framework/templatetags/rest_framework.py +++ b/rest_framework/templatetags/rest_framework.py @@ -362,3 +362,8 @@ def break_long_headers(header): if len(header) > 160 and ',' in header: header = mark_safe('
' + ',
'.join(header.split(','))) return header + + +@register.filter +def symbolize(word): + return word.replace('-', ' ').upper() diff --git a/tests/test_atomic_requests.py b/tests/test_atomic_requests.py index 9085bfc89..f925ce3d3 100644 --- a/tests/test_atomic_requests.py +++ b/tests/test_atomic_requests.py @@ -45,6 +45,7 @@ class NonAtomicAPIExceptionView(APIView): BasicModel.objects.all() raise Http404 + urlpatterns = ( url(r'^$', NonAtomicAPIExceptionView.as_view()), ) diff --git a/tests/test_fields.py b/tests/test_fields.py index 968c41d3f..1bc87182f 100644 --- a/tests/test_fields.py +++ b/tests/test_fields.py @@ -1132,9 +1132,6 @@ class TestDateTimeField(FieldValues): valid_inputs = { '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: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. '2001-01-01T13:00Z': datetime.datetime(2001, 1, 1, 13, 00, tzinfo=utc) } diff --git a/tests/test_permissions.py b/tests/test_permissions.py index cabf66883..6fbf766a0 100644 --- a/tests/test_permissions.py +++ b/tests/test_permissions.py @@ -246,6 +246,7 @@ class ObjectPermissionInstanceView(generics.RetrieveUpdateDestroyAPIView): authentication_classes = [authentication.BasicAuthentication] permission_classes = [ViewObjectPermissions] + object_permissions_view = ObjectPermissionInstanceView.as_view() @@ -255,6 +256,7 @@ class ObjectPermissionListView(generics.ListAPIView): authentication_classes = [authentication.BasicAuthentication] permission_classes = [ViewObjectPermissions] + object_permissions_list_view = ObjectPermissionListView.as_view() @@ -443,6 +445,7 @@ class DeniedObjectView(PermissionInstanceView): class DeniedObjectViewWithDetail(PermissionInstanceView): permission_classes = (BasicObjectPermWithDetail,) + denied_view = DeniedView.as_view() denied_view_with_detail = DeniedViewWithDetail.as_view() diff --git a/tests/test_renderers.py b/tests/test_renderers.py index 09cef435e..1625b0b70 100644 --- a/tests/test_renderers.py +++ b/tests/test_renderers.py @@ -108,6 +108,7 @@ class HTMLView1(APIView): def get(self, request, **kwargs): return Response('text') + urlpatterns = [ url(r'^.*\.(?P.+)$', MockView.as_view(renderer_classes=[RendererA, RendererB])), url(r'^$', MockView.as_view(renderer_classes=[RendererA, RendererB])), diff --git a/tests/test_request.py b/tests/test_request.py index 428b969f5..2cf9320ea 100644 --- a/tests/test_request.py +++ b/tests/test_request.py @@ -120,6 +120,7 @@ class MockView(APIView): return Response(status=status.HTTP_500_INTERNAL_SERVER_ERROR) + urlpatterns = [ url(r'^$', MockView.as_view()), ] diff --git a/tests/test_response.py b/tests/test_response.py index 33c51e773..e92bf54c1 100644 --- a/tests/test_response.py +++ b/tests/test_response.py @@ -32,6 +32,7 @@ class MockJsonRenderer(BaseRenderer): class MockTextMediaRenderer(BaseRenderer): media_type = 'text/html' + DUMMYSTATUS = status.HTTP_200_OK DUMMYCONTENT = 'dummycontent' diff --git a/tests/test_reverse.py b/tests/test_reverse.py index 2ca44ab77..47eda256e 100644 --- a/tests/test_reverse.py +++ b/tests/test_reverse.py @@ -13,6 +13,7 @@ factory = APIRequestFactory() def null_view(request): pass + urlpatterns = [ url(r'^view$', null_view, name='view'), ] diff --git a/tests/test_schemas.py b/tests/test_schemas.py index 24131480f..42781176f 100644 --- a/tests/test_schemas.py +++ b/tests/test_schemas.py @@ -70,6 +70,7 @@ class ExampleViewSet(ModelViewSet): assert self.action return super(ExampleViewSet, self).get_serializer(*args, **kwargs) + if coreapi: schema_view = get_schema_view(title='Example API') else: @@ -109,12 +110,6 @@ class TestRouterGeneratedSchema(TestCase): url='/example/custom_list_action/', action='get' ), - 'custom_list_action_multiple_methods': { - 'read': coreapi.Link( - url='/example/custom_list_action_multiple_methods/', - action='get' - ) - }, 'read': coreapi.Link( url='/example/{id}/', action='get', @@ -122,6 +117,12 @@ class TestRouterGeneratedSchema(TestCase): 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()) ] ), - '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( url='/example/{id}/', action='put', @@ -213,8 +189,33 @@ class TestRouterGeneratedSchema(TestCase): fields=[ 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 @@ -304,16 +305,15 @@ class TestSchemaGenerator(TestCase): fields=[ coreapi.Field('id', required=True, location='path', schema=coreschema.String()) ] - ), - 'sub': { - 'list': coreapi.Link( - url='/example/{id}/sub/', - action='get', - fields=[ - coreapi.Field('id', required=True, location='path', schema=coreschema.String()) - ] - ) - } + )}, + 'example-sub': { + 'list': coreapi.Link( + url='/example/{id}/sub/', + action='get', + fields=[ + coreapi.Field('id', required=True, location='path', schema=coreschema.String()) + ] + ) } } ) @@ -340,7 +340,7 @@ class TestSchemaGeneratorNotAtRoot(TestCase): url='', title='Example API', content={ - 'example': { + 'api-v1-example': { 'create': coreapi.Link( url='/api/v1/example/', action='post', @@ -357,16 +357,15 @@ class TestSchemaGeneratorNotAtRoot(TestCase): fields=[ coreapi.Field('id', required=True, location='path', schema=coreschema.String()) ] - ), - 'sub': { - 'list': coreapi.Link( - url='/api/v1/example/{id}/sub/', - action='get', - fields=[ - coreapi.Field('id', required=True, location='path', schema=coreschema.String()) - ] - ) - } + )}, + 'api-v1-example-sub': { + 'list': coreapi.Link( + url='/api/v1/example/{id}/sub/', + action='get', + fields=[ + coreapi.Field('id', required=True, location='path', schema=coreschema.String()) + ] + ) } } ) @@ -467,8 +466,11 @@ class TestSchemaGeneratorWithRestrictedViewSets(TestCase): class Test4605Regression(TestCase): def test_4605_regression(self): generator = SchemaGenerator() - prefix = generator.determine_path_prefix([ - '/api/v1/items/', + overall = generator.make_overall( + '/api/', + ) + assert overall == 'api' + overall = generator.make_overall( '/auth/convert-token/' - ]) - assert prefix == '/' + ) + assert overall == 'auth-convert-token' diff --git a/tests/test_validation.py b/tests/test_validation.py index 8ff4aaf38..8b71693c5 100644 --- a/tests/test_validation.py +++ b/tests/test_validation.py @@ -243,6 +243,7 @@ class RegexSerializer(serializers.Serializer): validators=[RegexValidator(regex=re.compile('^[0-9]{4,6}$'), message='A PIN is 4-6 digits')]) + expected_repr = """ RegexSerializer(): pin = CharField(validators=[])