Rebrand application as MetalCircle
This commit is contained in:
+189
-8
@@ -21,7 +21,8 @@ except ImportError:
|
||||
Image = ImageOps = UnidentifiedImageError = None
|
||||
|
||||
from fastapi import FastAPI, File, Form, Request, UploadFile
|
||||
from fastapi.responses import HTMLResponse, RedirectResponse
|
||||
from fastapi.responses import HTMLResponse, JSONResponse, RedirectResponse
|
||||
from fastapi.encoders import jsonable_encoder
|
||||
from fastapi.staticfiles import StaticFiles
|
||||
|
||||
from jinja2 import Environment, FileSystemLoader, select_autoescape
|
||||
@@ -77,8 +78,8 @@ INITIAL_ADMIN_PASSWORD = os.environ.get("INITIAL_ADMIN_PASSWORD")
|
||||
INITIAL_ADMIN_EMAIL = os.environ.get("INITIAL_ADMIN_EMAIL")
|
||||
|
||||
BADGE_DEFINITIONS = (
|
||||
("founder", "Gründer", "⚔️", None, "Von Anfang an dabei und Pingu Concerts mit aufgebaut", "special"),
|
||||
("admin", "Admin", "🏴☠️", None, "Verantwortung für Pingu Concerts", "special"),
|
||||
("founder", "Gründer", "⚔️", None, "Von Anfang an dabei und MetalCircle mit aufgebaut", "special"),
|
||||
("admin", "Admin", "🏴☠️", None, "Verantwortung für MetalCircle", "special"),
|
||||
("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 am selben Veranstaltungsort besucht", "attendance"),
|
||||
@@ -405,7 +406,7 @@ async def lifespan(_app: FastAPI):
|
||||
yield
|
||||
|
||||
|
||||
app = FastAPI(title="Pingu Concerts", lifespan=lifespan)
|
||||
app = FastAPI(title="MetalCircle", lifespan=lifespan)
|
||||
|
||||
|
||||
@app.middleware("http")
|
||||
@@ -458,6 +459,8 @@ async def require_login(request: Request, call_next):
|
||||
or request.url.path == "/register"
|
||||
or request.url.path.startswith("/register/")
|
||||
or request.url.path.startswith("/password-reset")
|
||||
or request.url.path in {"/impressum", "/datenschutz"}
|
||||
or request.url.path == "/profile/export"
|
||||
or request.url.path.startswith("/static/")
|
||||
):
|
||||
return await call_next(request)
|
||||
@@ -2037,6 +2040,18 @@ def register_page(token: str):
|
||||
|
||||
|
||||
# ============================================================
|
||||
# Rechtliche Hinweise
|
||||
|
||||
@app.get("/impressum", response_class=HTMLResponse)
|
||||
def impressum_page():
|
||||
return templates.get_template("impressum.html").render()
|
||||
|
||||
|
||||
@app.get("/datenschutz", response_class=HTMLResponse)
|
||||
def privacy_page():
|
||||
return templates.get_template("datenschutz.html").render()
|
||||
|
||||
|
||||
# Login
|
||||
# ============================================================
|
||||
|
||||
@@ -2280,6 +2295,134 @@ def own_profile(request: Request, saved: str = ""):
|
||||
)
|
||||
|
||||
|
||||
@app.get("/profile/export")
|
||||
def export_profile_data(request: Request):
|
||||
"""Download the authenticated user's application data as JSON."""
|
||||
user = get_current_user(request)
|
||||
if not user:
|
||||
return login_redirect("/profile/export")
|
||||
|
||||
user_id = user["id"]
|
||||
with get_db_connection() as connection:
|
||||
with connection.cursor() as cursor:
|
||||
cursor.execute(
|
||||
"""
|
||||
SELECT id, username, email, display_name, avatar_path,
|
||||
instagram_url, profile_visibility, is_admin, created_at
|
||||
FROM users WHERE id = %s
|
||||
""",
|
||||
(user_id,),
|
||||
)
|
||||
account = cursor.fetchone()
|
||||
|
||||
cursor.execute(
|
||||
"""
|
||||
SELECT c.id, c.artist, c.event_type, c.start_datetime, c.end_datetime,
|
||||
c.venue_id, c.description, c.ticket_url, c.ticket_price,
|
||||
c.flyer_path, c.flyer_url, c.visibility, c.created_at
|
||||
FROM concerts c WHERE c.created_by = %s ORDER BY c.start_datetime
|
||||
""",
|
||||
(user_id,),
|
||||
)
|
||||
created_events = cursor.fetchall()
|
||||
|
||||
cursor.execute(
|
||||
"""
|
||||
SELECT concert_id, status, updated_at
|
||||
FROM concert_attendance WHERE user_id = %s ORDER BY updated_at
|
||||
""",
|
||||
(user_id,),
|
||||
)
|
||||
attendance = cursor.fetchall()
|
||||
|
||||
cursor.execute(
|
||||
"""
|
||||
SELECT id, requester_id, addressee_id, status, created_at, updated_at
|
||||
FROM friendships WHERE requester_id = %s OR addressee_id = %s
|
||||
ORDER BY created_at
|
||||
""",
|
||||
(user_id, user_id),
|
||||
)
|
||||
friendships = cursor.fetchall()
|
||||
|
||||
cursor.execute(
|
||||
"""
|
||||
SELECT id, sender_id, recipient_id, body, read_at, created_at
|
||||
FROM direct_messages WHERE sender_id = %s OR recipient_id = %s
|
||||
ORDER BY created_at
|
||||
""",
|
||||
(user_id, user_id),
|
||||
)
|
||||
messages = cursor.fetchall()
|
||||
|
||||
cursor.execute(
|
||||
"""
|
||||
SELECT concert_id, invited_by, viewed_at, created_at
|
||||
FROM event_invitations WHERE user_id = %s ORDER BY created_at
|
||||
""",
|
||||
(user_id,),
|
||||
)
|
||||
invitations = cursor.fetchall()
|
||||
|
||||
cursor.execute(
|
||||
"""
|
||||
SELECT id, concert_id, body, created_at
|
||||
FROM concert_comments WHERE user_id = %s ORDER BY created_at
|
||||
""",
|
||||
(user_id,),
|
||||
)
|
||||
comments = cursor.fetchall()
|
||||
|
||||
cursor.execute(
|
||||
"""
|
||||
SELECT id, concert_id, path, created_at
|
||||
FROM concert_photos WHERE user_id = %s ORDER BY created_at
|
||||
""",
|
||||
(user_id,),
|
||||
)
|
||||
photos = cursor.fetchall()
|
||||
|
||||
cursor.execute(
|
||||
"""
|
||||
SELECT badge_code, awarded_at FROM user_badges
|
||||
WHERE user_id = %s ORDER BY awarded_at
|
||||
""",
|
||||
(user_id,),
|
||||
)
|
||||
badges = cursor.fetchall()
|
||||
|
||||
def rows_to_dicts(rows, keys):
|
||||
return [dict(zip(keys, row)) for row in rows]
|
||||
|
||||
data = {
|
||||
"export_version": 1,
|
||||
"exported_at": datetime.now(),
|
||||
"account": dict(zip(
|
||||
("id", "username", "email", "display_name", "avatar_path",
|
||||
"instagram_url", "profile_visibility", "is_admin", "created_at"),
|
||||
account,
|
||||
)) if account else None,
|
||||
"created_events": rows_to_dicts(
|
||||
created_events,
|
||||
("id", "artist", "event_type", "start_datetime", "end_datetime", "venue_id",
|
||||
"description", "ticket_url", "ticket_price", "flyer_path", "flyer_url",
|
||||
"visibility", "created_at"),
|
||||
),
|
||||
"attendance": rows_to_dicts(attendance, ("concert_id", "status", "updated_at")),
|
||||
"friendships": rows_to_dicts(friendships, ("id", "requester_id", "addressee_id", "status", "created_at", "updated_at")),
|
||||
"messages": rows_to_dicts(messages, ("id", "sender_id", "recipient_id", "body", "read_at", "created_at")),
|
||||
"event_invitations": rows_to_dicts(invitations, ("concert_id", "invited_by", "viewed_at", "created_at")),
|
||||
"comments": rows_to_dicts(comments, ("id", "concert_id", "body", "created_at")),
|
||||
"photos": rows_to_dicts(photos, ("id", "concert_id", "path", "created_at")),
|
||||
"badges": rows_to_dicts(badges, ("badge_code", "awarded_at")),
|
||||
}
|
||||
filename = re.sub(r"[^A-Za-z0-9_-]", "_", user["username"])
|
||||
return JSONResponse(
|
||||
content=jsonable_encoder(data),
|
||||
headers={"Content-Disposition": f'attachment; filename="pingu-concerts-{filename}-daten.json"'},
|
||||
)
|
||||
|
||||
|
||||
@app.get("/users/{username}", response_class=HTMLResponse)
|
||||
def user_profile(request: Request, username: str):
|
||||
return render_profile(request, username)
|
||||
@@ -2375,7 +2518,7 @@ def send_friend_request(request: Request, username: str):
|
||||
|
||||
|
||||
@app.post("/friendships/{friendship_id}/{action}")
|
||||
def manage_friendship(request: Request, friendship_id: int, action: str):
|
||||
def manage_friendship(request: Request, friendship_id: int, action: str, return_to: str = Form("")):
|
||||
user = get_current_user(request)
|
||||
if action not in {"accept", "decline", "remove"}:
|
||||
return HTMLResponse("Ungültige Aktion.", status_code=400)
|
||||
@@ -2405,6 +2548,8 @@ def manage_friendship(request: Request, friendship_id: int, action: str):
|
||||
with connection.cursor() as cursor:
|
||||
cursor.execute("SELECT username FROM users WHERE id = %s", (other_id,))
|
||||
other = cursor.fetchone()
|
||||
if return_to == "/messages":
|
||||
return RedirectResponse("/messages", status_code=303)
|
||||
return RedirectResponse(f"/users/{other[0]}" if other else "/", status_code=303)
|
||||
|
||||
|
||||
@@ -2440,8 +2585,41 @@ def load_chat_partner(cursor, user, username: str):
|
||||
def message_inbox(request: Request):
|
||||
user = get_current_user(request)
|
||||
conversations = []
|
||||
friend_requests = []
|
||||
event_invitations = []
|
||||
with get_db_connection() as connection:
|
||||
with connection.cursor() as cursor:
|
||||
cursor.execute(
|
||||
"""
|
||||
SELECT f.id, u.username, COALESCE(u.display_name, u.username), f.created_at
|
||||
FROM friendships f JOIN users u ON u.id = f.requester_id
|
||||
WHERE f.addressee_id = %s AND f.status = 'pending'
|
||||
ORDER BY f.created_at DESC
|
||||
""",
|
||||
(user["id"],),
|
||||
)
|
||||
friend_requests = [
|
||||
{"id": row[0], "username": row[1], "display_name": row[2],
|
||||
"created_at": row[3].strftime("%d.%m.%Y %H:%M")}
|
||||
for row in cursor.fetchall()
|
||||
]
|
||||
cursor.execute(
|
||||
"""
|
||||
SELECT ei.concert_id, c.artist, c.start_datetime,
|
||||
COALESCE(u.display_name, u.username)
|
||||
FROM event_invitations ei
|
||||
JOIN concerts c ON c.id = ei.concert_id
|
||||
LEFT JOIN users u ON u.id = ei.invited_by
|
||||
WHERE ei.user_id = %s AND ei.viewed_at IS NULL
|
||||
ORDER BY ei.created_at DESC
|
||||
""",
|
||||
(user["id"],),
|
||||
)
|
||||
event_invitations = [
|
||||
{"concert_id": row[0], "artist": row[1],
|
||||
"date": row[2].strftime("%d.%m.%Y %H:%M"), "invited_by": row[3] or "Ein Mitglied"}
|
||||
for row in cursor.fetchall()
|
||||
]
|
||||
cursor.execute(
|
||||
"""
|
||||
SELECT u.id, u.username, COALESCE(u.display_name, u.username), u.avatar_path
|
||||
@@ -2475,11 +2653,14 @@ def message_inbox(request: Request):
|
||||
"id": row[0], "username": row[1], "display_name": row[2],
|
||||
"avatar_path": row[3], "last_message": latest[0] if latest else None,
|
||||
"last_at": latest[1].strftime("%d.%m.%Y %H:%M") if latest else None,
|
||||
"last_at_raw": latest[1] if latest else None,
|
||||
"last_from_me": bool(latest and latest[2] == user["id"]),
|
||||
"unread_count": latest[3] if latest else 0,
|
||||
})
|
||||
conversations.sort(key=lambda item: (item["unread_count"] > 0, item["last_at_raw"] or datetime.min), reverse=True)
|
||||
template = templates.get_template("messages.html")
|
||||
return template.render(user=user, conversations=conversations, partner=None, messages=[])
|
||||
return template.render(user=user, conversations=conversations, friend_requests=friend_requests,
|
||||
event_invitations=event_invitations, partner=None, messages=[])
|
||||
|
||||
|
||||
@app.get("/messages/{username}", response_class=HTMLResponse)
|
||||
@@ -3375,7 +3556,7 @@ def legacy_search_venues(q: str):
|
||||
if not results:
|
||||
|
||||
headers = {
|
||||
"User-Agent": "PinguConcerts/1.0"
|
||||
"User-Agent": "MetalCircle/1.0"
|
||||
}
|
||||
|
||||
external_query = q
|
||||
@@ -3811,7 +3992,7 @@ def search_venues(q: str):
|
||||
"namedetails": 1,
|
||||
"limit": 25,
|
||||
},
|
||||
headers={"User-Agent": "PinguConcerts/1.0"},
|
||||
headers={"User-Agent": "MetalCircle/1.0"},
|
||||
timeout=8,
|
||||
)
|
||||
response.raise_for_status()
|
||||
|
||||
Reference in New Issue
Block a user