Compare commits
21 changed files with 322 additions and 319 deletions
|
|
@ -9,6 +9,11 @@ charset = utf-8
|
||||||
end_of_line = lf
|
end_of_line = lf
|
||||||
insert_final_newline = true
|
insert_final_newline = true
|
||||||
|
|
||||||
[*.{js,yaml}]
|
[*.py]
|
||||||
|
indent_style = space
|
||||||
|
indent_size = 4
|
||||||
|
|
||||||
|
[*.yaml]
|
||||||
indent_style = space
|
indent_style = space
|
||||||
indent_size = 2
|
indent_size = 2
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -14,13 +14,14 @@ jobs:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: apk --no-cache add hugo git openssh-client rsync
|
run: apk --no-cache add hugo python3 py3-pip git openssh-client rsync
|
||||||
|
|
||||||
- name: Check versions
|
- name: Check versions
|
||||||
run: |
|
run: |
|
||||||
cat /etc/os-release
|
cat /etc/os-release
|
||||||
git version
|
git version
|
||||||
hugo version
|
hugo version
|
||||||
|
python --version
|
||||||
rsync --version
|
rsync --version
|
||||||
ssh -V
|
ssh -V
|
||||||
|
|
||||||
|
|
@ -32,6 +33,9 @@ jobs:
|
||||||
git clone -b $GIT_BRANCH --recursive https://git.berlin.ccc.de/cccb-website-team/www.git .
|
git clone -b $GIT_BRANCH --recursive https://git.berlin.ccc.de/cccb-website-team/www.git .
|
||||||
git status
|
git status
|
||||||
|
|
||||||
|
- name: Install Python dependencies
|
||||||
|
run: python -m pip install -r requirements.txt --break-system-packages
|
||||||
|
|
||||||
- name: Render site
|
- name: Render site
|
||||||
run: ./build.sh
|
run: ./build.sh
|
||||||
|
|
||||||
|
|
|
||||||
58
.forgejo/workflows/miau.yaml
Normal file
58
.forgejo/workflows/miau.yaml
Normal file
|
|
@ -0,0 +1,58 @@
|
||||||
|
name: deploy blog
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
|
schedule:
|
||||||
|
- cron: '0 10 * * *' # daily at 10:00
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- staging
|
||||||
|
- production
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
deploy:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Install dependencies
|
||||||
|
run: apk --no-cache add hugo python3 py3-pip git openssh-client rsync
|
||||||
|
|
||||||
|
- name: Check versions
|
||||||
|
run: |
|
||||||
|
cat /etc/os-release
|
||||||
|
git version
|
||||||
|
hugo version
|
||||||
|
python --version
|
||||||
|
rsync --version
|
||||||
|
ssh -V
|
||||||
|
|
||||||
|
- name: Set envionment vars
|
||||||
|
run: echo "GIT_BRANCH=${{ forgejo.event_name == 'schedule' && 'production' || forgejo.ref_name }}" >> "$FORGEJO_ENV"
|
||||||
|
|
||||||
|
- name: Checkout repository
|
||||||
|
run: |
|
||||||
|
git clone -b $GIT_BRANCH --recursive https://git.berlin.ccc.de/cccb-website-team/www.git .
|
||||||
|
git status
|
||||||
|
|
||||||
|
- name: Install Python dependencies
|
||||||
|
run: python -m pip install -r requirements.txt --break-system-packages
|
||||||
|
|
||||||
|
- name: Render site
|
||||||
|
run: ./build.sh
|
||||||
|
|
||||||
|
- name: Setup SSH
|
||||||
|
run: |
|
||||||
|
mkdir -p ~/.ssh
|
||||||
|
printf "%s" "${{ secrets.KNOWN_HOSTS }}" | base64 -d > ~/.ssh/known_hosts
|
||||||
|
printf "%s" "${{ env.GIT_BRANCH == 'production' && secrets.SSH_PRIVATE_KEY_PRODUCTION || secrets.SSH_PRIVATE_KEY_STAGING }}" | base64 -d > ~/.ssh/id_ed25519
|
||||||
|
chmod 600 ~/.ssh/id_ed25519
|
||||||
|
ssh-keygen -f ~/.ssh/id_ed25519 -y > ~/.ssh/id_ed25519.pub
|
||||||
|
cat ~/.ssh/id_ed25519.pub
|
||||||
|
|
||||||
|
- name: Rsync rendered site
|
||||||
|
# TODO: add --delete
|
||||||
|
run: rsync -var -e 'ssh -i ~/.ssh/id_ed25519 -o StrictHostKeyChecking=yes' ./public/ deploy@www.berlin.ccc.de:${{ env.GIT_BRANCH == 'production' && '/srv/http/www/' || '/srv/http/www-staging/' }}
|
||||||
|
|
||||||
|
- name: Cleanup
|
||||||
|
if: ${{ always() }}
|
||||||
|
run: rm -rf ~/.ssh
|
||||||
|
|
||||||
1
.gitattributes
vendored
Normal file
1
.gitattributes
vendored
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
*.ics text eol=crlf
|
||||||
133
.gitignore
vendored
133
.gitignore
vendored
|
|
@ -1,9 +1,8 @@
|
||||||
# Created by https://www.toptal.com/developers/gitignore/api/windows,linux,macos,hugo,pycharm+all,vim,direnv
|
static/all.ics
|
||||||
# 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
|
||||||
|
|
@ -44,6 +43,7 @@ hugo.linux
|
||||||
# Icon must end with two \r
|
# Icon must end with two \r
|
||||||
Icon
|
Icon
|
||||||
|
|
||||||
|
|
||||||
# Thumbnails
|
# Thumbnails
|
||||||
._*
|
._*
|
||||||
|
|
||||||
|
|
@ -67,114 +67,6 @@ 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
|
||||||
|
|
@ -201,4 +93,17 @@ $RECYCLE.BIN/
|
||||||
# Windows shortcuts
|
# Windows shortcuts
|
||||||
*.lnk
|
*.lnk
|
||||||
|
|
||||||
# End of https://www.toptal.com/developers/gitignore/api/windows,linux,macos,hugo,pycharm+all,vim,direnv
|
# End of https://www.toptal.com/developers/gitignore/api/windows,linux,macos,hugo
|
||||||
|
|
||||||
|
# NixOS:
|
||||||
|
.envrc
|
||||||
|
shell.nix
|
||||||
|
.direnv
|
||||||
|
|
||||||
|
# Python
|
||||||
|
.venv
|
||||||
|
|
||||||
|
# murmeldin
|
||||||
|
overview.md
|
||||||
|
|
||||||
|
/result
|
||||||
|
|
|
||||||
1
.python-version
Normal file
1
.python-version
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
3.11
|
||||||
28
README.md
28
README.md
|
|
@ -27,20 +27,24 @@ 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 as `hugo serve` is running.
|
Every change you make on the project will be reflected in your browser as long
|
||||||
|
as `hugo serve` is running.
|
||||||
|
|
||||||
The *"Nächste Veranstaltungen"* table on the home page is generated by post-processing in `./build.sh`, not by Hugo, so
|
The *"Nächste Veranstaltungen"* table on the home page is generated by
|
||||||
it is **not** visible under `hugo serve`. To preview the fully built site (including the home-page calendar), or to
|
post-processing in `./build.sh`, not by Hugo, so it is **not** visible under
|
||||||
ready the site for upload, run:
|
`hugo serve`. To preview the fully-built site (including the home-page
|
||||||
|
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` replaces the `CALENDAR` placeholder in `index.html` with the upcoming-events table. It depends on Python
|
`build.sh` generates `all.ics` and replaces the `CALENDAR` placeholder in
|
||||||
with the `icalendar`, `python-dateutil`, and `pytz` packages, plus a `de_DE.UTF-8` locale (used to format weekday
|
`index.html` with the upcoming-events table. It depends on Python with the
|
||||||
names). Inside `nix develop` these are provided automatically.
|
`icalendar`, `python-dateutil`, and `pytz` packages, plus a `de_DE.UTF-8`
|
||||||
|
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'`
|
||||||
|
|
||||||
|
|
@ -51,8 +55,7 @@ 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 a merge request to merge changes from `staging` to `production` branch. Ask somebody to check merge request or
|
5. Create merge request to merge changes from `staging` to `production` branch. Ask somebody to check merge request or if small change, merge yourself.
|
||||||
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!
|
||||||
|
|
@ -61,12 +64,9 @@ 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
|
- You can build the staging and production builds with `nix build .#staging-content` and `nix build .#production-content`
|
||||||
`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)
|
||||||
- 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).
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -431,7 +431,7 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||||
}
|
}
|
||||||
|
|
||||||
// ICS-Datei abrufen und Events verarbeiten
|
// ICS-Datei abrufen und Events verarbeiten
|
||||||
fetch('/calendars/all.ics')
|
fetch('/all.ics')
|
||||||
.then(response => response.text())
|
.then(response => response.text())
|
||||||
.then(data => {
|
.then(data => {
|
||||||
events = parseICS(data);
|
events = parseICS(data);
|
||||||
|
|
@ -442,7 +442,9 @@ 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));
|
||||||
})();
|
})();
|
||||||
|
|
|
||||||
|
|
@ -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`
|
||||||
}
|
}
|
||||||
|
|
|
||||||
7
build.sh
7
build.sh
|
|
@ -1,7 +1,12 @@
|
||||||
#!/bin/sh
|
#!/bin/sh
|
||||||
|
|
||||||
set -ex
|
set -e
|
||||||
|
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
|
||||||
|
|
|
||||||
|
|
@ -28,7 +28,7 @@ filename = "sitemap.xml"
|
||||||
priority = 0.5
|
priority = 0.5
|
||||||
|
|
||||||
[permalinks]
|
[permalinks]
|
||||||
post = "/post/:year/:month/:day/:slug/"
|
post = "/post/:year/:month/:day/:title/"
|
||||||
|
|
||||||
[outputs]
|
[outputs]
|
||||||
home = ["html", "rss", "json"]
|
home = ["html", "rss", "json"]
|
||||||
|
|
|
||||||
|
|
@ -25,9 +25,6 @@ description: "Startseite CCCB mit Kurzkalender"
|
||||||
|
|
||||||
### Nächste Veranstaltungen
|
### Nächste Veranstaltungen
|
||||||
|
|
||||||
<table id="upcoming" class="table table-condensed">
|
CALENDAR
|
||||||
</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>
|
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
---
|
---
|
||||||
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"]
|
||||||
|
|
|
||||||
|
|
@ -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/neujahresempfang-2026/)
|
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/)
|
||||||
|
|
@ -12,15 +12,4 @@ herostyle: big
|
||||||
|
|
||||||
{{< calendar >}}
|
{{< calendar >}}
|
||||||
|
|
||||||
Keinen Termin mehr verpeilen? Einfach den [Veranstaltungskalender abonnieren](/calendars/all.ics)!
|
Keinen Termin mehr verpeilen? Einfach den [Veranstaltungskalender abonnieren](/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
8
flake.lock
generated
|
|
@ -2,16 +2,16 @@
|
||||||
"nodes": {
|
"nodes": {
|
||||||
"nixpkgs": {
|
"nixpkgs": {
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1786313170,
|
"lastModified": 1748995628,
|
||||||
"narHash": "sha256-9BG7OgUWdu0ONDO5X2q6+K4bsuBITkX/3W4nNJu1Ito=",
|
"narHash": "sha256-bFufQGSAEYQgjtc4wMrobS5HWN0hDP+ZX+zthYcml9U=",
|
||||||
"owner": "nixos",
|
"owner": "nixos",
|
||||||
"repo": "nixpkgs",
|
"repo": "nixpkgs",
|
||||||
"rev": "fcb8fcd6bf2d0adecae5bd491afaaaf8311b758d",
|
"rev": "8eb3b6a2366a7095939cd22f0dc0e9991313294b",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
"owner": "nixos",
|
"owner": "nixos",
|
||||||
"ref": "nixos-26.05",
|
"ref": "nixos-24.11",
|
||||||
"repo": "nixpkgs",
|
"repo": "nixpkgs",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
13
flake.nix
13
flake.nix
|
|
@ -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-26.05";
|
nixpkgs.url = "github:nixos/nixpkgs/nixos-24.11";
|
||||||
};
|
};
|
||||||
|
|
||||||
outputs =
|
outputs =
|
||||||
|
|
@ -34,7 +34,7 @@
|
||||||
|
|
||||||
devShells = forAllSystems (import ./devShells.nix);
|
devShells = forAllSystems (import ./devShells.nix);
|
||||||
|
|
||||||
formatter = forAllSystems ({ pkgs, ... }: pkgs.nixfmt-tree);
|
formatter = forAllSystems ({ pkgs, ... }: pkgs.nixfmt-rfc-style);
|
||||||
|
|
||||||
lib.mkWwwContent =
|
lib.mkWwwContent =
|
||||||
{ domain, system }:
|
{ domain, system }:
|
||||||
|
|
@ -50,6 +50,10 @@
|
||||||
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";
|
||||||
|
|
@ -58,6 +62,11 @@
|
||||||
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
|
||||||
|
|
|
||||||
2
requirements.txt
Normal file
2
requirements.txt
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
icalendar==5.0.7
|
||||||
|
pytz
|
||||||
|
|
@ -1,113 +0,0 @@
|
||||||
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);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
108
tools/gen_upcoming.py
Executable file
108
tools/gen_upcoming.py
Executable file
|
|
@ -0,0 +1,108 @@
|
||||||
|
#!/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)
|
||||||
|
|
||||||
31
tools/merge_cals.py
Executable file
31
tools/merge_cals.py
Executable file
|
|
@ -0,0 +1,31 @@
|
||||||
|
#!/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())
|
||||||
|
|
||||||
Loading…
Reference in a new issue