Een tijdje terug kwam weer het eerste mailtje binnen: “Bart, jij doet toch de jaarplanning van de kerk? Hier zijn wat alvast wat data.”
Okeee dan.
Even wat context: ik ben lid van een kerkelijke gemeente, en sinds een paar jaar maak ik voor die gemeente de jaarplanning, waar vervolgens een andere vrijwilliger een mooi opgemaakte jaarkalender van maakt. Die jaarplanning bevat alle activiteiten van grofweg september tot maart. Denk aan de Bijbelkring (ieder maand), de catechisaties (bijna iedere week), de Kerstviering (raad eens?), enzovoort. Alleen de normale kerkdiensten staan er niet in.
En aangezien ik er een handje van heb om overzichtjes te maken, en agenda’s bij te houden, en zo stom was om ooit te zeggen dat ik iets met computers kan, kwam die taak al snel bij mij te liggen (de vormgeving doet een ander, ik hou alleen de planning bij).
Goed, de jaarplanning dus. Een overzicht van het jaar, maar ook per activiteit. Ik moet kunnen achterhalen welke donderdagen in september vrij zijn (want iemand wil dan iets organiseren), maar ook snel het overzicht van alle keren Bijbelkring kunnen zien.
Gelukkig hebben we net zoals zoveel kerken de Scipio App. Zeg maar een soort van interne Facebook. Nieuwsberichten, groepjes, ledengegevens, verjaardagen, en… een agenda. Et voilà, gewoon alles in de agenda van de Scipio App gooien?
Ja, maar:
- Niet iedereen kan of wil de app gebruiken.
- Het geeft nog niet voldoende overzicht.
- Het is voor de vormgever niet handig (die er een overzichtelijk a4-tje van maakt).
Oké, alles in de app gooien, en dan een export maken? Nou, bijna. er is geen normale mens-leesbare export. Maar Scipio biedt wel een iCal-feed. Dat is een deelbaar linkje, die je in je eigen agendasysteem kunt invoeren als extra agenda.
Ah, mooi, denkt de computeraar in mij. Dat betekend namelijk gestructureerde, gestandaardiseerde, machine-leesbare data. En inderdaad, als ik het linkje rechtstreeks in de adresbalk van mijn browser gooi, dan download mijn browser een ICS-bestand.
Stap 2: de data verwerken
ICS/Icalender, hmm? Laat daar nou eens een mooie Python-library voor zijn.
Voor de niet-ingewijden: Python is een mooie programmeertaal om ’even snel’ een scriptje in te schrijven: kleine miniprogrammaatjes, ideaal voor dit soort klusjes (Python wordt overigens ook gebruikt voor grotere complexere dingen, niet beledigd zijn beste Python-ontwikkelaars). Een library is een setje voorgedefinieerde functieblokjes. In dit geval om een ICS-bestand in te lezen.
Cool:
with open(path_to_ics_file) as ics:
calendar = icalendar.Calendar.from_ical(ics.read())
for event in calendar.walk('VEVENT'):
# En nu?
We hebben nu iets om door alle agenda-items (VEVENT
) in de export te lopen.
Laten we een functie definiëren om zo’n agenda-item verder uit te lezen
en weg te schrijven naar een formaat dat ons wat meer past:
def handle_event(event, normaal, volledig):
summary = event.get('SUMMARY').strip()
De SUMMARY
is de titel van het item,
soms met spaties ervoor en erna, dus die strip()
-en we eraf.
Van sommige items weten we dat ze niet op de jaarkalender hoeven, bijvoorbeeld de reguliere kerkdiensten (die worden inclusief liturgie door een andere vrijwilliger ingevoerd). Filter time!
skippables = [
'^Morgendienst',
'^Avonddienst',
'...'
]
# Dit zal vast veel mooier kunnen.
for skippable in skippables:
if re.search(skippable, summary) is not None:
return
Dan komt er wat gedoe met tijdzones, want ICS bevat de tijd in UTC (wij leven in CET/CEST). Niet belangrijk voor de gemiddelde lezer hier, maar even voor de volledigheid.
dtstart = event.get('DTSTART').dt
if isinstance(dtstart, datetime.datetime):
dtstart = dtstart.astimezone(desired_timezone)
dtend_raw = event.get('DTEND')
if dtend_raw is None:
dtend = None
else:
dtend = dtend_raw.dt
if isinstance(dtend, datetime.datetime):
dtend = dtend.astimezone(desired_timezone)
Oké, next. In het normale bestand wil ik een kopje boven iedere maand:
# Dit is echt verschrikkelijke code.
# Dit kan echt veel mooier.
global prev_month
month = dtstart.strftime('%B %Y')
if month != prev_month:
prev_month = month
print(f'\n{month}\n', file=normaal)
Dan komt de echt uitvoer. We willen van meerdaagse activiteiten alleen de begin- en einddatum, zonder tijden. Van sommige enkeldaage activiteiten willen ook geen eindtijd, simpelweg omdat het de hele dag is. Let’s go:
start_time_only = [
'Biddag',
'Dankdag',
'...',
]
date = dtstart.strftime('%a %d')
time = ''
if dtend is not None:
duration = dtend - dtstart
if duration.days > 0:
# Meerdaags
date = f'{dtstart.strftime("%d")} - {dtend.strftime("%d")}'
else:
time = dtstart.strftime('%H:%M')
if summary.split(' ')[0] not in start_time_only:
# Enkeldaags, met eindtijd.
time += f' tot {dtend.strftime("%H:%M")}'
line = f'{date}|{summary}{suffixes.get(summary, "")}|{time}'
print(line, file=normaal)
print(f'{month}|{line}', file=volledig)
Het resultaat
Stukje van het normale bestand met de uitloop van afgelopen winterseizoen:
april 2025
do 03|Bijbelkring (gezamenlijk)|20:00 tot 21:30
di 08|Seniorenmiddag|14:30
di 08|Jonge ledenkring|20:00 tot 21:30
za 12|Jeugdvereniging Nathanaël|20:00 tot 22:00
za 19|Samenzanguurtje Passie en Pasen|14:30 tot 15:30
mei 2025
za 03|Jeugdvereniging Nathanaël|20:00 tot 22:00
za 17|Jeugdvereniging Nathanaël|20:00 tot 22:00
do 22|Bezinningsuur Heilig Avondmaal|20:00 tot 21:00
28 - 31|Kamp JV Nathanaël|
Stukje van het volledige bestand:
april 2025|do 03|Bijbelkring (gezamenlijk)|20:00 tot 21:30
april 2025|di 08|Seniorenmiddag|14:30
april 2025|di 08|Jonge ledenkring|20:00 tot 21:30
april 2025|za 12|Jeugdvereniging Nathanaël|20:00 tot 22:00
april 2025|za 19|Samenzanguurtje Passie en Pasen|14:30 tot 15:30
mei 2025|za 03|Jeugdvereniging Nathanaël|20:00 tot 22:00
mei 2025|za 17|Jeugdvereniging Nathanaël|20:00 tot 22:00
mei 2025|do 22|Bezinningsuur Heilig Avondmaal|20:00 tot 21:00
mei 2025|28 - 31|Kamp JV Nathanaël|
Automatiseren
Hierboven vroeg je je misschien af: waarom een normaal bestand en een volledig bestand? Het normale bestand is vooral heel praktisch voor de vormgever, het is al bijna in het formaat zoals hij het neerzet. Het volledige bestand is vooral voor mezelf. Dit bevat iedere activiteit op 1 regel, zonder kopjes. Oftewel: makkelijk geautomatiseerd verschillen zoeken!
Script time! Dit keer een shell scriptje.
#!/bin/sh
set -e
wget 'https://api.socie.nl/v2/public/ical/nietdeechtekeywatdenkjezelf' -O 'ical.ics'
previous=$(find jk -name '*-volledig.txt' | sort | tail -n 1)
base=$(./main.py)
new=$(find jk -name '*-volledig.txt' | sort | tail -n 1)
if [ -n "$previous" ]; then
# `|| true` omdat diff een 'fout' aangeeft bij wijzigingen
diff -ty --suppress-common-lines "$previous" "$new" > "jk/${base}-wijzigingen.txt" || true
fi
Het stappenplan
- Vraag de verantwoordelijke van activiteit X om de data door te geven.
- Voer de data in in Scipio.
- Draai het shell script, dat voor mij het volgende doet:
- Download het bestand.
- Zoek het meest recente volledige bestand
- Maak het nieuwe volledige bestand (en het normale bestand)
- Geef de ‘diff’ (het verschil) tussen het oude en het nieuwe volledige bestand.
- Staan in dit verschil precies de data die ik in moest voeren? Mooi, dan klopt het, en stuur ik alle bestanden alvast naar de vormgever. Hij ziet dan ook meteen wat er is gewijzigd.
- Repeat.