Merge exams grading into homework
Some checks failed
CI / build-and-test (push) Failing after 3m34s
CI / deploy (push) Has been skipped

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:
SpecialX
2025-12-31 11:59:03 +08:00
parent f8e39f518d
commit 13e91e628d
36 changed files with 4491 additions and 452 deletions

View File

@@ -13,7 +13,12 @@ import {
exams,
examQuestions,
examSubmissions,
submissionAnswers
submissionAnswers,
homeworkAssignments,
homeworkAssignmentQuestions,
homeworkAssignmentTargets,
homeworkSubmissions,
homeworkAnswers
} from "./schema";
// --- Users & Roles Relations ---
@@ -23,7 +28,9 @@ export const usersRelations = relations(users, ({ many }) => ({
sessions: many(sessions),
usersToRoles: many(usersToRoles),
createdExams: many(exams),
createdHomeworkAssignments: many(homeworkAssignments),
submissions: many(examSubmissions),
homeworkSubmissions: many(homeworkSubmissions),
authoredQuestions: many(questions),
}));
@@ -169,3 +176,62 @@ export const submissionAnswersRelations = relations(submissionAnswers, ({ one })
references: [questions.id],
}),
}));
export const homeworkAssignmentsRelations = relations(homeworkAssignments, ({ one, many }) => ({
creator: one(users, {
fields: [homeworkAssignments.creatorId],
references: [users.id],
}),
sourceExam: one(exams, {
fields: [homeworkAssignments.sourceExamId],
references: [exams.id],
}),
questions: many(homeworkAssignmentQuestions),
targets: many(homeworkAssignmentTargets),
submissions: many(homeworkSubmissions),
}));
export const homeworkAssignmentQuestionsRelations = relations(homeworkAssignmentQuestions, ({ one }) => ({
assignment: one(homeworkAssignments, {
fields: [homeworkAssignmentQuestions.assignmentId],
references: [homeworkAssignments.id],
}),
question: one(questions, {
fields: [homeworkAssignmentQuestions.questionId],
references: [questions.id],
}),
}));
export const homeworkAssignmentTargetsRelations = relations(homeworkAssignmentTargets, ({ one }) => ({
assignment: one(homeworkAssignments, {
fields: [homeworkAssignmentTargets.assignmentId],
references: [homeworkAssignments.id],
}),
student: one(users, {
fields: [homeworkAssignmentTargets.studentId],
references: [users.id],
}),
}));
export const homeworkSubmissionsRelations = relations(homeworkSubmissions, ({ one, many }) => ({
assignment: one(homeworkAssignments, {
fields: [homeworkSubmissions.assignmentId],
references: [homeworkAssignments.id],
}),
student: one(users, {
fields: [homeworkSubmissions.studentId],
references: [users.id],
}),
answers: many(homeworkAnswers),
}));
export const homeworkAnswersRelations = relations(homeworkAnswers, ({ one }) => ({
submission: one(homeworkSubmissions, {
fields: [homeworkAnswers.submissionId],
references: [homeworkSubmissions.id],
}),
question: one(questions, {
fields: [homeworkAnswers.questionId],
references: [questions.id],
}),
}));

View File

@@ -269,6 +269,127 @@ export const submissionAnswers = mysqlTable("submission_answers", {
submissionIdx: index("submission_idx").on(table.submissionId),
}));
export const homeworkAssignments = mysqlTable("homework_assignments", {
id: id("id").primaryKey(),
sourceExamId: varchar("source_exam_id", { length: 128 }).notNull(),
title: varchar("title", { length: 255 }).notNull(),
description: text("description"),
structure: json("structure"),
status: varchar("status", { length: 50 }).default("draft"),
creatorId: varchar("creator_id", { length: 128 }).notNull(),
availableAt: timestamp("available_at"),
dueAt: timestamp("due_at"),
allowLate: boolean("allow_late").default(false).notNull(),
lateDueAt: timestamp("late_due_at"),
maxAttempts: int("max_attempts").default(1).notNull(),
createdAt: timestamp("created_at").defaultNow().notNull(),
updatedAt: timestamp("updated_at").defaultNow().onUpdateNow().notNull(),
}, (table) => ({
creatorIdx: index("hw_assignment_creator_idx").on(table.creatorId),
sourceExamIdx: index("hw_assignment_source_exam_idx").on(table.sourceExamId),
statusIdx: index("hw_assignment_status_idx").on(table.status),
sourceExamFk: foreignKey({
columns: [table.sourceExamId],
foreignColumns: [exams.id],
name: "hw_asg_exam_fk",
}).onDelete("cascade"),
creatorFk: foreignKey({
columns: [table.creatorId],
foreignColumns: [users.id],
name: "hw_asg_creator_fk",
}).onDelete("cascade"),
}));
export const homeworkAssignmentQuestions = mysqlTable("homework_assignment_questions", {
assignmentId: varchar("assignment_id", { length: 128 }).notNull(),
questionId: varchar("question_id", { length: 128 }).notNull(),
score: int("score").default(0),
order: int("order").default(0),
}, (table) => ({
pk: primaryKey({ columns: [table.assignmentId, table.questionId] }),
assignmentIdx: index("hw_assignment_questions_assignment_idx").on(table.assignmentId),
assignmentFk: foreignKey({
columns: [table.assignmentId],
foreignColumns: [homeworkAssignments.id],
name: "hw_aq_a_fk",
}).onDelete("cascade"),
questionFk: foreignKey({
columns: [table.questionId],
foreignColumns: [questions.id],
name: "hw_aq_q_fk",
}).onDelete("cascade"),
}));
export const homeworkAssignmentTargets = mysqlTable("homework_assignment_targets", {
assignmentId: varchar("assignment_id", { length: 128 }).notNull(),
studentId: varchar("student_id", { length: 128 }).notNull(),
createdAt: timestamp("created_at").defaultNow().notNull(),
}, (table) => ({
pk: primaryKey({ columns: [table.assignmentId, table.studentId] }),
assignmentIdx: index("hw_assignment_targets_assignment_idx").on(table.assignmentId),
studentIdx: index("hw_assignment_targets_student_idx").on(table.studentId),
assignmentFk: foreignKey({
columns: [table.assignmentId],
foreignColumns: [homeworkAssignments.id],
name: "hw_at_a_fk",
}).onDelete("cascade"),
studentFk: foreignKey({
columns: [table.studentId],
foreignColumns: [users.id],
name: "hw_at_s_fk",
}).onDelete("cascade"),
}));
export const homeworkSubmissions = mysqlTable("homework_submissions", {
id: id("id").primaryKey(),
assignmentId: varchar("assignment_id", { length: 128 }).notNull(),
studentId: varchar("student_id", { length: 128 }).notNull(),
attemptNo: int("attempt_no").default(1).notNull(),
score: int("score"),
status: varchar("status", { length: 50 }).default("started"),
startedAt: timestamp("started_at").defaultNow().notNull(),
submittedAt: timestamp("submitted_at"),
isLate: boolean("is_late").default(false).notNull(),
createdAt: timestamp("created_at").defaultNow().notNull(),
updatedAt: timestamp("updated_at").defaultNow().onUpdateNow().notNull(),
}, (table) => ({
assignmentStudentIdx: index("hw_assignment_student_idx").on(table.assignmentId, table.studentId),
assignmentFk: foreignKey({
columns: [table.assignmentId],
foreignColumns: [homeworkAssignments.id],
name: "hw_sub_a_fk",
}).onDelete("cascade"),
studentFk: foreignKey({
columns: [table.studentId],
foreignColumns: [users.id],
name: "hw_sub_student_fk",
}).onDelete("cascade"),
}));
export const homeworkAnswers = mysqlTable("homework_answers", {
id: id("id").primaryKey(),
submissionId: varchar("submission_id", { length: 128 }).notNull(),
questionId: varchar("question_id", { length: 128 }).notNull(),
answerContent: json("answer_content"),
score: int("score"),
feedback: text("feedback"),
createdAt: timestamp("created_at").defaultNow().notNull(),
updatedAt: timestamp("updated_at").defaultNow().onUpdateNow().notNull(),
}, (table) => ({
submissionIdx: index("hw_answer_submission_idx").on(table.submissionId),
submissionQuestionIdx: index("hw_answer_submission_question_idx").on(table.submissionId, table.questionId),
submissionFk: foreignKey({
columns: [table.submissionId],
foreignColumns: [homeworkSubmissions.id],
name: "hw_ans_sub_fk",
}).onDelete("cascade"),
questionFk: foreignKey({
columns: [table.questionId],
foreignColumns: [questions.id],
name: "hw_ans_q_fk",
}),
}));
// Re-export old courses table if needed or deprecate it.
// Assuming we are replacing the old simple schema with this robust one.
// But if there were existing tables, we might keep them or comment them out.