From f62237dc3fe472c212a1c5bd20a7b05322d5cec3 Mon Sep 17 00:00:00 2001 From: Dilan Gilluly Date: Wed, 6 May 2026 17:44:16 -0400 Subject: [PATCH] API reports and search --- apiserver/src/core/db.py | 5 ++++ apiserver/src/data/models.py | 9 +++++++ apiserver/src/data/repos.py | 40 ++++++++++++++++++++++++++++- apiserver/src/data/routers.py | 44 ++++++++++++++++++++++++++++++-- apiserver/src/reports/repos.py | 23 +++++++++++++++++ apiserver/src/reports/routers.py | 20 +++++++++++++++ apiserver/src/routers.py | 5 ++++ apiserver/src/search/repo.py | 19 ++++++++++++++ apiserver/src/search/routers.py | 12 +++++++++ revproxy/dev/Caddyfile | 3 +++ 10 files changed, 177 insertions(+), 3 deletions(-) create mode 100644 apiserver/src/reports/repos.py create mode 100644 apiserver/src/reports/routers.py create mode 100644 apiserver/src/search/repo.py create mode 100644 apiserver/src/search/routers.py create mode 100644 revproxy/dev/Caddyfile diff --git a/apiserver/src/core/db.py b/apiserver/src/core/db.py index 04723a5..e4091f5 100644 --- a/apiserver/src/core/db.py +++ b/apiserver/src/core/db.py @@ -59,6 +59,11 @@ def init_db(): UNION ALL SELECT 'Total', COUNT(DISTINCT(CONCAT(first_name, last_name, phone_number))), COUNT(*) FROM tickets""") + cur.execute("""CREATE VIEW IF NOT EXISTS v_donors AS + SELECT bd.b_prefix, bd.b_id, bd.d_id, d.donor_name, d.donor_business, b.description + FROM r_basket_donor bd LEFT JOIN donors d ON bd.d_id = d.donor_id + LEFT JOIN baskets b ON bd.b_prefix = b.prefix AND bd.b_id = b.basket_id + ORDER BY bd.b_prefix, bd.b_id""") if config["mode"] != "prod": cur.execute("""REPLACE INTO auth VALUES ('2RO2T7GET9S7X64JUFN67OAV', 'Testing')""") conn.commit() diff --git a/apiserver/src/data/models.py b/apiserver/src/data/models.py index e6f9768..31154bc 100644 --- a/apiserver/src/data/models.py +++ b/apiserver/src/data/models.py @@ -40,6 +40,15 @@ class BasketDonorRel: b_id: int = 0 d_id: int = 0 +@dataclass +class BasketDonorView: + b_prefix: str = "" + b_id: int = 0 + d_id: int = 0 + donor_name: str = "" + donor_business: str = "" + description: str = "" + @dataclass class WinnerByBasket: prefix: str = "" diff --git a/apiserver/src/data/repos.py b/apiserver/src/data/repos.py index 6547770..b600bdf 100644 --- a/apiserver/src/data/repos.py +++ b/apiserver/src/data/repos.py @@ -1,5 +1,5 @@ from ..core.models import RepoTemplate -from .models import Prefix, Ticket, Count, Basket, WinnerByBasket +from .models import Prefix, Ticket, Count, Basket, WinnerByBasket, Donor, BasketDonorView, BasketDonorRel class PrefixRepo(RepoTemplate): @@ -156,3 +156,41 @@ class DrawingRepo(RepoTemplate): winning_ticket = EXCLUDED.winning_ticket""", (b.prefix, b.basket_id, b.winning_ticket)) self.conn.commit() return {"detail": "Winners posted successfully."} + +class DonorRepo(RepoTemplate): + """Repo that controls Donors.""" + + def get_all_donors(self): + """Gets all donors.""" + self.cur.execute("SELECT * FROM donors") + results = self.cur.fetchall() + return [Donor(*r) for r in results] + + def get_donor_relations(self, prefix: str, basket_id: int): + """Get a list of donors for one basket.""" + self.cur.execute("SELECT * FROM v_donors WHERE b_prefix = ? AND b_id = ?", (prefix, basket_id)) + results = self.cur.fetchall() + return [BasketDonorView(*r) for r in results] + + def post_donors(self, ds: list[Donor]): + """Posts donors""" + rtn_lst = [] + for d in ds: + self.cur.execute("INSERT INTO donors (donor_name, donor_business) VALUES (?, ?) RETURNING *", (d.donor_name, d.donor_business)) + rtn_lst.append(Donor(*self.cur.fetchone())) + self.conn.commit() + return rtn_lst + + def post_donor_relation(self, drs: list[BasketDonorRel]): + """Post donor relations.""" + for dr in drs: + self.cur.execute("""INSERT INTO r_basket_donor VALUES (?, ?, ?) ON CONFLICT (b_prefix, b_id, d_id) + DO NOTHING""", (dr.b_prefix, dr.b_id, dr.d_id)) + self.conn.commit() + return {"detail": "Relations posted successfully."} + + def del_donor_relation(self, r: BasketDonorRel): + """Delete donor relation.""" + self.cur.execute("DELETE FROM r_basket_donor WHERE b_prefix = ? AND b_id = ? AND d_id = ?", (r.b_prefix, r.b_id, r.d_id)) + self.conn.commit() + return {"detail": "Relation deleted successfully."} diff --git a/apiserver/src/data/routers.py b/apiserver/src/data/routers.py index 0630912..8181d10 100644 --- a/apiserver/src/data/routers.py +++ b/apiserver/src/data/routers.py @@ -1,8 +1,15 @@ from fastapi import APIRouter, Header, HTTPException, status from ..core.auth import AuthRepo -from .models import Basket, Prefix, Ticket -from .repos import BasketRepo, CountsRepo, DrawingRepo, PrefixRepo, TicketRepo +from .models import Basket, BasketDonorRel, Donor, Prefix, Ticket +from .repos import ( + BasketRepo, + CountsRepo, + DonorRepo, + DrawingRepo, + PrefixRepo, + TicketRepo, +) prefix_router = APIRouter(prefix="/api/prefix") @@ -164,3 +171,36 @@ def get_drawing_scope(prefix: str, s_id: str, tam_auth_key: str = Header("")): def post_winners(bs: list[Basket], tam_auth_key: str = Header("")): AuthRepo().verify_key(tam_auth_key) return DrawingRepo().post_winners(bs) + + +donor_router = APIRouter(prefix="/api/donors") + + +@donor_router.get("") +def get_all_donors(tam_auth_key: str = Header("")): + AuthRepo().verify_key(tam_auth_key) + return DonorRepo().get_all_donors() + + +@donor_router.get("/b/{prefix}/{b_id}") +def get_basket_donors(prefix: str, b_id: int, tam_auth_key: str = Header("")): + AuthRepo().verify_key(tam_auth_key) + return DonorRepo().get_donor_relations(prefix, b_id) + + +@donor_router.post("/new") +def post_donors(ds: list[Donor], tam_auth_key: str = Header("")): + AuthRepo().verify_key(tam_auth_key) + return DonorRepo().post_donors(ds) + + +@donor_router.post("/rel") +def post_donor_relations(drs: list[BasketDonorRel], tam_auth_key: str = Header("")): + AuthRepo().verify_key(tam_auth_key) + return DonorRepo().post_donor_relation(drs) + + +@donor_router.delete("/rel") +def delete_donor_relation(r: BasketDonorRel, tam_auth_key: str = Header("")): + AuthRepo().verify_key(tam_auth_key) + return DonorRepo().del_donor_relation(r) diff --git a/apiserver/src/reports/repos.py b/apiserver/src/reports/repos.py new file mode 100644 index 0000000..f43d385 --- /dev/null +++ b/apiserver/src/reports/repos.py @@ -0,0 +1,23 @@ +from ..core.models import RepoTemplate +from ..data.models import WinnerByBasket, WinnerByName, BasketDonorView + +class ReportsRepo(RepoTemplate): + """Repo that controls all report functions.""" + + def get_winners_by_basket(self, prefix: str): + """Gets prefix winners by basket.""" + self.cur.execute("SELECT * FROM winners_by_basket WHERE prefix = ?", (prefix,)) + results = self.cur.fetchall() + return [WinnerByBasket(*r) for r in results] + + def get_winners_by_name(self, prefix: str): + """Gets prefix winners by name.""" + self.cur.execute("SELECT * FROM winners_by_name WHERE prefix = ?", (prefix,)) + results = self.cur.fetchall() + return [WinnerByName(*r) for r in results] + + def get_donor_contributions(self): + """Gets all donor contributions.""" + self.cur.execute("SELECT * FROM v_donors") + results = self.cur.fetchall() + return [BasketDonorView(*r) for r in results] diff --git a/apiserver/src/reports/routers.py b/apiserver/src/reports/routers.py new file mode 100644 index 0000000..cec6942 --- /dev/null +++ b/apiserver/src/reports/routers.py @@ -0,0 +1,20 @@ +from fastapi import APIRouter, Header +from .repos import ReportsRepo +from ..core.auth import AuthRepo + +reports_router = APIRouter(prefix="/api/reports") + +@reports_router.get("/winners/bybasket/{prefix}") +def get_winners_by_basket(prefix: str, tam_auth_key: str = Header("")): + AuthRepo().verify_key(tam_auth_key) + return ReportsRepo().get_winners_by_basket(prefix) + +@reports_router.get("/winners/byname/{prefix}") +def get_winners_by_name(prefix: str, tam_auth_key: str = Header("")): + AuthRepo().verify_key(tam_auth_key) + return ReportsRepo().get_winners_by_name(prefix) + +@reports_router.get("/donors/contrib") +def get_donor_contributions(tam_auth_key: str = Header("")): + AuthRepo().verify_key(tam_auth_key) + return ReportsRepo().get_donor_contributions() diff --git a/apiserver/src/routers.py b/apiserver/src/routers.py index 6d3603c..b216b6f 100644 --- a/apiserver/src/routers.py +++ b/apiserver/src/routers.py @@ -2,6 +2,8 @@ from fastapi import FastAPI from .core.auth import auth_router from .data import routers +from .reports import routers as report_routers +from .search import routers as search_routers def append_routers(app: FastAPI): app.include_router(auth_router) @@ -10,3 +12,6 @@ def append_routers(app: FastAPI): app.include_router(routers.counts_router) app.include_router(routers.basket_router) app.include_router(routers.drawing_router) + app.include_router(routers.donor_router) + app.include_router(report_routers.reports_router) + app.include_router(search_routers.search_router) diff --git a/apiserver/src/search/repo.py b/apiserver/src/search/repo.py new file mode 100644 index 0000000..20ce152 --- /dev/null +++ b/apiserver/src/search/repo.py @@ -0,0 +1,19 @@ +from ..core.models import RepoTemplate +from ..data.models import Ticket + + +class SearchRepo(RepoTemplate): + """Repo that controls search.""" + + def search_tickets( + self, first_name: str = "", last_name: str = "", phone_number: str = "" + ): + """Searches tickets for a specific name.""" + self.cur.execute( + """SELECT * FROM tickets + WHERE first_name LIKE ? AND last_name LIKE ? AND phone_number LIKE ? + ORDER BY prefix, ticket_id""", + (f"%{first_name}%", f"%{last_name}%", f"%{phone_number}%"), + ) + results = self.cur.fetchall() + return [Ticket(*r) for r in results] diff --git a/apiserver/src/search/routers.py b/apiserver/src/search/routers.py new file mode 100644 index 0000000..c91357b --- /dev/null +++ b/apiserver/src/search/routers.py @@ -0,0 +1,12 @@ +from fastapi import APIRouter, Header +from .repo import SearchRepo +from ..core.auth import AuthRepo + +search_router = APIRouter(prefix="/api/search") + +@search_router.get("/tickets") +def search_tickets( + first_name: str = "", last_name: str = "", phone_number: str = "", tam_auth_key: str = Header("") +): + AuthRepo().verify_key(tam_auth_key) + return SearchRepo().search_tickets(first_name, last_name, phone_number) diff --git a/revproxy/dev/Caddyfile b/revproxy/dev/Caddyfile new file mode 100644 index 0000000..99fe6d2 --- /dev/null +++ b/revproxy/dev/Caddyfile @@ -0,0 +1,3 @@ +localhost:8443 { + reverse_proxy localhost:8000 +}