9 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
Esa Kataja 512729398b Release 1.0rc5 2025-05-16 13:13:28 +03:00
Esa Kataja 3f7b6d10f9 Fix default documentation paths 2025-05-16 13:02:41 +03:00
Esa Kataja c404582c03 Change default doc path for security reasons 2025-05-16 12:54:31 +03:00
Esa Kataja e1c3f6e7c5 Add version update docs 2025-05-16 12:54:05 +03:00
10 changed files with 190 additions and 11 deletions
+11
View File
@@ -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
+3
View File
@@ -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
View File
@@ -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:
+11
View File
@@ -0,0 +1,11 @@
# Version update
1. Update version in `pyproject.toml`
2. Update version in `uv.lock`
3. Update version in `src/app.py`
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
View File
@@ -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"
+4 -1
View File
@@ -12,7 +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.0rc3",
version="1.0rc6",
openapi_url="/sec_schema.json",
docs_url="/docut",
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(
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
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(
Generated
+4 -4
View File
@@ -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" },