feat(core): 补全剩余 6 个 StreamField Block (ProductCard/Pricing/Timeline/Team/TechStack/Video)
- apps/core/blocks.py 新增 ProductCardBlock/PricingBlock/TimelineBlock/TeamBlock/TechStackBlock/VideoBlock,COMMON_BLOCKS 扩充至 14 个内容 Block - TeamBlock 新增 TeamMemberChooserBlock(SnippetChooserBlock),复用 FormBlock 已有的 get_api_representation 展开模式,展开 TeamMember 完整字段(含 serialize_image 复用) - 为 blog/cases/home/products/solutions 五个 Page 模型生成并应用 alter field body 迁移(StreamField block 定义变更会写入迁移 state) - apps/core/tests.py 新增 TeamMemberChooserBlock 空值/展开单元测试,pytest 33 个用例全部通过 - 设计文档 §2.6/§2.16 同步更新为已实现
This commit is contained in:
@@ -7,6 +7,8 @@ from wagtail import blocks
|
||||
from wagtail.images.blocks import ImageChooserBlock
|
||||
from wagtail.snippets.blocks import SnippetChooserBlock
|
||||
|
||||
from .serializers import serialize_image
|
||||
|
||||
|
||||
class CTAButtonBlock(blocks.StructBlock):
|
||||
text = blocks.CharBlock(max_length=50, label=_("按钮文案"))
|
||||
@@ -150,6 +152,113 @@ class FormBlock(blocks.StructBlock):
|
||||
}
|
||||
|
||||
|
||||
class ProductCardItemBlock(blocks.StructBlock):
|
||||
title = blocks.CharBlock(max_length=100, label=_("标题"))
|
||||
description = blocks.TextBlock(required=False, label=_("描述"))
|
||||
image = ImageChooserBlock(required=False, label=_("图片"))
|
||||
link = blocks.URLBlock(required=False, label=_("链接"))
|
||||
|
||||
|
||||
class ProductCardBlock(blocks.StructBlock):
|
||||
heading = blocks.CharBlock(max_length=100, required=False, label=_("模块标题"))
|
||||
items = blocks.ListBlock(ProductCardItemBlock(), label=_("产品列表"))
|
||||
|
||||
class Meta:
|
||||
icon = "grip"
|
||||
label = _("产品矩阵")
|
||||
|
||||
|
||||
class PricingPlanBlock(blocks.StructBlock):
|
||||
name = blocks.CharBlock(max_length=50, label=_("方案名称"))
|
||||
price = blocks.CharBlock(max_length=50, label=_("价格"))
|
||||
features = blocks.ListBlock(
|
||||
blocks.CharBlock(max_length=100), label=_("功能列表")
|
||||
)
|
||||
highlighted = blocks.BooleanBlock(
|
||||
required=False, default=False, label=_("突出显示")
|
||||
)
|
||||
button = CTAButtonBlock(required=False, label=_("按钮"))
|
||||
|
||||
|
||||
class PricingBlock(blocks.StructBlock):
|
||||
heading = blocks.CharBlock(max_length=100, required=False, label=_("模块标题"))
|
||||
plans = blocks.ListBlock(PricingPlanBlock(), label=_("定价方案"))
|
||||
|
||||
class Meta:
|
||||
icon = "tag"
|
||||
label = _("定价")
|
||||
|
||||
|
||||
class TimelineItemBlock(blocks.StructBlock):
|
||||
year = blocks.CharBlock(max_length=20, label=_("年份/时间"))
|
||||
event = blocks.TextBlock(label=_("事件描述"))
|
||||
|
||||
|
||||
class TimelineBlock(blocks.StructBlock):
|
||||
heading = blocks.CharBlock(max_length=100, required=False, label=_("模块标题"))
|
||||
items = blocks.ListBlock(TimelineItemBlock(), label=_("时间线条目"))
|
||||
|
||||
class Meta:
|
||||
icon = "date"
|
||||
label = _("发展历程")
|
||||
|
||||
|
||||
class TeamMemberChooserBlock(SnippetChooserBlock):
|
||||
"""选择 apps.core.TeamMember Snippet,展开完整字段供前端渲染(而非仅主键)。"""
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
super().__init__("core.TeamMember", **kwargs)
|
||||
|
||||
def get_api_representation(self, value, context=None):
|
||||
if value is None:
|
||||
return None
|
||||
return {
|
||||
"id": value.pk,
|
||||
"name": value.name,
|
||||
"role": value.role,
|
||||
"bio": value.bio,
|
||||
"photo": serialize_image(value.photo),
|
||||
}
|
||||
|
||||
|
||||
class TeamBlock(blocks.StructBlock):
|
||||
heading = blocks.CharBlock(max_length=100, required=False, label=_("模块标题"))
|
||||
members = blocks.ListBlock(TeamMemberChooserBlock(), label=_("团队成员"))
|
||||
|
||||
class Meta:
|
||||
icon = "group"
|
||||
label = _("团队展示")
|
||||
|
||||
|
||||
class TechStackItemBlock(blocks.StructBlock):
|
||||
icon = blocks.CharBlock(max_length=50, required=False, label=_("图标"))
|
||||
label = blocks.CharBlock(max_length=50, label=_("名称"))
|
||||
|
||||
|
||||
class TechStackBlock(blocks.StructBlock):
|
||||
heading = blocks.CharBlock(max_length=100, required=False, label=_("模块标题"))
|
||||
items = blocks.ListBlock(TechStackItemBlock(), label=_("技术项"))
|
||||
|
||||
class Meta:
|
||||
icon = "cogs"
|
||||
label = _("技术架构")
|
||||
|
||||
|
||||
class VideoBlock(blocks.StructBlock):
|
||||
heading = blocks.CharBlock(max_length=100, required=False, label=_("标题"))
|
||||
video_url = blocks.URLBlock(
|
||||
label=_("视频地址"),
|
||||
help_text=_(
|
||||
"建议使用自建 OSS/COS 存储的 mp4 直链,或腾讯视频/哔哩哔哩等国内平台的"
|
||||
"嵌入地址,避免使用 YouTube/Vimeo(详见 §2.15 国内可访问性要求)"
|
||||
),
|
||||
)
|
||||
poster = ImageChooserBlock(required=False, label=_("封面图"))
|
||||
|
||||
class Meta:
|
||||
icon = "media"
|
||||
label = _("视频")
|
||||
|
||||
|
||||
COMMON_BLOCKS = [
|
||||
("hero", HeroBlock()),
|
||||
@@ -160,5 +269,11 @@ COMMON_BLOCKS = [
|
||||
("logo_cloud", LogoCloudBlock()),
|
||||
("case_study", CaseStudyBlock()),
|
||||
("form", FormBlock()),
|
||||
("product_card", ProductCardBlock()),
|
||||
("pricing", PricingBlock()),
|
||||
("timeline", TimelineBlock()),
|
||||
("team", TeamBlock()),
|
||||
("tech_stack", TechStackBlock()),
|
||||
("video", VideoBlock()),
|
||||
("richtext", blocks.RichTextBlock(label=_("富文本"))),
|
||||
]
|
||||
|
||||
+21
-1
@@ -2,7 +2,7 @@
|
||||
import pytest
|
||||
from rest_framework.test import APIClient
|
||||
|
||||
from apps.core.blocks import FormBlock
|
||||
from apps.core.blocks import FormBlock, TeamMemberChooserBlock
|
||||
from apps.core.models import (
|
||||
NavigationMenu,
|
||||
NavigationMenuItem,
|
||||
@@ -159,6 +159,26 @@ def test_testimonial_api_returns_list():
|
||||
assert response.data["items"][0]["author_name"] == "客户A"
|
||||
|
||||
|
||||
def test_team_member_chooser_block_api_representation_expands_fields():
|
||||
"""TeamMemberChooserBlock.get_api_representation 应展开 TeamMember 的完整字段,
|
||||
而不是仅返回 SnippetChooserBlock 默认的主键。"""
|
||||
member = TeamMember.objects.create(name="王五", role="产品经理", bio="十年行业经验")
|
||||
|
||||
block = TeamMemberChooserBlock()
|
||||
result = block.get_api_representation(member)
|
||||
|
||||
assert result["id"] == member.pk
|
||||
assert result["name"] == "王五"
|
||||
assert result["role"] == "产品经理"
|
||||
assert result["bio"] == "十年行业经验"
|
||||
assert result["photo"] is None
|
||||
|
||||
|
||||
def test_team_member_chooser_block_api_representation_handles_none():
|
||||
block = TeamMemberChooserBlock()
|
||||
assert block.get_api_representation(None) is None
|
||||
|
||||
|
||||
def test_partner_api_returns_list():
|
||||
Partner.objects.create(name="伙伴A", website_url="https://partner-a.example.com")
|
||||
|
||||
|
||||
Reference in New Issue
Block a user