Merge pull request #21207 from edx/nedbat/api-docs

REST API docs
This commit is contained in:
Ned Batchelder
2019-09-18 16:42:10 -04:00
committed by GitHub
29 changed files with 6409 additions and 123 deletions

View File

@@ -7,8 +7,10 @@ import six
from rest_framework import serializers
from openedx.core.lib.api.serializers import CourseKeyField, UsageKeyField
from openedx.core.openapi import is_schema_request
from . import DEFAULT_FIELDS
from . import DEFAULT_FIELDS, OPTIONAL_FIELDS
from .models import Bookmark
@@ -16,12 +18,27 @@ class BookmarkSerializer(serializers.ModelSerializer):
"""
Serializer for the Bookmark model.
"""
id = serializers.SerializerMethodField() # pylint: disable=invalid-name
course_id = CourseKeyField(source='course_key')
usage_id = UsageKeyField(source='usage_key')
id = serializers.SerializerMethodField( # pylint: disable=invalid-name
help_text=u"The identifier string for the bookmark: {user_id},{usage_id}.",
)
course_id = CourseKeyField(
source='course_key',
help_text=u"The identifier string of the bookmark's course.",
)
usage_id = UsageKeyField(
source='usage_key',
help_text=u"The identifier string of the bookmark's XBlock.",
)
block_type = serializers.ReadOnlyField(source='usage_key.block_type')
display_name = serializers.ReadOnlyField()
path = serializers.SerializerMethodField()
display_name = serializers.ReadOnlyField(
help_text=u"Display name of the XBlock.",
)
path = serializers.SerializerMethodField(
help_text=u"""
List of dicts containing {"usage_id": <usage-id>, display_name:<display-name>}
for the XBlocks from the top of the course tree till the parent of the bookmarked XBlock.
""",
)
def __init__(self, *args, **kwargs):
# Don't pass the 'fields' arg up to the superclass
@@ -34,6 +51,11 @@ class BookmarkSerializer(serializers.ModelSerializer):
# Drop any fields that are not specified in the `fields` argument.
required_fields = set(fields)
if 'request' in kwargs['context'] and is_schema_request(kwargs['context']['request']):
# We are serving the schema: include everything
required_fields.update(OPTIONAL_FIELDS)
all_fields = set(self.fields.keys())
for field_name in all_fields - required_fields:
self.fields.pop(field_name)

View File

@@ -11,6 +11,7 @@ import logging
import eventtracking
from django.conf import settings
from django.core.exceptions import ObjectDoesNotExist
from django.utils.decorators import method_decorator
from django.utils.translation import ugettext as _
from django.utils.translation import ugettext_noop
from edx_rest_framework_extensions.paginators import DefaultPagination
@@ -26,6 +27,7 @@ from rest_framework_oauth.authentication import OAuth2Authentication
from openedx.core.djangoapps.bookmarks.api import BookmarksLimitReachedError
from openedx.core.lib.api.permissions import IsUserInUrl
from openedx.core.lib.url_utils import unquote_slashes
from openedx.core.openapi import swagger_auto_schema, openapi
from xmodule.modulestore.exceptions import ItemNotFoundError
from . import DEFAULT_FIELDS, OPTIONAL_FIELDS, api
@@ -95,68 +97,47 @@ class BookmarksViewMixin(object):
)
class BookmarksListView(ListCreateAPIView, BookmarksViewMixin):
"""
**Use Case**
@method_decorator(name='get', decorator=swagger_auto_schema(
operation_summary="Get a paginated list of bookmarks for a user.",
operation_description=u"""
The list can be filtered by passing parameter "course_id=<course_id>"
to only include bookmarks from a particular course.
* Get a paginated list of bookmarks for a user.
The bookmarks are always sorted in descending order by creation date.
The list can be filtered by passing parameter "course_id=<course_id>"
to only include bookmarks from a particular course.
Each page in the list contains 10 bookmarks by default. The page
size can be altered by passing parameter "page_size=<page_size>".
The bookmarks are always sorted in descending order by creation date.
To include the optional fields pass the values in "fields" parameter
as a comma separated list. Possible values are:
Each page in the list contains 10 bookmarks by default. The page
size can be altered by passing parameter "page_size=<page_size>".
* "display_name"
* "path"
To include the optional fields pass the values in "fields" parameter
as a comma separated list. Possible values are:
* "display_name"
* "path"
* Create a new bookmark for a user.
The POST request only needs to contain one parameter "usage_id".
Http400 is returned if the format of the request is not correct,
the usage_id is invalid or a block corresponding to the usage_id
could not be found.
**Example Requests**
# Example Requests
GET /api/bookmarks/v1/bookmarks/?course_id={course_id1}&fields=display_name,path
""",
manual_parameters=[
openapi.Parameter(
'course_id',
openapi.IN_QUERY,
type=openapi.TYPE_STRING,
description="The id of the course to limit the list",
),
openapi.Parameter(
'fields',
openapi.IN_QUERY,
type=openapi.TYPE_STRING,
description="""
The fields to return: display_name, path.
""",
),
],
))
class BookmarksListView(ListCreateAPIView, BookmarksViewMixin):
"""REST endpoints for lists of bookmarks."""
POST /api/bookmarks/v1/bookmarks/
Request data: {"usage_id": <usage-id>}
**Response Values**
* count: The number of bookmarks in a course.
* next: The URI to the next page of bookmarks.
* previous: The URI to the previous page of bookmarks.
* num_pages: The number of pages listing bookmarks.
* results: A list of bookmarks returned. Each collection in the list
contains these fields.
* id: String. The identifier string for the bookmark: {user_id},{usage_id}.
* course_id: String. The identifier string of the bookmark's course.
* usage_id: String. The identifier string of the bookmark's XBlock.
* display_name: String. (optional) Display name of the XBlock.
* path: List. (optional) List of dicts containing {"usage_id": <usage-id>, display_name:<display-name>}
for the XBlocks from the top of the course tree till the parent of the bookmarked XBlock.
* created: ISO 8601 String. The timestamp of bookmark's creation.
"""
authentication_classes = (OAuth2Authentication, SessionAuthentication)
pagination_class = BookmarksPagination
permission_classes = (permissions.IsAuthenticated,)
@@ -220,11 +201,24 @@ class BookmarksListView(ListCreateAPIView, BookmarksViewMixin):
return page
def post(self, request):
"""
POST /api/bookmarks/v1/bookmarks/
Request data: {"usage_id": "<usage-id>"}
"""
@swagger_auto_schema(
operation_summary="Create a new bookmark for a user.",
operation_description=u"""
The POST request only needs to contain one parameter "usage_id".
Http400 is returned if the format of the request is not correct,
the usage_id is invalid or a block corresponding to the usage_id
could not be found.
# Example Requests
POST /api/bookmarks/v1/bookmarks/
Request data: {"usage_id": <usage-id>}
""",
)
def post(self, request, *unused_args, **unused_kwargs):
"""Create a new bookmark for a user."""
if not request.data:
return self.error_response(ugettext_noop(u'No data provided.'), DEFAULT_USER_MESSAGE)
@@ -320,6 +314,15 @@ class BookmarksDetailView(APIView, BookmarksViewMixin):
log.error(error_message)
return self.error_response(error_message, error_status=status.HTTP_404_NOT_FOUND)
@swagger_auto_schema(
operation_summary="Get a specific bookmark for a user.",
operation_description=u"""
# Example Requests
GET /api/bookmarks/v1/bookmarks/{username},{usage_id}/?fields=display_name,path
""",
)
def get(self, request, username=None, usage_id=None):
"""
GET /api/bookmarks/v1/bookmarks/{username},{usage_id}?fields=display_name,path

View File

@@ -2,9 +2,73 @@
Open API support.
"""
from rest_framework import permissions
from drf_yasg.views import get_schema_view
import textwrap
from drf_yasg import openapi
from drf_yasg.generators import OpenAPISchemaGenerator
from drf_yasg.utils import swagger_auto_schema as drf_swagger_auto_schema
from drf_yasg.views import get_schema_view
from rest_framework import permissions
# -- Code that will eventually be in another openapi-helpers repo -------------
class ApiSchemaGenerator(OpenAPISchemaGenerator):
"""A schema generator for /api/*
Only includes endpoints in the /api/* url tree, and sets the path prefix
appropriately.
"""
def get_endpoints(self, request):
endpoints = super(ApiSchemaGenerator, self).get_endpoints(request)
subpoints = {p: v for p, v in endpoints.items() if p.startswith("/api/")}
return subpoints
def determine_path_prefix(self, paths):
return "/api/"
def dedent(text):
"""
Dedent multi-line text nicely.
An initial empty line is ignored so that triple-quoted strings don't need
to start with a backslash.
"""
if "\n" in text:
first, rest = text.split("\n", 1)
if not first.strip():
# First line is blank, discard it.
text = rest
return textwrap.dedent(text)
def swagger_auto_schema(**kwargs):
"""
Decorator for documenting an OpenAPI endpoint.
Identical to `drf_yasg.utils.swagger_auto_schema`__ except that
description fields will be dedented properly. All description fields
should be in Markdown.
__ https://drf-yasg.readthedocs.io/en/stable/drf_yasg.html#drf_yasg.utils.swagger_auto_schema
"""
if 'operation_description' in kwargs:
kwargs['operation_description'] = dedent(kwargs['operation_description'])
for param in kwargs.get('manual_parameters', ()):
param.description = dedent(param.description)
return drf_swagger_auto_schema(**kwargs)
def is_schema_request(request):
"""Is this request serving an OpenAPI schema?"""
return request.query_params.get('format') == 'openapi'
# -----------------------------------------------------
openapi_info = openapi.Info(
title="Open edX API",
@@ -17,6 +81,7 @@ openapi_info = openapi.Info(
schema_view = get_schema_view(
openapi_info,
generator_class=ApiSchemaGenerator,
public=True,
permission_classes=(permissions.AllowAny,),
)