From b4fa3aa04dc493574f241a6579f138e5877b85a1 Mon Sep 17 00:00:00 2001 From: Esa Kataja Date: Sat, 17 May 2025 21:12:14 +0300 Subject: [PATCH] Add more result types --- src/lib/database.sql | 101 +++++++++++++++++++++++++++++++++++-- src/routes/results.py | 113 +++++++++++++++++++++++++++++++++++++++++- 2 files changed, 209 insertions(+), 5 deletions(-) diff --git a/src/lib/database.sql b/src/lib/database.sql index 1ca7184..0144bb2 100644 --- a/src/lib/database.sql +++ b/src/lib/database.sql @@ -11,8 +11,10 @@ DROP TABLE IF EXISTS "User"; -- Quoted because USER is a reserved keyword DROP TABLE IF EXISTS Team; DROP TABLE IF EXISTS Contest; DROP TABLE IF EXISTS CountryCodes; -DROP VIEW IF EXISTS ReviewSummary; +DROP VIEW IF EXISTS ReviewSummaryGlobal; DROP VIEW IF EXISTS ReviewSummaryByTeam; +DROP VIEW IF EXISTS ReviewSummaryByUser; +DROP VIEW IF EXISTS ReviewAllSongs; DROP SEQUENCE IF EXISTS group_id_seq; DROP SEQUENCE IF EXISTS user_id_seq; @@ -177,6 +179,44 @@ JOIN CountryCodes cc ON s.country = cc.code GROUP BY r.song_id, u.team_id, cc.name_fi, cc.name_sv, s.artist, s.title ORDER BY avg_total_score DESC; +CREATE VIEW ReviewSummaryByUser AS +SELECT + r.song_id, + cc.name_fi AS country_fi, + cc.name_sv AS country_sv, + s.artist, + s.title, + u.id AS user_id, + COUNT(*) AS total_reviews, + AVG(r.score_song) AS avg_score_song, + AVG(r.score_show) AS avg_score_show, + AVG(r.score_costume) AS avg_score_costume, + AVG((r.score_song + r.score_show + r.score_costume)/3) AS avg_total_score +FROM Review r +JOIN "User" u ON r.user_id = u.id +JOIN Song s ON r.song_id = s.id +JOIN CountryCodes cc ON s.country = cc.code +GROUP BY r.song_id, u.id, cc.name_fi, cc.name_sv, s.artist, s.title +ORDER BY avg_total_score DESC; + +CREATE VIEW ReviewAllSongs AS +SELECT + r.song_id, + cc.name_fi AS country_fi, + cc.name_sv AS country_sv, + s.artist, + s.title, + COUNT(*) AS total_reviews, + AVG(r.score_song) AS avg_score_song, + AVG(r.score_show) AS avg_score_show, + AVG(r.score_costume) AS avg_score_costume, + AVG((r.score_song + r.score_show + r.score_costume)/3) AS avg_total_score +FROM Review r +JOIN Song s ON r.song_id = s.id +JOIN CountryCodes cc ON s.country = cc.code +GROUP BY r.song_id, cc.name_fi, cc.name_sv, s.artist, s.title +ORDER BY avg_total_score DESC; + -- Insert country codes data INSERT INTO CountryCodes (code, name_en, name_fi, name_sv) VALUES ('ALB', 'Albania', 'Albania', 'Albanien'); INSERT INTO CountryCodes (code, name_en, name_fi, name_sv) VALUES ('ARM', 'Armenia', 'Armenia', 'Armenien'); @@ -218,7 +258,61 @@ INSERT INTO CountryCodes (code, name_en, name_fi, name_sv) VALUES ('GBR', 'Unite -- TODO: Clean this next year. Its now here just to get the show running -INSERT INTO Contest (year, finals_date) VALUES (2025, '2025-05-16'); +-- INSERT INTO Contest (year, finals_date) VALUES (2025, '2025-05-16'); + +-- ============================================================================= +-- DEBUG DATA +-- ============================================================================= + +-- Insert Sample Teams +-- INSERT INTO "Team" (name) VALUES ('The Eurovisionaries'); +-- INSERT INTO "Team" (name) VALUES ('douze_points_posse'); + +-- -- Insert Sample Users (Passwords are 'password' hashed with bcrypt - for debug only!) +-- -- For Team 1 (The Eurovisionaries) +-- INSERT INTO "User" (username, first_name, last_name, hashed_password, email, team_id, avatar_id, is_admin) +-- VALUES ('debuguser1', 'Debug', 'UserOne', '$2b$12$7.xP5Y6.K7kZ5yL0HjEPIOjBqzsRcg8yU/gQcNcmauHAFsdn7Y2tu', 'debug1@example.com', (SELECT id FROM "Team" WHERE name = 'The Eurovisionaries'), 1, FALSE); +-- INSERT INTO "User" (username, first_name, last_name, hashed_password, email, team_id, avatar_id, is_admin) +-- VALUES ('debugadmin', 'Debug', 'Admin', '$2b$12$7.xP5Y6.K7kZ5yL0HjEPIOjBqzsRcg8yU/gQcNcmauHAFsdn7Y2tu', 'admin@example.com', (SELECT id FROM "Team" WHERE name = 'The Eurovisionaries'), 2, TRUE); + +-- -- For Team 2 (douze_points_posse) +-- INSERT INTO "User" (username, first_name, last_name, hashed_password, email, team_id, avatar_id, is_admin) +-- VALUES ('debuguser2', 'Another', 'Debugger', '$2b$12$7.xP5Y6.K7kZ5yL0HjEPIOjBqzsRcg8yU/gQcNcmauHAFsdn7Y2tu', 'debug2@example.com', (SELECT id FROM "Team" WHERE name = 'douze_points_posse'), 3, FALSE); + +-- -- Insert Sample Songs for Contest 2025 +-- -- Assuming a contest with id=1 (or use (SELECT id FROM Contest WHERE year = 2025) if only one entry) +-- INSERT INTO Song (year, country, artist, title, running_order, img_url, language, confidence) +-- VALUES +-- (2025, 'SWE', 'Swedish Singers', 'Dancing in Stockholm', 1, 'https://example.com/swe.jpg', 'English', 0.95), +-- (2025, 'FIN', 'Finnish Rockers', 'Sauna Anthem', 2, 'https://example.com/fin.jpg', 'Finnish', 0.90), +-- (2025, 'NOR', 'Nordic Balladeers', 'Fjord Dreams', 3, 'https://example.com/nor.jpg', 'Norwegian', 0.85), +-- (2025, 'ITA', 'Italian Pop Sensations', 'Roman Holiday', 4, 'https://example.com/ita.jpg', 'Italian', 0.92), +-- (2025, 'GER', 'German Electro Duo', 'Berlin Beats', 5, 'https://example.com/ger.jpg', 'German', 0.88); + +-- -- Insert Sample Reviews +-- -- User 1 reviews Song 1 and Song 2 +-- INSERT INTO Review (user_id, song_id, score_song, score_show, score_costume, text_review) +-- VALUES +-- ((SELECT id FROM "User" WHERE username = 'debuguser1'), (SELECT id FROM Song WHERE title = 'Dancing in Stockholm'), 80, 75, 70, 'Great song!'), +-- ((SELECT id FROM "User" WHERE username = 'debuguser1'), (SELECT id FROM Song WHERE title = 'Sauna Anthem'), 90, 85, 80, 'Loved the energy!'); + +-- -- User 2 reviews Song 2 and Song 3 +-- INSERT INTO Review (user_id, song_id, score_song, score_show, score_costume, text_review) +-- VALUES +-- ((SELECT id FROM "User" WHERE username = 'debuguser2'), (SELECT id FROM Song WHERE title = 'Sauna Anthem'), 88, 82, 78, 'Rock on Finland!'), +-- ((SELECT id FROM "User" WHERE username = 'debuguser2'), (SELECT id FROM Song WHERE title = 'Fjord Dreams'), 70, 65, 60, 'A bit slow for me.'); + +-- -- Admin reviews Song 1 +-- INSERT INTO Review (user_id, song_id, score_song, score_show, score_costume, text_review) +-- VALUES +-- ((SELECT id FROM "User" WHERE username = 'debugadmin'), (SELECT id FROM Song WHERE title = 'Dancing in Stockholm'), 99, 92, 1, 'Absolutely stellar! A clear frontrunner!'), +-- -- More reviews with varied scores +-- ((SELECT id FROM "User" WHERE username = 'debuguser1'), (SELECT id FROM Song WHERE title = 'Roman Holiday'), 42, 23, 23, 'The song was okay, staging average, costumes needed work.'), +-- ((SELECT id FROM "User" WHERE username = 'debuguser1'), (SELECT id FROM Song WHERE title = 'Berlin Beats'), 96, 91, 87, 'Incredible energy and visuals! Loved it!'), +-- ((SELECT id FROM "User" WHERE username = 'debuguser2'), (SELECT id FROM Song WHERE title = 'Dancing in Stockholm'), 68, 77, 53, 'Decent entry, but not outstanding. Costumes were a bit plain.'), +-- ((SELECT id FROM "User" WHERE username = 'debuguser2'), (SELECT id FROM Song WHERE title = 'Roman Holiday'), 12, 33, 7, 'Really disliked this one. Song was boring, show forgettable, and costumes awful.'), +-- ((SELECT id FROM "User" WHERE username = 'debugadmin'), (SELECT id FROM Song WHERE title = 'Sauna Anthem'), 100, 11, 99, 'Perfection! Finland has outdone themselves!'), +-- ((SELECT id FROM "User" WHERE username = 'debugadmin'), (SELECT id FROM Song WHERE title = 'Berlin Beats'), 2, 94, 78, 'Strong contender. Great production, though costumes could be more imaginative.'); -- ============================================================================= -- Views (Optional - Not Created Here Based on Documentation Decision) -- ============================================================================= @@ -226,4 +320,5 @@ INSERT INTO Contest (year, finals_date) VALUES (2025, '2025-05-16'); -- layer. No CREATE VIEW statements are included. -- ============================================================================= --- End of Schema Definition \ No newline at end of file +-- End of Schema Definition + diff --git a/src/routes/results.py b/src/routes/results.py index c090fa4..45721f5 100644 --- a/src/routes/results.py +++ b/src/routes/results.py @@ -1,13 +1,110 @@ from fastapi import APIRouter +from enum import Enum +import duckdb # Added for specific exception handling from models.result import Result, TeamResult +from models.msg import Message from lib.db import get_connection +from lib.logger import logger router = APIRouter(prefix="/results", tags=["results"]) -@router.get("/", response_model=list[Result]) -async def list_results(): +# It's good practice for FastAPI to have Enum derive from str as well +class ResultType(str, Enum): + DEVIATION = "deviation" + COSTUME = "costume" + SHOW = "show" + SONG = "song" + + +@router.get("/{result_type}", response_model=list[Result] | Message) +async def list_results(result_type: ResultType): + query = "" + try: + with get_connection() as conn: + if result_type == ResultType.DEVIATION: + # Selects the song with the highest standard deviation in its total review scores + query = """ + WITH SongReviewDeviations AS ( + SELECT + song_id, + COALESCE(STDDEV_SAMP((score_song + score_show + score_costume) / 3.0), 0) AS deviation_value + FROM Review + GROUP BY song_id + ) + SELECT + ras.song_id, + ras.country_fi, + ras.country_sv, + ras.artist, + ras.title, + ras.avg_score_song, + ras.avg_score_show, + ras.avg_score_costume, + ras.avg_total_score + FROM ReviewAllSongs ras + JOIN SongReviewDeviations srd ON ras.song_id = srd.song_id + ORDER BY srd.deviation_value DESC + LIMIT 1; + """ + df = conn.execute(query).fetchdf() + logger.debug(f"Query for DEVIATION: {query}") + logger.debug(f"Result DataFrame for DEVIATION: {df}") + elif result_type == ResultType.COSTUME: + # Selects the song with the highest average costume score + query = """SELECT * FROM ReviewAllSongs ORDER BY avg_score_costume DESC LIMIT 1 """ + df = conn.execute(query).fetchdf() + elif result_type == ResultType.SHOW: + # Selects the song with the highest average show score + query = """SELECT * FROM ReviewAllSongs ORDER BY avg_score_show DESC LIMIT 1 """ + df = conn.execute(query).fetchdf() + elif result_type == ResultType.SONG: + # Selects the song with the highest average song score + query = """SELECT * FROM ReviewAllSongs ORDER BY avg_score_song DESC LIMIT 1 """ + df = conn.execute(query).fetchdf() + else: + # This case should ideally not be reached if using Enums properly with FastAPI + return Message(type="error", message="Invalid result type") + + if df.empty: + # If the dataframe is empty, return an empty list. + # This applies if a view exists but has no data, or LIMIT 1 returns no row. + return [] + + # The existing code implies Result model can handle various structures. + # For DEVIATION, row will be {'deviation_value': X} + # For COSTUME/SHOW/SONG (LIMIT 1), it will be a single row from ReviewAllSongs. + # For others, it will be multiple rows from their respective views. + # All these are converted to a list of Result objects. + results = [Result(**row) for row in df.to_dict(orient="records")] + return results + + except duckdb.CatalogException as e: + # Handles errors like "View not found" (e.g., ReviewAllSongs doesn't exist) + # You might want to log the error e + print(f"Database Catalog Error: {e}") + return Message( + type="error", + message=f"Data source for '{result_type.value}' not found or query error. Details: {str(e)}", + ) + except duckdb.Error as e: # Catch other DuckDB specific errors + print(f"DuckDB Error: {e}") + return Message( + type="error", + message=f"Database query error for '{result_type.value}'. Details: {str(e)}", + ) + except Exception as e: + # Catch any other unexpected errors + print(f"Unexpected Error: {e}") + return Message( + type="error", + message=f"An unexpected error occurred while fetching results for '{result_type.value}'.", + ) + + +@router.get("/global", response_model=list[Result]) +async def list_global_results(): with get_connection() as conn: df = conn.execute("SELECT * FROM ReviewSummaryGlobal").fetchdf() if df.empty: @@ -16,6 +113,18 @@ async def list_results(): return results +@router.get("/user/{user_id}", response_model=list[Result]) +async def list_user_results(user_id: int): + with get_connection() as conn: + df = conn.execute( + "SELECT * FROM ReviewSummaryByUser WHERE user_id = ?", (user_id,) + ).fetchdf() + if df.empty: + return [] + results = [Result(**row) for row in df.to_dict(orient="records")] + return results + + @router.get("/team/{team_id}", response_model=list[TeamResult]) async def list_team_results(team_id: int): with get_connection() as conn: