From 63060cb1490fa1a17d94bb25caebed691a9faab9 Mon Sep 17 00:00:00 2001 From: Dilan Gilluly Date: Thu, 25 Sep 2025 23:11:52 -0400 Subject: [PATCH] nightly - 2025-09-25 --- api/main.py | 6 +- api/repos/baskets.py | 43 ++++++ api/repos/combined.py | 43 ++++++ api/routers/baskets.py | 36 +++++ api/routers/combined.py | 36 +++++ db/schema.sql | 5 +- webapp/src/lib/components/FormHeader.svelte | 34 +++-- webapp/src/routes/api/baskets/+server.js | 43 ++++++ .../[prefix]/[b_from]/[b_to]/+server.js | 35 +++++ webapp/src/routes/api/prefixes/+server.js | 1 - webapp/src/routes/baskets/[prefix]/+page.js | 6 + .../src/routes/baskets/[prefix]/+page.svelte | 141 ++++++++++++++++++ .../src/routes/tickets/[prefix]/+page.svelte | 17 +-- 13 files changed, 419 insertions(+), 27 deletions(-) create mode 100644 api/repos/baskets.py create mode 100644 api/repos/combined.py create mode 100644 api/routers/baskets.py create mode 100644 api/routers/combined.py create mode 100644 webapp/src/routes/api/baskets/+server.js create mode 100644 webapp/src/routes/api/baskets/[prefix]/[b_from]/[b_to]/+server.js create mode 100644 webapp/src/routes/baskets/[prefix]/+page.js create mode 100644 webapp/src/routes/baskets/[prefix]/+page.svelte diff --git a/api/main.py b/api/main.py index b2f5ca3..646f243 100644 --- a/api/main.py +++ b/api/main.py @@ -5,6 +5,8 @@ from sys import argv from routers.prefixes import prefix_router from routers.tickets import ticket_router +from routers.baskets import basket_router +from routers.combined import combined_router if argv[1] == "run": app = FastAPI(title="TAM3 API Server", docs_url=None, redoc_url=None) @@ -12,4 +14,6 @@ else: app = FastAPI(title="TAM3 API Server") app.include_router(prefix_router) -app.include_router(ticket_router) \ No newline at end of file +app.include_router(ticket_router) +app.include_router(basket_router) +app.include_router(combined_router) \ No newline at end of file diff --git a/api/repos/baskets.py b/api/repos/baskets.py new file mode 100644 index 0000000..28f04dc --- /dev/null +++ b/api/repos/baskets.py @@ -0,0 +1,43 @@ +from dataclasses import dataclass +from .template import Repo + +@dataclass +class Basket: + prefix: str + b_id: int + description: str = "" + donors: str = "" + winning_ticket: int = 0 + changed: bool = False + +class BasketRepo(Repo): + def get_prefix_one(self, prefix: str, b_id: int): + self.cur.execute("SELECT * FROM baskets WHERE prefix = %s AND b_id = %s", (prefix, b_id)) + result = self.cur.fetchone() + if not result: + return Basket(prefix, b_id) + return Basket(*result) + def get_prefix_range(self, prefix: str, b_from: int, b_to: int): + r_dict = {i: Basket(prefix, i) for i in range(b_from, b_to+1)} + self.cur.execute("SELECT * FROM baskets WHERE prefix = %s AND b_id BETWEEN %s AND %s", (prefix, b_from, b_to)) + results = self.cur.fetchall() + for r in results: + r_dict[r[1]] = Basket(*r) + return list(r_dict.values()) + def get_prefix_all(self, prefix: str): + self.cur.execute("SELECT * FROM baskets WHERE prefix = %s", (prefix,)) + results = self.cur.fetchall() + if not results: + return [] + return [Basket(*r) for r in results] + def get_all(self): + self.cur.execute("SELECT * FROM baskets") + results = self.cur.fetchall() + if not results: + return [] + return [Basket(*r) for r in results] + def post_list(self, baskets: list[Basket]): + for b in baskets: + self.cur.execute("REPLACE INTO baskets VALUES (%s, %s, %s, %s, %s)", (b.prefix, b.b_id, b.description, b.donors, b.winning_ticket)) + self.conn.commit() + return {"detail": "Baskets posted successfully."} \ No newline at end of file diff --git a/api/repos/combined.py b/api/repos/combined.py new file mode 100644 index 0000000..a707290 --- /dev/null +++ b/api/repos/combined.py @@ -0,0 +1,43 @@ +from dataclasses import dataclass +from .template import Repo + +@dataclass +class Combined: + prefix: str + b_id: int + winning_ticket: int = 0 + winner: str = ", " + changed: bool = False + +class CombinedRepo(Repo): + def get_prefix_one(self, prefix: str, b_id: int) -> Combined: + self.cur.execute("SELECT * FROM combined WHERE prefix = %s AND b_id = %s", (prefix, b_id)) + result = self.cur.fetchone() + if not result: + return Combined(prefix, b_id) + return Combined(*result) + def get_prefix_range(self, prefix: str, b_from: int, b_to: int) -> list[Combined]: + r_dict = {i: Combined(prefix, i) for i in range(b_from, b_to+1)} + self.cur.execute("SELECT * FROM combined WHERE prefix = %s AND b_id BETWEEN %s AND %s", (prefix, b_from, b_to)) + results = self.cur.fetchall() + for b in results: + r_dict[b[1]] = Combined(*b) + return list(r_dict.values()) + def get_prefix_all(self, prefix:str) -> list[Combined]: + self.cur.execute("SELECT * FROM combined WHERE prefix = %s", (prefix,)) + results = self.cur.fetchall() + if not results: + return [] + return [Combined(*r) for r in results] + def get_all(self) -> list[Combined]: + self.cur.execute("SELECT * FROM combined") + results = self.cur.fetchall() + if not results: + return [] + return [Combined(*r) for r in results] + def post_list(self, c_entries: list[Combined]): + for combined in c_entries: + self.cur.execute("INSERT INTO baskets (prefix, b_id, winning_ticket) VALUES (%s, %s, %s) ON DUPLICATE KEY UPDATE winning_ticket=%s", + (combined.prefix, combined.b_id, combined.winning_ticket, combined.winning_ticket)) + self.conn.commit() + return {"detail": "Winners posted successfully"} \ No newline at end of file diff --git a/api/routers/baskets.py b/api/routers/baskets.py new file mode 100644 index 0000000..1b8c4f1 --- /dev/null +++ b/api/routers/baskets.py @@ -0,0 +1,36 @@ +from fastapi import APIRouter +from exceptions import bad_key +from repos.api_keys import ApiKeyRepo +from repos.baskets import Basket, BasketRepo + +basket_router = APIRouter(prefix="/api/baskets") + +@basket_router.get("/") +def get_all_baskets(api_key: str) -> list[Basket]: + if not ApiKeyRepo().check_api(api_key): + raise bad_key + return BasketRepo().get_all() + +@basket_router.get("/{prefix}/") +def get_prefix_baskets(api_key: str, prefix: str) -> list[Basket]: + if not ApiKeyRepo().check_api(api_key): + raise bad_key + return BasketRepo().get_prefix_all(prefix) + +@basket_router.get("/{prefix}/{b_id}/") +def get_prefix_basket_one(api_key: str, prefix: str, b_id: int) -> Basket: + if not ApiKeyRepo().check_api(api_key): + raise bad_key + return BasketRepo().get_prefix_one(prefix, b_id) + +@basket_router.get("/{prefix}/{b_from}/{b_to}/") +def get_prefix_basket_range(api_key: str, prefix: str, b_from: int, b_to: int) -> list[Basket]: + if not ApiKeyRepo().check_api(api_key): + raise bad_key + return BasketRepo().get_prefix_range(prefix, b_from, b_to) + +@basket_router.post("/") +def post_basket_list(api_key: str, baskets: list[Basket]): + if not ApiKeyRepo().check_api(api_key): + raise bad_key + return BasketRepo().post_list(baskets) \ No newline at end of file diff --git a/api/routers/combined.py b/api/routers/combined.py new file mode 100644 index 0000000..da5fba5 --- /dev/null +++ b/api/routers/combined.py @@ -0,0 +1,36 @@ +from fastapi import APIRouter +from repos.combined import Combined, CombinedRepo +from repos.api_keys import ApiKeyRepo +from exceptions import bad_key + +combined_router = APIRouter(prefix="/api/combined") + +@combined_router.get("/") +def get_all_combined(api_key: str) -> list[Combined]: + if not ApiKeyRepo().check_api(api_key): + raise bad_key + return CombinedRepo().get_all() + +@combined_router.get("/{prefix}/") +def get_prefix_combined(api_key: str, prefix: str) -> list[Combined]: + if not ApiKeyRepo().check_api(api_key): + raise bad_key + return CombinedRepo().get_prefix_all(prefix) + +@combined_router.get("/{prefix}/{b_id}/") +def get_prefix_combined_one(api_key: str, prefix: str, b_id: int) -> Combined: + if not ApiKeyRepo().check_api(api_key): + raise bad_key + return CombinedRepo().get_prefix_one(prefix, b_id) + +@combined_router.get("/{prefix}/{b_from}/{b_to}/") +def get_prefix_combined_range(api_key: str, prefix: str, b_from: int, b_to: int) -> list[Combined]: + if not ApiKeyRepo().check_api(api_key): + raise bad_key + return CombinedRepo().get_prefix_range(prefix, b_from, b_to) + +@combined_router.post("/") +def post_combined_range(api_key: str, winner_list: list[Combined]): + if not ApiKeyRepo().check_api(api_key): + raise bad_key + return CombinedRepo().post_list(winner_list) \ No newline at end of file diff --git a/db/schema.sql b/db/schema.sql index 76325cb..79ba527 100644 --- a/db/schema.sql +++ b/db/schema.sql @@ -4,7 +4,7 @@ CREATE TABLE IF NOT EXISTS api_keys ( ); CREATE TABLE IF NOT EXISTS prefixes ( - `name` VARCHAR(255), + `name` VARCHAR(255) PRIMARY KEY, `color` VARCHAR(255), `weight` INT DEFAULT 0 ); @@ -24,7 +24,8 @@ CREATE TABLE IF NOT EXISTS baskets ( `b_id` INT, `description` VARCHAR(255), `donors` VARCHAR(255), - `winning_ticket` INT + `winning_ticket` INT, + PRIMARY KEY (`prefix`, `b_id`) ); CREATE VIEW IF NOT EXISTS combined AS diff --git a/webapp/src/lib/components/FormHeader.svelte b/webapp/src/lib/components/FormHeader.svelte index e3f94c3..edde6e7 100644 --- a/webapp/src/lib/components/FormHeader.svelte +++ b/webapp/src/lib/components/FormHeader.svelte @@ -1,9 +1,25 @@
@@ -15,21 +31,21 @@
- - + +
- - - - - - + + + + + +
- +
diff --git a/webapp/src/routes/api/baskets/+server.js b/webapp/src/routes/api/baskets/+server.js new file mode 100644 index 0000000..a1c77b4 --- /dev/null +++ b/webapp/src/routes/api/baskets/+server.js @@ -0,0 +1,43 @@ +import { env } from "$env/dynamic/private"; +import { db } from "$lib/server/db"; +import { baskets } from "$lib/server/db/schema"; + +export async function GET() { + if (env.TAM3_REMOTE) { + const res = await fetch(`${env.TAM3_REMOTE}/api/baskets/?api_key=${env.TAM3_REMOTE_KEY}`); + if (!res.ok) { + return new Response(JSON.stringify({"detail": "Unable to fetch Baskets"}), {status: res.status, statusText: res.statusText}) + }; + const data = await res.json(); + return new Response(JSON.stringify(data), { + headers: {'Content-Type': 'application/json'}, + status: 200, + statusText: "Baskets fetched successfully." + }) + } else { + const data = await db.select().from(baskets); + return new Response(JSON.stringify(data), { + headers: {'Content-Type': 'application/json'}, + status: 200, + statusText: "Baskets loaded successfully." + }) + } +} + +export async function POST({ request }) { + const i_baskets = await request.json(); + for (let basket of i_baskets) { + await db.insert(baskets).values({prefix: basket.prefix, b_id: basket.b_id, description: basket.description, donors: basket.donors, winning_ticket: basket.winning_ticket}) + .onConflictDoUpdate({target: [baskets.prefix, baskets.b_id], set: {description: basket.description, donors: basket.donors, winning_ticket: basket.winning_ticket}}) + }; + if (env.TAM3_REMOTE) { + const res = await fetch(`${env.TAM3_REMOTE}/api/baskets/?api_key=${env.TAM3_REMOTE_KEY}`, { + body: JSON.stringify([...i_baskets]), method: 'POST', headers: {'Content-Type': 'application/json'} + }); + if (!res.ok) { + return new Response(JSON.stringify({details: "Issue posting baskets to remote."}), {status: res.status, statusText: res.statusText}) + }; + const data = await res.json(); + }; + return new Response(JSON.stringify({details: "Posted baskets successfully."}), {status: 200, statusText: "Posted baskets successfully."}) +} \ No newline at end of file diff --git a/webapp/src/routes/api/baskets/[prefix]/[b_from]/[b_to]/+server.js b/webapp/src/routes/api/baskets/[prefix]/[b_from]/[b_to]/+server.js new file mode 100644 index 0000000..56bf0ec --- /dev/null +++ b/webapp/src/routes/api/baskets/[prefix]/[b_from]/[b_to]/+server.js @@ -0,0 +1,35 @@ +import { env } from "$env/dynamic/private"; +import { db } from "$lib/server/db"; +import { baskets } from "$lib/server/db/schema"; +import { eq, and } from "drizzle-orm"; + +export async function GET({ params }) { + let n_b_from = parseInt(params.b_from), n_b_to = parseInt(params.b_to); + if (env.TAM3_REMOTE) { + const res = await fetch(`${env.TAM3_REMOTE}/api/baskets/${n_b_from}/${n_b_to}/?api_key=${env.TAM3_REMOTE_KEY}`); + if (!res.ok) { + return new Response(JSON.stringify({detail: "Unable to fetch baskets"}), {status: res.status, statusText: res.statusText}); + }; + const data = await res.json(); + return new Response(JSON.stringify(data), { + headers: {'Content-Type': 'application/json'}, + status: 200, + statusText: "Baskets fetched successfully" + }) + } else { + let r_dict = {}; + for (let i=n_b_from; i <= n_b_to; i++) { + let data = await db.select().from(baskets).where(and(eq(baskets.prefix, params.prefix), eq(baskets.b_id, i))); + if (data[0]) { + r_dict[i] = {...data[0]}; + } else { + r_dict[i] = {prefix: params.prefix, b_id: i, description: "", donors: "", winning_ticket: 0, changed: false}; + } + }; + return new Response(JSON.stringify(Object.values(r_dict)), { + headers: {'Content-Type': 'application/json'}, + status: 200, + statusText: "Baskets loaded successfully." + }) + } +} \ No newline at end of file diff --git a/webapp/src/routes/api/prefixes/+server.js b/webapp/src/routes/api/prefixes/+server.js index d39f889..b8f2d87 100644 --- a/webapp/src/routes/api/prefixes/+server.js +++ b/webapp/src/routes/api/prefixes/+server.js @@ -18,7 +18,6 @@ export async function GET() { export async function POST({ request }) { const { name, color, weight } = await request.json(); - console.log({name, color, weight}) await db.insert(prefixes).values({name: name, color: color, weight: weight}).onConflictDoUpdate({target: prefixes.name, set: {color: color, weight: weight}}); if (env.TAM3_REMOTE) { const res = await fetch(`${env.TAM3_REMOTE}/api/prefixes/?api_key=${env.TAM3_REMOTE_KEY}`, { diff --git a/webapp/src/routes/baskets/[prefix]/+page.js b/webapp/src/routes/baskets/[prefix]/+page.js new file mode 100644 index 0000000..25da94f --- /dev/null +++ b/webapp/src/routes/baskets/[prefix]/+page.js @@ -0,0 +1,6 @@ +export async function load({ fetch, params }) { + const { prefix } = await params; + const res = await fetch(`/api/prefixes/${prefix}`); + const prefix_data = await res.json(); + return {prefix: prefix_data} +} \ No newline at end of file diff --git a/webapp/src/routes/baskets/[prefix]/+page.svelte b/webapp/src/routes/baskets/[prefix]/+page.svelte new file mode 100644 index 0000000..12f3723 --- /dev/null +++ b/webapp/src/routes/baskets/[prefix]/+page.svelte @@ -0,0 +1,141 @@ + + +

{prefix.name} Basket Entry

+ + + + + + + + + + + + {#each current_baskets as basket, idx} + current_idx = idx}> + + + + + + {/each} + +
Basket IDDescriptionDonorsChanged
{basket.b_id} basket.changed = true} bind:value={basket.description}> basket.changed = true} bind:value={basket.donors}>
+ + \ No newline at end of file diff --git a/webapp/src/routes/tickets/[prefix]/+page.svelte b/webapp/src/routes/tickets/[prefix]/+page.svelte index abee7b9..9bae2ae 100644 --- a/webapp/src/routes/tickets/[prefix]/+page.svelte +++ b/webapp/src/routes/tickets/[prefix]/+page.svelte @@ -1,7 +1,6 @@