T-104 M2M 持久化修复:seed_data.py save() + 持久化测试(21 tests)
This commit is contained in:
@@ -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})"
|
||||
|
||||
@@ -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
|
||||
|
||||
+13
@@ -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)。
|
||||
|
||||
|
||||
Reference in New Issue
Block a user