Merge exams grading into homework
Redirect /teacher/exams/grading* to /teacher/homework/submissions; remove exam grading UI/actions/data-access; add homework student workflow and update design docs.
This commit is contained in:
@@ -1,12 +1,12 @@
|
||||
# Database Schema Changelog
|
||||
|
||||
## v1.1.0 - Exam Structure & Performance Optimization
|
||||
## v1.1.0 - Exam Structure Support
|
||||
**Date:** 2025-12-29
|
||||
**Migration ID:** `0001_flawless_texas_twister`
|
||||
**Author:** Principal Database Architect
|
||||
|
||||
### 1. Summary
|
||||
This release introduces support for hierarchical exam structures (Sectioning/Grouping) and optimizes database constraint naming for better compatibility with MySQL environments.
|
||||
This release introduces support for hierarchical exam structures (Sectioning/Grouping).
|
||||
|
||||
### 2. Changes
|
||||
|
||||
@@ -23,17 +23,55 @@ This release introduces support for hierarchical exam structures (Sectioning/Gro
|
||||
>
|
||||
```
|
||||
|
||||
#### 2.2 Table: `questions_to_knowledge_points`
|
||||
* **Action**: `RENAME FOREIGN KEY`
|
||||
### 3. Migration Strategy
|
||||
* **Up**: Run standard Drizzle migration.
|
||||
* **Down**: Revert `structure` column. Note that FK names can be kept short as they are implementation details.
|
||||
|
||||
### 4. Impact Analysis
|
||||
* **Performance**: Negligible. JSON parsing is done client-side or at application layer.
|
||||
* **Data Integrity**: High. Existing data is unaffected. New `structure` field defaults to `NULL`.
|
||||
|
||||
## v1.2.0 - Homework Module Tables & FK Name Hardening
|
||||
**Date:** 2025-12-31
|
||||
**Migration ID:** `0002_equal_wolfpack`
|
||||
**Author:** Principal Database Architect
|
||||
|
||||
### 1. Summary
|
||||
This release introduces homework-related tables and hardens foreign key names to avoid exceeding MySQL identifier length limits (MySQL 64-char constraint names).
|
||||
|
||||
### 2. Changes
|
||||
|
||||
#### 2.1 Tables: Homework Domain
|
||||
* **Action**: `CREATE TABLE`
|
||||
* **Tables**:
|
||||
* `homework_assignments`
|
||||
* `homework_assignment_questions`
|
||||
* `homework_assignment_targets`
|
||||
* `homework_submissions`
|
||||
* `homework_answers`
|
||||
* **Reason**: Support assignment lifecycle, targeting, submissions, and per-question grading.
|
||||
|
||||
#### 2.2 Foreign Keys: Homework Domain (Name Hardening)
|
||||
* **Action**: `ADD FOREIGN KEY` (with short constraint names)
|
||||
* **Details**:
|
||||
* `homework_assignments`: `hw_asg_exam_fk`, `hw_asg_creator_fk`
|
||||
* `homework_assignment_questions`: `hw_aq_a_fk`, `hw_aq_q_fk`
|
||||
* `homework_assignment_targets`: `hw_at_a_fk`, `hw_at_s_fk`
|
||||
* `homework_submissions`: `hw_sub_a_fk`, `hw_sub_student_fk`
|
||||
* `homework_answers`: `hw_ans_sub_fk`, `hw_ans_q_fk`
|
||||
* **Reason**: Default generated FK names can exceed 64 characters in MySQL and fail during migration.
|
||||
|
||||
#### 2.3 Table: `questions_to_knowledge_points`
|
||||
* **Action**: `RENAME FOREIGN KEY` (implemented as drop + add)
|
||||
* **Details**:
|
||||
* Old: `questions_to_knowledge_points_question_id_questions_id_fk` -> New: `q_kp_qid_fk`
|
||||
* Old: `questions_to_knowledge_points_knowledge_point_id_knowledge_points_id_fk` -> New: `q_kp_kpid_fk`
|
||||
* **Reason**: Previous names exceeded MySQL's 64-character identifier limit, causing potential migration failures in production environments.
|
||||
|
||||
### 3. Migration Strategy
|
||||
* **Up**: Run standard Drizzle migration. The script includes `ALTER TABLE ... DROP FOREIGN KEY` followed by `ADD CONSTRAINT`.
|
||||
* **Down**: Revert `structure` column. Note that FK names can be kept short as they are implementation details.
|
||||
* **Up**: Run standard Drizzle migration. The migration is resilient whether the legacy FK names exist or have already been renamed.
|
||||
* **Down**: Not provided. Removing homework tables and FKs is destructive and should be handled explicitly per environment.
|
||||
|
||||
### 4. Impact Analysis
|
||||
* **Performance**: Negligible. JSON parsing is done client-side or at application layer.
|
||||
* **Data Integrity**: High. Existing data is unaffected. New `structure` field defaults to `NULL`.
|
||||
* **Performance**: Minimal. New indexes are scoped to common homework access patterns.
|
||||
* **Data Integrity**: High. Foreign keys enforce referential integrity for homework workflow.
|
||||
|
||||
@@ -1,15 +1,17 @@
|
||||
# 考试模块实现设计文档
|
||||
|
||||
## 1. 概述
|
||||
考试模块提供了一个完整的评估管理生命周期,使教师能够创建考试、组卷(支持嵌套分组)、发布评估以及对学生的提交进行评分。
|
||||
考试模块用于教师侧的“试卷制作与管理”,覆盖创建考试、组卷(支持嵌套分组)、发布/归档等流程。
|
||||
|
||||
**说明(合并调整)**:与“作业(Homework)”模块合并后,考试模块不再提供“阅卷/评分(grading)”与提交流转;教师批改统一在 Homework 的 submissions 中完成。
|
||||
|
||||
## 2. 数据架构
|
||||
|
||||
### 2.1 核心实体
|
||||
- **Exams**: 根实体,包含元数据(标题、时间安排)和结构信息。
|
||||
- **ExamQuestions**: 关系链接,用于查询题目的使用情况(扁平化表示)。
|
||||
- **ExamSubmissions**: 学生的考试尝试记录。
|
||||
- **SubmissionAnswers**: 链接到特定题目的单个答案。
|
||||
- **ExamSubmissions**: (历史/保留)学生的考试尝试记录;当前 UI/路由不再使用。
|
||||
- **SubmissionAnswers**: (历史/保留)链接到特定题目的单个答案;当前 UI/路由不再使用。
|
||||
|
||||
### 2.2 `structure` 字段
|
||||
为了支持层级布局(如章节/分组),我们在 `exams` 表中引入了一个 JSON 列 `structure`。它作为“布局/呈现层”的单一事实来源(Source of Truth),用于渲染分组与排序;而 `exam_questions` 仍然承担题目关联、外键完整性与索引查询职责。
|
||||
@@ -46,14 +48,9 @@ type ExamNode = {
|
||||
- 可搜索/筛选的可用题目列表。
|
||||
- “添加”操作将节点追加到结构树中。
|
||||
|
||||
### 3.2 阅卷界面
|
||||
位于 `/teacher/exams/grading/[submissionId]`。
|
||||
|
||||
- **`GradingView` (客户端组件)**
|
||||
- **左侧面板**: 只读视图,显示学生的答案与题目内容。
|
||||
- **右侧面板**: 评分和反馈的输入字段。
|
||||
- **状态**: 在提交前管理本地更改。
|
||||
- **Actions**: `gradeSubmissionAction` 更新 `submissionAnswers` 并将总分聚合到 `examSubmissions`。
|
||||
### 3.2 阅卷界面(已下线)
|
||||
原阅卷路由 `/teacher/exams/grading` 与 `/teacher/exams/grading/[submissionId]` 已移除业务能力并重定向到 Homework:
|
||||
- `/teacher/exams/grading*` → `/teacher/homework/submissions`
|
||||
|
||||
### 3.3 列表页(All Exams)
|
||||
位于 `/teacher/exams/all`。
|
||||
@@ -74,13 +71,10 @@ type ExamNode = {
|
||||
- **保存**: 同时提交 `questionsJson`(扁平化,用于索引)和 `structureJson`(树状,用于布局)到 `updateExamAction`。
|
||||
3. **发布**: 状态变更为 `published`。
|
||||
|
||||
### 4.2 阅卷流程
|
||||
1. **列表**: 教师查看 `submission-data-table`。
|
||||
2. **评分**: 打开特定提交。
|
||||
3. **审查**: 遍历题目。
|
||||
- 系统显示学生答案。
|
||||
- 教师输入分数(上限为满分)和反馈。
|
||||
4. **提交**: 服务器更新单个答案记录并重新计算提交总分。
|
||||
### 4.2 阅卷/批改流程(迁移到 Homework)
|
||||
教师批改统一在 Homework 模块完成:
|
||||
- 提交列表:`/teacher/homework/submissions`
|
||||
- 批改页:`/teacher/homework/submissions/[submissionId]`
|
||||
|
||||
### 4.3 考试管理(All Exams Actions)
|
||||
位于 `/teacher/exams/all` 的表格行级菜单。
|
||||
@@ -118,14 +112,13 @@ type ExamNode = {
|
||||
- 面向未来(现代 React Hooks 模式)。
|
||||
|
||||
### 5.3 Server Actions
|
||||
所有变更操作(保存草稿、发布、复制、删除、评分)均使用 Next.js Server Actions,以确保类型安全并自动重新验证缓存。
|
||||
所有变更操作(保存草稿、发布、复制、删除)均使用 Next.js Server Actions,以确保类型安全并自动重新验证缓存。
|
||||
|
||||
已落地的 Server Actions:
|
||||
- `createExamAction`
|
||||
- `updateExamAction`
|
||||
- `duplicateExamAction`
|
||||
- `deleteExamAction`
|
||||
- `gradeSubmissionAction`
|
||||
|
||||
## 6. 接口与数据影响
|
||||
|
||||
@@ -153,11 +146,16 @@ type ExamNode = {
|
||||
- 依赖外键级联清理关联数据:`exam_questions`、`exam_submissions`、`submission_answers`
|
||||
- **缓存**:
|
||||
- `revalidatePath("/teacher/exams/all")`
|
||||
- `revalidatePath("/teacher/exams/grading")`
|
||||
|
||||
### 6.4 数据访问层(Data Access)
|
||||
位于 `src/modules/exams/data-access.ts`,对外提供与页面/组件解耦的查询函数。
|
||||
|
||||
- `getExams(params)`: 支持按 `q/status` 在数据库侧过滤;`difficulty` 因当前存储在 `description` JSON 中,采用内存过滤
|
||||
- `getExamById(id)`: 查询 exam 及其 `exam_questions`,并返回 `structure` 以用于构建器 Hydrate
|
||||
- `getExamSubmissions()`: 为阅卷列表提供 submissions 数据
|
||||
|
||||
## 7. 变更记录(合并 Homework)
|
||||
|
||||
**日期**:2025-12-31
|
||||
|
||||
- 移除 Exams grading 入口与实现:删除阅卷 UI、server action、data-access 查询。
|
||||
- Exams grading 路由改为重定向到 Homework submissions。
|
||||
|
||||
153
docs/design/006_homework_module_implementation.md
Normal file
153
docs/design/006_homework_module_implementation.md
Normal file
@@ -0,0 +1,153 @@
|
||||
# 作业模块实现设计文档(Homework Module)
|
||||
|
||||
**日期**: 2025-12-31
|
||||
**模块**: Homework (`src/modules/homework`)
|
||||
|
||||
---
|
||||
|
||||
## 1. 概述
|
||||
|
||||
作业模块提供“由试卷派发作业”的完整生命周期:
|
||||
|
||||
- 教师从已存在的 Exam 派发 Homework Assignment(冻结当时的结构与题目引用)
|
||||
- 指定作业目标学生(Targets)
|
||||
- 学生开始一次作答(Submission),保存答案(Answers),并最终提交
|
||||
- 教师在提交列表中查看并批改(按题给分/反馈,汇总总分)
|
||||
|
||||
核心目标是:在不破坏 Exam 本体数据的前提下,为作业提供可追溯、可批改、可统计的独立域模型。
|
||||
|
||||
**说明(合并调整)**:教师端“阅卷/批改”统一通过 Homework submissions 完成,`/teacher/exams/grading*` 相关路由已重定向到 `/teacher/homework/submissions`。
|
||||
|
||||
---
|
||||
|
||||
## 2. 数据架构
|
||||
|
||||
### 2.1 核心实体
|
||||
|
||||
- `homework_assignments`: 作业实例(从 exam 派生)
|
||||
- `homework_assignment_questions`: 作业与题目关系(score/order)
|
||||
- `homework_assignment_targets`: 作业目标学生列表
|
||||
- `homework_submissions`: 学生作业尝试(attempt_no/status/时间/是否迟交)
|
||||
- `homework_answers`: 每题答案(answer_content/score/feedback)
|
||||
|
||||
数据库变更记录见:[schema-changelog.md](file:///c:/Users/xiner/Desktop/CICD/docs/db/schema-changelog.md#L34-L77)
|
||||
|
||||
### 2.2 设计要点:冻结 Exam → Homework Assignment
|
||||
|
||||
- `homework_assignments.source_exam_id` 保存来源 Exam
|
||||
- `homework_assignments.structure` 在 publish 时复制 `exams.structure`(冻结当时的呈现结构)
|
||||
- 题目关联使用 `homework_assignment_questions`(仍引用 `questions` 表,作业侧记录分值与顺序)
|
||||
|
||||
---
|
||||
|
||||
## 3. 路由与页面
|
||||
|
||||
### 3.1 教师端
|
||||
|
||||
- `/teacher/homework/assignments`: 作业列表
|
||||
实现:[assignments/page.tsx](file:///c:/Users/xiner/Desktop/CICD/src/app/(dashboard)/teacher/homework/assignments/page.tsx)
|
||||
- `/teacher/homework/assignments/create`: 从 Exam 派发作业
|
||||
实现:[create/page.tsx](file:///c:/Users/xiner/Desktop/CICD/src/app/(dashboard)/teacher/homework/assignments/create/page.tsx)
|
||||
- `/teacher/homework/assignments/[id]`: 作业详情
|
||||
实现:[[id]/page.tsx](file:///c:/Users/xiner/Desktop/CICD/src/app/(dashboard)/teacher/homework/assignments/%5Bid%5D/page.tsx)
|
||||
- `/teacher/homework/assignments/[id]/submissions`: 作业提交列表(按作业筛选)
|
||||
实现:[[id]/submissions/page.tsx](file:///c:/Users/xiner/Desktop/CICD/src/app/(dashboard)/teacher/homework/assignments/%5Bid%5D/submissions/page.tsx)
|
||||
- `/teacher/homework/submissions`: 全部提交列表
|
||||
实现:[submissions/page.tsx](file:///c:/Users/xiner/Desktop/CICD/src/app/(dashboard)/teacher/homework/submissions/page.tsx)
|
||||
- `/teacher/homework/submissions/[submissionId]`: 批改页
|
||||
实现:[[submissionId]/page.tsx](file:///c:/Users/xiner/Desktop/CICD/src/app/(dashboard)/teacher/homework/submissions/%5BsubmissionId%5D/page.tsx)
|
||||
|
||||
关联重定向:
|
||||
|
||||
- `/teacher/exams/grading` → `/teacher/homework/submissions`
|
||||
实现:[grading/page.tsx](file:///c:/Users/xiner/Desktop/CICD/src/app/(dashboard)/teacher/exams/grading/page.tsx)
|
||||
- `/teacher/exams/grading/[submissionId]` → `/teacher/homework/submissions`
|
||||
实现:[grading/[submissionId]/page.tsx](file:///c:/Users/xiner/Desktop/CICD/src/app/(dashboard)/teacher/exams/grading/%5BsubmissionId%5D/page.tsx)
|
||||
|
||||
### 3.2 学生端
|
||||
|
||||
- `/student/learning/assignments`: 作业列表
|
||||
实现:[page.tsx](file:///c:/Users/xiner/Desktop/CICD/src/app/(dashboard)/student/learning/assignments/page.tsx)
|
||||
- `/student/learning/assignments/[assignmentId]`: 作答页(开始/保存/提交)
|
||||
实现:[[assignmentId]/page.tsx](file:///c:/Users/xiner/Desktop/CICD/src/app/(dashboard)/student/learning/assignments/%5BassignmentId%5D/page.tsx)
|
||||
|
||||
---
|
||||
|
||||
## 4. 数据访问层(Data Access)
|
||||
|
||||
数据访问位于:[data-access.ts](file:///c:/Users/xiner/Desktop/CICD/src/modules/homework/data-access.ts)
|
||||
|
||||
### 4.1 教师侧查询
|
||||
|
||||
- `getHomeworkAssignments`:作业列表(可按 creatorId/ids)
|
||||
- `getHomeworkAssignmentById`:作业详情(含目标人数、提交数统计)
|
||||
- `getHomeworkSubmissions`:提交列表(可按 assignmentId)
|
||||
- `getHomeworkSubmissionDetails`:提交详情(题目内容 + 学生答案 + 分值/顺序)
|
||||
|
||||
### 4.2 学生侧查询
|
||||
|
||||
- `getStudentHomeworkAssignments(studentId)`:只返回“已派发给该学生、已发布、且到达 availableAt”的作业
|
||||
- `getStudentHomeworkTakeData(assignmentId, studentId)`:进入作答页所需数据(assignment + 当前/最近 submission + 题目列表 + 已保存答案)
|
||||
|
||||
### 4.3 开发模式用户选择(Demo)
|
||||
|
||||
为了在未接入真实 Auth 的情况下可演示学生端页面,提供:
|
||||
|
||||
- `getDemoStudentUser()`:优先选取最早创建的 student;若无 student,则退化到任意用户
|
||||
|
||||
---
|
||||
|
||||
## 5. Server Actions
|
||||
|
||||
实现位于:[actions.ts](file:///c:/Users/xiner/Desktop/CICD/src/modules/homework/actions.ts)
|
||||
|
||||
### 5.1 教师侧
|
||||
|
||||
- `createHomeworkAssignmentAction`:从 exam 创建 assignment;可写入 targets;可选择 publish(默认 true)
|
||||
- `gradeHomeworkSubmissionAction`:按题写入 score/feedback,并汇总写入 submission.score 与 status=graded
|
||||
|
||||
### 5.2 学生侧
|
||||
|
||||
- `startHomeworkSubmissionAction`:创建一次 submission(attemptNo + startedAt),并校验:
|
||||
- assignment 已发布
|
||||
- student 在 targets 中
|
||||
- availableAt 已到
|
||||
- 未超过 maxAttempts
|
||||
- `saveHomeworkAnswerAction`:保存/更新某题答案(upsert 到 homework_answers)
|
||||
- `submitHomeworkAction`:提交作业(校验 dueAt/lateDueAt/allowLate,写入 submittedAt/isLate/status=submitted)
|
||||
|
||||
---
|
||||
|
||||
## 6. UI 组件
|
||||
|
||||
### 6.1 教师批改视图
|
||||
|
||||
- [HomeworkGradingView](file:///c:/Users/xiner/Desktop/CICD/src/modules/homework/components/homework-grading-view.tsx)
|
||||
- 左侧:学生答案只读展示
|
||||
- 右侧:按题录入分数与反馈,并提交批改
|
||||
|
||||
### 6.2 学生作答视图
|
||||
|
||||
- [HomeworkTakeView](file:///c:/Users/xiner/Desktop/CICD/src/modules/homework/components/homework-take-view.tsx)
|
||||
- Start:开始一次作答
|
||||
- Save:按题保存
|
||||
- Submit:提交(提交前会先保存当前题目答案)
|
||||
- 题型支持:`text` / `judgment` / `single_choice` / `multiple_choice`
|
||||
|
||||
题目 content 约定与题库一致:`{ text, options?: [{ id, text, isCorrect? }] }`(作答页仅消费 `id/text`)。
|
||||
|
||||
---
|
||||
|
||||
## 7. 类型定义
|
||||
|
||||
类型位于:[types.ts](file:///c:/Users/xiner/Desktop/CICD/src/modules/homework/types.ts)
|
||||
|
||||
- 教师侧:`HomeworkAssignmentListItem` / `HomeworkSubmissionDetails` 等
|
||||
- 学生侧:`StudentHomeworkAssignmentListItem` / `StudentHomeworkTakeData` 等
|
||||
|
||||
---
|
||||
|
||||
## 8. 校验
|
||||
|
||||
- `npm run typecheck`: 通过
|
||||
- `npm run lint`: 0 errors(仓库其他位置存在 warnings,与本模块新增功能无直接关联)
|
||||
90
docs/scripts/baseline-migrations.js
Normal file
90
docs/scripts/baseline-migrations.js
Normal file
@@ -0,0 +1,90 @@
|
||||
require("dotenv/config");
|
||||
|
||||
const fs = require("node:fs");
|
||||
const crypto = require("node:crypto");
|
||||
const path = require("node:path");
|
||||
const mysql = require("mysql2/promise");
|
||||
|
||||
const JOURNAL = {
|
||||
"0000_aberrant_cobalt_man": 1766460456274,
|
||||
"0001_flawless_texas_twister": 1767004087964,
|
||||
"0002_equal_wolfpack": 1767145757594,
|
||||
};
|
||||
|
||||
function sha256Hex(input) {
|
||||
return crypto.createHash("sha256").update(input).digest("hex");
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const url = process.env.DATABASE_URL;
|
||||
if (!url) {
|
||||
throw new Error("DATABASE_URL is not set");
|
||||
}
|
||||
|
||||
const conn = await mysql.createConnection(url);
|
||||
|
||||
await conn.query(
|
||||
"CREATE TABLE IF NOT EXISTS `__drizzle_migrations` (id serial primary key, hash text not null, created_at bigint)"
|
||||
);
|
||||
|
||||
const [existing] = await conn.query(
|
||||
"SELECT id, hash, created_at FROM `__drizzle_migrations` ORDER BY created_at DESC LIMIT 1"
|
||||
);
|
||||
if (Array.isArray(existing) && existing.length > 0) {
|
||||
console.log("✅ __drizzle_migrations already has entries. Skip baselining.");
|
||||
await conn.end();
|
||||
return;
|
||||
}
|
||||
|
||||
const [[accountsRow]] = await conn.query(
|
||||
"SELECT COUNT(*) AS cnt FROM information_schema.tables WHERE table_schema=DATABASE() AND table_name='accounts'"
|
||||
);
|
||||
const accountsExists = Number(accountsRow?.cnt ?? 0) > 0;
|
||||
if (!accountsExists) {
|
||||
console.log("ℹ️ No existing tables detected (accounts missing). Skip baselining.");
|
||||
await conn.end();
|
||||
return;
|
||||
}
|
||||
|
||||
const [[structureRow]] = await conn.query(
|
||||
"SELECT COUNT(*) AS cnt FROM information_schema.columns WHERE table_schema=DATABASE() AND table_name='exams' AND column_name='structure'"
|
||||
);
|
||||
const examsStructureExists = Number(structureRow?.cnt ?? 0) > 0;
|
||||
|
||||
const [[homeworkRow]] = await conn.query(
|
||||
"SELECT COUNT(*) AS cnt FROM information_schema.tables WHERE table_schema=DATABASE() AND table_name='homework_assignments'"
|
||||
);
|
||||
const homeworkExists = Number(homeworkRow?.cnt ?? 0) > 0;
|
||||
|
||||
const baselineTags = [];
|
||||
baselineTags.push("0000_aberrant_cobalt_man");
|
||||
if (examsStructureExists) baselineTags.push("0001_flawless_texas_twister");
|
||||
if (homeworkExists) baselineTags.push("0002_equal_wolfpack");
|
||||
|
||||
const drizzleDir = path.resolve(__dirname, "..", "..", "drizzle");
|
||||
for (const tag of baselineTags) {
|
||||
const sqlPath = path.join(drizzleDir, `${tag}.sql`);
|
||||
if (!fs.existsSync(sqlPath)) {
|
||||
throw new Error(`Missing migration file: ${sqlPath}`);
|
||||
}
|
||||
const sqlText = fs.readFileSync(sqlPath).toString();
|
||||
const hash = sha256Hex(sqlText);
|
||||
const createdAt = JOURNAL[tag];
|
||||
if (typeof createdAt !== "number") {
|
||||
throw new Error(`Missing journal timestamp for: ${tag}`);
|
||||
}
|
||||
await conn.query(
|
||||
"INSERT INTO `__drizzle_migrations` (`hash`, `created_at`) VALUES (?, ?)",
|
||||
[hash, createdAt]
|
||||
);
|
||||
}
|
||||
|
||||
console.log(`✅ Baselined __drizzle_migrations: ${baselineTags.join(", ")}`);
|
||||
await conn.end();
|
||||
}
|
||||
|
||||
main().catch((err) => {
|
||||
console.error("❌ Baseline failed:", err);
|
||||
process.exit(1);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user