Compare commits
7
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2cb255b538 | ||
|
|
230eb35cf2 | ||
|
|
c43b6f2e54 | ||
|
|
a6f61a3c5b | ||
|
|
49347e463a | ||
|
|
512729398b | ||
|
|
3f7b6d10f9 |
+11
@@ -1,5 +1,16 @@
|
||||
# Eurovision 25 Backend Changelog
|
||||
|
||||
## 1.0rc6 (2025-05-16)
|
||||
|
||||
### Features
|
||||
- Added ability for users to change their password and avatar image
|
||||
|
||||
## 1.0rc5 (2025-05-16)
|
||||
|
||||
### Changes
|
||||
- Updated dependencies to latest versions
|
||||
- Changed API documentation for security reasons
|
||||
|
||||
## 1.0rc4 (2025-05-16)
|
||||
|
||||
### Features
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
# Eurovision 25 Homereview Backend
|
||||
|
||||
**Version: 1.0rc6**
|
||||
|
||||
## Description
|
||||
|
||||
This project provides the backend API for the "Eurovision 25 Homereview" application. It allows a small, known group of users, organized into households/groups, to collaboratively review and score Eurovision Song Contest entries during the live show. Users submit numerical scores (1-100) for stage show, wardrobe, and song quality, along with optional text comments. Users can update their scores until the contest is marked as finished by an admin. Afterward, users can view aggregated results within their group or across all participants, including basic statistics.
|
||||
@@ -224,6 +226,7 @@ Automated tests are not implemented at this time. Manual testing via the API doc
|
||||
* [x] Implement more comprehensive logging
|
||||
* [ ] Implement user disabling functionality
|
||||
* [x] Implement user profile update functionality (name, email, password, avatar, etc.)
|
||||
* [x] Fix avatar upload size validation
|
||||
|
||||
## Contributing
|
||||
|
||||
|
||||
+1
-1
@@ -8,7 +8,7 @@ services:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: Dockerfile
|
||||
image: eurovision-25-backend:1.0rc4
|
||||
image: eurovision-25-backend:1.0rc6
|
||||
ports:
|
||||
- "8000:8000"
|
||||
volumes:
|
||||
|
||||
@@ -3,8 +3,9 @@
|
||||
1. Update version in `pyproject.toml`
|
||||
2. Update version in `uv.lock`
|
||||
3. Update version in `src/app.py`
|
||||
4. Update `CHANGES.md`. Take info from git log
|
||||
5. Update `README.md`
|
||||
6. Commit changes
|
||||
7. Tag release
|
||||
8. Push changes
|
||||
4. Update version in `docker-compose.yml`
|
||||
5. Update `CHANGES.md`. Take info from git log
|
||||
6. Update `README.md`
|
||||
7. Commit changes
|
||||
8. Tag release
|
||||
9. Push changes
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
[project]
|
||||
name = "Eurovision-25-backend"
|
||||
version = "1.0rc4"
|
||||
version = "1.0rc6"
|
||||
description = "Backend for Eurovision 25"
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.12"
|
||||
|
||||
+3
-3
@@ -12,10 +12,10 @@ from lib.db import init_db
|
||||
app = FastAPI(
|
||||
title="Eurovision 25 Homereview API",
|
||||
description="Backend API for Eurovision 25 Homereview application",
|
||||
version="1.0rc4",
|
||||
openapi_url=None,
|
||||
version="1.0rc6",
|
||||
openapi_url="/sec_schema.json",
|
||||
docs_url="/docut",
|
||||
redoc_url="/redocut",
|
||||
redoc_url=None,
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,150 @@
|
||||
-- PostgreSQL Schema Definition for Eurovision 25 Homereview Backend
|
||||
-- Converted from DuckDB schema
|
||||
-- Target Database: PostgreSQL
|
||||
-- Note: Timestamps default to creation time; application logic should handle 'updated_at' updates.
|
||||
|
||||
-- Drop objects in reverse order of dependency and existence check
|
||||
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 CountryCodes;
|
||||
|
||||
-- =============================================================================
|
||||
-- Table: Group
|
||||
-- Stores information about the different households or groups participating.
|
||||
-- =============================================================================
|
||||
CREATE TABLE "Team" (
|
||||
id INTEGER GENERATED ALWAYS AS IDENTITY PRIMARY KEY, -- Use IDENTITY for auto-increment
|
||||
name VARCHAR(255) UNIQUE NOT NULL, -- The name of the household/group
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, -- Timestamp when the group was created
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP -- Timestamp when the group was last updated
|
||||
);
|
||||
|
||||
-- =============================================================================
|
||||
-- Table: User
|
||||
-- Stores information about individual users (players and admins).
|
||||
-- =============================================================================
|
||||
CREATE TABLE "User" (
|
||||
id INTEGER GENERATED ALWAYS AS IDENTITY PRIMARY KEY, -- Use IDENTITY for auto-increment
|
||||
username VARCHAR(255) UNIQUE NOT NULL, -- The user's login name
|
||||
hashed_password VARCHAR(255) NOT NULL, -- The securely hashed password
|
||||
email VARCHAR(255) UNIQUE, -- User's email address (nullable)
|
||||
team_id INTEGER NOT NULL, -- Foreign Key -> Group.id
|
||||
avatar_id INTEGER DEFAULT 0 NOT NULL, -- Identifier for the user's avatar
|
||||
is_active BOOLEAN DEFAULT TRUE NOT NULL, -- Flag indicating if the user account is active
|
||||
is_admin BOOLEAN DEFAULT FALSE NOT NULL, -- Flag indicating if the user has admin privileges
|
||||
last_login TIMESTAMP DEFAULT '1970-01-01 00:00:00', -- Timestamp when the user was last logged in. Epoch (1970-01-01) means "never logged in"
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, -- Timestamp when the user was created
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, -- Timestamp when the user was last updated
|
||||
|
||||
FOREIGN KEY (team_id) REFERENCES "Team"(id) -- Link to the Team table
|
||||
);
|
||||
|
||||
-- =============================================================================
|
||||
-- Table: Song
|
||||
-- Stores information about each participating song in the contest.
|
||||
-- =============================================================================
|
||||
CREATE TABLE Song (
|
||||
id INTEGER GENERATED ALWAYS AS IDENTITY PRIMARY KEY, -- Use IDENTITY for auto-increment
|
||||
year INTEGER NOT NULL, -- The year of the Eurovision contest
|
||||
country VARCHAR(255) NOT NULL, -- The participating country name
|
||||
artist VARCHAR(255) NOT NULL, -- The name of the performing artist(s)
|
||||
title VARCHAR(255) NOT NULL, -- The title of the song
|
||||
running_order INTEGER NOT NULL, -- Official order in the show (nullable)
|
||||
lyrics_url VARCHAR(255), -- URL to the original lyrics (nullable)
|
||||
artist_url VARCHAR(255), -- URL to the artist's page (nullable)
|
||||
flag_url VARCHAR(255), -- URL to the country flag (nullable)
|
||||
lyrics_original TEXT, -- Original lyrics (nullable)
|
||||
lyrics_translation_fi TEXT, -- Finnish translation (nullable)
|
||||
tags TEXT[], -- Array of tags (PostgreSQL array type)
|
||||
confidence FLOAT NOT NULL DEFAULT 0.0, -- Confidence level of the translation (nullable)
|
||||
language VARCHAR(50) NOT NULL, -- Language of the song (nullable)
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, -- Timestamp when the song entry was created
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP -- Timestamp when the song entry was last updated
|
||||
);
|
||||
|
||||
-- Add index for faster lookups by year as specified in documentation
|
||||
CREATE INDEX idx_song_year ON Song (year);
|
||||
|
||||
-- =============================================================================
|
||||
-- Table: Review
|
||||
-- Stores the scores and comments submitted by a user for a specific song.
|
||||
-- =============================================================================
|
||||
CREATE TABLE Review (
|
||||
id INTEGER GENERATED ALWAYS AS IDENTITY PRIMARY KEY, -- Use IDENTITY for auto-increment
|
||||
user_id INTEGER NOT NULL, -- Foreign Key -> User.id
|
||||
song_id INTEGER NOT NULL, -- Foreign Key -> Song.id
|
||||
score_song INTEGER NOT NULL, -- Score (1-100) for song quality
|
||||
score_show INTEGER NOT NULL, -- Score (1-100) for the stage show
|
||||
score_costume INTEGER NOT NULL, -- Score (1-100) for wardrobe/costumes
|
||||
text_review TEXT, -- Optional textual comments (nullable)
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, -- Timestamp when the review was created
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, -- Timestamp when the review was last modified
|
||||
|
||||
FOREIGN KEY (user_id) REFERENCES "User"(id), -- Link to the User table
|
||||
FOREIGN KEY (song_id) REFERENCES Song(id), -- Link to the Song table
|
||||
|
||||
-- Ensure scores are within the valid range (1-100)
|
||||
CHECK (score_song >= 1 AND score_song <= 100),
|
||||
CHECK (score_show >= 1 AND score_show <= 100),
|
||||
CHECK (score_costume >= 1 AND score_costume <= 100),
|
||||
|
||||
-- Ensure each user can only submit one review per song
|
||||
UNIQUE (user_id, song_id)
|
||||
);
|
||||
|
||||
CREATE TABLE CountryCodes (
|
||||
code VARCHAR(10) PRIMARY KEY,
|
||||
name_en VARCHAR(255) NOT NULL,
|
||||
name_fi VARCHAR(255) NOT NULL,
|
||||
name_sv VARCHAR(255) NOT NULL
|
||||
);
|
||||
|
||||
-- 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');
|
||||
INSERT INTO CountryCodes (code, name_en, name_fi, name_sv) VALUES ('AUS', 'Australia', 'Australia', 'Australien');
|
||||
INSERT INTO CountryCodes (code, name_en, name_fi, name_sv) VALUES ('AUT', 'Austria', 'Itävalta', 'Österrike');
|
||||
INSERT INTO CountryCodes (code, name_en, name_fi, name_sv) VALUES ('AZE', 'Azerbaijan', 'Azerbaidžan', 'Azerbajdzjan');
|
||||
INSERT INTO CountryCodes (code, name_en, name_fi, name_sv) VALUES ('BEL', 'Belgium', 'Belgia', 'Belgien');
|
||||
INSERT INTO CountryCodes (code, name_en, name_fi, name_sv) VALUES ('HRV', 'Croatia', 'Kroatia', 'Kroatien');
|
||||
INSERT INTO CountryCodes (code, name_en, name_fi, name_sv) VALUES ('CYP', 'Cyprus', 'Kypros', 'Cypern');
|
||||
INSERT INTO CountryCodes (code, name_en, name_fi, name_sv) VALUES ('CZE', 'Czechia', 'Tšekki', 'Tjeckien');
|
||||
INSERT INTO CountryCodes (code, name_en, name_fi, name_sv) VALUES ('DNK', 'Denmark', 'Tanska', 'Danmark');
|
||||
INSERT INTO CountryCodes (code, name_en, name_fi, name_sv) VALUES ('EST', 'Estonia', 'Viro', 'Estland');
|
||||
INSERT INTO CountryCodes (code, name_en, name_fi, name_sv) VALUES ('FIN', 'Finland', 'Suomi', 'Finland');
|
||||
INSERT INTO CountryCodes (code, name_en, name_fi, name_sv) VALUES ('FRA', 'France', 'Ranska', 'Frankrike');
|
||||
INSERT INTO CountryCodes (code, name_en, name_fi, name_sv) VALUES ('GEO', 'Georgia', 'Georgia', 'Georgien');
|
||||
INSERT INTO CountryCodes (code, name_en, name_fi, name_sv) VALUES ('DEU', 'Germany', 'Saksa', 'Tyskland');
|
||||
INSERT INTO CountryCodes (code, name_en, name_fi, name_sv) VALUES ('GRC', 'Greece', 'Kreikka', 'Grekland');
|
||||
INSERT INTO CountryCodes (code, name_en, name_fi, name_sv) VALUES ('ISL', 'Iceland', 'Islanti', 'Island');
|
||||
INSERT INTO CountryCodes (code, name_en, name_fi, name_sv) VALUES ('IRL', 'Ireland', 'Irlanti', 'Irland');
|
||||
INSERT INTO CountryCodes (code, name_en, name_fi, name_sv) VALUES ('ISR', 'Israel', 'Israel', 'Israel');
|
||||
INSERT INTO CountryCodes (code, name_en, name_fi, name_sv) VALUES ('ITA', 'Italy', 'Italia', 'Italien');
|
||||
INSERT INTO CountryCodes (code, name_en, name_fi, name_sv) VALUES ('LVA', 'Latvia', 'Latvia', 'Lettland');
|
||||
INSERT INTO CountryCodes (code, name_en, name_fi, name_sv) VALUES ('LTU', 'Lithuania', 'Liettua', 'Litauen');
|
||||
INSERT INTO CountryCodes (code, name_en, name_fi, name_sv) VALUES ('LUX', 'Luxembourg', 'Luxemburg', 'Luxemburg');
|
||||
INSERT INTO CountryCodes (code, name_en, name_fi, name_sv) VALUES ('MLT', 'Malta', 'Malta', 'Malta');
|
||||
INSERT INTO CountryCodes (code, name_en, name_fi, name_sv) VALUES ('MNE', 'Montenegro', 'Montenegro', 'Montenegro');
|
||||
INSERT INTO CountryCodes (code, name_en, name_fi, name_sv) VALUES ('NLD', 'Netherlands', 'Alankomaat', 'Nederländerna');
|
||||
INSERT INTO CountryCodes (code, name_en, name_fi, name_sv) VALUES ('NOR', 'Norway', 'Norja', 'Norge');
|
||||
INSERT INTO CountryCodes (code, name_en, name_fi, name_sv) VALUES ('POL', 'Poland', 'Puola', 'Polen');
|
||||
INSERT INTO CountryCodes (code, name_en, name_fi, name_sv) VALUES ('PRT', 'Portugal', 'Portugali', 'Portugal');
|
||||
INSERT INTO CountryCodes (code, name_en, name_fi, name_sv) VALUES ('SMR', 'San Marino', 'San Marino', 'San Marino');
|
||||
INSERT INTO CountryCodes (code, name_en, name_fi, name_sv) VALUES ('SRB', 'Serbia', 'Serbia', 'Serbien');
|
||||
INSERT INTO CountryCodes (code, name_en, name_fi, name_sv) VALUES ('SVN', 'Slovenia', 'Slovenia', 'Slovenien');
|
||||
INSERT INTO CountryCodes (code, name_en, name_fi, name_sv) VALUES ('ESP', 'Spain', 'Espanja', 'Spanien');
|
||||
INSERT INTO CountryCodes (code, name_en, name_fi, name_sv) VALUES ('SWE', 'Sweden', 'Ruotsi', 'Sverige');
|
||||
INSERT INTO CountryCodes (code, name_en, name_fi, name_sv) VALUES ('CHE', 'Switzerland', 'Sveitsi', 'Schweiz');
|
||||
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');
|
||||
|
||||
-- =============================================================================
|
||||
-- Views (Optional - Not Created Here Based on Documentation Decision)
|
||||
-- =============================================================================
|
||||
-- The documentation suggests performing result aggregations in the application
|
||||
-- layer. No CREATE VIEW statements are included.
|
||||
-- =============================================================================
|
||||
|
||||
-- End of Schema Definition
|
||||
@@ -55,3 +55,4 @@ class UpdateUser(BaseModel):
|
||||
password: str | None = Field(
|
||||
None, description="Password of the user", examples=["p1p4l1"]
|
||||
)
|
||||
updated_at: datetime = Field(default=datetime.now(), description="Updated at")
|
||||
|
||||
+4
-4
@@ -47,7 +47,6 @@ async def update_user(user: UpdateUser, request: Request):
|
||||
avatar_id = user.avatar_id if user.avatar_id else None
|
||||
email = user.email if user.email else None
|
||||
password = hash_password(user.password) if user.password else None
|
||||
updated_at = datetime.now()
|
||||
|
||||
with get_connection() as conn:
|
||||
# Build query dynamically based on non-None values
|
||||
@@ -63,14 +62,15 @@ async def update_user(user: UpdateUser, request: Request):
|
||||
params.append(email)
|
||||
|
||||
if password is not None:
|
||||
update_fields.append("password = ?")
|
||||
update_fields.append("hashed_password = ?")
|
||||
params.append(password)
|
||||
|
||||
# Only proceed if there are fields to update
|
||||
if update_fields:
|
||||
query = f'UPDATE "User" SET {", ".join(update_fields)}, updated_at = ? WHERE id = ?'
|
||||
params.append(updated_at)
|
||||
query = f'UPDATE "User" SET {", ".join(update_fields)}, updated_at = CURRENT_TIMESTAMP WHERE id = ?'
|
||||
params.append(user_id)
|
||||
logger.debug(f"Executing query: {query}")
|
||||
logger.debug(f"Query parameters: {params}")
|
||||
conn.execute(query, params)
|
||||
|
||||
logger.debug(
|
||||
|
||||
@@ -86,14 +86,14 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "click"
|
||||
version = "8.1.8"
|
||||
version = "8.2.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "colorama", marker = "sys_platform == 'win32'" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/b9/2e/0090cbf739cee7d23781ad4b89a9894a41538e4fcf4c31dcdd705b78eb8b/click-8.1.8.tar.gz", hash = "sha256:ed53c9d8990d83c2a27deae68e4ee337473f6330c040a31d4225c9574d16096a", size = 226593, upload-time = "2024-12-21T18:38:44.339Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/cd/0f/62ca20172d4f87d93cf89665fbaedcd560ac48b465bd1d92bfc7ea6b0a41/click-8.2.0.tar.gz", hash = "sha256:f5452aeddd9988eefa20f90f05ab66f17fce1ee2a36907fd30b05bbb5953814d", size = 235857, upload-time = "2025-05-10T22:21:03.111Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/7e/d4/7ebdbd03970677812aac39c869717059dbb71a4cfc033ca6e5221787892c/click-8.1.8-py3-none-any.whl", hash = "sha256:63c132bbbed01578a06712a2d1f497bb62d9c1c0d329b7903a866228027263b2", size = 98188, upload-time = "2024-12-21T18:38:41.666Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a2/58/1f37bf81e3c689cc74ffa42102fa8915b59085f54a6e4a80bc6265c0f6bf/click-8.2.0-py3-none-any.whl", hash = "sha256:6b303f0b2aa85f1cb4e5303078fadcbcd4e476f114fab9b5007005711839325c", size = 102156, upload-time = "2025-05-10T22:21:01.352Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -131,7 +131,7 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "eurovision-25-backend"
|
||||
version = "1.0rc4"
|
||||
version = "1.0rc6"
|
||||
source = { virtual = "." }
|
||||
dependencies = [
|
||||
{ name = "aiofiles" },
|
||||
|
||||
Reference in New Issue
Block a user