factory/apps/utils/swagger.py

368 lines
12 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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__