Add iCal subscription feed for Norda Biznes events
Some checks are pending
NordaBiz Tests / Unit & Integration Tests (push) Waiting to run
NordaBiz Tests / E2E Tests (Playwright) (push) Blocked by required conditions
NordaBiz Tests / Smoke Tests (Production) (push) Blocked by required conditions
NordaBiz Tests / Send Failure Notification (push) Blocked by required conditions
Some checks are pending
NordaBiz Tests / Unit & Integration Tests (push) Waiting to run
NordaBiz Tests / E2E Tests (Playwright) (push) Blocked by required conditions
NordaBiz Tests / Smoke Tests (Production) (push) Blocked by required conditions
NordaBiz Tests / Send Failure Notification (push) Blocked by required conditions
New endpoint /kalendarz/ical returns .ics file with all upcoming events. Compatible with Google Calendar, iOS Calendar, and Outlook subscription. - Auto-refreshes every 6 hours (REFRESH-INTERVAL) - Includes event time, location, description, organizer - Handles all-day events (no time_start) and timed events - "Subscribe" button on calendar page with copy-to-clipboard modal - Instructions for iPhone, Google Calendar, and Outlook Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
be4bbfc1fd
commit
b28d3c1879
@ -3,11 +3,12 @@ Calendar Routes
|
||||
===============
|
||||
|
||||
Public calendar and event registration endpoints.
|
||||
Includes iCal subscription feed at /kalendarz/ical
|
||||
"""
|
||||
|
||||
from datetime import date
|
||||
from datetime import date, datetime, timedelta
|
||||
import calendar as cal_module
|
||||
from flask import render_template, request, redirect, url_for, flash, jsonify
|
||||
from flask import render_template, request, redirect, url_for, flash, jsonify, Response
|
||||
from flask_login import login_required, current_user
|
||||
|
||||
from . import bp
|
||||
@ -361,3 +362,91 @@ def rsvp(event_id):
|
||||
})
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
|
||||
@bp.route('/ical', endpoint='calendar_ical')
|
||||
def ical_feed():
|
||||
"""
|
||||
iCal subscription feed — public endpoint (no login required).
|
||||
Returns .ics file with all upcoming public/member events.
|
||||
Subscribe once in Google Calendar / iOS Calendar and events sync automatically.
|
||||
"""
|
||||
db = SessionLocal()
|
||||
try:
|
||||
events = db.query(NordaEvent).filter(
|
||||
NordaEvent.event_date >= date.today() - timedelta(days=30),
|
||||
NordaEvent.access_level.in_(['public', 'members_only'])
|
||||
).order_by(NordaEvent.event_date.asc()).all()
|
||||
|
||||
lines = [
|
||||
'BEGIN:VCALENDAR',
|
||||
'VERSION:2.0',
|
||||
'PRODID:-//NordaBiznes//Kalendarz//PL',
|
||||
'CALSCALE:GREGORIAN',
|
||||
'METHOD:PUBLISH',
|
||||
'X-WR-CALNAME:Norda Biznes - Wydarzenia',
|
||||
'X-WR-TIMEZONE:Europe/Warsaw',
|
||||
'REFRESH-INTERVAL;VALUE=DURATION:PT6H',
|
||||
'X-PUBLISHED-TTL:PT6H',
|
||||
]
|
||||
|
||||
for event in events:
|
||||
uid = f'event-{event.id}@nordabiznes.pl'
|
||||
|
||||
# Build DTSTART/DTEND
|
||||
if event.time_start:
|
||||
dt_start = datetime.combine(event.event_date, event.time_start)
|
||||
dtstart = f'DTSTART;TZID=Europe/Warsaw:{dt_start.strftime("%Y%m%dT%H%M%S")}'
|
||||
if event.time_end:
|
||||
dt_end = datetime.combine(event.event_date, event.time_end)
|
||||
dtend = f'DTEND;TZID=Europe/Warsaw:{dt_end.strftime("%Y%m%dT%H%M%S")}'
|
||||
else:
|
||||
dt_end = dt_start + timedelta(hours=2)
|
||||
dtend = f'DTEND;TZID=Europe/Warsaw:{dt_end.strftime("%Y%m%dT%H%M%S")}'
|
||||
else:
|
||||
# All-day event
|
||||
dtstart = f'DTSTART;VALUE=DATE:{event.event_date.strftime("%Y%m%d")}'
|
||||
next_day = event.event_date + timedelta(days=1)
|
||||
dtend = f'DTEND;VALUE=DATE:{next_day.strftime("%Y%m%d")}'
|
||||
|
||||
# Clean description (remove HTML, limit length)
|
||||
desc = (event.description or '').replace('\r\n', '\\n').replace('\n', '\\n').replace(',', '\\,').replace(';', '\\;')
|
||||
if len(desc) > 500:
|
||||
desc = desc[:497] + '...'
|
||||
|
||||
summary = (event.title or '').replace(',', '\\,').replace(';', '\\;')
|
||||
location = (event.location or '').replace(',', '\\,').replace(';', '\\;')
|
||||
|
||||
created = event.created_at.strftime('%Y%m%dT%H%M%SZ') if event.created_at else datetime.now().strftime('%Y%m%dT%H%M%SZ')
|
||||
|
||||
lines.append('BEGIN:VEVENT')
|
||||
lines.append(f'UID:{uid}')
|
||||
lines.append(dtstart)
|
||||
lines.append(dtend)
|
||||
lines.append(f'SUMMARY:{summary}')
|
||||
if location:
|
||||
lines.append(f'LOCATION:{location}')
|
||||
if desc:
|
||||
lines.append(f'DESCRIPTION:{desc}')
|
||||
if event.external_url:
|
||||
lines.append(f'URL:{event.external_url}')
|
||||
else:
|
||||
lines.append(f'URL:https://nordabiznes.pl/kalendarz/{event.id}')
|
||||
lines.append(f'ORGANIZER;CN={event.organizer_name or "Norda Biznes"}:mailto:{event.organizer_email or "biuro@norda-biznes.info"}')
|
||||
lines.append(f'DTSTAMP:{created}')
|
||||
lines.append('END:VEVENT')
|
||||
|
||||
lines.append('END:VCALENDAR')
|
||||
|
||||
ical_content = '\r\n'.join(lines)
|
||||
|
||||
return Response(
|
||||
ical_content,
|
||||
mimetype='text/calendar; charset=utf-8',
|
||||
headers={
|
||||
'Content-Disposition': 'attachment; filename="norda-biznes.ics"',
|
||||
'Cache-Control': 'no-cache, must-revalidate',
|
||||
}
|
||||
)
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
@ -410,7 +410,35 @@
|
||||
{% block content %}
|
||||
<div class="calendar-header">
|
||||
<h1>Kalendarz wydarzeń</h1>
|
||||
<p class="text-muted">Spotkania i wydarzenia Norda Biznes</p>
|
||||
<div style="display: flex; align-items: center; gap: var(--spacing-md); flex-wrap: wrap;">
|
||||
<p class="text-muted" style="margin: 0;">Spotkania i wydarzenia Norda Biznes</p>
|
||||
<button onclick="showSubscribeModal()" style="display: inline-flex; align-items: center; gap: 6px; padding: 6px 14px; background: #eff6ff; color: #1d4ed8; border: 1px solid #bfdbfe; border-radius: var(--radius); font-size: 13px; cursor: pointer; font-weight: 500; white-space: nowrap;">
|
||||
<svg width="14" height="14" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><rect x="3" y="4" width="18" height="18" rx="2" ry="2"/><line x1="16" y1="2" x2="16" y2="6"/><line x1="8" y1="2" x2="8" y2="6"/><line x1="3" y1="10" x2="21" y2="10"/></svg>
|
||||
Subskrybuj kalendarz
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Subscribe modal -->
|
||||
<div id="subscribeModal" style="display:none; position:fixed; top:0; left:0; right:0; bottom:0; background:rgba(0,0,0,0.5); z-index:9999; align-items:center; justify-content:center;">
|
||||
<div style="background:white; border-radius:var(--radius-xl); padding:24px; max-width:500px; width:90%; margin:auto; position:relative; top:50%; transform:translateY(-50%);">
|
||||
<button onclick="document.getElementById('subscribeModal').style.display='none'" style="position:absolute; top:12px; right:16px; background:none; border:none; font-size:20px; cursor:pointer; color:#64748b;">×</button>
|
||||
<h3 style="margin:0 0 8px 0; font-size:18px;">Dodaj wydarzenia do kalendarza</h3>
|
||||
<p style="color:#64748b; font-size:13px; line-height:1.5; margin-bottom:16px;">
|
||||
Skopiuj poniższy link i dodaj go jako subskrypcję w swoim kalendarzu.
|
||||
Wydarzenia Izby będą się automatycznie synchronizować.
|
||||
</p>
|
||||
<div style="display:flex; gap:8px; margin-bottom:16px;">
|
||||
<input id="icalUrl" readonly value="https://nordabiznes.pl/kalendarz/ical" style="flex:1; padding:8px 12px; border:1px solid #d1d5db; border-radius:var(--radius); font-size:13px; background:#f9fafb; font-family:monospace;">
|
||||
<button onclick="copyIcalUrl()" id="copyBtn" style="padding:8px 16px; background:#1d4ed8; color:white; border:none; border-radius:var(--radius); font-size:13px; cursor:pointer; font-weight:500; white-space:nowrap;">Kopiuj</button>
|
||||
</div>
|
||||
<div style="font-size:12px; color:#64748b; line-height:1.6;">
|
||||
<strong>Jak dodać:</strong><br>
|
||||
<strong>iPhone/Mac:</strong> Ustawienia → Konta → Dodaj konto → Inne → Subskrypcja kalendarza → wklej link<br>
|
||||
<strong>Google Calendar:</strong> Inne kalendarze (+) → Z adresu URL → wklej link<br>
|
||||
<strong>Outlook:</strong> Dodaj kalendarz → Subskrybuj z internetu → wklej link
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Toolbar z przyciskami widoku -->
|
||||
@ -664,4 +692,21 @@ async function toggleListRSVP(btn) {
|
||||
cb.addEventListener('change', applyFilter);
|
||||
applyFilter();
|
||||
})();
|
||||
|
||||
function showSubscribeModal() {
|
||||
document.getElementById('subscribeModal').style.display = 'flex';
|
||||
}
|
||||
|
||||
function copyIcalUrl() {
|
||||
var input = document.getElementById('icalUrl');
|
||||
input.select();
|
||||
document.execCommand('copy');
|
||||
var btn = document.getElementById('copyBtn');
|
||||
btn.textContent = 'Skopiowano!';
|
||||
setTimeout(function() { btn.textContent = 'Kopiuj'; }, 2000);
|
||||
}
|
||||
|
||||
document.getElementById('subscribeModal').addEventListener('click', function(e) {
|
||||
if (e.target === this) this.style.display = 'none';
|
||||
});
|
||||
{% endblock %}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user