Automate diary entries and add photo galleries
This commit is contained in:
+105
-32
@@ -426,7 +426,7 @@ def ensure_schema():
|
||||
CREATE TABLE IF NOT EXISTS concert_diary (
|
||||
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
concert_id INTEGER NOT NULL REFERENCES concerts(id) ON DELETE CASCADE,
|
||||
rating SMALLINT NOT NULL CHECK (rating BETWEEN 1 AND 5),
|
||||
rating SMALLINT CHECK (rating BETWEEN 1 AND 5),
|
||||
favorite_song VARCHAR(255),
|
||||
notes TEXT,
|
||||
photo_path TEXT,
|
||||
@@ -439,6 +439,27 @@ def ensure_schema():
|
||||
ALTER TABLE concert_diary ADD COLUMN IF NOT EXISTS photo_path TEXT
|
||||
""",
|
||||
"""
|
||||
ALTER TABLE concert_diary ALTER COLUMN rating DROP NOT NULL
|
||||
""",
|
||||
"""
|
||||
CREATE TABLE IF NOT EXISTS concert_diary_photos (
|
||||
id SERIAL PRIMARY KEY,
|
||||
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
concert_id INTEGER NOT NULL REFERENCES concerts(id) ON DELETE CASCADE,
|
||||
path TEXT NOT NULL UNIQUE,
|
||||
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||
)
|
||||
""",
|
||||
"""
|
||||
CREATE INDEX IF NOT EXISTS idx_concert_diary_photos_entry
|
||||
ON concert_diary_photos (user_id, concert_id, created_at)
|
||||
""",
|
||||
"""
|
||||
INSERT INTO concert_diary_photos (user_id, concert_id, path)
|
||||
SELECT user_id, concert_id, photo_path FROM concert_diary WHERE photo_path IS NOT NULL
|
||||
ON CONFLICT (path) DO NOTHING
|
||||
""",
|
||||
"""
|
||||
CREATE TABLE IF NOT EXISTS concert_bands (
|
||||
concert_id INTEGER NOT NULL REFERENCES concerts(id) ON DELETE CASCADE,
|
||||
band_key VARCHAR(255) NOT NULL,
|
||||
@@ -2793,6 +2814,11 @@ def export_profile_data(request: Request):
|
||||
(user_id,),
|
||||
)
|
||||
diary_entries = cursor.fetchall()
|
||||
cursor.execute(
|
||||
"SELECT id, concert_id, path, created_at FROM concert_diary_photos WHERE user_id = %s ORDER BY created_at",
|
||||
(user_id,),
|
||||
)
|
||||
diary_photos = cursor.fetchall()
|
||||
|
||||
cursor.execute(
|
||||
"""
|
||||
@@ -2830,6 +2856,7 @@ def export_profile_data(request: Request):
|
||||
"followed_bands": rows_to_dicts(followed_bands, ("band_key", "display_name", "created_at")),
|
||||
"followed_venues": rows_to_dicts(followed_venues, ("venue_id", "created_at")),
|
||||
"concert_diary": rows_to_dicts(diary_entries, ("concert_id", "rating", "favorite_song", "notes", "photo_path", "created_at", "updated_at")),
|
||||
"concert_diary_photos": rows_to_dicts(diary_photos, ("id", "concert_id", "path", "created_at")),
|
||||
"badges": rows_to_dicts(badges, ("badge_code", "awarded_at", "trigger_concert_id")),
|
||||
}
|
||||
filename = re.sub(r"[^A-Za-z0-9_-]", "_", user["username"])
|
||||
@@ -2854,7 +2881,7 @@ def delete_own_account(request: Request):
|
||||
photo_paths = [row[0] for row in cursor.fetchall()]
|
||||
cursor.execute("SELECT flyer_path FROM concerts WHERE created_by = %s AND flyer_path IS NOT NULL", (user_id,))
|
||||
flyer_paths = [row[0] for row in cursor.fetchall()]
|
||||
cursor.execute("SELECT photo_path FROM concert_diary WHERE user_id = %s AND photo_path IS NOT NULL", (user_id,))
|
||||
cursor.execute("SELECT path FROM concert_diary_photos WHERE user_id = %s", (user_id,))
|
||||
diary_photo_paths = [row[0] for row in cursor.fetchall()]
|
||||
cursor.execute("UPDATE registration_invites SET used_by = NULL WHERE used_by = %s", (user_id,))
|
||||
cursor.execute("UPDATE concerts SET flyer_path = NULL WHERE created_by = %s", (user_id,))
|
||||
@@ -3395,11 +3422,22 @@ def diary_page(request: Request):
|
||||
user = get_current_user(request)
|
||||
with get_db_connection() as connection:
|
||||
with connection.cursor() as cursor:
|
||||
cursor.execute(
|
||||
"""
|
||||
INSERT INTO concert_diary (user_id, concert_id)
|
||||
SELECT ca.user_id, ca.concert_id
|
||||
FROM concert_attendance ca JOIN concerts c ON c.id = ca.concert_id
|
||||
WHERE ca.user_id = %s AND ca.status = 'attending'
|
||||
AND COALESCE(c.end_datetime, c.start_datetime) < CURRENT_TIMESTAMP
|
||||
ON CONFLICT (user_id, concert_id) DO NOTHING
|
||||
""",
|
||||
(user["id"],),
|
||||
)
|
||||
cursor.execute(
|
||||
"""
|
||||
SELECT d.concert_id, c.artist, c.start_datetime, d.rating,
|
||||
COALESCE(d.favorite_song, ''), COALESCE(d.notes, ''),
|
||||
COALESCE(v.name, ''), COALESCE(v.city, ''), d.photo_path
|
||||
COALESCE(v.name, ''), COALESCE(v.city, '')
|
||||
FROM concert_diary d JOIN concerts c ON c.id = d.concert_id
|
||||
LEFT JOIN venues v ON v.id = c.venue_id
|
||||
WHERE d.user_id = %s ORDER BY c.start_datetime DESC
|
||||
@@ -3409,9 +3447,17 @@ def diary_page(request: Request):
|
||||
entries = [
|
||||
{"concert_id": row[0], "artist": row[1], "date": row[2].strftime("%d.%m.%Y"),
|
||||
"rating": row[3], "favorite_song": row[4], "notes": row[5],
|
||||
"venue": ", ".join(filter(None, (row[6], row[7]))), "photo_path": row[8]}
|
||||
"venue": ", ".join(filter(None, (row[6], row[7]))), "photos": []}
|
||||
for row in cursor.fetchall()
|
||||
]
|
||||
entry_by_concert = {entry["concert_id"]: entry for entry in entries}
|
||||
cursor.execute(
|
||||
"SELECT id, concert_id, path FROM concert_diary_photos WHERE user_id = %s ORDER BY created_at, id",
|
||||
(user["id"],),
|
||||
)
|
||||
for photo_id, concert_id, path in cursor.fetchall():
|
||||
if concert_id in entry_by_concert:
|
||||
entry_by_concert[concert_id]["photos"].append({"id": photo_id, "path": path})
|
||||
cursor.execute(
|
||||
"""
|
||||
SELECT ub.badge_code, ub.trigger_concert_id, ub.awarded_at, ba.path
|
||||
@@ -3432,6 +3478,7 @@ def diary_page(request: Request):
|
||||
})
|
||||
for entry in entries:
|
||||
entry["badges"] = diary_badges.get(entry["concert_id"], [])
|
||||
connection.commit()
|
||||
return templates.get_template("diary.html").render(user=user, entries=entries)
|
||||
|
||||
|
||||
@@ -3444,7 +3491,7 @@ def diary_photo(request: Request, filename: str):
|
||||
with get_db_connection() as connection:
|
||||
with connection.cursor() as cursor:
|
||||
cursor.execute(
|
||||
"SELECT 1 FROM concert_diary WHERE user_id = %s AND photo_path = %s",
|
||||
"SELECT 1 FROM concert_diary_photos WHERE user_id = %s AND path = %s",
|
||||
(user["id"], photo_path),
|
||||
)
|
||||
if not cursor.fetchone():
|
||||
@@ -3457,16 +3504,20 @@ def diary_photo(request: Request, filename: str):
|
||||
|
||||
@app.post("/concerts/{concert_id}/diary")
|
||||
def save_diary_entry(
|
||||
request: Request, concert_id: int, rating: int = Form(...),
|
||||
request: Request, concert_id: int, rating: str = Form(""),
|
||||
favorite_song: str = Form(""), notes: str = Form(""),
|
||||
photo: UploadFile | None = File(None), remove_photo: str = Form("")
|
||||
photos: list[UploadFile] = File(default=[])
|
||||
):
|
||||
user = get_current_user(request)
|
||||
concert = load_concert(concert_id)
|
||||
if not concert or not can_view_event(user, concert):
|
||||
return HTMLResponse("Veranstaltung nicht gefunden.", status_code=404)
|
||||
if not concert["is_past"] or rating not in range(1, 6):
|
||||
return HTMLResponse("Das Tagebuch ist nur für vergangene Konzerte mit einer Bewertung von 1 bis 5 verfügbar.", status_code=400)
|
||||
try:
|
||||
rating_value = int(rating) if rating else None
|
||||
except ValueError:
|
||||
rating_value = None
|
||||
if not concert["is_past"] or (rating_value is not None and rating_value not in range(1, 6)):
|
||||
return HTMLResponse("Das Tagebuch ist nur für vergangene Konzerte mit einer optionalen Bewertung von 1 bis 5 verfügbar.", status_code=400)
|
||||
favorite_song = favorite_song.strip()
|
||||
notes = notes.strip()
|
||||
if len(favorite_song) > 255 or len(notes) > 5000:
|
||||
@@ -3479,31 +3530,47 @@ def save_diary_entry(
|
||||
)
|
||||
if not cursor.fetchone():
|
||||
return HTMLResponse("Ein Tagebucheintrag ist nur für besuchte Konzerte möglich.", status_code=403)
|
||||
cursor.execute(
|
||||
"SELECT photo_path FROM concert_diary WHERE user_id = %s AND concert_id = %s",
|
||||
(user["id"], concert_id),
|
||||
)
|
||||
previous_row = cursor.fetchone()
|
||||
previous_photo = previous_row[0] if previous_row else None
|
||||
new_photo, error = save_image(photo, DIARY_PHOTO_DIR, "/diary/photo/")
|
||||
if error:
|
||||
return error
|
||||
photo_path = new_photo or (None if remove_photo else previous_photo)
|
||||
cursor.execute("SELECT COUNT(*) FROM concert_diary_photos WHERE user_id = %s AND concert_id = %s", (user["id"], concert_id))
|
||||
existing_photo_count = cursor.fetchone()[0]
|
||||
uploads = [photo for photo in photos if photo and photo.filename]
|
||||
if existing_photo_count + len(uploads) > 3:
|
||||
return HTMLResponse("Pro Tagebucheintrag sind maximal drei Bilder möglich.", status_code=400)
|
||||
saved_paths = []
|
||||
for photo in uploads:
|
||||
saved_path, error = save_image(photo, DIARY_PHOTO_DIR, "/diary/photo/")
|
||||
if error:
|
||||
for path in saved_paths:
|
||||
remove_uploaded_file(path, DIARY_PHOTO_DIR, "/diary/photo/")
|
||||
return error
|
||||
saved_paths.append(saved_path)
|
||||
cursor.execute(
|
||||
"""
|
||||
INSERT INTO concert_diary (user_id, concert_id, rating, favorite_song, notes, photo_path)
|
||||
VALUES (%s, %s, %s, %s, %s, %s)
|
||||
INSERT INTO concert_diary (user_id, concert_id, rating, favorite_song, notes)
|
||||
VALUES (%s, %s, %s, %s, %s)
|
||||
ON CONFLICT (user_id, concert_id) DO UPDATE SET
|
||||
rating = EXCLUDED.rating, favorite_song = EXCLUDED.favorite_song,
|
||||
notes = EXCLUDED.notes, photo_path = EXCLUDED.photo_path,
|
||||
updated_at = CURRENT_TIMESTAMP
|
||||
notes = EXCLUDED.notes, updated_at = CURRENT_TIMESTAMP
|
||||
""",
|
||||
(user["id"], concert_id, rating, favorite_song or None, notes or None, photo_path),
|
||||
(user["id"], concert_id, rating_value, favorite_song or None, notes or None),
|
||||
)
|
||||
for path in saved_paths:
|
||||
cursor.execute("INSERT INTO concert_diary_photos (user_id, concert_id, path) VALUES (%s, %s, %s)", (user["id"], concert_id, path))
|
||||
connection.commit()
|
||||
if previous_photo and previous_photo != photo_path:
|
||||
remove_uploaded_file(previous_photo, DIARY_PHOTO_DIR, "/diary/photo/")
|
||||
return RedirectResponse(f"/concerts/{concert_id}#diary", status_code=303)
|
||||
return RedirectResponse(f"/diary#concert-{concert_id}", status_code=303)
|
||||
|
||||
|
||||
@app.post("/diary/photos/{photo_id}/delete")
|
||||
def delete_diary_photo(request: Request, photo_id: int):
|
||||
user = get_current_user(request)
|
||||
with get_db_connection() as connection:
|
||||
with connection.cursor() as cursor:
|
||||
cursor.execute("DELETE FROM concert_diary_photos WHERE id = %s AND user_id = %s RETURNING path, concert_id", (photo_id, user["id"]))
|
||||
deleted = cursor.fetchone()
|
||||
connection.commit()
|
||||
if not deleted:
|
||||
return HTMLResponse("Bild nicht gefunden.", status_code=404)
|
||||
remove_uploaded_file(deleted[0], DIARY_PHOTO_DIR, "/diary/photo/")
|
||||
return RedirectResponse(f"/diary#concert-{deleted[1]}", status_code=303)
|
||||
|
||||
|
||||
@app.post("/concerts/{concert_id}/diary/delete")
|
||||
@@ -3511,13 +3578,14 @@ def delete_diary_entry(request: Request, concert_id: int):
|
||||
user = get_current_user(request)
|
||||
with get_db_connection() as connection:
|
||||
with connection.cursor() as cursor:
|
||||
cursor.execute("SELECT photo_path FROM concert_diary WHERE user_id = %s AND concert_id = %s", (user["id"], concert_id))
|
||||
row = cursor.fetchone()
|
||||
cursor.execute("SELECT path FROM concert_diary_photos WHERE user_id = %s AND concert_id = %s", (user["id"], concert_id))
|
||||
photo_paths = [row[0] for row in cursor.fetchall()]
|
||||
cursor.execute("DELETE FROM concert_diary_photos WHERE user_id = %s AND concert_id = %s", (user["id"], concert_id))
|
||||
cursor.execute("DELETE FROM concert_diary WHERE user_id = %s AND concert_id = %s", (user["id"], concert_id))
|
||||
connection.commit()
|
||||
if row and row[0]:
|
||||
remove_uploaded_file(row[0], DIARY_PHOTO_DIR, "/diary/photo/")
|
||||
return RedirectResponse(f"/concerts/{concert_id}#diary", status_code=303)
|
||||
for path in photo_paths:
|
||||
remove_uploaded_file(path, DIARY_PHOTO_DIR, "/diary/photo/")
|
||||
return RedirectResponse("/diary", status_code=303)
|
||||
|
||||
|
||||
# ============================================================
|
||||
@@ -4190,6 +4258,11 @@ def set_attendance(
|
||||
""",
|
||||
(concert_id, user["id"], status),
|
||||
)
|
||||
if status == "attending" and concert["is_past"]:
|
||||
cursor.execute(
|
||||
"INSERT INTO concert_diary (user_id, concert_id) VALUES (%s, %s) ON CONFLICT DO NOTHING",
|
||||
(user["id"], concert_id),
|
||||
)
|
||||
connection.commit()
|
||||
|
||||
return RedirectResponse(f"/concerts/{concert_id}#attendance", status_code=303)
|
||||
|
||||
Reference in New Issue
Block a user