diff --git a/app/main.py b/app/main.py index 38ca41d..5235143 100644 --- a/app/main.py +++ b/app/main.py @@ -42,9 +42,17 @@ AVATAR_DIR = os.path.join( "avatars" ) +PATCH_DIR = os.path.join( + BASE_DIR, + "static", + "uploads", + "patches" +) + os.makedirs(UPLOAD_DIR, exist_ok=True) os.makedirs(PHOTO_DIR, exist_ok=True) os.makedirs(AVATAR_DIR, exist_ok=True) +os.makedirs(PATCH_DIR, exist_ok=True) SESSION_COOKIE = "pingu_session" SESSION_DAYS = 30 @@ -55,11 +63,22 @@ INITIAL_ADMIN_PASSWORD = os.environ.get("INITIAL_ADMIN_PASSWORD") INITIAL_ADMIN_EMAIL = os.environ.get("INITIAL_ADMIN_EMAIL") BADGE_DEFINITIONS = ( - ("first_gig", "Erster Gig", "🎸", 1, "Dein erstes besuchtes Konzert"), - ("regular", "Stammgast", "🤘", 5, "5 Konzerte besucht"), - ("ten_gigs", "Zehnerrunde", "🔥", 10, "10 Konzerte besucht"), - ("tour_veteran", "Tourveteran", "⚡", 25, "25 Konzerte besucht"), + ("beta_tester", "Beta Tester", "🧪", None, "In der Beta dabei", "beta"), + ("first_gig", "Erster Gig", "🎸", 1, "Dein erstes besuchtes Konzert", "attendance"), + ("regular", "Stammgast", "🤘", 5, "5 Konzerte besucht", "attendance"), + ("ten_gigs", "Zehnerrunde", "🔥", 10, "10 Konzerte besucht", "attendance"), + ("tour_veteran", "Tourveteran", "⚡", 25, "25 Konzerte besucht", "attendance"), ) +ATTENDANCE_BADGE_CODES = tuple( + badge_code + for badge_code, _name, _icon, _threshold, _description, category in BADGE_DEFINITIONS + if category == "attendance" +) +BETA_REGISTRATION_DEADLINE = datetime(2026, 9, 16) +BADGE_BY_CODE = { + badge_code: (name, icon, threshold, description, category) + for badge_code, name, icon, threshold, description, category in BADGE_DEFINITIONS +} def get_db_connection(): @@ -147,6 +166,14 @@ def ensure_schema(): PRIMARY KEY (user_id, badge_code) ) """, + """ + CREATE TABLE IF NOT EXISTS badge_assets ( + badge_code VARCHAR(50) PRIMARY KEY, + path TEXT NOT NULL, + updated_by INTEGER REFERENCES users(id) ON DELETE SET NULL, + updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP + ) + """, ] with get_db_connection() as connection: @@ -496,22 +523,48 @@ def attended_concert_count(user_id: int) -> int: return cursor.fetchone()[0] -def grant_earned_badges(user_id: int, attended_count: int): +def grant_earned_badges(user_id: int, attended_count: int, registered_at): + highest_attendance_badge = None + + for badge_code, _name, _icon, threshold, _description, category in BADGE_DEFINITIONS: + if category == "attendance" and attended_count >= threshold: + highest_attendance_badge = badge_code + with get_db_connection() as connection: with connection.cursor() as cursor: - for badge_code, _name, _icon, threshold, _description in BADGE_DEFINITIONS: - if attended_count >= threshold: - cursor.execute( - """ - INSERT INTO user_badges (user_id, badge_code) - VALUES (%s, %s) - ON CONFLICT (user_id, badge_code) DO NOTHING - """, - (user_id, badge_code), - ) + if registered_at < BETA_REGISTRATION_DEADLINE: + cursor.execute( + """ + INSERT INTO user_badges (user_id, badge_code) + VALUES (%s, 'beta_tester') + ON CONFLICT (user_id, badge_code) DO NOTHING + """, + (user_id,), + ) + + if highest_attendance_badge: + cursor.execute( + "DELETE FROM user_badges WHERE user_id = %s AND badge_code = ANY(%s)", + (user_id, list(ATTENDANCE_BADGE_CODES)), + ) + cursor.execute( + """ + INSERT INTO user_badges (user_id, badge_code) + VALUES (%s, %s) + ON CONFLICT (user_id, badge_code) DO NOTHING + """, + (user_id, highest_attendance_badge), + ) connection.commit() +def load_badge_assets(): + with get_db_connection() as connection: + with connection.cursor() as cursor: + cursor.execute("SELECT badge_code, path FROM badge_assets") + return {row[0]: row[1] for row in cursor.fetchall()} + + def load_profile(username: str): with get_db_connection() as connection: with connection.cursor() as cursor: @@ -540,6 +593,7 @@ def load_profile(username: str): "display_name": row[2] or row[1], "avatar_path": row[3], "created_at": row[4].strftime("%d.%m.%Y"), + "registered_at": row[4], "earned_codes": earned_codes, } @@ -1134,8 +1188,13 @@ def render_profile(request: Request, username: str): return HTMLResponse("
Für besuchte Konzerte schaltest du neue Patches frei.
+Deine freigeschalteten Patches. Besuchs-Patches werden jeweils durch die höchste Stufe ersetzt.
Noch keine Patches – dein erstes Konzert wartet!
+ {% endif %}