fix(课堂看板): 作业里有 SQL 题就布置不上去(报「不能用 C 交」),也没法布置一节 SQL 课
- 布置作业的语言加上 SQL,数据库课整节选 SQL - C / Python 的作业里可以夹 SQL 题:SQL 题只收 SQL,不受作业语言限制,交对 SQL 就算做完 (学生首页课堂条、看板格子、班里做了几个人三处一起改) - 看板点 SQL 题的格子,提交列表按 SQL 筛,不再是一片空白;提交列表「这节课」遇到夹了 SQL 题的作业不再按 C 筛 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
98dfcc1e7e
commit
fcf63c2ecb
7 files changed
+71
-42
No files matched your search
@@ -44,13 +44,13 @@ import type {
|
||||
FlowchartStatus,
|
||||
HintDiagnosis,
|
||||
JudgeStatus,
|
||||
LessonLanguage,
|
||||
ProblemDifficulty,
|
||||
ProblemLanguage,
|
||||
ProblemPermission,
|
||||
ProblemSetDifficulty,
|
||||
ProblemSetStatus,
|
||||
ReactionKey,
|
||||
RunnableLanguage,
|
||||
SqlConfig,
|
||||
SqlDisplay,
|
||||
TagCategory,
|
||||
@@ -1497,10 +1497,10 @@ export const classLesson = pgTable(
|
||||
day: text().notNull(),
|
||||
problemIds: jsonb("problem_ids").notNull().$type<number[]>(),
|
||||
/**
|
||||
* 这份作业用什么语言做(Python / C / C++),老师布置时选。可空:加这一列之前布置的
|
||||
* 没有它。写入侧在 PUT /classroom/lesson 按 runnableLanguageSchema 校验过
|
||||
* 这份作业用什么语言做(Python / C / C++ / SQL),老师布置时选。可空:加这一列之前布置的
|
||||
* 没有它。写入侧在 PUT /classroom/lesson 按 lessonLanguageSchema 校验过
|
||||
*/
|
||||
language: text().$type<RunnableLanguage>(),
|
||||
language: text().$type<LessonLanguage>(),
|
||||
createdBy: integer("created_by").notNull(),
|
||||
updatedAt: timestamp("updated_at", {
|
||||
withTimezone: true,
|
||||
|
||||
@@ -12,7 +12,7 @@ import {
|
||||
type ClassRankItem,
|
||||
type ClassUserRank,
|
||||
type LastVisit,
|
||||
type RunnableLanguage,
|
||||
type LessonLanguage,
|
||||
FLOWCHART_PASS_GRADES,
|
||||
} from "@oj2/contract"
|
||||
import {
|
||||
@@ -221,12 +221,21 @@ function classMemberIds(className: string) {
|
||||
* 有的题一节课全班都在画流程图、代码提交个位数,只数代码的话这节课在统计里等于没上。
|
||||
* 两边都按 `user_id in (...)` + `create_time >=` 走各自的 (user_id, create_time) 索引。
|
||||
*/
|
||||
/**
|
||||
* 老师布置时选了语言:代码只数这个语言交的。SQL 题例外 —— 它只收 SQL,按 C 去数的话,
|
||||
* C 的作业里夹一道 SQL 题,学生怎么交都算不了做完。非 SQL 题交不进 SQL(提交接口按题目的
|
||||
* 语言拦),所以一律放行 SQL 不会把别的题算宽
|
||||
*/
|
||||
function lessonLanguageFilter(language: LessonLanguage | null | undefined) {
|
||||
return language ? inArray(schema.submission.language, [language, "SQL"]) : undefined
|
||||
}
|
||||
|
||||
function activityRows(options: {
|
||||
since: string
|
||||
users?: number[] | ReturnType<typeof classMemberIds>
|
||||
problemIds?: number[]
|
||||
/** 老师布置时选了语言:代码只数这个语言交的,流程图不受影响 */
|
||||
language?: RunnableLanguage | null
|
||||
/** 老师布置时选了语言:代码只数这个语言交的(见 lessonLanguageFilter),流程图不受影响 */
|
||||
language?: LessonLanguage | null
|
||||
}) {
|
||||
const { since, users, problemIds, language } = options
|
||||
const code = db
|
||||
@@ -243,7 +252,7 @@ function activityRows(options: {
|
||||
gte(schema.submission.createTime, since),
|
||||
users ? inArray(schema.submission.userId, users) : undefined,
|
||||
problemIds ? inArray(schema.submission.problemId, problemIds) : undefined,
|
||||
language ? eq(schema.submission.language, language) : undefined,
|
||||
lessonLanguageFilter(language),
|
||||
),
|
||||
)
|
||||
const flowchart = db
|
||||
@@ -272,7 +281,7 @@ function activityRows(options: {
|
||||
async function classDayGroups(
|
||||
className: string,
|
||||
since: string,
|
||||
options: { minUsers?: number; problemIds?: number[]; language?: RunnableLanguage | null } = {},
|
||||
options: { minUsers?: number; problemIds?: number[]; language?: LessonLanguage | null } = {},
|
||||
): Promise<ClassDayGroup[]> {
|
||||
const activity = activityRows({
|
||||
since,
|
||||
@@ -297,12 +306,14 @@ async function classDayGroups(
|
||||
|
||||
/** 按 id 取题,只留学生看得见的(题库里、visible),被藏起来的点进去也是 404 */
|
||||
async function visibleProblems(ids: number[]) {
|
||||
if (!ids.length) return new Map<number, { id: number; displayId: string; title: string }>()
|
||||
if (!ids.length)
|
||||
return new Map<number, { id: number; displayId: string; title: string; isSql: boolean }>()
|
||||
const rows = await db
|
||||
.select({
|
||||
id: schema.problem.id,
|
||||
displayId: schema.problem.displayId,
|
||||
title: schema.problem.title,
|
||||
isSql: sql<boolean>`${schema.problem.languages} @> '["SQL"]'::jsonb`,
|
||||
})
|
||||
.from(schema.problem)
|
||||
.where(
|
||||
@@ -533,7 +544,7 @@ classroomRoutes.get("/me/class-activity", requireAuth, async (c) => {
|
||||
inArray(schema.submission.problemId, ids),
|
||||
isNull(schema.submission.contestId),
|
||||
// 布置的是 C:以前用 Python 做对过不算这份作业做完
|
||||
lesson.language ? eq(schema.submission.language, lesson.language) : undefined,
|
||||
lessonLanguageFilter(lesson.language),
|
||||
),
|
||||
)
|
||||
.groupBy(schema.submission.problemId)
|
||||
@@ -706,7 +717,7 @@ classroomRoutes.get("/classroom/board", requireTeacher, async (c) => {
|
||||
isNull(schema.submission.contestId),
|
||||
inArray(schema.submission.userId, userIds),
|
||||
inArray(schema.submission.problemId, ids),
|
||||
lesson.language ? eq(schema.submission.language, lesson.language) : undefined,
|
||||
lessonLanguageFilter(lesson.language),
|
||||
),
|
||||
)
|
||||
.groupBy(schema.submission.userId, schema.submission.problemId)
|
||||
@@ -807,6 +818,7 @@ classroomRoutes.get("/classroom/board", requireTeacher, async (c) => {
|
||||
problemId: problem.id,
|
||||
problemDisplayId: problem.displayId,
|
||||
title: problem.title,
|
||||
isSql: problem.isSql,
|
||||
})),
|
||||
students: roster.map(
|
||||
(student) =>
|
||||
@@ -852,10 +864,12 @@ classroomRoutes.get("/classroom/lesson", requireTeacher, async (c) => {
|
||||
} satisfies ClassLesson)
|
||||
// 和看板一样只回看今天:「这节课」不该冒出昨天的题
|
||||
const lesson = await classLessonProblems(className, 1)
|
||||
// C / Python 的作业夹了 SQL 题就不筛语言(见契约 classLessonSchema.language)
|
||||
const mixed = lesson.problems.some((problem) => problem.isSql !== (lesson.language === "SQL"))
|
||||
return success(c, {
|
||||
className,
|
||||
source: lesson.source,
|
||||
language: lesson.language,
|
||||
language: mixed ? null : lesson.language,
|
||||
problems: lesson.problems.map((problem) => ({
|
||||
problemDisplayId: problem.displayId,
|
||||
title: problem.title,
|
||||
@@ -911,10 +925,11 @@ classroomRoutes.put("/classroom/lesson", requireTeacher, async (c) => {
|
||||
return failure(c, 400, "problem-not-found", `这些题号不存在或没有公开:${missing.join("、")}`)
|
||||
}
|
||||
|
||||
// 选了语言就得每道都能用它交,不然学生点开才发现交不了(提交接口会拒:language-not-allowed)
|
||||
// 选了语言就得每道都能用它交,不然学生点开才发现交不了(提交接口会拒:language-not-allowed)。
|
||||
// SQL 题除外:它只收 SQL,C 的作业里夹一道也不会让人选错语言,计数那边也放行了 SQL
|
||||
if (language) {
|
||||
const unsupported = found
|
||||
.filter((row) => !row.languages.includes(language))
|
||||
.filter((row) => !row.languages.includes(language) && !row.languages.includes("SQL"))
|
||||
.map((row) => row.displayId)
|
||||
if (unsupported.length) {
|
||||
return failure(
|
||||
|
||||
@@ -42,7 +42,7 @@ import {
|
||||
type FlowchartStatistics,
|
||||
type SubmissionStatistics,
|
||||
type SubmissionLessons,
|
||||
type RunnableLanguage,
|
||||
type LessonLanguage,
|
||||
type SubmissionStatisticsGrid,
|
||||
type TodaySubmissionStatistics,
|
||||
} from "@oj2/contract"
|
||||
@@ -261,7 +261,7 @@ export function getClassLesson(className?: string) {
|
||||
export function setClassLesson(
|
||||
className: string,
|
||||
problemDisplayIds: string[],
|
||||
language: RunnableLanguage | null,
|
||||
language: LessonLanguage | null,
|
||||
) {
|
||||
return api.put<null>("classroom/lesson", { className, problemDisplayIds, language })
|
||||
}
|
||||
|
||||
@@ -3,11 +3,11 @@ import { getClassBoard, setClassLesson } from "oj/api"
|
||||
import { useLeaveStudents } from "shared/composables/leaveStudents"
|
||||
import { useCollabStore } from "shared/store/collab"
|
||||
import { useConfigStore } from "shared/store/config"
|
||||
import type { RunnableLanguage } from "@oj2/contract"
|
||||
import type { LessonLanguage } from "@oj2/contract"
|
||||
import { errorMessage } from "utils/api"
|
||||
import { LANGUAGE_SHOW_VALUE } from "utils/constants"
|
||||
import { parseTime } from "utils/functions"
|
||||
import type { ClassBoard, ClassBoardCell, ClassBoardStudent } from "utils/types"
|
||||
import type { ClassBoard, ClassBoardCell, ClassBoardProblem, ClassBoardStudent } from "utils/types"
|
||||
|
||||
/**
|
||||
* 课堂看板(老师):这个班、今天、这节课的几道题 × 全班学生。
|
||||
@@ -40,8 +40,8 @@ const lessonInput = ref("")
|
||||
* 这份作业用什么语言做,跟着这一次布置走(用户定的:不是给班级定死的设置)。默认是这个班
|
||||
* 上一次布置时选的;选了之后看板和学生那边的「做完」都只认这个语言交对的
|
||||
*/
|
||||
const lessonLanguage = ref<RunnableLanguage>("Python")
|
||||
const LESSON_LANGUAGES: RunnableLanguage[] = ["Python", "C", "C++"]
|
||||
const lessonLanguage = ref<LessonLanguage>("Python")
|
||||
const LESSON_LANGUAGES: LessonLanguage[] = ["Python", "C", "C++", "SQL"]
|
||||
const languageOptions = LESSON_LANGUAGES.map((value) => ({
|
||||
label: LANGUAGE_SHOW_VALUE[value],
|
||||
value,
|
||||
@@ -291,9 +291,11 @@ const router = useRouter()
|
||||
*/
|
||||
function submissionsHref(
|
||||
student: ClassBoardStudent,
|
||||
problemDisplayId: string | undefined,
|
||||
problem: ClassBoardProblem | undefined,
|
||||
flowchart: boolean,
|
||||
) {
|
||||
// SQL 题不受布置的语言限制,格子数的是 SQL 交的
|
||||
const language = problem?.isSql ? "SQL" : board.value?.language
|
||||
return router.resolve({
|
||||
name: "submissions",
|
||||
query: {
|
||||
@@ -301,12 +303,8 @@ function submissionsHref(
|
||||
exactUsername: "1",
|
||||
today: "1",
|
||||
// 布置时选了语言的,代码那边只看这个语言交的(和格子里数的一致)
|
||||
...(flowchart
|
||||
? { language: "Flowchart" }
|
||||
: board.value?.language
|
||||
? { language: board.value.language }
|
||||
: {}),
|
||||
...(problemDisplayId ? { problem: problemDisplayId } : {}),
|
||||
...(flowchart ? { language: "Flowchart" } : language ? { language } : {}),
|
||||
...(problem ? { problem: problem.problemDisplayId } : {}),
|
||||
},
|
||||
}).href
|
||||
}
|
||||
@@ -388,7 +386,10 @@ function submissionsHref(
|
||||
<n-text depth="3" class="meta">
|
||||
<template v-if="board.source === 'teacher'">
|
||||
学生首页的「班里在做」显示的就是这几道<template v-if="board.language"
|
||||
>,用 {{ LANGUAGE_SHOW_VALUE[board.language] }} 交对才算做完</template
|
||||
>,用 {{ LANGUAGE_SHOW_VALUE[board.language] }} 交对才算做完<template
|
||||
v-if="board.language !== 'SQL' && board.problems.some((p) => p.isSql)"
|
||||
>(SQL 题交对 SQL 就算)</template
|
||||
></template
|
||||
>。清空再点布置就改回自动推断。
|
||||
</template>
|
||||
<template v-else-if="board.source === 'inferred'">
|
||||
@@ -515,13 +516,7 @@ function submissionsHref(
|
||||
<a
|
||||
v-if="cell.attempts"
|
||||
class="cell-link"
|
||||
:href="
|
||||
submissionsHref(
|
||||
student,
|
||||
board.problems[i]!.problemDisplayId,
|
||||
!cell.codeAttempts,
|
||||
)
|
||||
"
|
||||
:href="submissionsHref(student, board.problems[i], !cell.codeAttempts)"
|
||||
target="_blank"
|
||||
:title="
|
||||
cell.codeAttempts
|
||||
|
||||
@@ -300,7 +300,13 @@ export type { ClassComparison, ClassRankItem, ClassUserRank } from "@oj2/contrac
|
||||
export type { ClassActivity, ClassActivityProblem, LastVisit } from "@oj2/contract"
|
||||
|
||||
/** 课堂看板:一个班今天这节课的题 × 全班学生 */
|
||||
export type { ClassBoard, ClassBoardStudent, ClassBoardCell, ClassLesson } from "@oj2/contract"
|
||||
export type {
|
||||
ClassBoard,
|
||||
ClassBoardProblem,
|
||||
ClassBoardStudent,
|
||||
ClassBoardCell,
|
||||
ClassLesson,
|
||||
} from "@oj2/contract"
|
||||
|
||||
/** 知识点地图:每个知识点做对几道、在第几档,只给自己看 */
|
||||
export type { KnowledgeLevel, KnowledgeMap } from "@oj2/contract"
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { z } from "zod"
|
||||
|
||||
import { judgeStatusSchema } from "./judge-status"
|
||||
import { runnableLanguageSchema } from "./language"
|
||||
import { lessonLanguageSchema as lessonLanguageValueSchema } from "./language"
|
||||
|
||||
export const classRankItemSchema = z.object({
|
||||
className: z.string(),
|
||||
@@ -85,10 +85,11 @@ export const classActivityProblemSchema = z.object({
|
||||
})
|
||||
|
||||
/**
|
||||
* 老师布置这节课时选的语言(Python / C / C++)。null = 没选:推断出来的课、加这一项之前
|
||||
* 布置的课。选了之后这份作业「做完」只认这个语言交对的,学生打开这几道题编辑器默认就是它
|
||||
* 老师布置这节课时选的语言(Python / C / C++ / SQL)。null = 没选:推断出来的课、加这一项之前
|
||||
* 布置的课。选了之后这份作业「做完」只认这个语言交对的(SQL 题例外,交对 SQL 就算),
|
||||
* 学生打开这几道题编辑器默认就是它
|
||||
*/
|
||||
const lessonLanguageSchema = runnableLanguageSchema.nullable()
|
||||
const lessonLanguageSchema = lessonLanguageValueSchema.nullable()
|
||||
|
||||
export const classActivitySchema = z.object({
|
||||
className: z.string().nullable(),
|
||||
@@ -147,6 +148,8 @@ export const classBoardProblemSchema = z.object({
|
||||
problemId: z.number().int(),
|
||||
problemDisplayId: z.string(),
|
||||
title: z.string(),
|
||||
/** SQL 题不受布置的语言限制:格子数的是 SQL 交的,点进提交列表也按 SQL 筛 */
|
||||
isSql: z.boolean(),
|
||||
})
|
||||
|
||||
export const classBoardCellSchema = z.object({
|
||||
@@ -214,7 +217,10 @@ export const classBoardSchema = z.object({
|
||||
export const classLessonSchema = z.object({
|
||||
className: z.string().nullable(),
|
||||
source: z.enum(["teacher", "inferred"]).nullable(),
|
||||
/** 选了语言时,列表那边连语言一起筛 */
|
||||
/**
|
||||
* 选了语言时,列表那边连语言一起筛。C / Python 的作业里夹了 SQL 题就是 null:
|
||||
* 列表只能筛一种语言,按 C 筛会把 SQL 题交的全藏起来,宁可多列一些
|
||||
*/
|
||||
language: lessonLanguageSchema,
|
||||
problems: z.array(z.object({ problemDisplayId: z.string(), title: z.string() })),
|
||||
})
|
||||
|
||||
@@ -43,7 +43,14 @@ export const problemLanguageSchema = z.enum([...judgeLanguageSchema.options, "SQ
|
||||
*/
|
||||
export const runnableLanguageSchema = judgeLanguageSchema.extract(["C", "C++", "Python"])
|
||||
|
||||
/**
|
||||
* 老师布置作业能选的语言:判题机跑得了的三种,加 SQL(数据库课一整节都是 SQL 题)。
|
||||
* SQL 题只收 SQL,所以 C / Python 的作业里夹 SQL 题也行 —— 那几道不受作业语言限制
|
||||
*/
|
||||
export const lessonLanguageSchema = problemLanguageSchema.extract(["Python", "C", "C++", "SQL"])
|
||||
|
||||
export type JudgeLanguage = z.infer<typeof judgeLanguageSchema>
|
||||
export type LessonLanguage = z.infer<typeof lessonLanguageSchema>
|
||||
export type RunnableLanguage = z.infer<typeof runnableLanguageSchema>
|
||||
export type ProblemLanguage = z.infer<typeof problemLanguageSchema>
|
||||
|
||||
|
||||
Reference in new issue
Block a user