feat: add solutions app, fix HomePage subpage_types, add pytest test suite
- apps/solutions: SolutionIndexPage/SolutionPage (mirrors products/cases) - apps/home: HomePage.subpage_types now includes products/solutions/cases index pages (previously only blog, blocking admin page creation) - pytest.ini + conftest.py (root_page/home_page fixtures) - unit tests for core (SEOablePage, FormBlock API repr), products, cases, forms (lead submit API)
This commit is contained in:
@@ -0,0 +1,8 @@
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class SolutionsConfig(AppConfig):
|
||||
default_auto_field = "django.db.models.BigAutoField"
|
||||
name = "apps.solutions"
|
||||
label = "solutions"
|
||||
verbose_name = "解决方案"
|
||||
@@ -0,0 +1,53 @@
|
||||
# Generated by Django 6.0.5 on 2026-08-06 08:14
|
||||
|
||||
import django.db.models.deletion
|
||||
import wagtail.fields
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
('wagtailcore', '0097_baselogentry_uuid_action_timestamp_indexes'),
|
||||
('wagtailimages', '0027_image_description'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='SolutionIndexPage',
|
||||
fields=[
|
||||
('page_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='wagtailcore.page')),
|
||||
('seo_title_override', models.CharField(blank=True, help_text='留空则使用页面标题', max_length=70, verbose_name='SEO 标题')),
|
||||
('seo_description_override', models.CharField(blank=True, max_length=160, verbose_name='SEO 描述')),
|
||||
('canonical_url', models.URLField(blank=True, verbose_name='Canonical URL')),
|
||||
('schema_json', models.JSONField(blank=True, default=dict, verbose_name='结构化数据 (JSON-LD)')),
|
||||
('intro', models.TextField(blank=True, verbose_name='栏目简介')),
|
||||
('og_image', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to='wagtailimages.image', verbose_name='社交分享图')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': '解决方案栏目页',
|
||||
},
|
||||
bases=('wagtailcore.page',),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='SolutionPage',
|
||||
fields=[
|
||||
('page_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='wagtailcore.page')),
|
||||
('seo_title_override', models.CharField(blank=True, help_text='留空则使用页面标题', max_length=70, verbose_name='SEO 标题')),
|
||||
('seo_description_override', models.CharField(blank=True, max_length=160, verbose_name='SEO 描述')),
|
||||
('canonical_url', models.URLField(blank=True, verbose_name='Canonical URL')),
|
||||
('schema_json', models.JSONField(blank=True, default=dict, verbose_name='结构化数据 (JSON-LD)')),
|
||||
('industry', models.CharField(blank=True, max_length=100, verbose_name='适用行业')),
|
||||
('summary', models.CharField(blank=True, max_length=250, verbose_name='方案简介')),
|
||||
('body', wagtail.fields.StreamField([('hero', 7), ('stats', 12), ('feature_grid', 19), ('faq', 24), ('cta', 27), ('logo_cloud', 31), ('case_study', 34), ('form', 36), ('richtext', 37)], blank=True, block_lookup={0: ('wagtail.blocks.CharBlock', (), {'label': '标题', 'max_length': 120}), 1: ('wagtail.blocks.TextBlock', (), {'label': '副标题', 'required': False}), 2: ('wagtail.images.blocks.ImageChooserBlock', (), {'label': '背景图', 'required': False}), 3: ('wagtail.blocks.CharBlock', (), {'label': '按钮文案', 'max_length': 50}), 4: ('wagtail.blocks.URLBlock', (), {'label': '按钮链接'}), 5: ('wagtail.blocks.StructBlock', [[('text', 3), ('link', 4)]], {}), 6: ('wagtail.blocks.ListBlock', (5,), {'label': '按钮组'}), 7: ('wagtail.blocks.StructBlock', [[('title', 0), ('subtitle', 1), ('background_image', 2), ('buttons', 6)]], {}), 8: ('wagtail.blocks.CharBlock', (), {'label': '数值', 'max_length': 20}), 9: ('wagtail.blocks.CharBlock', (), {'label': '说明文字', 'max_length': 50}), 10: ('wagtail.blocks.StructBlock', [[('value', 8), ('label', 9)]], {}), 11: ('wagtail.blocks.ListBlock', (10,), {'label': '数据项'}), 12: ('wagtail.blocks.StructBlock', [[('items', 11)]], {}), 13: ('wagtail.blocks.CharBlock', (), {'label': '模块标题', 'max_length': 100, 'required': False}), 14: ('wagtail.blocks.CharBlock', (), {'label': '图标', 'max_length': 50, 'required': False}), 15: ('wagtail.blocks.CharBlock', (), {'label': '标题', 'max_length': 100}), 16: ('wagtail.blocks.TextBlock', (), {'label': '描述'}), 17: ('wagtail.blocks.StructBlock', [[('icon', 14), ('title', 15), ('description', 16)]], {}), 18: ('wagtail.blocks.ListBlock', (17,), {'label': '能力列表'}), 19: ('wagtail.blocks.StructBlock', [[('heading', 13), ('items', 18)]], {}), 20: ('wagtail.blocks.CharBlock', (), {'label': '问题', 'max_length': 200}), 21: ('wagtail.blocks.RichTextBlock', (), {'label': '回答'}), 22: ('wagtail.blocks.StructBlock', [[('question', 20), ('answer', 21)]], {}), 23: ('wagtail.blocks.ListBlock', (22,), {'label': '问答列表'}), 24: ('wagtail.blocks.StructBlock', [[('items', 23)]], {}), 25: ('wagtail.blocks.TextBlock', (), {'label': '描述', 'required': False}), 26: ('wagtail.blocks.StructBlock', [[('text', 3), ('link', 4)]], {'label': '按钮'}), 27: ('wagtail.blocks.StructBlock', [[('heading', 0), ('description', 25), ('button', 26)]], {}), 28: ('wagtail.blocks.CharBlock', (), {'label': '标题', 'max_length': 100, 'required': False}), 29: ('wagtail.images.blocks.ImageChooserBlock', (), {}), 30: ('wagtail.blocks.ListBlock', (29,), {'label': 'Logo 列表'}), 31: ('wagtail.blocks.StructBlock', [[('heading', 28), ('logos', 30)]], {}), 32: ('wagtail.blocks.PageChooserBlock', (), {'label': '案例页面', 'page_type': ['cases.CaseStudyPage']}), 33: ('wagtail.blocks.CharBlock', (), {'label': '摘要覆盖', 'max_length': 250, 'required': False}), 34: ('wagtail.blocks.StructBlock', [[('case_page', 32), ('summary', 33)]], {}), 35: ('wagtail.snippets.blocks.SnippetChooserBlock', ('forms.FormDefinition',), {'label': '选择表单'}), 36: ('wagtail.blocks.StructBlock', [[('form', 35)]], {}), 37: ('wagtail.blocks.RichTextBlock', (), {'label': '富文本'})})),
|
||||
('cover_image', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to='wagtailimages.image', verbose_name='方案配图')),
|
||||
('og_image', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to='wagtailimages.image', verbose_name='社交分享图')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': '解决方案页',
|
||||
},
|
||||
bases=('wagtailcore.page',),
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,66 @@
|
||||
"""行业解决方案栏目与详情页。详见 documents/设计方案分析与完善版.md §2.5 页面树结构。"""
|
||||
from django.db import models
|
||||
from wagtail.admin.panels import FieldPanel
|
||||
from wagtail.fields import StreamField
|
||||
from wagtail.search import index
|
||||
|
||||
from apps.core.blocks import COMMON_BLOCKS
|
||||
from apps.core.models import SEOablePage
|
||||
|
||||
|
||||
class SolutionIndexPage(SEOablePage):
|
||||
"""解决方案栏目列表页,子页面为 SolutionPage。"""
|
||||
|
||||
intro = models.TextField(blank=True, verbose_name="栏目简介")
|
||||
|
||||
content_panels = SEOablePage.content_panels + [
|
||||
FieldPanel("intro"),
|
||||
]
|
||||
|
||||
subpage_types = ["solutions.SolutionPage"]
|
||||
parent_page_types = ["home.HomePage"]
|
||||
|
||||
class Meta:
|
||||
verbose_name = "解决方案栏目页"
|
||||
|
||||
def get_context(self, request, *args, **kwargs):
|
||||
context = super().get_context(request, *args, **kwargs)
|
||||
context["solutions"] = (
|
||||
SolutionPage.objects.live().descendant_of(self).order_by("path")
|
||||
)
|
||||
return context
|
||||
|
||||
|
||||
class SolutionPage(SEOablePage):
|
||||
"""单个行业解决方案详情页。"""
|
||||
|
||||
industry = models.CharField(max_length=100, blank=True, verbose_name="适用行业")
|
||||
summary = models.CharField(max_length=250, blank=True, verbose_name="方案简介")
|
||||
cover_image = models.ForeignKey(
|
||||
"wagtailimages.Image",
|
||||
null=True,
|
||||
blank=True,
|
||||
on_delete=models.SET_NULL,
|
||||
related_name="+",
|
||||
verbose_name="方案配图",
|
||||
)
|
||||
body = StreamField(COMMON_BLOCKS, use_json_field=True, blank=True)
|
||||
|
||||
search_fields = SEOablePage.search_fields + [
|
||||
index.SearchField("industry"),
|
||||
index.SearchField("summary"),
|
||||
index.SearchField("body"),
|
||||
]
|
||||
|
||||
content_panels = SEOablePage.content_panels + [
|
||||
FieldPanel("industry"),
|
||||
FieldPanel("summary"),
|
||||
FieldPanel("cover_image"),
|
||||
FieldPanel("body"),
|
||||
]
|
||||
|
||||
parent_page_types = ["solutions.SolutionIndexPage"]
|
||||
subpage_types = []
|
||||
|
||||
class Meta:
|
||||
verbose_name = "解决方案页"
|
||||
@@ -0,0 +1,14 @@
|
||||
{% load wagtailcore_tags %}
|
||||
<!doctype html>
|
||||
<html lang="zh-hans">
|
||||
<head><meta charset="utf-8"><title>{{ page.title }}</title></head>
|
||||
<body>
|
||||
<h1>{{ page.title }}</h1>
|
||||
<p>{{ page.intro }}</p>
|
||||
<ul>
|
||||
{% for solution in solutions %}
|
||||
<li><a href="{% pageurl solution %}">{{ solution.title }}</a></li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,13 @@
|
||||
{% load wagtailcore_tags %}
|
||||
<!doctype html>
|
||||
<html lang="zh-hans">
|
||||
<head><meta charset="utf-8"><title>{{ page.title }}</title></head>
|
||||
<body>
|
||||
<h1>{{ page.title }}</h1>
|
||||
<p>{{ page.industry }}</p>
|
||||
<p>{{ page.summary }}</p>
|
||||
{% for block in page.body %}
|
||||
{% include_block block %}
|
||||
{% endfor %}
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user