mirror of
https://github.com/Sendouc/sendou.ink.git
synced 2026-08-24 03:55:49 -05:00
New Plus Voting pass criteria (#2924)
This commit is contained in:
@@ -1115,14 +1115,15 @@ async function thisMonthsSuggestions() {
|
||||
}
|
||||
}
|
||||
|
||||
function syncPlusTiers() {
|
||||
sql
|
||||
.prepare(
|
||||
/* sql */ `
|
||||
insert into "PlusTier" ("userId", "tier") select "userId", "tier" from "FreshPlusTier" where "tier" is not null;
|
||||
`,
|
||||
)
|
||||
.run();
|
||||
async function syncPlusTiers() {
|
||||
const tiers = await PlusVotingRepository.allPlusTiersFromLatestVoting();
|
||||
|
||||
if (tiers.length === 0) return;
|
||||
|
||||
await db
|
||||
.insertInto("PlusTier")
|
||||
.values(tiers.map(({ userId, plusTier }) => ({ userId, tier: plusTier })))
|
||||
.execute();
|
||||
}
|
||||
|
||||
function getAvailableBadgeIds() {
|
||||
|
||||
@@ -226,11 +226,6 @@ export interface CalendarEventResultTeam {
|
||||
placement: number;
|
||||
}
|
||||
|
||||
export interface FreshPlusTier {
|
||||
tier: number | null;
|
||||
userId: number;
|
||||
}
|
||||
|
||||
export interface Group {
|
||||
chatCode: string | null;
|
||||
createdAt: Generated<number>;
|
||||
@@ -450,7 +445,6 @@ export interface PlusVotingResult {
|
||||
month: number;
|
||||
year: number;
|
||||
wasSuggested: DBBoolean;
|
||||
passedVoting: DBBoolean;
|
||||
}
|
||||
|
||||
export interface ReportedWeapon {
|
||||
@@ -1374,7 +1368,7 @@ export interface DB {
|
||||
CalendarEventDate: CalendarEventDate;
|
||||
CalendarEventResultPlayer: CalendarEventResultPlayer;
|
||||
CalendarEventResultTeam: CalendarEventResultTeam;
|
||||
FreshPlusTier: FreshPlusTier;
|
||||
|
||||
Group: Group;
|
||||
GroupLike: GroupLike;
|
||||
GroupMatch: GroupMatch;
|
||||
|
||||
@@ -91,12 +91,54 @@ describe("Plus voting", () => {
|
||||
expect(await countPlusTierMembers()).toBe(5);
|
||||
});
|
||||
|
||||
test("60% is the criteria to pass voting", async () => {
|
||||
test("60% or more guarantees pass", async () => {
|
||||
vi.setSystemTime(new Date("2023-12-12T00:00:00.000Z"));
|
||||
|
||||
await dbInsertUsers(10);
|
||||
|
||||
// 50%
|
||||
// 60% - auto-pass
|
||||
await PlusVotingRepository.upsertMany(
|
||||
Array.from({ length: 10 }).map((_, i) => {
|
||||
return voteArgs({
|
||||
authorId: i + 1,
|
||||
score: i < 4 ? -1 : 1,
|
||||
votedId: 1,
|
||||
});
|
||||
}),
|
||||
);
|
||||
|
||||
await adminAction({ _action: "REFRESH" }, { user: "admin" });
|
||||
|
||||
expect(await countPlusTierMembers()).toBe(1);
|
||||
});
|
||||
|
||||
test("40% or less does not pass", async () => {
|
||||
vi.setSystemTime(new Date("2023-12-12T00:00:00.000Z"));
|
||||
|
||||
await dbInsertUsers(10);
|
||||
|
||||
// 40% - auto-fail
|
||||
await PlusVotingRepository.upsertMany(
|
||||
Array.from({ length: 10 }).map((_, i) => {
|
||||
return voteArgs({
|
||||
authorId: i + 1,
|
||||
score: i < 6 ? -1 : 1,
|
||||
votedId: 1,
|
||||
});
|
||||
}),
|
||||
);
|
||||
|
||||
await adminAction({ _action: "REFRESH" }, { user: "admin" });
|
||||
|
||||
expect(await countPlusTierMembers()).toBe(0);
|
||||
});
|
||||
|
||||
test("middle zone (40-60%) passes when quota has room", async () => {
|
||||
vi.setSystemTime(new Date("2023-12-12T00:00:00.000Z"));
|
||||
|
||||
await dbInsertUsers(10);
|
||||
|
||||
// 50% - middle zone, should pass (quota=50 for tier 1)
|
||||
await PlusVotingRepository.upsertMany(
|
||||
Array.from({ length: 10 }).map((_, i) => {
|
||||
return voteArgs({
|
||||
@@ -106,27 +148,10 @@ describe("Plus voting", () => {
|
||||
});
|
||||
}),
|
||||
);
|
||||
// 60%
|
||||
await PlusVotingRepository.upsertMany(
|
||||
Array.from({ length: 10 }).map((_, i) => {
|
||||
return voteArgs({
|
||||
authorId: i + 1,
|
||||
score: i < 4 ? -1 : 1,
|
||||
votedId: 2,
|
||||
});
|
||||
}),
|
||||
);
|
||||
|
||||
await adminAction({ _action: "REFRESH" }, { user: "admin" });
|
||||
|
||||
const rows = await db
|
||||
.selectFrom("PlusTier")
|
||||
.select(["PlusTier.tier", "PlusTier.userId"])
|
||||
.where("PlusTier.tier", "=", 1)
|
||||
.execute();
|
||||
|
||||
expect(rows.length).toBe(1);
|
||||
expect(rows[0].userId).toBe(2);
|
||||
expect(await countPlusTierMembers()).toBe(1);
|
||||
});
|
||||
|
||||
test("combines leaderboard and voting results (after season over)", async () => {
|
||||
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
import invariant from "~/utils/invariant";
|
||||
import { COMMON_USER_FIELDS } from "~/utils/kysely.server";
|
||||
import type { Unwrapped } from "~/utils/types";
|
||||
import * as PlusVoting from "./core/PlusVoting";
|
||||
|
||||
const resultsByMonthYearQuery = (args: MonthYear) =>
|
||||
db
|
||||
@@ -19,9 +20,9 @@ const resultsByMonthYearQuery = (args: MonthYear) =>
|
||||
.select([
|
||||
...COMMON_USER_FIELDS,
|
||||
"PlusVotingResult.wasSuggested",
|
||||
"PlusVotingResult.passedVoting",
|
||||
"PlusVotingResult.tier",
|
||||
"PlusVotingResult.score",
|
||||
"PlusVotingResult.votedId",
|
||||
])
|
||||
.where("PlusVotingResult.month", "=", args.month)
|
||||
.where("PlusVotingResult.year", "=", args.year)
|
||||
@@ -30,27 +31,85 @@ type ResultsByMonthYearQueryReturnType = InferResult<
|
||||
ReturnType<typeof resultsByMonthYearQuery>
|
||||
>;
|
||||
|
||||
export function allPlusTiersFromLatestVoting() {
|
||||
return db
|
||||
.selectFrom("FreshPlusTier")
|
||||
.select(["FreshPlusTier.userId", "FreshPlusTier.tier as plusTier"])
|
||||
.where("FreshPlusTier.tier", "is not", null)
|
||||
.execute() as Promise<{ userId: number; plusTier: number }[]>;
|
||||
export async function allPlusTiersFromLatestVoting() {
|
||||
const rows = await db
|
||||
.selectFrom("PlusVotingResult")
|
||||
.select([
|
||||
"PlusVotingResult.votedId",
|
||||
"PlusVotingResult.tier",
|
||||
"PlusVotingResult.score",
|
||||
"PlusVotingResult.wasSuggested",
|
||||
])
|
||||
.where(
|
||||
"PlusVotingResult.year",
|
||||
"=",
|
||||
db
|
||||
.selectFrom("PlusVote")
|
||||
.select("PlusVote.year")
|
||||
.where("PlusVote.validAfter", "<", sql<number>`strftime('%s', 'now')`)
|
||||
.orderBy("PlusVote.year", "desc")
|
||||
.orderBy("PlusVote.month", "desc")
|
||||
.limit(1),
|
||||
)
|
||||
.where(
|
||||
"PlusVotingResult.month",
|
||||
"=",
|
||||
db
|
||||
.selectFrom("PlusVote")
|
||||
.select("PlusVote.month")
|
||||
.where("PlusVote.validAfter", "<", sql<number>`strftime('%s', 'now')`)
|
||||
.orderBy("PlusVote.year", "desc")
|
||||
.orderBy("PlusVote.month", "desc")
|
||||
.limit(1),
|
||||
)
|
||||
.execute();
|
||||
|
||||
const withPassed = PlusVoting.computePassedVoting(rows);
|
||||
return PlusVoting.computeFreshPlusTiers(withPassed);
|
||||
}
|
||||
|
||||
export type ResultsByMonthYearItem = Unwrapped<typeof resultsByMonthYear>;
|
||||
export async function resultsByMonthYear(args: MonthYear) {
|
||||
const rows = await resultsByMonthYearQuery(args).execute();
|
||||
|
||||
return groupPlusVotingResults(rows);
|
||||
const passedMap = new Map<
|
||||
string,
|
||||
{ passedVoting: number; wasSuggested: number }
|
||||
>();
|
||||
const rawForVoting = rows.map((row) => ({
|
||||
votedId: row.votedId,
|
||||
tier: row.tier,
|
||||
score: row.score,
|
||||
wasSuggested: row.wasSuggested,
|
||||
}));
|
||||
for (const r of PlusVoting.computePassedVoting(rawForVoting)) {
|
||||
passedMap.set(`${r.votedId}-${r.tier}`, {
|
||||
passedVoting: r.passedVoting,
|
||||
wasSuggested: r.wasSuggested,
|
||||
});
|
||||
}
|
||||
|
||||
const enrichedRows = rows.map((row) => {
|
||||
const computed = passedMap.get(`${row.votedId}-${row.tier}`);
|
||||
return {
|
||||
...row,
|
||||
passedVoting: computed?.passedVoting ?? 0,
|
||||
};
|
||||
});
|
||||
|
||||
return groupPlusVotingResults(enrichedRows);
|
||||
}
|
||||
|
||||
function groupPlusVotingResults(rows: ResultsByMonthYearQueryReturnType) {
|
||||
type EnrichedRow = ResultsByMonthYearQueryReturnType[number] & {
|
||||
passedVoting: number;
|
||||
};
|
||||
|
||||
function groupPlusVotingResults(rows: EnrichedRow[]) {
|
||||
const grouped: Record<
|
||||
number,
|
||||
{
|
||||
passed: ResultsByMonthYearQueryReturnType;
|
||||
failed: ResultsByMonthYearQueryReturnType;
|
||||
passed: EnrichedRow[];
|
||||
failed: EnrichedRow[];
|
||||
}
|
||||
> = {};
|
||||
|
||||
|
||||
222
app/features/plus-voting/core/PlusVoting.test.ts
Normal file
222
app/features/plus-voting/core/PlusVoting.test.ts
Normal file
@@ -0,0 +1,222 @@
|
||||
import { describe, expect, test } from "vitest";
|
||||
import { computeFreshPlusTiers, computePassedVoting } from "./PlusVoting";
|
||||
|
||||
const result = (
|
||||
overrides: Partial<{
|
||||
votedId: number;
|
||||
tier: number;
|
||||
score: number;
|
||||
wasSuggested: number;
|
||||
}> = {},
|
||||
) => ({
|
||||
votedId: 1,
|
||||
tier: 1,
|
||||
score: 0,
|
||||
wasSuggested: 0,
|
||||
...overrides,
|
||||
});
|
||||
|
||||
describe("computePassedVoting", () => {
|
||||
test("auto-passes users with 60% or more", () => {
|
||||
const results = computePassedVoting([
|
||||
result({ votedId: 1, score: 0.2 }),
|
||||
result({ votedId: 2, score: 0.5 }),
|
||||
result({ votedId: 3, score: 1 }),
|
||||
]);
|
||||
|
||||
expect(results.every((r) => r.passedVoting === 1)).toBe(true);
|
||||
});
|
||||
|
||||
test("auto-fails users with 40% or less", () => {
|
||||
const results = computePassedVoting([
|
||||
result({ votedId: 1, score: -0.2 }),
|
||||
result({ votedId: 2, score: -0.5 }),
|
||||
result({ votedId: 3, score: -1 }),
|
||||
]);
|
||||
|
||||
expect(results.every((r) => r.passedVoting === 0)).toBe(true);
|
||||
});
|
||||
|
||||
test("auto-passers pass even when exceeding quota", () => {
|
||||
const autoPassers = Array.from({ length: 100 }, (_, i) =>
|
||||
result({ votedId: i + 1, score: 0.3 }),
|
||||
);
|
||||
|
||||
const results = computePassedVoting(autoPassers);
|
||||
|
||||
expect(results.length).toBe(100);
|
||||
expect(results.every((r) => r.passedVoting === 1)).toBe(true);
|
||||
});
|
||||
|
||||
test("middle zone users pass when quota has remaining slots", () => {
|
||||
const results = computePassedVoting([
|
||||
result({ votedId: 1, score: 0.3 }),
|
||||
result({ votedId: 2, score: 0.1 }),
|
||||
result({ votedId: 3, score: 0.05 }),
|
||||
]);
|
||||
|
||||
expect(results.find((r) => r.votedId === 1)?.passedVoting).toBe(1);
|
||||
expect(results.find((r) => r.votedId === 2)?.passedVoting).toBe(1);
|
||||
expect(results.find((r) => r.votedId === 3)?.passedVoting).toBe(1);
|
||||
});
|
||||
|
||||
test("nobody from middle zone passes when auto-passers fill the quota", () => {
|
||||
const autoPassers = Array.from({ length: 50 }, (_, i) =>
|
||||
result({ votedId: i + 1, score: 0.3 }),
|
||||
);
|
||||
const middleZone = Array.from({ length: 10 }, (_, i) =>
|
||||
result({ votedId: 51 + i, score: 0.1 }),
|
||||
);
|
||||
|
||||
const results = computePassedVoting([...autoPassers, ...middleZone]);
|
||||
|
||||
const middleResults = results.filter((r) => r.votedId > 50);
|
||||
expect(middleResults.every((r) => r.passedVoting === 0)).toBe(true);
|
||||
});
|
||||
|
||||
test("middle zone fills remaining slots by highest score", () => {
|
||||
const autoPassers = Array.from({ length: 48 }, (_, i) =>
|
||||
result({ votedId: i + 1, score: 0.3 }),
|
||||
);
|
||||
|
||||
const middleZone = [
|
||||
result({ votedId: 100, score: 0.15 }),
|
||||
result({ votedId: 101, score: 0.1 }),
|
||||
result({ votedId: 102, score: 0.05 }),
|
||||
result({ votedId: 103, score: 0.0 }),
|
||||
];
|
||||
|
||||
const results = computePassedVoting([...autoPassers, ...middleZone]);
|
||||
|
||||
expect(results.find((r) => r.votedId === 100)?.passedVoting).toBe(1);
|
||||
expect(results.find((r) => r.votedId === 101)?.passedVoting).toBe(1);
|
||||
expect(results.find((r) => r.votedId === 102)?.passedVoting).toBe(0);
|
||||
expect(results.find((r) => r.votedId === 103)?.passedVoting).toBe(0);
|
||||
});
|
||||
|
||||
test("different tiers have different quotas", () => {
|
||||
const tier1AutoPassers = Array.from({ length: 49 }, (_, i) =>
|
||||
result({ votedId: i + 1, tier: 1, score: 0.3 }),
|
||||
);
|
||||
const tier1Middle = [result({ votedId: 200, tier: 1, score: 0.1 })];
|
||||
|
||||
const tier2AutoPassers = Array.from({ length: 74 }, (_, i) =>
|
||||
result({ votedId: 300 + i, tier: 2, score: 0.3 }),
|
||||
);
|
||||
const tier2Middle = [result({ votedId: 500, tier: 2, score: 0.1 })];
|
||||
|
||||
const results = computePassedVoting([
|
||||
...tier1AutoPassers,
|
||||
...tier1Middle,
|
||||
...tier2AutoPassers,
|
||||
...tier2Middle,
|
||||
]);
|
||||
|
||||
expect(results.find((r) => r.votedId === 200)?.passedVoting).toBe(1);
|
||||
expect(results.find((r) => r.votedId === 500)?.passedVoting).toBe(1);
|
||||
});
|
||||
|
||||
test("exact boundary: score of 0.2 auto-passes", () => {
|
||||
const results = computePassedVoting([result({ score: 0.2 })]);
|
||||
|
||||
expect(results[0].passedVoting).toBe(1);
|
||||
});
|
||||
|
||||
test("exact boundary: score of -0.2 auto-fails", () => {
|
||||
const results = computePassedVoting([result({ score: -0.2 })]);
|
||||
|
||||
expect(results[0].passedVoting).toBe(0);
|
||||
});
|
||||
|
||||
test("empty results returns empty array", () => {
|
||||
expect(computePassedVoting([])).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("computeFreshPlusTiers", () => {
|
||||
const withPassed = (
|
||||
overrides: Partial<{
|
||||
votedId: number;
|
||||
tier: number;
|
||||
score: number;
|
||||
wasSuggested: number;
|
||||
passedVoting: number;
|
||||
}> = {},
|
||||
) => ({
|
||||
votedId: 1,
|
||||
tier: 1,
|
||||
score: 0.5,
|
||||
wasSuggested: 0,
|
||||
passedVoting: 1,
|
||||
...overrides,
|
||||
});
|
||||
|
||||
test("passed voting keeps tier", () => {
|
||||
const tiers = computeFreshPlusTiers([
|
||||
withPassed({ votedId: 1, tier: 1, passedVoting: 1 }),
|
||||
]);
|
||||
|
||||
expect(tiers).toEqual([{ userId: 1, plusTier: 1 }]);
|
||||
});
|
||||
|
||||
test("failed + suggested = removed", () => {
|
||||
const tiers = computeFreshPlusTiers([
|
||||
withPassed({
|
||||
votedId: 1,
|
||||
tier: 1,
|
||||
passedVoting: 0,
|
||||
wasSuggested: 1,
|
||||
}),
|
||||
]);
|
||||
|
||||
expect(tiers).toEqual([]);
|
||||
});
|
||||
|
||||
test("failed + not suggested = demoted one tier", () => {
|
||||
const tiers = computeFreshPlusTiers([
|
||||
withPassed({
|
||||
votedId: 1,
|
||||
tier: 1,
|
||||
passedVoting: 0,
|
||||
wasSuggested: 0,
|
||||
}),
|
||||
]);
|
||||
|
||||
expect(tiers).toEqual([{ userId: 1, plusTier: 2 }]);
|
||||
});
|
||||
|
||||
test("failed tier 3 + not suggested = removed", () => {
|
||||
const tiers = computeFreshPlusTiers([
|
||||
withPassed({
|
||||
votedId: 1,
|
||||
tier: 3,
|
||||
passedVoting: 0,
|
||||
wasSuggested: 0,
|
||||
}),
|
||||
]);
|
||||
|
||||
expect(tiers).toEqual([]);
|
||||
});
|
||||
|
||||
test("multi-tier user gets best (lowest) tier", () => {
|
||||
const tiers = computeFreshPlusTiers([
|
||||
withPassed({ votedId: 1, tier: 1, passedVoting: 0, wasSuggested: 0 }),
|
||||
withPassed({ votedId: 1, tier: 2, passedVoting: 1 }),
|
||||
]);
|
||||
|
||||
expect(tiers).toEqual([{ userId: 1, plusTier: 2 }]);
|
||||
});
|
||||
|
||||
test("all fail = no tier", () => {
|
||||
const tiers = computeFreshPlusTiers([
|
||||
withPassed({
|
||||
votedId: 1,
|
||||
tier: 3,
|
||||
passedVoting: 0,
|
||||
wasSuggested: 1,
|
||||
}),
|
||||
]);
|
||||
|
||||
expect(tiers).toEqual([]);
|
||||
});
|
||||
});
|
||||
77
app/features/plus-voting/core/PlusVoting.ts
Normal file
77
app/features/plus-voting/core/PlusVoting.ts
Normal file
@@ -0,0 +1,77 @@
|
||||
import * as R from "remeda";
|
||||
import { PLUS_VOTING_CRITERIA } from "../plus-voting-constants";
|
||||
|
||||
interface RawVotingResult {
|
||||
votedId: number;
|
||||
tier: number;
|
||||
score: number;
|
||||
wasSuggested: number;
|
||||
}
|
||||
|
||||
interface VotingResultWithPassed extends RawVotingResult {
|
||||
passedVoting: number;
|
||||
}
|
||||
|
||||
export function computePassedVoting(
|
||||
results: RawVotingResult[],
|
||||
): VotingResultWithPassed[] {
|
||||
const byTier = R.groupBy(results, (r) => r.tier);
|
||||
|
||||
return Object.entries(byTier).flatMap(([tierStr, tierResults]) => {
|
||||
const tier = Number(tierStr) as keyof typeof PLUS_VOTING_CRITERIA;
|
||||
const criteria = PLUS_VOTING_CRITERIA[tier];
|
||||
|
||||
const passAvg = percentageToDbAvg(criteria.passPercentage);
|
||||
const failAvg = percentageToDbAvg(criteria.failPercentage);
|
||||
|
||||
const autoPassers = tierResults.filter((r) => r.score >= passAvg);
|
||||
const autoFailers = tierResults.filter((r) => r.score <= failAvg);
|
||||
const middleZone = tierResults
|
||||
.filter((r) => r.score > failAvg && r.score < passAvg)
|
||||
.sort((a, b) => b.score - a.score);
|
||||
|
||||
const remainingSlots = Math.max(0, criteria.quota - autoPassers.length);
|
||||
|
||||
return [
|
||||
...autoPassers.map((r) => ({ ...r, passedVoting: 1 as number })),
|
||||
...autoFailers.map((r) => ({ ...r, passedVoting: 0 as number })),
|
||||
...middleZone.map((r, i) => ({
|
||||
...r,
|
||||
passedVoting: i < remainingSlots ? (1 as number) : (0 as number),
|
||||
})),
|
||||
];
|
||||
});
|
||||
}
|
||||
|
||||
export function computeFreshPlusTiers(
|
||||
results: VotingResultWithPassed[],
|
||||
): { userId: number; plusTier: number }[] {
|
||||
const byUser = R.groupBy(results, (r) => r.votedId);
|
||||
|
||||
const output: { userId: number; plusTier: number }[] = [];
|
||||
|
||||
for (const [userIdStr, userResults] of Object.entries(byUser)) {
|
||||
const effectiveTiers: number[] = [];
|
||||
|
||||
for (const r of userResults) {
|
||||
if (r.passedVoting) {
|
||||
effectiveTiers.push(r.tier);
|
||||
} else if (!r.wasSuggested && r.tier !== 3) {
|
||||
effectiveTiers.push(r.tier + 1);
|
||||
}
|
||||
}
|
||||
|
||||
if (effectiveTiers.length === 0) continue;
|
||||
|
||||
output.push({
|
||||
userId: Number(userIdStr),
|
||||
plusTier: Math.min(...effectiveTiers),
|
||||
});
|
||||
}
|
||||
|
||||
return output;
|
||||
}
|
||||
|
||||
function percentageToDbAvg(percentage: number) {
|
||||
return (2 * percentage - 100) / 100;
|
||||
}
|
||||
@@ -1,2 +1,20 @@
|
||||
export const PLUS_UPVOTE = 1;
|
||||
export const PLUS_DOWNVOTE = -1;
|
||||
|
||||
export const PLUS_VOTING_CRITERIA = {
|
||||
1: {
|
||||
passPercentage: 60,
|
||||
failPercentage: 40,
|
||||
quota: 50,
|
||||
},
|
||||
2: {
|
||||
passPercentage: 60,
|
||||
failPercentage: 40,
|
||||
quota: 75,
|
||||
},
|
||||
3: {
|
||||
passPercentage: 60,
|
||||
failPercentage: 40,
|
||||
quota: 150,
|
||||
},
|
||||
} as const;
|
||||
|
||||
@@ -55,7 +55,7 @@ export default function PlusVotingResultsPage() {
|
||||
? `, your score was ${result.score}% ${
|
||||
result.betterThan
|
||||
? `(better than ${result.betterThan}% others)`
|
||||
: "(at least 60% required to pass)"
|
||||
: ""
|
||||
}`
|
||||
: ""}
|
||||
</li>
|
||||
|
||||
BIN
db-test.sqlite3
BIN
db-test.sqlite3
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
36
migrations/139-plus-voting-criteria.js
Normal file
36
migrations/139-plus-voting-criteria.js
Normal file
@@ -0,0 +1,36 @@
|
||||
export function up(db) {
|
||||
db.transaction(() => {
|
||||
db.prepare(`drop view "FreshPlusTier"`).run();
|
||||
db.prepare(`drop view "PlusVotingResult"`).run();
|
||||
|
||||
db.prepare(
|
||||
/* sql */ `
|
||||
create view "PlusVotingResult" as
|
||||
select
|
||||
"votedId",
|
||||
"tier",
|
||||
avg("score") as "score",
|
||||
"month",
|
||||
"year",
|
||||
exists (
|
||||
select
|
||||
1
|
||||
from
|
||||
"PlusSuggestion"
|
||||
where
|
||||
"PlusSuggestion"."month" = "PlusVote"."month"
|
||||
and "PlusSuggestion"."year" = "PlusVote"."year"
|
||||
and "PlusSuggestion"."suggestedId" = "PlusVote"."votedId"
|
||||
and "PlusSuggestion"."tier" = "PlusVote"."tier"
|
||||
) as "wasSuggested"
|
||||
from
|
||||
"PlusVote"
|
||||
group by
|
||||
"votedId",
|
||||
"tier",
|
||||
"month",
|
||||
"year";
|
||||
`,
|
||||
).run();
|
||||
})();
|
||||
}
|
||||
Reference in New Issue
Block a user