Make Polls Anonymous (#1197)
* make polls anonymous Introduce a `PollBlindVote` DB table that tracks when a user votes in a poll, but does not track which choice they made. Alter the `PollVote` DB table to remove the `userId` column, meaning `PollVote` now tracks poll votes anonymously - it captures votes per poll option, but does not track which user submitted the vote. Update the `poll_vote` DB function to work with both tables now. Update the `item.poll` resolver to calculate `meVoted` based on the `PollBlindVote` table instead of `PollVote`. * remove `meVoted` on `PollOption`s --------- Co-authored-by: Keyan <34140557+huumn@users.noreply.github.com>
This commit is contained in:
parent
2597eb56f3
commit
e3571af1e1
@ -1004,8 +1004,7 @@ export default {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const options = await models.$queryRaw`
|
const options = await models.$queryRaw`
|
||||||
SELECT "PollOption".id, option, count("PollVote"."userId")::INTEGER as count,
|
SELECT "PollOption".id, option, count("PollVote".id)::INTEGER as count
|
||||||
coalesce(bool_or("PollVote"."userId" = ${me?.id}), 'f') as "meVoted"
|
|
||||||
FROM "PollOption"
|
FROM "PollOption"
|
||||||
LEFT JOIN "PollVote" on "PollVote"."pollOptionId" = "PollOption".id
|
LEFT JOIN "PollVote" on "PollVote"."pollOptionId" = "PollOption".id
|
||||||
WHERE "PollOption"."itemId" = ${item.id}
|
WHERE "PollOption"."itemId" = ${item.id}
|
||||||
@ -1013,9 +1012,16 @@ export default {
|
|||||||
ORDER BY "PollOption".id ASC
|
ORDER BY "PollOption".id ASC
|
||||||
`
|
`
|
||||||
|
|
||||||
|
const meVoted = await models.pollBlindVote.findFirst({
|
||||||
|
where: {
|
||||||
|
userId: me?.id,
|
||||||
|
itemId: item.id
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
const poll = {}
|
const poll = {}
|
||||||
poll.options = options
|
poll.options = options
|
||||||
poll.meVoted = options.some(o => o.meVoted)
|
poll.meVoted = !!meVoted
|
||||||
poll.count = options.reduce((t, o) => t + o.count, 0)
|
poll.count = options.reduce((t, o) => t + o.count, 0)
|
||||||
|
|
||||||
return poll
|
return poll
|
||||||
|
@ -46,7 +46,6 @@ export default gql`
|
|||||||
id: ID,
|
id: ID,
|
||||||
option: String!
|
option: String!
|
||||||
count: Int!
|
count: Int!
|
||||||
meVoted: Boolean!
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type Poll {
|
type Poll {
|
||||||
|
@ -4,7 +4,6 @@ import { fixedDecimal, numWithUnits } from '@/lib/format'
|
|||||||
import { timeLeft } from '@/lib/time'
|
import { timeLeft } from '@/lib/time'
|
||||||
import { useMe } from './me'
|
import { useMe } from './me'
|
||||||
import styles from './poll.module.css'
|
import styles from './poll.module.css'
|
||||||
import Check from '@/svgs/checkbox-circle-fill.svg'
|
|
||||||
import { signIn } from 'next-auth/react'
|
import { signIn } from 'next-auth/react'
|
||||||
import ActionTooltip from './action-tooltip'
|
import ActionTooltip from './action-tooltip'
|
||||||
import { POLL_COST } from '@/lib/constants'
|
import { POLL_COST } from '@/lib/constants'
|
||||||
@ -40,9 +39,6 @@ export default function Poll ({ item }) {
|
|||||||
fields: {
|
fields: {
|
||||||
count (existingCount) {
|
count (existingCount) {
|
||||||
return existingCount + 1
|
return existingCount + 1
|
||||||
},
|
|
||||||
meVoted () {
|
|
||||||
return true
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@ -121,7 +117,7 @@ export default function Poll ({ item }) {
|
|||||||
function PollResult ({ v, progress }) {
|
function PollResult ({ v, progress }) {
|
||||||
return (
|
return (
|
||||||
<div className={styles.pollResult}>
|
<div className={styles.pollResult}>
|
||||||
<span className={styles.pollOption}>{v.option}{v.meVoted && <Check className='fill-grey ms-1 align-self-center flex-shrink-0' width={16} height={16} />}</span>
|
<span className={styles.pollOption}>{v.option}</span>
|
||||||
<span className='ms-auto me-2 align-self-center'>{progress}%</span>
|
<span className='ms-auto me-2 align-self-center'>{progress}%</span>
|
||||||
<div className={styles.pollProgress} style={{ width: `${progress}%` }} />
|
<div className={styles.pollProgress} style={{ width: `${progress}%` }} />
|
||||||
</div>
|
</div>
|
||||||
|
@ -126,7 +126,6 @@ export const POLL_FIELDS = gql`
|
|||||||
id
|
id
|
||||||
option
|
option
|
||||||
count
|
count
|
||||||
meVoted
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}`
|
}`
|
||||||
|
@ -0,0 +1,92 @@
|
|||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "PollBlindVote" (
|
||||||
|
"id" SERIAL NOT NULL,
|
||||||
|
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"updated_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"itemId" INTEGER NOT NULL,
|
||||||
|
"userId" INTEGER NOT NULL,
|
||||||
|
|
||||||
|
CONSTRAINT "PollBlindVote_pkey" PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "PollBlindVote.userId_index" ON "PollBlindVote"("userId");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE UNIQUE INDEX "PollBlindVote.itemId_userId_unique" ON "PollBlindVote"("itemId", "userId");
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "PollBlindVote" ADD CONSTRAINT "PollBlindVote_itemId_fkey" FOREIGN KEY ("itemId") REFERENCES "Item"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "PollBlindVote" ADD CONSTRAINT "PollBlindVote_userId_fkey" FOREIGN KEY ("userId") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- migrate existing poll votes
|
||||||
|
INSERT INTO "PollBlindVote" ("itemId", "userId")
|
||||||
|
SELECT "itemId", "userId" FROM "PollVote";
|
||||||
|
|
||||||
|
/*
|
||||||
|
Warnings:
|
||||||
|
|
||||||
|
- You are about to drop the column `userId` on the `PollVote` table. All the data in the column will be lost.
|
||||||
|
|
||||||
|
*/
|
||||||
|
-- DropForeignKey
|
||||||
|
ALTER TABLE "PollVote" DROP CONSTRAINT "PollVote_userId_fkey";
|
||||||
|
|
||||||
|
-- DropIndex
|
||||||
|
DROP INDEX "PollVote.itemId_userId_unique";
|
||||||
|
|
||||||
|
-- DropIndex
|
||||||
|
DROP INDEX "PollVote.userId_index";
|
||||||
|
|
||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "PollVote" DROP COLUMN "userId";
|
||||||
|
|
||||||
|
-- update `poll_vote` function to update both "PollVote" and "PollBlindVote" tables
|
||||||
|
-- create poll vote
|
||||||
|
-- if user hasn't already voted
|
||||||
|
-- charges user item.pollCost
|
||||||
|
-- adds POLL to ItemAct
|
||||||
|
-- adds PollVote
|
||||||
|
-- adds PollBlindVote
|
||||||
|
CREATE OR REPLACE FUNCTION poll_vote(option_id INTEGER, user_id INTEGER)
|
||||||
|
RETURNS "Item"
|
||||||
|
LANGUAGE plpgsql
|
||||||
|
AS $$
|
||||||
|
DECLARE
|
||||||
|
item "Item";
|
||||||
|
option "PollOption";
|
||||||
|
BEGIN
|
||||||
|
PERFORM ASSERT_SERIALIZED();
|
||||||
|
|
||||||
|
SELECT * INTO option FROM "PollOption" where id = option_id;
|
||||||
|
IF option IS NULL THEN
|
||||||
|
RAISE EXCEPTION 'INVALID_POLL_OPTION';
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
SELECT * INTO item FROM "Item" where id = option."itemId";
|
||||||
|
IF item IS NULL THEN
|
||||||
|
RAISE EXCEPTION 'POLL_DOES_NOT_EXIST';
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
IF item."userId" = user_id THEN
|
||||||
|
RAISE EXCEPTION 'POLL_OWNER_CANT_VOTE';
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
-- no longer check `PollVote` to see if a user has voted. Instead, check `PollBlindVote`
|
||||||
|
IF EXISTS (SELECT 1 FROM "PollBlindVote" WHERE "itemId" = item.id AND "userId" = user_id) THEN
|
||||||
|
RAISE EXCEPTION 'POLL_VOTE_ALREADY_EXISTS';
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
PERFORM item_act(item.id, user_id, 'POLL', item."pollCost");
|
||||||
|
|
||||||
|
INSERT INTO "PollVote" (created_at, updated_at, "itemId", "pollOptionId")
|
||||||
|
VALUES (now_utc(), now_utc(), item.id, option_id);
|
||||||
|
|
||||||
|
INSERT INTO "PollBlindVote" (created_at, updated_at, "itemId", "userId")
|
||||||
|
VALUES (now_utc(), now_utc(), item.id, user_id);
|
||||||
|
|
||||||
|
RETURN item;
|
||||||
|
END;
|
||||||
|
$$;
|
@ -80,7 +80,6 @@ model User {
|
|||||||
actions ItemAct[]
|
actions ItemAct[]
|
||||||
mentions Mention[]
|
mentions Mention[]
|
||||||
messages Message[]
|
messages Message[]
|
||||||
PollVote PollVote[]
|
|
||||||
PushSubscriptions PushSubscription[]
|
PushSubscriptions PushSubscription[]
|
||||||
ReferralAct ReferralAct[]
|
ReferralAct ReferralAct[]
|
||||||
Streak Streak[]
|
Streak Streak[]
|
||||||
@ -126,6 +125,7 @@ model User {
|
|||||||
Replies Reply[]
|
Replies Reply[]
|
||||||
walletLogs WalletLog[]
|
walletLogs WalletLog[]
|
||||||
Reminder Reminder[]
|
Reminder Reminder[]
|
||||||
|
PollBlindVote PollBlindVote[]
|
||||||
|
|
||||||
@@index([photoId])
|
@@index([photoId])
|
||||||
@@index([createdAt], map: "users.created_at_index")
|
@@index([createdAt], map: "users.created_at_index")
|
||||||
@ -427,6 +427,7 @@ model Item {
|
|||||||
Ancestors Reply[] @relation("AncestorReplyItem")
|
Ancestors Reply[] @relation("AncestorReplyItem")
|
||||||
Replies Reply[]
|
Replies Reply[]
|
||||||
Reminder Reminder[]
|
Reminder Reminder[]
|
||||||
|
PollBlindVote PollBlindVote[]
|
||||||
|
|
||||||
@@index([uploadId])
|
@@index([uploadId])
|
||||||
@@index([lastZapAt])
|
@@index([lastZapAt])
|
||||||
@ -504,16 +505,25 @@ model PollVote {
|
|||||||
id Int @id @default(autoincrement())
|
id Int @id @default(autoincrement())
|
||||||
createdAt DateTime @default(now()) @map("created_at")
|
createdAt DateTime @default(now()) @map("created_at")
|
||||||
updatedAt DateTime @default(now()) @updatedAt @map("updated_at")
|
updatedAt DateTime @default(now()) @updatedAt @map("updated_at")
|
||||||
userId Int
|
|
||||||
itemId Int
|
itemId Int
|
||||||
pollOptionId Int
|
pollOptionId Int
|
||||||
item Item @relation(fields: [itemId], references: [id], onDelete: Cascade)
|
item Item @relation(fields: [itemId], references: [id], onDelete: Cascade)
|
||||||
pollOption PollOption @relation(fields: [pollOptionId], references: [id], onDelete: Cascade)
|
pollOption PollOption @relation(fields: [pollOptionId], references: [id], onDelete: Cascade)
|
||||||
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
|
||||||
|
|
||||||
@@unique([itemId, userId], map: "PollVote.itemId_userId_unique")
|
|
||||||
@@index([pollOptionId], map: "PollVote.pollOptionId_index")
|
@@index([pollOptionId], map: "PollVote.pollOptionId_index")
|
||||||
@@index([userId], map: "PollVote.userId_index")
|
}
|
||||||
|
|
||||||
|
model PollBlindVote {
|
||||||
|
id Int @id @default(autoincrement())
|
||||||
|
createdAt DateTime @default(now()) @map("created_at")
|
||||||
|
updatedAt DateTime @default(now()) @updatedAt @map("updated_at")
|
||||||
|
itemId Int
|
||||||
|
userId Int
|
||||||
|
item Item @relation(fields: [itemId], references: [id], onDelete: Cascade)
|
||||||
|
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||||
|
|
||||||
|
@@unique([itemId, userId], map: "PollBlindVote.itemId_userId_unique")
|
||||||
|
@@index([userId], map: "PollBlindVote.userId_index")
|
||||||
}
|
}
|
||||||
|
|
||||||
enum BillingType {
|
enum BillingType {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user