Merge pull request #22547 from edx/nedbat/api-docs-from-lib
Use API doc tooling from its own library
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.apidocs.ApiSchemaGenerator -o $(SWAGGER)
|
||||
DJANGO_SETTINGS_MODULE=docs.docs_settings python manage.py lms generate_swagger --generator-class=edx_api_doc_tools.ApiSchemaGenerator -o $(SWAGGER)
|
||||
|
||||
api-docs-sphinx: swagger ## generate the sphinx source files for api-docs
|
||||
rm -f docs/api/gen/*
|
||||
|
||||
17
cms/urls.py
17
cms/urls.py
@@ -2,12 +2,12 @@
|
||||
Urls of Studio.
|
||||
"""
|
||||
|
||||
|
||||
from django.conf import settings
|
||||
from django.conf.urls import include, url
|
||||
from django.conf.urls.static import static
|
||||
from django.contrib.admin import autodiscover as django_autodiscover
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from edx_api_doc_tools import make_docs_urls
|
||||
from ratelimitbackend import admin
|
||||
|
||||
import contentstore.views
|
||||
@@ -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.apidocs import schema_view
|
||||
from openedx.core.apidocs import api_info
|
||||
|
||||
|
||||
django_autodiscover()
|
||||
@@ -280,18 +280,7 @@ urlpatterns += [
|
||||
]
|
||||
|
||||
# API docs.
|
||||
urlpatterns += [
|
||||
url(
|
||||
r'^swagger(?P<format>\.json|\.yaml)$',
|
||||
schema_view.without_ui(cache_timeout=settings.OPENAPI_CACHE_TIMEOUT), name='schema-json',
|
||||
),
|
||||
url(
|
||||
r'^swagger/$',
|
||||
schema_view.with_ui('swagger', cache_timeout=settings.OPENAPI_CACHE_TIMEOUT),
|
||||
name='schema-swagger-ui',
|
||||
),
|
||||
url(r'^api-docs/$', schema_view.with_ui('swagger', cache_timeout=settings.OPENAPI_CACHE_TIMEOUT)),
|
||||
]
|
||||
urlpatterns += make_docs_urls(api_info)
|
||||
|
||||
if 'openedx.testing.coverage_context_listener' in settings.INSTALLED_APPS:
|
||||
urlpatterns += [
|
||||
|
||||
@@ -1997,7 +1997,7 @@ paths:
|
||||
/edx_proctoring/v1/proctored_exam/active_exams_for_user:
|
||||
get:
|
||||
operationId: edx_proctoring_v1_proctored_exam_active_exams_for_user_list
|
||||
description: returns the get_active_exams_for_user
|
||||
description: Returns the get_active_exams_for_user
|
||||
parameters: []
|
||||
responses:
|
||||
'200':
|
||||
@@ -4615,73 +4615,6 @@ paths:
|
||||
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
|
||||
@@ -5387,7 +5320,6 @@ definitions:
|
||||
title: Order number
|
||||
type: string
|
||||
maxLength: 128
|
||||
minLength: 1
|
||||
x-nullable: true
|
||||
support_details:
|
||||
title: Support details
|
||||
|
||||
@@ -5,9 +5,11 @@ import logging
|
||||
|
||||
import six
|
||||
from django.contrib.auth import get_user_model
|
||||
import edx_api_doc_tools as apidocs
|
||||
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
|
||||
|
||||
from opaque_keys import InvalidKeyError
|
||||
from opaque_keys.edx.keys import CourseKey
|
||||
from rest_condition import C
|
||||
@@ -20,7 +22,6 @@ 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 import apidocs
|
||||
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
@@ -2535,7 +2535,7 @@ REST_FRAMEWORK = {
|
||||
}
|
||||
|
||||
SWAGGER_SETTINGS = {
|
||||
'DEFAULT_INFO': 'openedx.core.apidocs.default_info',
|
||||
'DEFAULT_INFO': 'openedx.core.apidocs.api_info',
|
||||
}
|
||||
|
||||
# How long to cache OpenAPI schemas and UI, in seconds.
|
||||
|
||||
17
lms/urls.py
17
lms/urls.py
@@ -12,6 +12,8 @@ from django.utils.translation import ugettext_lazy as _
|
||||
from django.views.generic.base import RedirectView
|
||||
from ratelimitbackend import admin
|
||||
|
||||
from edx_api_doc_tools import make_docs_urls
|
||||
|
||||
from branding import views as branding_views
|
||||
from lms.djangoapps.courseware.masquerade import handle_ajax as courseware_masquerade_handle_ajax
|
||||
from lms.djangoapps.courseware.module_render import (
|
||||
@@ -44,7 +46,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.apidocs import schema_view
|
||||
from openedx.core.apidocs import api_info
|
||||
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
|
||||
@@ -959,18 +961,7 @@ if settings.BRANCH_IO_KEY:
|
||||
]
|
||||
|
||||
# API docs.
|
||||
urlpatterns += [
|
||||
url(
|
||||
r'^swagger(?P<format>\.json|\.yaml)$',
|
||||
schema_view.without_ui(cache_timeout=settings.OPENAPI_CACHE_TIMEOUT), name='schema-json',
|
||||
),
|
||||
url(
|
||||
r'^swagger/$',
|
||||
schema_view.with_ui('swagger', cache_timeout=settings.OPENAPI_CACHE_TIMEOUT),
|
||||
name='schema-swagger-ui',
|
||||
),
|
||||
url(r'^api-docs/$', schema_view.with_ui('swagger', cache_timeout=settings.OPENAPI_CACHE_TIMEOUT)),
|
||||
]
|
||||
urlpatterns += make_docs_urls(api_info)
|
||||
|
||||
# edx-drf-extensions csrf app
|
||||
urlpatterns += [
|
||||
|
||||
@@ -2,161 +2,13 @@
|
||||
Open API support.
|
||||
"""
|
||||
|
||||
import textwrap
|
||||
from edx_api_doc_tools import make_api_info
|
||||
|
||||
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(
|
||||
api_info = make_api_info(
|
||||
title="Open edX API",
|
||||
default_version="v1",
|
||||
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"),
|
||||
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,),
|
||||
)
|
||||
|
||||
@@ -3,11 +3,11 @@ Serializers for Bookmarks.
|
||||
"""
|
||||
|
||||
|
||||
import six
|
||||
from edx_api_doc_tools import is_schema_request
|
||||
from rest_framework import serializers
|
||||
import six
|
||||
|
||||
from openedx.core.lib.api.serializers import CourseKeyField, UsageKeyField
|
||||
from openedx.core.apidocs import is_schema_request
|
||||
|
||||
|
||||
from . import DEFAULT_FIELDS, OPTIONAL_FIELDS
|
||||
|
||||
@@ -13,6 +13,7 @@ from django.conf import settings
|
||||
from django.core.exceptions import ObjectDoesNotExist
|
||||
from django.utils.translation import ugettext as _
|
||||
from django.utils.translation import ugettext_noop
|
||||
import edx_api_doc_tools as apidocs
|
||||
from edx_rest_framework_extensions.paginators import DefaultPagination
|
||||
from opaque_keys import InvalidKeyError
|
||||
from opaque_keys.edx.keys import CourseKey, UsageKey
|
||||
@@ -26,7 +27,6 @@ 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 import apidocs
|
||||
from xmodule.modulestore.exceptions import ItemNotFoundError
|
||||
|
||||
from . import DEFAULT_FIELDS, OPTIONAL_FIELDS, api
|
||||
|
||||
@@ -66,9 +66,9 @@ django-waffle==0.18.0
|
||||
django-webpack-loader # Used to wire webpack bundles into the django asset pipeline
|
||||
djangorestframework==3.9.4
|
||||
djangorestframework-jwt
|
||||
drf-yasg # Replacement for django-rest-swagger
|
||||
edx-ace
|
||||
edx-analytics-data-api-client
|
||||
edx-api-doc-tools
|
||||
edx-bulk-grades # LMS REST API for managing bulk grading operations
|
||||
edx-ccx-keys
|
||||
edx-celeryutils
|
||||
|
||||
@@ -44,8 +44,6 @@ git+https://github.com/edx/openedx-chem.git@ff4e3a03d3c7610e47a9af08eb648d8aabe2
|
||||
click==7.0 # via code-annotations, user-util
|
||||
code-annotations==0.3.3 # via edx-enterprise
|
||||
contextlib2==0.6.0.post1
|
||||
coreapi==2.3.3 # via drf-yasg
|
||||
coreschema==0.0.4 # via coreapi, drf-yasg
|
||||
git+https://github.com/edx/crowdsourcehinter.git@a7ffc85b134b7d8909bf1fefd23dbdb8eb28e467#egg=crowdsourcehinter-xblock==0.2
|
||||
cryptography==2.8
|
||||
cssutils==1.0.2 # via pynliner
|
||||
@@ -94,9 +92,10 @@ djangorestframework-xml==1.4.0 # via edx-enterprise
|
||||
djangorestframework==3.9.4
|
||||
docopt==0.6.2
|
||||
docutils==0.16 # via botocore
|
||||
drf-yasg==1.16
|
||||
drf-yasg==1.16 # via edx-api-doc-tools
|
||||
edx-ace==0.1.13
|
||||
edx-analytics-data-api-client==0.15.3
|
||||
edx-api-doc-tools==1.0.2
|
||||
edx-bulk-grades==0.6.6
|
||||
edx-ccx-keys==1.0.0
|
||||
edx-celeryutils==0.3.1
|
||||
@@ -136,11 +135,9 @@ help-tokens==1.0.5
|
||||
html5lib==1.0.1
|
||||
httplib2==0.16.0
|
||||
idna==2.8
|
||||
inflection==0.3.1 # via drf-yasg
|
||||
ipaddress==1.0.23
|
||||
isodate==0.6.0 # via python3-saml
|
||||
itypes==1.1.0 # via coreapi
|
||||
jinja2==2.10.3 # via code-annotations, coreschema
|
||||
jinja2==2.10.3 # via code-annotations
|
||||
jmespath==0.9.4 # via boto3, botocore
|
||||
jsondiff==1.2.0 # via edx-enterprise
|
||||
jsonfield==2.0.2
|
||||
@@ -210,8 +207,6 @@ requests-oauthlib==1.1.0
|
||||
requests==2.22.0
|
||||
rest-condition==1.0.3
|
||||
rfc6266-parser==0.0.6
|
||||
ruamel.yaml.clib==0.2.0 # via ruamel.yaml
|
||||
ruamel.yaml==0.16.5 # via drf-yasg
|
||||
rules==2.2
|
||||
s3transfer==0.1.13 # via boto3
|
||||
sailthru-client==2.2.3
|
||||
@@ -234,7 +229,6 @@ sympy==1.5.1
|
||||
testfixtures==6.10.3 # via edx-enterprise
|
||||
text-unidecode==1.3 # via python-slugify
|
||||
unicodecsv==0.14.1
|
||||
uritemplate==3.0.1 # via coreapi, drf-yasg
|
||||
urllib3==1.25.7
|
||||
user-util==0.1.5
|
||||
voluptuous==0.11.7
|
||||
|
||||
@@ -108,6 +108,7 @@ docutils==0.16
|
||||
drf-yasg==1.16
|
||||
edx-ace==0.1.13
|
||||
edx-analytics-data-api-client==0.15.3
|
||||
edx-api-doc-tools==1.0.2
|
||||
edx-bulk-grades==0.6.6
|
||||
edx-ccx-keys==1.0.0
|
||||
edx-celeryutils==0.3.1
|
||||
|
||||
@@ -49,8 +49,8 @@ click==7.0
|
||||
code-annotations==0.3.3
|
||||
colorama==0.4.3 # via radon
|
||||
contextlib2==0.6.0.post1
|
||||
coreapi==2.3.3
|
||||
coreschema==0.0.4
|
||||
coreapi==2.3.3 # via drf-yasg
|
||||
coreschema==0.0.4 # via coreapi, drf-yasg
|
||||
coverage==5.0.3
|
||||
git+https://github.com/nedbat/coverage_pytest_plugin.git@29de030251471e200ff255eb9e549218cd60e872#egg=coverage_pytest_plugin==0.0
|
||||
git+https://github.com/edx/crowdsourcehinter.git@a7ffc85b134b7d8909bf1fefd23dbdb8eb28e467#egg=crowdsourcehinter-xblock==0.2
|
||||
@@ -105,6 +105,7 @@ docutils==0.16
|
||||
drf-yasg==1.16
|
||||
edx-ace==0.1.13
|
||||
edx-analytics-data-api-client==0.15.3
|
||||
edx-api-doc-tools==1.0.2
|
||||
edx-bulk-grades==0.6.6
|
||||
edx-ccx-keys==1.0.0
|
||||
edx-celeryutils==0.3.1
|
||||
@@ -156,11 +157,11 @@ httpretty==0.9.7
|
||||
idna==2.8
|
||||
importlib-metadata==1.4.0
|
||||
inflect==3.0.2
|
||||
inflection==0.3.1
|
||||
inflection==0.3.1 # via drf-yasg
|
||||
ipaddress==1.0.23
|
||||
isodate==0.6.0
|
||||
isort==4.3.21
|
||||
itypes==1.1.0
|
||||
itypes==1.1.0 # via coreapi
|
||||
jinja2-pluralize==0.3.0
|
||||
jinja2==2.10.3
|
||||
jmespath==0.9.4
|
||||
@@ -258,8 +259,8 @@ requests-oauthlib==1.1.0
|
||||
requests==2.22.0
|
||||
rest-condition==1.0.3
|
||||
rfc6266-parser==0.0.6
|
||||
ruamel.yaml.clib==0.2.0
|
||||
ruamel.yaml==0.16.5
|
||||
ruamel.yaml.clib==0.2.0 # via ruamel.yaml
|
||||
ruamel.yaml==0.16.5 # via drf-yasg
|
||||
rules==2.2
|
||||
s3transfer==0.1.13
|
||||
sailthru-client==2.2.3
|
||||
@@ -289,7 +290,7 @@ tox==3.14.3
|
||||
transifex-client==0.13.4
|
||||
unicodecsv==0.14.1
|
||||
unidiff==0.5.5
|
||||
uritemplate==3.0.1
|
||||
uritemplate==3.0.1 # via coreapi, drf-yasg
|
||||
urllib3==1.25.7
|
||||
user-util==0.1.5
|
||||
virtualenv==16.7.9 # via tox
|
||||
|
||||
Reference in New Issue
Block a user