Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
530e1a3d5d
commit
195069450f
480 files changed
+15507
-24650
No files matched your search
@@ -8,22 +8,17 @@ import {
|
||||
type ProblemSetLock,
|
||||
type ProblemSetProblem,
|
||||
type UserBadge,
|
||||
} from "@oj2/contract"
|
||||
import { and, asc, count, desc, eq, ilike, inArray, isNull, notInArray, or, sql } from "drizzle-orm"
|
||||
import { Hono } from "hono"
|
||||
} from '@oj2/contract'
|
||||
import { and, asc, count, desc, eq, ilike, inArray, isNull, notInArray, or, sql } from 'drizzle-orm'
|
||||
import { Hono } from 'hono'
|
||||
|
||||
import { optionalAuth, requireAuth, requireTeacher, type AppEnv } from "../auth/middleware"
|
||||
import type { AuthUser } from "../auth/session"
|
||||
import { db, schema } from "../db"
|
||||
import { failure, parseBody, success } from "../http"
|
||||
import { JudgeStatus, NON_FAILURE_RESULTS } from "../judge/status"
|
||||
import {
|
||||
assigningSql,
|
||||
computeProgress,
|
||||
problemSetLockCutoffs,
|
||||
problemSetLocksFor,
|
||||
} from "../services/problemset"
|
||||
import { asRecord, isTeacherOrAbove, queryInteger, sampleUser } from "./helpers"
|
||||
import { optionalAuth, requireAuth, requireTeacher, type AppEnv } from '../auth/middleware'
|
||||
import type { AuthUser } from '../auth/session'
|
||||
import { db, schema } from '../db'
|
||||
import { failure, parseBody, success } from '../http'
|
||||
import { JudgeStatus, NON_FAILURE_RESULTS } from '../judge/status'
|
||||
import { assigningSql, computeProgress, problemSetLockCutoffs, problemSetLocksFor } from '../services/problemset'
|
||||
import { asRecord, isTeacherOrAbove, queryInteger, sampleUser } from './helpers'
|
||||
|
||||
export const problemsetRoutes = new Hono<AppEnv>()
|
||||
|
||||
@@ -68,12 +63,7 @@ async function loadVisible(id: number, user: AuthUser | null | undefined) {
|
||||
const [row] = await db
|
||||
.select({ set: schema.problemset, assigning: assigningSql })
|
||||
.from(schema.problemset)
|
||||
.where(
|
||||
and(
|
||||
eq(schema.problemset.id, id),
|
||||
isTeacherOrAbove(user) ? undefined : eq(schema.problemset.visible, true),
|
||||
),
|
||||
)
|
||||
.where(and(eq(schema.problemset.id, id), isTeacherOrAbove(user) ? undefined : eq(schema.problemset.visible, true)))
|
||||
.limit(1)
|
||||
return row ?? null
|
||||
}
|
||||
@@ -112,68 +102,45 @@ function badgeData(badge: typeof schema.problemsetBadge.$inferSelect, earnedTime
|
||||
* 一次把整页题单的附属数据全查回来,再在内存里按 problemsetId 分组。
|
||||
* 固定 6 条查询,与行数无关(按行查就是 N+1)。
|
||||
*/
|
||||
async function serializeProblemSets(
|
||||
rows: { set: ProblemSetRow; assigning: boolean }[],
|
||||
userId?: number,
|
||||
) {
|
||||
async function serializeProblemSets(rows: { set: ProblemSetRow; assigning: boolean }[], userId?: number) {
|
||||
if (rows.length === 0) return []
|
||||
const ids = rows.map((row) => row.set.id)
|
||||
const [problemCounts, joinedCounts, progresses, badges, earnedRows, creators] = await Promise.all(
|
||||
[
|
||||
db
|
||||
.select({
|
||||
problemsetId: schema.problemsetProblem.problemsetId,
|
||||
value: count(),
|
||||
required: sql<number>`count(*) filter (where ${schema.problemsetProblem.isRequired})::int`,
|
||||
})
|
||||
.from(schema.problemsetProblem)
|
||||
.where(inArray(schema.problemsetProblem.problemsetId, ids))
|
||||
.groupBy(schema.problemsetProblem.problemsetId),
|
||||
db
|
||||
.select({ problemsetId: schema.problemsetProgress.problemsetId, value: count() })
|
||||
.from(schema.problemsetProgress)
|
||||
.where(inArray(schema.problemsetProgress.problemsetId, ids))
|
||||
.groupBy(schema.problemsetProgress.problemsetId),
|
||||
userId
|
||||
? db
|
||||
.select()
|
||||
.from(schema.problemsetProgress)
|
||||
.where(
|
||||
and(
|
||||
inArray(schema.problemsetProgress.problemsetId, ids),
|
||||
eq(schema.problemsetProgress.userId, userId),
|
||||
),
|
||||
)
|
||||
: Promise.resolve([] as ProgressRow[]),
|
||||
db
|
||||
.select()
|
||||
.from(schema.problemsetBadge)
|
||||
.where(inArray(schema.problemsetBadge.problemsetId, ids))
|
||||
.orderBy(asc(schema.problemsetBadge.id)),
|
||||
userId
|
||||
? db
|
||||
.select({ id: schema.userBadge.badgeId, earnedTime: schema.userBadge.earnedTime })
|
||||
.from(schema.userBadge)
|
||||
.innerJoin(
|
||||
schema.problemsetBadge,
|
||||
eq(schema.userBadge.badgeId, schema.problemsetBadge.id),
|
||||
)
|
||||
.where(
|
||||
and(
|
||||
eq(schema.userBadge.userId, userId),
|
||||
inArray(schema.problemsetBadge.problemsetId, ids),
|
||||
),
|
||||
)
|
||||
: Promise.resolve([] as { id: number; earnedTime: string }[]),
|
||||
problemSetCreators([...new Set(rows.map((row) => row.set.createdById))]),
|
||||
],
|
||||
)
|
||||
const [problemCounts, joinedCounts, progresses, badges, earnedRows, creators] = await Promise.all([
|
||||
db
|
||||
.select({
|
||||
problemsetId: schema.problemsetProblem.problemsetId,
|
||||
value: count(),
|
||||
required: sql<number>`count(*) filter (where ${schema.problemsetProblem.isRequired})::int`,
|
||||
})
|
||||
.from(schema.problemsetProblem)
|
||||
.where(inArray(schema.problemsetProblem.problemsetId, ids))
|
||||
.groupBy(schema.problemsetProblem.problemsetId),
|
||||
db
|
||||
.select({ problemsetId: schema.problemsetProgress.problemsetId, value: count() })
|
||||
.from(schema.problemsetProgress)
|
||||
.where(inArray(schema.problemsetProgress.problemsetId, ids))
|
||||
.groupBy(schema.problemsetProgress.problemsetId),
|
||||
userId
|
||||
? db
|
||||
.select()
|
||||
.from(schema.problemsetProgress)
|
||||
.where(and(inArray(schema.problemsetProgress.problemsetId, ids), eq(schema.problemsetProgress.userId, userId)))
|
||||
: Promise.resolve([] as ProgressRow[]),
|
||||
db.select().from(schema.problemsetBadge).where(inArray(schema.problemsetBadge.problemsetId, ids)).orderBy(asc(schema.problemsetBadge.id)),
|
||||
userId
|
||||
? db
|
||||
.select({ id: schema.userBadge.badgeId, earnedTime: schema.userBadge.earnedTime })
|
||||
.from(schema.userBadge)
|
||||
.innerJoin(schema.problemsetBadge, eq(schema.userBadge.badgeId, schema.problemsetBadge.id))
|
||||
.where(and(eq(schema.userBadge.userId, userId), inArray(schema.problemsetBadge.problemsetId, ids)))
|
||||
: Promise.resolve([] as { id: number; earnedTime: string }[]),
|
||||
problemSetCreators([...new Set(rows.map((row) => row.set.createdById))]),
|
||||
])
|
||||
const countBySet = new Map(problemCounts.map((item) => [item.problemsetId, item]))
|
||||
const joinedBySet = new Map(joinedCounts.map((item) => [item.problemsetId, item.value]))
|
||||
const progressBySet = new Map(progresses.map((item) => [item.problemsetId, item]))
|
||||
const badgesBySet = new Map<number, (typeof schema.problemsetBadge.$inferSelect)[]>()
|
||||
for (const badge of badges)
|
||||
badgesBySet.set(badge.problemsetId, [...(badgesBySet.get(badge.problemsetId) ?? []), badge])
|
||||
for (const badge of badges) badgesBySet.set(badge.problemsetId, [...(badgesBySet.get(badge.problemsetId) ?? []), badge])
|
||||
const earned = new Map(earnedRows.map((item) => [item.id, item.earnedTime]))
|
||||
return rows.map(({ set: row, assigning }) => {
|
||||
const progress = progressBySet.get(row.id)
|
||||
@@ -181,8 +148,7 @@ async function serializeProblemSets(
|
||||
id: row.id,
|
||||
title: row.title,
|
||||
description: row.description,
|
||||
createdBy:
|
||||
creators.get(row.createdById) ?? sampleUser({ id: row.createdById, username: "" }, null),
|
||||
createdBy: creators.get(row.createdById) ?? sampleUser({ id: row.createdById, username: '' }, null),
|
||||
createTime: row.createTime,
|
||||
assignedUntil: row.assignedUntil,
|
||||
assigning,
|
||||
@@ -191,25 +157,17 @@ async function serializeProblemSets(
|
||||
requiredCount: countBySet.get(row.id)?.required || (countBySet.get(row.id)?.value ?? 0),
|
||||
joinedCount: joinedBySet.get(row.id) ?? 0,
|
||||
userProgress: progressSummary(progress),
|
||||
badges: (badgesBySet.get(row.id) ?? []).map((badge) =>
|
||||
badgeData(badge, userId ? (earned.get(badge.id) ?? null) : undefined),
|
||||
),
|
||||
badges: (badgesBySet.get(row.id) ?? []).map((badge) => badgeData(badge, userId ? (earned.get(badge.id) ?? null) : undefined)),
|
||||
} satisfies ProblemSet
|
||||
})
|
||||
}
|
||||
|
||||
problemsetRoutes.get("/problem-sets", optionalAuth, async (c) => {
|
||||
const limit = queryInteger(c.req.query("limit"), 10, { min: 1, max: 250 })
|
||||
const offset = queryInteger(c.req.query("offset"), 0, { min: 0 })
|
||||
problemsetRoutes.get('/problem-sets', optionalAuth, async (c) => {
|
||||
const limit = queryInteger(c.req.query('limit'), 10, { min: 1, max: 250 })
|
||||
const offset = queryInteger(c.req.query('offset'), 0, { min: 0 })
|
||||
const filters = [eq(schema.problemset.visible, true)]
|
||||
const keyword = c.req.query("keyword")?.trim()
|
||||
if (keyword)
|
||||
filters.push(
|
||||
or(
|
||||
ilike(schema.problemset.title, `%${keyword}%`),
|
||||
ilike(schema.problemset.description, `%${keyword}%`),
|
||||
)!,
|
||||
)
|
||||
const keyword = c.req.query('keyword')?.trim()
|
||||
if (keyword) filters.push(or(ilike(schema.problemset.title, `%${keyword}%`), ilike(schema.problemset.description, `%${keyword}%`))!)
|
||||
const where = and(...filters)
|
||||
const [totalRows, rows] = await Promise.all([
|
||||
db.select({ value: count() }).from(schema.problemset).where(where),
|
||||
@@ -222,18 +180,18 @@ problemsetRoutes.get("/problem-sets", optionalAuth, async (c) => {
|
||||
.offset(offset),
|
||||
])
|
||||
return success(c, {
|
||||
results: await serializeProblemSets(rows, c.get("user")?.id),
|
||||
results: await serializeProblemSets(rows, c.get('user')?.id),
|
||||
total: totalRows[0]?.value ?? 0,
|
||||
} satisfies ProblemSetList)
|
||||
})
|
||||
|
||||
// 字面量路由要排在 /problem-sets/:id 前面:Hono 按注册顺序匹配,不是静态优先
|
||||
// 题号(problemDisplayId)和主键(problemId)都认:提交列表那边手里只有题号
|
||||
problemsetRoutes.get("/problem-sets/locks", requireAuth, async (c) => {
|
||||
const user = c.get("user")!
|
||||
problemsetRoutes.get('/problem-sets/locks', requireAuth, async (c) => {
|
||||
const user = c.get('user')!
|
||||
if (isTeacherOrAbove(user)) return success(c, [] as ProblemSetLock[])
|
||||
let problemId = queryInteger(c.req.query("problemId"), 0, { min: 1 })
|
||||
const displayId = c.req.query("problemDisplayId")?.trim()
|
||||
let problemId = queryInteger(c.req.query('problemId'), 0, { min: 1 })
|
||||
const displayId = c.req.query('problemDisplayId')?.trim()
|
||||
if (!problemId && displayId) {
|
||||
const [problem] = await db
|
||||
.select({ id: schema.problem.id })
|
||||
@@ -247,17 +205,17 @@ problemsetRoutes.get("/problem-sets/locks", requireAuth, async (c) => {
|
||||
return success(c, rows satisfies ProblemSetLock[])
|
||||
})
|
||||
|
||||
problemsetRoutes.get("/problem-sets/:id", optionalAuth, async (c) => {
|
||||
const row = await loadVisible(queryInteger(c.req.param("id"), 0, { min: 1 }), c.get("user"))
|
||||
if (!row) return failure(c, 404, "problem-set-not-found", "题单不存在")
|
||||
const [data] = await serializeProblemSets([row], c.get("user")?.id)
|
||||
problemsetRoutes.get('/problem-sets/:id', optionalAuth, async (c) => {
|
||||
const row = await loadVisible(queryInteger(c.req.param('id'), 0, { min: 1 }), c.get('user'))
|
||||
if (!row) return failure(c, 404, 'problem-set-not-found', '题单不存在')
|
||||
const [data] = await serializeProblemSets([row], c.get('user')?.id)
|
||||
return success(c, data)
|
||||
})
|
||||
|
||||
problemsetRoutes.get("/problem-sets/:id/problems", optionalAuth, async (c) => {
|
||||
const user = c.get("user")
|
||||
const found = await loadVisible(queryInteger(c.req.param("id"), 0, { min: 1 }), user)
|
||||
if (!found) return failure(c, 404, "problem-set-not-found", "题单不存在")
|
||||
problemsetRoutes.get('/problem-sets/:id/problems', optionalAuth, async (c) => {
|
||||
const user = c.get('user')
|
||||
const found = await loadVisible(queryInteger(c.req.param('id'), 0, { min: 1 }), user)
|
||||
if (!found) return failure(c, 404, 'problem-set-not-found', '题单不存在')
|
||||
const id = found.set.id
|
||||
// order 后面必须再跟一个 tiebreaker:并列时 Postgres 不保证次序,而题目是按数组下标
|
||||
// 编号的(1、2、3),老题单里 order 大量重复,不定死的话「第 3 题」每次刷新都可能变
|
||||
@@ -279,27 +237,14 @@ problemsetRoutes.get("/problem-sets/:id/problems", optionalAuth, async (c) => {
|
||||
db
|
||||
.select({ detail: schema.problemsetProgress.progressDetail })
|
||||
.from(schema.problemsetProgress)
|
||||
.where(
|
||||
and(
|
||||
eq(schema.problemsetProgress.problemsetId, id),
|
||||
eq(schema.problemsetProgress.userId, user.id),
|
||||
),
|
||||
)
|
||||
.where(and(eq(schema.problemsetProgress.problemsetId, id), eq(schema.problemsetProgress.userId, user.id)))
|
||||
.limit(1),
|
||||
db
|
||||
.select({ problemId: schema.submission.problemId, value: count() })
|
||||
.from(schema.submission)
|
||||
.where(
|
||||
and(
|
||||
eq(schema.submission.userId, user.id),
|
||||
eq(schema.submission.problemsetId, id),
|
||||
notInArray(schema.submission.result, NON_FAILURE_RESULTS),
|
||||
),
|
||||
)
|
||||
.where(and(eq(schema.submission.userId, user.id), eq(schema.submission.problemsetId, id), notInArray(schema.submission.result, NON_FAILURE_RESULTS)))
|
||||
.groupBy(schema.submission.problemId),
|
||||
isTeacherOrAbove(user)
|
||||
? new Map<number, string>()
|
||||
: problemSetLockCutoffs(user.id, problemIds),
|
||||
isTeacherOrAbove(user) ? new Map<number, string>() : problemSetLockCutoffs(user.id, problemIds),
|
||||
])
|
||||
: [[], [], new Map<number, string>()]
|
||||
// 「以前做对过 · 旧代码先藏着」:这道题正被闸门挡着,而且分界之前确实做对过
|
||||
@@ -315,8 +260,7 @@ problemsetRoutes.get("/problem-sets/:id/problems", optionalAuth, async (c) => {
|
||||
inArray(schema.submission.result, SOLVED_RESULTS),
|
||||
or(
|
||||
...[...cutoffs].map(
|
||||
([problemId, cutoff]) =>
|
||||
sql`(${schema.submission.problemId} = ${problemId} and ${schema.submission.createTime} < ${cutoff})`,
|
||||
([problemId, cutoff]) => sql`(${schema.submission.problemId} = ${problemId} and ${schema.submission.createTime} < ${cutoff})`,
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -338,7 +282,7 @@ problemsetRoutes.get("/problem-sets/:id/problems", optionalAuth, async (c) => {
|
||||
order: link.order,
|
||||
isRequired: link.isRequired,
|
||||
isCompleted: entry !== undefined,
|
||||
solvedTime: typeof solvedTime === "string" ? solvedTime : null,
|
||||
solvedTime: typeof solvedTime === 'string' ? solvedTime : null,
|
||||
wrongCount: wrong.get(problemId) ?? 0,
|
||||
oldCodeHidden: hidden.has(problemId),
|
||||
} satisfies ProblemSetProblem
|
||||
@@ -346,24 +290,19 @@ problemsetRoutes.get("/problem-sets/:id/problems", optionalAuth, async (c) => {
|
||||
)
|
||||
})
|
||||
|
||||
problemsetRoutes.post("/problem-set-progress", requireAuth, async (c) => {
|
||||
const parsed = await parseBody(c, joinProblemSetRequestSchema, "Invalid problem set")
|
||||
problemsetRoutes.post('/problem-set-progress', requireAuth, async (c) => {
|
||||
const parsed = await parseBody(c, joinProblemSetRequestSchema, 'Invalid problem set')
|
||||
if (!parsed.success) return parsed.response
|
||||
const user = c.get("user")!
|
||||
const user = c.get('user')!
|
||||
const found = await loadVisible(parsed.data.problemSetId, user)
|
||||
if (!found) return failure(c, 404, "problem-set-not-found", "题单不存在")
|
||||
if (!found) return failure(c, 404, 'problem-set-not-found', '题单不存在')
|
||||
const problemSet = found.set
|
||||
const [existing] = await db
|
||||
.select({ id: schema.problemsetProgress.id })
|
||||
.from(schema.problemsetProgress)
|
||||
.where(
|
||||
and(
|
||||
eq(schema.problemsetProgress.problemsetId, problemSet.id),
|
||||
eq(schema.problemsetProgress.userId, user.id),
|
||||
),
|
||||
)
|
||||
.where(and(eq(schema.problemsetProgress.problemsetId, problemSet.id), eq(schema.problemsetProgress.userId, user.id)))
|
||||
.limit(1)
|
||||
if (existing) return failure(c, 409, "already-joined", "已经加入该题单")
|
||||
if (existing) return failure(c, 409, 'already-joined', '已经加入该题单')
|
||||
await db.transaction(async (tx) => {
|
||||
const links = await tx
|
||||
.select({
|
||||
@@ -383,16 +322,16 @@ problemsetRoutes.post("/problem-set-progress", requireAuth, async (c) => {
|
||||
return success(c, null, 201)
|
||||
})
|
||||
|
||||
problemsetRoutes.get("/users/:username/badges", optionalAuth, async (c) => {
|
||||
const requested = c.req.param("username")
|
||||
const username = requested === "me" ? c.get("user")?.username : requested
|
||||
if (!username) return failure(c, 401, "login-required", "Authentication required")
|
||||
problemsetRoutes.get('/users/:username/badges', optionalAuth, async (c) => {
|
||||
const requested = c.req.param('username')
|
||||
const username = requested === 'me' ? c.get('user')?.username : requested
|
||||
if (!username) return failure(c, 401, 'login-required', 'Authentication required')
|
||||
const [target] = await db
|
||||
.select({ id: schema.user.id })
|
||||
.from(schema.user)
|
||||
.where(and(eq(schema.user.username, username), eq(schema.user.isDisabled, false)))
|
||||
.limit(1)
|
||||
if (!target) return failure(c, 404, "user-not-found", "用户不存在")
|
||||
if (!target) return failure(c, 404, 'user-not-found', '用户不存在')
|
||||
const rows = await db
|
||||
.select({
|
||||
userBadge: schema.userBadge,
|
||||
@@ -429,9 +368,9 @@ problemsetRoutes.get("/users/:username/badges", optionalAuth, async (c) => {
|
||||
* 想看别的班再换(和比赛「全班情况」按参赛者推断班级是一个思路)。名单是那个班的全部学生,
|
||||
* 没加入的也列出来,好点名。
|
||||
*/
|
||||
problemsetRoutes.get("/problem-sets/:id/class-view", requireTeacher, async (c) => {
|
||||
const found = await loadVisible(queryInteger(c.req.param("id"), 0, { min: 1 }), c.get("user"))
|
||||
if (!found) return failure(c, 404, "problem-set-not-found", "题单不存在")
|
||||
problemsetRoutes.get('/problem-sets/:id/class-view', requireTeacher, async (c) => {
|
||||
const found = await loadVisible(queryInteger(c.req.param('id'), 0, { min: 1 }), c.get('user'))
|
||||
if (!found) return failure(c, 404, 'problem-set-not-found', '题单不存在')
|
||||
const id = found.set.id
|
||||
const studentRole = inArray(schema.user.adminType, [...STUDENT_ROLES])
|
||||
const [links, joinedClasses] = await Promise.all([
|
||||
@@ -454,13 +393,7 @@ problemsetRoutes.get("/problem-sets/:id/class-view", requireTeacher, async (c) =
|
||||
})
|
||||
.from(schema.problemsetProgress)
|
||||
.innerJoin(schema.user, eq(schema.user.id, schema.problemsetProgress.userId))
|
||||
.where(
|
||||
and(
|
||||
eq(schema.problemsetProgress.problemsetId, id),
|
||||
studentRole,
|
||||
sql`coalesce(${schema.user.className}, '') <> ''`,
|
||||
),
|
||||
)
|
||||
.where(and(eq(schema.problemsetProgress.problemsetId, id), studentRole, sql`coalesce(${schema.user.className}, '') <> ''`))
|
||||
.groupBy(schema.user.className)
|
||||
.orderBy(sql`max(${schema.problemsetProgress.joinTime}) desc`),
|
||||
])
|
||||
@@ -490,15 +423,9 @@ problemsetRoutes.get("/problem-sets/:id/class-view", requireTeacher, async (c) =
|
||||
size: sizeOf.get(row.className) ?? row.joined,
|
||||
}))
|
||||
.map((row, recency) => ({ row, recency, doing: row.joined >= 5 && row.joined * 3 >= row.size }))
|
||||
.sort((a, b) =>
|
||||
a.doing !== b.doing
|
||||
? Number(b.doing) - Number(a.doing)
|
||||
: a.doing
|
||||
? a.recency - b.recency
|
||||
: b.row.joined - a.row.joined,
|
||||
)
|
||||
.sort((a, b) => (a.doing !== b.doing ? Number(b.doing) - Number(a.doing) : a.doing ? a.recency - b.recency : b.row.joined - a.row.joined))
|
||||
.map((item) => item.row)
|
||||
const wanted = c.req.query("className")?.trim()
|
||||
const wanted = c.req.query('className')?.trim()
|
||||
const className = wanted || classes[0]?.className || null
|
||||
const graded = gradedIds(links)
|
||||
const empty: ProblemSetClassView = {
|
||||
@@ -525,16 +452,8 @@ problemsetRoutes.get("/problem-sets/:id/class-view", requireTeacher, async (c) =
|
||||
})
|
||||
.from(schema.user)
|
||||
.leftJoin(schema.userProfile, eq(schema.userProfile.userId, schema.user.id))
|
||||
.leftJoin(
|
||||
schema.problemsetProgress,
|
||||
and(
|
||||
eq(schema.problemsetProgress.userId, schema.user.id),
|
||||
eq(schema.problemsetProgress.problemsetId, id),
|
||||
),
|
||||
)
|
||||
.where(
|
||||
and(eq(schema.user.className, className), studentRole, eq(schema.user.isDisabled, false)),
|
||||
)
|
||||
.leftJoin(schema.problemsetProgress, and(eq(schema.problemsetProgress.userId, schema.user.id), eq(schema.problemsetProgress.problemsetId, id)))
|
||||
.where(and(eq(schema.user.className, className), studentRole, eq(schema.user.isDisabled, false)))
|
||||
.orderBy(asc(schema.user.username))
|
||||
if (roster.length === 0) return success(c, empty)
|
||||
const userIds = roster.map((row) => row.userId)
|
||||
@@ -547,25 +466,13 @@ problemsetRoutes.get("/problem-sets/:id/class-view", requireTeacher, async (c) =
|
||||
value: count(),
|
||||
})
|
||||
.from(schema.submission)
|
||||
.where(
|
||||
and(
|
||||
eq(schema.submission.problemsetId, id),
|
||||
inArray(schema.submission.userId, userIds),
|
||||
notInArray(schema.submission.result, NON_FAILURE_RESULTS),
|
||||
),
|
||||
)
|
||||
.where(and(eq(schema.submission.problemsetId, id), inArray(schema.submission.userId, userIds), notInArray(schema.submission.result, NON_FAILURE_RESULTS)))
|
||||
.groupBy(schema.submission.userId, schema.submission.problemId),
|
||||
// 加入之前就做对过(哪个入口都算):老师据此知道谁可能是凭记忆默写的
|
||||
db
|
||||
.select({ userId: schema.submission.userId, problemId: schema.submission.problemId })
|
||||
.from(schema.submission)
|
||||
.innerJoin(
|
||||
schema.problemsetProgress,
|
||||
and(
|
||||
eq(schema.problemsetProgress.userId, schema.submission.userId),
|
||||
eq(schema.problemsetProgress.problemsetId, id),
|
||||
),
|
||||
)
|
||||
.innerJoin(schema.problemsetProgress, and(eq(schema.problemsetProgress.userId, schema.submission.userId), eq(schema.problemsetProgress.problemsetId, id)))
|
||||
.where(
|
||||
and(
|
||||
inArray(schema.submission.userId, userIds),
|
||||
@@ -591,7 +498,7 @@ problemsetRoutes.get("/problem-sets/:id/class-view", requireTeacher, async (c) =
|
||||
if (graded.has(link.problemId)) completed += 1
|
||||
}
|
||||
return {
|
||||
solvedTime: typeof time === "string" ? time : null,
|
||||
solvedTime: typeof time === 'string' ? time : null,
|
||||
wrongCount: wrong.get(key(row.userId, link.problemId)) ?? 0,
|
||||
solvedBefore: before.has(key(row.userId, link.problemId)),
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user