20 Commits
Author SHA1 Message Date
Esa Kataja 61af47d651 Update docker-compose.yml for 1.0rc3 with Podman compatibility notes 2025-05-16 08:19:22 +03:00
Esa Kataja c3b63a61cc Add user profile update functionality to planned features for 1.0 2025-05-16 08:16:04 +03:00
Esa Kataja b6a1d4aeaf Prepare 1.0rc3 release 2025-05-16 08:11:54 +03:00
Esa Kataja 6c512c78aa Add avatar images 2025-05-16 08:05:10 +03:00
Esa Kataja 0aee5ca021 Change result end point to output artist info 2025-05-15 20:53:23 +03:00
Esa Kataja e863a8a088 Fix data dir was copied from dev and errored on build 2025-05-15 20:52:37 +03:00
Esa Kataja 58632e29cb Fix when no reviews exist raises error 2025-05-15 19:16:54 +03:00
Esa Kataja e560809c25 Add result endpoints 2025-05-15 19:16:08 +03:00
Esa Kataja a0344903ec Add contest table and routes 2025-05-15 17:58:52 +03:00
Esa Kataja 9ecdc1b040 Fix container not starting if data dir did not exist 2025-05-13 21:10:32 +03:00
Esa Kataja 4a480a312e Create result views to database schema 2025-05-13 20:58:28 +03:00
Esa Kataja f00bff5f64 Fix review put failing when no previous review exist 2025-05-13 20:11:38 +03:00
Esa Kataja 25f528c86d Add gitignore entries 2025-05-13 19:51:41 +03:00
Esa Kataja 21e30b0fb2 Add more logging 2025-05-13 19:48:37 +03:00
Esa Kataja 07d8e15925 Update lock file to rc2 2025-05-10 15:19:17 +03:00
Esa Kataja 7609159f60 Add message model 2025-05-10 15:18:26 +03:00
Esa Kataja c2884ac9ce Add more logging 2025-05-10 15:17:57 +03:00
Esa Kataja 40010c9860 Change logger loglevel operation 2025-05-10 15:17:42 +03:00
Esa Kataja d97a665a9c Prepare 1.0rc2 release 2025-05-10 14:33:35 +03:00
Esa Kataja 39da976ef3 Add more comprehensive logging 2025-05-10 14:28:34 +03:00
48 changed files with 512 additions and 83 deletions
+3
View File
@@ -2,3 +2,6 @@
*.duckdb
.env
__pycache__
src/data/
+2
View File
@@ -16,4 +16,6 @@ wheels/
*.log
*.gz
data
.vscode/*
+48
View File
@@ -0,0 +1,48 @@
# Eurovision 25 Backend Changelog
## 1.0rc3 (2025-05-16)
### Features
- Added contest table and routes
- Added result endpoints with artist information
- Created result views in database schema
- Added avatar images for users
### Fixes
- Fixed data directory copied from dev environment causing build errors
- Fixed error when no reviews exist
- Fixed container not starting if data directory did not exist
- Fixed review PUT failing when no previous review exists
### Changes
- Updated application version to 1.0rc3
- Merged changelog files for better version tracking
- Added more comprehensive logging
- Added gitignore entries
## 1.0rc2 (2025-05-10)
### Changes
- Updated application version to 1.0rc2
- Added more comprehensive logging throughout the application
## 1.0rc1 (2025-05-01)
### Features
- Initial release candidate
- Eurovision 25 Homereview API implementation
- Authentication and authorization system
- User management features
- Song management and reviews
- Review system
- Results calculation and display
- Admin functionality
- Static file serving for avatars and country flags
The application is feature complete, but may still contain bugs that will be addressed before the final 1.0.0 release.
## Planned for 1.0.0 (Final Release)
### Features
- Implementation of user profile update functionality to allow users to edit their personal information (name, email, password, avatar, etc.)
- Additional security improvements and bug fixes
+3
View File
@@ -6,10 +6,13 @@ WORKDIR /app
COPY pyproject.toml uv.lock /app/
RUN uv sync --frozen --no-install-project --no-dev
COPY src /app
# Ensure data directory exists but don't copy from host
RUN uv sync --frozen --no-dev
FROM base
COPY --from=builder /app /app
ENV PATH="/app/.venv/bin:$PATH"
WORKDIR /app
# Create data directory
RUN mkdir -p /app/data && chmod 755 /app/data
CMD ["uvicorn", "app:app", "--host", "0.0.0.0", "--port", "8000"]
+4
View File
@@ -220,6 +220,10 @@ Automated tests are not implemented at this time. Manual testing via the API doc
## TODOs
* [ ] Replace password hashing with passlib
* [ ] Switch to PostgreSQL from DuckDB
* [x] Implement more comprehensive logging
* [ ] Implement user disabling functionality
* [ ] Implement user profile update functionality (name, email, password, avatar, etc.)
## Contributing
+4
View File
@@ -1,10 +1,14 @@
version: '3.8'
# This compose file can be used with either Docker or Podman
# For Podman: podman-compose up -d
services:
backend:
build:
context: .
dockerfile: Dockerfile
image: eurovision-25-backend:1.0rc3
ports:
- "8000:8000"
volumes:
+1 -1
View File
@@ -1,6 +1,6 @@
[project]
name = "Eurovision-25-backend"
version = "1.0rc1"
version = "1.0rc2"
description = "Backend for Eurovision 25"
readme = "README.md"
requires-python = ">=3.12"
+12 -2
View File
@@ -3,7 +3,7 @@ from fastapi.middleware.cors import CORSMiddleware
from fastapi.staticfiles import StaticFiles
# Import routers
from routes import auth, admin, users, songs, reviews, results
from routes import auth, admin, users, songs, reviews, results, contest
from lib.logger import logger
from lib.db import init_db
@@ -12,9 +12,18 @@ from lib.db import init_db
app = FastAPI(
title="Eurovision 25 Homereview API",
description="Backend API for Eurovision 25 Homereview application",
version="1.0rc1",
version="1.0rc3",
)
@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
app.add_middleware(
CORSMiddleware,
@@ -51,3 +60,4 @@ app.include_router(users.router)
app.include_router(songs.router)
app.include_router(reviews.router)
app.include_router(results.router)
app.include_router(contest.router)
+64
View File
@@ -9,12 +9,16 @@ DROP TABLE IF EXISTS Review;
DROP TABLE IF EXISTS Song;
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 ReviewSummaryByTeam;
DROP SEQUENCE IF EXISTS group_id_seq;
DROP SEQUENCE IF EXISTS user_id_seq;
DROP SEQUENCE IF EXISTS song_id_seq;
DROP SEQUENCE IF EXISTS review_id_seq;
DROP SEQUENCE IF EXISTS contest_id_seq;
-- =============================================================================
-- Sequences for Primary Keys
@@ -23,6 +27,23 @@ CREATE SEQUENCE group_id_seq START 1;
CREATE SEQUENCE user_id_seq START 1;
CREATE SEQUENCE song_id_seq START 1;
CREATE SEQUENCE review_id_seq START 1;
CREATE SEQUENCE contest_id_seq START 1;
-- =============================================================================
-- Table: Contest
-- Stores information about Eurovision contests by year.
-- =============================================================================
CREATE TABLE Contest (
id INTEGER PRIMARY KEY DEFAULT nextval('contest_id_seq'), -- Use sequence for auto-increment
year INTEGER DEFAULT EXTRACT(YEAR FROM CURRENT_DATE) NOT NULL, -- The year of the Eurovision contest
is_active BOOLEAN DEFAULT TRUE NOT NULL, -- Flag indicating if this is the active contest
finals_date DATE NOT NULL, -- Date of the finals
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, -- Timestamp when the contest was created
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP -- Timestamp when the contest was last updated
);
-- Add index for faster lookups by year
CREATE INDEX idx_contest_year ON Contest (year);
-- =============================================================================
-- Table: Group
@@ -42,6 +63,8 @@ CREATE TABLE "Team" (
CREATE TABLE "User" (
id INTEGER PRIMARY KEY DEFAULT nextval('user_id_seq'), -- Use sequence for auto-increment
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
email VARCHAR UNIQUE, -- User's email address (nullable)
team_id INTEGER NOT NULL, -- Foreign Key -> Group.id
@@ -115,6 +138,44 @@ CREATE TABLE CountryCodes (
name_sv VARCHAR NOT NULL
);
CREATE VIEW ReviewSummaryGlobal AS
SELECT
song_id,
cc.name_fi AS country_fi,
cc.name_sv AS country_sv,
s.artist,
s.title,
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 r
JOIN Song s ON r.song_id = s.id
JOIN CountryCodes cc ON s.country = cc.code
GROUP BY song_id, cc.name_fi, cc.name_sv, s.artist, s.title
ORDER BY avg_total_score DESC;
CREATE VIEW ReviewSummaryByTeam AS
SELECT
r.song_id,
cc.name_fi AS country_fi,
cc.name_sv AS country_sv,
s.artist,
s.title,
u.team_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.team_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');
@@ -154,6 +215,9 @@ INSERT INTO CountryCodes (code, name_en, name_fi, name_sv) VALUES ('CHE', 'Switz
INSERT INTO CountryCodes (code, name_en, name_fi, name_sv) VALUES ('UKR', 'Ukraine', 'Ukraina', 'Ukraina');
INSERT INTO CountryCodes (code, name_en, name_fi, name_sv) VALUES ('GBR', 'United Kingdom', 'Englanti', 'Storbritannien');
-- 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');
-- =============================================================================
-- Views (Optional - Not Created Here Based on Documentation Decision)
-- =============================================================================
+8
View File
@@ -6,6 +6,7 @@ from dotenv import load_dotenv
import os
from lib.helpers import hash_password
from lib.logger import logger
load_dotenv()
@@ -61,6 +62,9 @@ def seed_db() -> None:
# Seed Admin users
admin_username = os.getenv("ADMIN_USERNAME")
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:
conn.execute(
'INSERT INTO "User" (username, hashed_password, team_id, is_active, is_admin) VALUES (?, ?, ?, ?, ?)',
@@ -75,8 +79,12 @@ def seed_db() -> None:
def init_db():
logger.info("Initializing database...")
if Path(DB_PATH).exists():
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:
sql = f.read()
with get_connection() as conn:
+17 -7
View File
@@ -1,16 +1,26 @@
from loguru import logger
from pathlib import Path
import sys
from dotenv import load_dotenv
import os
LOG_PATH = Path("./data/logs/logs.log").absolute()
ERROR_PATH = Path("./data/logs/errors.log").absolute()
load_dotenv()
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.add(LOG_PATH, rotation="10 MB", compression="gz", level="INFO")
logger.add(sys.stdout, level="INFO")
logger.add(
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(sys.stderr, level="ERROR")
logger.add(sys.stdout, level="DEBUG")
logger.add(
ERROR_PATH,
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
+25
View File
@@ -0,0 +1,25 @@
from pydantic import BaseModel, Field
from datetime import date, datetime
class Contest(BaseModel):
id: int
year: int = Field(
default_factory=lambda: datetime.now().year,
description="The year of the contest",
examples=[2025],
)
is_active: bool = Field(
default=True, description="Is this the active contest?", examples=[True, False]
)
finals_date: date = Field(
description="The date of the finals", examples=["2025-05-15"]
)
created_at: datetime = Field(
description="The date and time when the contest was created",
examples=["2025-05-15T17:40:42.000Z"],
)
updated_at: datetime = Field(
description="The date and time when the contest was last updated",
examples=["2025-05-15T17:40:42.000Z"],
)
+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)
+28
View File
@@ -0,0 +1,28 @@
from pydantic import BaseModel, Field
class Result(BaseModel):
song_id: int = Field(description="ID of the song", examples=[1])
country_fi: str = Field(
description="Country of the song in Finnish", examples=["Suomi"]
)
country_sv: str = Field(
description="Country of the song in Swedish", examples=["Sverige"]
)
artist: str = Field(description="Artist of the song", examples=["Käärijä"])
title: str = Field(description="Title of the song", examples=["Cha cha cha"])
total_reviews: int = Field(0, description="Total number of reviews", examples=[10])
avg_score_song: float = Field(
description="Average score for song quality", examples=[85.5]
)
avg_score_show: float = Field(
description="Average score for stage show", examples=[82.3]
)
avg_score_costume: float = Field(
description="Average score for wardrobe/costumes", examples=[88.1]
)
avg_total_score: float = Field(description="Average total score", examples=[85.5])
class TeamResult(Result):
team_id: int = Field(description="ID of the team", examples=[1])
+57 -19
View File
@@ -1,9 +1,10 @@
from fastapi import APIRouter, Body, HTTPException
from fastapi import APIRouter, Body, HTTPException, Request
from models.user import CreateUser
from models.team import TeamBase, Team
from lib.db import get_connection
from lib.helpers import hash_password
from lib.logger import logger
router = APIRouter(prefix="/admin", tags=["Admin"])
@@ -19,36 +20,73 @@ router = APIRouter(prefix="/admin", tags=["Admin"])
@router.post("/teams")
async def create_team(team: TeamBase):
with get_connection() as conn:
conn.execute("INSERT INTO Team (name) VALUES (?)", (team.name,))
return {"message": "Team created successfully"}
async def create_team(team: TeamBase, request: Request):
logger.info(
f"Admin action: Creating new team '{team.name}' from IP: {request.client.host}"
)
try:
with get_connection() as conn:
conn.execute("INSERT INTO Team (name) VALUES (?)", (team.name,))
logger.info(f"Team '{team.name}' created successfully")
return {"message": "Team created successfully"}
except Exception as e:
logger.error(f"Failed to create team '{team.name}': {str(e)}")
raise HTTPException(status_code=500, detail="Failed to create team")
@router.get("/teams", response_model=list[Team])
async def list_teams():
async def list_teams(request: Request):
logger.debug(f"Admin action: Listing all teams from IP: {request.client.host}")
with get_connection() as conn:
teams = conn.execute("SELECT * FROM Team").fetchdf()
return teams.to_dict(orient="records")
@router.post("/users")
async def create_user(user: CreateUser):
with get_connection() as conn:
conn.execute(
"INSERT INTO User (username, hashed_password, team_id, is_admin) VALUES (?, ?, ?, ?)",
(
user.username,
hash_password(user.password),
user.team_id,
user.is_admin,
),
async def create_user(user: CreateUser, request: Request):
logger.info(
f"Admin action: Creating new user '{user.username}' from IP: {request.client.host}"
)
try:
with get_connection() as conn:
# Check if username already exists
existing = conn.execute(
"SELECT COUNT(*) as count FROM User WHERE username = ?",
(user.username,),
).fetchdf()
if existing["count"][0] > 0:
logger.warning(
f"Failed to create user: Username '{user.username}' already exists"
)
raise HTTPException(status_code=400, detail="Username already exists")
conn.execute(
"INSERT INTO User (username, hashed_password, team_id, is_admin) VALUES (?, ?, ?, ?)",
(
user.username,
hash_password(user.password),
user.team_id,
user.is_admin,
),
)
logger.info(
f"User '{user.username}' created successfully with team_id: {user.team_id}, admin status: {user.is_admin}"
)
return {"message": "User created successfully"}
return {"message": "User created successfully"}
except HTTPException:
# Re-raise HTTP exceptions
raise
except Exception as e:
logger.error(f"Failed to create user '{user.username}': {str(e)}")
raise HTTPException(status_code=500, detail="Failed to create user")
@router.post("/users/")
async def disable_user(user_id: int = Body(..., embed=True)):
@router.post("/users/disable")
async def disable_user(user_id: int = Body(..., embed=True), request: Request = None):
logger.info(
f"Admin action: Attempting to disable user with ID: {user_id} from IP: {request.client.host}"
)
logger.warning(f"Disable user functionality not implemented for user_id: {user_id}")
raise HTTPException(status_code=405, detail="Method not yet implemented")
+21 -6
View File
@@ -1,24 +1,34 @@
from fastapi import APIRouter, HTTPException
from fastapi import APIRouter, HTTPException, Request
from lib.db import get_connection
from models.user import User, UserLogin
from lib.helpers import verify_password
from lib.logger import logger
router = APIRouter(prefix="/auth", tags=["auth"])
@router.post("/token", response_model=User)
async def login(user_login: UserLogin):
async def login(user_login: UserLogin, request: Request):
logger.debug(
f"Login attempt for user: {user_login.username} from IP: {request.client.host}"
)
with get_connection() as conn:
user_df = conn.execute(
'SELECT * FROM "User" WHERE username = ?', (user_login.username,)
).fetchdf()
if user_df.empty:
logger.warning(
f"Failed login: Username {user_login.username} not found - IP: {request.client.host}"
)
raise HTTPException(status_code=401, detail="Invalid username or password")
user_data = user_df.to_dict(orient="records")[0]
if not verify_password(user_login.password, user_data["hashed_password"]):
logger.warning(
f"Failed login: Incorrect password for user {user_login.username} - IP: {request.client.host}"
)
raise HTTPException(status_code=401, detail="Invalid username or password")
# Update the login timestamp in the database
@@ -29,9 +39,14 @@ async def login(user_login: UserLogin):
)
# Fetch the updated user data with the new last_login timestamp
updated_user = conn.execute(
'SELECT * FROM "User" WHERE id = ?',
(user_data["id"],)
).fetchdf().to_dict(orient="records")[0]
updated_user = (
conn.execute('SELECT * FROM "User" WHERE id = ?', (user_data["id"],))
.fetchdf()
.to_dict(orient="records")[0]
)
logger.info(
f"Successful login: User {user_login.username} (ID: {user_data['id']}) logged in from {request.client.host}"
)
return User(**updated_user)
+39
View File
@@ -0,0 +1,39 @@
from fastapi import APIRouter, HTTPException
from lib.db import get_connection
from lib.logger import logger
from models.contest import Contest
from models.msg import Message
router = APIRouter(prefix="/contest", tags=["contest"])
@router.get("/", response_model=list[Contest])
async def list_contests():
with get_connection() as conn:
logger.debug("Listing all contests")
df = conn.execute("SELECT * FROM Contest").fetchdf()
logger.debug(f"Contests: {df}")
contests = [Contest(**row) for row in df.to_dict(orient="records")]
return contests
@router.get("/{year}", response_model=Contest)
async def get_contest(year: int):
with get_connection() as conn:
df = conn.execute("SELECT * FROM Contest WHERE year = ?", (year,)).fetchdf()
if df.empty:
raise HTTPException(status_code=404, detail="Contest not found")
contest = Contest(**df.to_dict(orient="records")[0])
return contest
@router.put("/", response_model=Message)
async def update_contest(year: int, is_active: bool):
with get_connection() as conn:
logger.debug(f"Updating contest {year} to active: {is_active}")
conn.execute(
"UPDATE Contest SET is_active = ? WHERE year = ?",
(is_active, year),
)
return Message(message="Contest updated successfully")
+25
View File
@@ -1,3 +1,28 @@
from fastapi import APIRouter
from models.result import Result, TeamResult
from lib.db import get_connection
router = APIRouter(prefix="/results", tags=["results"])
@router.get("/", response_model=list[Result])
async def list_results():
with get_connection() as conn:
df = conn.execute("SELECT * FROM ReviewSummaryGlobal").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:
df = conn.execute(
"SELECT * FROM ReviewSummaryByTeam WHERE team_id = ?", (team_id,)
).fetchdf()
if df.empty:
return []
results = [TeamResult(**row) for row in df.to_dict(orient="records")]
return results
+123 -39
View File
@@ -1,7 +1,9 @@
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.logger import logger
router = APIRouter(prefix="/reviews", tags=["reviews"])
@@ -11,57 +13,139 @@ async def list_reviews():
with get_connection() as conn:
reviews = conn.execute("SELECT * FROM Review").fetchdf()
if reviews.empty:
raise HTTPException(status_code=404, detail="Reviews not found")
return []
return reviews.to_dict(orient="records")
@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:
review = conn.execute(
"SELECT * FROM Review WHERE song_id = ? AND user_id = ?",
(review_id.song_id, review_id.user_id),
(song_id, user_id),
).fetchdf()
if review.empty:
raise HTTPException(status_code=404, detail="Review not found")
return []
return review.to_dict(orient="records")[0]
@router.post("/", response_model=ReviewOut)
@router.post("/", response_model=Message)
async def create_review(review: ReviewIn):
with get_connection() as conn:
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,
),
).fetchdf()
if review.empty:
raise HTTPException(status_code=404, detail="Review not found")
return review.to_dict(orient="records")[0]
logger.debug(f"Received create review request with data: {review.dict()}")
try:
with get_connection() as conn:
# 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 (?, ?, ?, ?, ?, ?)",
(
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):
with get_connection() as conn:
review = conn.execute(
"UPDATE Review SET user_id = ?, song_id = ?, 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_show,
review.score_costume,
review.text_review,
review.song_id,
review.user_id,
),
).fetchdf()
if review.empty:
raise HTTPException(status_code=404, detail="Review not found")
return review.to_dict(orient="records")[0]
print("update_review")
logger.debug(f"Received update review request with data: {review.dict()}")
try:
with get_connection() as conn:
# Log the SQL query and parameters
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.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
+16 -5
View File
@@ -1,14 +1,15 @@
from fastapi import APIRouter
from fastapi import HTTPException
from fastapi import APIRouter, HTTPException, Request
from models.user import User, UserChangePassword
from lib.db import get_connection
from lib.logger import logger
router = APIRouter(prefix="/users", tags=["users"])
@router.get("/", response_model=list[User])
async def list_users():
async def list_users(request: Request):
logger.info(f"User list requested from {request.client.host}")
with get_connection() as conn:
users = conn.execute('SELECT * FROM "User"').fetchdf()
@@ -18,19 +19,29 @@ async def list_users():
@router.get("/{user_id}", response_model=User)
async def get_user(user_id: int):
async def get_user(user_id: int, request: Request):
logger.debug(
f"User details requested for user_id: {user_id} from {request.client.host}"
)
with get_connection() as conn:
user_df = conn.execute(
'SELECT * FROM "User" WHERE id = ?', (user_id,)
).fetchdf()
if user_df.empty:
logger.warning(f"Failed user lookup: user_id {user_id} not found")
raise HTTPException(status_code=404, detail="User not found")
user_data = user_df.to_dict(orient="records")[0]
logger.debug(f"User details retrieved: {user_data}")
return User(**user_data)
@router.patch("/{user_id}")
async def change_password(user: UserChangePassword):
async def change_password(user_id: int, user: UserChangePassword, request: Request):
logger.info(
f"Password change attempt for user_id: {user_id} from {request.client.host}"
)
# Implementation will go here when completed
logger.warning(f"Password change not implemented for user_id: {user_id}")
raise HTTPException(status_code=401, detail="Not implemented")
Binary file not shown.

After

Width:  |  Height:  |  Size: 6.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

Generated
+1 -1
View File
@@ -131,7 +131,7 @@ wheels = [
[[package]]
name = "eurovision-25-backend"
version = "0.1.0"
version = "1.0rc2"
source = { virtual = "." }
dependencies = [
{ name = "aiofiles" },