Commit 68d20201 by Tom Christie Committed by GitHub

Live API documentation (#4755)

parent 7f59ce1e
coverage:
status:
project: false
patch: false
changes: false
comment: off
<style>
.promo li a {
float: left;
width: 130px;
height: 20px;
text-align: center;
margin: 10px 30px;
padding: 150px 0 0 0;
background-position: 0 50%;
background-size: 130px auto;
background-repeat: no-repeat;
font-size: 120%;
color: black;
}
.promo li {
list-style: none;
}
</style>
# Django REST framework 3.6
---
## Funding
The 3.6 release would not have been possible without our [collaborative funding model][funding].
If you use REST framework commercially and would like to see this work continue,
we strongly encourage you to invest in its continued development by
**[signing up for a paid&nbsp;plan][funding]**.
<ul class="premium-promo promo">
<li><a href="http://jobs.rover.com/" style="background-image: url(https://fund-rest-framework.s3.amazonaws.com/rover_130x130.png)">Rover.com</a></li>
<li><a href="https://getsentry.com/welcome/" style="background-image: url(https://fund-rest-framework.s3.amazonaws.com/sentry130.png)">Sentry</a></li>
<li><a href="https://getstream.io/try-the-api/?utm_source=drf&utm_medium=banner&utm_campaign=drf" style="background-image: url(https://fund-rest-framework.s3.amazonaws.com/stream-130.png)">Stream</a></li>
<li><a href="https://hello.machinalis.co.uk/" style="background-image: url(https://fund-rest-framework.s3.amazonaws.com/Machinalis130.png)">Machinalis</a></li>
<li><a href="https://rollbar.com" style="background-image: url(https://fund-rest-framework.s3.amazonaws.com/rollbar.png)">Rollbar</a></li>
<li><a href="https://micropyramid.com/django-rest-framework-development-services/" style="background-image: url(https://fund-rest-framework.s3.amazonaws.com/mp-text-logo.png)">MicroPyramid</a></li>
</ul>
<div style="clear: both; padding-bottom: 20px;"></div>
*Many thanks to all our [sponsors][sponsors], and in particular to our premium backers, [Rover](http://jobs.rover.com/), [Sentry](https://getsentry.com/welcome/), [Stream](https://getstream.io/?utm_source=drf&utm_medium=banner&utm_campaign=drf), [Machinalis](https://hello.machinalis.co.uk/), [Rollbar](https://rollbar.com), and [MicroPyramid](https://micropyramid.com/django-rest-framework-development-services/).*
---
## API documentation
...
## JavaScript Client
...
---
## Deprecations
...
---
[sponsors]: https://fund.django-rest-framework.org/topics/funding/#our-sponsors
[funding]: funding.md
...@@ -6,9 +6,13 @@ ...@@ -6,9 +6,13 @@
There are a variety of approaches to API documentation. This document introduces a few of the various tools and options you might choose from. The approaches should not be considered exclusive - you may want to provide more than one documentation style for you API, such as a self describing API that also includes static documentation of the various API endpoints. There are a variety of approaches to API documentation. This document introduces a few of the various tools and options you might choose from. The approaches should not be considered exclusive - you may want to provide more than one documentation style for you API, such as a self describing API that also includes static documentation of the various API endpoints.
## Endpoint documentation ##
The most common way to document Web APIs today is to produce documentation that lists the API endpoints verbatim, and describes the allowable operations on each. There are various tools that allow you to do this in an automated or semi-automated way. ... TODO ...
## Third party packages
There are a number of mature third-party packages for providing API documentation.
--- ---
......
...@@ -3,7 +3,7 @@ ...@@ -3,7 +3,7 @@
Looking for a new Django REST Framework related role? On this site we provide a list of job resources that may be helpful. It's also worth checking out if any of [our sponsors are hiring][drf-funding]. Looking for a new Django REST Framework related role? On this site we provide a list of job resources that may be helpful. It's also worth checking out if any of [our sponsors are hiring][drf-funding].
## Places to Look for Django REST Framework Jobs ## Places to look for Django REST Framework Jobs
* [https://www.djangoproject.com/community/jobs/][djangoproject-website] * [https://www.djangoproject.com/community/jobs/][djangoproject-website]
* [https://www.python.org/jobs/][python-org-jobs] * [https://www.python.org/jobs/][python-org-jobs]
......
https://github.com/twbs/bootstrap/
The MIT License (MIT)
Copyright (c) 2011-2016 Twitter, Inc.
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
https://github.com/bazh/jquery.json-view/
The MIT License (MIT)
Copyright (c) 2014 bazh. (http://github.com/bazh)
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
the Software, and to permit persons to whom the Software is furnished to do so,
subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
...@@ -62,14 +62,15 @@ pages: ...@@ -62,14 +62,15 @@ pages:
- 'Tutorials and Resources': 'topics/tutorials-and-resources.md' - 'Tutorials and Resources': 'topics/tutorials-and-resources.md'
- 'Contributing to REST framework': 'topics/contributing.md' - 'Contributing to REST framework': 'topics/contributing.md'
- 'Project management': 'topics/project-management.md' - 'Project management': 'topics/project-management.md'
- 'Jobs': 'topics/jobs.md'
- '3.0 Announcement': 'topics/3.0-announcement.md' - '3.0 Announcement': 'topics/3.0-announcement.md'
- '3.1 Announcement': 'topics/3.1-announcement.md' - '3.1 Announcement': 'topics/3.1-announcement.md'
- '3.2 Announcement': 'topics/3.2-announcement.md' - '3.2 Announcement': 'topics/3.2-announcement.md'
- '3.3 Announcement': 'topics/3.3-announcement.md' - '3.3 Announcement': 'topics/3.3-announcement.md'
- '3.4 Announcement': 'topics/3.4-announcement.md' - '3.4 Announcement': 'topics/3.4-announcement.md'
- '3.5 Announcement': 'topics/3.5-announcement.md' - '3.5 Announcement': 'topics/3.5-announcement.md'
- '3.6 Announcement': 'topics/3.6-announcement.md'
- 'Kickstarter Announcement': 'topics/kickstarter-announcement.md' - 'Kickstarter Announcement': 'topics/kickstarter-announcement.md'
- 'Mozilla Grant': 'topics/mozilla-grant.md' - 'Mozilla Grant': 'topics/mozilla-grant.md'
- 'Funding': 'topics/funding.md' - 'Funding': 'topics/funding.md'
- 'Release Notes': 'topics/release-notes.md' - 'Release Notes': 'topics/release-notes.md'
- 'Jobs': 'topics/jobs.md'
...@@ -2,4 +2,5 @@ ...@@ -2,4 +2,5 @@
markdown==2.6.4 markdown==2.6.4
django-guardian==1.4.6 django-guardian==1.4.6
django-filter==1.0.0 django-filter==1.0.0
coreapi==2.0.8 coreapi==2.2.4
coreschema==0.0.4
...@@ -175,6 +175,13 @@ except (ImportError, SyntaxError): ...@@ -175,6 +175,13 @@ except (ImportError, SyntaxError):
uritemplate = None uritemplate = None
# coreschema is optional
try:
import coreschema
except ImportError:
coreschema = None
# django-filter is optional # django-filter is optional
try: try:
import django_filters import django_filters
......
from django.conf.urls import include, url
from rest_framework.renderers import (
CoreJSONRenderer, DocumentationRenderer, SchemaJSRenderer
)
from rest_framework.schemas import get_schema_view
def get_docs_view(title=None, description=None, schema_url=None, public=True):
renderer_classes = [DocumentationRenderer, CoreJSONRenderer]
return get_schema_view(
title=title,
url=schema_url,
description=description,
renderer_classes=renderer_classes,
public=public
)
def get_schemajs_view(title=None, description=None, schema_url=None, public=True):
renderer_classes = [SchemaJSRenderer]
return get_schema_view(
title=title,
url=schema_url,
description=description,
renderer_classes=renderer_classes,
public=public
)
def include_docs_urls(title=None, description=None, schema_url=None, public=True):
docs_view = get_docs_view(
title=title,
description=description,
schema_url=schema_url,
public=public
)
schema_js_view = get_schemajs_view(
title=title,
description=description,
schema_url=schema_url,
public=public
)
urls = [
url(r'^$', docs_view, name='docs-index'),
url(r'^schema.js$', schema_js_view, name='schema-js')
]
return include(urls, namespace='api-docs')
...@@ -13,10 +13,11 @@ from django.db import models ...@@ -13,10 +13,11 @@ from django.db import models
from django.db.models.constants import LOOKUP_SEP from django.db.models.constants import LOOKUP_SEP
from django.template import loader from django.template import loader
from django.utils import six from django.utils import six
from django.utils.encoding import force_text
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from rest_framework.compat import ( 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 from rest_framework.settings import api_settings
...@@ -34,6 +35,7 @@ class BaseFilterBackend(object): ...@@ -34,6 +35,7 @@ class BaseFilterBackend(object):
def get_schema_fields(self, view): def get_schema_fields(self, view):
assert coreapi is not None, 'coreapi must be installed to use `get_schema_fields()`' 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 [] return []
...@@ -82,6 +84,8 @@ class SearchFilter(BaseFilterBackend): ...@@ -82,6 +84,8 @@ class SearchFilter(BaseFilterBackend):
'@': 'search', '@': 'search',
'$': 'iregex', '$': 'iregex',
} }
search_title = _('Search')
search_description = _('A search term.')
def get_search_terms(self, request): def get_search_terms(self, request):
""" """
...@@ -162,13 +166,26 @@ class SearchFilter(BaseFilterBackend): ...@@ -162,13 +166,26 @@ class SearchFilter(BaseFilterBackend):
def get_schema_fields(self, view): def get_schema_fields(self, view):
assert coreapi is not None, 'coreapi must be installed to use `get_schema_fields()`' 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=force_text(self.search_title),
description=force_text(self.search_description)
)
)
]
class OrderingFilter(BaseFilterBackend): class OrderingFilter(BaseFilterBackend):
# The URL query parameter used for the ordering. # The URL query parameter used for the ordering.
ordering_param = api_settings.ORDERING_PARAM ordering_param = api_settings.ORDERING_PARAM
ordering_fields = None ordering_fields = None
ordering_title = _('Ordering')
ordering_description = _('Which field to use when ordering the results.')
template = 'rest_framework/filters/ordering.html' template = 'rest_framework/filters/ordering.html'
def get_ordering(self, request, queryset, view): def get_ordering(self, request, queryset, view):
...@@ -280,7 +297,18 @@ class OrderingFilter(BaseFilterBackend): ...@@ -280,7 +297,18 @@ class OrderingFilter(BaseFilterBackend):
def get_schema_fields(self, view): def get_schema_fields(self, view):
assert coreapi is not None, 'coreapi must be installed to use `get_schema_fields()`' 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=force_text(self.ordering_title),
description=force_text(self.ordering_description)
)
)
]
class DjangoObjectPermissionsFilter(BaseFilterBackend): class DjangoObjectPermissionsFilter(BaseFilterBackend):
......
...@@ -12,10 +12,11 @@ from django.core.paginator import Paginator as DjangoPaginator ...@@ -12,10 +12,11 @@ from django.core.paginator import Paginator as DjangoPaginator
from django.core.paginator import InvalidPage from django.core.paginator import InvalidPage
from django.template import loader from django.template import loader
from django.utils import six from django.utils import six
from django.utils.encoding import force_text
from django.utils.six.moves.urllib import parse as urlparse from django.utils.six.moves.urllib import parse as urlparse
from django.utils.translation import ugettext_lazy as _ 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.exceptions import NotFound
from rest_framework.response import Response from rest_framework.response import Response
from rest_framework.settings import api_settings from rest_framework.settings import api_settings
...@@ -178,10 +179,12 @@ class PageNumberPagination(BasePagination): ...@@ -178,10 +179,12 @@ class PageNumberPagination(BasePagination):
# Client can control the page using this query parameter. # Client can control the page using this query parameter.
page_query_param = 'page' page_query_param = 'page'
page_query_description = _('A page number within the paginated result set.')
# Client can control the page size using this query parameter. # Client can control the page size using this query parameter.
# Default is 'None'. Set to eg 'page_size' to enable usage. # Default is 'None'. Set to eg 'page_size' to enable usage.
page_size_query_param = None page_size_query_param = None
page_size_query_description = _('Number of results to return per page.')
# Set to an integer to limit the maximum page size the client may request. # Set to an integer to limit the maximum page size the client may request.
# Only relevant if 'page_size_query_param' has also been set. # Only relevant if 'page_size_query_param' has also been set.
...@@ -286,12 +289,29 @@ class PageNumberPagination(BasePagination): ...@@ -286,12 +289,29 @@ class PageNumberPagination(BasePagination):
def get_schema_fields(self, view): def get_schema_fields(self, view):
assert coreapi is not None, 'coreapi must be installed to use `get_schema_fields()`' 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 = [ fields = [
coreapi.Field(name=self.page_query_param, required=False, location='query') coreapi.Field(
name=self.page_query_param,
required=False,
location='query',
schema=coreschema.Integer(
title='Page',
description=force_text(self.page_query_description)
)
)
] ]
if self.page_size_query_param is not None: if self.page_size_query_param is not None:
fields.append( fields.append(
coreapi.Field(name=self.page_size_query_param, required=False, location='query') coreapi.Field(
name=self.page_size_query_param,
required=False,
location='query',
schema=coreschema.Integer(
title='Page size',
description=force_text(self.page_size_query_description)
)
)
) )
return fields return fields
...@@ -305,7 +325,9 @@ class LimitOffsetPagination(BasePagination): ...@@ -305,7 +325,9 @@ class LimitOffsetPagination(BasePagination):
""" """
default_limit = api_settings.PAGE_SIZE default_limit = api_settings.PAGE_SIZE
limit_query_param = 'limit' limit_query_param = 'limit'
limit_query_description = _('Number of results to return per page.')
offset_query_param = 'offset' offset_query_param = 'offset'
offset_query_description = _('The initial index from which to return the results.')
max_limit = None max_limit = None
template = 'rest_framework/pagination/numbers.html' template = 'rest_framework/pagination/numbers.html'
...@@ -424,9 +446,26 @@ class LimitOffsetPagination(BasePagination): ...@@ -424,9 +446,26 @@ class LimitOffsetPagination(BasePagination):
def get_schema_fields(self, view): def get_schema_fields(self, view):
assert coreapi is not None, 'coreapi must be installed to use `get_schema_fields()`' 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 [ return [
coreapi.Field(name=self.limit_query_param, required=False, location='query'), coreapi.Field(
coreapi.Field(name=self.offset_query_param, required=False, location='query') name=self.limit_query_param,
required=False,
location='query',
schema=coreschema.Integer(
title='Limit',
description=force_text(self.limit_query_description)
)
),
coreapi.Field(
name=self.offset_query_param,
required=False,
location='query',
schema=coreschema.Integer(
title='Offset',
description=force_text(self.offset_query_description)
)
)
] ]
...@@ -437,6 +476,7 @@ class CursorPagination(BasePagination): ...@@ -437,6 +476,7 @@ class CursorPagination(BasePagination):
http://cramer.io/2011/03/08/building-cursors-for-the-disqus-api http://cramer.io/2011/03/08/building-cursors-for-the-disqus-api
""" """
cursor_query_param = 'cursor' cursor_query_param = 'cursor'
cursor_query_description = _('The pagination cursor value.')
page_size = api_settings.PAGE_SIZE page_size = api_settings.PAGE_SIZE
invalid_cursor_message = _('Invalid cursor') invalid_cursor_message = _('Invalid cursor')
ordering = '-created' ordering = '-created'
...@@ -738,6 +778,15 @@ class CursorPagination(BasePagination): ...@@ -738,6 +778,15 @@ class CursorPagination(BasePagination):
def get_schema_fields(self, view): def get_schema_fields(self, view):
assert coreapi is not None, 'coreapi must be installed to use `get_schema_fields()`' 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 [ return [
coreapi.Field(name=self.cursor_query_param, required=False, location='query') coreapi.Field(
name=self.cursor_query_param,
required=False,
location='query',
schema=coreschema.String(
title='Cursor',
description=force_text(self.cursor_query_description)
)
)
] ]
...@@ -8,7 +8,6 @@ from django.http import Http404 ...@@ -8,7 +8,6 @@ from django.http import Http404
from rest_framework import exceptions from rest_framework import exceptions
from rest_framework.compat import is_authenticated from rest_framework.compat import is_authenticated
SAFE_METHODS = ('GET', 'HEAD', 'OPTIONS') SAFE_METHODS = ('GET', 'HEAD', 'OPTIONS')
......
...@@ -8,6 +8,7 @@ REST framework also provides an HTML renderer that renders the browsable API. ...@@ -8,6 +8,7 @@ REST framework also provides an HTML renderer that renders the browsable API.
""" """
from __future__ import unicode_literals from __future__ import unicode_literals
import base64
import json import json
from collections import OrderedDict from collections import OrderedDict
...@@ -19,6 +20,7 @@ from django.http.multipartparser import parse_header ...@@ -19,6 +20,7 @@ from django.http.multipartparser import parse_header
from django.template import Template, loader from django.template import Template, loader
from django.test.client import encode_multipart from django.test.client import encode_multipart
from django.utils import six from django.utils import six
from django.utils.html import mark_safe
from rest_framework import VERSION, exceptions, serializers, status from rest_framework import VERSION, exceptions, serializers, status
from rest_framework.compat import ( from rest_framework.compat import (
...@@ -794,6 +796,47 @@ class AdminRenderer(BrowsableAPIRenderer): ...@@ -794,6 +796,47 @@ class AdminRenderer(BrowsableAPIRenderer):
return context return context
class DocumentationRenderer(BaseRenderer):
media_type = 'text/html'
format = 'html'
charset = 'utf-8'
template = 'rest_framework/docs/index.html'
code_style = 'emacs'
def get_context(self, data, request):
from pygments.formatters import HtmlFormatter
formatter = HtmlFormatter(style=self.code_style)
code_style = formatter.get_style_defs('.highlight')
langs = ['shell', 'javascript', 'python']
return {
'document': data,
'langs': langs,
'code_style': code_style,
'request': request
}
def render(self, data, accepted_media_type=None, renderer_context=None):
template = loader.get_template(self.template)
context = self.get_context(data, renderer_context['request'])
return template_render(template, context, request=renderer_context['request'])
class SchemaJSRenderer(BaseRenderer):
media_type = 'script/javascript'
format = 'javascript'
charset = 'utf-8'
template = 'rest_framework/schema.js'
def render(self, data, accepted_media_type=None, renderer_context=None):
codec = coreapi.codecs.CoreJSONCodec()
schema = base64.b64encode(codec.encode(data))
template = loader.get_template(self.template)
context = {'schema': mark_safe(schema)}
request = renderer_context['request']
return template_render(template, context, request=request)
class MultiPartRenderer(BaseRenderer): class MultiPartRenderer(BaseRenderer):
media_type = 'multipart/form-data; boundary=BoUnDaRyStRiNg' media_type = 'multipart/form-data; boundary=BoUnDaRyStRiNg'
format = 'multipart' format = 'multipart'
......
...@@ -5,37 +5,82 @@ from importlib import import_module ...@@ -5,37 +5,82 @@ from importlib import import_module
from django.conf import settings from django.conf import settings
from django.contrib.admindocs.views import simplify_regex from django.contrib.admindocs.views import simplify_regex
from django.core.exceptions import PermissionDenied from django.core.exceptions import PermissionDenied
from django.db import models
from django.http import Http404 from django.http import Http404
from django.utils import six from django.utils import six
from django.utils.encoding import force_text, smart_text from django.utils.encoding import force_text, smart_text
from django.utils.translation import ugettext_lazy as _
from rest_framework import exceptions, renderers, serializers from rest_framework import exceptions, renderers, serializers
from rest_framework.compat import ( 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.request import clone_request
from rest_framework.response import Response from rest_framework.response import Response
from rest_framework.settings import api_settings from rest_framework.settings import api_settings
from rest_framework.utils import formatting from rest_framework.utils import formatting
from rest_framework.utils.field_mapping import ClassLookupDict
from rest_framework.utils.model_meta import _get_pk from rest_framework.utils.model_meta import _get_pk
from rest_framework.views import APIView from rest_framework.views import APIView
header_regex = re.compile('^[a-zA-Z][0-9A-Za-z_]*:') header_regex = re.compile('^[a-zA-Z][0-9A-Za-z_]*:')
types_lookup = ClassLookupDict({
serializers.Field: 'string', def field_to_schema(field):
serializers.IntegerField: 'integer', title = force_text(field.label) if field.label else ''
serializers.FloatField: 'number', description = force_text(field.help_text) if field.help_text else ''
serializers.DecimalField: 'number',
serializers.BooleanField: 'boolean', if isinstance(field, serializers.ListSerializer):
serializers.FileField: 'file', child_schema = field_to_schema(field.child)
serializers.MultipleChoiceField: 'array', return coreschema.Array(
serializers.ManyRelatedField: 'array', items=child_schema,
serializers.Serializer: 'object', title=title,
serializers.ListSerializer: 'array' description=description
}) )
elif isinstance(field, serializers.Serializer):
return coreschema.Object(
properties=OrderedDict([
(key, field_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.keys())),
title=title,
description=description
)
elif isinstance(field, serializers.ChoiceField):
return coreschema.Enum(
enum=list(field.choices.keys()),
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)
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): def common_path(paths):
...@@ -113,6 +158,20 @@ def endpoint_ordering(endpoint): ...@@ -113,6 +158,20 @@ def endpoint_ordering(endpoint):
return (path, method_priority) return (path, method_priority)
def get_pk_description(model, model_field):
if isinstance(model_field, models.AutoField):
value_type = _('unique integer value')
elif isinstance(model_field, models.UUIDField):
value_type = _('UUID string')
else:
value_type = _('unique value')
return _('A {value_type} identifying this {name}.').format(
value_type=value_type,
name=model._meta.verbose_name,
)
class EndpointInspector(object): class EndpointInspector(object):
""" """
A class to determine the available API endpoints that a project exposes. A class to determine the available API endpoints that a project exposes.
...@@ -216,8 +275,9 @@ class SchemaGenerator(object): ...@@ -216,8 +275,9 @@ class SchemaGenerator(object):
# Set by 'SCHEMA_COERCE_PATH_PK'. # Set by 'SCHEMA_COERCE_PATH_PK'.
coerce_path_pk = None coerce_path_pk = None
def __init__(self, title=None, url=None, patterns=None, urlconf=None): def __init__(self, title=None, url=None, description=None, patterns=None, urlconf=None):
assert coreapi, '`coreapi` must be installed for schema support.' assert coreapi, '`coreapi` must be installed for schema support.'
assert coreschema, '`coreschema` must be installed for schema support.'
if url and not url.endswith('/'): if url and not url.endswith('/'):
url += '/' url += '/'
...@@ -228,10 +288,11 @@ class SchemaGenerator(object): ...@@ -228,10 +288,11 @@ class SchemaGenerator(object):
self.patterns = patterns self.patterns = patterns
self.urlconf = urlconf self.urlconf = urlconf
self.title = title self.title = title
self.description = description
self.url = url self.url = url
self.endpoints = None self.endpoints = None
def get_schema(self, request=None): def get_schema(self, request=None, public=False):
""" """
Generate a `coreapi.Document` representing the API schema. Generate a `coreapi.Document` representing the API schema.
""" """
...@@ -239,10 +300,18 @@ class SchemaGenerator(object): ...@@ -239,10 +300,18 @@ class SchemaGenerator(object):
inspector = self.endpoint_inspector_cls(self.patterns, self.urlconf) inspector = self.endpoint_inspector_cls(self.patterns, self.urlconf)
self.endpoints = inspector.get_api_endpoints() self.endpoints = inspector.get_api_endpoints()
links = self.get_links(request) links = self.get_links(None if public else request)
if not links: if not links:
return None return None
return coreapi.Document(title=self.title, url=self.url, content=links)
url = self.url
if not url and request is not None:
url = request.build_absolute_uri()
return coreapi.Document(
title=self.title, description=self.description,
url=url, content=links
)
def get_links(self, request=None): def get_links(self, request=None):
""" """
...@@ -450,10 +519,37 @@ class SchemaGenerator(object): ...@@ -450,10 +519,37 @@ class SchemaGenerator(object):
Return a list of `coreapi.Field` instances corresponding to any Return a list of `coreapi.Field` instances corresponding to any
templated path variables. templated path variables.
""" """
model = getattr(getattr(view, 'queryset', None), 'model', None)
fields = [] fields = []
for variable in uritemplate.variables(path): for variable in uritemplate.variables(path):
field = coreapi.Field(name=variable, location='path', required=True) title = ''
description = ''
schema_cls = coreschema.String
if model is not None:
# Attempt to infer a field description if possible.
try:
model_field = model._meta.get_field(variable)
except:
pass
if model_field is not None and model_field.verbose_name:
title = force_text(model_field.verbose_name)
if model_field is not None and model_field.help_text:
description = force_text(model_field.help_text)
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,
schema=schema_cls(title=title, description=description)
)
fields.append(field) fields.append(field)
return fields return fields
...@@ -477,7 +573,7 @@ class SchemaGenerator(object): ...@@ -477,7 +573,7 @@ class SchemaGenerator(object):
name='data', name='data',
location='body', location='body',
required=True, required=True,
type='array' schema=coreschema.Array()
) )
] ]
...@@ -490,13 +586,11 @@ class SchemaGenerator(object): ...@@ -490,13 +586,11 @@ class SchemaGenerator(object):
continue continue
required = field.required and method != 'PATCH' required = field.required and method != 'PATCH'
description = force_text(field.help_text) if field.help_text else ''
field = coreapi.Field( field = coreapi.Field(
name=field.field_name, name=field.field_name,
location='form', location='form',
required=required, required=required,
description=description, schema=field_to_schema(field)
type=types_lookup[field]
) )
fields.append(field) fields.append(field)
...@@ -571,11 +665,11 @@ class SchemaGenerator(object): ...@@ -571,11 +665,11 @@ class SchemaGenerator(object):
return named_path_components + [action] return named_path_components + [action]
def get_schema_view(title=None, url=None, urlconf=None, renderer_classes=None): def get_schema_view(title=None, url=None, description=None, urlconf=None, renderer_classes=None, public=False):
""" """
Return a schema view. Return a schema view.
""" """
generator = SchemaGenerator(title=title, url=url, urlconf=urlconf) generator = SchemaGenerator(title=title, url=url, description=description, urlconf=urlconf)
if renderer_classes is None: if renderer_classes is None:
if renderers.BrowsableAPIRenderer in api_settings.DEFAULT_RENDERER_CLASSES: if renderers.BrowsableAPIRenderer in api_settings.DEFAULT_RENDERER_CLASSES:
rclasses = [renderers.CoreJSONRenderer, renderers.BrowsableAPIRenderer] rclasses = [renderers.CoreJSONRenderer, renderers.BrowsableAPIRenderer]
...@@ -590,7 +684,7 @@ def get_schema_view(title=None, url=None, urlconf=None, renderer_classes=None): ...@@ -590,7 +684,7 @@ def get_schema_view(title=None, url=None, urlconf=None, renderer_classes=None):
renderer_classes = rclasses renderer_classes = rclasses
def get(self, request, *args, **kwargs): def get(self, request, *args, **kwargs):
schema = generator.get_schema(request) schema = generator.get_schema(request, public)
if schema is None: if schema is None:
raise exceptions.PermissionDenied() raise exceptions.PermissionDenied()
return Response(schema) return Response(schema)
......
h1 {
font-size: 45px;
}
.intro-code {
margin-top: 20px;
}
pre.highlight code * {
white-space: nowrap; // this sets all children inside to nowrap
}
pre.highlight {
overflow-x: auto; // this sets the scrolling in x
}
pre.highlight code {
white-space: pre; // forces <code> to respect <pre> formatting
}
.main-container {
padding-left: 30px;
padding-right: 30px;
}
.btn:focus,
.btn:focus:active {
outline: none;
}
.sidebar {
overflow: auto;
font-family: verdana;
font-size: 12px;
font-weight: 200;
background-color: #2e353d;
position: fixed;
top: 0px;
width: 225px;
height: 100%;
color: #FFF;
}
.sidebar .brand {
background-color: #23282e;
display: block;
text-align: center;
padding: 25px 0;
margin-top: 0;
margin-bottom: 0;
}
.sidebar .brand a {
color: #FFF;
}
.sidebar .brand a:hover,
.sidebar .brand a:active,
.sidebar .brand a:focus {
text-decoration: none;
}
.sidebar .toggle-btn {
display: none;
}
.sidebar .menu-list ul,
.sidebar .menu-list li {
background: #2e353d;
list-style: none;
padding: 0px;
margin: 0px;
line-height: 35px;
cursor: pointer;
}
.sidebar .menu-list ul :not(collapsed) .arrow:before,
.sidebar .menu-list li :not(collapsed) .arrow:before {
font-family: FontAwesome;
content: "\f078";
display: inline-block;
padding-left: 10px;
padding-right: 10px;
vertical-align: middle;
float: right;
}
.sidebar .menu-list ul .active,
.sidebar .menu-list li .active {
border-left: 3px solid #d19b3d;
background-color: #4f5b69;
}
.sidebar .menu-list ul .sub-menu li.active,
.sidebar .menu-list li .sub-menu li.active {
color: #d19b3d;
}
.sidebar .menu-list ul .sub-menu li.active a,
.sidebar .menu-list li .sub-menu li.active a {
color: #d19b3d;
}
.sidebar .menu-list ul .sub-menu li,
.sidebar .menu-list li .sub-menu li {
background-color: #181c20;
border: none;
border-bottom: 1px solid #23282e;
margin-left: 0px;
text-indent: 10px;
}
.sidebar .menu-list ul .sub-menu li:hover,
.sidebar .menu-list li .sub-menu li:hover {
background-color: #020203;
}
.sidebar .menu-list ul .sub-menu li a,
.sidebar .menu-list li .sub-menu li a {
display: block;
}
.sidebar .menu-list ul .sub-menu li a:before,
.sidebar .menu-list li .sub-menu li a:before {
font-family: FontAwesome;
content: "\f105";
display: inline-block;
padding-left: 10px;
padding-right: 10px;
vertical-align: middle;
}
.sidebar .menu-list li {
padding-left: 0px;
border-left: 3px solid #2e353d;
border-bottom: 1px solid #23282e;
}
.sidebar .menu-list li a {
text-decoration: none;
color: white;
}
.sidebar .menu-list li a i {
padding-left: 10px;
width: 20px;
padding-right: 20px;
}
.sidebar .menu-list li:hover {
border-left: 3px solid #d19b3d;
background-color: #4f5b69;
-webkit-transition: all 1s ease;
-moz-transition: all 1s ease;
-o-transition: all 1s ease;
-ms-transition: all 1s ease;
transition: all 1s ease;
}
body {
margin: 0px;
padding: 0px;
}
.coredocs-section-title {
margin-top: 20px;
padding-bottom: 10px;
border-bottom: 1px solid lightgrey;
}
.coredocs-link-title a,
.coredocs-section-title a {
display: none;
}
.coredocs-link-title a,
.coredocs-section-title a {
text-decoration: none;
}
.coredocs-link-title:hover a,
.coredocs-section-title:hover a {
display: inline;
font-size: 20px;
}
.coredocs-section-title:last-child {
margin-top: 0;
}
/* @group Language Switcher */
.sidebar .menu-list.menu-list-bottom {
margin-bottom: 0;
position: absolute;
bottom: 0;
left: 0;
right: 0;
border-top: 1px solid #23282e;
}
.sidebar .menu-list-bottom li span {
float: right;
margin-right: 20px;
color: #d19b3d;
}
/* @end Language Switcher */
/* @group Docs Content */
.docs-content .meta .label {
vertical-align: middle;
font-size: 14px;
font-weight: normal;
}
.docs-content .meta code {
vertical-align: middle;
padding: .2em .6em .3em;
font-size: 14px;
}
.docs-content .btn {
font-size: inherit;
}
.code-samples pre {
margin-top: 20px;
}
/* @end Docs Content */
@media (max-width: 767px) {
.main-container {
padding-left: 15px;
padding-right: 15px;
}
.sidebar {
position: relative;
width: 100%;
margin-bottom: 10px;
overflow: visible;
}
.sidebar .toggle-btn {
display: block;
cursor: pointer;
position: absolute;
right: 10px;
top: 10px;
z-index: 10 !important;
padding: 3px;
width: 40px;
text-align: center;
}
.sidebar .menu-list.menu-list-bottom {
position: static;
}
.sidebar .brand {
margin-top: 0;
margin-bottom: 0;
text-align: left !important;
font-size: 22px;
padding: 0;
padding-left: 20px;
line-height: 50px !important;
}
}
@media (min-width: 767px) {
.sidebar .menu-list .menu-content {
display: block;
}
#main {
width:calc(100% - 225px);
float: right;
}
}
@media (min-width: 992px) {
.modal-lg {
width: 980px;
}
}
.api-modal .modal-title .fa {
color: #93c54b;
}
.api-modal .modal-body .request-awaiting {
padding: 35px 10px;
color: #7F8177;
text-align: center;
}
.api-modal .modal-body .meta {
margin-bottom: 20px;
}
.api-modal .modal-body .meta .label {
vertical-align: middle;
font-size: 14px;
font-weight: normal;
}
.api-modal .modal-body .meta code {
vertical-align: middle;
padding: .2em .6em .3em;
font-size: 14px;
}
.api-modal .modal-content .toggle-view {
text-align: right;
float: right;
}
.api-modal .modal-content .response .well {
margin: 0;
max-height: 550px;
}
.highlight {
background-color: #f7f7f9
}
.checkbox label.control-label {
font-weight: bold
}
@media (min-width: 768px) {
.navbar-nav.navbar-right:last-child {
margin-right: 0 !important;
}
}
This source diff could not be displayed because it is too large. You can view the blob instead.
This source diff could not be displayed because it is too large. You can view the blob instead.
/*
This is the GitHub theme for highlight.js
github.com style (c) Vasily Polovnyov <vast@whiteants.net>
*/
.hljs {
display: block;
overflow-x: auto;
padding: 0.5em;
color: #333;
-webkit-text-size-adjust: none;
}
.hljs-comment,
.diff .hljs-header,
.hljs-javadoc {
color: #998;
font-style: italic;
}
.hljs-keyword,
.css .rule .hljs-keyword,
.hljs-winutils,
.nginx .hljs-title,
.hljs-subst,
.hljs-request,
.hljs-status {
color: #333;
font-weight: bold;
}
.hljs-number,
.hljs-hexcolor,
.ruby .hljs-constant {
color: #008080;
}
.hljs-string,
.hljs-tag .hljs-value,
.hljs-phpdoc,
.hljs-dartdoc,
.tex .hljs-formula {
color: #d14;
}
.hljs-title,
.hljs-id,
.scss .hljs-preprocessor {
color: #900;
font-weight: bold;
}
.hljs-list .hljs-keyword,
.hljs-subst {
font-weight: normal;
}
.hljs-class .hljs-title,
.hljs-type,
.vhdl .hljs-literal,
.tex .hljs-command {
color: #458;
font-weight: bold;
}
.hljs-tag,
.hljs-tag .hljs-title,
.hljs-rule .hljs-property,
.django .hljs-tag .hljs-keyword {
color: #000080;
font-weight: normal;
}
.hljs-attribute,
.hljs-variable,
.lisp .hljs-body,
.hljs-name {
color: #008080;
}
.hljs-regexp {
color: #009926;
}
.hljs-symbol,
.ruby .hljs-symbol .hljs-string,
.lisp .hljs-keyword,
.clojure .hljs-keyword,
.scheme .hljs-keyword,
.tex .hljs-special,
.hljs-prompt {
color: #990073;
}
.hljs-built_in {
color: #0086b3;
}
.hljs-preprocessor,
.hljs-pragma,
.hljs-pi,
.hljs-doctype,
.hljs-shebang,
.hljs-cdata {
color: #999;
font-weight: bold;
}
.hljs-deletion {
background: #fdd;
}
.hljs-addition {
background: #dfd;
}
.diff .hljs-change {
background: #0086b3;
}
.hljs-chunk {
color: #aaa;
}
.json-view{position:relative}
.json-view .collapser{width:20px;height:18px;display:block;position:absolute;left:-1.7em;top:-.2em;z-index:5;background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAICAYAAADED76LAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAD1JREFUeNpiYGBgOADE%2F3Hgw0DM4IRHgSsDFOzFInmMAQnY49ONzZRjDFiADT7dMLALiE8y4AGW6LoBAgwAuIkf%2F%2FB7O9sAAAAASUVORK5CYII%3D);background-repeat:no-repeat;background-position:center center;opacity:.5;cursor:pointer}
.json-view .collapsed{-ms-transform:rotate(-90deg);-moz-transform:rotate(-90deg);-khtml-transform:rotate(-90deg);-webkit-transform:rotate(-90deg);-o-transform:rotate(-90deg);transform:rotate(-90deg)}
.json-view .bl{display:block;padding-left:20px;margin-left:-20px;position:relative}
.json-view{font-family:monospace}
.json-view ul{list-style-type:none;padding-left:2em;border-left:1px dotted;margin:.3em}
.json-view ul li{position:relative}
.json-view .comments,.json-view .dots{display:none;-moz-user-select:none;-ms-user-select:none;-khtml-user-select:none;-webkit-user-select:none;-o-user-select:none;user-select:none}
.json-view .comments{padding-left:.8em;font-style:italic;color:#888}
.json-view .bool,.json-view .null,.json-view .num,.json-view .undef{font-weight:700;color:#1A01CC}
.json-view .str{color:#800}
\ No newline at end of file
This source diff could not be displayed because it is too large. You can view the blob instead.
function normalizeHTTPHeader(str) {
return (str.charAt(0).toUpperCase() + str.substring(1))
.replace( /-(.)/g, function($1) { return $1.toUpperCase(); })
.replace( /(Www)/g, function($1) { return 'WWW'; })
.replace( /(Xss)/g, function($1) { return 'XSS'; })
.replace( /(Md5)/g, function($1) { return 'MD5'; })
}
function getCookie(name) {
var cookieValue = null;
if (document.cookie && document.cookie !== '') {
var cookies = document.cookie.split(';');
for (var i = 0; i < cookies.length; i++) {
var cookie = jQuery.trim(cookies[i]);
// Does this cookie string begin with the name we want?
if (cookie.substring(0, name.length + 1) === (name + '=')) {
cookieValue = decodeURIComponent(cookie.substring(name.length + 1));
break;
}
}
}
return cookieValue;
}
let responseDisplay = 'data';
const coreapi = window.coreapi
const schema = window.schema
// Language Control
$('#language-control li').click(function (event) {
event.preventDefault();
const languageMenuItem = $(this).find('a');
var language = languageMenuItem.data("language")
var languageControls = $(this).closest('ul').find('li');
languageControls.find('a').not('[data-language="' + language +'"]').parent().removeClass("active")
languageControls.find('a').filter('[data-language="' + language +'"]').parent().addClass("active")
$('#selected-language').text(language)
var codeBlocks = $('pre.highlight')
codeBlocks.not('[data-language="' + language +'"]').addClass("hide")
codeBlocks.filter('[data-language="' + language +'"]').removeClass("hide")
})
// API Explorer
$('form.api-interaction').submit(function(event) {
event.preventDefault();
const form = $(this).closest("form");
const key = form.data("key");
var params = {};
const formData = new FormData(form.get()[0]);
for (var [paramKey, paramValue] of formData.entries()) {
var elem = form.find("[name=" + paramKey + "]")
var dataType = elem.data('type') || 'string'
var dataLocation = elem.data('location')
if (dataType === 'integer' && paramValue) {
paramValue = parseInt(paramValue)
} else if (dataType === 'number' && paramValue) {
paramValue = parseFloat(paramValue)
} else if (dataType === 'boolean' && paramValue) {
paramValue = {
'true': true,
'false': false
}[paramValue.toLowerCase()]
} else if (dataType === 'array' && paramValue) {
paramValue = JSON.parse(paramValue)
}
if (dataLocation === 'query' && !paramValue) {
continue
}
params[paramKey] = paramValue
}
form.find(":checkbox").each(function( index ) {
var name = $(this).attr("name");
if (!params.hasOwnProperty(name)) {
params[name] = false
}
})
function requestCallback(request) {
// Fill in the "GET /foo/" display.
let parser = document.createElement('a');
parser.href = request.url;
const method = request.options.method
const path = parser.pathname + parser.hash + parser.search
form.find(".request-method").text(method)
form.find(".request-url").text(path)
}
function responseCallback(response, responseText) {
// Display the 'Data'/'Raw' control.
form.closest(".modal-content").find(".toggle-view").removeClass("hide")
// Fill in the "200 OK" display.
form.find(".response-status-code").removeClass("label-success").removeClass("label-danger")
if (response.ok) {
form.find(".response-status-code").addClass("label-success")
} else {
form.find(".response-status-code").addClass("label-danger")
}
form.find(".response-status-code").text(response.status)
form.find(".meta").removeClass("hide")
// Fill in the Raw HTTP response display.
var panelText = 'HTTP/1.1 ' + response.status + ' ' + response.statusText + '\n';
response.headers.forEach((header, key) => {
panelText += normalizeHTTPHeader(key) + ': ' + header + '\n'
})
if (responseText) {
panelText += '\n' + responseText
}
form.find(".response-raw-response").text(panelText)
}
// Instantiate a client to make the outgoing request.
let options = {
requestCallback: requestCallback,
responseCallback: responseCallback,
}
// Setup authentication options.
if (window.auth && window.auth.type === 'token') {
// Header authentication
options.headers = {
'Authorization': window.auth.value
}
} else if (window.auth && window.auth.type === 'basic') {
// Basic authentication
const token = window.auth.username + ':' + window.auth.password
const hash = window.btoa(token)
options.headers = {
'Authorization': 'Basic ' + hash
}
} else if (window.auth && window.auth.type === 'session') {
// Session authentication
options.csrf = {
'X-CSRFToken': getCookie('csrftoken')
}
}
const client = new coreapi.Client(options)
client.action(schema, key, params).then(function (data) {
var response = JSON.stringify(data, null, 2);
form.find(".request-awaiting").addClass("hide")
form.find(".response-raw").addClass("hide")
form.find(".response-data").addClass("hide")
form.find(".response-data").text('')
form.find(".response-data").jsonView(response)
if (responseDisplay === 'data') {
form.find(".response-data").removeClass("hide")
} else {
form.find(".response-raw").removeClass("hide")
}
}).catch(function (error) {
var response = JSON.stringify(error.content, null, 2);
form.find(".request-awaiting").addClass("hide")
form.find(".response-raw").addClass("hide")
form.find(".response-data").addClass("hide")
form.find(".response-data").text('')
form.find(".response-data").jsonView(response)
if (responseDisplay === 'data') {
form.find(".response-data").removeClass("hide")
} else {
form.find(".response-raw").removeClass("hide")
}
})
});
// 'Data'/'Raw' control
$('.toggle-view button').click(function() {
responseDisplay = $(this).data("display-toggle");
$(this).removeClass("btn-default").addClass('btn-info').siblings().removeClass('btn-info');
if (responseDisplay === 'raw') {
$(this).closest(".modal-content").find(".response-raw").removeClass("hide");
$(this).closest(".modal-content").find(".response-data").addClass("hide");
} else {
$(this).closest(".modal-content").find(".response-data").removeClass("hide");
$(this).closest(".modal-content").find(".response-raw").addClass("hide");
}
});
// Authentication: none
$('#auth-control').find("[data-auth='none']").click(function (event) {
event.preventDefault();
window.auth = null;
$('#selected-authentication').text('none');
$('#auth-control').children().removeClass('active');
$('#auth-control').find("[data-auth='none']").addClass('active');
})
// Authentication: token
$('form.authentication-token-form').submit(function(event) {
event.preventDefault();
const form = $(this).closest("form");
const value = form.find('input').val();
window.auth = {
'type': 'token',
'value': value,
};
$('#selected-authentication').text('header');
$('#auth-control').children().removeClass('active');
$('#auth-control').find("[data-auth='token']").addClass('active');
$('#auth_token_modal').modal('hide');
});
// Authentication: basic
$('form.authentication-basic-form').submit(function(event) {
event.preventDefault();
const form = $(this).closest("form");
const username = form.find('input#username').val();
const password = form.find('input#password').val();
window.auth = {
'type': 'basic',
'username': username,
'password': password,
};
$('#selected-authentication').text('basic');
$('#auth-control').children().removeClass('active');
$('#auth-control').find("[data-auth='basic']").addClass('active');
$('#auth_basic_modal').modal('hide');
});
// Authentication: session
$('form.authentication-session-form').submit(function(event) {
event.preventDefault();
window.auth = {
'type': 'session',
};
$('#selected-authentication').text('session');
$('#auth-control').children().removeClass('active');
$('#auth-control').find("[data-auth='session']").addClass('active');
$('#auth_session_modal').modal('hide');
});
function getSearchTerm()
{
var sPageURL = window.location.search.substring(1);
var sURLVariables = sPageURL.split('&');
for (var i = 0; i < sURLVariables.length; i++)
{
var sParameterName = sURLVariables[i].split('=');
if (sParameterName[0] == 'q')
{
return sParameterName[1];
}
}
}
$(document).ready(function() {
var search_term = getSearchTerm(),
$search_modal = $('#mkdocs_search_modal');
if(search_term){
$search_modal.modal();
}
// make sure search input gets autofocus everytime modal opens.
$search_modal.on('shown.bs.modal', function () {
$search_modal.find('#mkdocs-search-query').focus();
});
// Highlight.js
hljs.initHighlightingOnLoad();
$('table').addClass('table table-striped table-hover');
});
$('body').scrollspy({
target: '.bs-sidebar',
});
/* Prevent disabled links from causing a page reload */
$("li.disabled a").click(function() {
event.preventDefault();
});
This source diff could not be displayed because it is too large. You can view the blob instead.
/**
* jquery.json-view - jQuery collapsible JSON plugin
* @version v1.0.0
* @link http://github.com/bazh/jquery.json-view
* @license MIT
*/
!function(e){"use strict";var n=function(n){var a=e("<span />",{"class":"collapser",on:{click:function(){var n=e(this);n.toggleClass("collapsed");var a=n.parent().children(".block"),p=a.children("ul");n.hasClass("collapsed")?(p.hide(),a.children(".dots, .comments").show()):(p.show(),a.children(".dots, .comments").hide())}}});return n&&a.addClass("collapsed"),a},a=function(a,p){var t=e.extend({},{nl2br:!0},p),r=function(e){return e.toString()?e.toString().replace(/&/g,"&amp;").replace(/"/g,"&quot;").replace(/</g,"&lt;").replace(/>/g,"&gt;"):""},s=function(n,a){return e("<span />",{"class":a,html:r(n)})},l=function(a,p){switch(e.type(a)){case"object":p||(p=0);var c=e("<span />",{"class":"block"}),d=Object.keys(a).length;if(!d)return c.append(s("{","b")).append(" ").append(s("}","b"));c.append(s("{","b"));var i=e("<ul />",{"class":"obj collapsible level"+p});return e.each(a,function(a,t){d--;var r=e("<li />").append(s('"',"q")).append(a).append(s('"',"q")).append(": ").append(l(t,p+1));-1===["object","array"].indexOf(e.type(t))||e.isEmptyObject(t)||r.prepend(n()),d>0&&r.append(","),i.append(r)}),c.append(i),c.append(s("...","dots")),c.append(s("}","b")),c.append(1===Object.keys(a).length?s("// 1 item","comments"):s("// "+Object.keys(a).length+" items","comments")),c;case"array":p||(p=0);var d=a.length,c=e("<span />",{"class":"block"});if(!d)return c.append(s("[","b")).append(" ").append(s("]","b"));c.append(s("[","b"));var i=e("<ul />",{"class":"obj collapsible level"+p});return e.each(a,function(a,t){d--;var r=e("<li />").append(l(t,p+1));-1===["object","array"].indexOf(e.type(t))||e.isEmptyObject(t)||r.prepend(n()),d>0&&r.append(","),i.append(r)}),c.append(i),c.append(s("...","dots")),c.append(s("]","b")),c.append(1===a.length?s("// 1 item","comments"):s("// "+a.length+" items","comments")),c;case"string":if(a=r(a),/^(http|https|file):\/\/[^\s]+$/i.test(a))return e("<span />").append(s('"',"q")).append(e("<a />",{href:a,text:a})).append(s('"',"q"));if(t.nl2br){var o=/\n/g;o.test(a)&&(a=(a+"").replace(o,"<br />"))}var u=e("<span />",{"class":"str"}).html(a);return e("<span />").append(s('"',"q")).append(u).append(s('"',"q"));case"number":return s(a.toString(),"num");case"undefined":return s("undefined","undef");case"null":return s("null","null");case"boolean":return s(a?"true":"false","bool")}};return l(a)};return e.fn.jsonView=function(n,p){var t=e(this);if(p=e.extend({},{nl2br:!0},p),"string"==typeof n)try{n=JSON.parse(n)}catch(r){}return t.append(e("<div />",{"class":"json-view"}).append(a(n,p))),t}}(jQuery);
\ No newline at end of file
This source diff could not be displayed because it is too large. You can view the blob instead.
{% load rest_framework %}
<!-- Modal -->
<div class="modal fade auth-modal auth-basic" id="auth_basic_modal" tabindex="-1" role="dialog" aria-labelledby="basic authentication modal">
<div class="modal-dialog modal-md" role="document">
<div class="modal-content">
<div class="modal-header">
<h3 class="modal-title"><i class="fa fa-key"></i> Basic Authentication</h3>
</div>
<form class="form-horizontal authentication-basic-form">
<div class="modal-body">
<div class="form-group">
<label for="authorization" class="col-sm-2 control-label">Username:</label>
<div class="col-sm-10">
<input type="text" class="form-control" id="username" required>
</div>
</div>
<div class="form-group">
<label for="authorization" class="col-sm-2 control-label">Password:</label>
<div class="col-sm-10">
<input type="password" class="form-control" id="password" required>
</div>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-default" data-dismiss="modal">Close</button>
<button type="submit" class="btn btn-primary">Use Basic Authentication</button>
</div>
</form>
</div>
</div>
</div>
{% load rest_framework %}
<!-- Modal -->
<div class="modal fade auth-modal auth-session" id="auth_session_modal" tabindex="-1" role="dialog" aria-labelledby="session authentication modal">
<div class="modal-dialog modal-md" role="document">
<div class="modal-content">
<div class="modal-header">
<h3 class="modal-title"><i class="fa fa-key"></i> Session Authentication</h3>
</div>
<form class="form-horizontal authentication-session-form">
<div class="modal-body">
{% if user.is_authenticated %}
<h4 class="text-center">You are logged in as {{ user.username }}.</h4>
{% else %}
<div class="text-center">
<h4 class="text-center">You need to {% optional_docs_login request %} to enable Session Authentication.</h4>
</div>
{% endif %}
</div>
<div class="modal-footer">
<button type="button" class="btn btn-default" data-dismiss="modal">Close</button>
{% if user.is_authenticated %}
<button type="submit" class="btn btn-primary">Use Session Authentication</button>
{% endif %}
</div>
</form>
</div>
</div>
</div>
{% load rest_framework %}
<!-- Modal -->
<div class="modal fade auth-modal auth-token" id="auth_token_modal" tabindex="-1" role="dialog" aria-labelledby="token authentication modal">
<div class="modal-dialog modal-md" role="document">
<div class="modal-content">
<div class="modal-header">
<h3 class="modal-title"><i class="fa fa-key"></i> Authentication Header</h3>
</div>
<form class="form-horizontal authentication-token-form">
<div class="modal-body">
<div class="form-group">
<label for="authorization" class="col-sm-2 control-label">Authorization:</label>
<div class="col-sm-10">
<input type="text" class="form-control" id="authorization" placeholder="Bearer XXXX-XXXX-XXXX-XXXX" aria-describedby="helpBlock" required>
<span id="helpBlock" class="help-block">The value to include for the <code>Authorization</code> header in outgoing HTTP requests.</span>
</div>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-default" data-dismiss="modal">Close</button>
<button type="submit" class="btn btn-primary">Use Authentication Header</button>
</div>
</form>
</div>
</div>
</div>
{% load rest_framework %}
<div class="row intro">
<div class="col-md-6 intro-title">
<h1>{{ document.title }}</h1>
{% if document.description %}
<p>{% render_markdown document.description %}</p>
{% endif %}
</div>
<div class="col-md-6 intro-code">
{% if 'shell' in langs %}{% include "rest_framework/docs/langs/shell-intro.html" %}{% endif %}
{% if 'python' in langs %}{% include "rest_framework/docs/langs/python-intro.html" %}{% endif %}
{% if 'javascript' in langs %}{% include "rest_framework/docs/langs/javascript-intro.html" %}{% endif %}
</div>
</div>
{% for section_key, section in document.data.items %}
<h2 id="{{ section_key }}" class="coredocs-section-title">{{ section_key }} <a href="#{{ section_key }}"><i class="fa fa-link" aria-hidden="true"></i>
</a></h2>
{% for link_key, link in section.links.items %}
{% include "rest_framework/docs/link.html" %}
{% endfor %}
{% endfor %}
{% for link_key, link in document.links.items %}
{% include "rest_framework/docs/link.html" %}
{% endfor %}
{% load staticfiles %}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{{ document.title }}</title>
<link href="{% static 'rest_framework/docs/css/bootstrap.min.css' %}" rel="stylesheet">
<link href="{% static 'rest_framework/docs/css/bootstrap-theme.min.css' %}" rel="stylesheet">
<link href="{% static 'rest_framework/docs/css/font-awesome-4.0.3.css' %}" rel="stylesheet">
<link href="{% static 'rest_framework/docs/css/base.css' %}" rel="stylesheet">
<link href="{% static 'rest_framework/docs/css/jquery.json-view.min.css' %}" rel="stylesheet">
<link href="{% static 'rest_framework/docs/img/favicon.ico' %}" rel="shortcut icon">
<style>{{ code_style }}</style>
<script src="{% static 'rest_framework/js/coreapi-0.0.20.js' %}"></script>
<script src="{% url 'api-docs:schema-js' %}"></script>
</head>
<body data-spy="scroll" data-target="#sidebar-nav" data-offset="50">
{% include "rest_framework/docs/sidebar.html" %}
<div class="container" id="main">
<div class="row">
<div class="col-md-12 main-container">
{% include "rest_framework/docs/document.html" %}
</div>
</div>
</div>
{% include "rest_framework/docs/auth/token.html" %}
{% include "rest_framework/docs/auth/basic.html" %}
{% include "rest_framework/docs/auth/session.html" %}
<script src="{% static 'rest_framework/docs/js/jquery-1.10.2.min.js' %}"></script>
<script src="{% static 'rest_framework/docs/js/bootstrap.min.js' %}"></script>
<script src="{% static 'rest_framework/docs/js/jquery.json-view.min.js' %}"></script>
<script src="{% static 'rest_framework/docs/js/api.js' %}"></script>
<script>
{% if user.is_authenticated %}
window.auth = {
'type': 'session',
};
$('#selected-authentication').text('session');
$('#auth-control').children().removeClass('active');
$('#auth-control').find("[data-auth='session']").addClass('active');
{% endif %}
</script>
</body>
</html>
{% load rest_framework %}
<!-- Modal -->
<div class="modal fade api-modal" id="{{ section_key }}_{{ link_key }}_modal" tabindex="-1" role="dialog" aria-labelledby="api explorer modal">
<div class="modal-dialog modal-lg" role="document">
<div class="modal-content">
<div class="modal-header">
<div class="toggle-view hide">
<div class="btn-group" role="group">
<button type="button" data-display-toggle="data" class="btn btn-sm btn-info">Data</button>
<button type="button" data-display-toggle="raw" class="btn btn-sm">Raw</button>
</div>
</div>
<h3 class="modal-title"><i class="fa fa-exchange"></i> {{ link.title|default:link_key }}</h3>
</div>
<form data-key='["{{ section_key }}", "{{ link_key }}"]' class="api-interaction">
<div class="modal-body">
<div class="row">
<div class="col-lg-6 request">
{% form_for_link link %}
</div>
<hr class="hidden-lg hidden-xl" />
<div class="col-lg-6 response" id="response">
<div class="request-awaiting">Awaiting request</div>
<div class="meta hide">
<span class="label label-primary request-method"></span>
<code class="request-url"></code>
<label class="label label-lg response-status-code pull-right"></label>
</div>
<pre class="well response-data hide"></pre>
<pre class="well response-raw hide"><div class="response-raw-request"></div><div class="response-raw-response"></div></pre>
</div>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-default" data-dismiss="modal">Close</button>
<button type="submit" class="btn btn-primary">Send Request</button>
</div>
</form>
</div>
</div>
</div>
{% load rest_framework %}
{% load staticfiles %}
<pre class="highlight javascript hide" data-language="javascript"><code>{% code html %}<!-- Load the JavaScript client library -->
<script src="{% static 'rest_framework/js/coreapi-0.0.20.js' %}"></script>
<script src="{% url 'api-docs:schema-js' %}"></script>{% endcode %}</code></pre>
{% load rest_framework %}
<pre class="highlight javascript hide" data-language="javascript"><code>{% code javascript %}var coreapi = window.coreapi // Loaded by `coreapi.js`
var schema = window.schema // Loaded by `schema.js`
// Initialize a client
var client = new coreapi.Client()
// Interact with the API endpoint
var action = [{% if section_key %}"{{ section_key }}", {% endif %}"{{ link_key }}"]
{% if link.fields %}var params = {
{% for field in link.fields %} {{ field.name }}: ...{% if not loop.last %},{% endif %}
{% endfor %}}
{% endif %}client.action(schema, action{% if link.fields %}, params{% endif %}).then(function(result) {
// Return value is in 'result'
}){% endcode %}</code></pre>
{% load rest_framework %}
<pre class="highlight python hide" data-language="python"><code>{% code bash %}# Install the Python client library
$ pip install coreapi{% endcode %}</code></pre>
{% load rest_framework %}
<pre class="highlight python hide" data-language="python"><code>{% code python %}import coreapi
# Initialize a client & load the schema document
client = coreapi.Client()
schema = client.get("{{ document.url }}"{% if schema_format %}, format="{{ schema_format }}"{% endif %})
# Interact with the API endpoint
action = [{% if section_key %}"{{ section_key }}", {% endif %}"{{ link_key }}"]
{% if link.fields %}params = {
{% for field in link.fields %} "{{ field.name }}": ...{% if not loop.last %},{% endif %}
{% endfor %}}
{% endif %}result = client.action(schema, action{% if link.fields %}, params=params{% endif %}){% endcode %}</code></pre>
{% load rest_framework %}
<pre class="highlight shell" data-language="shell"><code>{% code bash %}# Install the command line client
$ pip install coreapi-cli{% endcode %}</code></pre>
{% load rest_framework %}
<pre class="highlight shell" data-language="shell"><code>{% code bash %}# Load the schema document
$ coreapi get {{ document.url }}{% if schema_format %} --format {{ schema_format }}{% endif %}
# Interact with the API endpoint
$ coreapi action {% if section_key %}{{ section_key }} {% endif %}{{ link_key }}{% for field in link.fields %} -p {{ field.name }}=...{% endfor %}{% endcode %}</code></pre>
{% load rest_framework %}
<div class="row coredocs-link">
<div class="col-md-6 docs-content">
<button
class="btn btn-sm btn-success"
style="float: right; margin-top: 20px"
data-toggle="modal"
data-target="#{{ section_key }}_{{ link_key }}_modal">
<i class="fa fa-exchange"></i> Interact
</button>
<h3 id="{{ section_key }}-{{ link_key }}" class="coredocs-link-title">{{ link.title|default:link_key }} <a href="#{{ section_key }}-{{ link_key }}"><i class="fa fa-link" aria-hidden="true"></i>
</a></h3>
<div class="meta">
<span class="label label-primary">{{ link.action|upper }}</span>
<code>{{ link.url }}</code>
</div>
<p>{% render_markdown link.description %}</p>
{% if link.fields|with_location:'path' %}
<h4>Path Parameters</h4>
<p>The following parameters should be included in the URL path.</p>
<table class="parameters table table-bordered table-striped">
<thead>
<tr><th>Parameter</th><th>Description</th></tr>
</thead>
<tbody>
{% for field in link.fields|with_location:'path' %}
<tr><td class="parameter-name"><code>{{ field.name }}</code>{% if field.required %} <span class="label label-warning">required</span>{% endif %}</td><td>{% if field.schema.description %}{{ field.schema.description }}{% endif %}</td></tr>
{% endfor %}
</tbody>
</table>
{% endif %}
{% if link.fields|with_location:'query' %}
<h4>Query Parameters</h4>
<p>The following parameters should be included as part of a URL query string.</p>
<table class="parameters table table-bordered table-striped">
<thead>
<tr><th>Parameter</th><th>Description</th></tr>
</thead>
<tbody>
{% for field in link.fields|with_location:'query' %}
<tr><td class="parameter-name"><code>{{ field.name }}</code>{% if field.required %} <span class="label label-warning">required</span>{% endif %}</td><td>{% if field.schema.description %}{{ field.schema.description }}{% endif %}</td></tr>
{% endfor %}
</tbody>
</table>
{% endif %}
{% if link.fields|with_location:'header' %}
<h4>Header Parameters</h4>
<p>The following parameters should be included as HTTP headers.</p>
<table class="parameters table table-bordered table-striped">
<thead>
<tr><th>Parameter</th><th>Description</th></tr>
</thead>
<tbody>
{% for field in link.fields|with_location:'header' %}
<tr><td class="parameter-name"><code>{{ field.name }}</code>{% if field.required %} <span class="label label-warning">required</span>{% endif %}</td><td>{% if field.schema.description %}{{ field.schema.description }}{% endif %}</td></tr>
{% endfor %}
</tbody>
</table>
{% endif %}
{% if link.fields|with_location:'body' %}
<h4>Request Body</h4>
<p>The request body should be <code>"{{ link.encoding }}"</code> encoded, and should contain a single item.</p>
<table class="parameters table table-bordered table-striped">
<thead>
<tr><th>Parameter</th><th>Description</th></tr>
</thead>
<tbody>
{% for field in link.fields|with_location:'body' %}
<tr><td class="parameter-name"><code>{{ field.name }}</code>{% if field.required %} <span class="label label-warning">required</span>{% endif %}</td><td>{% if field.schema.description %}{{ field.schema.description }}{% endif %}</td></tr>
{% endfor %}
</tbody>
</table>
{% elif link.fields|with_location:'form' %}
<h4>Request Body</h4>
<p>The request body should be a <code>"{{ link.encoding }}"</code> encoded object, containing the following&nbsp;items.</p>
<table class="parameters table table-bordered table-striped">
<thead>
<tr><th>Parameter</th><th>Description</th></tr>
</thead>
<tbody>
{% for field in link.fields|with_location:'form' %}
<tr><td class="parameter-name"><code>{{ field.name }}</code>{% if field.required %} <span class="label label-warning">required</span>{% endif %}</td><td>{% if field.schema.description %}{{ field.schema.description }}{% endif %}</td></tr>
{% endfor %}
</tbody>
</table>
{% endif %}
</div>
<div class="col-md-6 code-samples">
{% if 'shell' in langs %}{% include "rest_framework/docs/langs/shell.html" %}{% endif %}
{% if 'python' in langs %}{% include "rest_framework/docs/langs/python.html" %}{% endif %}
{% if 'javascript' in langs %}{% include "rest_framework/docs/langs/javascript.html" %}{% endif %}
</div>
</div>
{% include "rest_framework/docs/interact.html" with link=link schema=schema %}
<div class="sidebar">
<h3 class="brand"><a href="#">{{ document.title }}</a></h3>
<i class="fa fa-bars fa-2x toggle-btn" data-toggle="collapse" data-target="#menu-content"></i>
<div class="menu-list">
<ul id="menu-content" class="menu-content collapse out">
{% for section_key, section in document.data.items %}
<li data-toggle="collapse" data-target="#{{ section_key }}-dropdown" class="collapsed">
<a><i class="fa fa-dot-circle-o fa-lg"></i> {{ section_key }} <span class="arrow"></span></a>
</li>
<ul class="sub-menu collapse" id="{{ section_key }}-dropdown">
{% for link_key, link in section.links.items %}
<li><a href="#{{ section_key }}-{{ link_key }}">{{ link.title|default:link_key }}</a></li>
{% endfor %}
</ul>
{% endfor %}
</ul>
<ul class="menu-list menu-list-bottom">
<li data-toggle="collapse" data-target="#auth-control" class="collapsed">
<a><i class="fa fa-user fa-lg"></i> Authentication</a> <span id="selected-authentication">{% if user.is_authenticated %}session{% else %}none{% endif %}</span>
</li>
<ul class="sub-menu collapse out" id="auth-control">
<li data-auth="none" {% if not user.is_authenticated %}class="active"{% endif %}><a href="#" data-language="none">none</a></li>
<li data-auth="token" data-toggle="modal" data-target="#auth_token_modal"><a href="#">header</a></li>
<li data-auth="basic" data-toggle="modal" data-target="#auth_basic_modal"><a href="#">basic</a></li>
<li data-auth="session" data-toggle="modal" data-target="#auth_session_modal" {% if user.is_authenticated %}class="active"{% endif %}><a href="#">session</a></li>
</ul>
<li data-toggle="collapse" data-target="#language-control" class="collapsed">
<a><i class="fa fa-code fa-lg"></i> Source Code</a> <span id="selected-language">shell</span>
</li>
<ul class="sub-menu collapse out" id="language-control">
<li class="active"><a href="#" data-language="shell">shell</a></li>
<li><a href="#" data-language="javascript">javascript</a></li>
<li><a href="#" data-language="python">python</a></li>
</ul>
</ul>
</div>
</div>
let codec = new window.coreapi.codecs.CoreJSONCodec()
let coreJSON = window.atob('{{ schema }}')
window.schema = codec.decode(coreJSON)
...@@ -2,23 +2,99 @@ from __future__ import absolute_import, unicode_literals ...@@ -2,23 +2,99 @@ from __future__ import absolute_import, unicode_literals
import re import re
from collections import OrderedDict
from django import template from django import template
from django.template import loader from django.template import loader
from django.utils import six from django.utils import six
from django.utils.encoding import force_text, iri_to_uri from django.utils.encoding import force_text, iri_to_uri
from django.utils.html import escape, format_html, smart_urlquote from django.utils.html import escape, format_html, smart_urlquote
from django.utils.safestring import SafeData, mark_safe from django.utils.safestring import SafeData, mark_safe
from markdown.extensions.fenced_code import FencedBlockPreprocessor
from rest_framework.compat import NoReverseMatch, reverse, template_render from rest_framework.compat import (
NoReverseMatch, markdown, reverse, template_render
)
from rest_framework.renderers import HTMLFormRenderer from rest_framework.renderers import HTMLFormRenderer
from rest_framework.utils.urls import replace_query_param from rest_framework.utils.urls import replace_query_param
register = template.Library() register = template.Library()
# Regex for adding classes to html snippets # Regex for adding classes to html snippets
class_re = re.compile(r'(?<=class=["\'])(.*)(?=["\'])') class_re = re.compile(r'(?<=class=["\'])(.*)(?=["\'])')
class CustomFencedBlockPreprocessor(FencedBlockPreprocessor):
CODE_WRAP = '<pre%s><code>%s</code></pre>'
LANG_TAG = ' class="highlight %s"'
class FencedCodeExtension(markdown.Extension):
def extendMarkdown(self, md, md_globals):
""" Add FencedBlockPreprocessor to the Markdown instance. """
md.registerExtension(self)
md.preprocessors.add('fenced_code_block',
CustomFencedBlockPreprocessor(md),
">normalize_whitespace")
@register.tag(name='code')
def highlight_code(parser, token):
code = token.split_contents()[-1]
nodelist = parser.parse(('endcode',))
parser.delete_first_token()
return CodeNode(code, nodelist)
class CodeNode(template.Node):
style = 'emacs'
def __init__(self, lang, code):
self.lang = lang
self.nodelist = code
def render(self, context):
from pygments import highlight
from pygments.lexers import get_lexer_by_name
from pygments.formatters import HtmlFormatter
body = self.nodelist.render(context)
lexer = get_lexer_by_name(self.lang, stripall=False)
formatter = HtmlFormatter(nowrap=True, style=self.style)
code = highlight(body, lexer, formatter)
return code
@register.filter()
def with_location(fields, location):
return [
field for field in fields
if field.location == location
]
@register.simple_tag
def form_for_link(link):
import coreschema
properties = OrderedDict([
(field.name, field.schema or coreschema.String())
for field in link.fields
])
required = [
field.name
for field in link.fields
if field.required
]
schema = coreschema.Object(properties=properties, required=required)
return coreschema.render_to_form(schema)
@register.simple_tag
def render_markdown(markdown_text):
return markdown.markdown(markdown_text, extensions=[FencedCodeExtension(), "tables"])
@register.simple_tag @register.simple_tag
def get_pagination_html(pager): def get_pagination_html(pager):
return pager.to_html() return pager.to_html()
...@@ -54,6 +130,22 @@ def optional_login(request): ...@@ -54,6 +130,22 @@ def optional_login(request):
@register.simple_tag @register.simple_tag
def optional_docs_login(request):
"""
Include a login snippet if REST framework's login view is in the URLconf.
"""
try:
login_url = reverse('rest_framework:login')
except NoReverseMatch:
return 'log in'
snippet = "<a href='{href}?next={next}'>log in</a>"
snippet = format_html(snippet, href=login_url, next=escape(request.path))
return mark_safe(snippet)
@register.simple_tag
def optional_logout(request, user): def optional_logout(request, user):
""" """
Include a logout snippet if REST framework's logout view is in the URLconf. Include a logout snippet if REST framework's logout view is in the URLconf.
......
...@@ -8,7 +8,7 @@ from django.conf.urls import url ...@@ -8,7 +8,7 @@ from django.conf.urls import url
from django.http import HttpResponse from django.http import HttpResponse
from django.test import override_settings from django.test import override_settings
from rest_framework.compat import coreapi from rest_framework.compat import coreapi, coreschema
from rest_framework.parsers import FileUploadParser from rest_framework.parsers import FileUploadParser
from rest_framework.renderers import CoreJSONRenderer from rest_framework.renderers import CoreJSONRenderer
from rest_framework.response import Response from rest_framework.response import Response
...@@ -25,10 +25,10 @@ def get_schema(): ...@@ -25,10 +25,10 @@ def get_schema():
'headers': coreapi.Link('/headers/'), 'headers': coreapi.Link('/headers/'),
'location': { 'location': {
'query': coreapi.Link('/example/', fields=[ 'query': coreapi.Link('/example/', fields=[
coreapi.Field(name='example', description='example field') coreapi.Field(name='example', schema=coreschema.String(description='example field'))
]), ]),
'form': coreapi.Link('/example/', action='post', fields=[ 'form': coreapi.Link('/example/', action='post', fields=[
coreapi.Field(name='example'), coreapi.Field(name='example')
]), ]),
'body': coreapi.Link('/example/', action='post', fields=[ 'body': coreapi.Link('/example/', action='post', fields=[
coreapi.Field(name='example', location='body') coreapi.Field(name='example', location='body')
...@@ -199,7 +199,7 @@ class APIClientTests(APITestCase): ...@@ -199,7 +199,7 @@ class APIClientTests(APITestCase):
assert schema.title == 'Example API' assert schema.title == 'Example API'
assert schema.url == 'https://api.example.com/' assert schema.url == 'https://api.example.com/'
assert schema['simple_link'].description == 'example link' assert schema['simple_link'].description == 'example link'
assert schema['location']['query'].fields[0].description == 'example field' assert schema['location']['query'].fields[0].schema.description == 'example field'
data = client.action(schema, ['simple_link']) data = client.action(schema, ['simple_link'])
expected = { expected = {
'method': 'GET', 'method': 'GET',
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment