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:
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -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")
|
||||
|
||||
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user