switch to ics files; make calendars work; fix some minor issues

This commit is contained in:
XenGi 2026-08-12 02:01:58 +02:00
commit c28f04c6e8
Signed by: xengi
SSH key fingerprint: SHA256:jxWM2RTHvxxcncXycwwWkP7HCWb4VREN05UGJTbIPZg
19 changed files with 318 additions and 259 deletions

View file

@ -9,11 +9,6 @@ charset = utf-8
end_of_line = lf end_of_line = lf
insert_final_newline = true insert_final_newline = true
[*.py] [*.{js,yaml}]
indent_style = space
indent_size = 4
[*.yaml]
indent_style = space indent_style = space
indent_size = 2 indent_size = 2

1
.gitattributes vendored
View file

@ -1 +0,0 @@
*.ics text eol=crlf

133
.gitignore vendored
View file

@ -1,8 +1,9 @@
static/all.ics # Created by https://www.toptal.com/developers/gitignore/api/windows,linux,macos,hugo,pycharm+all,vim,direnv
# Edit at https://www.toptal.com/developers/gitignore?templates=windows,linux,macos,hugo,pycharm+all,vim,direnv
### direnv ###
.direnv
.envrc .envrc
*.swp
# Created by https://www.toptal.com/developers/gitignore/api/windows,linux,macos,hugo
# Edit at https://www.toptal.com/developers/gitignore?templates=windows,linux,macos,hugo
### Hugo ### ### Hugo ###
# Generated files by hugo # Generated files by hugo
@ -43,7 +44,6 @@ hugo.linux
# Icon must end with two \r # Icon must end with two \r
Icon Icon
# Thumbnails # Thumbnails
._* ._*
@ -67,6 +67,114 @@ Temporary Items
# iCloud generated files # iCloud generated files
*.icloud *.icloud
### PyCharm+all ###
# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider
# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
# User-specific stuff
.idea/**/workspace.xml
.idea/**/tasks.xml
.idea/**/usage.statistics.xml
.idea/**/dictionaries
.idea/**/shelf
# AWS User-specific
.idea/**/aws.xml
# Generated files
.idea/**/contentModel.xml
# Sensitive or high-churn files
.idea/**/dataSources/
.idea/**/dataSources.ids
.idea/**/dataSources.local.xml
.idea/**/sqlDataSources.xml
.idea/**/dynamic.xml
.idea/**/uiDesigner.xml
.idea/**/dbnavigator.xml
# Gradle
.idea/**/gradle.xml
.idea/**/libraries
# Gradle and Maven with auto-import
# When using Gradle or Maven with auto-import, you should exclude module files,
# since they will be recreated, and may cause churn. Uncomment if using
# auto-import.
# .idea/artifacts
# .idea/compiler.xml
# .idea/jarRepositories.xml
# .idea/modules.xml
# .idea/*.iml
# .idea/modules
# *.iml
# *.ipr
# CMake
cmake-build-*/
# Mongo Explorer plugin
.idea/**/mongoSettings.xml
# File-based project format
*.iws
# IntelliJ
out/
# mpeltonen/sbt-idea plugin
.idea_modules/
# JIRA plugin
atlassian-ide-plugin.xml
# Cursive Clojure plugin
.idea/replstate.xml
# SonarLint plugin
.idea/sonarlint/
# Crashlytics plugin (for Android Studio and IntelliJ)
com_crashlytics_export_strings.xml
crashlytics.properties
crashlytics-build.properties
fabric.properties
# Editor-based Rest Client
.idea/httpRequests
# Android studio 3.1+ serialized cache file
.idea/caches/build_file_checksums.ser
### PyCharm+all Patch ###
# Ignore everything but code style settings and run configurations
# that are supposed to be shared within teams.
.idea/*
!.idea/codeStyles
!.idea/runConfigurations
### Vim ###
# Swap
[._]*.s[a-v][a-z]
!*.svg # comment out if you don't need vector files
[._]*.sw[a-p]
[._]s[a-rt-v][a-z]
[._]ss[a-gi-z]
[._]sw[a-p]
# Session
Session.vim
Sessionx.vim
# Temporary
.netrwhist
# Auto-generated tag files
tags
# Persistent undo
[._]*.un~
### Windows ### ### Windows ###
# Windows thumbnail cache files # Windows thumbnail cache files
Thumbs.db Thumbs.db
@ -93,17 +201,4 @@ $RECYCLE.BIN/
# Windows shortcuts # Windows shortcuts
*.lnk *.lnk
# End of https://www.toptal.com/developers/gitignore/api/windows,linux,macos,hugo # End of https://www.toptal.com/developers/gitignore/api/windows,linux,macos,hugo,pycharm+all,vim,direnv
# NixOS:
.envrc
shell.nix
.direnv
# Python
.venv
# murmeldin
overview.md
/result

View file

@ -1 +0,0 @@
3.11

View file

@ -27,24 +27,20 @@ This is the website of the CCCB.
5. Point your browser to: <http://localhost:1313/> 5. Point your browser to: <http://localhost:1313/>
Every change you make on the project will be reflected in your browser as long Every change you make on the project will be reflected in your browser as long as `hugo serve` is running.
as `hugo serve` is running.
The *"Nächste Veranstaltungen"* table on the home page is generated by The *"Nächste Veranstaltungen"* table on the home page is generated by post-processing in `./build.sh`, not by Hugo, so
post-processing in `./build.sh`, not by Hugo, so it is **not** visible under it is **not** visible under `hugo serve`. To preview the fully built site (including the home-page calendar), or to
`hugo serve`. To preview the fully-built site (including the home-page ready the site for upload, run:
calendar), or to ready the site for upload, run:
```shell ```shell
./build.sh ./build.sh
python3 -m http.server -d public 1313 python3 -m http.server -d public 1313
``` ```
`build.sh` generates `all.ics` and replaces the `CALENDAR` placeholder in `build.sh` replaces the `CALENDAR` placeholder in `index.html` with the upcoming-events table. It depends on Python
`index.html` with the upcoming-events table. It depends on Python with the with the `icalendar`, `python-dateutil`, and `pytz` packages, plus a `de_DE.UTF-8` locale (used to format weekday
`icalendar`, `python-dateutil`, and `pytz` packages, plus a `de_DE.UTF-8` names). Inside `nix develop` these are provided automatically.
locale (used to format weekday names). Inside `nix develop` these are
provided automatically.
To build with *nix*: `nix build '.?submodules=1#production-content'` To build with *nix*: `nix build '.?submodules=1#production-content'`
@ -55,7 +51,8 @@ To build with *nix*: `nix build '.?submodules=1#production-content'`
3. Commit (and push) your change. 3. Commit (and push) your change.
4. ~~GitHub Actions is running the release workflow.~~ 4. ~~GitHub Actions is running the release workflow.~~
- If successful, check [Staging Website](https://staging.berlin.ccc.de/) if change is correct. - If successful, check [Staging Website](https://staging.berlin.ccc.de/) if change is correct.
5. Create merge request to merge changes from `staging` to `production` branch. Ask somebody to check merge request or if small change, merge yourself. 5. Create a merge request to merge changes from `staging` to `production` branch. Ask somebody to check merge request or
if small change, merge yourself.
6. ~~GitHub Actions is running the release workflow.~~ 6. ~~GitHub Actions is running the release workflow.~~
- If successfull, check [Website](https://berlin.ccc.de/) if change is correct. - If successfull, check [Website](https://berlin.ccc.de/) if change is correct.
7. Profit! 7. Profit!
@ -64,9 +61,12 @@ To build with *nix*: `nix build '.?submodules=1#production-content'`
- After entering the shell with `nix develop`, hugo is available and `hugo serve` should work - After entering the shell with `nix develop`, hugo is available and `hugo serve` should work
- Python including required packages will be available, so the `build.sh` should work without a venv - Python including required packages will be available, so the `build.sh` should work without a venv
- You can build the staging and production builds with `nix build .#staging-content` and `nix build .#production-content` - You can build the staging and production builds with `nix build .#staging-content` and
- Do not update the nixpkgs branch - 25.05 contains a newer hugo version that is incompatible with the theme (last checked June 2025) `nix build .#production-content`
- Do not update the nixpkgs branch - 25.05 contains a newer hugo version that is incompatible with the theme (last
checked June 2025)
--- ---
Made with ❤️ and [Hugo](https://gohugo.io). Made with ❤️ and [Hugo](https://gohugo.io).

View file

@ -431,7 +431,7 @@ document.addEventListener('DOMContentLoaded', function() {
} }
// ICS-Datei abrufen und Events verarbeiten // ICS-Datei abrufen und Events verarbeiten
fetch('/all.ics') fetch('/calendars/all.ics')
.then(response => response.text()) .then(response => response.text())
.then(data => { .then(data => {
events = parseICS(data); events = parseICS(data);
@ -442,9 +442,7 @@ document.addEventListener('DOMContentLoaded', function() {
currentMonth = today.getMonth(); currentMonth = today.getMonth();
// Process events for current month // Process events for current month
updateEventsForMonth(currentYear, currentMonth);
updateEventsForMonth(currentYear, currentMonth);
}) })
.catch(err => console.error('Fehler beim Laden der ICS-Datei:', err)); .catch(err => console.error('Fehler beim Laden der ICS-Datei:', err));
})(); })();

View file

@ -9,11 +9,11 @@ function timeDistanceDE(pastDate) {
const hours = Math.floor(minutes / 60) const hours = Math.floor(minutes / 60)
const days = Math.floor(hours / 24) const days = Math.floor(hours / 24)
if (days = 1) return `since ${days}d` if (days === 1) return `since ${days}d`
if (days > 1) return `since ${days}d` if (days > 1) return `since ${days}d`
if (hours = 1) return `since ${hours}h` if (hours === 1) return `since ${hours}h`
if (hours > 1) return `since ${hours}h` if (hours > 1) return `since ${hours}h`
if (minutes = 1) return `since ${minutes}min` if (minutes === 1) return `since ${minutes}min`
if (minutes > 1) return `since ${minutes}min` if (minutes > 1) return `since ${minutes}min`
return `gerade eben` return `gerade eben`
} }

View file

@ -1,12 +1,7 @@
#!/bin/sh #!/bin/sh
set -e set -ex
set -x
rm -rf public/ # delete old generated files rm -rf public/ # delete old generated files
hugo $(cat .hugo-params) hugo $(cat .hugo-params)
./tools/merge_cals.py
upcoming="$(tools/gen_upcoming.py static/all.ics 20 5 | tr '\n' ' ')"
cp static/all.ics public/all.ics
sed -i "s#CALENDAR#$upcoming#g" public/index.html

View file

@ -28,7 +28,7 @@ filename = "sitemap.xml"
priority = 0.5 priority = 0.5
[permalinks] [permalinks]
post = "/post/:year/:month/:day/:title/" post = "/post/:year/:month/:day/:slug/"
[outputs] [outputs]
home = ["html", "rss", "json"] home = ["html", "rss", "json"]

View file

@ -25,6 +25,9 @@ description: "Startseite CCCB mit Kurzkalender"
### Nächste Veranstaltungen ### Nächste Veranstaltungen
CALENDAR <table id="upcoming" class="table table-condensed">
</table>
Weitere Termine findest du im [Veranstaltungskalender](/verein/calendar/). Weitere Termine findest du im [Veranstaltungskalender](/verein/calendar/).
<script type="module" src="/js/upcoming.js"></script>

View file

@ -1,5 +1,6 @@
--- ---
title: "Der Neujahresempfang 2026 #NJE26" title: "Der Neujahresempfang 2026 #NJE26"
slug: "neujahresempfang-2026"
date: 2026-03-05T12:00:00+01:00 date: 2026-03-05T12:00:00+01:00
showHero: false showHero: false
tags: ["NJE", "Ankündigung"] tags: ["NJE", "Ankündigung"]

View file

@ -10,4 +10,4 @@ menu:
tag: ["Veranstaltung"] tag: ["Veranstaltung"]
--- ---
Am 23. Mai lädt der Chaos Computer Club Berlin zum Neujahresempfang. [Hier geht es zur Infoseite!](https://berlin.ccc.de/post/2026/03/05/der-neujahresempfang-2026-%23nje26/) Am 23. Mai lädt der Chaos Computer Club Berlin zum Neujahresempfang. [Hier geht es zur Infoseite!](https://berlin.ccc.de/post/2026/03/05/neujahresempfang-2026/)

View file

@ -12,4 +12,15 @@ herostyle: big
{{< calendar >}} {{< calendar >}}
Keinen Termin mehr verpeilen? Einfach den [Veranstaltungskalender abonnieren](/all.ics)! Keinen Termin mehr verpeilen? Einfach den [Veranstaltungskalender abonnieren](/calendars/all.ics)!
Oder willst du nur den Kalender von bestimmten Events?
- [Club Discordia](/calendars/Club_Discordia.ics)
- [Neujahresempfang](/calendars/Neujahresempfang.ics)
- [Datengarten](/calendars/Datengarten.ics)
- [Spieleabend](/calendars/Spieleabend.ics)
- [OpenWRT](/calendars/OpenWRT.ics)
- [Plenum](/calendars/Plenum.ics)
- [Amateurfunk](/calendars/Amateurfunk.ics)
- [Irreguläre Events](/calendars/Random.ics)

8
flake.lock generated
View file

@ -2,16 +2,16 @@
"nodes": { "nodes": {
"nixpkgs": { "nixpkgs": {
"locked": { "locked": {
"lastModified": 1748995628, "lastModified": 1786313170,
"narHash": "sha256-bFufQGSAEYQgjtc4wMrobS5HWN0hDP+ZX+zthYcml9U=", "narHash": "sha256-9BG7OgUWdu0ONDO5X2q6+K4bsuBITkX/3W4nNJu1Ito=",
"owner": "nixos", "owner": "nixos",
"repo": "nixpkgs", "repo": "nixpkgs",
"rev": "8eb3b6a2366a7095939cd22f0dc0e9991313294b", "rev": "fcb8fcd6bf2d0adecae5bd491afaaaf8311b758d",
"type": "github" "type": "github"
}, },
"original": { "original": {
"owner": "nixos", "owner": "nixos",
"ref": "nixos-24.11", "ref": "nixos-26.05",
"repo": "nixpkgs", "repo": "nixpkgs",
"type": "github" "type": "github"
} }

View file

@ -2,7 +2,7 @@
description = "A flake containing a development environment for the CCCB website."; description = "A flake containing a development environment for the CCCB website.";
inputs = { inputs = {
nixpkgs.url = "github:nixos/nixpkgs/nixos-24.11"; nixpkgs.url = "github:nixos/nixpkgs/nixos-26.05";
}; };
outputs = outputs =
@ -34,7 +34,7 @@
devShells = forAllSystems (import ./devShells.nix); devShells = forAllSystems (import ./devShells.nix);
formatter = forAllSystems ({ pkgs, ... }: pkgs.nixfmt-rfc-style); formatter = forAllSystems ({ pkgs, ... }: pkgs.nixfmt-tree);
lib.mkWwwContent = lib.mkWwwContent =
{ domain, system }: { domain, system }:
@ -50,10 +50,6 @@
nativeBuildInputs = [ nativeBuildInputs = [
pkgs.hugo pkgs.hugo
pkgs.glibcLocales pkgs.glibcLocales
(pkgs.python3.withPackages (python-pkgs: [
python-pkgs.icalendar
python-pkgs.pytz
]))
]; ];
# LOCALE_ARCHIVE = builtins.trace pkgs.glibcLocales "${pkgs.glibcLocales}/lib/locale/locale-archive"; # LOCALE_ARCHIVE = builtins.trace pkgs.glibcLocales "${pkgs.glibcLocales}/lib/locale/locale-archive";
@ -62,11 +58,6 @@
buildPhase = '' buildPhase = ''
mkdir -p public mkdir -p public
hugo --baseURL=https://${domain}/ hugo --baseURL=https://${domain}/
python3 ./tools/merge_cals.py
upcoming="$(python3 tools/gen_upcoming.py static/all.ics 20 5 | tr '\n' ' ')"
cp static/all.ics public/all.ics
sed -i "s#CALENDAR#$upcoming#g" public/index.html
''; '';
# Install phase - copy the public directory to the output # Install phase - copy the public directory to the output

View file

@ -1,2 +0,0 @@
icalendar==5.0.7
pytz

113
static/js/upcoming.js Normal file
View file

@ -0,0 +1,113 @@
import ICAL from "https://unpkg.com/ical.js/dist/ical.min.js";
/**
* Parse an ICS calendar and return upcoming event occurrences.
*
* @param {string} icsText The contents of the .ics file
* @param {Date} now Events must start after this date
* @param {number} maxEvents Maximum number of events to return
* @param {number} maxDays Maximum number of days into the future
* @returns {{start: Date, name: string, url: string}[]}
*/
function getUpcomingEvents(icsText, now, maxEvents, maxDays) {
const jcal = ICAL.parse(icsText);
const calendar = new ICAL.Component(jcal);
const end = new Date(now.getTime());
end.setDate(end.getDate() + maxDays);
const events = [];
for (const component of calendar.getAllSubcomponents("vevent")) {
const event = new ICAL.Event(component);
if (!event.startDate) {
continue;
}
if (event.isRecurring()) {
const iterator = event.iterator();
while (true) {
const occurrence = iterator.next();
if (!occurrence) {
break;
}
const start = occurrence.toJSDate();
// Recurrences are chronological, so we're done
// once we pass the end of our search window.
if (start > end) {
break;
}
if (start > now) {
events.push({
start,
name: event.summary ?? "",
url: event.url ?? "",
});
}
}
} else {
const start = event.startDate.toJSDate();
if (start > now && start <= end) {
events.push({
start,
name: event.summary ?? "",
url: event.url ?? "",
});
}
}
}
// We have occurrences from multiple events, so sort them
// before applying the maximum event count.
events.sort((a, b) => a.start - b.start);
return events.slice(0, maxEvents);
}
document.addEventListener("DOMContentLoaded", () => {
const ics = "/calendars/all.ics";
const max_days = 20;
const max_items = 5;
const now = new Date();
const table = document.getElementById("upcoming");
fetch(ics)
.then(response => response.text())
.then(icsText => {
getUpcomingEvents(icsText, now, max_items, max_days).forEach(event => {
const row = document.createElement("tr");
const colBegin = document.createElement("td");
const formattedStart = event.start.toLocaleString("de-DE", {
weekday: "long",
day: "2-digit",
month: "2-digit",
hour: "2-digit",
minute: "2-digit",
}).replace(",", "");
colBegin.innerText = `${formattedStart.replace(" ", ", ")} Uhr`;
row.appendChild(colBegin);
const colName = document.createElement("td");
const a = document.createElement("a");
a.href = event.url;
a.text = event.name;
colName.appendChild(a);
row.appendChild(colName);
table.appendChild(row);
});
});
});

View file

@ -1,108 +0,0 @@
#!/usr/bin/env python3
import sys
import logging
import locale
from dateutil.parser import parse as _parse
from datetime import datetime, timedelta
from dateutil.rrule import rruleset, rrulestr
import icalendar
def parse(value):
# Some templates emit DTSTART with a trailing "Z" (e.g. 20260507T190000Z)
# which produces tz-aware datetimes, while others emit naive local times.
# Normalize to naive so comparisons and sorting don't mix the two.
dt = _parse(value)
if dt.tzinfo is not None:
dt = dt.replace(tzinfo=None)
return dt
def vevent_to_event(event, rrstart=None):
if rrstart == None:
begin = parse(event["DTSTART"].to_ical())
else:
begin = rrstart
return {
"name": event["SUMMARY"].to_ical(),
"url": event["URL"].to_ical(),
"begin": begin
}
def parse_single_event(event, start, end):
logging.info(f"Processing single event {event['SUMMARY'].to_ical().decode('utf-8')}")
dtstart = parse(event["DTSTART"].to_ical())
if dtstart >= start and dtstart < end:
return vevent_to_event(event)
def parse_recurring_event(event, start, end):
logging.info(f"Processing recurring event {event['SUMMARY'].to_ical().decode('utf-8')}")
dtstart = parse(event["DTSTART"].to_ical())
rs = rruleset()
# dtstart is normalized to naive local time above; pass ignoretz so a
# tz-aware UNTIL (RFC 5545 requires UTC for it) is treated as naive too,
# otherwise dateutil rejects the naive/aware mismatch.
rs.rrule(rrulestr(event["RRULE"].to_ical().decode("utf-8"), dtstart=dtstart, ignoretz=True))
if "EXDATE" in event.keys():
for exdate in event["EXDATE"]:
rs.exdate(parse(exdate.to_ical()))
events = []
for date in list(rs):
if date >= start and date < end:
events.append(vevent_to_event(event, date))
return events
def find_events(icsfilestr, start, end, num):
with open(icsfilestr, "r") as icsfile:
cal = icalendar.Calendar.from_ical(icsfile.read())
events = []
for event in cal.subcomponents:
if event.name == "VEVENT":
if "RRULE" in event.keys():
events.extend(parse_recurring_event(event, start, end))
else:
ev = parse_single_event(event, start, end)
if ev is not None:
events.append(ev)
events = sorted(events, key=lambda k: k["begin"])
events = events[0:num]
return events
def format_events(events):
print("<table class=\"table table-condensed\">")
for event in events:
print(
"<tr>"
f"<td>{event['begin'].strftime('%A, %d.%m um %H:%M Uhr')}</td>"
f"<td><a href=\"{event['url'].decode('utf-8')}\">{event['name'].decode('utf-8')}</a></td>"
"</tr>"
)
print("</table><!--/.table .table-condensed-->")
if __name__ == "__main__":
if len(sys.argv) < 3:
print(f"Usage: {sys.argv[0]} calendar max_days max_items")
sys.exit(-1)
locale.setlocale(locale.LC_TIME, "de_DE.UTF-8")
calendar = sys.argv[1]
max_days = int(sys.argv[2])
max_items = int(sys.argv[3])
now = datetime.now()
events = find_events(calendar, now, now + timedelta(days=max_days), max_items)
format_events(events)

View file

@ -1,31 +0,0 @@
#!/usr/bin/env python3
from glob import glob
import pytz
import icalendar
calendars = []
merged = icalendar.Calendar()
merged.add("prodid", "-//CCCB Calendar Generator//berlin.ccc.de//")
merged.add("version", "2.0")
for icsfilestr in glob("public/*/**/*.ics", recursive=True):
with open(icsfilestr, "r") as icsfile:
print(f"Importing {icsfilestr}")
calendars.append(icalendar.Calendar.from_ical(icsfile.read()))
for calendar in calendars:
for event in calendar.subcomponents:
if event.name != "VEVENT":
continue
if "DTSTART" not in event:
continue
merged.add_component(event)
outfile = "static/all.ics"
with open(outfile, "wb") as f:
print(f"writing to {outfile}...")
f.write(merged.to_ical())