Allows users to register accompanying guests (non-portal users) for events. Covers data model, API endpoints, UI design, and migration plan. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
9.1 KiB
Osoby towarzyszące na wydarzeniach (Event Guests)
Data: 2026-03-31 Status: Draft Kontekst: Użytkownicy portalu chcą zapisywać na wydarzenia osoby towarzyszące — współpracowników, pracowników, osoby spoza Izby — które nie mają konta na portalu. Scenariusz zgłoszony przez prezesa Leszka Glazę: sam nie uczestniczy, ale chce zapisać kogoś w swoim imieniu.
Wymagania
- Zalogowany użytkownik może dodać jednego lub wielu gości na wydarzenie
- Użytkownik nie musi sam być zapisany, żeby dodać gościa
- Dane gościa (imię, nazwisko, firma/organizacja) są opcjonalne, ale minimum jedno pole musi być wypełnione
- Goście wliczają się do limitu
max_attendees - Goście są widoczni na liście uczestników z imieniem, nazwiskiem, firmą i informacją kto ich zapisał
- Host może edytować i usuwać swoich gości
- Admin (OFFICE_MANAGER+) może usuwać/edytować dowolnych gości
- Nie dotyczy wydarzeń zewnętrznych (
is_external = True) — tam rejestracja jest u organizatora - Max 5 gości per użytkownik per wydarzenie (stała aplikacyjna)
Model danych
Nowa tabela event_guests
| Kolumna | Typ | Nullable | Default | Opis |
|---|---|---|---|---|
id |
Integer PK |
— | auto | — |
event_id |
Integer FK → norda_events.id |
NOT NULL | — | CASCADE DELETE |
host_user_id |
Integer FK → users.id |
NOT NULL | — | CASCADE DELETE |
first_name |
String(100) |
TAK | NULL |
Imię gościa |
last_name |
String(100) |
TAK | NULL |
Nazwisko gościa |
organization |
String(255) |
TAK | NULL |
Firma/organizacja |
created_at |
DateTime |
NOT NULL | now() |
Timestamp dodania |
Indeksy:
ix_event_guests_event_idnaevent_idix_event_guests_host_user_idnahost_user_id
Brak unique constraint — użytkownik może zapisać osoby bez danych lub z powtarzającymi się danymi.
Nowy model SQLAlchemy: EventGuest
class EventGuest(db.Model):
__tablename__ = 'event_guests'
id = db.Column(db.Integer, primary_key=True)
event_id = db.Column(db.Integer, db.ForeignKey('norda_events.id', ondelete='CASCADE'), nullable=False, index=True)
host_user_id = db.Column(db.Integer, db.ForeignKey('users.id', ondelete='CASCADE'), nullable=False, index=True)
first_name = db.Column(db.String(100), nullable=True)
last_name = db.Column(db.String(100), nullable=True)
organization = db.Column(db.String(255), nullable=True)
created_at = db.Column(db.DateTime, default=datetime.utcnow, nullable=False)
event = db.relationship('NordaEvent', backref=db.backref('guests', cascade='all, delete-orphan', lazy='dynamic'))
host = db.relationship('User', backref=db.backref('hosted_guests', lazy='dynamic'))
Zmiany w NordaEvent
Nowa property:
@property
def total_attendee_count(self):
"""Łączna liczba uczestników + gości (do sprawdzania limitu max_attendees)."""
return len(self.attendees) + self.guests.count()
Istniejąca attendee_count pozostaje bez zmian (kompatybilność wsteczna).
API
Stała: MAX_GUESTS_PER_USER = 5
POST /kalendarz/<int:event_id>/guests
Dodaje gościa na wydarzenie.
Request body (JSON):
{
"first_name": "Jan",
"last_name": "Kowalski",
"organization": "Sigma Budownictwo"
}
Wszystkie pola opcjonalne, ale minimum jedno niepuste.
Logika:
@login_required- Sprawdź czy wydarzenie istnieje i nie jest przeszłe
- Sprawdź
event.can_user_attend(current_user) - Sprawdź
event.is_external == False - Sprawdź limit gości:
EventGuest.query.filter_by(event_id=event_id, host_user_id=current_user.id).count() < MAX_GUESTS_PER_USER - Sprawdź
max_attendees: jeśli ustawiony,event.total_attendee_count < event.max_attendees - Walidacja: minimum jedno pole (first_name, last_name, organization) niepuste
- Utwórz
EventGuest, commit - Zwróć
201z danymi gościa
Odpowiedź (201):
{
"action": "added",
"guest": {
"id": 42,
"first_name": "Jan",
"last_name": "Kowalski",
"organization": "Sigma Budownictwo"
}
}
Błędy: 400 (walidacja), 403 (brak uprawnień), 409 (limit gości lub max_attendees)
PATCH /kalendarz/<int:event_id>/guests/<int:guest_id>
Edytuje dane gościa.
Request body (JSON): jak POST, pola które mają się zmienić.
Logika:
@login_required- Sprawdź czy gość istnieje i należy do tego wydarzenia
- Sprawdź czy
current_userjest hostem gościa LUB ma rolęOFFICE_MANAGER+ - Sprawdź czy wydarzenie nie jest przeszłe
- Walidacja: po aktualizacji minimum jedno pole niepuste
- Aktualizuj pola, commit
- Zwróć
200z zaktualizowanymi danymi
DELETE /kalendarz/<int:event_id>/guests/<int:guest_id>
Usuwa gościa z wydarzenia.
Logika:
@login_required- Sprawdź czy gość istnieje i należy do tego wydarzenia
- Sprawdź czy
current_userjest hostem LUB ma rolęOFFICE_MANAGER+ - Usuń rekord, commit
- Zwróć
200z{"action": "removed"}
Zmiana w istniejącym RSVP
W route calendar_rsvp, zmiana sprawdzania limitu:
# Było:
if event.max_attendees and event.attendee_count >= event.max_attendees:
# Jest:
if event.max_attendees and event.total_attendee_count >= event.max_attendees:
UI — strona wydarzenia (event.html)
Sekcja "Osoby towarzyszące"
Wyświetlana poniżej przycisku RSVP. Warunki widoczności:
event.can_user_attend(current_user)= True- Wydarzenie nie jest przeszłe
- Wydarzenie nie jest external
Przycisk
[+ Dodaj osobę towarzyszącą]
Po kliknięciu rozwija inline formularz (toggle, bez przeładowania strony).
Formularz (inline, rozwijany)
┌─────────────────────────────────────┐
│ Imię: [____________] │
│ Nazwisko: [____________] │
│ Firma/org.: [____________] │
│ │
│ [Dodaj] [Anuluj] │
└─────────────────────────────────────┘
- Tryb dodawania: przycisk "Dodaj", pola puste
- Tryb edycji: przycisk "Zapisz", pola wypełnione danymi gościa
- Walidacja frontend: minimum jedno pole niepuste, komunikat "Podaj przynajmniej imię, nazwisko lub firmę"
- Po sukcesie: formularz się czyści (tryb dodawania) lub zamyka (tryb edycji), lista gości się odświeża
- Komunikacja via fetch/JSON, spójnie z istniejącym RSVP
Lista gości bieżącego użytkownika
Pod formularzem, widoczna tylko jeśli użytkownik ma gości na tym wydarzeniu:
Twoi goście (2/5):
• Jan Kowalski (Sigma Budownictwo) [edytuj] [✕]
• Anna Nowak [edytuj] [✕]
- Kliknięcie
[edytuj]otwiera formularz w trybie edycji - Kliknięcie
[✕]usuwa gościa (po potwierdzeniunordaConfirm()) (2/5)— informacja o wykorzystanym limicie
Lista uczestników (istniejąca sekcja, zmodyfikowana)
Goście wyświetlani pod swoim hostem z wcięciem:
Zapisani (7):
• Leszek Glaza — Sigma Budownictwo
└ gość: Jan Kowalski (Sigma Budownictwo)
└ gość: Anna Nowak
• Roman Wierciński — Sigma Budownictwo
• Maciej Pienczyn — InPi
└ gość: Tomek Zieliński (ARP)
Jeśli host nie jest zapisany sam (scenariusz Leszka), wyświetlany jest bez oznaczenia uczestnictwa:
• Leszek Glaza (nie uczestniczy) — Sigma Budownictwo
└ gość: Jan Kowalski (Sigma Budownictwo)
Licznik
7 osób zapisanych (było: {{ event.attendee_count }}, jest: {{ event.total_attendee_count }})
Migracja SQL
Plik: database/migrations/091_event_guests.sql
CREATE TABLE event_guests (
id SERIAL PRIMARY KEY,
event_id INTEGER NOT NULL REFERENCES norda_events(id) ON DELETE CASCADE,
host_user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
first_name VARCHAR(100),
last_name VARCHAR(100),
organization VARCHAR(255),
created_at TIMESTAMP NOT NULL DEFAULT NOW()
);
CREATE INDEX ix_event_guests_event_id ON event_guests(event_id);
CREATE INDEX ix_event_guests_host_user_id ON event_guests(host_user_id);
GRANT ALL ON TABLE event_guests TO nordabiz_app;
GRANT USAGE, SELECT ON SEQUENCE event_guests_id_seq TO nordabiz_app;
Poza zakresem
- Powiadomienia email dla gości (brak konta = brak dostarczenia)
- Rejestracja gości na wydarzenia zewnętrzne
- Samodzielna rejestracja gościa (bez konta)
- Eksport listy uczestników do CSV/PDF
- Panel admin do zarządzania gośćmi (admin korzysta z widoku wydarzenia)
Podsumowanie zmian
| Komponent | Zmiana |
|---|---|
database.py |
Nowy model EventGuest, nowa property total_attendee_count na NordaEvent |
blueprints/community/calendar/routes.py |
3 nowe endpointy (POST/PATCH/DELETE guests), zmiana sprawdzania limitu w RSVP |
templates/calendar/event.html |
Sekcja gości (formularz + lista), modyfikacja listy uczestników i licznika |
database/migrations/091_event_guests.sql |
Nowa tabela |