User Profile sind nun verfügbar
This commit is contained in:
+177
@@ -35,8 +35,16 @@ PHOTO_DIR = os.path.join(
|
||||
"photos"
|
||||
)
|
||||
|
||||
AVATAR_DIR = os.path.join(
|
||||
BASE_DIR,
|
||||
"static",
|
||||
"uploads",
|
||||
"avatars"
|
||||
)
|
||||
|
||||
os.makedirs(UPLOAD_DIR, exist_ok=True)
|
||||
os.makedirs(PHOTO_DIR, exist_ok=True)
|
||||
os.makedirs(AVATAR_DIR, exist_ok=True)
|
||||
|
||||
SESSION_COOKIE = "pingu_session"
|
||||
SESSION_DAYS = 30
|
||||
@@ -46,6 +54,13 @@ INITIAL_ADMIN_USERNAME = os.environ.get("INITIAL_ADMIN_USERNAME")
|
||||
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"),
|
||||
)
|
||||
|
||||
|
||||
def get_db_connection():
|
||||
return psycopg.connect(DATABASE_URL)
|
||||
@@ -69,6 +84,10 @@ def ensure_schema():
|
||||
ADD COLUMN IF NOT EXISTS is_admin BOOLEAN NOT NULL DEFAULT FALSE
|
||||
""",
|
||||
"""
|
||||
ALTER TABLE users
|
||||
ADD COLUMN IF NOT EXISTS avatar_path TEXT
|
||||
""",
|
||||
"""
|
||||
CREATE TABLE IF NOT EXISTS registration_invites (
|
||||
id SERIAL PRIMARY KEY,
|
||||
token_hash TEXT NOT NULL UNIQUE,
|
||||
@@ -120,6 +139,14 @@ def ensure_schema():
|
||||
PRIMARY KEY (concert_id, user_id)
|
||||
)
|
||||
""",
|
||||
"""
|
||||
CREATE TABLE IF NOT EXISTS user_badges (
|
||||
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
badge_code VARCHAR(50) NOT NULL,
|
||||
awarded_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (user_id, badge_code)
|
||||
)
|
||||
""",
|
||||
]
|
||||
|
||||
with get_db_connection() as connection:
|
||||
@@ -449,6 +476,74 @@ def save_image(upload: UploadFile, destination_dir: str, url_prefix: str):
|
||||
return f"{url_prefix}{filename}", None
|
||||
|
||||
|
||||
def attended_concert_count(user_id: int) -> int:
|
||||
with get_db_connection() as connection:
|
||||
with connection.cursor() as cursor:
|
||||
cursor.execute(
|
||||
"""
|
||||
SELECT COUNT(*)
|
||||
FROM concert_attendance
|
||||
JOIN concerts ON concerts.id = concert_attendance.concert_id
|
||||
WHERE concert_attendance.user_id = %s
|
||||
AND concert_attendance.status = 'attending'
|
||||
AND COALESCE(
|
||||
concerts.end_datetime,
|
||||
concerts.start_datetime
|
||||
) < CURRENT_TIMESTAMP
|
||||
""",
|
||||
(user_id,),
|
||||
)
|
||||
return cursor.fetchone()[0]
|
||||
|
||||
|
||||
def grant_earned_badges(user_id: int, attended_count: int):
|
||||
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),
|
||||
)
|
||||
connection.commit()
|
||||
|
||||
|
||||
def load_profile(username: str):
|
||||
with get_db_connection() as connection:
|
||||
with connection.cursor() as cursor:
|
||||
cursor.execute(
|
||||
"""
|
||||
SELECT id, username, display_name, avatar_path, created_at
|
||||
FROM users
|
||||
WHERE LOWER(username) = LOWER(%s)
|
||||
""",
|
||||
(username,),
|
||||
)
|
||||
row = cursor.fetchone()
|
||||
|
||||
if not row:
|
||||
return None
|
||||
|
||||
cursor.execute(
|
||||
"SELECT badge_code FROM user_badges WHERE user_id = %s",
|
||||
(row[0],),
|
||||
)
|
||||
earned_codes = {badge_row[0] for badge_row in cursor.fetchall()}
|
||||
|
||||
return {
|
||||
"id": row[0],
|
||||
"username": row[1],
|
||||
"display_name": row[2] or row[1],
|
||||
"avatar_path": row[3],
|
||||
"created_at": row[4].strftime("%d.%m.%Y"),
|
||||
"earned_codes": earned_codes,
|
||||
}
|
||||
|
||||
|
||||
def resolve_venue(
|
||||
cursor,
|
||||
venue_id: str,
|
||||
@@ -1027,6 +1122,86 @@ def home(request: Request):
|
||||
)
|
||||
|
||||
|
||||
# ============================================================
|
||||
# Profiles
|
||||
# ============================================================
|
||||
|
||||
def render_profile(request: Request, username: str):
|
||||
viewer = get_current_user(request)
|
||||
profile = load_profile(username)
|
||||
|
||||
if not profile:
|
||||
return HTMLResponse("<h1>Benutzer nicht gefunden</h1>", status_code=404)
|
||||
|
||||
attended_count = attended_concert_count(profile["id"])
|
||||
grant_earned_badges(profile["id"], attended_count)
|
||||
profile = load_profile(username)
|
||||
badges = [
|
||||
{
|
||||
"code": code,
|
||||
"name": name,
|
||||
"icon": icon,
|
||||
"threshold": threshold,
|
||||
"description": description,
|
||||
"earned": code in profile["earned_codes"],
|
||||
}
|
||||
for code, name, icon, threshold, description in BADGE_DEFINITIONS
|
||||
]
|
||||
|
||||
template = templates.get_template("profile.html")
|
||||
return template.render(
|
||||
user=viewer,
|
||||
profile=profile,
|
||||
attended_count=attended_count,
|
||||
badges=badges,
|
||||
is_own_profile=viewer["id"] == profile["id"],
|
||||
)
|
||||
|
||||
|
||||
@app.get("/profile", response_class=HTMLResponse)
|
||||
def own_profile(request: Request):
|
||||
user = get_current_user(request)
|
||||
return render_profile(request, user["username"])
|
||||
|
||||
|
||||
@app.get("/users/{username}", response_class=HTMLResponse)
|
||||
def user_profile(request: Request, username: str):
|
||||
return render_profile(request, username)
|
||||
|
||||
|
||||
@app.post("/profile")
|
||||
async def update_profile(
|
||||
request: Request,
|
||||
display_name: str = Form(""),
|
||||
avatar: UploadFile | None = File(None),
|
||||
):
|
||||
user = get_current_user(request)
|
||||
avatar_path, error = save_image(
|
||||
avatar,
|
||||
AVATAR_DIR,
|
||||
"/static/uploads/avatars/",
|
||||
)
|
||||
|
||||
if error:
|
||||
return error
|
||||
|
||||
with get_db_connection() as connection:
|
||||
with connection.cursor() as cursor:
|
||||
cursor.execute(
|
||||
"""
|
||||
UPDATE users
|
||||
SET
|
||||
display_name = COALESCE(NULLIF(%s, ''), display_name),
|
||||
avatar_path = COALESCE(%s, avatar_path)
|
||||
WHERE id = %s
|
||||
""",
|
||||
(display_name.strip(), avatar_path, user["id"]),
|
||||
)
|
||||
connection.commit()
|
||||
|
||||
return RedirectResponse("/profile", status_code=303)
|
||||
|
||||
|
||||
# ============================================================
|
||||
# New concert
|
||||
# ============================================================
|
||||
@@ -1124,6 +1299,7 @@ def concert_detail(request: Request, concert_id: int):
|
||||
"body": row[1],
|
||||
"created_at": row[2].strftime("%d.%m.%Y %H:%M"),
|
||||
"author": row[3] or row[4],
|
||||
"username": row[4],
|
||||
}
|
||||
for row in comment_rows
|
||||
]
|
||||
@@ -1143,6 +1319,7 @@ def concert_detail(request: Request, concert_id: int):
|
||||
"user_id": row[0],
|
||||
"status": row[1],
|
||||
"name": row[2] or row[3],
|
||||
"username": row[3],
|
||||
}
|
||||
for row in attendance_rows
|
||||
]
|
||||
|
||||
Reference in New Issue
Block a user