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`
|
||||
SELECT "PollOption".id, option, count("PollVote"."userId")::INTEGER as count,
|
||||
coalesce(bool_or("PollVote"."userId" = ${me?.id}), 'f') as "meVoted"
|
||||
SELECT "PollOption".id, option, count("PollVote".id)::INTEGER as count
|
||||
FROM "PollOption"
|
||||
LEFT JOIN "PollVote" on "PollVote"."pollOptionId" = "PollOption".id
|
||||
WHERE "PollOption"."itemId" = ${item.id}
|
||||
|
@ -1013,9 +1012,16 @@ export default {
|
|||
ORDER BY "PollOption".id ASC
|
||||
`
|
||||
|
||||
const meVoted = await models.pollBlindVote.findFirst({
|
||||
where: {
|
||||
userId: me?.id,
|
||||
itemId: item.id
|
||||
}
|
||||
})
|
||||
|
||||
const poll = {}
|
||||
poll.options = options
|
||||
poll.meVoted = options.some(o => o.meVoted)
|
||||
poll.meVoted = !!meVoted
|
||||
poll.count = options.reduce((t, o) => t + o.count, 0)
|
||||
|
||||
return poll
|
||||
|
|
|
@ -46,7 +46,6 @@ export default gql`
|
|||
id: ID,
|
||||
option: String!
|
||||
count: Int!
|
||||
meVoted: Boolean!
|
||||
}
|
||||
|
||||
type Poll {
|
||||
|
|
|
@ -4,7 +4,6 @@ import { fixedDecimal, numWithUnits } from '@/lib/format'
|
|||
import { timeLeft } from '@/lib/time'
|
||||
import { useMe } from './me'
|
||||
import styles from './poll.module.css'
|
||||
import Check from '@/svgs/checkbox-circle-fill.svg'
|
||||
import { signIn } from 'next-auth/react'
|
||||
import ActionTooltip from './action-tooltip'
|
||||
import { POLL_COST } from '@/lib/constants'
|
||||
|
@ -40,9 +39,6 @@ export default function Poll ({ item }) {
|
|||
fields: {
|
||||
count (existingCount) {
|
||||
return existingCount + 1
|
||||
},
|
||||
meVoted () {
|
||||
return true
|
||||
}
|
||||
}
|
||||
})
|
||||
|
@ -121,7 +117,7 @@ export default function Poll ({ item }) {
|
|||
function PollResult ({ v, progress }) {
|
||||
return (
|
||||
<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>
|
||||
<div className={styles.pollProgress} style={{ width: `${progress}%` }} />
|
||||
</div>
|
||||
|
|
|
@ -126,7 +126,6 @@ export const POLL_FIELDS = gql`
|
|||
id
|
||||
option
|
||||
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[]
|
||||
mentions Mention[]
|
||||
messages Message[]
|
||||
PollVote PollVote[]
|
||||
PushSubscriptions PushSubscription[]
|
||||
ReferralAct ReferralAct[]
|
||||
Streak Streak[]
|
||||
|
@ -126,6 +125,7 @@ model User {
|
|||
Replies Reply[]
|
||||
walletLogs WalletLog[]
|
||||
Reminder Reminder[]
|
||||
PollBlindVote PollBlindVote[]
|
||||
|
||||
@@index([photoId])
|
||||
@@index([createdAt], map: "users.created_at_index")
|
||||
|
@ -427,6 +427,7 @@ model Item {
|
|||
Ancestors Reply[] @relation("AncestorReplyItem")
|
||||
Replies Reply[]
|
||||
Reminder Reminder[]
|
||||
PollBlindVote PollBlindVote[]
|
||||
|
||||
@@index([uploadId])
|
||||
@@index([lastZapAt])
|
||||
|
@ -504,16 +505,25 @@ model PollVote {
|
|||
id Int @id @default(autoincrement())
|
||||
createdAt DateTime @default(now()) @map("created_at")
|
||||
updatedAt DateTime @default(now()) @updatedAt @map("updated_at")
|
||||
userId Int
|
||||
itemId Int
|
||||
pollOptionId Int
|
||||
item Item @relation(fields: [itemId], 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([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 {
|
||||
|
|
Loading…
Reference in New Issue