Phase 2 评审修复: P0 min_score+&q=500 (ORM→list 顺序) + P1 场景分页(20/page) + P2 文档同步 + 36 tests
This commit is contained in:
+11
-5
@@ -31,12 +31,18 @@ class ScenarioPage(Page):
|
||||
|
||||
def get_context(self, request, *args, **kwargs):
|
||||
context = super().get_context(request, *args, **kwargs)
|
||||
context["projects"] = (
|
||||
projects = (
|
||||
SkeletonProjectPage.objects.live()
|
||||
.filter(scenarios=self)
|
||||
.prefetch_related("languages", "scenarios")
|
||||
.order_by("-first_published_at")
|
||||
)
|
||||
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)
|
||||
return context
|
||||
|
||||
class Meta:
|
||||
@@ -148,6 +154,10 @@ class ProjectIndexPage(Page):
|
||||
projects = projects.filter(frameworks__slug=framework)
|
||||
if database:
|
||||
projects = projects.filter(databases__slug=database)
|
||||
if q:
|
||||
projects = projects.filter(
|
||||
Q(title__icontains=q) | Q(summary__icontains=q)
|
||||
)
|
||||
try:
|
||||
score_val = int(min_score)
|
||||
if 0 <= score_val <= 30:
|
||||
@@ -156,10 +166,6 @@ class ProjectIndexPage(Page):
|
||||
]
|
||||
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")
|
||||
|
||||
@@ -22,6 +22,18 @@
|
||||
</a>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
{% if projects.paginator.num_pages > 1 %}
|
||||
<nav class="pagination">
|
||||
{% if projects.has_previous %}
|
||||
<a href="?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="?page={{ projects.next_page_number }}">Next »</a>
|
||||
{% endif %}
|
||||
</nav>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
{% include "includes/empty_state.html" with message="No projects found in this scenario." %}
|
||||
{% endif %}
|
||||
|
||||
@@ -75,6 +75,46 @@ class ScenarioPageTests(PageTreeMixin, TestCase):
|
||||
self.assertContains(response, "Nothing here yet")
|
||||
|
||||
|
||||
class ScenarioPaginationTests(PageTreeMixin, TestCase):
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
cls.setUpPageTree()
|
||||
cls.lang = Language.objects.create(name="Python", slug="python")
|
||||
for i in range(21):
|
||||
project = SkeletonProjectPage(
|
||||
title=f"Pagination Project {i+1}",
|
||||
slug=f"pagination-project-{i+1}",
|
||||
summary=f"Project {i+1} for pagination test",
|
||||
github_url="https://github.com/test/pagination",
|
||||
maturity="stable",
|
||||
recommended_for="Testing",
|
||||
structure_score=3, docs_score=3, tests_score=3,
|
||||
example_score=3, dependency_score=3, incremental_score=3,
|
||||
)
|
||||
cls.project_index.add_child(instance=project)
|
||||
project.languages.add(cls.lang)
|
||||
project.scenarios.add(cls.scenario)
|
||||
project.save()
|
||||
|
||||
def test_first_page_has_20_items(self):
|
||||
resp = self.client.get(self.scenario.url)
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
self.assertContains(resp, "Pagination Project 1")
|
||||
self.assertContains(resp, "Pagination Project 20")
|
||||
self.assertNotContains(resp, "Pagination Project 21")
|
||||
|
||||
def test_second_page_has_1_item(self):
|
||||
resp = self.client.get(f"{self.scenario.url}?page=2")
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
self.assertNotContains(resp, "Pagination Project 1")
|
||||
self.assertContains(resp, "Pagination Project 21")
|
||||
|
||||
def test_invalid_page_falls_back_to_first(self):
|
||||
resp = self.client.get(f"{self.scenario.url}?page=abc")
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
self.assertContains(resp, "Pagination Project 1")
|
||||
|
||||
|
||||
class ProjectFilterTests(PageTreeMixin, TestCase):
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
@@ -135,6 +175,21 @@ class ProjectFilterTests(PageTreeMixin, TestCase):
|
||||
resp = self.client.get("/projects/?min_score=abc&page=xyz")
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
|
||||
def test_combined_filters_return_200(self):
|
||||
resp = self.client.get(
|
||||
"/projects/?language=python&framework=django&min_score=18&q=Django"
|
||||
)
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
self.assertContains(resp, "Django Project")
|
||||
self.assertNotContains(resp, "Go Project")
|
||||
|
||||
def test_combined_filters_no_results_returns_empty_state(self):
|
||||
resp = self.client.get(
|
||||
"/projects/?language=go&framework=django&min_score=18"
|
||||
)
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
self.assertContains(resp, "No projects match")
|
||||
|
||||
|
||||
class SkeletonProjectDetailTests(PageTreeMixin, TestCase):
|
||||
@classmethod
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
- git:分支 `ds`;`.gitignore` 已生效(`.venv/`、`db.sqlite3`、`media/` 等被忽略)
|
||||
- 技术栈:Wagtail 7.4.2 + Django 6.0.6 + Python 3.12 + SQLite;第一版英文单语言站点
|
||||
- 生产代码:已初始化,`manage.py`、`skelet/`、`home/`、`search/`、`core/` 已建
|
||||
- 测试:`home/tests.py` 含 5 个测试,`core/tests.py` 含 17 个测试,全部通过
|
||||
- 测试:31 个测试(`home/tests.py` + `core/tests.py`),全部通过;`manage.py test` 可运行
|
||||
- 数据:3 个场景、5 个骨架项目、2 篇文章已录入(`core/management/commands/seed_data.py`)
|
||||
- 当前 blocker:无;下一步执行 `T-301`(Phase 3 SEO 与上线准备)
|
||||
|
||||
|
||||
@@ -0,0 +1,122 @@
|
||||
# Phase 2 前台 MVP 评审
|
||||
|
||||
评审日期:2026-07-06
|
||||
评审角色:全栈开发工程师
|
||||
评审对象:DeepSeek 完成的 Phase 2(T-201 至 T-206)
|
||||
|
||||
## 结论
|
||||
|
||||
Phase 2 **暂不达标**。
|
||||
|
||||
基础页面、首页、项目详情、文章详情等主要页面已经可访问,自动化测试也通过;但筛选搜索的组合参数存在 500 错误,且场景页没有实现任务详单要求的分页。因此不能把 Phase 2 视为完整验收通过。
|
||||
|
||||
## 阻断问题
|
||||
|
||||
### P0:项目列表 `min_score + q` 组合筛选会 500
|
||||
|
||||
- 影响任务:T-204
|
||||
- 位置:`core/models.py:151` 至 `core/models.py:162`
|
||||
- 现象:`min_score` 合法时,代码把 `projects` 从 QuerySet 转成 list;后续如果同时存在 `q`,继续调用 `projects.filter(...)`,触发 `AttributeError: 'list' object has no attribute 'filter'`。
|
||||
- 复现:
|
||||
|
||||
```bash
|
||||
.venv/bin/python3.12.exe manage.py shell -c "from django.test import Client; c=Client(raise_request_exception=False); print(c.get('/projects/?min_score=18&q=Django').status_code)"
|
||||
```
|
||||
|
||||
实际结果:`500`
|
||||
|
||||
任务契约要求 `language` / `framework` / `database` / `min_score` / `q` / `page` 多参数同时出现时按 AND 组合,见 `docs/04-architecture.md:157` 至 `docs/04-architecture.md:161`。当前实现只覆盖了单项筛选测试,没有覆盖组合筛选,见 `core/tests.py:119` 至 `core/tests.py:136`。
|
||||
|
||||
建议修复方向:
|
||||
|
||||
- 保持筛选链路的数据类型一致。
|
||||
- 可以先用 ORM 对 `q`、language、framework、database 过滤,再对 `min_score` 做 Python 层过滤;也可以用 annotation 计算总分后在 ORM 层过滤。
|
||||
- 增加组合筛选测试,例如 `/projects/?language=python&framework=django&min_score=18&q=Django`。
|
||||
|
||||
### P1:场景页未实现分页
|
||||
|
||||
- 影响任务:T-203
|
||||
- 位置:`core/models.py:32` 至 `core/models.py:40`,`core/templates/core/scenario_page.html:7` 至 `core/templates/core/scenario_page.html:27`
|
||||
- 现象:场景页直接把全部匹配项目放进 `context["projects"]`,模板也没有分页控件。
|
||||
- 任务详单要求:场景页只列 live 项目,分页每页 20,空场景渲染空状态。
|
||||
|
||||
当前只满足 live 项目和空状态;分页缺失。已有测试只验证有项目和空状态,未验证超过 20 条时分页行为,见 `core/tests.py:40` 至 `core/tests.py:75`。
|
||||
|
||||
建议修复方向:
|
||||
|
||||
- 在 `ScenarioPage.get_context()` 中使用 `Paginator(projects, 20)`。
|
||||
- 支持非法 `page` 回退到第一页,行为和项目列表页一致。
|
||||
- 在 `scenario_page.html` 复用项目列表页的分页 UI。
|
||||
- 增加 21 个项目的场景页分页测试。
|
||||
|
||||
## 非阻断问题
|
||||
|
||||
### P2:`docs/current-state.md` 测试数量已过期
|
||||
|
||||
- 位置:`docs/current-state.md:19`
|
||||
- 现象:文档写的是 `home/tests.py` 5 个、`core/tests.py` 17 个;实际 `manage.py test` 输出为 31 个测试。
|
||||
- 影响:不影响运行,但会误导后续 agent 判断当前状态。
|
||||
|
||||
建议修复:更新当前快照里的测试数量。
|
||||
|
||||
### P2:文章列表实现偏薄,但可接受
|
||||
|
||||
- 影响任务:T-206
|
||||
- 位置:`core/templates/core/article_index_page.html:7` 至 `core/templates/core/article_index_page.html:13`
|
||||
- 现状:文章列表通过 `page.get_children.live` 直接渲染标题链接,没有自定义 `get_context()`,也没有分页。
|
||||
- 评估:T-206 只要求文章可访问、文章详情可链接项目详情;当前能满足最低验收。后续内容量增长时建议补排序、摘要和分页。
|
||||
|
||||
## 逐任务验收
|
||||
|
||||
| 任务 | 结论 | 说明 |
|
||||
| --- | --- | --- |
|
||||
| T-201 基础页面框架 | 达标 | `base.html` 有 viewport meta,导航、页脚、基础 CSS 已实现,容器使用 `max-width` 和响应式宽度。 |
|
||||
| T-202 首页 | 达标 | 首页展示场景入口、推荐骨架、最新文章,内容来自数据库上下文。 |
|
||||
| T-203 场景页和项目列表页 | 未达标 | 项目列表页有分页和空状态;场景页有项目和空状态,但缺少每页 20 的分页。 |
|
||||
| T-204 筛选与基础搜索 | 未达标 | 单项筛选可用,但 `min_score` 与 `q` 组合请求返回 500,不符合 AND 组合契约。 |
|
||||
| T-205 骨架详情页 | 达标 | 展示详情字段、评分、推荐理由,`is_sponsored=True` 时渲染 Sponsored 标识。 |
|
||||
| T-206 文章列表和详情 | 达标 | `/articles/` 和文章详情可访问,文章详情可链接关联项目;实现较薄但满足最低验收。 |
|
||||
|
||||
## 验证记录
|
||||
|
||||
已执行:
|
||||
|
||||
```bash
|
||||
.venv/bin/python3.12.exe manage.py check
|
||||
.venv/bin/python3.12.exe manage.py makemigrations --check --dry-run
|
||||
.venv/bin/python3.12.exe manage.py test
|
||||
.venv/bin/python3.12.exe manage.py shell -c "from core.models import ScenarioPage,SkeletonProjectPage,ArticlePage; print(ScenarioPage.objects.live().count(), SkeletonProjectPage.objects.live().count(), ArticlePage.objects.live().count())"
|
||||
.venv/bin/python3.12.exe manage.py shell -c "from django.test import Client; c=Client(raise_request_exception=False); paths=['/','/projects/','/articles/','/projects/?language=python','/projects/?min_score=18','/projects/?q=Django','/projects/?min_score=18&q=Django']; [print(p, c.get(p).status_code) for p in paths]"
|
||||
```
|
||||
|
||||
结果摘要:
|
||||
|
||||
- `manage.py check`:0 error,9 个 treebeard/Wagtail 兼容 warning。
|
||||
- `makemigrations --check --dry-run`:No changes detected。
|
||||
- `manage.py test`:31 tests passed。
|
||||
- seed 数据:3 个 live 场景、5 个 live 项目、2 篇 live 文章。
|
||||
- URL 补测:
|
||||
- `/`:200
|
||||
- `/projects/`:200
|
||||
- `/articles/`:200
|
||||
- `/projects/?language=python`:200
|
||||
- `/projects/?min_score=18`:200
|
||||
- `/projects/?q=Django`:200
|
||||
- `/projects/?min_score=18&q=Django`:500
|
||||
|
||||
## 修复验收建议
|
||||
|
||||
修复后至少补充并通过以下测试:
|
||||
|
||||
1. 项目列表组合筛选:`language + framework + database + min_score + q` 同时存在时返回 200,并只展示满足全部条件的项目。
|
||||
2. 项目列表组合筛选无结果时返回 200,并展示空状态。
|
||||
3. 场景页 21 个 live 项目时第一页展示 20 个,第二页展示剩余项目。
|
||||
4. 场景页非法 `page` 参数返回第一页,不报错。
|
||||
|
||||
修复完成后重新运行:
|
||||
|
||||
```bash
|
||||
.venv/bin/python3.12.exe manage.py check
|
||||
.venv/bin/python3.12.exe manage.py makemigrations --check --dry-run
|
||||
.venv/bin/python3.12.exe manage.py test
|
||||
```
|
||||
+17
@@ -267,3 +267,20 @@
|
||||
- 阻塞:无。
|
||||
- 下一步:T-301 补 SEO 基础(Phase 3)。
|
||||
|
||||
## 2026-07-06 Phase 2 评审修复(P0 min_score+500 + P1 场景分页 + P2 文档同步)
|
||||
|
||||
- 状态:DONE
|
||||
- 变更:
|
||||
- **P0**:`core/models.py` ProjectIndexPage.get_context() 调换 `q` 与 `min_score` 的执行顺序——ORM 筛选(`q`/language/framework/database)先执行,最后做 Python 层 `min_score` 列表过滤,避免 QuerySet→list 后 `.filter()` 崩溃
|
||||
- **P1**:`core/models.py` ScenarioPage.get_context() 新增 `Paginator(projects, 20)`;`scenario_page.html` 复用分页 UI;新增 3 个分页测试
|
||||
- **P2**:`docs/current-state.md` 测试数量同步为 31
|
||||
- 新增测试:`test_combined_filters_return_200`、`test_combined_filters_no_results_returns_empty_state`、`test_first_page_has_20_items`、`test_second_page_has_1_item`、`test_invalid_page_falls_back_to_first`
|
||||
- 验证:
|
||||
- `manage.py test core home`:36 tests passed
|
||||
- `min_score=18&q=Django` 返回 200 ✅
|
||||
- `min_score=18&page=abc` 返回 200 ✅
|
||||
- `manage.py check`:0 errors
|
||||
- `makemigrations --check --dry-run`:No changes
|
||||
- 阻塞:无。
|
||||
- 下一步:T-301 补 SEO 基础(Phase 3)。
|
||||
|
||||
|
||||
Reference in New Issue
Block a user