Files
web/docs/plans/2026-03-09-submission-flag.md
T
xuyueandClaude Opus 5.5 47d4679984 chore: 合并为单仓库,统一部署流程
- 根目录加入 docker-compose、Caddyfile、文档等原本不在仓库里的文件
- 前后端两份 GitHub Actions 合并成根目录的一个 deploy workflow:
  构建前端上传到 build/,再在服务器上 git pull 并重建容器

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-26 16:02:12 +08:00

455 lines
11 KiB
Markdown

# Submission Flag Implementation Plan
> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
**Goal:** Add colored flag markers (red/blue/green/yellow) to submissions, settable by teachers/admins, visible to all users, with list filtering support.
**Architecture:** Add a nullable `flag` CharField to the Submission model. New PUT endpoint for setting/clearing flags with role check. Frontend shows flag icons in the table with a popover for admins to pick colors, plus a dropdown filter.
**Tech Stack:** Django + django-ninja (backend), Vue 3 + Naive UI + TypeScript (frontend)
---
### Task 1: Add `flag` field to Submission model
**Files:**
- Modify: `api/submission/models.py:1-13` (add FLAG_CHOICES and field)
**Step 1: Add FLAG_CHOICES and field to model**
In `api/submission/models.py`, add after the imports (before the class):
```python
class FlagChoices(models.TextChoices):
RED = "red", "值得展示"
BLUE = "blue", "需要讲解"
GREEN = "green", "优秀作品"
YELLOW = "yellow", "需要改进"
```
In the `Submission` class, add after the `conversation` field (line 33):
```python
flag = models.CharField(
max_length=10,
choices=FlagChoices.choices,
null=True,
blank=True,
default=None,
verbose_name="标记",
)
```
**Step 2: Create and run migration**
Run:
```bash
cd api && uv run manage.py makemigrations submission && uv run manage.py migrate
```
Expected: Migration created and applied successfully.
**Step 3: Commit**
```bash
git add api/submission/models.py api/submission/migrations/
git commit -m "feat: add flag field to Submission model"
```
---
### Task 2: Add flag to API schemas and filter
**Files:**
- Modify: `api/submission/schemas.py`
**Step 1: Update SubmissionOut schema**
In `api/submission/schemas.py`, add to the `SubmissionOut` class fields (after line 27 `conversation_id`):
```python
flag: Optional[str] = None
```
**Step 2: Update SubmissionOut.list() static method**
Add `"flag": submission.flag,` to the return dict in `list()` (after the `conversation_id` line):
```python
"flag": submission.flag,
```
**Step 3: Update SubmissionOut.get() static method**
Add `"flag": submission.flag,` to the return dict in `get()` (after the `conversation_id` line):
```python
"flag": submission.flag,
```
**Step 4: Update SubmissionFilter**
Add flag filter field to `SubmissionFilter`:
```python
class SubmissionFilter(Schema):
task_id: Optional[int] = None
task_type: Optional[Literal["tutorial", "challenge"]] = None
username: Optional[str] = None
flag: Optional[Literal["red", "blue", "green", "yellow"]] = None
```
**Step 5: Add FlagIn schema**
Add at the end of `schemas.py`:
```python
class FlagIn(Schema):
flag: Optional[Literal["red", "blue", "green", "yellow"]] = None
```
**Step 6: Commit**
```bash
git add api/submission/schemas.py
git commit -m "feat: add flag to submission schemas and filter"
```
---
### Task 3: Add flag API endpoint and filter logic
**Files:**
- Modify: `api/submission/api.py`
**Step 1: Add flag filter to list_submissions**
In `api/submission/api.py`, in the `list_submissions` function, add after the username filter block (after line 65):
```python
if filters.flag:
submissions = submissions.filter(flag=filters.flag)
```
**Step 2: Add flag endpoint**
Import `FlagIn` in the imports from `.schemas`. Add the new endpoint after `update_score`:
```python
@router.put("/{submission_id}/flag")
@login_required
def update_flag(request, submission_id: UUID, payload: FlagIn):
"""
设置或清除提交的标记(仅管理员和超级管理员可操作)
"""
if request.user.role not in (RoleChoices.SUPER, RoleChoices.ADMIN):
raise HttpError(403, "没有权限")
submission = get_object_or_404(Submission, id=submission_id)
submission.flag = payload.flag
submission.save(update_fields=["flag"])
return {"flag": submission.flag}
```
Also add `RoleChoices` to the imports from `account.models`:
```python
from account.models import RoleChoices
```
**Step 3: Commit**
```bash
git add api/submission/api.py
git commit -m "feat: add flag endpoint and filter logic"
```
---
### Task 4: Update frontend types and API client
**Files:**
- Modify: `client/src/utils/type.ts`
- Modify: `client/src/api.ts`
- Modify: `client/src/store/submission.ts`
**Step 1: Add flag type to TypeScript interfaces**
In `client/src/utils/type.ts`, add the FlagType and update interfaces:
```typescript
export type FlagType = "red" | "blue" | "green" | "yellow" | null
```
Add `flag?: FlagType` to `SubmissionOut` (after `conversation_id`):
```typescript
export interface SubmissionOut {
id: string
userid: number
username: string
task_display: number
task_type: TASK_TYPE
task_title: string
score: number
my_score: number
conversation_id?: string
flag?: FlagType
created: Date
modified: Date
}
```
Add `flag?: FlagType` to `SubmissionAll` (after `my_score`):
```typescript
export interface SubmissionAll {
id: string
userid: number
username: string
task_id: number
task_display: number
task_type: TASK_TYPE
task_title: string
score: number
my_score: number
flag?: FlagType
html: ""
css: ""
js: ""
created: Date
modified: Date
}
```
**Step 2: Add updateFlag to API client**
In `client/src/api.ts`, add to the `Submission` object:
```typescript
async updateFlag(id: string, flag: FlagType) {
const res = await http.put(`/submission/${id}/flag`, { flag })
return res.data
},
```
Also add the import at the top:
```typescript
import type { TutorialIn, ChallengeIn, FlagType } from "./utils/type"
```
**Step 3: Commit**
```bash
git add client/src/utils/type.ts client/src/api.ts client/src/store/submission.ts
git commit -m "feat: add flag type and API method to frontend"
```
---
### Task 5: Add flag column and interaction to Submissions.vue
**Files:**
- Modify: `client/src/pages/Submissions.vue`
**Step 1: Add imports and flag config**
In the `<script setup>` section, add imports:
```typescript
import { roleAdmin, roleSuper } from "../store/user"
import type { FlagType } from "../utils/type"
```
Add flag color config after the existing refs:
```typescript
const FLAG_OPTIONS: { value: FlagType; color: string; label: string }[] = [
{ value: "red", color: "#e03030", label: "值得展示" },
{ value: "blue", color: "#2080f0", label: "需要讲解" },
{ value: "green", color: "#18a058", label: "优秀作品" },
{ value: "yellow", color: "#f0a020", label: "需要改进" },
]
const isAdmin = computed(() => roleAdmin.value || roleSuper.value)
```
**Step 2: Add flag update function**
```typescript
async function updateFlag(row: SubmissionOut, flag: FlagType) {
await Submission.updateFlag(row.id, flag)
row.flag = flag
}
```
**Step 3: Add flag column to columns array**
Add as the first column in the `columns` array (before the "时间" column):
```typescript
{
title: "标记",
key: "flag",
width: 50,
render: (row) => {
const flagOption = FLAG_OPTIONS.find((f) => f.value === row.flag)
const flagIcon = h("span", {
style: {
display: "inline-block",
width: "12px",
height: "12px",
borderRadius: "50%",
backgroundColor: flagOption ? flagOption.color : "transparent",
border: flagOption ? "none" : "1px dashed #ccc",
cursor: isAdmin.value ? "pointer" : "default",
},
})
if (!isAdmin.value) return flagIcon
return h(
NPopover,
{ trigger: "click" },
{
trigger: () => flagIcon,
default: () =>
h(NSpace, { vertical: true, size: "small" }, () => [
...FLAG_OPTIONS.map((opt) =>
h(
NButton,
{
text: true,
onClick: () => updateFlag(row, opt.value),
},
() =>
h("span", { style: { display: "flex", alignItems: "center", gap: "6px" } }, [
h("span", {
style: {
display: "inline-block",
width: "10px",
height: "10px",
borderRadius: "50%",
backgroundColor: opt.color,
},
}),
opt.label,
]),
),
),
row.flag
? h(
NButton,
{
text: true,
type: "error",
onClick: () => updateFlag(row, null),
},
() => "清除",
)
: null,
]),
},
)
},
},
```
**Step 4: Add NPopover and NSpace to imports**
Update the naive-ui import:
```typescript
import { NButton, NPopover, NSpace, type DataTableColumn } from "naive-ui"
```
**Step 5: Commit**
```bash
git add client/src/pages/Submissions.vue
git commit -m "feat: add flag column with color picker to submissions table"
```
---
### Task 6: Add flag filter dropdown
**Files:**
- Modify: `client/src/pages/Submissions.vue`
**Step 1: Add flag to query reactive object**
Update the `query` reactive:
```typescript
const query = reactive({
page: Number(route.params.page),
username: route.query.username ?? "",
flag: "" as string,
})
```
**Step 2: Add flag filter dropdown in template**
In the template, inside the `<n-flex align="center">` block (before the username input), add:
```html
<n-select
v-model:value="query.flag"
style="width: 100px"
clearable
placeholder="标记"
:options="[
{ label: '红旗', value: 'red' },
{ label: '蓝旗', value: 'blue' },
{ label: '绿旗', value: 'green' },
{ label: '黄旗', value: 'yellow' },
]"
/>
```
**Step 3: Add watcher for flag filter**
Add a watch for the flag filter (alongside the existing username watcher):
```typescript
watch(
() => query.flag,
() => {
query.page = 1
init()
},
)
```
**Step 4: Commit**
```bash
git add client/src/pages/Submissions.vue
git commit -m "feat: add flag filter dropdown to submissions list"
```
---
### Task 7: Manual verification
**Step 1: Start dev servers**
```bash
cd api && uv run manage.py runserver &
cd client && npm run start &
```
**Step 2: Verify the following**
1. Submissions list shows the flag column
2. As admin/super: clicking the dot opens a color picker popover
3. Selecting a color sets the flag (colored dot appears)
4. Clicking "清除" removes the flag
5. As normal user: flag dot is visible but not clickable
6. Flag filter dropdown filters the list correctly
7. Page refresh preserves flag state (persisted in DB)
**Step 3: Final commit if any fixes needed**