Phase 2: T-201 至 T-206 前台 MVP 全部实现(11 templates + CSS + 31 tests)
This commit is contained in:
@@ -1,4 +1,6 @@
|
||||
from django.core.paginator import Paginator, EmptyPage
|
||||
from django.db import models
|
||||
from django.db.models import Q
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.core.validators import MinValueValidator, MaxValueValidator
|
||||
from modelcluster.fields import ParentalManyToManyField
|
||||
@@ -25,6 +27,18 @@ class ScenarioPage(Page):
|
||||
|
||||
content_panels = Page.content_panels
|
||||
|
||||
template = "core/scenario_page.html"
|
||||
|
||||
def get_context(self, request, *args, **kwargs):
|
||||
context = super().get_context(request, *args, **kwargs)
|
||||
context["projects"] = (
|
||||
SkeletonProjectPage.objects.live()
|
||||
.filter(scenarios=self)
|
||||
.prefetch_related("languages", "scenarios")
|
||||
.order_by("-first_published_at")
|
||||
)
|
||||
return context
|
||||
|
||||
class Meta:
|
||||
verbose_name = "Scenario"
|
||||
|
||||
@@ -112,11 +126,65 @@ class ProjectIndexPage(Page):
|
||||
|
||||
content_panels = Page.content_panels
|
||||
|
||||
template = "core/project_index_page.html"
|
||||
|
||||
def get_context(self, request, *args, **kwargs):
|
||||
context = super().get_context(request, *args, **kwargs)
|
||||
projects = (
|
||||
SkeletonProjectPage.objects.live()
|
||||
.prefetch_related("languages", "scenarios")
|
||||
.order_by("-first_published_at")
|
||||
)
|
||||
|
||||
language = request.GET.get("language", "")
|
||||
framework = request.GET.get("framework", "")
|
||||
database = request.GET.get("database", "")
|
||||
min_score = request.GET.get("min_score", "")
|
||||
q = request.GET.get("q", "")
|
||||
|
||||
if language:
|
||||
projects = projects.filter(languages__slug=language)
|
||||
if framework:
|
||||
projects = projects.filter(frameworks__slug=framework)
|
||||
if database:
|
||||
projects = projects.filter(databases__slug=database)
|
||||
try:
|
||||
score_val = int(min_score)
|
||||
if 0 <= score_val <= 30:
|
||||
projects = [
|
||||
p for p in projects if p.total_score >= score_val
|
||||
]
|
||||
except (ValueError, TypeError):
|
||||
pass
|
||||
if q:
|
||||
projects = projects.filter(
|
||||
Q(title__icontains=q) | Q(summary__icontains=q)
|
||||
)
|
||||
|
||||
paginator = Paginator(projects, 20)
|
||||
page = request.GET.get("page", "1")
|
||||
try:
|
||||
context["projects"] = paginator.page(int(page))
|
||||
except (ValueError, EmptyPage):
|
||||
context["projects"] = paginator.page(1)
|
||||
|
||||
context["current_language"] = language
|
||||
context["current_framework"] = framework
|
||||
context["current_database"] = database
|
||||
context["current_min_score"] = min_score
|
||||
context["current_q"] = q
|
||||
context["languages"] = Language.objects.all()
|
||||
context["frameworks"] = Framework.objects.all()
|
||||
context["databases"] = DatabaseOption.objects.all()
|
||||
return context
|
||||
|
||||
class Meta:
|
||||
verbose_name = "Project Index"
|
||||
|
||||
|
||||
class SkeletonProjectPage(Page):
|
||||
template = "core/skeleton_project_page.html"
|
||||
|
||||
MATURITY_CHOICES = [
|
||||
("experimental", "Experimental"),
|
||||
("stable", "Stable"),
|
||||
@@ -255,6 +323,7 @@ class SkeletonProjectPage(Page):
|
||||
|
||||
|
||||
class ArticleIndexPage(Page):
|
||||
template = "core/article_index_page.html"
|
||||
parent_page_types = ["home.HomePage"]
|
||||
subpage_types = ["core.ArticlePage"]
|
||||
max_count = 1
|
||||
@@ -266,6 +335,7 @@ class ArticleIndexPage(Page):
|
||||
|
||||
|
||||
class ArticlePage(Page):
|
||||
template = "core/article_page.html"
|
||||
body = RichTextField()
|
||||
related_projects = ParentalManyToManyField(
|
||||
"core.SkeletonProjectPage", blank=True
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
<h1>{{ page.title }}</h1>
|
||||
|
||||
{% if page.get_children.live %}
|
||||
<div class="card-grid">
|
||||
{% for article in page.get_children.live %}
|
||||
<a class="card" href="{% url 'wagtail_serve' '' %}articles/{{ article.slug }}/">
|
||||
<h3>{{ article.title }}</h3>
|
||||
</a>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% else %}
|
||||
{% include "includes/empty_state.html" with message="No articles yet." %}
|
||||
{% endif %}
|
||||
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,21 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
<h1>{{ page.title }}</h1>
|
||||
|
||||
<div class="rich-text">{{ page.body|safe }}</div>
|
||||
|
||||
{% if page.related_projects.exists %}
|
||||
<h2>Related Projects</h2>
|
||||
<div class="card-grid">
|
||||
{% for project in page.related_projects.all %}
|
||||
<a class="card" href="{% url 'wagtail_serve' '' %}projects/{{ project.slug }}/">
|
||||
<h3>{{ project.title }}</h3>
|
||||
<p>{{ project.summary|truncatewords:20 }}</p>
|
||||
</a>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,85 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
<h1>{{ page.title }}</h1>
|
||||
|
||||
<form class="filters" method="get">
|
||||
<label>
|
||||
Language:
|
||||
<select name="language">
|
||||
<option value="">All</option>
|
||||
{% for lang in languages %}
|
||||
<option value="{{ lang.slug }}" {% if current_language == lang.slug %}selected{% endif %}>{{ lang.name }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</label>
|
||||
|
||||
<label>
|
||||
Framework:
|
||||
<select name="framework">
|
||||
<option value="">All</option>
|
||||
{% for fw in frameworks %}
|
||||
<option value="{{ fw.slug }}" {% if current_framework == fw.slug %}selected{% endif %}>{{ fw.name }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</label>
|
||||
|
||||
<label>
|
||||
Database:
|
||||
<select name="database">
|
||||
<option value="">All</option>
|
||||
{% for db in databases %}
|
||||
<option value="{{ db.slug }}" {% if current_database == db.slug %}selected{% endif %}>{{ db.name }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</label>
|
||||
|
||||
<label>
|
||||
Min Score:
|
||||
<input type="number" name="min_score" min="0" max="30" value="{{ current_min_score }}" style="width:5ch;">
|
||||
</label>
|
||||
|
||||
<label>
|
||||
Search:
|
||||
<input type="text" name="q" value="{{ current_q }}" placeholder="Keyword...">
|
||||
</label>
|
||||
|
||||
<button type="submit">Filter</button>
|
||||
</form>
|
||||
|
||||
{% if projects %}
|
||||
<div class="card-grid">
|
||||
{% for project in projects %}
|
||||
<a class="card" href="{% url 'wagtail_serve' '' %}projects/{{ project.slug }}/">
|
||||
<h3>{{ project.title }}</h3>
|
||||
<p>{{ project.summary|truncatewords:20 }}</p>
|
||||
<div class="score-bar">
|
||||
<span class="score-total">{{ project.total_score }}/30</span>
|
||||
<span class="score-detail">AI-Friendly Score</span>
|
||||
</div>
|
||||
<div class="tag-list">
|
||||
{% for lang in project.languages.all|slice:":3" %}
|
||||
<span class="tag">{{ lang.name }}</span>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</a>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
{% if projects.paginator.num_pages > 1 %}
|
||||
<nav class="pagination">
|
||||
{% if projects.has_previous %}
|
||||
<a href="?{% for k,v in request.GET.items %}{% if k != 'page' %}{{ k }}={{ v }}&{% endif %}{% endfor %}page={{ projects.previous_page_number }}">« Prev</a>
|
||||
{% endif %}
|
||||
<span class="current">Page {{ projects.number }} of {{ projects.paginator.num_pages }}</span>
|
||||
{% if projects.has_next %}
|
||||
<a href="?{% for k,v in request.GET.items %}{% if k != 'page' %}{{ k }}={{ v }}&{% endif %}{% endfor %}page={{ projects.next_page_number }}">Next »</a>
|
||||
{% endif %}
|
||||
</nav>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
{% include "includes/empty_state.html" with message="No projects match your filters." %}
|
||||
{% endif %}
|
||||
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,29 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
<h1>{{ page.title }}</h1>
|
||||
|
||||
{% if projects %}
|
||||
<div class="card-grid">
|
||||
{% for project in projects %}
|
||||
<a class="card" href="{% url 'wagtail_serve' '' %}projects/{{ project.slug }}/">
|
||||
<h3>{{ project.title }}</h3>
|
||||
<p>{{ project.summary|truncatewords:20 }}</p>
|
||||
<div class="score-bar">
|
||||
<span class="score-total">{{ project.total_score }}/30</span>
|
||||
<span class="score-detail">AI-Friendly Score</span>
|
||||
</div>
|
||||
<div class="tag-list">
|
||||
{% for lang in project.languages.all|slice:":3" %}
|
||||
<span class="tag">{{ lang.name }}</span>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</a>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% else %}
|
||||
{% include "includes/empty_state.html" with message="No projects found in this scenario." %}
|
||||
{% endif %}
|
||||
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,76 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
<article>
|
||||
<h1>
|
||||
{{ page.title }}
|
||||
{% if page.is_featured %}<span class="badge badge-featured">Featured</span>{% endif %}
|
||||
{% if page.is_sponsored %}{% include "includes/sponsored_badge.html" %}{% endif %}
|
||||
</h1>
|
||||
|
||||
<p class="rich-text">{{ page.summary }}</p>
|
||||
|
||||
<table class="data-table">
|
||||
<tr><th>GitHub</th><td><a href="{{ page.github_url }}" target="_blank" rel="noopener">{{ page.github_url }}</a></td></tr>
|
||||
{% if page.official_url %}
|
||||
<tr><th>Official Site</th><td><a href="{{ page.official_url }}" target="_blank" rel="noopener">{{ page.official_url }}</a></td></tr>
|
||||
{% endif %}
|
||||
{% if page.license_name %}
|
||||
<tr><th>License</th><td>{{ page.license_name }}</td></tr>
|
||||
{% endif %}
|
||||
<tr><th>Maturity</th><td>{{ page.get_maturity_display }}</td></tr>
|
||||
<tr><th>Maintenance</th><td>{{ page.get_maintenance_status_display }}</td></tr>
|
||||
{% if page.scenarios.exists %}
|
||||
<tr><th>Scenarios</th><td>
|
||||
{% for s in page.scenarios.all %}<span class="tag">{{ s.title }}</span>{% endfor %}
|
||||
</td></tr>
|
||||
{% endif %}
|
||||
{% if page.languages.exists %}
|
||||
<tr><th>Languages</th><td>
|
||||
{% for l in page.languages.all %}<span class="tag">{{ l.name }}</span>{% endfor %}
|
||||
</td></tr>
|
||||
{% endif %}
|
||||
{% if page.frameworks.exists %}
|
||||
<tr><th>Frameworks</th><td>
|
||||
{% for f in page.frameworks.all %}<span class="tag">{{ f.name }}</span>{% endfor %}
|
||||
</td></tr>
|
||||
{% endif %}
|
||||
{% if page.databases.exists %}
|
||||
<tr><th>Databases</th><td>
|
||||
{% for d in page.databases.all %}<span class="tag">{{ d.name }}</span>{% endfor %}
|
||||
</td></tr>
|
||||
{% endif %}
|
||||
{% if page.features.exists %}
|
||||
<tr><th>Features</th><td>
|
||||
{% for feat in page.features.all %}<span class="tag">{{ feat.name }}</span>{% endfor %}
|
||||
</td></tr>
|
||||
{% endif %}
|
||||
</table>
|
||||
|
||||
<h2>Scores</h2>
|
||||
<table class="data-table">
|
||||
<tr><th>Structure</th><td>{{ page.structure_score }}/5</td></tr>
|
||||
<tr><th>Documentation</th><td>{{ page.docs_score }}/5</td></tr>
|
||||
<tr><th>Testing</th><td>{{ page.tests_score }}/5</td></tr>
|
||||
<tr><th>Examples</th><td>{{ page.example_score }}/5</td></tr>
|
||||
<tr><th>Dependency Control</th><td>{{ page.dependency_score }}/5</td></tr>
|
||||
<tr><th>Incremental Development</th><td>{{ page.incremental_score }}/5</td></tr>
|
||||
<tr><th>Total</th><td><strong>{{ page.total_score }}/30</strong></td></tr>
|
||||
</table>
|
||||
|
||||
<h2>Recommended For</h2>
|
||||
<div class="rich-text">{{ page.recommended_for|safe }}</div>
|
||||
|
||||
{% if page.not_recommended_for %}
|
||||
<h2>Not Recommended For</h2>
|
||||
<div class="rich-text">{{ page.not_recommended_for|safe }}</div>
|
||||
{% endif %}
|
||||
|
||||
{% if page.review_notes %}
|
||||
<h2>Review Notes</h2>
|
||||
<div class="rich-text">{{ page.review_notes|safe }}</div>
|
||||
{% endif %}
|
||||
</article>
|
||||
|
||||
{% endblock %}
|
||||
+200
-17
@@ -20,6 +20,206 @@ from core.models import (
|
||||
)
|
||||
|
||||
|
||||
class PageTreeMixin:
|
||||
@classmethod
|
||||
def setUpPageTree(cls):
|
||||
cls.home = HomePage.objects.get(slug="home")
|
||||
|
||||
cls.scenario_index = ScenarioIndexPage(title="Scenarios", slug="scenarios")
|
||||
cls.home.add_child(instance=cls.scenario_index)
|
||||
cls.scenario = ScenarioPage(title="SaaS", slug="saas")
|
||||
cls.scenario_index.add_child(instance=cls.scenario)
|
||||
|
||||
cls.project_index = ProjectIndexPage(title="Projects", slug="projects")
|
||||
cls.home.add_child(instance=cls.project_index)
|
||||
|
||||
cls.article_index = ArticleIndexPage(title="Articles", slug="articles")
|
||||
cls.home.add_child(instance=cls.article_index)
|
||||
|
||||
|
||||
class ScenarioPageTests(PageTreeMixin, TestCase):
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
cls.setUpPageTree()
|
||||
cls.lang = Language.objects.create(name="Python", slug="python")
|
||||
|
||||
def test_scenario_page_with_projects(self):
|
||||
project = SkeletonProjectPage(
|
||||
title="Scenario Project",
|
||||
slug="scenario-project",
|
||||
summary="A project in this scenario",
|
||||
github_url="https://github.com/test/sproject",
|
||||
maturity="stable",
|
||||
recommended_for="Testing",
|
||||
structure_score=3, docs_score=3, tests_score=3,
|
||||
example_score=3, dependency_score=3, incremental_score=3,
|
||||
)
|
||||
self.project_index.add_child(instance=project)
|
||||
project.languages.add(self.lang)
|
||||
project.scenarios.add(self.scenario)
|
||||
project.save()
|
||||
|
||||
response = self.client.get(self.scenario.url)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertContains(response, "Scenario Project")
|
||||
|
||||
def test_empty_scenario_page(self):
|
||||
ScenarioPage.objects.create(
|
||||
title="Empty Scenario",
|
||||
slug="empty-scenario",
|
||||
path=self.scenario_index.path + "9999",
|
||||
depth=self.scenario_index.depth + 1,
|
||||
)
|
||||
response = self.client.get("/scenarios/empty-scenario/")
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertContains(response, "Nothing here yet")
|
||||
|
||||
|
||||
class ProjectFilterTests(PageTreeMixin, TestCase):
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
cls.setUpPageTree()
|
||||
cls.lang_py = Language.objects.create(name="Python", slug="python")
|
||||
cls.lang_go = Language.objects.create(name="Go", slug="go")
|
||||
cls.fw_dj = Framework.objects.create(name="Django", slug="django")
|
||||
cls.db_pg = DatabaseOption.objects.create(name="PostgreSQL", slug="postgresql")
|
||||
|
||||
cls.proj_a = SkeletonProjectPage(
|
||||
title="Django Project",
|
||||
slug="django-project",
|
||||
summary="A Django project for testing",
|
||||
github_url="https://github.com/test/django",
|
||||
maturity="stable",
|
||||
recommended_for="Testing",
|
||||
structure_score=5, docs_score=5, tests_score=5,
|
||||
example_score=5, dependency_score=5, incremental_score=5,
|
||||
)
|
||||
cls.project_index.add_child(instance=cls.proj_a)
|
||||
cls.proj_a.languages.add(cls.lang_py)
|
||||
cls.proj_a.frameworks.add(cls.fw_dj)
|
||||
cls.proj_a.databases.add(cls.db_pg)
|
||||
cls.proj_a.scenarios.add(cls.scenario)
|
||||
cls.proj_a.save()
|
||||
|
||||
cls.proj_b = SkeletonProjectPage(
|
||||
title="Go Project",
|
||||
slug="go-project",
|
||||
summary="A Go project for testing",
|
||||
github_url="https://github.com/test/go",
|
||||
maturity="experimental",
|
||||
recommended_for="Testing",
|
||||
structure_score=1, docs_score=1, tests_score=1,
|
||||
example_score=1, dependency_score=1, incremental_score=1,
|
||||
)
|
||||
cls.project_index.add_child(instance=cls.proj_b)
|
||||
cls.proj_b.languages.add(cls.lang_go)
|
||||
cls.proj_b.scenarios.add(cls.scenario)
|
||||
cls.proj_b.save()
|
||||
|
||||
def test_language_filter(self):
|
||||
resp = self.client.get("/projects/?language=python")
|
||||
self.assertContains(resp, "Django Project")
|
||||
self.assertNotContains(resp, "Go Project")
|
||||
|
||||
def test_min_score_filter(self):
|
||||
resp = self.client.get("/projects/?min_score=18")
|
||||
self.assertContains(resp, "Django Project")
|
||||
self.assertNotContains(resp, "Go Project")
|
||||
|
||||
def test_keyword_search(self):
|
||||
resp = self.client.get("/projects/?q=Django")
|
||||
self.assertContains(resp, "Django Project")
|
||||
self.assertNotContains(resp, "Go Project")
|
||||
|
||||
def test_invalid_params_ignored(self):
|
||||
resp = self.client.get("/projects/?min_score=abc&page=xyz")
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
|
||||
|
||||
class SkeletonProjectDetailTests(PageTreeMixin, TestCase):
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
cls.setUpPageTree()
|
||||
cls.lang = Language.objects.create(name="Ruby", slug="ruby")
|
||||
|
||||
def test_detail_shows_scores_and_recommendation(self):
|
||||
project = SkeletonProjectPage(
|
||||
title="Detail Test",
|
||||
slug="detail-test",
|
||||
summary="Testing detail page",
|
||||
github_url="https://github.com/test/detail",
|
||||
maturity="stable",
|
||||
recommended_for="Developers who need structure",
|
||||
structure_score=4, docs_score=3, tests_score=3,
|
||||
example_score=4, dependency_score=3, incremental_score=4,
|
||||
)
|
||||
self.project_index.add_child(instance=project)
|
||||
project.languages.add(self.lang)
|
||||
project.scenarios.add(self.scenario)
|
||||
project.save()
|
||||
|
||||
resp = self.client.get(project.url)
|
||||
self.assertContains(resp, "Detail Test")
|
||||
self.assertContains(resp, "21/30")
|
||||
self.assertContains(resp, "Developers who need structure")
|
||||
|
||||
def test_sponsored_badge(self):
|
||||
project = SkeletonProjectPage(
|
||||
title="Sponsored Project",
|
||||
slug="sponsored-project",
|
||||
summary="A sponsored test",
|
||||
github_url="https://github.com/test/sponsored",
|
||||
maturity="experimental",
|
||||
recommended_for="Sponsored users",
|
||||
structure_score=2, docs_score=2, tests_score=2,
|
||||
example_score=2, dependency_score=2, incremental_score=2,
|
||||
is_sponsored=True,
|
||||
)
|
||||
self.project_index.add_child(instance=project)
|
||||
project.languages.add(self.lang)
|
||||
project.scenarios.add(self.scenario)
|
||||
project.save()
|
||||
|
||||
resp = self.client.get(project.url)
|
||||
self.assertContains(resp, "Sponsored")
|
||||
|
||||
|
||||
class ArticleDetailTests(PageTreeMixin, TestCase):
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
cls.setUpPageTree()
|
||||
cls.lang = Language.objects.create(name="Rust", slug="rust")
|
||||
|
||||
def test_article_links_to_project(self):
|
||||
project = SkeletonProjectPage(
|
||||
title="Linked Project",
|
||||
slug="linked-project",
|
||||
summary="Project linked from article",
|
||||
github_url="https://github.com/test/linked",
|
||||
maturity="stable",
|
||||
recommended_for="Testing",
|
||||
structure_score=3, docs_score=3, tests_score=3,
|
||||
example_score=3, dependency_score=3, incremental_score=3,
|
||||
)
|
||||
self.project_index.add_child(instance=project)
|
||||
project.languages.add(self.lang)
|
||||
project.scenarios.add(self.scenario)
|
||||
project.save()
|
||||
|
||||
article = ArticlePage(
|
||||
title="Review Article",
|
||||
slug="review-article",
|
||||
body="<p>About the linked project.</p>",
|
||||
)
|
||||
self.article_index.add_child(instance=article)
|
||||
article.related_projects.add(project)
|
||||
article.save()
|
||||
|
||||
resp = self.client.get(article.url)
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
self.assertContains(resp, "/projects/linked-project/")
|
||||
|
||||
|
||||
class LanguageTests(TestCase):
|
||||
def test_create_language(self):
|
||||
lang = Language.objects.create(name="Python", slug="python")
|
||||
@@ -64,23 +264,6 @@ class SkeletonFeatureTests(TestCase):
|
||||
SkeletonFeature.objects.create(name="Docker2", slug="docker")
|
||||
|
||||
|
||||
class PageTreeMixin:
|
||||
@classmethod
|
||||
def setUpPageTree(cls):
|
||||
cls.home = HomePage.objects.get(slug="home")
|
||||
|
||||
cls.scenario_index = ScenarioIndexPage(title="Scenarios", slug="scenarios")
|
||||
cls.home.add_child(instance=cls.scenario_index)
|
||||
cls.scenario = ScenarioPage(title="SaaS", slug="saas")
|
||||
cls.scenario_index.add_child(instance=cls.scenario)
|
||||
|
||||
cls.project_index = ProjectIndexPage(title="Projects", slug="projects")
|
||||
cls.home.add_child(instance=cls.project_index)
|
||||
|
||||
cls.article_index = ArticleIndexPage(title="Articles", slug="articles")
|
||||
cls.home.add_child(instance=cls.article_index)
|
||||
|
||||
|
||||
class SkeletonProjectPageTests(PageTreeMixin, TestCase):
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
|
||||
Reference in New Issue
Block a user