mirror of
https://github.com/Sendouc/sendou.ink.git
synced 2026-05-09 04:02:40 -05:00
Merge c870e3909a into 7d60fcc4d1
This commit is contained in:
commit
8553df2e99
|
|
@ -1094,14 +1094,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>;
|
||||
|
|
@ -434,7 +429,6 @@ export interface PlusVotingResult {
|
|||
month: number;
|
||||
year: number;
|
||||
wasSuggested: DBBoolean;
|
||||
passedVoting: DBBoolean;
|
||||
}
|
||||
|
||||
export interface ReportedWeapon {
|
||||
|
|
@ -1346,7 +1340,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)
|
||||
|
|
@ -31,26 +32,88 @@ type ResultsByMonthYearQueryReturnType = InferResult<
|
|||
>;
|
||||
|
||||
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 }[]>;
|
||||
return (
|
||||
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()
|
||||
// CLAUDETODO: don't use then, just await (assign the value to intermediate variable)
|
||||
.then((rows) => {
|
||||
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>
|
||||
|
|
|
|||
36
migrations/127-plus-voting-criteria.js
Normal file
36
migrations/127-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();
|
||||
})();
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user