5 Commits
Author SHA1 Message Date
Esa Kataja 2cb255b538 1.0rc6 release: Add user password and avatar change functionality 2025-05-16 14:36:22 +03:00
Esa Kataja 230eb35cf2 Add sql schema for postgres 2025-05-16 14:35:07 +03:00
Esa Kataja c43b6f2e54 Add user update endpoint 2025-05-16 14:31:03 +03:00
Esa Kataja a6f61a3c5b Update TODOS 2025-05-16 14:29:59 +03:00
Esa Kataja 49347e463a Update workflow 2025-05-16 14:29:42 +03:00
10 changed files with 173 additions and 25 deletions
+6 -9
View File
@@ -1,18 +1,15 @@
# Eurovision 25 Backend Changelog # 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) ## 1.0rc5 (2025-05-16)
### Features
- Added vote weighting system for jury scores
- Improved performance for results calculation
### Fixes
- Fixed inconsistent sorting in results view
- Fixed avatar upload size validation
### Changes ### Changes
- Updated dependencies to latest versions - Updated dependencies to latest versions
- Enhanced API documentation - Changed API documentation for security reasons
## 1.0rc4 (2025-05-16) ## 1.0rc4 (2025-05-16)
+2 -3
View File
@@ -1,6 +1,6 @@
# Eurovision 25 Homereview Backend # Eurovision 25 Homereview Backend
**Version: 1.0rc5** **Version: 1.0rc6**
## Description ## Description
@@ -224,9 +224,8 @@ Automated tests are not implemented at this time. Manual testing via the API doc
* [ ] Replace password hashing with passlib * [ ] Replace password hashing with passlib
* [ ] Switch to PostgreSQL from DuckDB * [ ] Switch to PostgreSQL from DuckDB
* [x] Implement more comprehensive logging * [x] Implement more comprehensive logging
* [x] Implement user disabling functionality * [ ] Implement user disabling functionality
* [x] Implement user profile update functionality (name, email, password, avatar, etc.) * [x] Implement user profile update functionality (name, email, password, avatar, etc.)
* [x] Add vote weighting system for jury scores
* [x] Fix avatar upload size validation * [x] Fix avatar upload size validation
## Contributing ## Contributing
+1 -1
View File
@@ -8,7 +8,7 @@ services:
build: build:
context: . context: .
dockerfile: Dockerfile dockerfile: Dockerfile
image: eurovision-25-backend:1.0rc5 image: eurovision-25-backend:1.0rc6
ports: ports:
- "8000:8000" - "8000:8000"
volumes: volumes:
+6 -5
View File
@@ -3,8 +3,9 @@
1. Update version in `pyproject.toml` 1. Update version in `pyproject.toml`
2. Update version in `uv.lock` 2. Update version in `uv.lock`
3. Update version in `src/app.py` 3. Update version in `src/app.py`
4. Update `CHANGES.md`. Take info from git log 4. Update version in `docker-compose.yml`
5. Update `README.md` 5. Update `CHANGES.md`. Take info from git log
6. Commit changes 6. Update `README.md`
7. Tag release 7. Commit changes
8. Push changes 8. Tag release
9. Push changes
+1 -1
View File
@@ -1,6 +1,6 @@
[project] [project]
name = "Eurovision-25-backend" name = "Eurovision-25-backend"
version = "1.0rc5" version = "1.0rc6"
description = "Backend for Eurovision 25" description = "Backend for Eurovision 25"
readme = "README.md" readme = "README.md"
requires-python = ">=3.12" requires-python = ">=3.12"
+1 -1
View File
@@ -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.0rc5", version="1.0rc6",
openapi_url="/sec_schema.json", openapi_url="/sec_schema.json",
docs_url="/docut", docs_url="/docut",
redoc_url=None, redoc_url=None,
+150
View File
@@ -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
+1
View File
@@ -55,3 +55,4 @@ class UpdateUser(BaseModel):
password: str | None = Field( password: str | None = Field(
None, description="Password of the user", examples=["p1p4l1"] None, description="Password of the user", examples=["p1p4l1"]
) )
updated_at: datetime = Field(default=datetime.now(), description="Updated at")
+4 -4
View File
@@ -47,7 +47,6 @@ async def update_user(user: UpdateUser, request: Request):
avatar_id = user.avatar_id if user.avatar_id else None avatar_id = user.avatar_id if user.avatar_id else None
email = user.email if user.email else None email = user.email if user.email else None
password = hash_password(user.password) if user.password else None password = hash_password(user.password) if user.password else None
updated_at = datetime.now()
with get_connection() as conn: with get_connection() as conn:
# Build query dynamically based on non-None values # Build query dynamically based on non-None values
@@ -63,14 +62,15 @@ async def update_user(user: UpdateUser, request: Request):
params.append(email) params.append(email)
if password is not None: if password is not None:
update_fields.append("password = ?") update_fields.append("hashed_password = ?")
params.append(password) params.append(password)
# Only proceed if there are fields to update # Only proceed if there are fields to update
if update_fields: if update_fields:
query = f'UPDATE "User" SET {", ".join(update_fields)}, updated_at = ? WHERE id = ?' query = f'UPDATE "User" SET {", ".join(update_fields)}, updated_at = CURRENT_TIMESTAMP WHERE id = ?'
params.append(updated_at)
params.append(user_id) params.append(user_id)
logger.debug(f"Executing query: {query}")
logger.debug(f"Query parameters: {params}")
conn.execute(query, params) conn.execute(query, params)
logger.debug( logger.debug(
Generated
+1 -1
View File
@@ -131,7 +131,7 @@ wheels = [
[[package]] [[package]]
name = "eurovision-25-backend" name = "eurovision-25-backend"
version = "1.0rc5" version = "1.0rc6"
source = { virtual = "." } source = { virtual = "." }
dependencies = [ dependencies = [
{ name = "aiofiles" }, { name = "aiofiles" },