diff --git a/core/management/commands/seed_data.py b/core/management/commands/seed_data.py index 240a69d..47df3c3 100644 --- a/core/management/commands/seed_data.py +++ b/core/management/commands/seed_data.py @@ -19,6 +19,19 @@ from core.models import ( class Command(BaseCommand): help = "Seed database with user-confirmed initial content" + def add_arguments(self, parser): + parser.add_argument( + "--clear", + action="store_true", + help="Delete existing content pages before seeding", + ) + + def _clear_existing(self): + for model in [ArticlePage, SkeletonProjectPage, ProjectIndexPage, + ScenarioPage, ScenarioIndexPage, ArticleIndexPage]: + model.objects.all().delete() + self.stdout.write(" Cleared existing content pages") + def _create_snippets(self): languages = [ ("Python", "python"), @@ -50,6 +63,13 @@ class Command(BaseCommand): def _create_pages(self): home = HomePage.objects.get(slug="home") + if ScenarioIndexPage.objects.filter(slug="scenarios").exists(): + scenario_index = ScenarioIndexPage.objects.get(slug="scenarios") + project_index = ProjectIndexPage.objects.get(slug="projects") + article_index = ArticleIndexPage.objects.get(slug="articles") + self.stdout.write(" Page tree already exists, skipping") + return scenario_index, project_index, article_index + scenario_index = ScenarioIndexPage(title="Scenarios", slug="scenarios") home.add_child(instance=scenario_index) @@ -68,7 +88,7 @@ class Command(BaseCommand): article_index = ArticleIndexPage(title="Articles", slug="articles") home.add_child(instance=article_index) - self.stdout.write(" Page tree created") + self.stdout.write(" Page tree ready") return scenario_index, project_index, article_index def _get_snippets(self): @@ -226,6 +246,10 @@ class Command(BaseCommand): ] for proj in projects: + if SkeletonProjectPage.objects.filter(slug=proj["slug"]).exists(): + self.stdout.write(f" Skipping existing project: {proj['title']}") + continue + scenarios = proj.pop("scenarios") languages = proj.pop("languages") frameworks = proj.pop("frameworks") @@ -239,6 +263,7 @@ class Command(BaseCommand): project.frameworks.add(*frameworks) if databases: project.databases.add(*databases) + project.save() self.stdout.write(f" Created project: {proj['title']}") def _create_articles(self, article_index): @@ -297,13 +322,25 @@ class Command(BaseCommand): ] for art in articles: + if ArticlePage.objects.filter(slug=art["slug"]).exists(): + self.stdout.write(f" Skipping existing article: {art['title']}") + continue + related = art.pop("related_projects") article = ArticlePage(**art) article_index.add_child(instance=article) article.related_projects.add(*related) + article.save() self.stdout.write(f" Created article: {art['title']}") def handle(self, *args, **options): + if options["clear"]: + self._clear_existing() + + if not options["clear"] and ScenarioIndexPage.objects.filter(slug="scenarios").exists(): + self.stdout.write("Seed data already exists. Use --clear to reset.") + return + self.stdout.write("Creating snippets...") self._create_snippets() @@ -320,6 +357,19 @@ class Command(BaseCommand): # Verify self.stdout.write("") self.stdout.write("=== Seed verification ===") + for proj in SkeletonProjectPage.objects.all(): + sc = proj.scenarios.count() + la = proj.languages.count() + status = "OK" if sc >= 1 and la >= 1 else "MISSING_M2M" + self.stdout.write( + f" {proj.title}: scenarios={sc}, languages={la} [{status}]" + ) + for art in ArticlePage.objects.all(): + rp = art.related_projects.count() + status = "OK" if rp >= 1 else "MISSING_M2M" + self.stdout.write( + f" {art.title}: related_projects={rp} [{status}]" + ) self.stdout.write( f"Scenarios: {ScenarioPage.objects.count()} " f"(≥3: {ScenarioPage.objects.count() >= 3})" diff --git a/core/tests.py b/core/tests.py index 7df0f4e..6adde42 100644 --- a/core/tests.py +++ b/core/tests.py @@ -161,6 +161,16 @@ class SkeletonProjectPageTests(PageTreeMixin, TestCase): except ValidationError: self.fail("full_clean() should pass with all M2M set") + def test_m2m_persists_after_save_and_reload(self): + project = self._make_project() + project.languages.add(self.lang) + project.scenarios.add(self.scenario) + project.save() + + reloaded = SkeletonProjectPage.objects.get(pk=project.pk) + self.assertGreaterEqual(reloaded.scenarios.count(), 1) + self.assertGreaterEqual(reloaded.languages.count(), 1) + class ArticlePageTests(PageTreeMixin, TestCase): @classmethod diff --git a/progress.md b/progress.md index 17c8a8f..ad01929 100644 --- a/progress.md +++ b/progress.md @@ -246,3 +246,16 @@ - 阻塞:无。 - 下一步:T-201 实现基础页面框架(Phase 2 前台 MVP)。 +## 2026-07-06 T-104 seed_data M2M 持久化修复 + +- 状态:DONE +- 变更: + - `core/management/commands/seed_data.py`:`add_child()` 后 M2M `.add()` 仅在内存操作 cluster,不持久化到 DB;新增 `project.save()` / `article.save()` 确保关联写入 + - 添加 `--clear` 参数和重复运行保护 + - `core/tests.py`:新增 `test_m2m_persists_after_save_and_reload` 测试——save() 后从 DB 重新加载,断言 `scenarios.count() >= 1` 和 `languages.count() >= 1` +- 验证: + - `manage.py seed_data --clear`:5 个项目全部 `scenarios≥1, languages≥1 [OK]`,2 篇文章全部 `related_projects=1 [OK]` + - `manage.py test core home`:21 tests passed(新增 1 个持久化测试) +- 阻塞:无。 +- 下一步:T-201 实现基础页面框架(Phase 2 前台 MVP)。 +