Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
189c6ae1c7 | ||
|
|
b4fa3aa04d | ||
|
|
08624052aa | ||
|
|
1a3a2aef86 |
+2
-1
@@ -18,4 +18,5 @@ wheels/
|
||||
|
||||
data
|
||||
|
||||
.vscode/*
|
||||
.vscode/*
|
||||
*.tar
|
||||
+12
@@ -1,5 +1,17 @@
|
||||
# Eurovision 25 Backend Changelog
|
||||
|
||||
## 1.0rc8 (2025-05-17)
|
||||
|
||||
### Features
|
||||
- Add more result types (b4fa3aa)
|
||||
|
||||
### Fixes
|
||||
- Fix regression in db seeding (0862405)
|
||||
|
||||
### Chores
|
||||
- Add docker exports to git ignore (1a3a2ae)
|
||||
|
||||
|
||||
## 1.0rc7 (2025-05-16)
|
||||
|
||||
### Features
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Eurovision 25 Homereview Backend
|
||||
|
||||
**Version: 1.0rc7**
|
||||
**Version: 1.0rc8**
|
||||
|
||||
## Description
|
||||
|
||||
|
||||
+1
-1
@@ -8,7 +8,7 @@ services:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: Dockerfile
|
||||
image: eurovision-25-backend:1.0rc7
|
||||
image: eurovision-25-backend:1.0rc8
|
||||
ports:
|
||||
- "8000:8000"
|
||||
volumes:
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
[project]
|
||||
name = "Eurovision-25-backend"
|
||||
version = "1.0rc7"
|
||||
version = "1.0rc8"
|
||||
description = "Backend for Eurovision 25"
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.12"
|
||||
|
||||
+1
-1
@@ -12,7 +12,7 @@ from lib.db import init_db
|
||||
app = FastAPI(
|
||||
title="Eurovision 25 Homereview API",
|
||||
description="Backend API for Eurovision 25 Homereview application",
|
||||
version="1.0rc7",
|
||||
version="1.0rc8",
|
||||
openapi_url="/sec_schema.json",
|
||||
docs_url="/docut",
|
||||
redoc_url=None,
|
||||
|
||||
+98
-3
@@ -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', '[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)
|
||||
-- =============================================================================
|
||||
@@ -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
|
||||
-- End of Schema Definition
|
||||
|
||||
|
||||
+2
-1
@@ -36,12 +36,13 @@ def seed_db() -> None:
|
||||
with get_connection() as conn:
|
||||
logger.debug(f"Inserting song {idx + 1} of {len(data)}", item)
|
||||
conn.execute(
|
||||
"INSERT INTO Song (year, artist, country, title, lyrics_url, artist_url, flag_url, running_order, lyrics_original, lyrics_translation_fi, tags, confidence, language) VALUES (2025, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
|
||||
"INSERT INTO Song (year, artist, country, title, lyrics_url, img_url, artist_url, flag_url, running_order, lyrics_original, lyrics_translation_fi, tags, confidence, language) VALUES (2025, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
|
||||
(
|
||||
item["artist"],
|
||||
item["country"],
|
||||
item["title"],
|
||||
item["lyrics_url"],
|
||||
item["img_url"],
|
||||
item["artist_url"],
|
||||
item["flag_url"],
|
||||
item["running_order"],
|
||||
|
||||
+111
-2
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user