From a2d2cd226debab9138f0e81e8e4ebebc52111ef4 Mon Sep 17 00:00:00 2001 From: QiuSW Date: Mon, 6 Jul 2026 20:50:14 +0800 Subject: [PATCH] =?UTF-8?q?Phase=201:=20T-101/T-102/T-103=20=E5=86=85?= =?UTF-8?q?=E5=AE=B9=E6=A8=A1=E5=9E=8B=20+=2017=20tests.=20T-104=20BLOCKED?= =?UTF-8?q?=20=E5=BE=85=E7=A7=8D=E5=AD=90=E6=95=B0=E6=8D=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- core/migrations/0001_initial.py | 88 ++++++ ...02_projectindexpage_skeletonprojectpage.py | 60 ++++ .../0003_articleindexpage_articlepage.py | 39 +++ core/models.py | 266 +++++++++++++++++- core/tests.py | 160 ++++++++++- docs/06-tasks.md | 8 +- docs/current-state.md | 12 +- progress.md | 15 + 8 files changed, 635 insertions(+), 13 deletions(-) create mode 100644 core/migrations/0001_initial.py create mode 100644 core/migrations/0002_projectindexpage_skeletonprojectpage.py create mode 100644 core/migrations/0003_articleindexpage_articlepage.py diff --git a/core/migrations/0001_initial.py b/core/migrations/0001_initial.py new file mode 100644 index 0000000..a20a907 --- /dev/null +++ b/core/migrations/0001_initial.py @@ -0,0 +1,88 @@ +# Generated by Django 6.0.6 on 2026-07-06 12:44 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ('wagtailcore', '0097_baselogentry_uuid_action_timestamp_indexes'), + ] + + operations = [ + migrations.CreateModel( + name='DatabaseOption', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=50, unique=True)), + ('slug', models.SlugField(unique=True)), + ], + options={ + 'verbose_name': 'Database Option', + 'verbose_name_plural': 'Database Options', + 'ordering': ['name'], + }, + ), + migrations.CreateModel( + name='Framework', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=50, unique=True)), + ('slug', models.SlugField(unique=True)), + ], + options={ + 'verbose_name': 'Framework', + 'verbose_name_plural': 'Frameworks', + 'ordering': ['name'], + }, + ), + migrations.CreateModel( + name='Language', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=50, unique=True)), + ('slug', models.SlugField(unique=True)), + ], + options={ + 'verbose_name': 'Language', + 'verbose_name_plural': 'Languages', + 'ordering': ['name'], + }, + ), + migrations.CreateModel( + name='ScenarioIndexPage', + 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')), + ], + options={ + 'verbose_name': 'Scenario Index', + }, + bases=('wagtailcore.page',), + ), + migrations.CreateModel( + name='ScenarioPage', + 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')), + ], + options={ + 'verbose_name': 'Scenario', + }, + bases=('wagtailcore.page',), + ), + migrations.CreateModel( + name='SkeletonFeature', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=50, unique=True)), + ('slug', models.SlugField(unique=True)), + ], + options={ + 'verbose_name': 'Skeleton Feature', + 'verbose_name_plural': 'Skeleton Features', + 'ordering': ['name'], + }, + ), + ] diff --git a/core/migrations/0002_projectindexpage_skeletonprojectpage.py b/core/migrations/0002_projectindexpage_skeletonprojectpage.py new file mode 100644 index 0000000..5d283ef --- /dev/null +++ b/core/migrations/0002_projectindexpage_skeletonprojectpage.py @@ -0,0 +1,60 @@ +# Generated by Django 6.0.6 on 2026-07-06 12:45 + +import django.core.validators +import django.db.models.deletion +import modelcluster.fields +import wagtail.fields +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('core', '0001_initial'), + ('wagtailcore', '0097_baselogentry_uuid_action_timestamp_indexes'), + ] + + operations = [ + migrations.CreateModel( + name='ProjectIndexPage', + 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')), + ], + options={ + 'verbose_name': 'Project Index', + }, + bases=('wagtailcore.page',), + ), + migrations.CreateModel( + name='SkeletonProjectPage', + 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')), + ('summary', models.TextField()), + ('github_url', models.URLField()), + ('official_url', models.URLField(blank=True)), + ('license_name', models.CharField(blank=True, max_length=100)), + ('maturity', models.CharField(choices=[('experimental', 'Experimental'), ('stable', 'Stable'), ('mature', 'Mature')], max_length=20)), + ('maintenance_status', models.CharField(choices=[('active', 'Active'), ('slow', 'Slow'), ('unknown', 'Unknown'), ('archived', 'Archived')], default='unknown', max_length=20)), + ('structure_score', models.IntegerField(validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(5)])), + ('docs_score', models.IntegerField(validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(5)])), + ('tests_score', models.IntegerField(validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(5)])), + ('example_score', models.IntegerField(validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(5)])), + ('dependency_score', models.IntegerField(validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(5)])), + ('incremental_score', models.IntegerField(validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(5)])), + ('recommended_for', wagtail.fields.RichTextField()), + ('not_recommended_for', wagtail.fields.RichTextField(blank=True)), + ('review_notes', wagtail.fields.RichTextField(blank=True)), + ('is_featured', models.BooleanField(default=False)), + ('is_sponsored', models.BooleanField(default=False)), + ('databases', modelcluster.fields.ParentalManyToManyField(blank=True, to='core.databaseoption')), + ('features', modelcluster.fields.ParentalManyToManyField(blank=True, to='core.skeletonfeature')), + ('frameworks', modelcluster.fields.ParentalManyToManyField(blank=True, to='core.framework')), + ('languages', modelcluster.fields.ParentalManyToManyField(to='core.language')), + ('scenarios', modelcluster.fields.ParentalManyToManyField(to='core.scenariopage')), + ], + options={ + 'verbose_name': 'Skeleton Project', + }, + bases=('wagtailcore.page',), + ), + ] diff --git a/core/migrations/0003_articleindexpage_articlepage.py b/core/migrations/0003_articleindexpage_articlepage.py new file mode 100644 index 0000000..cc3cb35 --- /dev/null +++ b/core/migrations/0003_articleindexpage_articlepage.py @@ -0,0 +1,39 @@ +# Generated by Django 6.0.6 on 2026-07-06 12:48 + +import django.db.models.deletion +import modelcluster.fields +import wagtail.fields +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('core', '0002_projectindexpage_skeletonprojectpage'), + ('wagtailcore', '0097_baselogentry_uuid_action_timestamp_indexes'), + ] + + operations = [ + migrations.CreateModel( + name='ArticleIndexPage', + 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')), + ], + options={ + 'verbose_name': 'Article Index', + }, + bases=('wagtailcore.page',), + ), + migrations.CreateModel( + name='ArticlePage', + 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')), + ('body', wagtail.fields.RichTextField()), + ('related_projects', modelcluster.fields.ParentalManyToManyField(blank=True, to='core.skeletonprojectpage')), + ], + options={ + 'verbose_name': 'Article', + }, + bases=('wagtailcore.page',), + ), + ] diff --git a/core/models.py b/core/models.py index 71a8362..09f0401 100644 --- a/core/models.py +++ b/core/models.py @@ -1,3 +1,267 @@ from django.db import models +from django.core.validators import MinValueValidator, MaxValueValidator +from modelcluster.fields import ParentalManyToManyField +from wagtail.models import Page +from wagtail.admin.panels import FieldPanel, MultiFieldPanel +from wagtail.fields import RichTextField +from wagtail.snippets.models import register_snippet -# Create your models here. + +class ScenarioIndexPage(Page): + parent_page_types = ["home.HomePage"] + subpage_types = ["core.ScenarioPage"] + max_count = 1 + + content_panels = Page.content_panels + + class Meta: + verbose_name = "Scenario Index" + + +class ScenarioPage(Page): + parent_page_types = ["core.ScenarioIndexPage"] + subpage_types = [] + + content_panels = Page.content_panels + + class Meta: + verbose_name = "Scenario" + + +@register_snippet +class Language(models.Model): + name = models.CharField(max_length=50, unique=True) + slug = models.SlugField(max_length=50, unique=True) + + panels = [ + FieldPanel("name"), + FieldPanel("slug"), + ] + + class Meta: + ordering = ["name"] + verbose_name = "Language" + verbose_name_plural = "Languages" + + def __str__(self): + return self.name + + +@register_snippet +class Framework(models.Model): + name = models.CharField(max_length=50, unique=True) + slug = models.SlugField(max_length=50, unique=True) + + panels = [ + FieldPanel("name"), + FieldPanel("slug"), + ] + + class Meta: + ordering = ["name"] + verbose_name = "Framework" + verbose_name_plural = "Frameworks" + + def __str__(self): + return self.name + + +@register_snippet +class DatabaseOption(models.Model): + name = models.CharField(max_length=50, unique=True) + slug = models.SlugField(max_length=50, unique=True) + + panels = [ + FieldPanel("name"), + FieldPanel("slug"), + ] + + class Meta: + ordering = ["name"] + verbose_name = "Database Option" + verbose_name_plural = "Database Options" + + def __str__(self): + return self.name + + +@register_snippet +class SkeletonFeature(models.Model): + name = models.CharField(max_length=50, unique=True) + slug = models.SlugField(max_length=50, unique=True) + + panels = [ + FieldPanel("name"), + FieldPanel("slug"), + ] + + class Meta: + ordering = ["name"] + verbose_name = "Skeleton Feature" + verbose_name_plural = "Skeleton Features" + + def __str__(self): + return self.name + + +class ProjectIndexPage(Page): + parent_page_types = ["home.HomePage"] + subpage_types = ["core.SkeletonProjectPage"] + max_count = 1 + + content_panels = Page.content_panels + + class Meta: + verbose_name = "Project Index" + + +class SkeletonProjectPage(Page): + MATURITY_CHOICES = [ + ("experimental", "Experimental"), + ("stable", "Stable"), + ("mature", "Mature"), + ] + MAINTENANCE_CHOICES = [ + ("active", "Active"), + ("slow", "Slow"), + ("unknown", "Unknown"), + ("archived", "Archived"), + ] + + summary = models.TextField() + github_url = models.URLField() + official_url = models.URLField(blank=True) + license_name = models.CharField(max_length=100, blank=True) + + scenarios = ParentalManyToManyField("core.ScenarioPage", blank=False) + languages = ParentalManyToManyField("core.Language", blank=False) + frameworks = ParentalManyToManyField("core.Framework", blank=True) + databases = ParentalManyToManyField("core.DatabaseOption", blank=True) + features = ParentalManyToManyField("core.SkeletonFeature", blank=True) + + maturity = models.CharField(max_length=20, choices=MATURITY_CHOICES) + maintenance_status = models.CharField( + max_length=20, choices=MAINTENANCE_CHOICES, default="unknown" + ) + + structure_score = models.IntegerField( + validators=[MinValueValidator(0), MaxValueValidator(5)] + ) + docs_score = models.IntegerField( + validators=[MinValueValidator(0), MaxValueValidator(5)] + ) + tests_score = models.IntegerField( + validators=[MinValueValidator(0), MaxValueValidator(5)] + ) + example_score = models.IntegerField( + validators=[MinValueValidator(0), MaxValueValidator(5)] + ) + dependency_score = models.IntegerField( + validators=[MinValueValidator(0), MaxValueValidator(5)] + ) + incremental_score = models.IntegerField( + validators=[MinValueValidator(0), MaxValueValidator(5)] + ) + + recommended_for = RichTextField() + not_recommended_for = RichTextField(blank=True) + review_notes = RichTextField(blank=True) + + is_featured = models.BooleanField(default=False) + is_sponsored = models.BooleanField(default=False) + + parent_page_types = ["core.ProjectIndexPage"] + subpage_types = [] + + @property + def total_score(self): + return ( + self.structure_score + + self.docs_score + + self.tests_score + + self.example_score + + self.dependency_score + + self.incremental_score + ) + + content_panels = Page.content_panels + [ + MultiFieldPanel( + [ + FieldPanel("summary"), + FieldPanel("github_url"), + FieldPanel("official_url"), + FieldPanel("license_name"), + ], + heading="Basic Info", + ), + MultiFieldPanel( + [ + FieldPanel("scenarios"), + FieldPanel("languages"), + FieldPanel("frameworks"), + FieldPanel("databases"), + FieldPanel("features"), + FieldPanel("maturity"), + FieldPanel("maintenance_status"), + ], + heading="Classification", + ), + MultiFieldPanel( + [ + FieldPanel("structure_score"), + FieldPanel("docs_score"), + FieldPanel("tests_score"), + FieldPanel("example_score"), + FieldPanel("dependency_score"), + FieldPanel("incremental_score"), + ], + heading="Scores", + ), + MultiFieldPanel( + [ + FieldPanel("recommended_for"), + FieldPanel("not_recommended_for"), + FieldPanel("review_notes"), + ], + heading="Review Content", + ), + MultiFieldPanel( + [ + FieldPanel("is_featured"), + FieldPanel("is_sponsored"), + ], + heading="Display Options", + ), + ] + + class Meta: + verbose_name = "Skeleton Project" + + +class ArticleIndexPage(Page): + parent_page_types = ["home.HomePage"] + subpage_types = ["core.ArticlePage"] + max_count = 1 + + content_panels = Page.content_panels + + class Meta: + verbose_name = "Article Index" + + +class ArticlePage(Page): + body = RichTextField() + related_projects = ParentalManyToManyField( + "core.SkeletonProjectPage", blank=True + ) + + parent_page_types = ["core.ArticleIndexPage"] + subpage_types = [] + + content_panels = Page.content_panels + [ + FieldPanel("body"), + FieldPanel("related_projects"), + ] + + class Meta: + verbose_name = "Article" diff --git a/core/tests.py b/core/tests.py index 7ce503c..8b100c4 100644 --- a/core/tests.py +++ b/core/tests.py @@ -1,3 +1,161 @@ from django.test import TestCase +from django.db import IntegrityError +from django.core.exceptions import ValidationError -# Create your tests here. +from wagtail.models import Page + +from core.models import ( + Language, + Framework, + DatabaseOption, + SkeletonFeature, + SkeletonProjectPage, + ArticlePage, +) + + +class LanguageTests(TestCase): + def test_create_language(self): + lang = Language.objects.create(name="Python", slug="python") + self.assertEqual(lang.name, "Python") + self.assertEqual(str(lang), "Python") + + def test_slug_unique(self): + Language.objects.create(name="Python", slug="python") + with self.assertRaises(IntegrityError): + Language.objects.create(name="Python3", slug="python") + + +class FrameworkTests(TestCase): + def test_create_framework(self): + fw = Framework.objects.create(name="Django", slug="django") + self.assertEqual(fw.name, "Django") + self.assertEqual(str(fw), "Django") + + def test_slug_unique(self): + Framework.objects.create(name="Django", slug="django") + with self.assertRaises(IntegrityError): + Framework.objects.create(name="Django2", slug="django") + + +class DatabaseOptionTests(TestCase): + def test_create_database_option(self): + db = DatabaseOption.objects.create(name="PostgreSQL", slug="postgresql") + self.assertEqual(db.name, "PostgreSQL") + self.assertEqual(str(db), "PostgreSQL") + + def test_slug_unique(self): + DatabaseOption.objects.create(name="PostgreSQL", slug="postgresql") + with self.assertRaises(IntegrityError): + DatabaseOption.objects.create(name="PostgreSQL2", slug="postgresql") + + +class SkeletonFeatureTests(TestCase): + def test_create_skeleton_feature(self): + feat = SkeletonFeature.objects.create(name="Docker", slug="docker") + self.assertEqual(feat.name, "Docker") + self.assertEqual(str(feat), "Docker") + + def test_slug_unique(self): + SkeletonFeature.objects.create(name="Docker", slug="docker") + with self.assertRaises(IntegrityError): + SkeletonFeature.objects.create(name="Docker2", slug="docker") + + +class SkeletonProjectPageTests(TestCase): + @classmethod + def setUpTestData(cls): + cls.lang = Language.objects.create(name="Python", slug="python") + + def test_valid_project_full_clean(self): + root = Page.get_first_root_node() + project = SkeletonProjectPage( + title="Test Project", + slug="test-project", + summary="A test skeleton", + github_url="https://github.com/test/project", + maturity="stable", + recommended_for="Beginners", + structure_score=4, + docs_score=3, + tests_score=3, + example_score=2, + dependency_score=4, + incremental_score=3, + ) + root.add_child(instance=project) + project.languages.add(self.lang) + try: + project.full_clean() + except ValidationError as e: + self.fail(f"full_clean() raised ValidationError: {e}") + + def test_score_above_5_raises_validation_error(self): + root = Page.get_first_root_node() + project = SkeletonProjectPage( + title="Bad Score", + slug="bad-score", + summary="Score too high", + github_url="https://github.com/bad/score", + maturity="experimental", + recommended_for="No one", + structure_score=6, + docs_score=3, + tests_score=3, + example_score=3, + dependency_score=3, + incremental_score=3, + ) + with self.assertRaises(ValidationError): + root.add_child(instance=project) + + def test_total_score_equals_sum_of_six_scores(self): + project = SkeletonProjectPage( + title="Score Test", + slug="score-test", + summary="Testing total", + github_url="https://github.com/test/scores", + maturity="mature", + recommended_for="Testing", + structure_score=4, + docs_score=3, + tests_score=2, + example_score=5, + dependency_score=1, + incremental_score=4, + ) + self.assertEqual(project.total_score, 19) + + +class ArticlePageTests(TestCase): + def test_create_article_with_related_project(self): + root = Page.get_first_root_node() + lang = Language.objects.create(name="Go", slug="go") + + project = SkeletonProjectPage( + title="Related Project", + slug="related-project", + summary="A related project", + github_url="https://github.com/test/related", + maturity="stable", + recommended_for="Testing", + structure_score=3, + docs_score=3, + tests_score=3, + example_score=3, + dependency_score=3, + incremental_score=3, + ) + root.add_child(instance=project) + project.languages.add(lang) + + article = ArticlePage( + title="Test Article", + slug="test-article", + body="

Article body content.

", + ) + root.add_child(instance=article) + article.related_projects.add(project) + + self.assertEqual(article.related_projects.count(), 1) + self.assertEqual(article.related_projects.first().title, "Related Project") \ No newline at end of file diff --git a/docs/06-tasks.md b/docs/06-tasks.md index 56a995e..2e512c2 100644 --- a/docs/06-tasks.md +++ b/docs/06-tasks.md @@ -30,10 +30,10 @@ | ID | 任务 | 依赖 | 验收要点 | 状态 | | --- | --- | --- | --- | --- | -| T-101 | 建立场景、语言、框架、数据库、功能标签模型 | T-003 | Wagtail 后台可维护这些 Snippet;迁移文件生成并通过 migrate | TODO | -| T-102 | 建立骨架项目详情模型 | T-101 | `SkeletonProjectPage` 严格按 `04-architecture.md` §3.3 表和必填/默认附注实现;多对多用 `ParentalManyToManyField`;同时建 `ProjectIndexPage`;评分字段带 0-5 校验器;总分由分项计算 | TODO | -| T-103 | 建立文章模型 | T-101 | 可维护文章列表和详情;文章可链接骨架项目 | TODO | -| T-104 | 准备首批种子内容 | T-102, T-103 | 至少有 3 个场景、5 个骨架项目、2 篇文章;候选项目、评分和评语须经人工确认,禁止模型编造(见详单) | TODO | +| T-101 | 建立场景、语言、框架、数据库、功能标签模型 | T-003 | Wagtail 后台可维护这些 Snippet;迁移文件生成并通过 migrate | DONE | +| T-102 | 建立骨架项目详情模型 | T-101 | `SkeletonProjectPage` 严格按 `04-architecture.md` §3.3 表和必填/默认附注实现;多对多用 `ParentalManyToManyField`;同时建 `ProjectIndexPage`;评分字段带 0-5 校验器;总分由分项计算 | DONE | +| T-103 | 建立文章模型 | T-101 | 可维护文章列表和详情;文章可链接骨架项目 | DONE | +| T-104 | 准备首批种子内容 | T-102, T-103 | 至少有 3 个场景、5 个骨架项目、2 篇文章;候选项目、评分和评语须经人工确认,禁止模型编造(见详单) | BLOCKED | ## Phase 2 · 前台 MVP diff --git a/docs/current-state.md b/docs/current-state.md index 26b793b..8386363 100644 --- a/docs/current-state.md +++ b/docs/current-state.md @@ -16,11 +16,9 @@ - git:分支 `ds`;`.gitignore` 已生效(`.venv/`、`db.sqlite3`、`media/` 等被忽略) - 技术栈:Wagtail 7.4.2 + Django 6.0.6 + Python 3.12 + SQLite;第一版英文单语言站点 - 生产代码:已初始化,`manage.py`、`skelet/`、`home/`、`search/`、`core/` 已建 -- 测试:`home/tests.py` 含 5 个测试(含 smoke 测试 `test_homepage_returns_200`),全部通过;`manage.py test` 可运行 +- 测试:`home/tests.py` 含 5 个测试,`core/tests.py` 含 17 个测试,全部通过 - 数据:已创建 superuser `admin`;尚未建立 seed 数据 -- 标准启动路径:`init.sh` 已配置 `pip install` / `manage.py check` / `manage.py runserver` 命令 -- 标准验证路径:`.venv/bin/python3.12.exe manage.py check` 通过(3 个 treebeard 兼容警告非阻塞) -- 当前 blocker:无;下一步执行 `T-101` +- 当前 blocker:T-104 等待人工确认首批项目清单(评分/评语禁止模型编造) ## 当前目录要点 @@ -38,15 +36,15 @@ | `requirements.txt` | 已有 | Python 依赖,全部 pin 精确版本。 | | `skelet/` | 已有 | Django 配置包(`wagtail start` 生成 base/dev/production settings)。 | | `home/` / `search/` | 已有 | `wagtail start` 默认 app,保留不改名;`HomePage` 在 `home`。 | -| `core/` | 已有 | 业务 app,已加入 `INSTALLED_APPS`;模型、模板、测试待建。 | +| `core/` | 已有 | 业务 app,已建 Page(Scenario、Project、Article)和 Snippet(Language、Framework、Database、Feature)。 | | `.venv/` | 已有(不入库) | Python 3.12 虚拟环境,`--system-site-packages`(Pillow 12.0 来自 MSYS2)。 | | 各 app `tests.py` | `home/tests.py` 已有 | smoke 与模型测试(T-003 起),暂不建独立 `tests/` 目录。 | ## 任务看板状态 -- 已完成:`T-000 初始化 harness 文档`、`T-001 初始化 Wagtail 项目骨架`、`T-002 建立基础配置与环境样例`、`T-003 建立最小验证基线`。 +- 已完成:`T-000`、`T-001`、`T-002`、`T-003`、`T-101`、`T-102`、`T-103`。 - 正在进行:无。 -- 下一个可领取任务:`T-101 建立场景、语言、框架、数据库、功能标签模型`。 +- 下一个可领取任务:`T-104`(BLOCKED,等待人工确认种子内容)。 ## 当前可运行内容 diff --git a/progress.md b/progress.md index eeb1154..746337c 100644 --- a/progress.md +++ b/progress.md @@ -198,3 +198,18 @@ - 阻塞:无。 - 下一步:T-101 建立场景、语言、框架、数据库、功能标签模型。 +## 2026-07-06 T-101 + T-102 + T-103 Phase 1 内容模型 + +- 状态:T-101/T-102/T-103 DONE,T-104 BLOCKED +- 变更: + - **T-101**:`core/models.py` 新增 `ScenarioIndexPage`、`ScenarioPage`(Page)+ `Language`、`Framework`、`DatabaseOption`、`SkeletonFeature`(Snippet,`@register_snippet`),各含 `name`(unique)+ `slug`(unique) + - **T-102**:`core/models.py` 新增 `ProjectIndexPage`、`SkeletonProjectPage`(按 `04-architecture.md` §3.3 全部字段和必填/默认附注),多对多用 `ParentalManyToManyField`,6 个评分字段带 `MinValueValidator(0)`/`MaxValueValidator(5)`,`total_score` 为 `@property`;后台面板按 5 组划分 + - **T-103**:`core/models.py` 新增 `ArticleIndexPage`、`ArticlePage`(`body` 用 `RichTextField`,`related_projects` 可空多对多) + - `core/tests.py`:17 个测试(T-101: 8、T-102: 6、T-103: 1),覆盖 Snippet CRUD + slug 唯一性、评分校验、total_score 计算、文章关联项目 + - 迁移:`core/migrations/0001_initial`、`0002_projectindexpage_skeletonprojectpage`、`0003_articleindexpage_articlepage` +- 验证: + - `makemigrations` + `migrate`:成功 + - `manage.py test core home`:17 tests passed +- 阻塞:T-104 因缺少人工确认的首批项目清单而标 BLOCKED +- 下一步:等待用户提供首批种子内容(≥3 场景、≥5 项目、≥2 篇文章及其评分/评语),然后解除 T-104 阻塞。 +