Phase 1: T-101/T-102/T-103 内容模型 + 17 tests. T-104 BLOCKED 待种子数据

This commit is contained in:
ila
2026-07-06 20:50:14 +08:00
parent fdec385afc
commit a2d2cd226d
8 changed files with 635 additions and 13 deletions
+88
View File
@@ -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'],
},
),
]
@@ -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',),
),
]
@@ -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',),
),
]
+265 -1
View File
@@ -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"
+159 -1
View File
@@ -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="<p>Article body content.</p>",
)
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")