-
-
Notifications
You must be signed in to change notification settings - Fork 10
Feature/absolute evaluation voting #3316
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
26 commits
Select commit
Hold shift + click to select a range
a684dbd
add: schema.prismaに投票機能に必要なテーブルを追加
river0525 068e6b9
fix: relationの設定不足&マイグレーション忘れを修正
river0525 8f45a8d
add: 投票用ドロップダウンを出す機能を追加
river0525 0646e35
fix: 未ログイン時にドロップダウンのレイアウトが崩れる問題を修正
river0525 d608c59
add: 選択したグレードをコンソールに出力する機能を追加
river0525 afa66b5
add: 票をデータベースに送信する機能を実装
river0525 0f595cf
add: 投票したグレードにチェックマークを表示する機能を追加
river0525 cc06b0c
add: 票の統計データ化を実装
river0525 9c8e40c
add: ページロード時に投票済み中央値グレードを反映
river0525 71d3d56
fix: グレード付与済み問題への投票を制限
river0525 7a240ed
add: 投票関連ページを追加
river0525 2ee029d
fix: /(admin)/votes と /votes のルート衝突を解消
river0525 a673e76
add: 投票機能のUI改善
river0525 0cf12da
fix: 投票詳細ページの説明文を投票後も表示
river0525 c6ed2ca
fix: 詳細ページへの遷移をドロップダウンの横幅に合う表記に変更
river0525 5c795b5
add: 投票管理ページの改善と投票制限の解除
river0525 8fb1f25
refactor: 投票機能のコード品質改善
river0525 57bee46
test: vote_crud.ts の単体テストを追加
river0525 f672086
refactor: vote_crud.ts を vote_grade / vote_statistics に分割し型・依存を整理
river0525 3374c3a
test: vote_grade のテストにインクリメント確認と統計未更新の assertion を追加
river0525 1179387
fix: CodeRabbit potential_issue の対応 (7件)
river0525 024f9bb
fix: lint修正・e2eテスト追加・バリデーションルール追記
river0525 74b5c7f
fix: .gitignoreとERD図に関する指摘を修正
river0525 c11fe23
fix: address CodeRabbit review findings on PR #3316
river0525 61d6892
fix: constrain GradeLabel outer div to content width to prevent D6 bo…
river0525 8a15fce
fix: address remaining CodeRabbit/Copilot findings on PR #3316
river0525 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -157,4 +157,3 @@ prisma/.fabbrica | |
|
|
||
| # Directory for playwright test results | ||
| test-results | ||
|
|
||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,178 @@ | ||
| import { test, expect, type Page } from '@playwright/test'; | ||
|
|
||
| import { loginAsAdmin, loginAsUser } from './helpers/auth'; | ||
|
|
||
| const TIMEOUT = 60 * 1000; | ||
| const VOTES_LIST_URL = '/votes'; | ||
| const VOTE_MANAGEMENT_URL = '/vote_management'; | ||
|
|
||
| // --------------------------------------------------------------------------- | ||
| // Votes list page (/votes) | ||
| // --------------------------------------------------------------------------- | ||
|
|
||
| test.describe('votes list page (/votes)', () => { | ||
| test('unauthenticated user can view the page without redirect', async ({ page }) => { | ||
| await page.goto(VOTES_LIST_URL); | ||
| await expect(page).toHaveURL(VOTES_LIST_URL, { timeout: TIMEOUT }); | ||
| await expect(page.getByRole('heading', { name: 'グレード投票' })).toBeVisible({ | ||
| timeout: TIMEOUT, | ||
| }); | ||
| }); | ||
|
|
||
| test('task table is visible to unauthenticated user', async ({ page }) => { | ||
| await page.goto(VOTES_LIST_URL); | ||
| await expect(page.getByRole('columnheader', { name: '問題' })).toBeVisible({ | ||
| timeout: TIMEOUT, | ||
| }); | ||
| await expect(page.getByRole('columnheader', { name: 'コンテスト' })).toBeVisible({ | ||
| timeout: TIMEOUT, | ||
| }); | ||
| }); | ||
|
|
||
| test('logged-in user can view the page', async ({ page }) => { | ||
| await loginAsUser(page); | ||
| await page.goto(VOTES_LIST_URL); | ||
| await expect(page).toHaveURL(VOTES_LIST_URL, { timeout: TIMEOUT }); | ||
| await expect(page.getByRole('heading', { name: 'グレード投票' })).toBeVisible({ | ||
| timeout: TIMEOUT, | ||
| }); | ||
| }); | ||
|
|
||
| test('search input filters tasks by title', async ({ page }) => { | ||
| await page.goto(VOTES_LIST_URL); | ||
| const searchInput = page.getByPlaceholder('問題名・問題ID・コンテストIDで検索'); | ||
| await expect(searchInput).toBeVisible({ timeout: TIMEOUT }); | ||
|
|
||
| // Type a string unlikely to match any task to get 0 results | ||
| await searchInput.fill('__no_match_expected__'); | ||
| await expect(page.getByText('該当する問題が見つかりませんでした')).toBeVisible({ | ||
| timeout: TIMEOUT, | ||
| }); | ||
| }); | ||
| }); | ||
|
|
||
| // --------------------------------------------------------------------------- | ||
| // Vote detail page (/votes/[slug]) | ||
| // --------------------------------------------------------------------------- | ||
|
|
||
| test.describe('vote detail page (/votes/[slug])', () => { | ||
| /** | ||
| * Navigates to the first task in the vote list. | ||
| * Assumes at least one task exists in the DB. | ||
| */ | ||
| async function navigateToFirstVoteDetailPage(page: Page): Promise<void> { | ||
| await page.goto(VOTES_LIST_URL); | ||
| await expect(page.getByRole('columnheader', { name: '問題' })).toBeVisible({ | ||
| timeout: TIMEOUT, | ||
| }); | ||
| // Click the first task title link in the table | ||
| await page.locator('table').getByRole('link').first().click(); | ||
| await expect(page).toHaveURL(/\/votes\/.+/, { timeout: TIMEOUT }); | ||
| } | ||
|
|
||
| test.describe('unauthenticated user', () => { | ||
| test('can view the task detail page without redirect', async ({ page }) => { | ||
| await navigateToFirstVoteDetailPage(page); | ||
| // Should stay on /votes/[slug], not redirected | ||
| await expect(page).toHaveURL(/\/votes\/.+/, { timeout: TIMEOUT }); | ||
| }); | ||
|
|
||
| test('sees login prompt instead of vote buttons', async ({ page }) => { | ||
| await navigateToFirstVoteDetailPage(page); | ||
| await expect(page.getByText('投票するにはログインが必要です')).toBeVisible({ | ||
| timeout: TIMEOUT, | ||
| }); | ||
| await expect(page.getByRole('link', { name: 'ログイン' })).toBeVisible({ | ||
| timeout: TIMEOUT, | ||
| }); | ||
| await expect(page.getByRole('link', { name: 'アカウント作成' })).toBeVisible({ | ||
| timeout: TIMEOUT, | ||
| }); | ||
| }); | ||
|
|
||
| test('does not see vote grade buttons', async ({ page }) => { | ||
| await navigateToFirstVoteDetailPage(page); | ||
| // Grade buttons are only rendered inside the vote form (not shown when logged out) | ||
| await expect(page.locator('form[action="?/voteAbsoluteGrade"]')).not.toBeAttached(); | ||
| }); | ||
|
|
||
| test('breadcrumb link navigates back to /votes', async ({ page }) => { | ||
| await navigateToFirstVoteDetailPage(page); | ||
| await page.getByRole('link', { name: 'グレード投票' }).click(); | ||
| await expect(page).toHaveURL(VOTES_LIST_URL, { timeout: TIMEOUT }); | ||
| }); | ||
| }); | ||
|
|
||
| test.describe('logged-in user', () => { | ||
| test.beforeEach(async ({ page }) => { | ||
| await loginAsUser(page); | ||
| }); | ||
|
|
||
| test('can view the task detail page', async ({ page }) => { | ||
| await navigateToFirstVoteDetailPage(page); | ||
| await expect(page).toHaveURL(/\/votes\/.+/, { timeout: TIMEOUT }); | ||
| }); | ||
|
|
||
| test('sees vote grade buttons', async ({ page }) => { | ||
| await navigateToFirstVoteDetailPage(page); | ||
| // Vote form with grade buttons is rendered for logged-in users | ||
| await expect(page.locator('form[action="?/voteAbsoluteGrade"]')).toBeVisible({ | ||
| timeout: TIMEOUT, | ||
| }); | ||
| // The grade buttons should include Q11 (11Q) | ||
| await expect(page.getByRole('button', { name: '11Q' })).toBeVisible({ timeout: TIMEOUT }); | ||
| }); | ||
|
|
||
| test('does not see login prompt', async ({ page }) => { | ||
| await navigateToFirstVoteDetailPage(page); | ||
| await expect(page.getByText('投票するにはログインが必要です')).not.toBeVisible(); | ||
| }); | ||
| }); | ||
| }); | ||
|
|
||
| // --------------------------------------------------------------------------- | ||
| // Vote management page (/vote_management) — admin only | ||
| // --------------------------------------------------------------------------- | ||
|
|
||
| test.describe('vote management page (/vote_management)', () => { | ||
| test('unauthenticated user is redirected to /login', async ({ page }) => { | ||
| await page.goto(VOTE_MANAGEMENT_URL); | ||
| await expect(page).toHaveURL('/login', { timeout: TIMEOUT }); | ||
| }); | ||
|
|
||
| test('non-admin user is redirected to /login', async ({ page }) => { | ||
| await loginAsUser(page); | ||
| await page.goto(VOTE_MANAGEMENT_URL); | ||
| await expect(page).toHaveURL('/login', { timeout: TIMEOUT }); | ||
| }); | ||
|
|
||
| test.describe('admin user', () => { | ||
| test.beforeEach(async ({ page }) => { | ||
| await loginAsAdmin(page); | ||
| }); | ||
|
|
||
| test('can access the page', async ({ page }) => { | ||
| await page.goto(VOTE_MANAGEMENT_URL); | ||
| await expect(page).toHaveURL(VOTE_MANAGEMENT_URL, { timeout: TIMEOUT }); | ||
| await expect(page.getByRole('heading', { name: '投票管理' })).toBeVisible({ | ||
| timeout: TIMEOUT, | ||
| }); | ||
| }); | ||
|
|
||
| test('sees the vote management table with expected columns', async ({ page }) => { | ||
| await page.goto(VOTE_MANAGEMENT_URL); | ||
| await expect(page.getByRole('columnheader', { name: '問題' })).toBeVisible({ | ||
| timeout: TIMEOUT, | ||
| }); | ||
| await expect(page.getByRole('columnheader', { name: 'DBグレード' })).toBeVisible({ | ||
| timeout: TIMEOUT, | ||
| }); | ||
| await expect(page.getByRole('columnheader', { name: '中央値グレード' })).toBeVisible({ | ||
| timeout: TIMEOUT, | ||
| }); | ||
| await expect(page.getByRole('columnheader', { name: '票数' })).toBeVisible({ | ||
| timeout: TIMEOUT, | ||
| }); | ||
| }); | ||
| }); | ||
| }); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
45 changes: 45 additions & 0 deletions
45
prisma/migrations/20260315003958_add_vote_table/migration.sql
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,45 @@ | ||
| -- CreateTable | ||
| CREATE TABLE "votegrade" ( | ||
| "id" TEXT NOT NULL, | ||
| "userId" TEXT NOT NULL, | ||
| "taskId" TEXT NOT NULL, | ||
| "grade" "TaskGrade" NOT NULL, | ||
| "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, | ||
| "updatedAt" TIMESTAMP(3) NOT NULL, | ||
|
|
||
| CONSTRAINT "votegrade_pkey" PRIMARY KEY ("id") | ||
| ); | ||
|
|
||
| -- CreateTable | ||
| CREATE TABLE "votedgradecounter" ( | ||
| "id" TEXT NOT NULL, | ||
| "taskId" TEXT NOT NULL, | ||
| "grade" "TaskGrade" NOT NULL, | ||
| "count" INTEGER NOT NULL, | ||
| "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, | ||
| "updatedAt" TIMESTAMP(3) NOT NULL, | ||
|
|
||
| CONSTRAINT "votedgradecounter_pkey" PRIMARY KEY ("id") | ||
| ); | ||
|
|
||
| -- CreateTable | ||
| CREATE TABLE "votedgradestatistics" ( | ||
| "id" TEXT NOT NULL, | ||
| "taskId" TEXT NOT NULL, | ||
| "grade" "TaskGrade" NOT NULL, | ||
| "isExperimental" BOOLEAN NOT NULL DEFAULT false, | ||
| "isApproved" BOOLEAN NOT NULL DEFAULT true, | ||
| "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, | ||
| "updatedAt" TIMESTAMP(3) NOT NULL, | ||
|
|
||
| CONSTRAINT "votedgradestatistics_pkey" PRIMARY KEY ("id") | ||
| ); | ||
|
|
||
| -- CreateIndex | ||
| CREATE UNIQUE INDEX "votegrade_userId_taskId_key" ON "votegrade"("userId", "taskId"); | ||
KATO-Hiro marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| -- AddForeignKey | ||
| ALTER TABLE "votegrade" ADD CONSTRAINT "votegrade_userId_fkey" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE RESTRICT ON UPDATE CASCADE; | ||
|
|
||
| -- AddForeignKey | ||
| ALTER TABLE "votegrade" ADD CONSTRAINT "votegrade_taskId_fkey" FOREIGN KEY ("taskId") REFERENCES "task"("task_id") ON DELETE RESTRICT ON UPDATE CASCADE; | ||
13 changes: 13 additions & 0 deletions
13
prisma/migrations/20260320143157_change_vote_grade_table/migration.sql
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,13 @@ | ||
| /* | ||
| Warnings: | ||
|
|
||
| - The primary key for the `votegrade` table will be changed. If it partially fails, the table could be left without primary key constraint. | ||
| - A unique constraint covering the columns `[id]` on the table `votegrade` will be added. If there are existing duplicate values, this will fail. | ||
|
|
||
| */ | ||
| -- AlterTable | ||
| ALTER TABLE "votegrade" DROP CONSTRAINT "votegrade_pkey", | ||
| ADD CONSTRAINT "votegrade_pkey" PRIMARY KEY ("userId", "taskId"); | ||
|
|
||
| -- CreateIndex | ||
| CREATE UNIQUE INDEX "votegrade_id_key" ON "votegrade"("id"); |
17 changes: 17 additions & 0 deletions
17
prisma/migrations/20260321164035_change_voted_grade_counter/migration.sql
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,17 @@ | ||
| /* | ||
| Warnings: | ||
|
|
||
| - The primary key for the `votedgradecounter` table will be changed. If it partially fails, the table could be left without primary key constraint. | ||
| - A unique constraint covering the columns `[id]` on the table `votedgradecounter` will be added. If there are existing duplicate values, this will fail. | ||
| - A unique constraint covering the columns `[taskId,grade]` on the table `votedgradecounter` will be added. If there are existing duplicate values, this will fail. | ||
|
|
||
| */ | ||
| -- AlterTable | ||
| ALTER TABLE "votedgradecounter" DROP CONSTRAINT "votedgradecounter_pkey", | ||
| ADD CONSTRAINT "votedgradecounter_pkey" PRIMARY KEY ("taskId", "grade"); | ||
|
|
||
| -- CreateIndex | ||
| CREATE UNIQUE INDEX "votedgradecounter_id_key" ON "votedgradecounter"("id"); | ||
|
|
||
| -- CreateIndex | ||
| CREATE UNIQUE INDEX "votedgradecounter_taskId_grade_key" ON "votedgradecounter"("taskId", "grade"); |
14 changes: 14 additions & 0 deletions
14
prisma/migrations/20260322043315_change_some_vote_tables_decorator/migration.sql
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,14 @@ | ||
| /* | ||
| Warnings: | ||
|
|
||
| - A unique constraint covering the columns `[taskId]` on the table `votedgradestatistics` will be added. If there are existing duplicate values, this will fail. | ||
|
|
||
| */ | ||
| -- DropIndex | ||
| DROP INDEX "votedgradecounter_taskId_grade_key"; | ||
|
|
||
| -- DropIndex | ||
| DROP INDEX "votegrade_userId_taskId_key"; | ||
|
|
||
| -- CreateIndex | ||
| CREATE UNIQUE INDEX "votedgradestatistics_taskId_key" ON "votedgradestatistics"("taskId"); |
8 changes: 8 additions & 0 deletions
8
prisma/migrations/20260325000000_remove_is_approved_from_vote_statistics/migration.sql
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,8 @@ | ||
| /* | ||
| Warnings: | ||
|
|
||
| - You are about to drop the column `isApproved` on the `votedgradestatistics` table. All the data in the column will be lost. | ||
|
|
||
| */ | ||
| -- AlterTable | ||
| ALTER TABLE "votedgradestatistics" DROP COLUMN "isApproved"; |
11 changes: 11 additions & 0 deletions
11
prisma/migrations/20260326140100_add_task_fk_cascade_to_vote_tables/migration.sql
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,11 @@ | ||
| -- DropForeignKey | ||
| ALTER TABLE "votegrade" DROP CONSTRAINT "votegrade_taskId_fkey"; | ||
|
|
||
| -- AddForeignKey | ||
| ALTER TABLE "votegrade" ADD CONSTRAINT "votegrade_taskId_fkey" FOREIGN KEY ("taskId") REFERENCES "task"("task_id") ON DELETE CASCADE ON UPDATE CASCADE; | ||
|
|
||
| -- AddForeignKey | ||
| ALTER TABLE "votedgradecounter" ADD CONSTRAINT "votedgradecounter_taskId_fkey" FOREIGN KEY ("taskId") REFERENCES "task"("task_id") ON DELETE CASCADE ON UPDATE CASCADE; | ||
|
|
||
| -- AddForeignKey | ||
| ALTER TABLE "votedgradestatistics" ADD CONSTRAINT "votedgradestatistics_taskId_fkey" FOREIGN KEY ("taskId") REFERENCES "task"("task_id") ON DELETE CASCADE ON UPDATE CASCADE; |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.