feat(calendar): fuzzy Polish name matching + richer ICS/Google Calendar export
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

Name linking now handles Polish declensions (Iwonę/Iwoną/Iwony → Iwona)
using stem-based regex matching. ICS and Google Calendar exports now include
full event description, speaker name, and properly formatted newlines.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Maciej Pienczyn 2026-03-17 15:01:42 +01:00
parent 08a499ac40
commit c456ab49af
2 changed files with 39 additions and 5 deletions

View File

@ -144,10 +144,25 @@ def _enrich_event_description(db, html):
).all()
members = sorted(members, key=lambda m: len(m.name), reverse=True)
# person_map: exact name → url
# person_fuzzy: list of (pattern, display_name, url) for declined Polish names
person_map = {}
person_fuzzy = []
for m in members:
if m.name not in person_map: # first match wins (longest name already sorted)
person_map[m.name] = flask_url_for('public.user_profile', user_id=m.id)
if m.name in person_map:
continue
url = flask_url_for('public.user_profile', user_id=m.id)
person_map[m.name] = url
# Build fuzzy pattern for Polish name declensions: "Iwona Spaleniak" → "Iwon\w+ Spaleniak"
parts = m.name.split()
if len(parts) >= 2:
first = parts[0]
rest = ' '.join(parts[1:])
# Stem: keep at least 3 chars, cut last 1-2 chars depending on length
stem_len = max(3, len(first) - 2)
stem = re.escape(first[:stem_len])
fuzzy_pattern = r'\b' + stem + r'\w*\s+' + re.escape(rest) + r'\b'
person_fuzzy.append((fuzzy_pattern, m.name, url))
# Build replacement maps — companies
from database import Company
@ -173,10 +188,16 @@ def _enrich_event_description(db, html):
text = re.sub(url_pattern, url_replacer, text)
# 2. Link person names (pill badge — green)
# First: exact matches
for name, url in person_map.items():
pattern = r'\b' + re.escape(name) + r'\b'
link = f'<a href="{url}" class="person-link" title="Zobacz profil">{name}</a>'
text = re.sub(pattern, link, text)
# Then: fuzzy matches for Polish declensions (Iwonę → Iwona, etc.)
for fuzzy_pattern, display_name, url in person_fuzzy:
def fuzzy_replacer(m, _url=url, _display=display_name):
return f'<a href="{_url}" class="person-link" title="Zobacz profil">{m.group(0)}</a>'
text = re.sub(fuzzy_pattern, fuzzy_replacer, text)
# 3. Link company names (pill badge — orange)
for name, url in company_map.items():

View File

@ -640,18 +640,26 @@ const _evt = {
start: '{{ event.time_start.strftime("%H%M") if event.time_start else "0000" }}',
end: '{{ event.time_end.strftime("%H%M") if event.time_end else "" }}',
location: {{ (event.location or '')|tojson }},
speaker: {{ (event.speaker_name or '')|tojson }},
description: {{ (event.description or '')|tojson }},
url: window.location.href,
};
function addToGoogleCalendar() {
const start = _evt.date + 'T' + _evt.start + '00';
const end = _evt.end ? (_evt.date + 'T' + _evt.end + '00') : '';
const details = [
_evt.speaker ? 'Prowadzący: ' + _evt.speaker : '',
_evt.description,
'',
'Szczegóły: ' + _evt.url,
].filter(Boolean).join('\n');
const params = new URLSearchParams({
action: 'TEMPLATE',
text: _evt.title,
dates: start + '/' + (end || start),
location: _evt.location,
details: 'Szczegóły: ' + _evt.url,
details: details,
ctz: 'Europe/Warsaw',
});
window.open('https://calendar.google.com/calendar/render?' + params, '_blank');
@ -662,8 +670,13 @@ function downloadICS() {
const end = _evt.end ? (_evt.date + 'T' + _evt.end + '00') : (_evt.date + 'T' + _evt.start + '00');
const uid = 'nordabiz-event-{{ event.id }}@nordabiznes.pl';
const now = new Date().toISOString().replace(/[-:]/g,'').split('.')[0] + 'Z';
const speaker = {{ (event.speaker_name or '')|tojson }};
const desc = (speaker ? 'Prowadzący: ' + speaker + '\\n' : '') + 'Szczegóły: ' + _evt.url;
const descParts = [
_evt.speaker ? 'Prowadzący: ' + _evt.speaker : '',
_evt.description,
'',
'Szczegóły: ' + _evt.url,
].filter(Boolean);
const desc = descParts.join('\\n');
const ics = [
'BEGIN:VCALENDAR',
'VERSION:2.0',