# Showcase Feature Implementation Plan > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. **Goal:** Build a showcase page where logged-in students can browse award-winning submissions grouped by award category, with optional prompt-chain viewing. **Architecture:** Two new Django models (`Award`, `SubmissionAward`) stored in `submission/`, three new API endpoints under `/api/submission/showcase/`, two new Vue pages (`Showcase.vue`, `ShowcaseDetail.vue`) plus a nav entry in `TaskPanel.vue`. **Tech Stack:** Django + django-ninja, Vue 3 + Naive UI, `TimeStampedModel` from `django_extensions` --- ## File Map **Create:** - `api/submission/tests_showcase.py` — all showcase backend tests - `client/src/pages/Showcase.vue` — award grid page - `client/src/pages/ShowcaseDetail.vue` — submission detail + prompt chain **Modify:** - `api/submission/models.py` — add `Award`, `SubmissionAward`, `ItemOrdering` - `api/submission/admin.py` — register `Award`, `SubmissionAward` - `api/submission/schemas.py` — add `ShowcaseItemOut`, `AwardOut`, `ShowcaseDetailOut`, `PromptRoundOut` - `api/submission/api.py` — add showcase endpoints - `client/src/utils/type.ts` — add showcase types - `client/src/api.ts` — add `Showcase` object - `client/src/router.ts` — add `/showcase` and `/showcase/:id` routes - `client/src/components/task/TaskPanel.vue` — add nav button --- ## Task 1: Add Award and SubmissionAward models **Files:** - Modify: `api/submission/models.py` - [ ] **Step 1: Add models** Append to `api/submission/models.py` after the `Rating` model and its signal: ```python class ItemOrdering(models.TextChoices): MANUAL = "manual", "手动排序" AWARDED_AT = "awarded_at", "授奖时间倒序" SCORE = "score", "评分倒序" VIEW_COUNT = "view_count", "浏览量倒序" class Award(TimeStampedModel): name = models.CharField(max_length=100, unique=True, verbose_name="奖项名称") description = models.TextField(blank=True, default="", verbose_name="奖项简介") sort_order = models.IntegerField(default=0, db_index=True, verbose_name="排序值") is_active = models.BooleanField(default=True, verbose_name="是否启用") item_ordering = models.CharField( max_length=20, choices=ItemOrdering.choices, default=ItemOrdering.MANUAL, verbose_name="作品排序方式", ) class Meta: ordering = ("sort_order",) def __str__(self): return self.name class SubmissionAward(TimeStampedModel): submission = models.ForeignKey( Submission, on_delete=models.CASCADE, related_name="awards" ) award = models.ForeignKey( Award, on_delete=models.CASCADE, related_name="submission_awards" ) sort_order = models.IntegerField(default=0, db_index=True, verbose_name="手动排序值") awarded_at = models.DateTimeField(auto_now_add=True, db_index=True, verbose_name="授奖时间") class Meta: unique_together = ("submission", "award") ordering = ("sort_order",) def __str__(self): return f"{self.award.name} — {self.submission}" ``` - [ ] **Step 2: Create migration** ```bash cd api && uv run manage.py makemigrations submission --name add_award_submissionaward ``` Expected: creates `api/submission/migrations/0011_add_award_submissionaward.py` - [ ] **Step 3: Apply migration** ```bash cd api && uv run manage.py migrate ``` Expected: `OK` with no errors - [ ] **Step 4: Commit** ```bash git add api/submission/models.py api/submission/migrations/0011_add_award_submissionaward.py git commit -m "feat: add Award and SubmissionAward models" ``` --- ## Task 2: Register Award and SubmissionAward in Django admin **Files:** - Modify: `api/submission/admin.py` - [ ] **Step 1: Write failing test** Create `api/submission/tests_showcase.py`: ```python from django.test import TestCase from django.contrib.auth import get_user_model from task.models import Task from submission.models import Submission, Award, SubmissionAward User = get_user_model() def _make_user(username="student1", role="normal"): return User.objects.create_user(username=username, password="pw", role=role) def _make_task(display=1): return Task.objects.create( title=f"Task {display}", task_type="challenge", display=display, content="" ) def _make_submission(user, task, score=0.0): return Submission.objects.create( user=user, task=task, html="
hi
", css="", js="", score=score ) def _make_award(name="最佳设计", sort_order=0, is_active=True, item_ordering="manual"): return Award.objects.create( name=name, sort_order=sort_order, is_active=is_active, item_ordering=item_ordering, ) class AwardModelTest(TestCase): def test_unique_submission_award(self): """Same submission + award combo cannot be created twice""" user = _make_user() task = _make_task() sub = _make_submission(user, task) award = _make_award() SubmissionAward.objects.create(submission=sub, award=award) from django.db import IntegrityError with self.assertRaises(IntegrityError): SubmissionAward.objects.create(submission=sub, award=award) def test_submission_can_have_multiple_awards(self): user = _make_user() task = _make_task() sub = _make_submission(user, task) a1 = _make_award("奖1", sort_order=0) a2 = _make_award("奖2", sort_order=1) SubmissionAward.objects.create(submission=sub, award=a1) SubmissionAward.objects.create(submission=sub, award=a2) self.assertEqual(sub.awards.count(), 2) def test_award_can_have_multiple_submissions(self): user = _make_user() task = _make_task() sub1 = _make_submission(user, task, score=3.0) task2 = _make_task(display=2) sub2 = _make_submission(user, task2, score=4.0) award = _make_award() SubmissionAward.objects.create(submission=sub1, award=award) SubmissionAward.objects.create(submission=sub2, award=award) self.assertEqual(award.submission_awards.count(), 2) ``` - [ ] **Step 2: Run test to verify it passes (models already created)** ```bash cd api && uv run manage.py test submission.tests_showcase.AwardModelTest -v 2 ``` Expected: 3 tests PASS - [ ] **Step 3: Implement admin registration** Replace contents of `api/submission/admin.py`: ```python from django.contrib import admin from .models import Award, Submission, SubmissionAward @admin.register(Award) class AwardAdmin(admin.ModelAdmin): list_display = ("name", "sort_order", "is_active", "item_ordering", "created") list_filter = ("is_active", "item_ordering") search_fields = ("name",) ordering = ("sort_order",) @admin.register(SubmissionAward) class SubmissionAwardAdmin(admin.ModelAdmin): list_display = ( "award_name", "submission_username", "submission_task_title", "submission_score", "submission_view_count", "sort_order", "awarded_at", ) list_filter = ("award", "submission__task", "submission__user__classname") search_fields = ( "award__name", "submission__user__username", "submission__task__title", ) raw_id_fields = ("submission",) @admin.display(description="奖项") def award_name(self, obj): return obj.award.name @admin.display(description="提交作者") def submission_username(self, obj): return obj.submission.user.username @admin.display(description="挑战标题") def submission_task_title(self, obj): return obj.submission.task.title @admin.display(description="评分") def submission_score(self, obj): return obj.submission.score @admin.display(description="浏览量") def submission_view_count(self, obj): return obj.submission.view_count ``` - [ ] **Step 4: Commit** ```bash git add api/submission/admin.py api/submission/tests_showcase.py git commit -m "feat: register Award and SubmissionAward in Django admin" ``` --- ## Task 3: Add showcase schemas **Files:** - Modify: `api/submission/schemas.py` - [ ] **Step 1: Add schemas** Append to the bottom of `api/submission/schemas.py`: ```python class ShowcaseItemOut(Schema): submission_id: UUID username: str task_title: str task_display: int score: float view_count: int html: Optional[str] = None css: Optional[str] = None js: Optional[str] = None has_prompt_chain: bool class AwardOut(Schema): id: int name: str description: str item_ordering: str items: list[ShowcaseItemOut] class ShowcaseDetailOut(Schema): submission_id: UUID username: str task_title: str task_display: int score: float view_count: int html: Optional[str] = None css: Optional[str] = None js: Optional[str] = None awards: list[str] has_prompt_chain: bool class PromptRoundOut(Schema): question: str source: str prompt_level: Optional[int] = None html: Optional[str] = None css: Optional[str] = None js: Optional[str] = None ``` - [ ] **Step 2: Verify import is not broken** ```bash cd api && uv run manage.py check ``` Expected: `System check identified no issues.` - [ ] **Step 3: Commit** ```bash git add api/submission/schemas.py git commit -m "feat: add showcase schemas" ``` --- ## Task 4: Implement showcase list API endpoint with tests **Files:** - Modify: `api/submission/api.py` - Modify: `api/submission/tests_showcase.py` - [ ] **Step 1: Write failing tests** Append to `api/submission/tests_showcase.py`: ```python class ShowcaseListTest(TestCase): def setUp(self): self.user = _make_user("student1") self.task = _make_task() def test_unauthenticated_returns_401(self): resp = self.client.get("/api/submission/showcase/") self.assertEqual(resp.status_code, 401) def test_authenticated_returns_200(self): self.client.force_login(self.user) resp = self.client.get("/api/submission/showcase/") self.assertEqual(resp.status_code, 200) def test_inactive_award_not_returned(self): _make_award("停用奖", is_active=False) sub = _make_submission(self.user, self.task) award = Award.objects.get(name="停用奖") SubmissionAward.objects.create(submission=sub, award=award) self.client.force_login(self.user) resp = self.client.get("/api/submission/showcase/") data = resp.json() self.assertEqual(len(data), 0) def test_award_with_no_items_not_returned(self): _make_award("空奖项") self.client.force_login(self.user) resp = self.client.get("/api/submission/showcase/") data = resp.json() self.assertEqual(len(data), 0) def test_active_award_with_items_returned(self): award = _make_award("最佳设计") sub = _make_submission(self.user, self.task) SubmissionAward.objects.create(submission=sub, award=award) self.client.force_login(self.user) resp = self.client.get("/api/submission/showcase/") data = resp.json() self.assertEqual(len(data), 1) self.assertEqual(data[0]["name"], "最佳设计") self.assertEqual(len(data[0]["items"]), 1) item = data[0]["items"][0] self.assertEqual(item["username"], "student1") self.assertEqual(item["has_prompt_chain"], False) def test_manual_ordering_uses_sort_order(self): award = _make_award("奖", item_ordering="manual") sub1 = _make_submission(self.user, self.task) task2 = _make_task(display=2) sub2 = _make_submission(self.user, task2) SubmissionAward.objects.create(submission=sub1, award=award, sort_order=2) SubmissionAward.objects.create(submission=sub2, award=award, sort_order=1) self.client.force_login(self.user) resp = self.client.get("/api/submission/showcase/") items = resp.json()[0]["items"] self.assertEqual(items[0]["task_display"], task2.display) self.assertEqual(items[1]["task_display"], self.task.display) def test_score_ordering(self): award = _make_award("奖", item_ordering="score") sub1 = _make_submission(self.user, self.task, score=2.0) task2 = _make_task(display=2) sub2 = _make_submission(self.user, task2, score=4.0) SubmissionAward.objects.create(submission=sub1, award=award) SubmissionAward.objects.create(submission=sub2, award=award) self.client.force_login(self.user) resp = self.client.get("/api/submission/showcase/") items = resp.json()[0]["items"] self.assertGreater(items[0]["score"], items[1]["score"]) def test_view_count_ordering(self): award = _make_award("奖", item_ordering="view_count") sub1 = _make_submission(self.user, self.task) sub1.view_count = 5 sub1.save(update_fields=["view_count"]) task2 = _make_task(display=2) sub2 = _make_submission(self.user, task2) sub2.view_count = 20 sub2.save(update_fields=["view_count"]) SubmissionAward.objects.create(submission=sub1, award=award) SubmissionAward.objects.create(submission=sub2, award=award) self.client.force_login(self.user) resp = self.client.get("/api/submission/showcase/") items = resp.json()[0]["items"] self.assertGreater(items[0]["view_count"], items[1]["view_count"]) def test_has_prompt_chain_true_when_source_message_exists(self): from prompt.models import Conversation, Message award = _make_award("奖") sub = _make_submission(self.user, self.task) conv = Conversation.objects.create(user=self.user, task=self.task) Message.objects.create(conversation=conv, role="user", content="做个按钮") Message.objects.create( conversation=conv, role="assistant", content="好的", code_html="", code_css="", code_js="", submission=sub, ) SubmissionAward.objects.create(submission=sub, award=award) self.client.force_login(self.user) resp = self.client.get("/api/submission/showcase/") item = resp.json()[0]["items"][0] self.assertTrue(item["has_prompt_chain"]) ``` - [ ] **Step 2: Run tests to see them fail** ```bash cd api && uv run manage.py test submission.tests_showcase.ShowcaseListTest -v 2 ``` Expected: FAIL — `404` response since endpoints don't exist yet - [ ] **Step 3: Implement showcase list endpoint** In `api/submission/api.py`, add these imports at the top of the file (after existing imports): ```python from django.db.models import Exists from .models import Award, SubmissionAward, ItemOrdering from .schemas import AwardOut, ShowcaseDetailOut, ShowcaseItemOut, PromptRoundOut ``` Then append this endpoint to `api/submission/api.py` (before the final `get_submission` endpoint to keep URL specificity correct — add right before `@router.get("/{submission_id}", ...)`): ```python @router.get("/showcase/", response=List[AwardOut]) @login_required def list_showcase(request): ordering_map = { ItemOrdering.MANUAL: "sort_order", ItemOrdering.AWARDED_AT: "-awarded_at", ItemOrdering.SCORE: "-submission__score", ItemOrdering.VIEW_COUNT: "-submission__view_count", } awards = Award.objects.filter(is_active=True).order_by("sort_order") result = [] for award in awards: order_field = ordering_map.get(award.item_ordering, "sort_order") items_qs = ( SubmissionAward.objects.filter(award=award) .select_related("submission", "submission__user", "submission__task") .annotate( has_prompt_chain=Exists( Message.objects.filter(submission_id=OuterRef("submission_id")) ) ) .order_by(order_field) ) items = list(items_qs) if not items: continue result.append({ "id": award.id, "name": award.name, "description": award.description, "item_ordering": award.item_ordering, "items": [ { "submission_id": sa.submission_id, "username": sa.submission.user.username, "task_title": sa.submission.task.title, "task_display": sa.submission.task.display, "score": sa.submission.score, "view_count": sa.submission.view_count, "html": sa.submission.html, "css": sa.submission.css, "js": sa.submission.js, "has_prompt_chain": sa.has_prompt_chain, } for sa in items ], }) return result ``` - [ ] **Step 4: Run tests to verify they pass** ```bash cd api && uv run manage.py test submission.tests_showcase.ShowcaseListTest -v 2 ``` Expected: 8 tests PASS - [ ] **Step 5: Commit** ```bash git add api/submission/api.py api/submission/schemas.py api/submission/tests_showcase.py git commit -m "feat: add showcase list API endpoint" ``` --- ## Task 5: Implement showcase detail API endpoint with tests **Files:** - Modify: `api/submission/api.py` - Modify: `api/submission/tests_showcase.py` - [ ] **Step 1: Write failing tests** Append to `api/submission/tests_showcase.py`: ```python class ShowcaseDetailTest(TestCase): def setUp(self): self.user = _make_user("student1") self.task = _make_task() self.award = _make_award("最佳设计") self.sub = _make_submission(self.user, self.task, score=4.5) self.sub.view_count = 10 self.sub.save(update_fields=["view_count"]) SubmissionAward.objects.create(submission=self.sub, award=self.award) def test_unauthenticated_returns_401(self): resp = self.client.get(f"/api/submission/showcase/{self.sub.id}/") self.assertEqual(resp.status_code, 401) def test_awarded_submission_accessible(self): self.client.force_login(self.user) resp = self.client.get(f"/api/submission/showcase/{self.sub.id}/") self.assertEqual(resp.status_code, 200) data = resp.json() self.assertEqual(data["username"], "student1") self.assertEqual(data["score"], 4.5) self.assertEqual(data["view_count"], 10) self.assertIn("最佳设计", data["awards"]) self.assertFalse(data["has_prompt_chain"]) def test_non_awarded_submission_returns_404(self): other_sub = _make_submission(self.user, self.task) self.client.force_login(self.user) resp = self.client.get(f"/api/submission/showcase/{other_sub.id}/") self.assertEqual(resp.status_code, 404) def test_submission_shows_all_its_awards(self): award2 = _make_award("最佳游戏", sort_order=1) SubmissionAward.objects.create(submission=self.sub, award=award2) self.client.force_login(self.user) resp = self.client.get(f"/api/submission/showcase/{self.sub.id}/") data = resp.json() self.assertIn("最佳设计", data["awards"]) self.assertIn("最佳游戏", data["awards"]) ``` - [ ] **Step 2: Run tests to see them fail** ```bash cd api && uv run manage.py test submission.tests_showcase.ShowcaseDetailTest -v 2 ``` Expected: FAIL — `404` or `405` since endpoint doesn't exist - [ ] **Step 3: Implement showcase detail endpoint** Add this endpoint to `api/submission/api.py`, right after `list_showcase`: ```python @router.get("/showcase/{submission_id}/", response=ShowcaseDetailOut) @login_required def get_showcase_detail(request, submission_id: UUID): if not SubmissionAward.objects.filter(submission_id=submission_id).exists(): raise HttpError(404, "作品不存在或未授奖") sub = get_object_or_404( Submission.objects.select_related("user", "task"), id=submission_id, ) has_chain = Message.objects.filter(submission=sub).exists() award_names = list( SubmissionAward.objects.filter(submission=sub) .select_related("award") .values_list("award__name", flat=True) ) return { "submission_id": sub.id, "username": sub.user.username, "task_title": sub.task.title, "task_display": sub.task.display, "score": sub.score, "view_count": sub.view_count, "html": sub.html, "css": sub.css, "js": sub.js, "awards": award_names, "has_prompt_chain": has_chain, } ``` - [ ] **Step 4: Run tests to verify they pass** ```bash cd api && uv run manage.py test submission.tests_showcase.ShowcaseDetailTest -v 2 ``` Expected: 4 tests PASS - [ ] **Step 5: Commit** ```bash git add api/submission/api.py api/submission/tests_showcase.py git commit -m "feat: add showcase detail API endpoint" ``` --- ## Task 6: Implement showcase prompt-chain API endpoint with tests **Files:** - Modify: `api/submission/api.py` - Modify: `api/submission/tests_showcase.py` - [ ] **Step 1: Write failing tests** Append to `api/submission/tests_showcase.py`: ```python class ShowcasePromptChainTest(TestCase): def setUp(self): from prompt.models import Conversation, Message as Msg self.user = _make_user("student1") self.task = _make_task() self.award = _make_award("最佳设计") self.sub = _make_submission(self.user, self.task) SubmissionAward.objects.create(submission=self.sub, award=self.award) self.conv = Conversation.objects.create(user=self.user, task=self.task) self.user_msg = Msg.objects.create( conversation=self.conv, role="user", content="做个按钮", source="conversation", prompt_level=3, ) self.asst_msg = Msg.objects.create( conversation=self.conv, role="assistant", content="好的", code_html="", code_css="button{color:red}", code_js="console.log(1)", submission=self.sub, ) def test_unauthenticated_returns_401(self): resp = self.client.get(f"/api/submission/showcase/{self.sub.id}/prompt-chain/") self.assertEqual(resp.status_code, 401) def test_no_source_message_returns_404(self): other_sub = _make_submission(self.user, self.task) SubmissionAward.objects.create(submission=other_sub, award=self.award) self.client.force_login(self.user) resp = self.client.get(f"/api/submission/showcase/{other_sub.id}/prompt-chain/") self.assertEqual(resp.status_code, 404) def test_non_awarded_submission_returns_404(self): from prompt.models import Conversation, Message as Msg other_sub = _make_submission(self.user, self.task) conv = Conversation.objects.create(user=self.user, task=self.task) Msg.objects.create( conversation=conv, role="assistant", content="x", submission=other_sub, ) self.client.force_login(self.user) resp = self.client.get(f"/api/submission/showcase/{other_sub.id}/prompt-chain/") self.assertEqual(resp.status_code, 404) def test_returns_prompt_rounds(self): self.client.force_login(self.user) resp = self.client.get(f"/api/submission/showcase/{self.sub.id}/prompt-chain/") self.assertEqual(resp.status_code, 200) rounds = resp.json() self.assertEqual(len(rounds), 1) r = rounds[0] self.assertEqual(r["question"], "做个按钮") self.assertEqual(r["source"], "conversation") self.assertEqual(r["prompt_level"], 3) self.assertEqual(r["html"], "") self.assertEqual(r["css"], "button{color:red}") self.assertEqual(r["js"], "console.log(1)") def test_multiple_rounds(self): from prompt.models import Message as Msg Msg.objects.create( conversation=self.conv, role="user", content="再加个标题", source="manual", ) Msg.objects.create( conversation=self.conv, role="assistant", content="好", code_html="