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ń.