Add private event invitations

This commit is contained in:
kai
2026-08-26 18:24:22 +02:00
parent a9741b6859
commit c4fa6f077c
9 changed files with 268 additions and 40 deletions
+191 -37
View File
@@ -78,6 +78,7 @@ 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"),
("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"),
@@ -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 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 (
id SERIAL PRIMARY KEY,
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]),
"pending_friend_count": row[4],
"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
WHERE addressee_id = users.id AND status = 'pending'),
(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
JOIN users
ON users.id = sessions.user_id
@@ -700,7 +722,8 @@ def load_concert(concert_id: int):
concerts.parent_event_id,
parent_event.artist,
parent_event.event_type,
concerts.flyer_url
concerts.flyer_url,
concerts.visibility
FROM concerts
LEFT JOIN venues
ON concerts.venue_id = venues.id
@@ -729,6 +752,7 @@ def load_concert(concert_id: int):
"ticket_price": row[6],
"flyer_path": row[7],
"flyer_url": row[21],
"visibility": row[22],
"created_by": row[8],
"is_past": is_past,
"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"]
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):
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"
if city:
venue_text += f", {city}"
@@ -802,6 +830,8 @@ def serialize_concert_card(row):
"event_type": event_type,
"event_type_label": EVENT_TYPES[event_type],
"parent_event_id": parent_event_id,
"visibility": visibility,
"is_invited": bool(is_invited),
"children": [],
"_start_datetime": start_datetime,
"_end_datetime": end_datetime,
@@ -830,18 +860,38 @@ def build_event_overview(rows):
return upcoming, past
def load_event_overview(search_query: str = ""):
def load_event_overview(user, search_query: str = ""):
query = """
SELECT concerts.id, concerts.artist, concerts.start_datetime,
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
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
"""
with get_db_connection() as connection:
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())
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):
term = search_query.strip()
if not term:
@@ -1055,10 +1148,18 @@ def load_profile(username: str):
return None
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],),
)
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 {
"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,
"profile_visibility": row[6],
"is_admin": bool(row[7]),
"is_founder": is_founder,
"earned_codes": earned_codes,
"badge_awarded_at": badge_awarded_at,
}
@@ -1567,7 +1670,6 @@ def update_user_role(
request: Request,
user_id: int,
role: str = Form(...),
founder_badge: str = Form(""),
):
user = require_admin(request)
@@ -1587,20 +1689,6 @@ def update_user_role(
"UPDATE users SET is_admin = %s WHERE id = %s",
(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()
return RedirectResponse("/admin/users", status_code=303)
@@ -1981,11 +2069,12 @@ def logout(request: Request):
@app.get("/", response_class=HTMLResponse)
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")
return template.render(
user=get_current_user(request),
user=user,
upcoming_concerts=upcoming_concerts,
past_concerts=[],
archive=False,
@@ -1996,10 +2085,11 @@ def home(request: Request, q: str = ""):
@app.get("/events/past", response_class=HTMLResponse)
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")
return template.render(
user=get_current_user(request),
user=user,
upcoming_concerts=[],
past_concerts=past_concerts,
archive=True,
@@ -2102,9 +2192,11 @@ def render_profile(
"category": category,
"earned": code in profile["earned_codes"],
"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
]
badges.sort(key=lambda badge: badge["sort_key"])
template = templates.get_template("profile.html")
return HTMLResponse(
@@ -2416,7 +2508,8 @@ def new_concert(request: Request):
return login_redirect("/concerts/new")
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):
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(
"<h1>Veranstaltung nicht gefunden</h1>",
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)
with get_db_connection() as connection:
@@ -2561,6 +2662,8 @@ async def create_concert(
artist: str = Form(...),
event_type: str = Form("concert"),
parent_event_id: str = Form(""),
visibility: str = Form("public"),
invited_user_ids: list[int] = Form(default=[]),
venue_id: str = Form(""),
venue_name: 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)
if event_type != "festival":
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):
return HTMLResponse("<h1>Das Enddatum darf nicht vor dem Beginn liegen.</h1>", status_code=400)
try:
@@ -2639,10 +2746,11 @@ async def create_concert(
ticket_price,
flyer_path,
flyer_url,
visibility,
created_by
)
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
""",
@@ -2658,11 +2766,22 @@ async def create_concert(
ticket_price or None,
flyer_path,
normalized_flyer_url,
visibility,
user["id"],
),
)
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()
@@ -2690,7 +2809,7 @@ def edit_concert_page(request: Request, concert_id: int):
concert = load_concert(concert_id)
if not concert:
if not concert or not can_view_event(user, concert):
return HTMLResponse(
"<h1>Veranstaltung nicht gefunden</h1>",
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_details=can_edit_details(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(""),
event_type: str = Form("concert"),
parent_event_id: str = Form(""),
visibility: str = Form("public"),
invited_user_ids: list[int] = Form(default=[]),
venue_id: str = Form(""),
venue_name: str = Form(""),
city: str = Form(""),
@@ -2743,7 +2867,7 @@ async def edit_concert(
concert = load_concert(concert_id)
if not concert:
if not concert or not can_view_event(user, concert):
return HTMLResponse(
"<h1>Veranstaltung nicht gefunden</h1>",
status_code=404
@@ -2768,15 +2892,25 @@ async def edit_concert(
next_flyer_url = concert["flyer_url"]
next_venue_id = concert["venue"]["id"]
next_event_type = concert["event_type"]
next_visibility = concert["visibility"]
next_parent_event_id = concert["parent_event"]["id"] if concert["parent_event"] else None
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:
return HTMLResponse("<h1>Ungültige Veranstaltungskategorie.</h1>", status_code=400)
if event_type == "festival" and not end_datetime:
return HTMLResponse("<h1>Bei Festivals ist ein Enddatum erforderlich.</h1>", status_code=400)
if event_type != "festival":
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"]
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)
@@ -2820,6 +2954,7 @@ async def edit_concert(
next_start = start_datetime or concert["start_datetime"]
next_end = end_datetime or None
next_event_type = event_type
next_visibility = visibility
next_description = description or None
next_ticket_url = ticket_url or None
next_ticket_price = ticket_price or None
@@ -2843,6 +2978,7 @@ async def edit_concert(
ticket_price = %s,
flyer_path = %s,
flyer_url = %s
, visibility = %s
WHERE id = %s
""",
(
@@ -2857,9 +2993,26 @@ async def edit_concert(
next_ticket_price,
next_flyer,
next_flyer_url,
next_visibility,
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()
return RedirectResponse(
@@ -2877,7 +3030,7 @@ def delete_concert(request: Request, concert_id: int):
concert = load_concert(concert_id)
if not concert:
if not concert or not can_view_event(user, concert):
return HTMLResponse(
"<h1>Veranstaltung nicht gefunden</h1>",
status_code=404
@@ -2918,7 +3071,8 @@ def set_attendance(
return login_redirect(f"/concerts/{concert_id}")
if status not in {"attending", "maybe", "ticket_search", "ticket_offer"}:
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)
with get_db_connection() as connection:
@@ -2952,7 +3106,7 @@ def add_comment(
concert = load_concert(concert_id)
if not concert:
if not concert or not can_view_event(user, concert):
return HTMLResponse(
"<h1>Veranstaltung nicht gefunden</h1>",
status_code=404
@@ -3006,7 +3160,7 @@ async def add_photo(
concert = load_concert(concert_id)
if not concert:
if not concert or not can_view_event(user, concert):
return HTMLResponse(
"<h1>Veranstaltung nicht gefunden</h1>",
status_code=404
+1
View File
@@ -3,6 +3,7 @@
<div class="user-menu-panel">
<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>
{% 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">
<button type="submit">Abmelden</button>
</form>
-1
View File
@@ -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>
<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>
<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>
</form>
<div class="role-form">
+1
View File
@@ -63,6 +63,7 @@
<div class="concert-content">
<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>
{{ concert.artist }}
+23 -1
View File
@@ -23,7 +23,7 @@
{% if can_edit_details %}
<p>
<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="festival" {% if concert.event_type == 'festival' %}selected{% endif %}>Festival</option>
<option value="other" {% if concert.event_type == 'other' %}selected{% endif %}>Sonstiges</option>
@@ -40,6 +40,19 @@
</select>
</label>
</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 %}
<p>
<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 festivalEndField = document.getElementById("festival-end-field");
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() {
const isFestival = eventType.value === "festival";
@@ -132,11 +148,17 @@ function updateEventFields() {
festivalEndField.hidden = !isFestival;
festivalEndDatetime.required = isFestival;
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 (!isOther) parentEventId.value = "";
}
eventType.addEventListener("change", updateEventFields);
visibilityInputs.forEach(input => input.addEventListener("change", updateEventFields));
</script>
{% endif %}
</body>
+2 -1
View File
@@ -205,6 +205,7 @@
{% macro event_card(concert, linked=false) %}
<a href="/concerts/{{ concert.id }}" class="concert-card{% if linked %} linked-event{% endif %}">
<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">
{% if concert.event_type == 'festival' %}🎪{% elif concert.event_type == 'other' %}🥂{% else %}🎸{% endif %}
{{ concert.artist }}
@@ -290,7 +291,7 @@
{% if upcoming_concerts or past_concerts %}
<div class="concert-list">
<div class="concert-list" id="event-invitations">
{% for concert in upcoming_concerts %}
<div class="event-group">
+22
View File
@@ -126,6 +126,21 @@
<small>Zum Beispiel ein gemeinsames Vortrinken vor einem Konzert.</small>
</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 -->
<!-- ================================================= -->
@@ -389,6 +404,9 @@ const parentEventField = document.getElementById("parent-event-field");
const parentEventId = document.getElementById("parent-event-id");
const festivalEndField = document.getElementById("festival-end-field");
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() {
const isFestival = eventType.value === "festival";
@@ -396,11 +414,15 @@ function updateEventFields() {
festivalEndField.hidden = !isFestival;
festivalEndDatetime.required = isFestival;
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 (!isOther) parentEventId.value = "";
}
eventType.addEventListener("change", updateEventFields);
visibilityInputs.forEach(input => input.addEventListener("change", updateEventFields));
updateEventFields();
/* ============================================================