diff --git a/src/features/votes/services/vote_statistics.test.ts b/src/features/votes/services/vote_statistics.test.ts index ea3113f30..89d32000d 100644 --- a/src/features/votes/services/vote_statistics.test.ts +++ b/src/features/votes/services/vote_statistics.test.ts @@ -4,6 +4,7 @@ import { TaskGrade } from '@prisma/client'; import { getVoteGradeStatistics, + getVoteGradeStatisticsForTaskIds, getAllTasksWithVoteInfo, getVoteCountersByTaskId, getVoteStatsByTaskId, @@ -290,3 +291,52 @@ describe('getAllVoteCounters', () => { expect(prisma.votedGradeCounter.findMany).toHaveBeenCalledWith(); }); }); + +describe('getVoteGradeStatisticsForTaskIds', () => { + test('returns an empty Map without querying the DB when taskIds is empty', async () => { + const result = await getVoteGradeStatisticsForTaskIds([]); + + expect(result.size).toBe(0); + expect(prisma.votedGradeStatistics.findMany).not.toHaveBeenCalled(); + }); + + test('queries with a WHERE IN filter for the given taskIds', async () => { + mockVotedGradeStatisticsFindMany([]); + + await getVoteGradeStatisticsForTaskIds(['abc001_a', 'abc002_a']); + + expect(prisma.votedGradeStatistics.findMany).toHaveBeenCalledWith({ + where: { taskId: { in: ['abc001_a', 'abc002_a'] } }, + }); + }); + + test('returns an empty Map when no matching statistics exist', async () => { + mockVotedGradeStatisticsFindMany([]); + + const result = await getVoteGradeStatisticsForTaskIds(['abc001_a']); + + expect(result.size).toBe(0); + }); + + test('maps each taskId to its statistics record', async () => { + const stat = makeStatisticsRecord({ taskId: 'abc001_a', grade: TaskGrade.Q5 }); + mockVotedGradeStatisticsFindMany([stat]); + + const result = await getVoteGradeStatisticsForTaskIds(['abc001_a']); + + expect(result.get('abc001_a')?.grade).toBe(TaskGrade.Q5); + }); + + test('returns only statistics for the specified taskIds', async () => { + const records = [ + makeStatisticsRecord({ taskId: 'abc001_a', grade: TaskGrade.Q5 }), + makeStatisticsRecord({ id: 'stats-abc002_a', taskId: 'abc002_a', grade: TaskGrade.D1 }), + ]; + mockVotedGradeStatisticsFindMany(records); + + const result = await getVoteGradeStatisticsForTaskIds(['abc001_a', 'abc002_a']); + + expect(result.size).toBe(2); + expect(result.get('abc002_a')?.grade).toBe(TaskGrade.D1); + }); +}); diff --git a/src/features/votes/services/vote_statistics.ts b/src/features/votes/services/vote_statistics.ts index 9149fc664..e84483c50 100644 --- a/src/features/votes/services/vote_statistics.ts +++ b/src/features/votes/services/vote_statistics.ts @@ -24,6 +24,19 @@ export async function getVoteGradeStatistics(): Promise> { + if (taskIds.length === 0) { + return new Map(); + } + + const stats = await prisma.votedGradeStatistics.findMany({ + where: { taskId: { in: taskIds } }, + }); + return new Map(stats.map((s) => [s.taskId, s])); +} + export async function getAllTasksWithVoteInfo(): Promise { const [allTasks, stats, counters] = await Promise.all([ prisma.task.findMany({ orderBy: { task_id: 'desc' } }), diff --git a/src/routes/workbooks/[slug]/+page.server.ts b/src/routes/workbooks/[slug]/+page.server.ts index 2d5ef1f40..599213f9b 100644 --- a/src/routes/workbooks/[slug]/+page.server.ts +++ b/src/routes/workbooks/[slug]/+page.server.ts @@ -1,11 +1,11 @@ import { error, type Actions } from '@sveltejs/kit'; import { Roles } from '$lib/types/user'; -import type { TaskResult } from '$lib/types/task'; import * as taskResultsCrud from '$lib/services/task_results'; import { getWorkbookWithAuthor } from '$features/workbooks/services/workbooks'; import * as action from '$lib/actions/update_task_result'; +import { getVoteGradeStatisticsForTaskIds } from '$features/votes/services/vote_statistics'; import { isAdmin, canRead } from '$lib/utils/authorship'; import { getLoggedInUser } from '$features/auth/services/session'; @@ -36,16 +36,18 @@ export async function load({ locals, params, url }) { error(FORBIDDEN, `問題集id: ${slug} にアクセスする権限がありません。`); } - const taskResults: Map = await taskResultsCrud.getTaskResultsByTaskId( - workBook.workBookTasks, - loggedInUser?.id as string, - ); + const taskIds = workBook.workBookTasks.map((t) => t.taskId); + const [taskResults, voteStatisticsMap] = await Promise.all([ + taskResultsCrud.getTaskResultsByTaskId(workBook.workBookTasks, loggedInUser?.id as string), + getVoteGradeStatisticsForTaskIds(taskIds), + ]); return { isLoggedIn: loggedInUser !== null, loggedInAsAdmin: loggedInAsAdmin, ...workbookWithAuthor, taskResults: taskResults, + voteStatisticsMap: voteStatisticsMap, }; } diff --git a/src/routes/workbooks/[slug]/+page.svelte b/src/routes/workbooks/[slug]/+page.svelte index 87152b3d2..3cfbe519e 100644 --- a/src/routes/workbooks/[slug]/+page.svelte +++ b/src/routes/workbooks/[slug]/+page.svelte @@ -18,6 +18,7 @@ import ExternalLinkWrapper from '$lib/components/ExternalLinkWrapper.svelte'; import PublicationStatusLabel from '$features/workbooks/components/shared/PublicationStatusLabel.svelte'; import CommentAndHint from '$features/workbooks/components/detail/CommentAndHint.svelte'; + import RelativeEvaluationBadge from '$features/votes/components/RelativeEvaluationBadge.svelte'; import { getBackgroundColorFrom } from '$lib/services/submission_status'; @@ -33,6 +34,7 @@ let workBook = data.workBook; let workBookTasks: WorkBookTaskBase[] = $state([]); let taskResults: Map = $derived(data.taskResults); + let voteStatisticsMap = $derived(data.voteStatisticsMap); let isLoggedIn = data.isLoggedIn; @@ -157,6 +159,8 @@ {#each workBookTasks as workBookTask (workBookTask.taskId)} + {@const taskGrade = getTaskGrade(workBookTask.taskId)} + {@const statsEntry = voteStatisticsMap?.get(workBookTask.taskId)}
- +
+ + {#if taskGrade !== TaskGrade.PENDING && statsEntry} + + {/if} +