Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
61af47d651 | ||
|
|
c3b63a61cc | ||
|
|
b6a1d4aeaf | ||
|
|
6c512c78aa | ||
|
|
0aee5ca021 | ||
|
|
e863a8a088 | ||
|
|
58632e29cb | ||
|
|
e560809c25 | ||
|
|
a0344903ec | ||
|
|
9ecdc1b040 | ||
|
|
4a480a312e | ||
|
|
f00bff5f64 | ||
|
|
25f528c86d |
@@ -2,3 +2,6 @@
|
|||||||
*.duckdb
|
*.duckdb
|
||||||
.env
|
.env
|
||||||
__pycache__
|
__pycache__
|
||||||
|
|
||||||
|
|
||||||
|
src/data/
|
||||||
@@ -16,4 +16,6 @@ wheels/
|
|||||||
*.log
|
*.log
|
||||||
*.gz
|
*.gz
|
||||||
|
|
||||||
|
data
|
||||||
|
|
||||||
.vscode/*
|
.vscode/*
|
||||||
@@ -1,19 +1,48 @@
|
|||||||
# Eurovision 25 Backend Changelog
|
# 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)
|
## 1.0rc2 (2025-05-10)
|
||||||
|
|
||||||
### Changes
|
### Changes
|
||||||
- Updated application version to 1.0rc2
|
- Updated application version to 1.0rc2
|
||||||
- Added more comprehensive logging throughout the application
|
- Added more comprehensive logging throughout the application
|
||||||
|
|
||||||
|
## 1.0rc1 (2025-05-01)
|
||||||
## 1.0rc1 (Previous Release)
|
|
||||||
|
|
||||||
### Features
|
### Features
|
||||||
- Initial release candidate
|
- Initial release candidate
|
||||||
- Eurovision 25 Homereview API implementation
|
- Eurovision 25 Homereview API implementation
|
||||||
- Authentication and authorization system
|
- Authentication and authorization system
|
||||||
- User management features
|
- User management features
|
||||||
- Song management
|
- Song management and reviews
|
||||||
- Review system
|
- Review system
|
||||||
- Results calculation
|
- 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
|
||||||
|
|||||||
@@ -6,10 +6,13 @@ WORKDIR /app
|
|||||||
COPY pyproject.toml uv.lock /app/
|
COPY pyproject.toml uv.lock /app/
|
||||||
RUN uv sync --frozen --no-install-project --no-dev
|
RUN uv sync --frozen --no-install-project --no-dev
|
||||||
COPY src /app
|
COPY src /app
|
||||||
|
# Ensure data directory exists but don't copy from host
|
||||||
RUN uv sync --frozen --no-dev
|
RUN uv sync --frozen --no-dev
|
||||||
|
|
||||||
FROM base
|
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
|
||||||
|
# Create data directory
|
||||||
|
RUN mkdir -p /app/data && chmod 755 /app/data
|
||||||
CMD ["uvicorn", "app:app", "--host", "0.0.0.0", "--port", "8000"]
|
CMD ["uvicorn", "app:app", "--host", "0.0.0.0", "--port", "8000"]
|
||||||
|
|||||||
@@ -223,7 +223,7 @@ Automated tests are not implemented at this time. Manual testing via the API doc
|
|||||||
* [ ] Switch to PostgreSQL from DuckDB
|
* [ ] Switch to PostgreSQL from DuckDB
|
||||||
* [x] Implement more comprehensive logging
|
* [x] Implement more comprehensive logging
|
||||||
* [ ] Implement user disabling functionality
|
* [ ] Implement user disabling functionality
|
||||||
* [ ] Implement user password change functionality
|
* [ ] Implement user profile update functionality (name, email, password, avatar, etc.)
|
||||||
|
|
||||||
## Contributing
|
## Contributing
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +1,14 @@
|
|||||||
version: '3.8'
|
version: '3.8'
|
||||||
|
|
||||||
|
# This compose file can be used with either Docker or Podman
|
||||||
|
# For Podman: podman-compose up -d
|
||||||
|
|
||||||
services:
|
services:
|
||||||
backend:
|
backend:
|
||||||
build:
|
build:
|
||||||
context: .
|
context: .
|
||||||
dockerfile: Dockerfile
|
dockerfile: Dockerfile
|
||||||
|
image: eurovision-25-backend:1.0rc3
|
||||||
ports:
|
ports:
|
||||||
- "8000:8000"
|
- "8000:8000"
|
||||||
volumes:
|
volumes:
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ from fastapi.middleware.cors import CORSMiddleware
|
|||||||
from fastapi.staticfiles import StaticFiles
|
from fastapi.staticfiles import StaticFiles
|
||||||
|
|
||||||
# Import routers
|
# 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.logger import logger
|
||||||
from lib.db import init_db
|
from lib.db import init_db
|
||||||
@@ -12,7 +12,7 @@ from lib.db import init_db
|
|||||||
app = FastAPI(
|
app = FastAPI(
|
||||||
title="Eurovision 25 Homereview API",
|
title="Eurovision 25 Homereview API",
|
||||||
description="Backend API for Eurovision 25 Homereview application",
|
description="Backend API for Eurovision 25 Homereview application",
|
||||||
version="1.0rc2",
|
version="1.0rc3",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -60,3 +60,4 @@ app.include_router(users.router)
|
|||||||
app.include_router(songs.router)
|
app.include_router(songs.router)
|
||||||
app.include_router(reviews.router)
|
app.include_router(reviews.router)
|
||||||
app.include_router(results.router)
|
app.include_router(results.router)
|
||||||
|
app.include_router(contest.router)
|
||||||
|
|||||||
@@ -9,12 +9,16 @@ DROP TABLE IF EXISTS Review;
|
|||||||
DROP TABLE IF EXISTS Song;
|
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 Contest;
|
||||||
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;
|
||||||
DROP SEQUENCE IF EXISTS song_id_seq;
|
DROP SEQUENCE IF EXISTS song_id_seq;
|
||||||
DROP SEQUENCE IF EXISTS review_id_seq;
|
DROP SEQUENCE IF EXISTS review_id_seq;
|
||||||
|
DROP SEQUENCE IF EXISTS contest_id_seq;
|
||||||
|
|
||||||
-- =============================================================================
|
-- =============================================================================
|
||||||
-- Sequences for Primary Keys
|
-- Sequences for Primary Keys
|
||||||
@@ -23,6 +27,23 @@ CREATE SEQUENCE group_id_seq START 1;
|
|||||||
CREATE SEQUENCE user_id_seq START 1;
|
CREATE SEQUENCE user_id_seq START 1;
|
||||||
CREATE SEQUENCE song_id_seq START 1;
|
CREATE SEQUENCE song_id_seq START 1;
|
||||||
CREATE SEQUENCE review_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
|
-- Table: Group
|
||||||
@@ -42,6 +63,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 +138,44 @@ CREATE TABLE CountryCodes (
|
|||||||
name_sv VARCHAR NOT NULL
|
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 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');
|
||||||
@@ -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 ('UKR', 'Ukraine', 'Ukraina', 'Ukraina');
|
||||||
INSERT INTO CountryCodes (code, name_en, name_fi, name_sv) VALUES ('GBR', 'United Kingdom', 'Englanti', 'Storbritannien');
|
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)
|
-- Views (Optional - Not Created Here Based on Documentation Decision)
|
||||||
-- =============================================================================
|
-- =============================================================================
|
||||||
|
|||||||
@@ -79,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:
|
||||||
|
|||||||
@@ -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"],
|
||||||
|
)
|
||||||
@@ -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])
|
||||||
@@ -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")
|
||||||
@@ -1,3 +1,28 @@
|
|||||||
from fastapi import APIRouter
|
from fastapi import APIRouter
|
||||||
|
|
||||||
|
from models.result import Result, TeamResult
|
||||||
|
from lib.db import get_connection
|
||||||
|
|
||||||
router = APIRouter(prefix="/results", tags=["results"])
|
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
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ async def list_reviews():
|
|||||||
with get_connection() as conn:
|
with get_connection() as conn:
|
||||||
reviews = conn.execute("SELECT * FROM Review").fetchdf()
|
reviews = conn.execute("SELECT * FROM Review").fetchdf()
|
||||||
if reviews.empty:
|
if reviews.empty:
|
||||||
raise HTTPException(status_code=404, detail="Reviews not found")
|
return []
|
||||||
return reviews.to_dict(orient="records")
|
return reviews.to_dict(orient="records")
|
||||||
|
|
||||||
|
|
||||||
@@ -25,7 +25,7 @@ async def get_review(song_id: int, user_id: int):
|
|||||||
(song_id, user_id),
|
(song_id, user_id),
|
||||||
).fetchdf()
|
).fetchdf()
|
||||||
if review.empty:
|
if review.empty:
|
||||||
raise HTTPException(status_code=404, detail="Review not found")
|
return []
|
||||||
return review.to_dict(orient="records")[0]
|
return review.to_dict(orient="records")[0]
|
||||||
|
|
||||||
|
|
||||||
@@ -93,18 +93,38 @@ async def update_review(review: ReviewIn):
|
|||||||
f"AND user_id = {review.user_id}"
|
f"AND user_id = {review.user_id}"
|
||||||
)
|
)
|
||||||
|
|
||||||
# Update the review
|
# Check if the review exists
|
||||||
conn.execute(
|
review_df = conn.execute(
|
||||||
"UPDATE Review SET score_song = ?, score_show = ?, score_costume = ?, text_review = ? WHERE song_id = ? AND user_id = ?",
|
"SELECT * FROM Review WHERE song_id = ? AND user_id = ?",
|
||||||
(
|
(review.song_id, review.user_id),
|
||||||
review.score_song,
|
).fetchdf()
|
||||||
review.score_show,
|
|
||||||
review.score_costume,
|
if not review_df.empty:
|
||||||
review.text_review,
|
# Update the review
|
||||||
review.song_id,
|
conn.execute(
|
||||||
review.user_id,
|
"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
|
# Fetch the updated review
|
||||||
logger.debug(
|
logger.debug(
|
||||||
|
|||||||
|
After Width: | Height: | Size: 6.8 KiB |
|
After Width: | Height: | Size: 4.3 KiB |
|
After Width: | Height: | Size: 4.6 KiB |
|
After Width: | Height: | Size: 3.3 KiB |
|
After Width: | Height: | Size: 3.5 KiB |
|
After Width: | Height: | Size: 4.2 KiB |
|
After Width: | Height: | Size: 2.9 KiB |
|
After Width: | Height: | Size: 3.8 KiB |
|
After Width: | Height: | Size: 6.3 KiB |
|
After Width: | Height: | Size: 4.7 KiB |
|
After Width: | Height: | Size: 10 KiB |
|
After Width: | Height: | Size: 10 KiB |
|
After Width: | Height: | Size: 2.8 KiB |
|
After Width: | Height: | Size: 6.9 KiB |
|
After Width: | Height: | Size: 14 KiB |
|
After Width: | Height: | Size: 5.8 KiB |
|
After Width: | Height: | Size: 6.8 KiB |
|
After Width: | Height: | Size: 4.5 KiB |
|
After Width: | Height: | Size: 12 KiB |
|
After Width: | Height: | Size: 3.3 KiB |
|
After Width: | Height: | Size: 2.1 KiB |
|
After Width: | Height: | Size: 5.7 KiB |
|
After Width: | Height: | Size: 1.4 KiB |
|
After Width: | Height: | Size: 3.1 KiB |
|
After Width: | Height: | Size: 5.3 KiB |
|
After Width: | Height: | Size: 2.6 KiB |
|
After Width: | Height: | Size: 2.5 KiB |