monaco-editor-lsp-next/src/app/(protected)/dashboard/actions/teacher-assignments.ts

304 lines
7.4 KiB
TypeScript

"use server";
import { revalidatePath } from "next/cache";
import prisma from "@/lib/prisma";
import {
assertCourseManagePermission,
assertTeacherOrAdmin,
getAuthenticatedActor,
} from "@/app/(protected)/dashboard/actions/course-auth";
interface AssignmentProblemInput {
problemId: string;
order?: number;
}
interface CreateAssignmentInput {
courseId: string;
title: string;
description?: string;
opensAt?: string;
dueAt?: string;
published?: boolean;
problems: AssignmentProblemInput[];
}
interface UpdateAssignmentInput {
title?: string;
description?: string | null;
opensAt?: string | null;
dueAt?: string | null;
published?: boolean;
problems?: AssignmentProblemInput[];
}
async function validateProblems(problems: AssignmentProblemInput[]) {
if (!Array.isArray(problems) || problems.length === 0) {
throw new Error("作业至少需要一道题目");
}
const uniqueProblemIds = [...new Set(problems.map((item) => item.problemId))];
const existingProblems = await prisma.problem.findMany({
where: {
id: { in: uniqueProblemIds },
isPublished: true,
},
select: { id: true },
});
if (existingProblems.length !== uniqueProblemIds.length) {
throw new Error("题目不存在或未发布");
}
}
function parseDateValue(value?: string | null) {
if (!value) {
return null;
}
const date = new Date(value);
if (Number.isNaN(date.getTime())) {
throw new Error("日期格式不合法");
}
return date;
}
export async function createAssignment(input: CreateAssignmentInput) {
const actor = await getAuthenticatedActor();
assertTeacherOrAdmin(actor);
await assertCourseManagePermission(input.courseId, actor);
await validateProblems(input.problems);
const title = input.title.trim();
if (!title) {
throw new Error("作业标题不能为空");
}
const opensAt = parseDateValue(input.opensAt);
const dueAt = parseDateValue(input.dueAt);
if (opensAt && dueAt && opensAt >= dueAt) {
throw new Error("截止时间必须晚于开始时间");
}
const assignment = await prisma.assignment.create({
data: {
courseId: input.courseId,
title,
description: input.description?.trim() || null,
opensAt,
dueAt,
published: Boolean(input.published),
createdById: actor.id,
problems: {
create: input.problems.map((item, index) => ({
problemId: item.problemId,
order: item.order ?? index + 1,
})),
},
},
select: {
id: true,
courseId: true,
},
});
revalidatePath(`/dashboard/teacher/courses/${assignment.courseId}`);
revalidatePath(
`/dashboard/teacher/courses/${assignment.courseId}/assignments/${assignment.id}`
);
revalidatePath("/dashboard/student/courses");
return assignment;
}
export async function updateAssignment(
assignmentId: string,
input: UpdateAssignmentInput
) {
const actor = await getAuthenticatedActor();
assertTeacherOrAdmin(actor);
const assignment = await prisma.assignment.findUnique({
where: { id: assignmentId },
select: { id: true, courseId: true },
});
if (!assignment) {
throw new Error("作业不存在");
}
await assertCourseManagePermission(assignment.courseId, actor);
const nextData: {
title?: string;
description?: string | null;
opensAt?: Date | null;
dueAt?: Date | null;
published?: boolean;
} = {};
if (typeof input.title === "string") {
const title = input.title.trim();
if (!title) {
throw new Error("作业标题不能为空");
}
nextData.title = title;
}
if (typeof input.description !== "undefined") {
nextData.description = input.description?.trim() || null;
}
if (typeof input.opensAt !== "undefined") {
nextData.opensAt = parseDateValue(input.opensAt);
}
if (typeof input.dueAt !== "undefined") {
nextData.dueAt = parseDateValue(input.dueAt);
}
if (typeof input.published === "boolean") {
nextData.published = input.published;
}
if (nextData.opensAt && nextData.dueAt && nextData.opensAt >= nextData.dueAt) {
throw new Error("截止时间必须晚于开始时间");
}
await prisma.assignment.update({
where: { id: assignmentId },
data: nextData,
});
if (input.problems) {
await validateProblems(input.problems);
await prisma.assignmentProblem.deleteMany({
where: { assignmentId },
});
await prisma.assignmentProblem.createMany({
data: input.problems.map((item, index) => ({
assignmentId,
problemId: item.problemId,
order: item.order ?? index + 1,
})),
skipDuplicates: true,
});
}
revalidatePath(`/dashboard/teacher/courses/${assignment.courseId}`);
revalidatePath(
`/dashboard/teacher/courses/${assignment.courseId}/assignments/${assignmentId}`
);
revalidatePath(
`/dashboard/student/courses/${assignment.courseId}/assignments/${assignmentId}`
);
}
export async function publishAssignment(
assignmentId: string,
published: boolean = true
) {
return updateAssignment(assignmentId, { published });
}
export async function getAssignmentDetail(assignmentId: string) {
const actor = await getAuthenticatedActor();
assertTeacherOrAdmin(actor);
const assignment = await prisma.assignment.findUnique({
where: { id: assignmentId },
select: {
id: true,
title: true,
description: true,
opensAt: true,
dueAt: true,
published: true,
courseId: true,
course: {
select: {
id: true,
title: true,
teacherId: true,
teacher: {
select: {
name: true,
email: true,
},
},
},
},
problems: {
orderBy: [{ order: "asc" }, { problem: { displayId: "asc" } }],
select: {
problemId: true,
order: true,
problem: {
select: {
displayId: true,
difficulty: true,
localizations: {
where: { type: "TITLE", locale: "zh" },
select: { content: true },
},
},
},
},
},
_count: {
select: {
submissions: true,
},
},
},
});
if (!assignment) {
throw new Error("作业不存在");
}
await assertCourseManagePermission(assignment.courseId, actor);
return assignment;
}
export async function listCourseAssignments(courseId: string) {
const actor = await getAuthenticatedActor();
assertTeacherOrAdmin(actor);
await assertCourseManagePermission(courseId, actor);
return prisma.assignment.findMany({
where: { courseId },
orderBy: [{ dueAt: "asc" }, { createdAt: "desc" }],
select: {
id: true,
title: true,
description: true,
opensAt: true,
dueAt: true,
published: true,
createdAt: true,
_count: {
select: {
problems: true,
submissions: true,
},
},
},
});
}
export async function listAssignableProblems() {
const actor = await getAuthenticatedActor();
assertTeacherOrAdmin(actor);
return prisma.problem.findMany({
where: { isPublished: true },
orderBy: { displayId: "asc" },
select: {
id: true,
displayId: true,
difficulty: true,
localizations: {
where: { type: "TITLE", locale: "zh" },
select: { content: true },
},
},
});
}