feat(swagger): generate localized static API schema
This commit is contained in:
parent
4bf5f1e585
commit
ed952d2d3a
|
|
@ -71,6 +71,8 @@ class ExamViewSet(CustomModelViewSet):
|
|||
|
||||
def get_queryset(self):
|
||||
qs = super().get_queryset()
|
||||
if getattr(self, 'swagger_fake_view', False):
|
||||
return qs
|
||||
if has_perm(self.request.user, ["exam.view"]):
|
||||
return qs
|
||||
user:User = self.request.user
|
||||
|
|
@ -142,6 +144,8 @@ class ExamRecordViewSet(ListModelMixin, DestroyModelMixin, RetrieveModelMixin, C
|
|||
|
||||
def get_queryset(self):
|
||||
qs = super().get_queryset()
|
||||
if getattr(self, 'swagger_fake_view', False):
|
||||
return qs
|
||||
if has_perm(self.request.user, ["examrecord.view"]):
|
||||
return qs
|
||||
return qs.filter(create_by=self.request.user)
|
||||
|
|
@ -207,6 +211,8 @@ class TrainRecordViewSet(CustomModelViewSet):
|
|||
|
||||
def get_queryset(self):
|
||||
qs = super().get_queryset()
|
||||
if getattr(self, 'swagger_fake_view', False):
|
||||
return qs
|
||||
if has_perm(self.request.user, ["train.view"]):
|
||||
return qs
|
||||
return qs.filter(create_by=self.request.user)
|
||||
|
|
|
|||
|
|
@ -651,6 +651,7 @@ class FileViewSet(BulkCreateModelMixin, RetrieveModelMixin, CustomListModelMixin
|
|||
class ApkViewSet(MyLoggingMixin, CustomListModelMixin, BulkCreateModelMixin, GenericViewSet):
|
||||
perms_map = {'get': '*', 'post': 'apk.upload'}
|
||||
serializer_class = ApkSerializer
|
||||
filter_backends = []
|
||||
|
||||
def get_authenticators(self):
|
||||
if self.request.method == 'GET':
|
||||
|
|
|
|||
|
|
@ -69,6 +69,7 @@ class SpeakerViewSet(CustomGenericViewSet):
|
|||
"""
|
||||
perms_map = {}
|
||||
serializer_class = serializers.Serializer
|
||||
filter_backends = []
|
||||
|
||||
@action(methods=['get'], detail=False,
|
||||
permission_classes=[IsAuthenticated])
|
||||
|
|
@ -125,6 +126,7 @@ class XxTestView(APIView):
|
|||
class XxCommonViewSet(CreateModelMixin, CustomGenericViewSet):
|
||||
perms_map = {'post': '*'}
|
||||
serializer_class = RequestCommonSerializer
|
||||
filter_backends = []
|
||||
|
||||
def create(self, request, *args, **kwargs):
|
||||
"""
|
||||
|
|
@ -258,6 +260,7 @@ class KingCommonViewSet(CreateModelMixin, CustomGenericViewSet):
|
|||
class DhCommonViewSet(CreateModelMixin, CustomGenericViewSet):
|
||||
perms_map = {'post': '*'}
|
||||
serializer_class = RequestCommonSerializer
|
||||
filter_backends = []
|
||||
|
||||
def create(self, request, *args, **kwargs):
|
||||
"""
|
||||
|
|
|
|||
|
|
@ -0,0 +1,71 @@
|
|||
import json
|
||||
import os
|
||||
from io import StringIO
|
||||
from pathlib import Path
|
||||
|
||||
from django.conf import settings
|
||||
from django.core.management import BaseCommand, CommandError, call_command
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = "生成供 Swagger UI 和 ReDoc 使用的静态 Swagger JSON"
|
||||
|
||||
def add_arguments(self, parser):
|
||||
parser.add_argument(
|
||||
"--output",
|
||||
help="输出路径,默认使用 settings.SWAGGER_SCHEMA_PATH",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--url",
|
||||
help="文档中的 API 根地址,默认使用 settings.BASE_URL",
|
||||
)
|
||||
|
||||
def handle(self, *args, **options):
|
||||
target = Path(options["output"] or settings.SWAGGER_SCHEMA_PATH)
|
||||
if not target.is_absolute():
|
||||
target = Path(settings.BASE_DIR) / target
|
||||
target = target.resolve()
|
||||
target.parent.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
temporary = target.with_name(f".{target.name}.{os.getpid()}.tmp")
|
||||
try:
|
||||
output = StringIO()
|
||||
call_command(
|
||||
"generate_swagger",
|
||||
"-",
|
||||
format="json",
|
||||
api_url=options["url"] or settings.BASE_URL,
|
||||
mock=True,
|
||||
verbosity=0,
|
||||
stdout=output,
|
||||
)
|
||||
content = output.getvalue()
|
||||
schema = json.loads(content)
|
||||
if schema.get("swagger") != "2.0" or not schema.get("paths"):
|
||||
raise CommandError("生成的 Swagger 文档缺少版本或接口路径")
|
||||
|
||||
content = json.dumps(
|
||||
schema,
|
||||
ensure_ascii=False,
|
||||
separators=(",", ":"),
|
||||
)
|
||||
temporary.write_text(content, encoding="utf-8")
|
||||
os.replace(temporary, target)
|
||||
except Exception as exc:
|
||||
if isinstance(exc, CommandError):
|
||||
raise
|
||||
raise CommandError(f"生成 Swagger 文档失败:{exc}") from exc
|
||||
finally:
|
||||
temporary.unlink(missing_ok=True)
|
||||
|
||||
operation_count = sum(
|
||||
method.lower() in {"get", "post", "put", "patch", "delete"}
|
||||
for path in schema["paths"].values()
|
||||
for method in path
|
||||
)
|
||||
self.stdout.write(
|
||||
self.style.SUCCESS(
|
||||
f"Swagger文档已生成:{target} "
|
||||
f"({len(schema['paths'])}个路径,{operation_count}个操作)"
|
||||
)
|
||||
)
|
||||
|
|
@ -0,0 +1,367 @@
|
|||
import re
|
||||
from pathlib import Path
|
||||
|
||||
from django.apps import apps
|
||||
from django.conf import settings
|
||||
from django.http import FileResponse, JsonResponse
|
||||
from drf_yasg import openapi
|
||||
from drf_yasg.inspectors import FieldInspector, SwaggerAutoSchema
|
||||
from drf_yasg.inspectors.base import NotHandled
|
||||
|
||||
|
||||
CRUD_SUMMARIES = {
|
||||
"list": "查询{resource}列表",
|
||||
"retrieve": "查询{resource}详情",
|
||||
"create": "新增{resource}",
|
||||
"update": "更新{resource}",
|
||||
"partial_update": "部分更新{resource}",
|
||||
"destroy": "删除{resource}",
|
||||
}
|
||||
|
||||
TAG_NAMES = {
|
||||
"am": "区域与准入管理",
|
||||
"asm": "资产管理",
|
||||
"cm": "标签管理",
|
||||
"cms": "内容管理",
|
||||
"develop": "开发工具",
|
||||
"ecm": "事件管理",
|
||||
"edu": "培训考试",
|
||||
"em": "设备管理",
|
||||
"enm": "能源管理",
|
||||
"inm": "库存管理",
|
||||
"mpr": "物资申购与领用",
|
||||
"mtm": "物料与工艺管理",
|
||||
"ofm": "办公管理",
|
||||
"opm": "作业许可",
|
||||
"pm": "生产任务管理",
|
||||
"pum": "采购管理",
|
||||
"qm": "质量管理",
|
||||
"rem": "研发项目管理",
|
||||
"rpm": "相关方管理",
|
||||
"sam": "销售管理",
|
||||
"third": "第三方集成",
|
||||
"utils": "通用工具",
|
||||
"wpm": "生产管理",
|
||||
"wpmw": "动态产品管理",
|
||||
"file": "文件管理",
|
||||
}
|
||||
|
||||
FIELD_NAMES = {
|
||||
"id": "主键ID",
|
||||
"ids": "主键ID列表",
|
||||
"access": "访问令牌",
|
||||
"refresh": "刷新令牌",
|
||||
"password_check": "密码确认",
|
||||
"base64": "Base64数据",
|
||||
"server_time": "服务器时间",
|
||||
"timezone": "时区",
|
||||
"next": "下一页",
|
||||
"previous": "上一页",
|
||||
"results": "结果列表",
|
||||
"detail": "详情",
|
||||
"items": "明细列表",
|
||||
"files": "附件列表",
|
||||
"echart_options": "图表配置",
|
||||
"tdata_list": "数据列表",
|
||||
"page": "页码",
|
||||
"page_size": "每页数量",
|
||||
"ordering": "排序字段",
|
||||
"querys": "查询条件列表",
|
||||
"annotate_field_list": "聚合字段列表",
|
||||
}
|
||||
|
||||
FIELD_TOKENS = {
|
||||
"name": "名称",
|
||||
"code": "编码",
|
||||
"number": "编号",
|
||||
"description": "说明",
|
||||
"note": "备注",
|
||||
"employee": "人员",
|
||||
"user": "用户",
|
||||
"leader": "负责人",
|
||||
"manager": "负责人",
|
||||
"keeper": "保管人",
|
||||
"participant": "参与人",
|
||||
"post": "岗位",
|
||||
"dept": "部门",
|
||||
"belong": "所属",
|
||||
"create": "创建",
|
||||
"update": "更新",
|
||||
"submit": "提交",
|
||||
"handle": "处理",
|
||||
"test": "检验",
|
||||
"material": "物料",
|
||||
"supplier": "供应商",
|
||||
"defect": "缺陷",
|
||||
"equipment": "设备",
|
||||
"warehouse": "仓库",
|
||||
"process": "工序",
|
||||
"operation": "操作",
|
||||
"state": "状态",
|
||||
"cate": "分类",
|
||||
"type": "类型",
|
||||
"area": "区域",
|
||||
"team": "班组",
|
||||
"shift": "班次",
|
||||
"ticket": "工单",
|
||||
"file": "文件",
|
||||
"photo": "照片",
|
||||
"image": "图片",
|
||||
"origin": "来源",
|
||||
"in": "入库",
|
||||
"out": "出库",
|
||||
"list": "列表",
|
||||
"count": "数量",
|
||||
"total": "总计",
|
||||
"enabled": "是否启用",
|
||||
}
|
||||
|
||||
QUERY_PARAMETERS = {
|
||||
"page": "页码",
|
||||
"page_size": "每页数量",
|
||||
"search": "搜索关键字",
|
||||
"ordering": "排序字段,字段名前加“-”表示倒序",
|
||||
"format": "响应格式",
|
||||
}
|
||||
|
||||
LOOKUP_NAMES = {
|
||||
"in": "属于列表",
|
||||
"contains": "包含",
|
||||
"icontains": "包含(忽略大小写)",
|
||||
"gte": "大于或等于",
|
||||
"gt": "大于",
|
||||
"lte": "小于或等于",
|
||||
"lt": "小于",
|
||||
"isnull": "是否为空",
|
||||
"exact": "等于",
|
||||
}
|
||||
|
||||
|
||||
def _contains_chinese(value):
|
||||
return bool(re.search(r"[\u4e00-\u9fff]", str(value or "")))
|
||||
|
||||
|
||||
def swagger_schema_file(request):
|
||||
schema_path = Path(settings.SWAGGER_SCHEMA_PATH)
|
||||
if not schema_path.is_file():
|
||||
return JsonResponse(
|
||||
{"detail": "Swagger文档尚未生成,请先运行 manage.py build_swagger"},
|
||||
status=503,
|
||||
)
|
||||
|
||||
response = FileResponse(
|
||||
schema_path.open("rb"),
|
||||
content_type="application/json; charset=utf-8",
|
||||
filename="swagger.json",
|
||||
)
|
||||
response["Content-Disposition"] = 'inline; filename="swagger.json"'
|
||||
response["Cache-Control"] = "no-cache"
|
||||
return response
|
||||
|
||||
|
||||
def _serializer_model(field):
|
||||
parent = getattr(field, "parent", None)
|
||||
while parent is not None:
|
||||
meta = getattr(parent, "Meta", None)
|
||||
model = getattr(meta, "model", None)
|
||||
if model is not None:
|
||||
return model
|
||||
parent = getattr(parent, "parent", None)
|
||||
return None
|
||||
|
||||
|
||||
def _model_path_label(model, parts):
|
||||
labels = []
|
||||
for part in parts:
|
||||
if model is None:
|
||||
break
|
||||
try:
|
||||
model_field = model._meta.get_field(part)
|
||||
except Exception:
|
||||
break
|
||||
verbose_name = getattr(model_field, "verbose_name", "")
|
||||
if _contains_chinese(verbose_name):
|
||||
labels.append(str(verbose_name))
|
||||
model = getattr(model_field, "related_model", None)
|
||||
return " / ".join(labels)
|
||||
|
||||
|
||||
def _field_name_label(field_name):
|
||||
field_name = str(field_name or "").strip("_")
|
||||
if field_name in FIELD_NAMES:
|
||||
return FIELD_NAMES[field_name]
|
||||
tokens = field_name.split("_")
|
||||
if tokens and all(token in FIELD_TOKENS for token in tokens):
|
||||
return "".join(FIELD_TOKENS[token] for token in tokens)
|
||||
return ""
|
||||
|
||||
|
||||
class ChineseFieldInspector(FieldInspector):
|
||||
"""优先使用模型字段中文名称补全 serializer 字段标题。"""
|
||||
|
||||
def field_to_swagger_object(self, field, **kwargs):
|
||||
return NotHandled
|
||||
|
||||
def process_result(self, result, method_name, obj, **kwargs):
|
||||
if (
|
||||
method_name != "field_to_swagger_object"
|
||||
or not isinstance(result, openapi.SwaggerDict)
|
||||
or "$ref" in result
|
||||
or _contains_chinese(result.get("title"))
|
||||
):
|
||||
return result
|
||||
|
||||
source_attrs = getattr(obj, "source_attrs", None) or []
|
||||
model_label = _model_path_label(_serializer_model(obj), source_attrs)
|
||||
label = model_label or _field_name_label(getattr(obj, "field_name", ""))
|
||||
field_name = getattr(obj, "field_name", "")
|
||||
if label:
|
||||
result["title"] = label
|
||||
elif field_name:
|
||||
result["title"] = f"{field_name}(字段)"
|
||||
return result
|
||||
|
||||
|
||||
class ChineseSwaggerAutoSchema(SwaggerAutoSchema):
|
||||
"""为未显式编写文档的接口补充稳定、可读的中文展示信息。"""
|
||||
|
||||
field_inspectors = [ChineseFieldInspector] + SwaggerAutoSchema.field_inspectors
|
||||
|
||||
def get_operation(self, operation_keys=None):
|
||||
operation = super().get_operation(operation_keys)
|
||||
model = getattr(getattr(self.view, "queryset", None), "model", None)
|
||||
for parameter in operation.get("parameters", []):
|
||||
current = parameter.get("description", "")
|
||||
if _contains_chinese(current):
|
||||
continue
|
||||
location = parameter.get("in")
|
||||
if location == openapi.IN_BODY:
|
||||
description = "请求数据"
|
||||
elif location == openapi.IN_PATH:
|
||||
description = f"路径参数:{parameter.get('name', '')}"
|
||||
else:
|
||||
description = self._get_parameter_description(
|
||||
parameter.get("name", ""), model
|
||||
)
|
||||
if current:
|
||||
description = f"{description};{current}"
|
||||
parameter["description"] = description
|
||||
return operation
|
||||
|
||||
def get_summary_and_description(self):
|
||||
summary, description = super().get_summary_and_description()
|
||||
if summary:
|
||||
if description and not _contains_chinese(description):
|
||||
description = f"{summary}\n\n{description}"
|
||||
return summary, description or summary
|
||||
|
||||
resource = self._get_resource_name()
|
||||
action = getattr(self.view, "action", None)
|
||||
template = CRUD_SUMMARIES.get(action)
|
||||
if template:
|
||||
summary = template.format(resource=resource)
|
||||
elif resource:
|
||||
action_name = str(action or self.method).replace("_", " ")
|
||||
display_resource = resource
|
||||
if not _contains_chinese(display_resource):
|
||||
display_resource = f"{display_resource}接口"
|
||||
summary = f"{display_resource}:{action_name}"
|
||||
|
||||
if description and not _contains_chinese(description):
|
||||
description = f"{summary}\n\n{description}"
|
||||
return summary, description or summary
|
||||
|
||||
def get_request_body_parameters(self, consumes):
|
||||
parameters = super().get_request_body_parameters(consumes)
|
||||
for parameter in parameters:
|
||||
if parameter.get("in") == openapi.IN_BODY and not _contains_chinese(
|
||||
parameter.get("description")
|
||||
):
|
||||
parameter["description"] = "请求数据"
|
||||
return parameters
|
||||
|
||||
def get_query_parameters(self):
|
||||
parameters = super().get_query_parameters()
|
||||
model = getattr(getattr(self.view, "queryset", None), "model", None)
|
||||
for parameter in parameters:
|
||||
current = parameter.get("description", "")
|
||||
if _contains_chinese(current):
|
||||
continue
|
||||
description = self._get_parameter_description(
|
||||
parameter.get("name", ""), model
|
||||
)
|
||||
if current:
|
||||
description = f"{description};{current}"
|
||||
parameter["description"] = description
|
||||
return parameters
|
||||
|
||||
def get_responses(self):
|
||||
responses = super().get_responses()
|
||||
descriptions = {
|
||||
"200": "请求成功",
|
||||
"201": "创建成功",
|
||||
"202": "请求已接受",
|
||||
"204": "操作成功,无响应内容",
|
||||
"400": "请求参数错误",
|
||||
"401": "未认证或认证已失效",
|
||||
"403": "无权访问",
|
||||
"404": "资源不存在",
|
||||
}
|
||||
for status, response in responses.items():
|
||||
if not response.get("description"):
|
||||
response["description"] = descriptions.get(str(status), "接口响应")
|
||||
return responses
|
||||
|
||||
def _get_parameter_description(self, name, model):
|
||||
if name in QUERY_PARAMETERS:
|
||||
return QUERY_PARAMETERS[name]
|
||||
|
||||
parts = str(name).split("__")
|
||||
lookup = LOOKUP_NAMES.get(parts[-1])
|
||||
field_parts = parts[:-1] if lookup else parts
|
||||
label = _model_path_label(model, field_parts)
|
||||
if not label:
|
||||
label = _field_name_label(field_parts[-1] if field_parts else name)
|
||||
if not label:
|
||||
label = f"查询参数:{name}"
|
||||
if lookup:
|
||||
label = f"{label}({lookup})"
|
||||
return label
|
||||
|
||||
def get_tags(self, operation_keys=None):
|
||||
tags = super().get_tags(operation_keys)
|
||||
if self.overrides.get("tags") or not tags:
|
||||
return tags
|
||||
|
||||
if tags[0] in TAG_NAMES:
|
||||
return [TAG_NAMES[tags[0]]]
|
||||
|
||||
try:
|
||||
app_config = apps.get_app_config(tags[0])
|
||||
except LookupError:
|
||||
return tags
|
||||
|
||||
if _contains_chinese(app_config.verbose_name):
|
||||
return [str(app_config.verbose_name)]
|
||||
return tags
|
||||
|
||||
def _get_resource_name(self):
|
||||
queryset = getattr(self.view, "queryset", None)
|
||||
model = getattr(queryset, "model", None)
|
||||
|
||||
if model is None:
|
||||
serializer_class = getattr(self.view, "serializer_class", None)
|
||||
meta = getattr(serializer_class, "Meta", None)
|
||||
model = getattr(meta, "model", None)
|
||||
|
||||
if model is None:
|
||||
return "接口"
|
||||
|
||||
match = re.search(r"TN\s*[::]\s*([^\n\r]+)", model.__doc__ or "")
|
||||
if match:
|
||||
return match.group(1).strip()
|
||||
|
||||
verbose_name = str(model._meta.verbose_name)
|
||||
if _contains_chinese(verbose_name):
|
||||
return verbose_name
|
||||
return model.__name__
|
||||
|
|
@ -0,0 +1,142 @@
|
|||
import json
|
||||
from pathlib import Path
|
||||
from tempfile import TemporaryDirectory
|
||||
from types import SimpleNamespace
|
||||
from unittest.mock import patch
|
||||
|
||||
from django.conf import settings
|
||||
from django.core.management import call_command
|
||||
from django.test import SimpleTestCase, override_settings
|
||||
|
||||
from apps.am.models import Area
|
||||
from apps.am.views import AreaViewSet
|
||||
from apps.utils.swagger import ChineseSwaggerAutoSchema, swagger_schema_file
|
||||
|
||||
|
||||
class ChineseSwaggerAutoSchemaTests(SimpleTestCase):
|
||||
def make_schema(self, view, method="GET"):
|
||||
schema = ChineseSwaggerAutoSchema.__new__(ChineseSwaggerAutoSchema)
|
||||
schema.view = view
|
||||
schema.method = method
|
||||
schema.path = "/am/area/"
|
||||
schema.overrides = {}
|
||||
schema.operation_keys = ("am", "area", "list")
|
||||
schema._sch = SimpleNamespace(get_description=lambda path, method: "")
|
||||
return schema
|
||||
|
||||
def test_crud_summary_uses_model_chinese_name(self):
|
||||
view = SimpleNamespace(queryset=Area.objects.all(), action="list")
|
||||
schema = self.make_schema(view)
|
||||
|
||||
summary, description = schema.get_summary_and_description()
|
||||
|
||||
self.assertEqual(summary, "查询地图区域列表")
|
||||
self.assertEqual(description, "查询地图区域列表")
|
||||
|
||||
def test_explicit_summary_takes_priority(self):
|
||||
view = SimpleNamespace(queryset=Area.objects.all(), action="list")
|
||||
schema = self.make_schema(view)
|
||||
schema.overrides = {
|
||||
"operation_summary": "区域自定义查询",
|
||||
"operation_description": "自定义说明",
|
||||
}
|
||||
|
||||
summary, description = schema.get_summary_and_description()
|
||||
|
||||
self.assertEqual(summary, "区域自定义查询")
|
||||
self.assertEqual(description, "自定义说明")
|
||||
|
||||
def test_custom_action_with_english_model_name_has_chinese_hint(self):
|
||||
model = SimpleNamespace(
|
||||
__doc__="",
|
||||
__name__="Dataset",
|
||||
_meta=SimpleNamespace(verbose_name="dataset"),
|
||||
)
|
||||
queryset = SimpleNamespace(model=model)
|
||||
view = SimpleNamespace(queryset=queryset, action="base")
|
||||
schema = self.make_schema(view)
|
||||
|
||||
summary, _ = schema.get_summary_and_description()
|
||||
|
||||
self.assertEqual(summary, "Dataset接口:base")
|
||||
|
||||
def test_tag_uses_chinese_business_module_name(self):
|
||||
view = SimpleNamespace(queryset=Area.objects.all(), action="list")
|
||||
schema = self.make_schema(view)
|
||||
|
||||
self.assertEqual(schema.get_tags(("am", "area", "list")), ["区域与准入管理"])
|
||||
|
||||
def test_filter_parameter_uses_model_field_labels(self):
|
||||
view = SimpleNamespace(queryset=Area.objects.all(), action="list")
|
||||
schema = self.make_schema(view)
|
||||
|
||||
description = schema._get_parameter_description(
|
||||
"manager__name__contains",
|
||||
Area,
|
||||
)
|
||||
|
||||
self.assertIn("区域负责人", description)
|
||||
self.assertIn("包含", description)
|
||||
|
||||
def test_swagger_queryset_skips_permission_data_lookup(self):
|
||||
view = AreaViewSet(basename="area")
|
||||
view.action = "list"
|
||||
view.swagger_fake_view = True
|
||||
|
||||
with patch("apps.utils.viewsets.get_user_perms_map") as permission_lookup:
|
||||
queryset = view.get_queryset()
|
||||
|
||||
self.assertIs(queryset.model, Area)
|
||||
permission_lookup.assert_not_called()
|
||||
|
||||
|
||||
class SwaggerSettingsTests(SimpleTestCase):
|
||||
def test_swagger_supports_jwt_authorization_header(self):
|
||||
from django.conf import settings
|
||||
|
||||
bearer = settings.SWAGGER_SETTINGS["SECURITY_DEFINITIONS"]["Bearer"]
|
||||
|
||||
self.assertEqual(bearer["type"], "apiKey")
|
||||
self.assertEqual(bearer["name"], "Authorization")
|
||||
self.assertEqual(bearer["in"], "header")
|
||||
|
||||
def test_swagger_ui_uses_static_schema(self):
|
||||
from django.conf import settings
|
||||
|
||||
self.assertEqual(settings.SWAGGER_SETTINGS["SPEC_URL"], "schema-swagger-json")
|
||||
self.assertEqual(settings.REDOC_SETTINGS["SPEC_URL"], "schema-swagger-json")
|
||||
|
||||
|
||||
class BuildSwaggerCommandTests(SimpleTestCase):
|
||||
def test_command_writes_valid_utf8_schema(self):
|
||||
schema = {
|
||||
"swagger": "2.0",
|
||||
"info": {"title": "中文文档"},
|
||||
"paths": {"/demo/": {"get": {}}},
|
||||
}
|
||||
|
||||
def generate_schema(command_name, output_file, **options):
|
||||
self.assertEqual(command_name, "generate_swagger")
|
||||
self.assertEqual(output_file, "-")
|
||||
options["stdout"].write(json.dumps(schema, ensure_ascii=False))
|
||||
|
||||
with TemporaryDirectory(dir=settings.BASE_DIR) as directory:
|
||||
target = Path(directory) / "swagger.json"
|
||||
with override_settings(SWAGGER_SCHEMA_PATH=str(target)):
|
||||
with patch(
|
||||
"apps.utils.management.commands.build_swagger.call_command",
|
||||
side_effect=generate_schema,
|
||||
):
|
||||
call_command("build_swagger", verbosity=0)
|
||||
|
||||
content = target.read_text(encoding="utf-8")
|
||||
self.assertIn("中文文档", content)
|
||||
self.assertEqual(json.loads(content), schema)
|
||||
|
||||
with override_settings(SWAGGER_SCHEMA_PATH=str(target)):
|
||||
response = swagger_schema_file(SimpleNamespace())
|
||||
body = b"".join(response.streaming_content)
|
||||
response.close()
|
||||
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertEqual(json.loads(body), schema)
|
||||
|
|
@ -154,6 +154,9 @@ class CustomGenericViewSet(MyLoggingMixin, GenericViewSet):
|
|||
def get_queryset(self):
|
||||
queryset = super().get_queryset()
|
||||
queryset = self.get_queryset_custom(queryset)
|
||||
# drf-yasg 生成文档时不应读取权限或业务数据。
|
||||
if getattr(self, 'swagger_fake_view', False):
|
||||
return queryset
|
||||
if self.data_filter:
|
||||
user = self.request.user
|
||||
if user.is_superuser:
|
||||
|
|
|
|||
|
|
@ -178,6 +178,7 @@ USE_TZ = True
|
|||
|
||||
STATIC_URL = '/static/'
|
||||
STATIC_ROOT = os.path.join(BASE_DIR, 'dist/static')
|
||||
SWAGGER_SCHEMA_PATH = os.path.join(STATIC_ROOT, 'openapi/swagger.json')
|
||||
# STATICFILES_DIRS = (
|
||||
# os.path.join(BASE_DIR, 'dist/static'),
|
||||
# )
|
||||
|
|
@ -267,8 +268,27 @@ CELERYD_SOFT_TIME_LIMIT = 60*10
|
|||
|
||||
# swagger配置
|
||||
SWAGGER_SETTINGS = {
|
||||
'DEFAULT_INFO': 'server.swagger.api_info',
|
||||
'DEFAULT_API_URL': BASE_URL,
|
||||
'SPEC_URL': 'schema-swagger-json',
|
||||
'LOGIN_URL': '/django/admin/login/',
|
||||
'LOGOUT_URL': '/django/admin/logout/',
|
||||
'DEFAULT_AUTO_SCHEMA_CLASS': 'apps.utils.swagger.ChineseSwaggerAutoSchema',
|
||||
'SECURITY_DEFINITIONS': {
|
||||
'Bearer': {
|
||||
'type': 'apiKey',
|
||||
'name': 'Authorization',
|
||||
'in': 'header',
|
||||
'description': 'JWT认证,请输入:Bearer <access token>',
|
||||
},
|
||||
'Basic': {
|
||||
'type': 'basic',
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
REDOC_SETTINGS = {
|
||||
'SPEC_URL': 'schema-swagger-json',
|
||||
}
|
||||
|
||||
# 日志配置
|
||||
|
|
|
|||
|
|
@ -0,0 +1,12 @@
|
|||
from django.conf import settings
|
||||
from drf_yasg import openapi
|
||||
|
||||
from server.settings import get_sysconfig
|
||||
|
||||
|
||||
api_info = openapi.Info(
|
||||
title=f'{settings.SYS_NAME}--{get_sysconfig("base.base_name", "demo")}',
|
||||
default_version=settings.SYS_VERSION,
|
||||
contact=openapi.Contact(email="caoqianming@foxmail.com"),
|
||||
license=openapi.License(name="MIT License"),
|
||||
)
|
||||
|
|
@ -17,19 +17,14 @@ from django.conf import settings
|
|||
from django.conf.urls.static import static
|
||||
from django.contrib import admin
|
||||
from django.urls import include, path
|
||||
from drf_yasg import openapi
|
||||
from drf_yasg.views import get_schema_view
|
||||
from rest_framework.documentation import include_docs_urls
|
||||
from django.views.generic import TemplateView
|
||||
from server.settings import get_sysconfig
|
||||
from apps.utils.swagger import swagger_schema_file
|
||||
from server.swagger import api_info
|
||||
|
||||
schema_view = get_schema_view(
|
||||
openapi.Info(
|
||||
title=f'{settings.SYS_NAME}--{get_sysconfig("base.base_name", "demo")}',
|
||||
default_version=settings.SYS_VERSION,
|
||||
contact=openapi.Contact(email="caoqianming@foxmail.com"),
|
||||
license=openapi.License(name="MIT License"),
|
||||
),
|
||||
api_info,
|
||||
public=True,
|
||||
permission_classes=[],
|
||||
url=settings.BASE_URL
|
||||
|
|
@ -89,6 +84,8 @@ urlpatterns = [
|
|||
if getattr(settings, 'ENABLE_SWAGGER', True):
|
||||
urlpatterns += [
|
||||
# api文档
|
||||
path('api/swagger.json', swagger_schema_file,
|
||||
name='schema-swagger-json'),
|
||||
path('api/swagger/', schema_view.with_ui('swagger',
|
||||
cache_timeout=0), name='schema-swagger-ui'),
|
||||
path('api/redoc/', schema_view.with_ui('redoc',
|
||||
|
|
|
|||
Loading…
Reference in New Issue