Compare commits
9
Commits
v1.0rc2
...
9ecdc1b040
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9ecdc1b040 | ||
|
|
4a480a312e | ||
|
|
f00bff5f64 | ||
|
|
25f528c86d | ||
|
|
21e30b0fb2 | ||
|
|
07d8e15925 | ||
|
|
7609159f60 | ||
|
|
c2884ac9ce | ||
|
|
40010c9860 |
@@ -16,4 +16,6 @@ wheels/
|
|||||||
*.log
|
*.log
|
||||||
*.gz
|
*.gz
|
||||||
|
|
||||||
|
data
|
||||||
|
|
||||||
.vscode/*
|
.vscode/*
|
||||||
@@ -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"]
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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');
|
||||||
|
|||||||
@@ -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
@@ -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
|
||||||
|
|||||||
@@ -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)
|
||||||
+121
-37
@@ -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,52 +18,134 @@ 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):
|
||||||
with get_connection() as conn:
|
logger.debug(f"Received create review request with data: {review.dict()}")
|
||||||
review = conn.execute(
|
try:
|
||||||
"INSERT INTO Review (user_id, song_id, score_song, score_show, score_costume, text_review) VALUES (?, ?, ?, ?, ?, ?)",
|
with get_connection() as conn:
|
||||||
(
|
# Log the SQL query and parameters
|
||||||
review.user_id,
|
logger.debug(
|
||||||
review.song_id,
|
f"Executing INSERT INTO Review (user_id, song_id, score_song, score_show, score_costume, text_review) "
|
||||||
review.score_song,
|
f"VALUES ({review.user_id}, {review.song_id}, {review.score_song}, "
|
||||||
review.score_show,
|
f"{review.score_show}, {review.score_costume}, '{review.text_review}')"
|
||||||
review.score_costume,
|
)
|
||||||
review.text_review,
|
|
||||||
),
|
# Insert the review
|
||||||
).fetchdf()
|
conn.execute(
|
||||||
if review.empty:
|
"INSERT INTO Review (user_id, song_id, score_song, score_show, score_costume, text_review) VALUES (?, ?, ?, ?, ?, ?)",
|
||||||
raise HTTPException(status_code=404, detail="Review not found")
|
(
|
||||||
return review.to_dict(orient="records")[0]
|
review.user_id,
|
||||||
|
review.song_id,
|
||||||
|
review.score_song,
|
||||||
|
review.score_show,
|
||||||
|
review.score_costume,
|
||||||
|
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()
|
||||||
|
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")
|
||||||
|
|
||||||
|
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):
|
||||||
with get_connection() as conn:
|
print("update_review")
|
||||||
review = conn.execute(
|
logger.debug(f"Received update review request with data: {review.dict()}")
|
||||||
"UPDATE Review SET user_id = ?, song_id = ?, score_song = ?, score_show = ?, score_costume = ?, text_review = ? WHERE song_id = ? AND user_id = ?",
|
try:
|
||||||
(
|
with get_connection() as conn:
|
||||||
review.user_id,
|
# Log the SQL query and parameters
|
||||||
review.song_id,
|
logger.debug(
|
||||||
review.score_song,
|
f"Executing UPDATE Review SET score_song = {review.score_song}, "
|
||||||
review.score_show,
|
f"score_show = {review.score_show}, score_costume = {review.score_costume}, "
|
||||||
review.score_costume,
|
f"text_review = '{review.text_review}' WHERE song_id = {review.song_id} "
|
||||||
review.text_review,
|
f"AND user_id = {review.user_id}"
|
||||||
review.song_id,
|
)
|
||||||
review.user_id,
|
|
||||||
),
|
# Check if the review exists
|
||||||
).fetchdf()
|
review_df = conn.execute(
|
||||||
if review.empty:
|
"SELECT * FROM Review WHERE song_id = ? AND user_id = ?",
|
||||||
raise HTTPException(status_code=404, detail="Review not found")
|
(review.song_id, review.user_id),
|
||||||
return review.to_dict(orient="records")[0]
|
).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.score_song,
|
||||||
|
review.score_show,
|
||||||
|
review.score_costume,
|
||||||
|
review.text_review,
|
||||||
|
review.song_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()
|
||||||
|
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")
|
||||||
|
|
||||||
|
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
|
||||||
|
|||||||
@@ -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" },
|
||||||
|
|||||||
Reference in New Issue
Block a user