diff --git a/rest_framework/compat.py b/rest_framework/compat.py index b0e076203..d2cfa05cf 100644 --- a/rest_framework/compat.py +++ b/rest_framework/compat.py @@ -180,6 +180,13 @@ except (ImportError, SyntaxError): uritemplate = None +# coreschema is optional +try: + import coreschema +except ImportError: + coreschema = None + + # django-filter is optional try: import django_filters diff --git a/rest_framework/filters.py b/rest_framework/filters.py index 00e753d42..db116efb0 100644 --- a/rest_framework/filters.py +++ b/rest_framework/filters.py @@ -16,7 +16,7 @@ from django.utils import six from django.utils.translation import ugettext_lazy as _ from rest_framework.compat import ( - coreapi, distinct, django_filters, guardian, template_render + coreapi, coreschema, distinct, django_filters, guardian, template_render ) from rest_framework.settings import api_settings @@ -34,6 +34,7 @@ class BaseFilterBackend(object): def get_schema_fields(self, view): assert coreapi is not None, 'coreapi must be installed to use `get_schema_fields()`' + assert coreschema is not None, 'coreschema must be installed to use `get_schema_fields()`' return [] @@ -162,7 +163,18 @@ class SearchFilter(BaseFilterBackend): def get_schema_fields(self, view): assert coreapi is not None, 'coreapi must be installed to use `get_schema_fields()`' - return [coreapi.Field(name=self.search_param, required=False, location='query')] + assert coreschema is not None, 'coreschema must be installed to use `get_schema_fields()`' + return [ + coreapi.Field( + name=self.search_param, + required=False, + location='query', + schema=coreschema.String( + title='Search', + description='...' + ) + ) + ] class OrderingFilter(BaseFilterBackend): @@ -280,7 +292,18 @@ class OrderingFilter(BaseFilterBackend): def get_schema_fields(self, view): assert coreapi is not None, 'coreapi must be installed to use `get_schema_fields()`' - return [coreapi.Field(name=self.ordering_param, required=False, location='query')] + assert coreschema is not None, 'coreschema must be installed to use `get_schema_fields()`' + return [ + coreapi.Field( + name=self.ordering_param, + required=False, + location='query', + schema=coreschema.String( + title='Ordering', + description='...' + ) + ) + ] class DjangoObjectPermissionsFilter(BaseFilterBackend): diff --git a/rest_framework/pagination.py b/rest_framework/pagination.py index 3bb3f7897..95ba1448d 100644 --- a/rest_framework/pagination.py +++ b/rest_framework/pagination.py @@ -16,7 +16,7 @@ from django.utils.encoding import force_text from django.utils.six.moves.urllib import parse as urlparse from django.utils.translation import ugettext_lazy as _ -from rest_framework.compat import coreapi, template_render +from rest_framework.compat import coreapi, coreschema, template_render from rest_framework.exceptions import NotFound from rest_framework.response import Response from rest_framework.settings import api_settings @@ -289,12 +289,16 @@ class PageNumberPagination(BasePagination): def get_schema_fields(self, view): assert coreapi is not None, 'coreapi must be installed to use `get_schema_fields()`' + assert coreschema is not None, 'coreschema must be installed to use `get_schema_fields()`' fields = [ coreapi.Field( name=self.page_query_param, required=False, location='query', - #description=force_text(self.page_query_description) + schema=coreschema.Integer( + title='Page', + description=force_text(self.page_query_description) + ) ) ] if self.page_size_query_param is not None: @@ -439,18 +443,25 @@ class LimitOffsetPagination(BasePagination): def get_schema_fields(self, view): assert coreapi is not None, 'coreapi must be installed to use `get_schema_fields()`' + assert coreschema is not None, 'coreschema must be installed to use `get_schema_fields()`' return [ coreapi.Field( name=self.limit_query_param, required=False, location='query', - #description=force_text(self.limit_query_description) + schema=coreschema.Integer( + title='Limit', + description=force_text(self.limit_query_description) + ) ), coreapi.Field( name=self.offset_query_param, required=False, location='query', - #description=force_text(self.offset_query_description) + schema=coreschema.Integer( + title='Offset', + description=force_text(self.offset_query_description) + ) ) ] @@ -764,11 +775,15 @@ class CursorPagination(BasePagination): def get_schema_fields(self, view): assert coreapi is not None, 'coreapi must be installed to use `get_schema_fields()`' + assert coreschema is not None, 'coreschema must be installed to use `get_schema_fields()`' return [ coreapi.Field( name=self.cursor_query_param, required=False, location='query', - description=force_text(self.cursor_query_description) + schema=coreschema.String( + title='Cursor', + description=force_text(self.cursor_query_description) + ) ) ] diff --git a/rest_framework/renderers.py b/rest_framework/renderers.py index 95ea3690c..40ec51ac6 100644 --- a/rest_framework/renderers.py +++ b/rest_framework/renderers.py @@ -808,7 +808,7 @@ class DocumentationRenderer(BaseRenderer): code_style = formatter.get_style_defs('.highlight') langs = ['shell', 'javascript', 'python'] codec = coreapi.codecs.CoreJSONCodec() - schema = mark_safe(codec.encode(data)) + schema = mark_safe(json.dumps(codec.encode(data))) return { 'document': data, 'langs': langs, diff --git a/rest_framework/schemas.py b/rest_framework/schemas.py index 3a35db2cc..a875cc451 100644 --- a/rest_framework/schemas.py +++ b/rest_framework/schemas.py @@ -13,7 +13,7 @@ from django.utils.translation import ugettext_lazy as _ from rest_framework import exceptions, renderers, serializers from rest_framework.compat import ( - RegexURLPattern, RegexURLResolver, coreapi, uritemplate, urlparse + RegexURLPattern, RegexURLResolver, coreapi, coreschema, uritemplate, urlparse ) from rest_framework.request import clone_request from rest_framework.response import Response @@ -26,36 +26,62 @@ from rest_framework.views import APIView header_regex = re.compile('^[a-zA-Z][0-9A-Za-z_]*:') -types_lookup = ClassLookupDict({ - serializers.Field: 'string', - serializers.IntegerField: 'integer', - serializers.FloatField: 'number', - serializers.DecimalField: 'number', - serializers.BooleanField: 'boolean', - serializers.FileField: 'file', - serializers.MultipleChoiceField: 'array', - serializers.ManyRelatedField: 'array', - serializers.Serializer: 'object', - serializers.ListSerializer: 'array' -}) -input_lookup = ClassLookupDict({ - serializers.Field: 'text', - serializers.IntegerField: 'number', - serializers.FloatField: 'number', - serializers.DecimalField: 'number', - serializers.BooleanField: 'checkbox', - serializers.FileField: 'file', - serializers.ChoiceField: 'select' -}) +def field_to_schema(field): + title = force_text(field.label) if field.label else '' + description = force_text(field.help_text) if field.help_text else '' + if isinstance(field, serializers.ListSerializer): + child_schema = serializer_to_schema(field.child) + return coreschema.Array( + items=child_schema, + title=title, + description=description + ) + elif isinstance(field, serializers.Serializer): + return coreschema.Object( + properties={ + key: serializer_to_schema(value) + for key, value + in field.fields.items() + }, + title=title, + description=description + ) + elif isinstance(field, serializers.ManyRelatedField): + return coreschema.Array( + items=coreschema.String(), + title=title, + description=description + ) + elif isinstance(field, serializers.RelatedField): + return coreschema.String(title=title, description=description) + elif isinstance(field, serializers.MultipleChoiceField): + return coreschema.Array( + items=coreschema.Enum(enum=list(field.choices.values())), + title=title, + description=description + ) + elif isinstance(field, serializers.ChoiceField): + return coreschema.Enum( + enum=list(field.choices.values()), + title=title, + description=description + ) + elif isinstance(field, serializers.BooleanField): + return coreschema.Boolean(title=title, description=description) + elif isinstance(field, (serializers.DecimalField, serializers.FloatField)): + return coreschema.Number(title=title, description=description) + elif isinstance(field, serializers.IntegerField): + return coreschema.Integer(title=title, description=description) -def determine_input(field): - input_type = input_lookup[field] - base_template = field.style.get('base_template') - if base_template == 'textarea.html': - input_type = 'textarea' - return input_type + if field.style.get('base_template') == 'textarea.html': + return coreschema.String( + title=title, + description=description, + format='textarea' + ) + return coreschema.String(title=title, description=description) def common_path(paths): @@ -252,6 +278,7 @@ class SchemaGenerator(object): def __init__(self, title=None, url=None, patterns=None, urlconf=None): assert coreapi, '`coreapi` must be installed for schema support.' + assert coreschema, '`coreschema` must be installed for schema support.' if url and not url.endswith('/'): url += '/' @@ -493,8 +520,9 @@ class SchemaGenerator(object): fields = [] for variable in uritemplate.variables(path): - title = None - description = None + title = '' + description = '' + schema_cls = coreschema.String if model is not None: # Attempt to infer a field description if possible. try: @@ -510,12 +538,14 @@ class SchemaGenerator(object): elif model_field is not None and model_field.primary_key: description = get_pk_description(model, model_field) + if isinstance(model_field, models.AutoField): + schema_cls = coreschema.Integer + field = coreapi.Field( name=variable, location='path', required=True, - #title='' if (title is None) else title, - #description='' if (description is None) else description + schema=schema_cls(title=title, description=description) ) fields.append(field) @@ -540,7 +570,7 @@ class SchemaGenerator(object): name='data', location='body', required=True, - #type='array' + schema=coreschema.Array() ) ] @@ -553,17 +583,11 @@ class SchemaGenerator(object): continue required = field.required and method != 'PATCH' - title = force_text(field.label) if field.label else '' - description = force_text(field.help_text) if field.help_text else '' field = coreapi.Field( name=field.field_name, location='form', required=required, - #title=title, - #description=description, - #type=types_lookup[field], - #input=determine_input(field), - #choices=getattr(field, 'choices', None) + schema=field_to_schema(field) ) fields.append(field) diff --git a/rest_framework/templates/rest_framework/docs/index.html b/rest_framework/templates/rest_framework/docs/index.html index af0ccad2b..ab6492c68 100644 --- a/rest_framework/templates/rest_framework/docs/index.html +++ b/rest_framework/templates/rest_framework/docs/index.html @@ -85,7 +85,7 @@ const coreapi = window.coreapi const codec = new coreapi.codecs.CoreJSONCodec() const schema = {{ schema }} - const doc = codec.decode(schema, {preloaded: true}) + const doc = codec.decode(schema) const client = new coreapi.Client(null, null, csrf) // Language Control diff --git a/rest_framework/templates/rest_framework/docs/link.html b/rest_framework/templates/rest_framework/docs/link.html index 2d0f1879a..4e6fac231 100644 --- a/rest_framework/templates/rest_framework/docs/link.html +++ b/rest_framework/templates/rest_framework/docs/link.html @@ -20,7 +20,7 @@
{% for field in link.fields|with_location:'path' %} -{{ field.name }}
{% if field.required %} required{% endif %}{{ field.name }}
{% if field.required %} required{% endif %}{{ field.name }}
{% if field.required %} required{% endif %}{{ field.name }}
{% if field.required %} required{% endif %}{{ field.name }}
{% if field.required %} required{% endif %}{{ field.name }}
{% if field.required %} required{% endif %}{{ field.name }}
{% if field.required %} required{% endif %}{{ field.name }}
{% if field.required %} required{% endif %}{{ field.name }}
{% if field.required %} required{% endif %}{{ field.name }}
{% if field.required %} required{% endif %}