Skip to content

Commit 2561499

Browse files
authored
PENDINGの投票制限を緩和 (#3681)
1 parent 6a4a881 commit 2561499

5 files changed

Lines changed: 56 additions & 13 deletions

File tree

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,11 @@
11
/**
2-
* Minimum number of votes required to compute and store the median grade.
3-
* Below this threshold the sample size is too small to be meaningful.
2+
* Minimum votes to compute and store the median for a confirmed-grade task.
3+
* Used for the relative evaluation badge (++/+/±0/-/--).
44
*/
55
export const MIN_VOTES_FOR_STATISTICS = 3;
6+
7+
/**
8+
* Minimum votes to compute and store the median for a PENDING task.
9+
* Used for the provisional grade display (暫定グレード).
10+
*/
11+
export const MIN_VOTES_FOR_PROVISIONAL_GRADE = 1;

src/features/votes/services/vote_grade.test.ts

Lines changed: 33 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,9 @@ vi.mock('$lib/server/database', () => ({
1818
votedGradeStatistics: {
1919
upsert: vi.fn(),
2020
},
21+
task: {
22+
findUnique: vi.fn(),
23+
},
2124
$transaction: vi.fn(),
2225
},
2326
}));
@@ -48,13 +51,16 @@ function setupTransaction() {
4851
voteGrade: { findUnique: vi.fn(), upsert: vi.fn() },
4952
votedGradeCounter: { updateMany: vi.fn(), upsert: vi.fn(), findMany: vi.fn() },
5053
votedGradeStatistics: { upsert: vi.fn() },
54+
task: { findUnique: vi.fn() },
5155
};
5256
vi.mocked(prisma.$transaction).mockImplementation(async (callback: unknown) =>
5357
(callback as (tx: typeof mockTx) => Promise<unknown>)(mockTx),
5458
);
5559
return mockTx;
5660
}
5761

62+
type MockTx = ReturnType<typeof setupTransaction>;
63+
5864
// ---------------------------------------------------------------------------
5965
// Tests
6066
// ---------------------------------------------------------------------------
@@ -117,6 +123,7 @@ describe('upsertVoteGradeTables', () => {
117123
{ grade: TaskGrade.Q5, count: 1 },
118124
{ grade: TaskGrade.Q4, count: 0 },
119125
]);
126+
tx.task.findUnique.mockResolvedValue({ grade: TaskGrade.Q3 });
120127

121128
await upsertVoteGradeTables('user-1', 'abc001_a', TaskGrade.Q5);
122129

@@ -134,7 +141,7 @@ describe('upsertVoteGradeTables', () => {
134141
update: { count: { increment: 1 } },
135142
}),
136143
);
137-
// Total = 1 vote (Q5:1 + Q4:0), below MIN_VOTES_FOR_STATISTICS — statistics must not be updated
144+
// Total = 1 vote (Q5:1 + Q4:0), below MIN_VOTES_FOR_STATISTICS (3) for non-PENDING task — statistics must not be updated
138145
expect(tx.votedGradeStatistics.upsert).not.toHaveBeenCalled();
139146
});
140147

@@ -144,6 +151,7 @@ describe('upsertVoteGradeTables', () => {
144151
tx.voteGrade.upsert.mockResolvedValue({});
145152
tx.votedGradeCounter.upsert.mockResolvedValue({});
146153
tx.votedGradeCounter.findMany.mockResolvedValue([{ grade: TaskGrade.Q5, count: 1 }]);
154+
tx.task.findUnique.mockResolvedValue({ grade: TaskGrade.Q3 });
147155

148156
await upsertVoteGradeTables('user-1', 'abc001_a', TaskGrade.Q5);
149157

@@ -159,17 +167,18 @@ describe('upsertVoteGradeTables', () => {
159167
update: { count: { increment: 1 } },
160168
}),
161169
);
162-
// Total = 1 vote (Q5:1), below MIN_VOTES_FOR_STATISTICS — statistics must not be updated
170+
// Total = 1 vote (Q5:1), below MIN_VOTES_FOR_STATISTICS (3) for non-PENDING task — statistics must not be updated
163171
expect(tx.votedGradeStatistics.upsert).not.toHaveBeenCalled();
164172
});
165173

166-
test('upserts VotedGradeStatistics when total votes reaches 3', async () => {
174+
test('upserts VotedGradeStatistics when total votes reaches 3 for non-PENDING task', async () => {
167175
const tx = setupTransaction();
168176
tx.voteGrade.findUnique.mockResolvedValue(null);
169177
tx.voteGrade.upsert.mockResolvedValue({});
170178
tx.votedGradeCounter.upsert.mockResolvedValue({});
171179
// 3 votes all on Q5 → median = Q5
172180
tx.votedGradeCounter.findMany.mockResolvedValue([{ grade: TaskGrade.Q5, count: 3 }]);
181+
tx.task.findUnique.mockResolvedValue({ grade: TaskGrade.Q3 });
173182
tx.votedGradeStatistics.upsert.mockResolvedValue({});
174183

175184
await upsertVoteGradeTables('user-1', 'abc001_a', TaskGrade.Q5);
@@ -182,15 +191,35 @@ describe('upsertVoteGradeTables', () => {
182191
);
183192
});
184193

185-
test('does not upsert VotedGradeStatistics when total votes is below 3', async () => {
194+
test('does not upsert VotedGradeStatistics when total votes is below 3 for non-PENDING task', async () => {
186195
const tx = setupTransaction();
187196
tx.voteGrade.findUnique.mockResolvedValue(null);
188197
tx.voteGrade.upsert.mockResolvedValue({});
189198
tx.votedGradeCounter.upsert.mockResolvedValue({});
190199
tx.votedGradeCounter.findMany.mockResolvedValue([{ grade: TaskGrade.Q5, count: 2 }]);
200+
tx.task.findUnique.mockResolvedValue({ grade: TaskGrade.Q3 });
191201

192202
await upsertVoteGradeTables('user-1', 'abc001_a', TaskGrade.Q5);
193203

194204
expect(tx.votedGradeStatistics.upsert).not.toHaveBeenCalled();
195205
});
206+
207+
test('upserts VotedGradeStatistics after 1 vote for PENDING task', async () => {
208+
const tx = setupTransaction();
209+
tx.voteGrade.findUnique.mockResolvedValue(null);
210+
tx.voteGrade.upsert.mockResolvedValue({});
211+
tx.votedGradeCounter.upsert.mockResolvedValue({});
212+
tx.votedGradeCounter.findMany.mockResolvedValue([{ grade: TaskGrade.Q5, count: 1 }]);
213+
tx.task.findUnique.mockResolvedValue({ grade: TaskGrade.PENDING });
214+
tx.votedGradeStatistics.upsert.mockResolvedValue({});
215+
216+
await upsertVoteGradeTables('user-1', 'abc001_a', TaskGrade.Q5);
217+
218+
expect(tx.votedGradeStatistics.upsert).toHaveBeenCalledWith(
219+
expect.objectContaining({
220+
where: { taskId: 'abc001_a' },
221+
update: { grade: TaskGrade.Q5 },
222+
}),
223+
);
224+
});
196225
});

src/features/votes/services/vote_grade.ts

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,10 @@ import { TaskGrade } from '@prisma/client';
33
import { sha256 } from '$lib/utils/hash';
44
import type { VoteGradeResult } from '$features/votes/types/vote_result';
55
import { computeMedianGrade } from '$features/votes/utils/median';
6-
import { MIN_VOTES_FOR_STATISTICS } from '$features/votes/constants/statistics';
6+
import {
7+
MIN_VOTES_FOR_STATISTICS,
8+
MIN_VOTES_FOR_PROVISIONAL_GRADE,
9+
} from '$features/votes/constants/statistics';
710

811
export async function getVoteGrade(userId: string, taskId: string): Promise<VoteGradeResult> {
912
const voteRecord = await prisma.voteGrade.findUnique({
@@ -51,11 +54,14 @@ export async function upsertVoteGradeTables(
5154
});
5255

5356
const total = latestCounters.reduce((sum, counter) => sum + counter.count, 0);
54-
if (total < MIN_VOTES_FOR_STATISTICS) {
57+
const taskRecord = await tx.task.findUnique({ where: { task_id: taskId }, select: { grade: true } });
58+
const minVotes =
59+
taskRecord?.grade === TaskGrade.PENDING ? MIN_VOTES_FOR_PROVISIONAL_GRADE : MIN_VOTES_FOR_STATISTICS;
60+
if (total < minVotes) {
5561
return;
5662
}
5763

58-
await updateVoteStatistics(tx, taskId, latestCounters);
64+
await updateVoteStatistics(tx, taskId, latestCounters, minVotes);
5965
});
6066
return { success: true };
6167
}
@@ -118,8 +124,9 @@ async function updateVoteStatistics(
118124
tx: TxClient,
119125
taskId: string,
120126
counters: GradeCounter[],
127+
minVotes: number,
121128
): Promise<void> {
122-
const medianGrade = computeMedianGrade(counters);
129+
const medianGrade = computeMedianGrade(counters, minVotes);
123130
if (medianGrade === null) {
124131
return;
125132
}

src/routes/votes/+page.svelte

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
import RelativeEvaluationBadge from '$features/votes/components/RelativeEvaluationBadge.svelte';
1919
2020
import { TaskGrade } from '$lib/types/task';
21+
import { MIN_VOTES_FOR_PROVISIONAL_GRADE } from '$features/votes/constants/statistics';
2122
2223
import { getContestNameLabel } from '$lib/utils/contest';
2324
import { getTaskUrl, compareByContestIdAndTaskId } from '$lib/utils/task';
@@ -75,7 +76,7 @@
7576
<FlaskConical class="w-4 h-4" aria-hidden="true" />
7677
</span>
7778
<Tooltip triggeredBy="#flask-{task.task_id}" placement="top">
78-
3票以上集まると中央値が暫定グレードとして一覧表に反映されます
79+
{MIN_VOTES_FOR_PROVISIONAL_GRADE}票以上集まると中央値が暫定グレードとして一覧表に反映されます
7980
</Tooltip>
8081
{/if}
8182

src/routes/votes/[slug]/+page.svelte

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
1818
import VoteDonutChart from '$features/votes/components/VoteDonutChart.svelte';
1919
import { qGrades, dGrades } from '$features/votes/utils/grade_options';
20-
import { MIN_VOTES_FOR_STATISTICS } from '$features/votes/constants/statistics';
20+
import { MIN_VOTES_FOR_PROVISIONAL_GRADE } from '$features/votes/constants/statistics';
2121
2222
import { EDIT_PROFILE_PAGE } from '$lib/constants/navbar-links';
2323
import { buildLoginPath } from '$features/auth/utils/login';
@@ -53,7 +53,7 @@
5353
<FlaskConical class="w-5 h-5" />
5454
</span>
5555
<Tooltip triggeredBy="#flask-icon" placement="bottom">
56-
{MIN_VOTES_FOR_STATISTICS}票以上集まると中央値が暫定グレードとして一覧表に反映されます。
56+
{MIN_VOTES_FOR_PROVISIONAL_GRADE}票以上集まると中央値が暫定グレードとして一覧表に反映されます。
5757
</Tooltip>
5858
{/if}
5959
<div class="relative inline-block">

0 commit comments

Comments
 (0)