- 新增 apps/blog、apps/solutions、apps/home、apps/api 测试文件,覆盖标签/排序/ 草稿过滤、HomePage.subpage_types 回归测试、page_preview 预览接口 - apps/core/tests.py 新增健康检查端点、发布 webhook 信号、bootstrap_deployment 命令的测试用例,pytest 用例数从 40 增至 65,全部通过 - 修复 apps/api/urls.py 中 PagePreviewAPIViewSet.get_object() 未捕获 DoesNotExist 导致 500 而非文档承诺 404 的 bug - 新增 apps/core/management/commands/bootstrap_deployment.py:部署时按环境变量 幂等创建首个超级管理员账号并联动初始化 RBAC 权限组,.env.example 补充对应 环境变量说明 - 新增 OpenSearch + 中文分词本地 PoC 脚手架(docker-compose.opensearch.yml + docker/opensearch/Dockerfile),调研结论详见设计文档 §2.10 - 更新设计文档 §2.16 Phase 1 进度清单
63 lines
2.7 KiB
Python
63 lines
2.7 KiB
Python
"""
|
||
Wagtail API v2 路由配置。
|
||
详见 documents/设计方案分析与完善版.md §2.7 Headless API 设计规范。
|
||
"""
|
||
from django.contrib.contenttypes.models import ContentType
|
||
from django.http import Http404
|
||
from rest_framework.response import Response
|
||
from wagtail.api.v2.views import PagesAPIViewSet
|
||
from wagtail.api.v2.router import WagtailAPIRouter
|
||
from wagtail.images.api.v2.views import ImagesAPIViewSet
|
||
from wagtail.documents.api.v2.views import DocumentsAPIViewSet
|
||
from wagtail_headless_preview.models import PagePreview
|
||
|
||
api_router = WagtailAPIRouter("wagtailapi")
|
||
|
||
api_router.register_endpoint("pages", PagesAPIViewSet)
|
||
api_router.register_endpoint("images", ImagesAPIViewSet)
|
||
api_router.register_endpoint("documents", DocumentsAPIViewSet)
|
||
|
||
|
||
class PagePreviewAPIViewSet(PagesAPIViewSet):
|
||
"""
|
||
预览模式专用只读接口:根据 `content_type`(app_label.model,小写)+ `token`
|
||
从 `wagtail_headless_preview` 存储的草稿快照中取出对应页面实例并序列化返回。
|
||
`token` 由编辑器在 Wagtail 后台点击"预览"时生成(详见 §2.15/§2.7),
|
||
未知或已过期的 token 会返回 404,不会暴露任何非预览用途的草稿数据。
|
||
"""
|
||
|
||
known_query_parameters = PagesAPIViewSet.known_query_parameters.union(
|
||
["content_type", "token"]
|
||
)
|
||
|
||
def listing_view(self, request):
|
||
# 统一走 detail_view,保证序列化格式与详情接口一致;pk 仅占位,实际查找依赖 content_type+token。
|
||
self.action = "detail_view"
|
||
return self.detail_view(request, 0)
|
||
|
||
def detail_view(self, request, pk):
|
||
page = self.get_object()
|
||
serializer = self.get_serializer(page)
|
||
return Response(serializer.data)
|
||
|
||
def get_object(self):
|
||
try:
|
||
app_label, model = self.request.GET["content_type"].split(".")
|
||
content_type = ContentType.objects.get(app_label=app_label, model=model)
|
||
page_preview = PagePreview.objects.get(
|
||
content_type=content_type, token=self.request.GET["token"]
|
||
)
|
||
except (KeyError, ValueError, ContentType.DoesNotExist, PagePreview.DoesNotExist):
|
||
# 缺少参数/content_type 格式错误/未知或已过期的 token,统一返回 404,
|
||
# 不暴露内部异常细节(详见类文档字符串)。
|
||
raise Http404("No matching page preview found.")
|
||
page = page_preview.as_page()
|
||
if not page.pk:
|
||
# 新建(尚未保存)页面的预览没有真实主键,填充占位值避免 API 路由生成 URL 时报错
|
||
page.pk = 0
|
||
return page
|
||
|
||
|
||
api_router.register_endpoint("page_preview", PagePreviewAPIViewSet)
|
||
|