fix: 老师看作品不用先交;补学生看/评同学作品的权限漏洞;随机评分改成弹框
Deploy / deploy (build, debian, 22) (push) Canceled after 0s
Deploy / deploy (build, debian, 22) (push) Canceled after 0s
- 老师打开"作品"直接看,显示作者和得分;"同学作品"改名"作品" - 随机评分只抽学生自己交过的题,不再泄露没做过的题的答案 - GET /submission/:id 学生打分前不返回同学的作者和得分(作品展除外) - 打分要先能看这份提交 - AI 生成时随机评分弹框遮住页面,写完时正在看的那份留着,换成"看结果" Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
0b3780e0cd
commit
e4fb723544
7 files changed
+135
-155
No files matched your search
@@ -1,6 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<n-drawer v-model:show="show" :width="600" placement="right">
|
<n-drawer v-model:show="show" :width="600" placement="right">
|
||||||
<n-drawer-content title="同学作品" :native-scrollbar="false">
|
<n-drawer-content title="作品" :native-scrollbar="false">
|
||||||
<n-spin :show="loading">
|
<n-spin :show="loading">
|
||||||
<n-result
|
<n-result
|
||||||
v-if="data?.locked"
|
v-if="data?.locked"
|
||||||
@@ -12,7 +12,11 @@
|
|||||||
<template v-else-if="data">
|
<template v-else-if="data">
|
||||||
<n-flex justify="space-between" align="center" class="bar">
|
<n-flex justify="space-between" align="center" class="bar">
|
||||||
<span class="hint">
|
<span class="hint">
|
||||||
同学们在这道题上最新的作品。打分之前不显示是谁做的,打完分才显示。
|
{{
|
||||||
|
roleStaff
|
||||||
|
? "学生们在这道题上最新的作品。"
|
||||||
|
: "同学们在这道题上最新的作品。打分之前不显示是谁做的,打完分才显示。"
|
||||||
|
}}
|
||||||
</span>
|
</span>
|
||||||
<n-radio-group v-model:value="scope" size="small">
|
<n-radio-group v-model:value="scope" size="small">
|
||||||
<n-radio-button value="class">本班</n-radio-button>
|
<n-radio-button value="class">本班</n-radio-button>
|
||||||
@@ -69,6 +73,7 @@
|
|||||||
import { ref, watch } from "vue"
|
import { ref, watch } from "vue"
|
||||||
import { useMessage } from "naive-ui"
|
import { useMessage } from "naive-ui"
|
||||||
import { Review, Submission } from "../../api"
|
import { Review, Submission } from "../../api"
|
||||||
|
import { roleStaff } from "../../store/user"
|
||||||
import { buildPreviewDocument, PREVIEW_SANDBOX } from "../../utils/previewDocument"
|
import { buildPreviewDocument, PREVIEW_SANDBOX } from "../../utils/previewDocument"
|
||||||
import type { PeerWork, PeerWorks } from "../../utils/type"
|
import type { PeerWork, PeerWorks } from "../../utils/type"
|
||||||
|
|
||||||
|
|||||||
@@ -41,7 +41,7 @@
|
|||||||
quaternary
|
quaternary
|
||||||
@click="showPeers = true"
|
@click="showPeers = true"
|
||||||
>
|
>
|
||||||
同学作品
|
作品
|
||||||
</n-button>
|
</n-button>
|
||||||
<n-button v-if="roleSuper" size="small" quaternary @click="edit">
|
<n-button v-if="roleSuper" size="small" quaternary @click="edit">
|
||||||
<template #icon><Icon icon="lucide:edit" /></template>
|
<template #icon><Icon icon="lucide:edit" /></template>
|
||||||
|
|||||||
@@ -1,68 +1,60 @@
|
|||||||
<template>
|
<template>
|
||||||
<div v-if="current || revealed" class="rating-card">
|
<n-modal
|
||||||
<div class="head">
|
:show="!!(current || revealed)"
|
||||||
<span>等 AI 的时候,给同学的作品打个分</span>
|
preset="card"
|
||||||
<n-button v-if="current" text size="tiny" @click="next(true)">跳过</n-button>
|
:title="done ? 'AI 写好了' : '等 AI 写代码的时候,给同学的作品打个分'"
|
||||||
</div>
|
:closable="false"
|
||||||
|
:mask-closable="false"
|
||||||
|
:close-on-esc="false"
|
||||||
|
:auto-focus="false"
|
||||||
|
style="width: 90vw; max-width: 1000px"
|
||||||
|
>
|
||||||
|
<template #header-extra>
|
||||||
|
<n-button v-if="done" type="primary" size="small" @click="close"
|
||||||
|
>看结果</n-button
|
||||||
|
>
|
||||||
|
<n-button v-else-if="current" text size="small" @click="next(true)"
|
||||||
|
>跳过这份</n-button
|
||||||
|
>
|
||||||
|
</template>
|
||||||
<div v-if="revealed" class="revealed">
|
<div v-if="revealed" class="revealed">
|
||||||
刚才那份是 <b>{{ revealed }}</b> 的作品,谢谢你的评分
|
刚才那份是 <b>{{ revealed }}</b> 的作品,谢谢你的评分
|
||||||
</div>
|
</div>
|
||||||
<div v-else-if="current" class="body">
|
<template v-else-if="current">
|
||||||
<button class="thumb" title="点击放大看" @click="showLarge = true">
|
<div class="task">{{ current.task_title }}</div>
|
||||||
|
<div class="large">
|
||||||
<iframe
|
<iframe
|
||||||
:key="current.submission_id"
|
:key="current.submission_id"
|
||||||
:sandbox="PREVIEW_SANDBOX"
|
:sandbox="PREVIEW_SANDBOX"
|
||||||
:srcdoc="doc"
|
:srcdoc="doc"
|
||||||
tabindex="-1"
|
|
||||||
loading="lazy"
|
|
||||||
></iframe>
|
></iframe>
|
||||||
</button>
|
|
||||||
<div class="info">
|
|
||||||
<div class="task">{{ current.task_title }}</div>
|
|
||||||
<div class="rates">
|
|
||||||
<n-tooltip v-for="s in scores" :key="s.value">
|
|
||||||
<template #trigger>
|
|
||||||
<button class="rate" :disabled="rating" @click="rate(s.value)">
|
|
||||||
{{ s.value }}
|
|
||||||
</button>
|
|
||||||
</template>
|
|
||||||
{{ s.value }} 分:{{ s.label }}
|
|
||||||
</n-tooltip>
|
|
||||||
</div>
|
|
||||||
<div class="legend">1 没完成 · 3 基本完成 · 5 完成且有自己的想法</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<div class="rates">
|
||||||
</div>
|
<button
|
||||||
<n-modal
|
v-for="s in scores"
|
||||||
v-model:show="showLarge"
|
:key="s.value"
|
||||||
preset="card"
|
class="rate"
|
||||||
:title="current ? `同学的作品 · ${current.task_title}` : '同学的作品'"
|
:disabled="rating"
|
||||||
style="width: 90vw; max-width: 1000px"
|
@click="rate(s.value)"
|
||||||
>
|
>
|
||||||
<div class="large">
|
<b>{{ s.value }}</b> {{ s.label }}
|
||||||
<iframe :key="current?.submission_id" :sandbox="PREVIEW_SANDBOX" :srcdoc="doc"></iframe>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="large-rates">
|
</template>
|
||||||
<button
|
|
||||||
v-for="s in scores"
|
|
||||||
:key="s.value"
|
|
||||||
class="rate wide"
|
|
||||||
:disabled="rating"
|
|
||||||
@click="rate(s.value)"
|
|
||||||
>
|
|
||||||
<b>{{ s.value }}</b> {{ s.label }}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</n-modal>
|
</n-modal>
|
||||||
</template>
|
</template>
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { computed, ref, watch } from "vue"
|
import { computed, ref, watch } from "vue"
|
||||||
import { useMessage } from "naive-ui"
|
import { useMessage } from "naive-ui"
|
||||||
import { Submission } from "../../api"
|
import { Submission } from "../../api"
|
||||||
import { buildPreviewDocument, PREVIEW_SANDBOX } from "../../utils/previewDocument"
|
import {
|
||||||
|
buildPreviewDocument,
|
||||||
|
PREVIEW_SANDBOX,
|
||||||
|
} from "../../utils/previewDocument"
|
||||||
import type { RandomRatingItem } from "../../utils/type"
|
import type { RandomRatingItem } from "../../utils/type"
|
||||||
|
|
||||||
// AI 生成期间在对话里显示一份同学的作品让学生打分。打分前不显示作者,减少给熟人打高分
|
// AI 生成期间弹框遮住页面,让学生给一份同学的作品打分(学生只关心结果,不用盯着代码一行行出来)。
|
||||||
|
// 打分前不显示作者,减少给熟人打高分。AI 写完时正在看的这份不收走,换成“看结果”按钮
|
||||||
const props = defineProps<{ active: boolean }>()
|
const props = defineProps<{ active: boolean }>()
|
||||||
const message = useMessage()
|
const message = useMessage()
|
||||||
|
|
||||||
@@ -77,7 +69,7 @@ const scores = [
|
|||||||
const current = ref<RandomRatingItem | null>(null)
|
const current = ref<RandomRatingItem | null>(null)
|
||||||
const revealed = ref("")
|
const revealed = ref("")
|
||||||
const rating = ref(false)
|
const rating = ref(false)
|
||||||
const showLarge = ref(false)
|
const done = ref(false)
|
||||||
let seq = 0
|
let seq = 0
|
||||||
|
|
||||||
const doc = computed(() =>
|
const doc = computed(() =>
|
||||||
@@ -103,6 +95,13 @@ async function next(skip = false) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function close() {
|
||||||
|
seq++
|
||||||
|
current.value = null
|
||||||
|
revealed.value = ""
|
||||||
|
done.value = false
|
||||||
|
}
|
||||||
|
|
||||||
async function rate(score: number) {
|
async function rate(score: number) {
|
||||||
if (!current.value) return
|
if (!current.value) return
|
||||||
rating.value = true
|
rating.value = true
|
||||||
@@ -110,10 +109,12 @@ async function rate(score: number) {
|
|||||||
const res = await Submission.updateScore(current.value.submission_id, score)
|
const res = await Submission.updateScore(current.value.submission_id, score)
|
||||||
revealed.value = res.username ?? "同学"
|
revealed.value = res.username ?? "同学"
|
||||||
current.value = null
|
current.value = null
|
||||||
showLarge.value = false
|
const s = seq
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
|
if (s !== seq) return
|
||||||
revealed.value = ""
|
revealed.value = ""
|
||||||
if (props.active) next()
|
if (props.active) next()
|
||||||
|
else done.value = false
|
||||||
}, 1800)
|
}, 1800)
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
message.error(err.response?.data?.detail ?? "打分失败,请重试")
|
message.error(err.response?.data?.detail ?? "打分失败,请重试")
|
||||||
@@ -125,85 +126,60 @@ async function rate(score: number) {
|
|||||||
watch(
|
watch(
|
||||||
() => props.active,
|
() => props.active,
|
||||||
(active) => {
|
(active) => {
|
||||||
if (active) next()
|
if (active) {
|
||||||
else {
|
done.value = false
|
||||||
|
next()
|
||||||
|
} else if (current.value) {
|
||||||
seq++
|
seq++
|
||||||
current.value = null
|
done.value = true
|
||||||
revealed.value = ""
|
} else {
|
||||||
showLarge.value = false
|
close()
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{ immediate: true },
|
{ immediate: true },
|
||||||
)
|
)
|
||||||
</script>
|
</script>
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.rating-card {
|
.task {
|
||||||
margin: 4px 0 16px;
|
font-weight: 600;
|
||||||
padding: 10px 12px;
|
|
||||||
border: 1px solid var(--wp-line);
|
|
||||||
border-radius: 8px;
|
|
||||||
background: var(--wp-surface);
|
|
||||||
}
|
|
||||||
|
|
||||||
.head {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
font-size: 12px;
|
|
||||||
color: var(--wp-muted);
|
|
||||||
margin-bottom: 8px;
|
margin-bottom: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.body {
|
.large {
|
||||||
display: grid;
|
height: 60vh;
|
||||||
grid-template-columns: 132px 1fr;
|
|
||||||
gap: 12px;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.thumb {
|
|
||||||
position: relative;
|
|
||||||
width: 132px;
|
|
||||||
height: 84px;
|
|
||||||
padding: 0;
|
|
||||||
border: 1px solid var(--wp-line);
|
border: 1px solid var(--wp-line);
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
cursor: zoom-in;
|
|
||||||
background: var(--wp-surface);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 按 4 倍大小渲染再缩小,看起来像网页缩略图 */
|
.large iframe {
|
||||||
.thumb iframe {
|
width: 100%;
|
||||||
width: 528px;
|
height: 100%;
|
||||||
height: 336px;
|
|
||||||
border: 0;
|
border: 0;
|
||||||
transform: scale(0.25);
|
|
||||||
transform-origin: 0 0;
|
|
||||||
pointer-events: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.task {
|
|
||||||
font-weight: 600;
|
|
||||||
margin-bottom: 6px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.rates {
|
.rates {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 6px;
|
gap: 8px;
|
||||||
|
margin-top: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.rate {
|
.rate {
|
||||||
min-width: 32px;
|
flex: 1;
|
||||||
padding: 4px 0;
|
padding: 8px;
|
||||||
border: 1px solid var(--wp-line);
|
border: 1px solid var(--wp-line);
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
background: var(--wp-surface);
|
background: var(--wp-surface);
|
||||||
color: var(--wp-ink);
|
color: var(--wp-ink);
|
||||||
font: 600 13px var(--wp-font-mono);
|
font: 13px var(--wp-font-sans);
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.rate b {
|
||||||
|
font-family: var(--wp-font-mono);
|
||||||
|
margin-right: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
.rate:hover:not(:disabled) {
|
.rate:hover:not(:disabled) {
|
||||||
border-color: var(--wp-brand);
|
border-color: var(--wp-brand);
|
||||||
background: var(--wp-brand-soft);
|
background: var(--wp-brand-soft);
|
||||||
@@ -215,44 +191,10 @@ watch(
|
|||||||
outline-offset: 1px;
|
outline-offset: 1px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.legend {
|
|
||||||
margin-top: 6px;
|
|
||||||
font-size: 11px;
|
|
||||||
color: var(--wp-faint);
|
|
||||||
}
|
|
||||||
|
|
||||||
.revealed {
|
.revealed {
|
||||||
font-size: 13px;
|
padding: 48px 0;
|
||||||
|
text-align: center;
|
||||||
|
font-size: 15px;
|
||||||
color: var(--wp-muted);
|
color: var(--wp-muted);
|
||||||
}
|
}
|
||||||
|
|
||||||
.large {
|
|
||||||
height: 65vh;
|
|
||||||
border: 1px solid var(--wp-line);
|
|
||||||
border-radius: 6px;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
.large iframe {
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
border: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.large-rates {
|
|
||||||
display: flex;
|
|
||||||
gap: 8px;
|
|
||||||
margin-top: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.rate.wide {
|
|
||||||
flex: 1;
|
|
||||||
padding: 8px;
|
|
||||||
font: 13px var(--wp-font-sans);
|
|
||||||
}
|
|
||||||
|
|
||||||
.rate.wide b {
|
|
||||||
font-family: var(--wp-font-mono);
|
|
||||||
margin-right: 4px;
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
@@ -15,7 +15,7 @@
|
|||||||
</n-tag>
|
</n-tag>
|
||||||
</template>
|
</template>
|
||||||
<template #tools>
|
<template #tools>
|
||||||
<n-button quaternary size="small" @click="showPeers = true">同学作品</n-button>
|
<n-button quaternary size="small" @click="showPeers = true">作品</n-button>
|
||||||
<n-button v-if="assets.length" quaternary size="small" @click="showAssets = true">
|
<n-button v-if="assets.length" quaternary size="small" @click="showAssets = true">
|
||||||
素材
|
素材
|
||||||
</n-button>
|
</n-button>
|
||||||
|
|||||||
@@ -135,16 +135,19 @@ router.get("/class", (c) => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
// 学生看同学在这道题上的作品:自己交过才开放;自己打分之前不显示作者和得分。
|
// 学生看同学在这道题上的作品:自己交过才开放;自己打分之前不显示作者和得分。
|
||||||
// scope=class 只看本班(默认,一个班几十份),scope=all 看全校所有班
|
// 老师不受这两条限制。scope=class 只看本班(默认,一个班几十份),scope=all 看全校所有班
|
||||||
router.get("/peers/:task_id", (c) => {
|
router.get("/peers/:task_id", (c) => {
|
||||||
const user = requireUser(c)
|
const user = requireUser(c)
|
||||||
const taskId = intParam(c, "task_id")
|
const taskId = intParam(c, "task_id")
|
||||||
const { scope } = query(c, z.object({ scope: z.enum(["class", "all"]).default("class") }))
|
const { scope } = query(c, z.object({ scope: z.enum(["class", "all"]).default("class") }))
|
||||||
orNotFound(get(`SELECT 1 FROM task_task WHERE id = $id`, { id: taskId }), "任务不存在")
|
orNotFound(get(`SELECT 1 FROM task_task WHERE id = $id`, { id: taskId }), "任务不存在")
|
||||||
const submitted = get(
|
const staff = isStaff(user)
|
||||||
`SELECT 1 FROM submission_submission WHERE user_id = $me AND task_id = $task_id LIMIT 1`,
|
const submitted =
|
||||||
{ me: user.id, task_id: taskId },
|
staff ||
|
||||||
)
|
get(`SELECT 1 FROM submission_submission WHERE user_id = $me AND task_id = $task_id LIMIT 1`, {
|
||||||
|
me: user.id,
|
||||||
|
task_id: taskId,
|
||||||
|
})
|
||||||
if (!submitted) return c.json({ locked: true, items: [] })
|
if (!submitted) return c.json({ locked: true, items: [] })
|
||||||
const byClass = scope === "class" && !!user.classname
|
const byClass = scope === "class" && !!user.classname
|
||||||
const members = `SELECT id FROM account_user
|
const members = `SELECT id FROM account_user
|
||||||
@@ -162,7 +165,7 @@ router.get("/peers/:task_id", (c) => {
|
|||||||
const items = [...rows.values()]
|
const items = [...rows.values()]
|
||||||
.sort((a, b) => (a.created < b.created ? 1 : -1))
|
.sort((a, b) => (a.created < b.created ? 1 : -1))
|
||||||
.map((r) => {
|
.map((r) => {
|
||||||
const rated = (r.my_score || 0) > 0
|
const rated = staff || (r.my_score || 0) > 0
|
||||||
const p = people.get(r.user_id)
|
const p = people.get(r.user_id)
|
||||||
return {
|
return {
|
||||||
id: uuidOut(r.id),
|
id: uuidOut(r.id),
|
||||||
|
|||||||
@@ -37,10 +37,12 @@ const router = new Hono<Env>()
|
|||||||
router.route("/showcase", showcase)
|
router.route("/showcase", showcase)
|
||||||
router.route("/review", review)
|
router.route("/review", review)
|
||||||
|
|
||||||
|
const inShowcase = (id: string) => !!get(`SELECT 1 FROM submission_submissionaward WHERE submission_id = $id`, { id })
|
||||||
|
|
||||||
// 学生能看的提交:自己的、进了作品展的、自己也交过的那道题上同学(任何班)的。老师都能看
|
// 学生能看的提交:自己的、进了作品展的、自己也交过的那道题上同学(任何班)的。老师都能看
|
||||||
function canView(user: User, sub: { id: string; user_id: number; task_id: number }) {
|
function canView(user: User, sub: { id: string; user_id: number; task_id: number }) {
|
||||||
if (isStaff(user) || sub.user_id === user.id) return true
|
if (isStaff(user) || sub.user_id === user.id) return true
|
||||||
if (get(`SELECT 1 FROM submission_submissionaward WHERE submission_id = $id`, { id: sub.id })) return true
|
if (inShowcase(sub.id)) return true
|
||||||
return !!get(
|
return !!get(
|
||||||
`SELECT 1 FROM submission_submission s WHERE s.user_id = $me AND s.task_id = $task_id LIMIT 1`,
|
`SELECT 1 FROM submission_submission s WHERE s.user_id = $me AND s.task_id = $task_id LIMIT 1`,
|
||||||
{ me: user.id, task_id: sub.task_id },
|
{ me: user.id, task_id: sub.task_id },
|
||||||
@@ -336,7 +338,9 @@ router.get("/random-for-rating", (c) => {
|
|||||||
}
|
}
|
||||||
if (user.role === Role.NORMAL && todayRatingCount(user.id) >= 30) return c.json(null)
|
if (user.role === Role.NORMAL && todayRatingCount(user.id) >= 30) return c.json(null)
|
||||||
|
|
||||||
|
// 学生只抽自己也交过的题(和同学作品一样,没交过的题不能看别人的答案)
|
||||||
const cond = `s.user_id != $me
|
const cond = `s.user_id != $me
|
||||||
|
${user.role === Role.NORMAL ? "AND +s.task_id IN (SELECT task_id FROM submission_submission WHERE user_id = $me)" : ""}
|
||||||
AND NOT EXISTS (SELECT 1 FROM submission_rating r WHERE r.submission_id = s.id AND r.user_id = $me)
|
AND NOT EXISTS (SELECT 1 FROM submission_rating r WHERE r.submission_id = s.id AND r.user_id = $me)
|
||||||
${excludeId ? "AND s.id != $exclude" : ""}`
|
${excludeId ? "AND s.id != $exclude" : ""}`
|
||||||
const params = { me: user.id, ...(excludeId ? { exclude: excludeId } : {}) }
|
const params = { me: user.id, ...(excludeId ? { exclude: excludeId } : {}) }
|
||||||
@@ -545,7 +549,12 @@ router.get("/:id", (c) => {
|
|||||||
const id = uuidParam(c, "id")
|
const id = uuidParam(c, "id")
|
||||||
requireView(user, id)
|
requireView(user, id)
|
||||||
const s = orNotFound(get(`${SUBMISSION_SELECT} ${SUBMISSION_FROM} WHERE s.id = $id`, { me: user.id, id }))
|
const s = orNotFound(get(`${SUBMISSION_SELECT} ${SUBMISSION_FROM} WHERE s.id = $id`, { me: user.id, id }))
|
||||||
return c.json(submissionOut(s))
|
const out = submissionOut(s)
|
||||||
|
// 和同学作品一样:学生给同学打分之前不告诉作者和得分(作品展里的本来就署名)
|
||||||
|
if (!isStaff(user) && s.user_id !== user.id && !out.my_score && !inShowcase(id)) {
|
||||||
|
return c.json({ ...out, userid: null, username: null, score: null })
|
||||||
|
}
|
||||||
|
return c.json(out)
|
||||||
})
|
})
|
||||||
|
|
||||||
// 增加浏览次数(仅在全屏预览时调用)
|
// 增加浏览次数(仅在全屏预览时调用)
|
||||||
@@ -562,6 +571,7 @@ router.put("/:id/score", async (c) => {
|
|||||||
const id = uuidParam(c, "id")
|
const id = uuidParam(c, "id")
|
||||||
const { score } = await body(c, z.object({ score: z.number().int().max(5) }))
|
const { score } = await body(c, z.object({ score: z.number().int().max(5) }))
|
||||||
if (score <= 0) throw new HttpError(400, "分数不能为零")
|
if (score <= 0) throw new HttpError(400, "分数不能为零")
|
||||||
|
requireView(user, id)
|
||||||
const sub = orNotFound(
|
const sub = orNotFound(
|
||||||
get<{ user_id: number; username: string }>(
|
get<{ user_id: number; username: string }>(
|
||||||
`SELECT s.user_id, u.username FROM submission_submission s
|
`SELECT s.user_id, u.username FROM submission_submission s
|
||||||
|
|||||||
@@ -171,6 +171,8 @@ describe("评分", () => {
|
|||||||
expect(row.score).toBeCloseTo(4.25)
|
expect(row.score).toBeCloseTo(4.25)
|
||||||
expect(row.zone).toBe("pending")
|
expect(row.zone).toBe("pending")
|
||||||
|
|
||||||
|
// 学生要自己也交过这道题才能打分
|
||||||
|
for (let i = 0; i < 3; i++) makeSubmission(get(`SELECT id FROM account_user WHERE username = 's${i}'`).id, task)
|
||||||
for (let i = 0; i < 3; i++) {
|
for (let i = 0; i < 3; i++) {
|
||||||
await (await client(`s${i}`))("PUT", `/api/submission/${uuid(sub)}/score`, { score: 5 })
|
await (await client(`s${i}`))("PUT", `/api/submission/${uuid(sub)}/score`, { score: 5 })
|
||||||
}
|
}
|
||||||
@@ -256,8 +258,9 @@ describe("提交与删除", () => {
|
|||||||
|
|
||||||
test("删除一轮对话:连带提问和关联提交;被评过分的不能删", async () => {
|
test("删除一轮对话:连带提问和关联提交;被评过分的不能删", async () => {
|
||||||
makeUser("stu")
|
makeUser("stu")
|
||||||
makeUser("other")
|
const other = makeUser("other")
|
||||||
const task = makeTask("T")
|
const task = makeTask("T")
|
||||||
|
const otherSub = makeSubmission(other, task)
|
||||||
const stu = await client("stu")
|
const stu = await client("stu")
|
||||||
const first = await stu("POST", "/api/submission/", { task_id: task, html: "<p>1", prompt: "第一版" })
|
const first = await stu("POST", "/api/submission/", { task_id: task, html: "<p>1", prompt: "第一版" })
|
||||||
const second = await stu("POST", "/api/submission/", { task_id: task, html: "<p>2", prompt: "第二版" })
|
const second = await stu("POST", "/api/submission/", { task_id: task, html: "<p>2", prompt: "第二版" })
|
||||||
@@ -270,14 +273,16 @@ describe("提交与删除", () => {
|
|||||||
const res = await stu("DELETE", `/api/prompt/messages/${asstOf(first.json.id)}/pair`)
|
const res = await stu("DELETE", `/api/prompt/messages/${asstOf(first.json.id)}/pair`)
|
||||||
expect(res.json).toEqual({ deleted: true, submission_deleted: true })
|
expect(res.json).toEqual({ deleted: true, submission_deleted: true })
|
||||||
expect(all(`SELECT content FROM prompt_message WHERE role = 'user'`)).toEqual([{ content: "第二版" }])
|
expect(all(`SELECT content FROM prompt_message WHERE role = 'user'`)).toEqual([{ content: "第二版" }])
|
||||||
expect(all(`SELECT COUNT(*) AS n FROM submission_submission`)).toEqual([{ n: 1 }])
|
expect(all(`SELECT COUNT(*) AS n FROM submission_submission WHERE id != $id`, { id: otherSub })).toEqual([{ n: 1 }])
|
||||||
})
|
})
|
||||||
|
|
||||||
test("被评分的提交只有超管能删", async () => {
|
test("被评分的提交只有超管能删", async () => {
|
||||||
const author = makeUser("author")
|
const author = makeUser("author")
|
||||||
makeUser("other")
|
const other = makeUser("other")
|
||||||
makeUser("boss", "super")
|
makeUser("boss", "super")
|
||||||
const sub = makeSubmission(author, makeTask("T"))
|
const task = makeTask("T")
|
||||||
|
const sub = makeSubmission(author, task)
|
||||||
|
makeSubmission(other, task)
|
||||||
await (await client("other"))("PUT", `/api/submission/${uuid(sub)}/score`, { score: 4 })
|
await (await client("other"))("PUT", `/api/submission/${uuid(sub)}/score`, { score: 4 })
|
||||||
expect((await (await client("author"))("DELETE", `/api/submission/${uuid(sub)}`)).status).toBe(400)
|
expect((await (await client("author"))("DELETE", `/api/submission/${uuid(sub)}`)).status).toBe(400)
|
||||||
expect((await (await client("boss"))("DELETE", `/api/submission/${uuid(sub)}`)).status).toBe(200)
|
expect((await (await client("boss"))("DELETE", `/api/submission/${uuid(sub)}`)).status).toBe(200)
|
||||||
@@ -511,7 +516,10 @@ describe("同学作品与批改", () => {
|
|||||||
|
|
||||||
expect((await stu("GET", `/api/submission/${uuid(otherSub)}`)).status).toBe(403)
|
expect((await stu("GET", `/api/submission/${uuid(otherSub)}`)).status).toBe(403)
|
||||||
makeSubmission(me, task)
|
makeSubmission(me, task)
|
||||||
expect((await stu("GET", `/api/submission/${uuid(mateSub)}`)).status).toBe(200)
|
const peer = await stu("GET", `/api/submission/${uuid(mateSub)}`)
|
||||||
|
expect(peer.status).toBe(200)
|
||||||
|
// 打分前不显示作者和得分
|
||||||
|
expect(peer.json).toMatchObject({ userid: null, username: null, score: null })
|
||||||
// 交过以后外班的也能看
|
// 交过以后外班的也能看
|
||||||
expect((await stu("GET", `/api/submission/${uuid(otherSub)}`)).status).toBe(200)
|
expect((await stu("GET", `/api/submission/${uuid(otherSub)}`)).status).toBe(200)
|
||||||
// 没交过的题仍然看不到
|
// 没交过的题仍然看不到
|
||||||
@@ -533,6 +541,11 @@ describe("同学作品与批改", () => {
|
|||||||
const mateSub = makeSubmission(mate, task)
|
const mateSub = makeSubmission(mate, task)
|
||||||
const stu = await client("me")
|
const stu = await client("me")
|
||||||
|
|
||||||
|
// 没交过的同学既不能打分,也抽不到这道题
|
||||||
|
const other = await client("other")
|
||||||
|
expect((await other("PUT", `/api/submission/${uuid(mateSub)}/score`, { score: 3 })).status).toBe(403)
|
||||||
|
expect((await other("GET", "/api/submission/random-for-rating")).json).toBeNull()
|
||||||
|
|
||||||
let peers = await stu("GET", `/api/submission/review/peers/${task}`)
|
let peers = await stu("GET", `/api/submission/review/peers/${task}`)
|
||||||
expect(peers.json.items).toHaveLength(1)
|
expect(peers.json.items).toHaveLength(1)
|
||||||
expect(peers.json.items[0]).toMatchObject({ id: uuid(mateSub), username: null, score: null, my_score: 0 })
|
expect(peers.json.items[0]).toMatchObject({ id: uuid(mateSub), username: null, score: null, my_score: 0 })
|
||||||
@@ -541,17 +554,24 @@ describe("同学作品与批改", () => {
|
|||||||
expect(rated.json.username).toBe("mate")
|
expect(rated.json.username).toBe("mate")
|
||||||
peers = await stu("GET", `/api/submission/review/peers/${task}`)
|
peers = await stu("GET", `/api/submission/review/peers/${task}`)
|
||||||
expect(peers.json.items[0]).toMatchObject({ username: "mate", my_score: 4 })
|
expect(peers.json.items[0]).toMatchObject({ username: "mate", my_score: 4 })
|
||||||
|
expect((await stu("GET", `/api/submission/${uuid(mateSub)}`)).json).toMatchObject({ username: "mate" })
|
||||||
const random = await (await client("other"))("GET", "/api/submission/random-for-rating")
|
|
||||||
expect(random.json.username).toBeUndefined()
|
|
||||||
|
|
||||||
// 默认只看本班;scope=all 能看到外班,标着班级,同样匿名
|
// 默认只看本班;scope=all 能看到外班,标着班级,同样匿名
|
||||||
const otherSub = makeSubmission(get(`SELECT id FROM account_user WHERE username = 'other'`).id, task)
|
const otherSub = makeSubmission(get(`SELECT id FROM account_user WHERE username = 'other'`).id, task)
|
||||||
|
const random = await other("GET", "/api/submission/random-for-rating")
|
||||||
|
expect(random.json.submission_id).toBeDefined()
|
||||||
|
expect(random.json.username).toBeUndefined()
|
||||||
const all = await stu("GET", `/api/submission/review/peers/${task}?scope=all`)
|
const all = await stu("GET", `/api/submission/review/peers/${task}?scope=all`)
|
||||||
expect(all.json.items.map((w: any) => [w.classname, w.username])).toContainEqual(["c2", null])
|
expect(all.json.items.map((w: any) => [w.classname, w.username])).toContainEqual(["c2", null])
|
||||||
expect(all.json.items.map((w: any) => w.id)).toContain(uuid(otherSub))
|
expect(all.json.items.map((w: any) => w.id)).toContain(uuid(otherSub))
|
||||||
const mine = await stu("GET", `/api/submission/review/peers/${task}`)
|
const mine = await stu("GET", `/api/submission/review/peers/${task}`)
|
||||||
expect(mine.json.items.map((w: any) => w.classname)).toEqual(["c1"])
|
expect(mine.json.items.map((w: any) => w.classname)).toEqual(["c1"])
|
||||||
|
|
||||||
|
// 老师没交过也能看,直接显示作者
|
||||||
|
makeUser("t", "admin")
|
||||||
|
const teacher = await (await client("t"))("GET", `/api/submission/review/peers/${task}`)
|
||||||
|
expect(teacher.json.locked).toBe(false)
|
||||||
|
expect(teacher.json.items.map((w: any) => w.username).sort()).toEqual(["mate", "me", "other"])
|
||||||
})
|
})
|
||||||
|
|
||||||
test("老师按班级 × 任务看作品墙;课上展示用 flag", async () => {
|
test("老师按班级 × 任务看作品墙;课上展示用 flag", async () => {
|
||||||
|
|||||||
Reference in new issue
Block a user