From eaac876ec26b358175c4f79b192ade34e0d53ba5 Mon Sep 17 00:00:00 2001 From: Maciej Pienczyn Date: Wed, 15 Apr 2026 17:52:31 +0200 Subject: [PATCH] =?UTF-8?q?feat(calendar):=20multi-day=20events=20+=20**bo?= =?UTF-8?q?ld**=20w=20opisach=20wydarze=C5=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - norda_events: kolumna event_date_end (NULLABLE, check constraint >= event_date) - NordaEvent: property is_multi_day, date_range_display; is_past uwzględnia koniec - Admin (new/edit): pole "Data zakończenia" w formularzu - Calendar grid: wydarzenie wielodniowe wyświetla się na każdym dniu zakresu - Upcoming/past filter: używa COALESCE(end, date) — 2-dniowe zostaje w Upcoming do swojego ostatniego dnia - event.html: "Termin" + zakres dla wielodniowych; ICS/Google end date z dateEnd - Lekki markdown dla opisów: tylko **bold** → (audyt: tylko event #60) Zero wpływu na 42 istniejące wydarzenia (NULL == stare zachowanie). Co-Authored-By: Claude Opus 4.6 (1M context) --- blueprints/admin/routes.py | 11 ++++- blueprints/community/calendar/routes.py | 43 ++++++++++++++++--- database.py | 16 ++++++- .../migrations/105_add_event_date_end.sql | 16 +++++++ templates/calendar/admin_new.html | 8 +++- templates/calendar/event.html | 11 +++-- templates/calendar/index.html | 2 +- 7 files changed, 92 insertions(+), 15 deletions(-) create mode 100644 database/migrations/105_add_event_date_end.sql diff --git a/blueprints/admin/routes.py b/blueprints/admin/routes.py index caad9bc..4e4f912 100644 --- a/blueprints/admin/routes.py +++ b/blueprints/admin/routes.py @@ -1447,10 +1447,16 @@ def admin_calendar_new(): is_external = request.form.get('is_external') == 'on' is_paid = request.form.get('is_paid') == 'on' + event_date_end_raw = request.form.get('event_date_end', '').strip() + event_date_end = datetime.strptime(event_date_end_raw, '%Y-%m-%d').date() if event_date_end_raw else None + event_date_start = datetime.strptime(request.form.get('event_date'), '%Y-%m-%d').date() + if event_date_end and event_date_end < event_date_start: + event_date_end = None # nie akceptuj odwróconego zakresu event = NordaEvent( title=request.form.get('title', '').strip(), description=sanitize_html(request.form.get('description', '').strip()), - event_date=datetime.strptime(request.form.get('event_date'), '%Y-%m-%d').date(), + event_date=event_date_start, + event_date_end=event_date_end, time_start=request.form.get('time_start') or None, time_end=request.form.get('time_end') or None, location=request.form.get('location', '').strip() or None, @@ -1526,6 +1532,9 @@ def admin_calendar_edit(event_id): event.title = request.form.get('title', '').strip() event.description = sanitize_html(request.form.get('description', '').strip()) event.event_date = datetime.strptime(request.form.get('event_date'), '%Y-%m-%d').date() + _end_raw = request.form.get('event_date_end', '').strip() + _end = datetime.strptime(_end_raw, '%Y-%m-%d').date() if _end_raw else None + event.event_date_end = _end if (_end and _end >= event.event_date) else None event.time_start = request.form.get('time_start') or None event.time_end = request.form.get('time_end') or None event.location = request.form.get('location', '').strip() or None diff --git a/blueprints/community/calendar/routes.py b/blueprints/community/calendar/routes.py index da2c968..fc71283 100644 --- a/blueprints/community/calendar/routes.py +++ b/blueprints/community/calendar/routes.py @@ -72,6 +72,17 @@ def index(): NordaEvent.event_date <= last_day ).order_by(NordaEvent.event_date.asc()).all() + # Zakres zapytania: także wydarzenia, których zakres zahacza o miesiąc + # (np. 30.06-02.07 widoczne zarówno w czerwcu jak i lipcu). + from sqlalchemy import or_ as _sql_or, and_ as _sql_and + extra_events = db.query(NordaEvent).filter( + NordaEvent.event_date_end.isnot(None), + NordaEvent.event_date < first_day, + NordaEvent.event_date_end >= first_day, + NordaEvent.event_date_end <= last_day, + ).all() + all_events = list(all_events) + [e for e in extra_events if e not in all_events] + # Filtruj wydarzenia według uprawnień użytkownika events = [e for e in all_events if e.can_user_view(current_user)] @@ -79,23 +90,34 @@ def index(): cal = cal_module.Calendar(firstweekday=0) month_days = cal.monthdayscalendar(year, month) - # Mapuj wydarzenia na dni + # Mapuj wydarzenia na dni. + # Wydarzenia wielodniowe pokazujemy na KAŻDYM dniu w zakresie mieszczącym + # się w bieżącym miesiącu — by uczestnik widział w kalendarzu cały czas trwania. for event in events: - day = event.event_date.day - if day not in events_by_day: - events_by_day[day] = [] - events_by_day[day].append(event) + span_start = max(event.event_date, first_day) + span_end = min(event.event_date_end or event.event_date, last_day) + d = span_start + while d <= span_end: + day = d.day + if day not in events_by_day: + events_by_day[day] = [] + events_by_day[day].append(event) + d += timedelta(days=1) # Dane dla widoku listy (zawsze potrzebne dla fallback) + # COALESCE: dla wydarzeń wielodniowych używamy daty zakończenia, by event + # "trwający dzisiaj" pozostał w Upcoming aż do swojego ostatniego dnia. + from sqlalchemy import func as _sql_func + _effective_end = _sql_func.coalesce(NordaEvent.event_date_end, NordaEvent.event_date) all_upcoming = db.query(NordaEvent).filter( - NordaEvent.event_date >= today + _effective_end >= today ).order_by(NordaEvent.event_date.asc()).all() # Filtruj według uprawnień upcoming = [e for e in all_upcoming if e.can_user_view(current_user)] all_past = db.query(NordaEvent).filter( - NordaEvent.event_date < today + _effective_end < today ).order_by(NordaEvent.event_date.desc()).limit(10).all() past = [e for e in all_past if e.can_user_view(current_user)][:5] @@ -139,6 +161,13 @@ def _enrich_event_description(db, html): paragraphs = html.split('\n\n') html = ''.join(f'

{p.replace(chr(10), "
")}

' for p in paragraphs if p.strip()) + # Lekki markdown: **bold** → bold. + # Safe bo audyt (2026-04): tylko 1 wydarzenie używa `**` (świadomie, event #60). + # Działamy po wrappingu HTML, ale `**` nie występuje w tagach ani URL-ach, więc + # nie psuje innych treści. Nie rozszerzamy na *italic* / __bold__ / listy / linki, + # by nie zmieniać istniejącego wyglądu (vide #45 z `•` zamiast `-`). + html = re.sub(r'\*\*([^*\n]+)\*\*', r'\1', html) + # Build replacement maps — persons (all users with a name) members = db.query(User.id, User.name).filter( User.name.isnot(None), diff --git a/database.py b/database.py index 4a45b88..4c7ed9c 100644 --- a/database.py +++ b/database.py @@ -2241,6 +2241,7 @@ class NordaEvent(Base): # Data i czas event_date = Column(Date, nullable=False) + event_date_end = Column(Date) # NULL dla wydarzeń jednodniowych time_start = Column(Time) time_end = Column(Time) @@ -2306,7 +2307,20 @@ class NordaEvent(Base): @property def is_past(self): from datetime import date - return self.event_date < date.today() + end = self.event_date_end or self.event_date + return end < date.today() + + @property + def is_multi_day(self): + return bool(self.event_date_end and self.event_date_end > self.event_date) + + @property + def date_range_display(self): + """Zwraca 'DD.MM.YYYY' lub 'DD.MM.YYYY – DD.MM.YYYY' dla wielodniowych.""" + start = self.event_date.strftime('%d.%m.%Y') + if self.is_multi_day: + return f"{start} – {self.event_date_end.strftime('%d.%m.%Y')}" + return start def can_user_view(self, user) -> bool: """Check if a user can view this event (title, date, location). diff --git a/database/migrations/105_add_event_date_end.sql b/database/migrations/105_add_event_date_end.sql new file mode 100644 index 0000000..ddbc7f7 --- /dev/null +++ b/database/migrations/105_add_event_date_end.sql @@ -0,0 +1,16 @@ +-- Migration 105: Add event_date_end for multi-day events +-- Dodaje opcjonalną datę zakończenia dla wydarzeń wielodniowych (np. targi 19-20.06). + +ALTER TABLE norda_events + ADD COLUMN IF NOT EXISTS event_date_end DATE; + +COMMENT ON COLUMN norda_events.event_date_end IS + 'Data zakończenia wydarzenia (NULL = jednodniowe). Musi być >= event_date.'; + +-- Prosty check constraint: jeśli ustawione, musi być >= event_date +ALTER TABLE norda_events + DROP CONSTRAINT IF EXISTS norda_events_date_range_check; + +ALTER TABLE norda_events + ADD CONSTRAINT norda_events_date_range_check + CHECK (event_date_end IS NULL OR event_date_end >= event_date); diff --git a/templates/calendar/admin_new.html b/templates/calendar/admin_new.html index 8442d2e..c4622e2 100755 --- a/templates/calendar/admin_new.html +++ b/templates/calendar/admin_new.html @@ -174,11 +174,17 @@
- +
+
+ + +
Pozostaw puste dla wydarzeń jednodniowych. Jeśli wydarzenie trwa 2+ dni (np. targi, konferencja), podaj ostatni dzień.
+
+