9 Commits
9 changed files with 196 additions and 45 deletions
+2
View File
@@ -16,4 +16,6 @@ wheels/
*.log *.log
*.gz *.gz
data
.vscode/* .vscode/*
+1
View File
@@ -12,4 +12,5 @@ FROM base
COPY --from=builder /app /app COPY --from=builder /app /app
ENV PATH="/app/.venv/bin:$PATH" ENV PATH="/app/.venv/bin:$PATH"
WORKDIR /app WORKDIR /app
RUN mkdir -p /app/data
CMD ["uvicorn", "app:app", "--host", "0.0.0.0", "--port", "8000"] CMD ["uvicorn", "app:app", "--host", "0.0.0.0", "--port", "8000"]
+9
View File
@@ -15,6 +15,15 @@ app = FastAPI(
version="1.0rc2", version="1.0rc2",
) )
@app.middleware("http")
async def log_requests(request, call_next):
body = await request.body()
logger.debug(f"Request body: {body.decode('utf-8')}")
response = await call_next(request)
return response
# Add CORS middleware # Add CORS middleware
app.add_middleware( app.add_middleware(
CORSMiddleware, CORSMiddleware,
+29
View File
@@ -10,6 +10,8 @@ DROP TABLE IF EXISTS Song;
DROP TABLE IF EXISTS "User"; -- Quoted because USER is a reserved keyword 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 CountryCodes; DROP TABLE IF EXISTS CountryCodes;
DROP VIEW IF EXISTS ReviewSummary;
DROP VIEW IF EXISTS ReviewSummaryByTeam;
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;
@@ -42,6 +44,8 @@ CREATE TABLE "Team" (
CREATE TABLE "User" ( CREATE TABLE "User" (
id INTEGER PRIMARY KEY DEFAULT nextval('user_id_seq'), -- Use sequence for auto-increment id INTEGER PRIMARY KEY DEFAULT nextval('user_id_seq'), -- Use sequence for auto-increment
username VARCHAR UNIQUE NOT NULL, -- The user's login name username VARCHAR UNIQUE NOT NULL, -- The user's login name
first_name VARCHAR DEFAULT '',
last_name VARCHAR DEFAULT '',
hashed_password VARCHAR NOT NULL, -- The securely hashed password hashed_password VARCHAR NOT NULL, -- The securely hashed password
email VARCHAR UNIQUE, -- User's email address (nullable) email VARCHAR UNIQUE, -- User's email address (nullable)
team_id INTEGER NOT NULL, -- Foreign Key -> Group.id team_id INTEGER NOT NULL, -- Foreign Key -> Group.id
@@ -115,6 +119,31 @@ CREATE TABLE CountryCodes (
name_sv VARCHAR NOT NULL name_sv VARCHAR NOT NULL
); );
CREATE VIEW ReviewSummaryGlobal AS
SELECT
song_id,
COUNT(*) AS total_reviews,
AVG(score_song) AS avg_score_song,
AVG(score_show) AS avg_score_show,
AVG(score_costume) AS avg_score_costume,
AVG((score_song + score_show + score_costume)/3) AS avg_total_score
FROM Review
GROUP BY song_id;
CREATE VIEW ReviewSummaryByTeam AS
SELECT
r.song_id,
u.team_id,
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
GROUP BY r.song_id, u.team_id
ORDER BY r.song_id, u.team_id;
-- 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');
+8
View File
@@ -6,6 +6,7 @@ from dotenv import load_dotenv
import os import os
from lib.helpers import hash_password from lib.helpers import hash_password
from lib.logger import logger
load_dotenv() load_dotenv()
@@ -61,6 +62,9 @@ def seed_db() -> None:
# Seed Admin users # Seed Admin users
admin_username = os.getenv("ADMIN_USERNAME") admin_username = os.getenv("ADMIN_USERNAME")
hashed_admin_password = hash_password(os.getenv("ADMIN_PASSWORD", "password")) hashed_admin_password = hash_password(os.getenv("ADMIN_PASSWORD", "password"))
logger.debug(
f"Admin user: {admin_username}, hashed password: {hashed_admin_password}"
)
with get_connection() as conn: with get_connection() as conn:
conn.execute( conn.execute(
'INSERT INTO "User" (username, hashed_password, team_id, is_active, is_admin) VALUES (?, ?, ?, ?, ?)', 'INSERT INTO "User" (username, hashed_password, team_id, is_active, is_admin) VALUES (?, ?, ?, ?, ?)',
@@ -75,8 +79,12 @@ def seed_db() -> None:
def init_db(): def init_db():
logger.info("Initializing database...")
if Path(DB_PATH).exists(): if Path(DB_PATH).exists():
return return
logger.info("Database does not exist, creating...")
if not Path(DB_PATH).parent.exists():
Path(DB_PATH).parent.mkdir(parents=True, exist_ok=True)
with open(SQL_PATH, "r") as f: with open(SQL_PATH, "r") as f:
sql = f.read() sql = f.read()
with get_connection() as conn: with get_connection() as conn:
+17 -7
View File
@@ -1,16 +1,26 @@
from loguru import logger from loguru import logger
from pathlib import Path from pathlib import Path
import sys import sys
from dotenv import load_dotenv
import os
LOG_PATH = Path("./data/logs/logs.log").absolute() load_dotenv()
ERROR_PATH = Path("./data/logs/errors.log").absolute()
LOG_PATH = Path(os.getenv("LOG_PATH", "./data/logs/logs.log")).absolute()
ERROR_PATH = Path(os.getenv("ERROR_PATH", "./data/logs/errors.log")).absolute()
logger.remove() logger.remove()
logger.add(LOG_PATH, rotation="10 MB", compression="gz", level="INFO") logger.add(
logger.add(sys.stdout, level="INFO") LOG_PATH, rotation="10 MB", compression="gz", level=os.getenv("LOG_LEVEL", "INFO")
)
logger.add(sys.stdout, level=os.getenv("LOG_LEVEL", "INFO"))
logger.add(ERROR_PATH, rotation="10 MB", compression="gz", level="ERROR") logger.add(
logger.add(sys.stderr, level="ERROR") ERROR_PATH,
logger.add(sys.stdout, level="DEBUG") rotation="10 MB",
compression="gz",
level=os.getenv("LOG_LEVEL_ERROR", "ERROR"),
)
logger.add(sys.stderr, level=os.getenv("LOG_LEVEL_ERROR", "ERROR"))
logger = logger logger = logger
+8
View File
@@ -0,0 +1,8 @@
from pydantic import BaseModel, Field
from datetime import datetime
class Message(BaseModel):
type: str | None = None
message: str
timestamp: datetime = Field(default_factory=datetime.now)
+98 -14
View File
@@ -1,7 +1,9 @@
from fastapi import APIRouter, HTTPException from fastapi import APIRouter, HTTPException
from models.review import ReviewOut, ReviewSearch, ReviewIn from models.review import ReviewOut, ReviewIn
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="/reviews", tags=["reviews"]) router = APIRouter(prefix="/reviews", tags=["reviews"])
@@ -16,21 +18,31 @@ async def list_reviews():
@router.get("/", response_model=ReviewOut) @router.get("/", response_model=ReviewOut)
async def get_review(review_id: ReviewSearch): async def get_review(song_id: int, user_id: int):
with get_connection() as conn: with get_connection() as conn:
review = conn.execute( review = conn.execute(
"SELECT * FROM Review WHERE song_id = ? AND user_id = ?", "SELECT * FROM Review WHERE song_id = ? AND user_id = ?",
(review_id.song_id, review_id.user_id), (song_id, user_id),
).fetchdf() ).fetchdf()
if review.empty: if review.empty:
raise HTTPException(status_code=404, detail="Review not found") raise HTTPException(status_code=404, detail="Review not found")
return review.to_dict(orient="records")[0] return review.to_dict(orient="records")[0]
@router.post("/", response_model=ReviewOut) @router.post("/", response_model=Message)
async def create_review(review: ReviewIn): async def create_review(review: ReviewIn):
logger.debug(f"Received create review request with data: {review.dict()}")
try:
with get_connection() as conn: with get_connection() as conn:
review = conn.execute( # Log the SQL query and parameters
logger.debug(
f"Executing INSERT INTO Review (user_id, song_id, score_song, score_show, score_costume, text_review) "
f"VALUES ({review.user_id}, {review.song_id}, {review.score_song}, "
f"{review.score_show}, {review.score_costume}, '{review.text_review}')"
)
# Insert the review
conn.execute(
"INSERT INTO Review (user_id, song_id, score_song, score_show, score_costume, text_review) VALUES (?, ?, ?, ?, ?, ?)", "INSERT INTO Review (user_id, song_id, score_song, score_show, score_costume, text_review) VALUES (?, ?, ?, ?, ?, ?)",
( (
review.user_id, review.user_id,
@@ -40,20 +52,58 @@ async def create_review(review: ReviewIn):
review.score_costume, review.score_costume,
review.text_review, review.text_review,
), ),
)
# Fetch the newly created review
logger.debug(
f"Checking if review was created for song_id={review.song_id}, user_id={review.user_id}"
)
result = conn.execute(
"SELECT * FROM Review WHERE song_id = ? AND user_id = ?",
(review.song_id, review.user_id),
).fetchdf() ).fetchdf()
if review.empty: logger.debug(f"Database result for newly created review: {result}")
if result.empty:
logger.warning(
f"Review not found after insert attempt: song_id={review.song_id}, user_id={review.user_id}"
)
raise HTTPException(status_code=404, detail="Review not found") raise HTTPException(status_code=404, detail="Review not found")
return review.to_dict(orient="records")[0]
logger.info(
f"Review created successfully for song_id={review.song_id}, user_id={review.user_id}"
)
return Message(type="success", message="Review created successfully")
except Exception as e:
logger.error(f"Error creating review: {str(e)}")
raise
@router.put("/", response_model=ReviewOut) @router.put("/", response_model=Message)
async def update_review(review: ReviewIn): async def update_review(review: ReviewIn):
print("update_review")
logger.debug(f"Received update review request with data: {review.dict()}")
try:
with get_connection() as conn: with get_connection() as conn:
review = conn.execute( # Log the SQL query and parameters
"UPDATE Review SET user_id = ?, song_id = ?, score_song = ?, score_show = ?, score_costume = ?, text_review = ? WHERE song_id = ? AND user_id = ?", logger.debug(
f"Executing UPDATE Review SET score_song = {review.score_song}, "
f"score_show = {review.score_show}, score_costume = {review.score_costume}, "
f"text_review = '{review.text_review}' WHERE song_id = {review.song_id} "
f"AND user_id = {review.user_id}"
)
# Check if the review exists
review_df = conn.execute(
"SELECT * FROM Review WHERE song_id = ? AND user_id = ?",
(review.song_id, review.user_id),
).fetchdf()
if not review_df.empty:
# Update the review
conn.execute(
"UPDATE Review SET score_song = ?, score_show = ?, score_costume = ?, text_review = ? WHERE song_id = ? AND user_id = ?",
( (
review.user_id,
review.song_id,
review.score_song, review.score_song,
review.score_show, review.score_show,
review.score_costume, review.score_costume,
@@ -61,7 +111,41 @@ async def update_review(review: ReviewIn):
review.song_id, review.song_id,
review.user_id, review.user_id,
), ),
)
else:
# Insert the review
conn.execute(
"INSERT INTO Review (user_id, song_id, score_song, score_show, score_costume, text_review) VALUES (?, ?, ?, ?, ?, ?)",
(
review.user_id,
review.song_id,
review.score_song,
review.score_show,
review.score_costume,
review.text_review,
),
)
# Fetch the updated review
logger.debug(
f"Checking if review was updated for song_id={review.song_id}, user_id={review.user_id}"
)
result = conn.execute(
"SELECT * FROM Review WHERE song_id = ? AND user_id = ?",
(review.song_id, review.user_id),
).fetchdf() ).fetchdf()
if review.empty: logger.debug(f"Database result: {result}")
if result.empty:
logger.warning(
f"Review not found after update attempt: song_id={review.song_id}, user_id={review.user_id}"
)
raise HTTPException(status_code=404, detail="Review not found") raise HTTPException(status_code=404, detail="Review not found")
return review.to_dict(orient="records")[0]
logger.info(
f"Review updated successfully for song_id={review.song_id}, user_id={review.user_id}"
)
return Message(type="success", message="Review updated successfully")
except Exception as e:
logger.error(f"Error updating review: {str(e)}")
raise
Generated
+1 -1
View File
@@ -131,7 +131,7 @@ wheels = [
[[package]] [[package]]
name = "eurovision-25-backend" name = "eurovision-25-backend"
version = "0.1.0" version = "1.0rc2"
source = { virtual = "." } source = { virtual = "." }
dependencies = [ dependencies = [
{ name = "aiofiles" }, { name = "aiofiles" },