Add private event invitations
This commit is contained in:
+191
-37
@@ -78,6 +78,7 @@ INITIAL_ADMIN_EMAIL = os.environ.get("INITIAL_ADMIN_EMAIL")
|
|||||||
|
|
||||||
BADGE_DEFINITIONS = (
|
BADGE_DEFINITIONS = (
|
||||||
("founder", "Gründer", "⚔️", None, "Von Anfang an dabei und Pingu Concerts mit aufgebaut", "special"),
|
("founder", "Gründer", "⚔️", None, "Von Anfang an dabei und Pingu Concerts mit aufgebaut", "special"),
|
||||||
|
("admin", "Admin", "🛡️", None, "Verantwortung für Pingu Concerts", "special"),
|
||||||
("beta_tester", "Beta Tester", "🧪", None, "In der Beta dabei", "beta"),
|
("beta_tester", "Beta Tester", "🧪", None, "In der Beta dabei", "beta"),
|
||||||
("first_gig", "Erster Gig", "🎸", 1, "Dein erstes besuchtes Konzert", "attendance"),
|
("first_gig", "Erster Gig", "🎸", 1, "Dein erstes besuchtes Konzert", "attendance"),
|
||||||
("regular", "Stammgast", "🤘", 5, "5 Konzerte besucht", "attendance"),
|
("regular", "Stammgast", "🤘", 5, "5 Konzerte besucht", "attendance"),
|
||||||
@@ -299,6 +300,24 @@ def ensure_schema():
|
|||||||
ALTER TABLE concerts ADD COLUMN IF NOT EXISTS flyer_url TEXT
|
ALTER TABLE concerts ADD COLUMN IF NOT EXISTS flyer_url TEXT
|
||||||
""",
|
""",
|
||||||
"""
|
"""
|
||||||
|
ALTER TABLE concerts ADD COLUMN IF NOT EXISTS visibility VARCHAR(20) NOT NULL DEFAULT 'public'
|
||||||
|
CHECK (visibility IN ('public', 'friends', 'private'))
|
||||||
|
""",
|
||||||
|
"""
|
||||||
|
CREATE TABLE IF NOT EXISTS event_invitations (
|
||||||
|
concert_id INTEGER NOT NULL REFERENCES concerts(id) ON DELETE CASCADE,
|
||||||
|
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||||
|
invited_by INTEGER REFERENCES users(id) ON DELETE SET NULL,
|
||||||
|
viewed_at TIMESTAMP,
|
||||||
|
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
PRIMARY KEY (concert_id, user_id)
|
||||||
|
)
|
||||||
|
""",
|
||||||
|
"""
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_event_invitations_user
|
||||||
|
ON event_invitations (user_id, viewed_at)
|
||||||
|
""",
|
||||||
|
"""
|
||||||
CREATE TABLE IF NOT EXISTS friendships (
|
CREATE TABLE IF NOT EXISTS friendships (
|
||||||
id SERIAL PRIMARY KEY,
|
id SERIAL PRIMARY KEY,
|
||||||
requester_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
requester_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||||
@@ -511,7 +530,8 @@ def user_from_row(row):
|
|||||||
"is_admin": bool(row[3]),
|
"is_admin": bool(row[3]),
|
||||||
"pending_friend_count": row[4],
|
"pending_friend_count": row[4],
|
||||||
"unread_message_count": row[5],
|
"unread_message_count": row[5],
|
||||||
"notification_count": row[4] + row[5],
|
"event_invitation_count": row[6],
|
||||||
|
"notification_count": row[4] + row[5] + row[6],
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -533,7 +553,9 @@ def get_current_user(request: Request):
|
|||||||
(SELECT COUNT(*) FROM friendships
|
(SELECT COUNT(*) FROM friendships
|
||||||
WHERE addressee_id = users.id AND status = 'pending'),
|
WHERE addressee_id = users.id AND status = 'pending'),
|
||||||
(SELECT COUNT(*) FROM direct_messages
|
(SELECT COUNT(*) FROM direct_messages
|
||||||
WHERE recipient_id = users.id AND read_at IS NULL)
|
WHERE recipient_id = users.id AND read_at IS NULL),
|
||||||
|
(SELECT COUNT(*) FROM event_invitations
|
||||||
|
WHERE user_id = users.id AND viewed_at IS NULL)
|
||||||
FROM sessions
|
FROM sessions
|
||||||
JOIN users
|
JOIN users
|
||||||
ON users.id = sessions.user_id
|
ON users.id = sessions.user_id
|
||||||
@@ -700,7 +722,8 @@ def load_concert(concert_id: int):
|
|||||||
concerts.parent_event_id,
|
concerts.parent_event_id,
|
||||||
parent_event.artist,
|
parent_event.artist,
|
||||||
parent_event.event_type,
|
parent_event.event_type,
|
||||||
concerts.flyer_url
|
concerts.flyer_url,
|
||||||
|
concerts.visibility
|
||||||
FROM concerts
|
FROM concerts
|
||||||
LEFT JOIN venues
|
LEFT JOIN venues
|
||||||
ON concerts.venue_id = venues.id
|
ON concerts.venue_id = venues.id
|
||||||
@@ -729,6 +752,7 @@ def load_concert(concert_id: int):
|
|||||||
"ticket_price": row[6],
|
"ticket_price": row[6],
|
||||||
"flyer_path": row[7],
|
"flyer_path": row[7],
|
||||||
"flyer_url": row[21],
|
"flyer_url": row[21],
|
||||||
|
"visibility": row[22],
|
||||||
"created_by": row[8],
|
"created_by": row[8],
|
||||||
"is_past": is_past,
|
"is_past": is_past,
|
||||||
"date": start_datetime.strftime("%d.%m.%Y"),
|
"date": start_datetime.strftime("%d.%m.%Y"),
|
||||||
@@ -786,8 +810,12 @@ def can_delete_concert(user, concert) -> bool:
|
|||||||
return user["is_admin"] or user["id"] == concert["created_by"]
|
return user["is_admin"] or user["id"] == concert["created_by"]
|
||||||
|
|
||||||
|
|
||||||
|
def can_manage_event_access(user, concert) -> bool:
|
||||||
|
return bool(user and (user["is_admin"] or user["id"] == concert["created_by"]))
|
||||||
|
|
||||||
|
|
||||||
def serialize_concert_card(row):
|
def serialize_concert_card(row):
|
||||||
concert_id, artist, start_datetime, end_datetime, venue, city, event_type, parent_event_id = row
|
concert_id, artist, start_datetime, end_datetime, venue, city, event_type, parent_event_id, visibility, is_invited = row
|
||||||
venue_text = venue or "Veranstaltungsort unbekannt"
|
venue_text = venue or "Veranstaltungsort unbekannt"
|
||||||
if city:
|
if city:
|
||||||
venue_text += f", {city}"
|
venue_text += f", {city}"
|
||||||
@@ -802,6 +830,8 @@ def serialize_concert_card(row):
|
|||||||
"event_type": event_type,
|
"event_type": event_type,
|
||||||
"event_type_label": EVENT_TYPES[event_type],
|
"event_type_label": EVENT_TYPES[event_type],
|
||||||
"parent_event_id": parent_event_id,
|
"parent_event_id": parent_event_id,
|
||||||
|
"visibility": visibility,
|
||||||
|
"is_invited": bool(is_invited),
|
||||||
"children": [],
|
"children": [],
|
||||||
"_start_datetime": start_datetime,
|
"_start_datetime": start_datetime,
|
||||||
"_end_datetime": end_datetime,
|
"_end_datetime": end_datetime,
|
||||||
@@ -830,18 +860,38 @@ def build_event_overview(rows):
|
|||||||
return upcoming, past
|
return upcoming, past
|
||||||
|
|
||||||
|
|
||||||
def load_event_overview(search_query: str = ""):
|
def load_event_overview(user, search_query: str = ""):
|
||||||
query = """
|
query = """
|
||||||
SELECT concerts.id, concerts.artist, concerts.start_datetime,
|
SELECT concerts.id, concerts.artist, concerts.start_datetime,
|
||||||
concerts.end_datetime, venues.name, venues.city,
|
concerts.end_datetime, venues.name, venues.city,
|
||||||
concerts.event_type, concerts.parent_event_id
|
concerts.event_type, concerts.parent_event_id,
|
||||||
|
concerts.visibility,
|
||||||
|
EXISTS (SELECT 1 FROM event_invitations ei
|
||||||
|
WHERE ei.concert_id = concerts.id AND ei.user_id = %s)
|
||||||
FROM concerts
|
FROM concerts
|
||||||
LEFT JOIN venues ON concerts.venue_id = venues.id
|
LEFT JOIN venues ON concerts.venue_id = venues.id
|
||||||
|
WHERE %s
|
||||||
|
OR concerts.visibility = 'public'
|
||||||
|
OR concerts.created_by = %s
|
||||||
|
OR (
|
||||||
|
concerts.visibility = 'friends' AND EXISTS (
|
||||||
|
SELECT 1 FROM friendships f
|
||||||
|
WHERE f.status = 'accepted'
|
||||||
|
AND ((f.requester_id = concerts.created_by AND f.addressee_id = %s)
|
||||||
|
OR (f.addressee_id = concerts.created_by AND f.requester_id = %s))
|
||||||
|
)
|
||||||
|
)
|
||||||
|
OR (
|
||||||
|
concerts.visibility = 'private' AND EXISTS (
|
||||||
|
SELECT 1 FROM event_invitations ei
|
||||||
|
WHERE ei.concert_id = concerts.id AND ei.user_id = %s
|
||||||
|
)
|
||||||
|
)
|
||||||
ORDER BY concerts.start_datetime ASC
|
ORDER BY concerts.start_datetime ASC
|
||||||
"""
|
"""
|
||||||
with get_db_connection() as connection:
|
with get_db_connection() as connection:
|
||||||
with connection.cursor() as cursor:
|
with connection.cursor() as cursor:
|
||||||
cursor.execute(query)
|
cursor.execute(query, (user["id"], user["is_admin"], user["id"], user["id"], user["id"], user["id"]))
|
||||||
upcoming, past = build_event_overview(cursor.fetchall())
|
upcoming, past = build_event_overview(cursor.fetchall())
|
||||||
|
|
||||||
term = search_query.strip().casefold()
|
term = search_query.strip().casefold()
|
||||||
@@ -858,6 +908,49 @@ def load_event_overview(search_query: str = ""):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def can_view_event(user, concert) -> bool:
|
||||||
|
if user["is_admin"] or concert["visibility"] == "public" or concert["created_by"] == user["id"]:
|
||||||
|
return True
|
||||||
|
with get_db_connection() as connection:
|
||||||
|
with connection.cursor() as cursor:
|
||||||
|
if concert["visibility"] == "private":
|
||||||
|
cursor.execute(
|
||||||
|
"SELECT 1 FROM event_invitations WHERE concert_id = %s AND user_id = %s",
|
||||||
|
(concert["id"], user["id"]),
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
cursor.execute(
|
||||||
|
"""
|
||||||
|
SELECT 1 FROM friendships WHERE status = 'accepted'
|
||||||
|
AND ((requester_id = %s AND addressee_id = %s)
|
||||||
|
OR (addressee_id = %s AND requester_id = %s))
|
||||||
|
""",
|
||||||
|
(concert["created_by"], user["id"], concert["created_by"], user["id"]),
|
||||||
|
)
|
||||||
|
return cursor.fetchone() is not None
|
||||||
|
|
||||||
|
|
||||||
|
def get_invitable_users(exclude_user_id: int):
|
||||||
|
with get_db_connection() as connection:
|
||||||
|
with connection.cursor() as cursor:
|
||||||
|
cursor.execute(
|
||||||
|
"""
|
||||||
|
SELECT id, username, COALESCE(display_name, username)
|
||||||
|
FROM users WHERE id <> %s
|
||||||
|
ORDER BY COALESCE(display_name, username), username
|
||||||
|
""",
|
||||||
|
(exclude_user_id,),
|
||||||
|
)
|
||||||
|
return [{"id": row[0], "username": row[1], "display_name": row[2]} for row in cursor.fetchall()]
|
||||||
|
|
||||||
|
|
||||||
|
def get_event_invitee_ids(concert_id: int):
|
||||||
|
with get_db_connection() as connection:
|
||||||
|
with connection.cursor() as cursor:
|
||||||
|
cursor.execute("SELECT user_id FROM event_invitations WHERE concert_id = %s", (concert_id,))
|
||||||
|
return {row[0] for row in cursor.fetchall()}
|
||||||
|
|
||||||
|
|
||||||
def search_users(search_query: str):
|
def search_users(search_query: str):
|
||||||
term = search_query.strip()
|
term = search_query.strip()
|
||||||
if not term:
|
if not term:
|
||||||
@@ -1055,10 +1148,18 @@ def load_profile(username: str):
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
cursor.execute(
|
cursor.execute(
|
||||||
"SELECT badge_code FROM user_badges WHERE user_id = %s",
|
"SELECT badge_code, awarded_at FROM user_badges WHERE user_id = %s",
|
||||||
(row[0],),
|
(row[0],),
|
||||||
)
|
)
|
||||||
earned_codes = {badge_row[0] for badge_row in cursor.fetchall()}
|
badge_rows = cursor.fetchall()
|
||||||
|
earned_codes = {badge_row[0] for badge_row in badge_rows}
|
||||||
|
badge_awarded_at = {badge_row[0]: badge_row[1] for badge_row in badge_rows}
|
||||||
|
|
||||||
|
is_founder = row[1].casefold() == "kai"
|
||||||
|
if is_founder:
|
||||||
|
earned_codes.add("founder")
|
||||||
|
if row[7]:
|
||||||
|
earned_codes.add("admin")
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"id": row[0],
|
"id": row[0],
|
||||||
@@ -1071,7 +1172,9 @@ def load_profile(username: str):
|
|||||||
"instagram_handle": row[5].rstrip("/").rsplit("/", 1)[-1] if row[5] else None,
|
"instagram_handle": row[5].rstrip("/").rsplit("/", 1)[-1] if row[5] else None,
|
||||||
"profile_visibility": row[6],
|
"profile_visibility": row[6],
|
||||||
"is_admin": bool(row[7]),
|
"is_admin": bool(row[7]),
|
||||||
|
"is_founder": is_founder,
|
||||||
"earned_codes": earned_codes,
|
"earned_codes": earned_codes,
|
||||||
|
"badge_awarded_at": badge_awarded_at,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -1567,7 +1670,6 @@ def update_user_role(
|
|||||||
request: Request,
|
request: Request,
|
||||||
user_id: int,
|
user_id: int,
|
||||||
role: str = Form(...),
|
role: str = Form(...),
|
||||||
founder_badge: str = Form(""),
|
|
||||||
):
|
):
|
||||||
user = require_admin(request)
|
user = require_admin(request)
|
||||||
|
|
||||||
@@ -1587,20 +1689,6 @@ def update_user_role(
|
|||||||
"UPDATE users SET is_admin = %s WHERE id = %s",
|
"UPDATE users SET is_admin = %s WHERE id = %s",
|
||||||
(role == "admin", user_id),
|
(role == "admin", user_id),
|
||||||
)
|
)
|
||||||
if founder_badge == "on":
|
|
||||||
cursor.execute(
|
|
||||||
"""
|
|
||||||
INSERT INTO user_badges (user_id, badge_code)
|
|
||||||
VALUES (%s, 'founder')
|
|
||||||
ON CONFLICT (user_id, badge_code) DO NOTHING
|
|
||||||
""",
|
|
||||||
(user_id,),
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
cursor.execute(
|
|
||||||
"DELETE FROM user_badges WHERE user_id = %s AND badge_code = 'founder'",
|
|
||||||
(user_id,),
|
|
||||||
)
|
|
||||||
connection.commit()
|
connection.commit()
|
||||||
|
|
||||||
return RedirectResponse("/admin/users", status_code=303)
|
return RedirectResponse("/admin/users", status_code=303)
|
||||||
@@ -1981,11 +2069,12 @@ def logout(request: Request):
|
|||||||
|
|
||||||
@app.get("/", response_class=HTMLResponse)
|
@app.get("/", response_class=HTMLResponse)
|
||||||
def home(request: Request, q: str = ""):
|
def home(request: Request, q: str = ""):
|
||||||
upcoming_concerts, _past_concerts = load_event_overview(q)
|
user = get_current_user(request)
|
||||||
|
upcoming_concerts, _past_concerts = load_event_overview(user, q)
|
||||||
|
|
||||||
template = templates.get_template("index.html")
|
template = templates.get_template("index.html")
|
||||||
return template.render(
|
return template.render(
|
||||||
user=get_current_user(request),
|
user=user,
|
||||||
upcoming_concerts=upcoming_concerts,
|
upcoming_concerts=upcoming_concerts,
|
||||||
past_concerts=[],
|
past_concerts=[],
|
||||||
archive=False,
|
archive=False,
|
||||||
@@ -1996,10 +2085,11 @@ def home(request: Request, q: str = ""):
|
|||||||
|
|
||||||
@app.get("/events/past", response_class=HTMLResponse)
|
@app.get("/events/past", response_class=HTMLResponse)
|
||||||
def past_events(request: Request, q: str = ""):
|
def past_events(request: Request, q: str = ""):
|
||||||
_upcoming_concerts, past_concerts = load_event_overview(q)
|
user = get_current_user(request)
|
||||||
|
_upcoming_concerts, past_concerts = load_event_overview(user, q)
|
||||||
template = templates.get_template("index.html")
|
template = templates.get_template("index.html")
|
||||||
return template.render(
|
return template.render(
|
||||||
user=get_current_user(request),
|
user=user,
|
||||||
upcoming_concerts=[],
|
upcoming_concerts=[],
|
||||||
past_concerts=past_concerts,
|
past_concerts=past_concerts,
|
||||||
archive=True,
|
archive=True,
|
||||||
@@ -2102,9 +2192,11 @@ def render_profile(
|
|||||||
"category": category,
|
"category": category,
|
||||||
"earned": code in profile["earned_codes"],
|
"earned": code in profile["earned_codes"],
|
||||||
"image_path": badge_assets.get(code),
|
"image_path": badge_assets.get(code),
|
||||||
|
"sort_key": (0, datetime.min) if code == "founder" else (1, datetime.min) if code == "admin" else (2, profile["badge_awarded_at"].get(code) or datetime.max),
|
||||||
}
|
}
|
||||||
for code, name, icon, threshold, description, category in BADGE_DEFINITIONS
|
for code, name, icon, threshold, description, category in BADGE_DEFINITIONS
|
||||||
]
|
]
|
||||||
|
badges.sort(key=lambda badge: badge["sort_key"])
|
||||||
|
|
||||||
template = templates.get_template("profile.html")
|
template = templates.get_template("profile.html")
|
||||||
return HTMLResponse(
|
return HTMLResponse(
|
||||||
@@ -2416,7 +2508,8 @@ def new_concert(request: Request):
|
|||||||
return login_redirect("/concerts/new")
|
return login_redirect("/concerts/new")
|
||||||
|
|
||||||
template = templates.get_template("new_concert.html")
|
template = templates.get_template("new_concert.html")
|
||||||
return template.render(user=user, linkable_events=get_linkable_events())
|
return template.render(user=user, linkable_events=get_linkable_events(),
|
||||||
|
invitable_users=get_invitable_users(user["id"]))
|
||||||
|
|
||||||
|
|
||||||
# ============================================================
|
# ============================================================
|
||||||
@@ -2429,13 +2522,21 @@ def new_concert(request: Request):
|
|||||||
)
|
)
|
||||||
def concert_detail(request: Request, concert_id: int):
|
def concert_detail(request: Request, concert_id: int):
|
||||||
concert = load_concert(concert_id)
|
concert = load_concert(concert_id)
|
||||||
|
user = get_current_user(request)
|
||||||
|
|
||||||
if not concert:
|
if not concert or not can_view_event(user, concert):
|
||||||
return HTMLResponse(
|
return HTMLResponse(
|
||||||
"<h1>Veranstaltung nicht gefunden</h1>",
|
"<h1>Veranstaltung nicht gefunden</h1>",
|
||||||
status_code=404
|
status_code=404
|
||||||
)
|
)
|
||||||
|
|
||||||
|
with get_db_connection() as connection:
|
||||||
|
with connection.cursor() as cursor:
|
||||||
|
cursor.execute(
|
||||||
|
"UPDATE event_invitations SET viewed_at = CURRENT_TIMESTAMP WHERE concert_id = %s AND user_id = %s AND viewed_at IS NULL",
|
||||||
|
(concert_id, user["id"]),
|
||||||
|
)
|
||||||
|
connection.commit()
|
||||||
user = get_current_user(request)
|
user = get_current_user(request)
|
||||||
|
|
||||||
with get_db_connection() as connection:
|
with get_db_connection() as connection:
|
||||||
@@ -2561,6 +2662,8 @@ async def create_concert(
|
|||||||
artist: str = Form(...),
|
artist: str = Form(...),
|
||||||
event_type: str = Form("concert"),
|
event_type: str = Form("concert"),
|
||||||
parent_event_id: str = Form(""),
|
parent_event_id: str = Form(""),
|
||||||
|
visibility: str = Form("public"),
|
||||||
|
invited_user_ids: list[int] = Form(default=[]),
|
||||||
venue_id: str = Form(""),
|
venue_id: str = Form(""),
|
||||||
venue_name: str = Form(""),
|
venue_name: str = Form(""),
|
||||||
city: str = Form(""),
|
city: str = Form(""),
|
||||||
@@ -2588,6 +2691,10 @@ async def create_concert(
|
|||||||
return HTMLResponse("<h1>Bei Festivals ist ein Enddatum erforderlich.</h1>", status_code=400)
|
return HTMLResponse("<h1>Bei Festivals ist ein Enddatum erforderlich.</h1>", status_code=400)
|
||||||
if event_type != "festival":
|
if event_type != "festival":
|
||||||
end_datetime = ""
|
end_datetime = ""
|
||||||
|
if visibility not in {"public", "friends", "private"}:
|
||||||
|
return HTMLResponse("<h1>Ungültige Sichtbarkeit.</h1>", status_code=400)
|
||||||
|
if event_type != "other":
|
||||||
|
visibility = "public"
|
||||||
if end_datetime and datetime.fromisoformat(end_datetime) < datetime.fromisoformat(start_datetime):
|
if end_datetime and datetime.fromisoformat(end_datetime) < datetime.fromisoformat(start_datetime):
|
||||||
return HTMLResponse("<h1>Das Enddatum darf nicht vor dem Beginn liegen.</h1>", status_code=400)
|
return HTMLResponse("<h1>Das Enddatum darf nicht vor dem Beginn liegen.</h1>", status_code=400)
|
||||||
try:
|
try:
|
||||||
@@ -2639,10 +2746,11 @@ async def create_concert(
|
|||||||
ticket_price,
|
ticket_price,
|
||||||
flyer_path,
|
flyer_path,
|
||||||
flyer_url,
|
flyer_url,
|
||||||
|
visibility,
|
||||||
created_by
|
created_by
|
||||||
)
|
)
|
||||||
VALUES (
|
VALUES (
|
||||||
%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s
|
%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s
|
||||||
)
|
)
|
||||||
RETURNING id
|
RETURNING id
|
||||||
""",
|
""",
|
||||||
@@ -2658,11 +2766,22 @@ async def create_concert(
|
|||||||
ticket_price or None,
|
ticket_price or None,
|
||||||
flyer_path,
|
flyer_path,
|
||||||
normalized_flyer_url,
|
normalized_flyer_url,
|
||||||
|
visibility,
|
||||||
user["id"],
|
user["id"],
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
concert_id = cursor.fetchone()[0]
|
concert_id = cursor.fetchone()[0]
|
||||||
|
if visibility == "private" and invited_user_ids:
|
||||||
|
cursor.execute(
|
||||||
|
"""
|
||||||
|
INSERT INTO event_invitations (concert_id, user_id, invited_by)
|
||||||
|
SELECT %s, id, %s FROM users
|
||||||
|
WHERE id = ANY(%s) AND id <> %s
|
||||||
|
ON CONFLICT (concert_id, user_id) DO NOTHING
|
||||||
|
""",
|
||||||
|
(concert_id, user["id"], invited_user_ids, user["id"]),
|
||||||
|
)
|
||||||
|
|
||||||
connection.commit()
|
connection.commit()
|
||||||
|
|
||||||
@@ -2690,7 +2809,7 @@ def edit_concert_page(request: Request, concert_id: int):
|
|||||||
|
|
||||||
concert = load_concert(concert_id)
|
concert = load_concert(concert_id)
|
||||||
|
|
||||||
if not concert:
|
if not concert or not can_view_event(user, concert):
|
||||||
return HTMLResponse(
|
return HTMLResponse(
|
||||||
"<h1>Veranstaltung nicht gefunden</h1>",
|
"<h1>Veranstaltung nicht gefunden</h1>",
|
||||||
status_code=404
|
status_code=404
|
||||||
@@ -2710,6 +2829,9 @@ def edit_concert_page(request: Request, concert_id: int):
|
|||||||
can_edit_title=can_edit_title(user, concert),
|
can_edit_title=can_edit_title(user, concert),
|
||||||
can_edit_details=can_edit_details(user, concert),
|
can_edit_details=can_edit_details(user, concert),
|
||||||
can_delete=can_delete_concert(user, concert),
|
can_delete=can_delete_concert(user, concert),
|
||||||
|
invitable_users=get_invitable_users(user["id"]),
|
||||||
|
invited_user_ids=get_event_invitee_ids(concert_id),
|
||||||
|
can_manage_access=can_manage_event_access(user, concert),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -2720,6 +2842,8 @@ async def edit_concert(
|
|||||||
artist: str = Form(""),
|
artist: str = Form(""),
|
||||||
event_type: str = Form("concert"),
|
event_type: str = Form("concert"),
|
||||||
parent_event_id: str = Form(""),
|
parent_event_id: str = Form(""),
|
||||||
|
visibility: str = Form("public"),
|
||||||
|
invited_user_ids: list[int] = Form(default=[]),
|
||||||
venue_id: str = Form(""),
|
venue_id: str = Form(""),
|
||||||
venue_name: str = Form(""),
|
venue_name: str = Form(""),
|
||||||
city: str = Form(""),
|
city: str = Form(""),
|
||||||
@@ -2743,7 +2867,7 @@ async def edit_concert(
|
|||||||
|
|
||||||
concert = load_concert(concert_id)
|
concert = load_concert(concert_id)
|
||||||
|
|
||||||
if not concert:
|
if not concert or not can_view_event(user, concert):
|
||||||
return HTMLResponse(
|
return HTMLResponse(
|
||||||
"<h1>Veranstaltung nicht gefunden</h1>",
|
"<h1>Veranstaltung nicht gefunden</h1>",
|
||||||
status_code=404
|
status_code=404
|
||||||
@@ -2768,15 +2892,25 @@ async def edit_concert(
|
|||||||
next_flyer_url = concert["flyer_url"]
|
next_flyer_url = concert["flyer_url"]
|
||||||
next_venue_id = concert["venue"]["id"]
|
next_venue_id = concert["venue"]["id"]
|
||||||
next_event_type = concert["event_type"]
|
next_event_type = concert["event_type"]
|
||||||
|
next_visibility = concert["visibility"]
|
||||||
next_parent_event_id = concert["parent_event"]["id"] if concert["parent_event"] else None
|
next_parent_event_id = concert["parent_event"]["id"] if concert["parent_event"] else None
|
||||||
|
|
||||||
if can_edit_details(user, concert):
|
if can_edit_details(user, concert):
|
||||||
|
if not can_manage_event_access(user, concert):
|
||||||
|
event_type = concert["event_type"]
|
||||||
if event_type not in EVENT_TYPES:
|
if event_type not in EVENT_TYPES:
|
||||||
return HTMLResponse("<h1>Ungültige Veranstaltungskategorie.</h1>", status_code=400)
|
return HTMLResponse("<h1>Ungültige Veranstaltungskategorie.</h1>", status_code=400)
|
||||||
if event_type == "festival" and not end_datetime:
|
if event_type == "festival" and not end_datetime:
|
||||||
return HTMLResponse("<h1>Bei Festivals ist ein Enddatum erforderlich.</h1>", status_code=400)
|
return HTMLResponse("<h1>Bei Festivals ist ein Enddatum erforderlich.</h1>", status_code=400)
|
||||||
if event_type != "festival":
|
if event_type != "festival":
|
||||||
end_datetime = ""
|
end_datetime = ""
|
||||||
|
if can_manage_event_access(user, concert):
|
||||||
|
if visibility not in {"public", "friends", "private"}:
|
||||||
|
return HTMLResponse("<h1>Ungültige Sichtbarkeit.</h1>", status_code=400)
|
||||||
|
if event_type != "other":
|
||||||
|
visibility = "public"
|
||||||
|
else:
|
||||||
|
visibility = concert["visibility"]
|
||||||
effective_start = start_datetime or concert["start_local"]
|
effective_start = start_datetime or concert["start_local"]
|
||||||
if end_datetime and datetime.fromisoformat(end_datetime) < datetime.fromisoformat(effective_start):
|
if end_datetime and datetime.fromisoformat(end_datetime) < datetime.fromisoformat(effective_start):
|
||||||
return HTMLResponse("<h1>Das Enddatum darf nicht vor dem Beginn liegen.</h1>", status_code=400)
|
return HTMLResponse("<h1>Das Enddatum darf nicht vor dem Beginn liegen.</h1>", status_code=400)
|
||||||
@@ -2820,6 +2954,7 @@ async def edit_concert(
|
|||||||
next_start = start_datetime or concert["start_datetime"]
|
next_start = start_datetime or concert["start_datetime"]
|
||||||
next_end = end_datetime or None
|
next_end = end_datetime or None
|
||||||
next_event_type = event_type
|
next_event_type = event_type
|
||||||
|
next_visibility = visibility
|
||||||
next_description = description or None
|
next_description = description or None
|
||||||
next_ticket_url = ticket_url or None
|
next_ticket_url = ticket_url or None
|
||||||
next_ticket_price = ticket_price or None
|
next_ticket_price = ticket_price or None
|
||||||
@@ -2843,6 +2978,7 @@ async def edit_concert(
|
|||||||
ticket_price = %s,
|
ticket_price = %s,
|
||||||
flyer_path = %s,
|
flyer_path = %s,
|
||||||
flyer_url = %s
|
flyer_url = %s
|
||||||
|
, visibility = %s
|
||||||
WHERE id = %s
|
WHERE id = %s
|
||||||
""",
|
""",
|
||||||
(
|
(
|
||||||
@@ -2857,9 +2993,26 @@ async def edit_concert(
|
|||||||
next_ticket_price,
|
next_ticket_price,
|
||||||
next_flyer,
|
next_flyer,
|
||||||
next_flyer_url,
|
next_flyer_url,
|
||||||
|
next_visibility,
|
||||||
concert_id,
|
concert_id,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
if can_manage_event_access(user, concert) and next_visibility == "private":
|
||||||
|
cursor.execute(
|
||||||
|
"DELETE FROM event_invitations WHERE concert_id = %s AND NOT (user_id = ANY(%s))",
|
||||||
|
(concert_id, invited_user_ids or [0]),
|
||||||
|
)
|
||||||
|
if invited_user_ids:
|
||||||
|
cursor.execute(
|
||||||
|
"""
|
||||||
|
INSERT INTO event_invitations (concert_id, user_id, invited_by)
|
||||||
|
SELECT %s, id, %s FROM users WHERE id = ANY(%s) AND id <> %s
|
||||||
|
ON CONFLICT (concert_id, user_id) DO NOTHING
|
||||||
|
""",
|
||||||
|
(concert_id, user["id"], invited_user_ids, user["id"]),
|
||||||
|
)
|
||||||
|
elif can_manage_event_access(user, concert):
|
||||||
|
cursor.execute("DELETE FROM event_invitations WHERE concert_id = %s", (concert_id,))
|
||||||
connection.commit()
|
connection.commit()
|
||||||
|
|
||||||
return RedirectResponse(
|
return RedirectResponse(
|
||||||
@@ -2877,7 +3030,7 @@ def delete_concert(request: Request, concert_id: int):
|
|||||||
|
|
||||||
concert = load_concert(concert_id)
|
concert = load_concert(concert_id)
|
||||||
|
|
||||||
if not concert:
|
if not concert or not can_view_event(user, concert):
|
||||||
return HTMLResponse(
|
return HTMLResponse(
|
||||||
"<h1>Veranstaltung nicht gefunden</h1>",
|
"<h1>Veranstaltung nicht gefunden</h1>",
|
||||||
status_code=404
|
status_code=404
|
||||||
@@ -2918,7 +3071,8 @@ def set_attendance(
|
|||||||
return login_redirect(f"/concerts/{concert_id}")
|
return login_redirect(f"/concerts/{concert_id}")
|
||||||
if status not in {"attending", "maybe", "ticket_search", "ticket_offer"}:
|
if status not in {"attending", "maybe", "ticket_search", "ticket_offer"}:
|
||||||
return HTMLResponse("<h1>Ungültige Auswahl</h1>", status_code=400)
|
return HTMLResponse("<h1>Ungültige Auswahl</h1>", status_code=400)
|
||||||
if not load_concert(concert_id):
|
concert = load_concert(concert_id)
|
||||||
|
if not concert or not can_view_event(user, concert):
|
||||||
return HTMLResponse("<h1>Veranstaltung nicht gefunden</h1>", status_code=404)
|
return HTMLResponse("<h1>Veranstaltung nicht gefunden</h1>", status_code=404)
|
||||||
|
|
||||||
with get_db_connection() as connection:
|
with get_db_connection() as connection:
|
||||||
@@ -2952,7 +3106,7 @@ def add_comment(
|
|||||||
|
|
||||||
concert = load_concert(concert_id)
|
concert = load_concert(concert_id)
|
||||||
|
|
||||||
if not concert:
|
if not concert or not can_view_event(user, concert):
|
||||||
return HTMLResponse(
|
return HTMLResponse(
|
||||||
"<h1>Veranstaltung nicht gefunden</h1>",
|
"<h1>Veranstaltung nicht gefunden</h1>",
|
||||||
status_code=404
|
status_code=404
|
||||||
@@ -3006,7 +3160,7 @@ async def add_photo(
|
|||||||
|
|
||||||
concert = load_concert(concert_id)
|
concert = load_concert(concert_id)
|
||||||
|
|
||||||
if not concert:
|
if not concert or not can_view_event(user, concert):
|
||||||
return HTMLResponse(
|
return HTMLResponse(
|
||||||
"<h1>Veranstaltung nicht gefunden</h1>",
|
"<h1>Veranstaltung nicht gefunden</h1>",
|
||||||
status_code=404
|
status_code=404
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
<div class="user-menu-panel">
|
<div class="user-menu-panel">
|
||||||
<a href="/profile">Mein Profil</a>
|
<a href="/profile">Mein Profil</a>
|
||||||
<a href="/messages">Nachrichten{% if user.unread_message_count %} <span class="menu-count">{{ user.unread_message_count }}</span>{% endif %}</a>
|
<a href="/messages">Nachrichten{% if user.unread_message_count %} <span class="menu-count">{{ user.unread_message_count }}</span>{% endif %}</a>
|
||||||
|
{% if user.event_invitation_count %}<a href="/#event-invitations">Veranstaltungseinladungen <span class="menu-count">{{ user.event_invitation_count }}</span></a>{% endif %}
|
||||||
<form method="post" action="/logout">
|
<form method="post" action="/logout">
|
||||||
<button type="submit">Abmelden</button>
|
<button type="submit">Abmelden</button>
|
||||||
</form>
|
</form>
|
||||||
|
|||||||
@@ -34,7 +34,6 @@
|
|||||||
<div><strong>{{ managed_user.display_name }}</strong><div class="admin-meta">@{{ managed_user.username }} · {{ managed_user.email }} · seit {{ managed_user.created_at }}</div></div>
|
<div><strong>{{ managed_user.display_name }}</strong><div class="admin-meta">@{{ managed_user.username }} · {{ managed_user.email }} · seit {{ managed_user.created_at }}</div></div>
|
||||||
<form class="role-form" method="post" action="/admin/users/{{ managed_user.id }}">
|
<form class="role-form" method="post" action="/admin/users/{{ managed_user.id }}">
|
||||||
<select name="role" aria-label="Rolle für {{ managed_user.username }}"><option value="user" {% if not managed_user.is_admin %}selected{% endif %}>Benutzer</option><option value="admin" {% if managed_user.is_admin %}selected{% endif %}>Admin</option></select>
|
<select name="role" aria-label="Rolle für {{ managed_user.username }}"><option value="user" {% if not managed_user.is_admin %}selected{% endif %}>Benutzer</option><option value="admin" {% if managed_user.is_admin %}selected{% endif %}>Admin</option></select>
|
||||||
<label><input type="checkbox" name="founder_badge" {% if managed_user.has_founder_badge %}checked{% endif %}> Gründer-Patch</label>
|
|
||||||
<button class="button" type="submit">Speichern</button>
|
<button class="button" type="submit">Speichern</button>
|
||||||
</form>
|
</form>
|
||||||
<div class="role-form">
|
<div class="role-form">
|
||||||
|
|||||||
@@ -63,6 +63,7 @@
|
|||||||
<div class="concert-content">
|
<div class="concert-content">
|
||||||
|
|
||||||
<span class="event-type-badge">{{ concert.event_type_label }}</span>
|
<span class="event-type-badge">{{ concert.event_type_label }}</span>
|
||||||
|
{% if concert.visibility == 'friends' %}<span class="event-type-badge">🔒 Nur Freunde</span>{% elif concert.visibility == 'private' %}<span class="event-type-badge">🔐 Privat / eingeladen</span>{% endif %}
|
||||||
|
|
||||||
<h1>
|
<h1>
|
||||||
{{ concert.artist }}
|
{{ concert.artist }}
|
||||||
|
|||||||
@@ -23,7 +23,7 @@
|
|||||||
{% if can_edit_details %}
|
{% if can_edit_details %}
|
||||||
<p>
|
<p>
|
||||||
<label>Kategorie<br>
|
<label>Kategorie<br>
|
||||||
<select name="event_type" id="event-type" required>
|
<select name="event_type" id="event-type" required {% if not can_manage_access %}disabled{% endif %}>
|
||||||
<option value="concert" {% if concert.event_type == 'concert' %}selected{% endif %}>Konzert</option>
|
<option value="concert" {% if concert.event_type == 'concert' %}selected{% endif %}>Konzert</option>
|
||||||
<option value="festival" {% if concert.event_type == 'festival' %}selected{% endif %}>Festival</option>
|
<option value="festival" {% if concert.event_type == 'festival' %}selected{% endif %}>Festival</option>
|
||||||
<option value="other" {% if concert.event_type == 'other' %}selected{% endif %}>Sonstiges</option>
|
<option value="other" {% if concert.event_type == 'other' %}selected{% endif %}>Sonstiges</option>
|
||||||
@@ -40,6 +40,19 @@
|
|||||||
</select>
|
</select>
|
||||||
</label>
|
</label>
|
||||||
</p>
|
</p>
|
||||||
|
{% if can_manage_access %}<fieldset id="event-visibility-field" {% if concert.event_type != 'other' %}hidden{% endif %}>
|
||||||
|
<legend>Sichtbarkeit</legend>
|
||||||
|
<label><input type="radio" name="visibility" value="public" {% if concert.visibility == 'public' %}checked{% endif %}> Öffentlich</label><br>
|
||||||
|
<label><input type="radio" name="visibility" value="friends" {% if concert.visibility == 'friends' %}checked{% endif %}> Nur Freunde</label><br>
|
||||||
|
<label><input type="radio" name="visibility" value="private" {% if concert.visibility == 'private' %}checked{% endif %}> Privat – nur eingeladene Mitglieder</label>
|
||||||
|
</fieldset>
|
||||||
|
<fieldset id="event-invitations-field" {% if concert.event_type != 'other' or concert.visibility != 'private' %}hidden{% endif %}>
|
||||||
|
<legend>Mitglieder einladen</legend>
|
||||||
|
{% for member in invitable_users %}
|
||||||
|
<label style="display:block;margin:7px 0"><input type="checkbox" name="invited_user_ids" value="{{ member.id }}" {% if member.id in invited_user_ids %}checked{% endif %}> {{ member.display_name }} <small>@{{ member.username }}</small></label>
|
||||||
|
{% else %}<p>Noch keine weiteren Mitglieder vorhanden.</p>{% endfor %}
|
||||||
|
</fieldset>
|
||||||
|
{% endif %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<p>
|
<p>
|
||||||
<label>Titel / Künstler / Band<br>
|
<label>Titel / Künstler / Band<br>
|
||||||
@@ -125,6 +138,9 @@ const parentEventField = document.getElementById("parent-event-field");
|
|||||||
const parentEventId = document.getElementById("parent-event-id");
|
const parentEventId = document.getElementById("parent-event-id");
|
||||||
const festivalEndField = document.getElementById("festival-end-field");
|
const festivalEndField = document.getElementById("festival-end-field");
|
||||||
const festivalEndDatetime = document.getElementById("festival-end-datetime");
|
const festivalEndDatetime = document.getElementById("festival-end-datetime");
|
||||||
|
const eventVisibilityField = document.getElementById("event-visibility-field");
|
||||||
|
const eventInvitationsField = document.getElementById("event-invitations-field");
|
||||||
|
const visibilityInputs = [...document.querySelectorAll('input[name="visibility"]')];
|
||||||
|
|
||||||
function updateEventFields() {
|
function updateEventFields() {
|
||||||
const isFestival = eventType.value === "festival";
|
const isFestival = eventType.value === "festival";
|
||||||
@@ -132,11 +148,17 @@ function updateEventFields() {
|
|||||||
festivalEndField.hidden = !isFestival;
|
festivalEndField.hidden = !isFestival;
|
||||||
festivalEndDatetime.required = isFestival;
|
festivalEndDatetime.required = isFestival;
|
||||||
parentEventField.hidden = !isOther;
|
parentEventField.hidden = !isOther;
|
||||||
|
if (eventVisibilityField) {
|
||||||
|
eventVisibilityField.hidden = !isOther;
|
||||||
|
const isPrivate = isOther && document.querySelector('input[name="visibility"]:checked').value === "private";
|
||||||
|
eventInvitationsField.hidden = !isPrivate;
|
||||||
|
}
|
||||||
if (!isFestival) festivalEndDatetime.value = "";
|
if (!isFestival) festivalEndDatetime.value = "";
|
||||||
if (!isOther) parentEventId.value = "";
|
if (!isOther) parentEventId.value = "";
|
||||||
}
|
}
|
||||||
|
|
||||||
eventType.addEventListener("change", updateEventFields);
|
eventType.addEventListener("change", updateEventFields);
|
||||||
|
visibilityInputs.forEach(input => input.addEventListener("change", updateEventFields));
|
||||||
</script>
|
</script>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</body>
|
</body>
|
||||||
|
|||||||
@@ -205,6 +205,7 @@
|
|||||||
{% macro event_card(concert, linked=false) %}
|
{% macro event_card(concert, linked=false) %}
|
||||||
<a href="/concerts/{{ concert.id }}" class="concert-card{% if linked %} linked-event{% endif %}">
|
<a href="/concerts/{{ concert.id }}" class="concert-card{% if linked %} linked-event{% endif %}">
|
||||||
<span class="event-category">{{ concert.event_type_label }}</span>
|
<span class="event-category">{{ concert.event_type_label }}</span>
|
||||||
|
{% if concert.is_invited %}<span class="event-category"> · ✉ Eingeladen</span>{% elif concert.visibility == 'friends' %}<span class="event-category"> · 🔒 Nur Freunde</span>{% elif concert.visibility == 'private' %}<span class="event-category"> · 🔐 Privat</span>{% endif %}
|
||||||
<div class="concert-artist">
|
<div class="concert-artist">
|
||||||
{% if concert.event_type == 'festival' %}🎪{% elif concert.event_type == 'other' %}🥂{% else %}🎸{% endif %}
|
{% if concert.event_type == 'festival' %}🎪{% elif concert.event_type == 'other' %}🥂{% else %}🎸{% endif %}
|
||||||
{{ concert.artist }}
|
{{ concert.artist }}
|
||||||
@@ -290,7 +291,7 @@
|
|||||||
|
|
||||||
{% if upcoming_concerts or past_concerts %}
|
{% if upcoming_concerts or past_concerts %}
|
||||||
|
|
||||||
<div class="concert-list">
|
<div class="concert-list" id="event-invitations">
|
||||||
|
|
||||||
{% for concert in upcoming_concerts %}
|
{% for concert in upcoming_concerts %}
|
||||||
<div class="event-group">
|
<div class="event-group">
|
||||||
|
|||||||
@@ -126,6 +126,21 @@
|
|||||||
<small>Zum Beispiel ein gemeinsames Vortrinken vor einem Konzert.</small>
|
<small>Zum Beispiel ein gemeinsames Vortrinken vor einem Konzert.</small>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
|
<fieldset id="event-visibility-field" hidden>
|
||||||
|
<legend>Sichtbarkeit</legend>
|
||||||
|
<label><input type="radio" name="visibility" value="public" checked> Öffentlich – alle können die Veranstaltung sehen und teilnehmen</label><br>
|
||||||
|
<label><input type="radio" name="visibility" value="friends"> Nur Freunde – nur bestätigte Freunde können sie sehen und teilnehmen</label><br>
|
||||||
|
<label><input type="radio" name="visibility" value="private"> Privat – nur gezielt eingeladene Mitglieder</label>
|
||||||
|
</fieldset>
|
||||||
|
|
||||||
|
<fieldset id="event-invitations-field" hidden>
|
||||||
|
<legend>Mitglieder einladen</legend>
|
||||||
|
<p class="flyer-help">Eingeladene erhalten eine Benachrichtigung und sehen die Veranstaltung in ihrer Übersicht.</p>
|
||||||
|
{% for member in invitable_users %}
|
||||||
|
<label style="display:block;margin:7px 0"><input type="checkbox" name="invited_user_ids" value="{{ member.id }}"> {{ member.display_name }} <small>@{{ member.username }}</small></label>
|
||||||
|
{% else %}<p>Noch keine weiteren Mitglieder vorhanden.</p>{% endfor %}
|
||||||
|
</fieldset>
|
||||||
|
|
||||||
<!-- ================================================= -->
|
<!-- ================================================= -->
|
||||||
<!-- Künstler -->
|
<!-- Künstler -->
|
||||||
<!-- ================================================= -->
|
<!-- ================================================= -->
|
||||||
@@ -389,6 +404,9 @@ const parentEventField = document.getElementById("parent-event-field");
|
|||||||
const parentEventId = document.getElementById("parent-event-id");
|
const parentEventId = document.getElementById("parent-event-id");
|
||||||
const festivalEndField = document.getElementById("festival-end-field");
|
const festivalEndField = document.getElementById("festival-end-field");
|
||||||
const festivalEndDatetime = document.getElementById("festival-end-datetime");
|
const festivalEndDatetime = document.getElementById("festival-end-datetime");
|
||||||
|
const eventVisibilityField = document.getElementById("event-visibility-field");
|
||||||
|
const eventInvitationsField = document.getElementById("event-invitations-field");
|
||||||
|
const visibilityInputs = [...document.querySelectorAll('input[name="visibility"]')];
|
||||||
|
|
||||||
function updateEventFields() {
|
function updateEventFields() {
|
||||||
const isFestival = eventType.value === "festival";
|
const isFestival = eventType.value === "festival";
|
||||||
@@ -396,11 +414,15 @@ function updateEventFields() {
|
|||||||
festivalEndField.hidden = !isFestival;
|
festivalEndField.hidden = !isFestival;
|
||||||
festivalEndDatetime.required = isFestival;
|
festivalEndDatetime.required = isFestival;
|
||||||
parentEventField.hidden = !isOther;
|
parentEventField.hidden = !isOther;
|
||||||
|
eventVisibilityField.hidden = !isOther;
|
||||||
|
const isPrivate = isOther && document.querySelector('input[name="visibility"]:checked').value === "private";
|
||||||
|
eventInvitationsField.hidden = !isPrivate;
|
||||||
if (!isFestival) festivalEndDatetime.value = "";
|
if (!isFestival) festivalEndDatetime.value = "";
|
||||||
if (!isOther) parentEventId.value = "";
|
if (!isOther) parentEventId.value = "";
|
||||||
}
|
}
|
||||||
|
|
||||||
eventType.addEventListener("change", updateEventFields);
|
eventType.addEventListener("change", updateEventFields);
|
||||||
|
visibilityInputs.forEach(input => input.addEventListener("change", updateEventFields));
|
||||||
updateEventFields();
|
updateEventFields();
|
||||||
|
|
||||||
/* ============================================================
|
/* ============================================================
|
||||||
|
|||||||
@@ -109,10 +109,23 @@ CREATE TABLE concerts (
|
|||||||
ticket_price NUMERIC(10,2),
|
ticket_price NUMERIC(10,2),
|
||||||
flyer_path TEXT,
|
flyer_path TEXT,
|
||||||
flyer_url TEXT,
|
flyer_url TEXT,
|
||||||
|
visibility VARCHAR(20) NOT NULL DEFAULT 'public'
|
||||||
|
CHECK (visibility IN ('public', 'friends', 'private')),
|
||||||
created_by INTEGER REFERENCES users(id) ON DELETE SET NULL,
|
created_by INTEGER REFERENCES users(id) ON DELETE SET NULL,
|
||||||
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
|
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||||
);
|
);
|
||||||
|
|
||||||
|
CREATE TABLE event_invitations (
|
||||||
|
concert_id INTEGER NOT NULL REFERENCES concerts(id) ON DELETE CASCADE,
|
||||||
|
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||||
|
invited_by INTEGER REFERENCES users(id) ON DELETE SET NULL,
|
||||||
|
viewed_at TIMESTAMP,
|
||||||
|
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
PRIMARY KEY (concert_id, user_id)
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX idx_event_invitations_user ON event_invitations (user_id, viewed_at);
|
||||||
|
|
||||||
CREATE TABLE concert_comments (
|
CREATE TABLE concert_comments (
|
||||||
id SERIAL PRIMARY KEY,
|
id SERIAL PRIMARY KEY,
|
||||||
concert_id INTEGER NOT NULL REFERENCES concerts(id) ON DELETE CASCADE,
|
concert_id INTEGER NOT NULL REFERENCES concerts(id) ON DELETE CASCADE,
|
||||||
|
|||||||
@@ -0,0 +1,15 @@
|
|||||||
|
ALTER TABLE concerts
|
||||||
|
ADD COLUMN IF NOT EXISTS visibility VARCHAR(20) NOT NULL DEFAULT 'public'
|
||||||
|
CHECK (visibility IN ('public', 'friends', 'private'));
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS event_invitations (
|
||||||
|
concert_id INTEGER NOT NULL REFERENCES concerts(id) ON DELETE CASCADE,
|
||||||
|
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||||
|
invited_by INTEGER REFERENCES users(id) ON DELETE SET NULL,
|
||||||
|
viewed_at TIMESTAMP,
|
||||||
|
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
PRIMARY KEY (concert_id, user_id)
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_event_invitations_user
|
||||||
|
ON event_invitations (user_id, viewed_at);
|
||||||
Reference in New Issue
Block a user