Phase 1: T-101/T-102/T-103 内容模型 + 17 tests. T-104 BLOCKED 待种子数据
This commit is contained in:
@@ -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
@@ -1,3 +1,267 @@
|
|||||||
from django.db import models
|
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
@@ -1,3 +1,161 @@
|
|||||||
from django.test import TestCase
|
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")
|
||||||
+4
-4
@@ -30,10 +30,10 @@
|
|||||||
|
|
||||||
| ID | 任务 | 依赖 | 验收要点 | 状态 |
|
| ID | 任务 | 依赖 | 验收要点 | 状态 |
|
||||||
| --- | --- | --- | --- | --- |
|
| --- | --- | --- | --- | --- |
|
||||||
| T-101 | 建立场景、语言、框架、数据库、功能标签模型 | T-003 | Wagtail 后台可维护这些 Snippet;迁移文件生成并通过 migrate | TODO |
|
| T-101 | 建立场景、语言、框架、数据库、功能标签模型 | T-003 | Wagtail 后台可维护这些 Snippet;迁移文件生成并通过 migrate | DONE |
|
||||||
| T-102 | 建立骨架项目详情模型 | T-101 | `SkeletonProjectPage` 严格按 `04-architecture.md` §3.3 表和必填/默认附注实现;多对多用 `ParentalManyToManyField`;同时建 `ProjectIndexPage`;评分字段带 0-5 校验器;总分由分项计算 | TODO |
|
| T-102 | 建立骨架项目详情模型 | T-101 | `SkeletonProjectPage` 严格按 `04-architecture.md` §3.3 表和必填/默认附注实现;多对多用 `ParentalManyToManyField`;同时建 `ProjectIndexPage`;评分字段带 0-5 校验器;总分由分项计算 | DONE |
|
||||||
| T-103 | 建立文章模型 | T-101 | 可维护文章列表和详情;文章可链接骨架项目 | TODO |
|
| T-103 | 建立文章模型 | T-101 | 可维护文章列表和详情;文章可链接骨架项目 | DONE |
|
||||||
| T-104 | 准备首批种子内容 | T-102, T-103 | 至少有 3 个场景、5 个骨架项目、2 篇文章;候选项目、评分和评语须经人工确认,禁止模型编造(见详单) | TODO |
|
| T-104 | 准备首批种子内容 | T-102, T-103 | 至少有 3 个场景、5 个骨架项目、2 篇文章;候选项目、评分和评语须经人工确认,禁止模型编造(见详单) | BLOCKED |
|
||||||
|
|
||||||
## Phase 2 · 前台 MVP
|
## Phase 2 · 前台 MVP
|
||||||
|
|
||||||
|
|||||||
@@ -16,11 +16,9 @@
|
|||||||
- git:分支 `ds`;`.gitignore` 已生效(`.venv/`、`db.sqlite3`、`media/` 等被忽略)
|
- git:分支 `ds`;`.gitignore` 已生效(`.venv/`、`db.sqlite3`、`media/` 等被忽略)
|
||||||
- 技术栈:Wagtail 7.4.2 + Django 6.0.6 + Python 3.12 + SQLite;第一版英文单语言站点
|
- 技术栈:Wagtail 7.4.2 + Django 6.0.6 + Python 3.12 + SQLite;第一版英文单语言站点
|
||||||
- 生产代码:已初始化,`manage.py`、`skelet/`、`home/`、`search/`、`core/` 已建
|
- 生产代码:已初始化,`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 数据
|
- 数据:已创建 superuser `admin`;尚未建立 seed 数据
|
||||||
- 标准启动路径:`init.sh` 已配置 `pip install` / `manage.py check` / `manage.py runserver` 命令
|
- 当前 blocker:T-104 等待人工确认首批项目清单(评分/评语禁止模型编造)
|
||||||
- 标准验证路径:`.venv/bin/python3.12.exe manage.py check` 通过(3 个 treebeard 兼容警告非阻塞)
|
|
||||||
- 当前 blocker:无;下一步执行 `T-101`
|
|
||||||
|
|
||||||
## 当前目录要点
|
## 当前目录要点
|
||||||
|
|
||||||
@@ -38,15 +36,15 @@
|
|||||||
| `requirements.txt` | 已有 | Python 依赖,全部 pin 精确版本。 |
|
| `requirements.txt` | 已有 | Python 依赖,全部 pin 精确版本。 |
|
||||||
| `skelet/` | 已有 | Django 配置包(`wagtail start` 生成 base/dev/production settings)。 |
|
| `skelet/` | 已有 | Django 配置包(`wagtail start` 生成 base/dev/production settings)。 |
|
||||||
| `home/` / `search/` | 已有 | `wagtail start` 默认 app,保留不改名;`HomePage` 在 `home`。 |
|
| `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)。 |
|
| `.venv/` | 已有(不入库) | Python 3.12 虚拟环境,`--system-site-packages`(Pillow 12.0 来自 MSYS2)。 |
|
||||||
| 各 app `tests.py` | `home/tests.py` 已有 | smoke 与模型测试(T-003 起),暂不建独立 `tests/` 目录。 |
|
| 各 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,等待人工确认种子内容)。
|
||||||
|
|
||||||
## 当前可运行内容
|
## 当前可运行内容
|
||||||
|
|
||||||
|
|||||||
+15
@@ -198,3 +198,18 @@
|
|||||||
- 阻塞:无。
|
- 阻塞:无。
|
||||||
- 下一步:T-101 建立场景、语言、框架、数据库、功能标签模型。
|
- 下一步: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 阻塞。
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user