Add more result types

This commit is contained in:
Esa Kataja
2025-05-17 21:12:14 +03:00
parent 08624052aa
commit b4fa3aa04d
2 changed files with 209 additions and 5 deletions
+98 -3
View File
@@ -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 Team;
DROP TABLE IF EXISTS Contest; DROP TABLE IF EXISTS Contest;
DROP TABLE IF EXISTS CountryCodes; DROP TABLE IF EXISTS CountryCodes;
DROP VIEW IF EXISTS ReviewSummary; DROP VIEW IF EXISTS ReviewSummaryGlobal;
DROP VIEW IF EXISTS ReviewSummaryByTeam; 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 group_id_seq;
DROP SEQUENCE IF EXISTS user_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 GROUP BY r.song_id, u.team_id, cc.name_fi, cc.name_sv, s.artist, s.title
ORDER BY avg_total_score DESC; 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 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 ('ALB', 'Albania', 'Albania', 'Albanien');
INSERT INTO CountryCodes (code, name_en, name_fi, name_sv) VALUES ('ARM', 'Armenia', 'Armenia', 'Armenien'); 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 -- 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', '[email protected]', (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', '[email protected]', (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', '[email protected]', (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) -- 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. -- layer. No CREATE VIEW statements are included.
-- ============================================================================= -- =============================================================================
-- End of Schema Definition -- End of Schema Definition
+111 -2
View File
@@ -1,13 +1,110 @@
from fastapi import APIRouter from fastapi import APIRouter
from enum import Enum
import duckdb # Added for specific exception handling
from models.result import Result, TeamResult from models.result import Result, TeamResult
from models.msg import Message
from lib.db import get_connection from lib.db import get_connection
from lib.logger import logger
router = APIRouter(prefix="/results", tags=["results"]) router = APIRouter(prefix="/results", tags=["results"])
@router.get("/", response_model=list[Result]) # It's good practice for FastAPI to have Enum derive from str as well
async def list_results(): 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: with get_connection() as conn:
df = conn.execute("SELECT * FROM ReviewSummaryGlobal").fetchdf() df = conn.execute("SELECT * FROM ReviewSummaryGlobal").fetchdf()
if df.empty: if df.empty:
@@ -16,6 +113,18 @@ async def list_results():
return 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]) @router.get("/team/{team_id}", response_model=list[TeamResult])
async def list_team_results(team_id: int): async def list_team_results(team_id: int):
with get_connection() as conn: with get_connection() as conn: