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.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.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")
|
||||
Reference in New Issue
Block a user