Merge pull request #21816 from regisb/regisb/simplify-swagger-auto-schema
Regisb/simplify swagger auto schema
This commit is contained in:
2
Makefile
2
Makefile
@@ -24,7 +24,7 @@ SWAGGER = docs/swagger.yaml
|
||||
docs: api-docs guides ## build all the developer documentation for this repository
|
||||
|
||||
swagger: ## generate the swagger.yaml file
|
||||
DJANGO_SETTINGS_MODULE=docs.docs_settings python manage.py lms generate_swagger --generator-class=openedx.core.openapi.ApiSchemaGenerator -o $(SWAGGER)
|
||||
DJANGO_SETTINGS_MODULE=docs.docs_settings python manage.py lms generate_swagger --generator-class=openedx.core.apidocs.ApiSchemaGenerator -o $(SWAGGER)
|
||||
|
||||
api-docs: swagger ## build the REST api docs
|
||||
rm -f docs/api/gen/*
|
||||
|
||||
@@ -17,7 +17,7 @@ import openedx.core.djangoapps.lang_pref.views
|
||||
from cms.djangoapps.contentstore.views.organization import OrganizationListView
|
||||
from openedx.core.djangoapps.password_policy import compliance as password_policy_compliance
|
||||
from openedx.core.djangoapps.password_policy.forms import PasswordPolicyAwareAdminAuthForm
|
||||
from openedx.core.openapi import schema_view
|
||||
from openedx.core.apidocs import schema_view
|
||||
|
||||
|
||||
django_autodiscover()
|
||||
|
||||
@@ -112,20 +112,7 @@ paths:
|
||||
\ passing parameter \"page_size=<page_size>\".\n\nTo include the optional\
|
||||
\ fields pass the values in \"fields\" parameter\nas a comma separated list.\
|
||||
\ Possible values are:\n\n* \"display_name\"\n* \"path\"\n\n# Example Requests\n\
|
||||
\nGET /api/bookmarks/v1/bookmarks/?course_id={course_id1}&fields=display_name,path\n\
|
||||
\n# Response Values\n\n* count: The number of bookmarks in a course.\n\n*\
|
||||
\ next: The URI to the next page of bookmarks.\n\n* previous: The URI to the\
|
||||
\ previous page of bookmarks.\n\n* num_pages: The number of pages listing\
|
||||
\ bookmarks.\n\n* results: A list of bookmarks returned. Each collection\
|
||||
\ in the list\n contains these fields.\n\n * id: String. The identifier\
|
||||
\ string for the bookmark: {user_id},{usage_id}.\n\n * course_id: String.\
|
||||
\ The identifier string of the bookmark's course.\n\n * usage_id: String.\
|
||||
\ The identifier string of the bookmark's XBlock.\n\n * display_name: String.\
|
||||
\ (optional) Display name of the XBlock.\n\n * path: List. (optional) List\
|
||||
\ of dicts containing {\"usage_id\": <usage-id>, display_name:<display-name>}\n\
|
||||
\ for the XBlocks from the top of the course tree till the parent of\
|
||||
\ the bookmarked XBlock.\n\n * created: ISO 8601 String. The timestamp\
|
||||
\ of bookmark's creation.\n\n"
|
||||
\nGET /api/bookmarks/v1/bookmarks/?course_id={course_id1}&fields=display_name,path"
|
||||
parameters:
|
||||
- name: page
|
||||
in: query
|
||||
@@ -139,7 +126,7 @@ paths:
|
||||
type: integer
|
||||
- name: course_id
|
||||
in: query
|
||||
description: The id of the course, of course
|
||||
description: The id of the course to limit the list
|
||||
type: string
|
||||
- name: fields
|
||||
in: query
|
||||
@@ -157,7 +144,7 @@ paths:
|
||||
.\n\nHttp400 is returned if the format of the request is not correct,\nthe\
|
||||
\ usage_id is invalid or a block corresponding to the usage_id\ncould not\
|
||||
\ be found.\n\n# Example Requests\n\nPOST /api/bookmarks/v1/bookmarks/\nRequest\
|
||||
\ data: {\"usage_id\": <usage-id>}\n\n"
|
||||
\ data: {\"usage_id\": <usage-id>}"
|
||||
parameters: []
|
||||
responses:
|
||||
'201':
|
||||
@@ -341,12 +328,10 @@ paths:
|
||||
get:
|
||||
operationId: certificates_v0_certificates_read
|
||||
summary: Get a paginated list of bookmarks for a user.
|
||||
description: "**Use Case**\n\n * Get the list of viewable course certificates\
|
||||
\ for a specific user.\n\n**Example Request**\n\n GET /api/certificates/v0/certificates/{username}\n\
|
||||
\n**GET Parameters**\n\n A GET request must include the following parameters.\n\
|
||||
\n * username: A string representation of an user's username.\n\n**GET\
|
||||
\ Response Values**\n\n If the request for information about the user's\
|
||||
\ certificates is successful,\n an HTTP 200 \"OK\" response is returned.\n\
|
||||
description: "**Use Case**\n\nGet the list of viewable course certificates for\
|
||||
\ a specific user.\n\n**Example Request**\n\nGET /api/certificates/v0/certificates/{username}\n\
|
||||
\n**GET Response Values**\n\n If the request for information about the\
|
||||
\ user's certificates is successful,\n an HTTP 200 \"OK\" response is returned.\n\
|
||||
\n The HTTP 200 response contains a list of dicts with the following keys/values.\n\
|
||||
\n * username: A string representation of an user's username passed in\
|
||||
\ the request.\n\n * course_id: A string representation of a Course ID.\n\
|
||||
@@ -363,8 +348,13 @@ paths:
|
||||
: \"edX/DemoX/Demo_Course\",\n \"certificate_type\": \"verified\",\n\
|
||||
\ \"created_date\": \"2015-12-03T13:14:28+0000\",\n \"status\"\
|
||||
: \"downloadable\",\n \"is_passing\": true,\n \"download_url\"\
|
||||
: \"http://www.example.com/cert.pdf\",\n \"grade\": \"0.98\"\n }]\n"
|
||||
parameters: []
|
||||
: \"http://www.example.com/cert.pdf\",\n \"grade\": \"0.98\"\n }]"
|
||||
parameters:
|
||||
- name: username
|
||||
in: path
|
||||
description: The users to get certificates for
|
||||
type: string
|
||||
required: true
|
||||
responses:
|
||||
'200':
|
||||
description: ''
|
||||
@@ -709,7 +699,14 @@ paths:
|
||||
/course_goals/v0/course_goals/:
|
||||
get:
|
||||
operationId: course_goals_v0_course_goals_list
|
||||
description: description from swagger_auto_schema via method_decorator
|
||||
summary: API calls to create and update a course goal.
|
||||
description: "Validates incoming data to ensure that course_key maps to an actual\n\
|
||||
course and that the goal_key is a valid option.\n\n**Use Case**\n * Create\
|
||||
\ a new goal for a user.\n * Update an existing goal for a user\n\n**Example\
|
||||
\ Requests**\n POST /api/course_goals/v0/course_goals/\n Request\
|
||||
\ data: {\"course_key\": <course-key>, \"goal_key\": \"<goal-key>\", \"user\"\
|
||||
: \"<username>\"}\n\nReturns Http400 response if the course_key does not map\
|
||||
\ to a known\ncourse or if the goal_key does not map to a valid goal key."
|
||||
parameters:
|
||||
- name: page
|
||||
in: query
|
||||
@@ -2704,6 +2701,18 @@ paths:
|
||||
required: true
|
||||
type: string
|
||||
pattern: '[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}'
|
||||
/experiments/v0/custom/REV-934/:
|
||||
get:
|
||||
operationId: experiments_v0_custom_REV-934_list
|
||||
description: Return the if the course should be upsold in the mobile app, if
|
||||
the user has appropriate permissions.
|
||||
parameters: []
|
||||
responses:
|
||||
'200':
|
||||
description: ''
|
||||
tags:
|
||||
- experiments
|
||||
parameters: []
|
||||
/experiments/v0/data/:
|
||||
get:
|
||||
operationId: experiments_v0_data_list
|
||||
@@ -3577,7 +3586,7 @@ paths:
|
||||
/program_enrollments/v1/programs/{program_uuid}/courses/{course_id}/enrollments/:
|
||||
get:
|
||||
operationId: program_enrollments_v1_programs_courses_enrollments_list
|
||||
description: Defines the GET list endpoint for ProgramCourseEnrollment objects.
|
||||
description: Get a list of students enrolled in a course within a program.
|
||||
parameters:
|
||||
- name: cursor
|
||||
in: query
|
||||
@@ -3690,7 +3699,7 @@ paths:
|
||||
- program_enrollments
|
||||
put:
|
||||
operationId: program_enrollments_v1_programs_enrollments_update
|
||||
description: Create/modify program enrollments for a list of learners
|
||||
description: Create/update program enrollments for a list of learners
|
||||
parameters: []
|
||||
responses:
|
||||
'200':
|
||||
@@ -3699,7 +3708,7 @@ paths:
|
||||
- program_enrollments
|
||||
patch:
|
||||
operationId: program_enrollments_v1_programs_enrollments_partial_update
|
||||
description: Modify program enrollments for a list of learners
|
||||
description: Update program enrollments for a list of learners
|
||||
parameters: []
|
||||
responses:
|
||||
'200':
|
||||
@@ -4592,6 +4601,129 @@ paths:
|
||||
required: true
|
||||
type: string
|
||||
pattern: ^[a-zA-Z0-9\-_]*$
|
||||
/xblock/v2/xblocks/{usage_key_str}/:
|
||||
get:
|
||||
operationId: xblock_v2_xblocks_read
|
||||
description: Get metadata about the specified block.
|
||||
parameters: []
|
||||
responses:
|
||||
'200':
|
||||
description: ''
|
||||
tags:
|
||||
- xblock
|
||||
parameters:
|
||||
- name: usage_key_str
|
||||
in: path
|
||||
required: true
|
||||
type: string
|
||||
/xblock/v2/xblocks/{usage_key_str}/handler/{user_id}-{secure_token}/{handler_name}/{suffix}:
|
||||
get:
|
||||
operationId: xblock_v2_xblocks_handler_read
|
||||
description: Run an XBlock's handler and return the result
|
||||
parameters: []
|
||||
responses:
|
||||
'200':
|
||||
description: ''
|
||||
tags:
|
||||
- xblock
|
||||
post:
|
||||
operationId: xblock_v2_xblocks_handler_create
|
||||
description: Run an XBlock's handler and return the result
|
||||
parameters: []
|
||||
responses:
|
||||
'201':
|
||||
description: ''
|
||||
tags:
|
||||
- xblock
|
||||
put:
|
||||
operationId: xblock_v2_xblocks_handler_update
|
||||
description: Run an XBlock's handler and return the result
|
||||
parameters: []
|
||||
responses:
|
||||
'200':
|
||||
description: ''
|
||||
tags:
|
||||
- xblock
|
||||
patch:
|
||||
operationId: xblock_v2_xblocks_handler_partial_update
|
||||
description: Run an XBlock's handler and return the result
|
||||
parameters: []
|
||||
responses:
|
||||
'200':
|
||||
description: ''
|
||||
tags:
|
||||
- xblock
|
||||
delete:
|
||||
operationId: xblock_v2_xblocks_handler_delete
|
||||
description: Run an XBlock's handler and return the result
|
||||
parameters: []
|
||||
responses:
|
||||
'204':
|
||||
description: ''
|
||||
tags:
|
||||
- xblock
|
||||
parameters:
|
||||
- name: handler_name
|
||||
in: path
|
||||
required: true
|
||||
type: string
|
||||
- name: secure_token
|
||||
in: path
|
||||
required: true
|
||||
type: string
|
||||
- name: suffix
|
||||
in: path
|
||||
required: true
|
||||
type: string
|
||||
- name: usage_key_str
|
||||
in: path
|
||||
required: true
|
||||
type: string
|
||||
- name: user_id
|
||||
in: path
|
||||
required: true
|
||||
type: string
|
||||
/xblock/v2/xblocks/{usage_key_str}/handler_url/{handler_name}/:
|
||||
get:
|
||||
operationId: xblock_v2_xblocks_handler_url_read
|
||||
summary: "Get an absolute URL which can be used (without any authentication)\
|
||||
\ to call\nthe given XBlock handler."
|
||||
description: The URL will expire but is guaranteed to be valid for a minimum
|
||||
of 2 days.
|
||||
parameters: []
|
||||
responses:
|
||||
'200':
|
||||
description: ''
|
||||
tags:
|
||||
- xblock
|
||||
parameters:
|
||||
- name: handler_name
|
||||
in: path
|
||||
required: true
|
||||
type: string
|
||||
- name: usage_key_str
|
||||
in: path
|
||||
required: true
|
||||
type: string
|
||||
/xblock/v2/xblocks/{usage_key_str}/view/{view_name}/:
|
||||
get:
|
||||
operationId: xblock_v2_xblocks_view_read
|
||||
description: Get the HTML, JS, and CSS needed to render the given XBlock.
|
||||
parameters: []
|
||||
responses:
|
||||
'200':
|
||||
description: ''
|
||||
tags:
|
||||
- xblock
|
||||
parameters:
|
||||
- name: usage_key_str
|
||||
in: path
|
||||
required: true
|
||||
type: string
|
||||
- name: view_name
|
||||
in: path
|
||||
required: true
|
||||
type: string
|
||||
definitions:
|
||||
BadgeClass:
|
||||
title: Badge class
|
||||
|
||||
@@ -5,7 +5,6 @@ import logging
|
||||
|
||||
import six
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.utils.decorators import method_decorator
|
||||
from edx_rest_framework_extensions import permissions
|
||||
from edx_rest_framework_extensions.auth.jwt.authentication import JwtAuthentication
|
||||
from edx_rest_framework_extensions.auth.session.authentication import SessionAuthenticationAllowInactiveUser
|
||||
@@ -21,7 +20,7 @@ from openedx.core.djangoapps.certificates.api import certificates_viewable_for_c
|
||||
from openedx.core.djangoapps.content.course_overviews.models import CourseOverview
|
||||
from openedx.core.djangoapps.user_api.accounts.api import visible_fields
|
||||
from openedx.core.lib.api.authentication import OAuth2AuthenticationAllowInactiveUser
|
||||
from openedx.core.openapi import swagger_auto_schema, openapi
|
||||
from openedx.core import apidocs
|
||||
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
@@ -134,9 +133,37 @@ class CertificatesDetailView(GenericAPIView):
|
||||
)
|
||||
|
||||
|
||||
@method_decorator(name='get', decorator=swagger_auto_schema(
|
||||
operation_summary="Get a paginated list of bookmarks for a user.",
|
||||
operation_description=u"""\
|
||||
class CertificatesListView(GenericAPIView):
|
||||
"""REST API endpoints for listing certificates."""
|
||||
authentication_classes = (
|
||||
JwtAuthentication,
|
||||
OAuth2AuthenticationAllowInactiveUser,
|
||||
SessionAuthenticationAllowInactiveUser,
|
||||
)
|
||||
|
||||
permission_classes = (
|
||||
C(IsAuthenticated) & (
|
||||
C(permissions.NotJwtRestrictedApplication) |
|
||||
(
|
||||
C(permissions.JwtRestrictedApplication) &
|
||||
permissions.JwtHasScope &
|
||||
permissions.JwtHasUserFilterForRequestedUser
|
||||
)
|
||||
),
|
||||
)
|
||||
|
||||
required_scopes = ['certificates:read']
|
||||
|
||||
@apidocs.schema(parameters=[
|
||||
apidocs.string_parameter(
|
||||
'username',
|
||||
apidocs.ParameterLocation.PATH,
|
||||
description="The users to get certificates for",
|
||||
)
|
||||
])
|
||||
def get(self, request, username):
|
||||
"""Get a paginated list of bookmarks for a user.
|
||||
|
||||
**Use Case**
|
||||
|
||||
Get the list of viewable course certificates for a specific user.
|
||||
@@ -185,38 +212,7 @@ class CertificatesDetailView(GenericAPIView):
|
||||
"download_url": "http://www.example.com/cert.pdf",
|
||||
"grade": "0.98"
|
||||
}]
|
||||
""",
|
||||
manual_parameters=[
|
||||
openapi.Parameter(
|
||||
'username',
|
||||
openapi.IN_PATH,
|
||||
type=openapi.TYPE_STRING,
|
||||
description="The users to get certificates for",
|
||||
),
|
||||
],
|
||||
))
|
||||
class CertificatesListView(GenericAPIView):
|
||||
"""REST API endpoints for listing certificates."""
|
||||
authentication_classes = (
|
||||
JwtAuthentication,
|
||||
OAuth2AuthenticationAllowInactiveUser,
|
||||
SessionAuthenticationAllowInactiveUser,
|
||||
)
|
||||
|
||||
permission_classes = (
|
||||
C(IsAuthenticated) & (
|
||||
C(permissions.NotJwtRestrictedApplication) |
|
||||
(
|
||||
C(permissions.JwtRestrictedApplication) &
|
||||
permissions.JwtHasScope &
|
||||
permissions.JwtHasUserFilterForRequestedUser
|
||||
)
|
||||
),
|
||||
)
|
||||
|
||||
required_scopes = ['certificates:read']
|
||||
|
||||
def get(self, request, username):
|
||||
"""
|
||||
user_certs = []
|
||||
if self._viewable_by_requestor(request, username):
|
||||
for user_cert in self._get_certificates_for_user(username):
|
||||
|
||||
@@ -2516,7 +2516,7 @@ REST_FRAMEWORK = {
|
||||
}
|
||||
|
||||
SWAGGER_SETTINGS = {
|
||||
'DEFAULT_INFO': 'openedx.core.openapi.openapi_info',
|
||||
'DEFAULT_INFO': 'openedx.core.apidocs.default_info',
|
||||
}
|
||||
|
||||
# How long to cache OpenAPI schemas and UI, in seconds.
|
||||
|
||||
@@ -45,7 +45,7 @@ from openedx.core.djangoapps.programs.models import ProgramsApiConfig
|
||||
from openedx.core.djangoapps.self_paced.models import SelfPacedConfiguration
|
||||
from openedx.core.djangoapps.site_configuration import helpers as configuration_helpers
|
||||
from openedx.core.djangoapps.verified_track_content import views as verified_track_content_views
|
||||
from openedx.core.openapi import schema_view
|
||||
from openedx.core.apidocs import schema_view
|
||||
from openedx.features.enterprise_support.api import enterprise_enabled
|
||||
from static_template_view import views as static_template_view_views
|
||||
from staticbook import views as staticbook_views
|
||||
|
||||
162
openedx/core/apidocs.py
Normal file
162
openedx/core/apidocs.py
Normal file
@@ -0,0 +1,162 @@
|
||||
"""
|
||||
Open API support.
|
||||
"""
|
||||
|
||||
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 schema(parameters=None):
|
||||
"""
|
||||
Decorator for documenting an API endpoint.
|
||||
|
||||
The operation summary and description are taken from the function docstring. All
|
||||
description fields should be in Markdown and will be automatically dedented.
|
||||
|
||||
Args:
|
||||
parameters (Parameter list): each object may be conveniently defined with the
|
||||
`parameter` function.
|
||||
|
||||
This is heavily inspired from the the `drf_yasg.utils.swagger_auto_schema`__
|
||||
decorator, but callers do not need to know about this abstraction.
|
||||
|
||||
__ https://drf-yasg.readthedocs.io/en/stable/drf_yasg.html#drf_yasg.utils.swagger_auto_schema
|
||||
"""
|
||||
for param in parameters or ():
|
||||
param.description = dedent(param.description)
|
||||
|
||||
def decorator(view_func):
|
||||
"""
|
||||
Final view decorator.
|
||||
"""
|
||||
operation_summary = None
|
||||
operation_description = None
|
||||
if view_func.__doc__ is not None:
|
||||
doc_lines = view_func.__doc__.strip().split("\n")
|
||||
if doc_lines:
|
||||
operation_summary = doc_lines[0].strip()
|
||||
if len(doc_lines) > 1:
|
||||
operation_description = dedent("\n".join(doc_lines[1:]))
|
||||
return drf_swagger_auto_schema(
|
||||
manual_parameters=parameters,
|
||||
operation_summary=operation_summary,
|
||||
operation_description=operation_description
|
||||
)(view_func)
|
||||
return decorator
|
||||
|
||||
|
||||
def is_schema_request(request):
|
||||
"""Is this request serving an OpenAPI schema?"""
|
||||
return request.query_params.get('format') == 'openapi'
|
||||
|
||||
|
||||
class ParameterLocation(object):
|
||||
"""Location of API parameter in request."""
|
||||
BODY = openapi.IN_BODY
|
||||
PATH = openapi.IN_PATH
|
||||
QUERY = openapi.IN_QUERY
|
||||
FORM = openapi.IN_FORM
|
||||
HEADER = openapi.IN_HEADER
|
||||
|
||||
|
||||
def string_parameter(name, in_, description=None):
|
||||
"""
|
||||
Convenient function for defining a string parameter.
|
||||
|
||||
Args:
|
||||
name (str)
|
||||
in_ (ParameterLocation attribute)
|
||||
description (str)
|
||||
"""
|
||||
return parameter(name, in_, str, description=description)
|
||||
|
||||
|
||||
def parameter(name, in_, param_type, description=None):
|
||||
"""
|
||||
Define typed parameters.
|
||||
|
||||
Args:
|
||||
name (str)
|
||||
in_ (ParameterLocation attribute)
|
||||
type (type): one of object, str, float, int, bool, list, file.
|
||||
description (str)
|
||||
"""
|
||||
openapi_type = None
|
||||
if param_type is object:
|
||||
openapi_type = openapi.TYPE_OBJECT
|
||||
elif param_type is str:
|
||||
openapi_type = openapi.TYPE_STRING
|
||||
elif param_type is float:
|
||||
openapi_type = openapi.TYPE_NUMBER
|
||||
elif param_type is int:
|
||||
openapi_type = openapi.TYPE_INTEGER
|
||||
elif param_type is bool:
|
||||
openapi_type = openapi.TYPE_BOOLEAN
|
||||
elif param_type is list:
|
||||
openapi_type = openapi.TYPE_ARRAY
|
||||
elif param_type is file:
|
||||
openapi_type = openapi.TYPE_FILE
|
||||
else:
|
||||
raise ValueError(u"Unsupported parameter type: '{}'".format(type))
|
||||
return openapi.Parameter(
|
||||
name,
|
||||
in_,
|
||||
type=openapi_type,
|
||||
description=description
|
||||
)
|
||||
# -----------------------------------------------------
|
||||
|
||||
|
||||
default_info = openapi.Info(
|
||||
title="Open edX API",
|
||||
default_version="v1",
|
||||
description="APIs for access to Open edX information",
|
||||
#terms_of_service="https://www.google.com/policies/terms/", # TODO: Do we have these?
|
||||
contact=openapi.Contact(email="oscm@edx.org"),
|
||||
#license=openapi.License(name="BSD License"), # TODO: What does this mean?
|
||||
)
|
||||
|
||||
schema_view = get_schema_view(
|
||||
default_info,
|
||||
generator_class=ApiSchemaGenerator,
|
||||
public=True,
|
||||
permission_classes=(permissions.AllowAny,),
|
||||
)
|
||||
@@ -7,7 +7,7 @@ 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 openedx.core.apidocs import is_schema_request
|
||||
|
||||
|
||||
from . import DEFAULT_FIELDS, OPTIONAL_FIELDS
|
||||
|
||||
@@ -11,7 +11,6 @@ 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
|
||||
@@ -27,7 +26,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 openedx.core import apidocs
|
||||
from xmodule.modulestore.exceptions import ItemNotFoundError
|
||||
|
||||
from . import DEFAULT_FIELDS, OPTIONAL_FIELDS, api
|
||||
@@ -97,9 +96,32 @@ class BookmarksViewMixin(object):
|
||||
)
|
||||
|
||||
|
||||
@method_decorator(name='get', decorator=swagger_auto_schema(
|
||||
operation_summary="Get a paginated list of bookmarks for a user.",
|
||||
operation_description=u"""
|
||||
class BookmarksListView(ListCreateAPIView, BookmarksViewMixin):
|
||||
"""REST endpoints for lists of bookmarks."""
|
||||
|
||||
authentication_classes = (OAuth2Authentication, SessionAuthentication)
|
||||
pagination_class = BookmarksPagination
|
||||
permission_classes = (permissions.IsAuthenticated,)
|
||||
serializer_class = BookmarkSerializer
|
||||
|
||||
@apidocs.schema(
|
||||
parameters=[
|
||||
apidocs.string_parameter(
|
||||
'course_id',
|
||||
apidocs.ParameterLocation.QUERY,
|
||||
description="The id of the course to limit the list",
|
||||
),
|
||||
apidocs.string_parameter(
|
||||
'fields',
|
||||
apidocs.ParameterLocation.QUERY,
|
||||
description="The fields to return: display_name, path.",
|
||||
),
|
||||
],
|
||||
)
|
||||
def get(self, request, *args, **kwargs):
|
||||
"""
|
||||
Get a paginated list of bookmarks for a user.
|
||||
|
||||
The list can be filtered by passing parameter "course_id=<course_id>"
|
||||
to only include bookmarks from a particular course.
|
||||
|
||||
@@ -117,31 +139,8 @@ class BookmarksViewMixin(object):
|
||||
# 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."""
|
||||
|
||||
authentication_classes = (OAuth2Authentication, SessionAuthentication)
|
||||
pagination_class = BookmarksPagination
|
||||
permission_classes = (permissions.IsAuthenticated,)
|
||||
serializer_class = BookmarkSerializer
|
||||
"""
|
||||
return super(BookmarksListView, self).get(request, *args, **kwargs)
|
||||
|
||||
def get_serializer_context(self):
|
||||
"""
|
||||
@@ -201,25 +200,21 @@ class BookmarksListView(ListCreateAPIView, BookmarksViewMixin):
|
||||
|
||||
return page
|
||||
|
||||
@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>}
|
||||
|
||||
""",
|
||||
)
|
||||
@apidocs.schema()
|
||||
def post(self, request, *unused_args, **unused_kwargs):
|
||||
"""Create a new bookmark for a user."""
|
||||
"""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
|
||||
|
||||
POST /api/bookmarks/v1/bookmarks/
|
||||
Request data: {"usage_id": <usage-id>}
|
||||
"""
|
||||
if not request.data:
|
||||
return self.error_response(ugettext_noop(u'No data provided.'), DEFAULT_USER_MESSAGE)
|
||||
|
||||
@@ -314,17 +309,13 @@ 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
|
||||
|
||||
""",
|
||||
)
|
||||
@apidocs.schema()
|
||||
def get(self, request, username=None, usage_id=None):
|
||||
"""
|
||||
Get a specific bookmark for a user.
|
||||
|
||||
# Example Requests
|
||||
|
||||
GET /api/bookmarks/v1/bookmarks/{username},{usage_id}?fields=display_name,path
|
||||
"""
|
||||
usage_key_or_response = self.get_usage_key_or_error_response(usage_id=usage_id)
|
||||
|
||||
@@ -1,87 +0,0 @@
|
||||
"""
|
||||
Open API support.
|
||||
"""
|
||||
|
||||
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",
|
||||
default_version="v1",
|
||||
description="APIs for access to Open edX information",
|
||||
#terms_of_service="https://www.google.com/policies/terms/", # TODO: Do we have these?
|
||||
contact=openapi.Contact(email="oscm@edx.org"),
|
||||
#license=openapi.License(name="BSD License"), # TODO: What does this mean?
|
||||
)
|
||||
|
||||
schema_view = get_schema_view(
|
||||
openapi_info,
|
||||
generator_class=ApiSchemaGenerator,
|
||||
public=True,
|
||||
permission_classes=(permissions.AllowAny,),
|
||||
)
|
||||
Reference in New Issue
Block a user