diff --git a/content/page/angebote/breitensport/index.md b/content/page/angebote/breitensport/index.md index 11f68d8..2021b85 100644 --- a/content/page/angebote/breitensport/index.md +++ b/content/page/angebote/breitensport/index.md @@ -14,9 +14,7 @@ Mit unserer Breitensportgruppe sprechen wie diejenigen an, die zwar (noch) keine   ## Trainingszeit Breitensport -| Breitensport Standard | Tag | Zeit | Ort | -|-----------------------|-------------|----------------|-----------| -| Breitensport Standard | Mittwoch | 20:00 - 21:30 | VH Hinten | +{{}} {{% tsc/link-offers %}} diff --git a/content/page/angebote/dancestyles/index.md b/content/page/angebote/dancestyles/index.md index ae84075..95530d1 100644 --- a/content/page/angebote/dancestyles/index.md +++ b/content/page/angebote/dancestyles/index.md @@ -22,36 +22,15 @@ Und wann bist Du dabei? ## Trainingszeiten Dance-Styles Adults -|Dance Styles | Alter | Tag | Zeit | Ort | -|--------------------|---------|------------|---------------|-----------| -| D-S Adults 0 | ab 18 | Mittwoch | 19:00 - 20:00 | VH Hinten | -| D-S Adults 1 | ab 18 | Dienstag | 18:45 - 20:15 | VH Vorn | -| D-S Adults 3 | ab 18 | Donnerstag | 19:30 - 20:30 | VH Vorn | -| D-S Adults 5 | ab 18 | Montag | 18:15 - 19:15 | VH Vorn | -| D-S Contest Gruppe | Anfrage | Montag | 19:15 - 20:45 | VH Vorn | +{{< tsc/calendar/table category="DSAdults" showAge="true" >}} ## Trainingszeiten Dance-Styles Teens -|Dance-Styles Teens | Jahrgang | Tag | Zeit | Ort | -|-------------------|-----------|----------|---------------|------------| -| D-S Teens 0 | 2008 - 10 | Mittwoch | 18:00 - 19:00 | VH Vorn | -| D-S Teens 1 | 2004 - 07 | Mittwoch | 18:00 - 19:00 | VH Hinten | -| D-S Teens 2 | 2006 - 09 | Montag | 17:30 - 18:30 | VH Mitte | +{{< tsc/calendar/table category="DSTeens" showAge="true" >}} ## Trainingszeiten Dance-Styles Kids -|Dance-Styles Kids | Jahrgang | Tag | Zeit | Ort | -|---------------------|-----------|------------|---------------|-----------| -| D-S Minis 0 | 2019 - 20 | Freitag | 16:30 - 17:30 | VH Vorn | -| D-S Minis 1 | 2020 - 21 | Donnerstag | 16:45 - 17:45 | VH Mitte | -| D-S Kids 0 | 2011 - 13 | Dienstag | 17:45 - 18:45 | VH Vorn | -| D-S Kids 1 | 2012 - 14 | Donnerstag | 18:30 - 19:30 | VH Vorn | -| D-S Kids 2 | 2010 - 12 | Mittwoch | 17:00 - 18:00 | VH Vorn | -| D-S Kids 3 | 2014 - 16 | Mittwoch | 16:45 - 17:45 | VH Hinten | -| D-S-Kids 4 | 2015 - 17 | Dienstag | 16:30 - 17:30 | VH Vorn | -| D-S Kids 5 | 2010 - 13 | Donnerstag | 17:00 - 18:00 | VH Vorn | -| D-S-Kids 6 | 2017 - 19 | Montag | 17:00 - 18:00 | VH Vorn | -| D-S Kids 7 | 2012 - 15 | Mittwoch | 18:00 - 19:00 | VH Mitte | +{{< tsc/calendar/table category="DSKids" showAge="true" >}} Interesse an Standard- und Lateintanzkursen für Kinder und Jugendliche? Im Bereich Kinder/Jugend findest du weitere Angebote. diff --git a/content/page/angebote/discofox/index.md b/content/page/angebote/discofox/index.md index 0a6fc3d..b131cd1 100644 --- a/content/page/angebote/discofox/index.md +++ b/content/page/angebote/discofox/index.md @@ -18,8 +18,6 @@ Wer schon Discofox tanzen kann, lernt bestimmt die eine oder andere tolle neue F ## Trainingszeit Discofox -|Discofox | Tag | Zeit | Ort | -|---|---|---|---| -|Discofox | Freitag | 20:30 - 21:30 | VH Vorn | +{{< tsc/calendar/table category="Discofox" >}} {{% tsc/link-offers %}} diff --git a/content/page/angebote/kinder/index.md b/content/page/angebote/kinder/index.md index 05b24af..a67f30a 100644 --- a/content/page/angebote/kinder/index.md +++ b/content/page/angebote/kinder/index.md @@ -20,12 +20,7 @@ Für manchen ist es bereits eine gute Erfahrung, um später in einer der größe ## Trainingszeiten Kinder- / Jugendgruppen, Standard und Latein -| Kinder | Jahrgang | Tag | Zeit | Ort | -|----------------------------------------|-----------|------------|---------------|------------| -| Kindertanz 0 | 2018 - 20 | Freitag | 14:30 - 15:30 | VH Hinten | -| Kindertanz 1 | 2016 - 18 | Freitag | 15:30 - 16:30 | VH Hinten | -| Kindertanz 2 Turnieraufbau Kinder | | Freitag | 16:30 - 19:00 | VH Hinten | -| Kindertanz 8 Turnieraufb. Latein Ki/Ju | | Donnerstag | 18:00 - 19:30 | VH Hinten | +{{< tsc/calendar/table category="Kinder" showAge="true" >}} Weitere spannende Angebote für Kinder und Teens findet ihr bei unseren Dance-Stylern. diff --git a/content/page/angebote/tanzkreise/index.md b/content/page/angebote/tanzkreise/index.md index b826b95..e9ccea3 100644 --- a/content/page/angebote/tanzkreise/index.md +++ b/content/page/angebote/tanzkreise/index.md @@ -17,16 +17,6 @@ Interessiert? Na, dann schauen Sie doch einfach einmal vorbei! ## Trainingszeiten Tanzkreise -| Tanzkreise | Tag | Zeit | Ort | -|-------------|-------------|---------------|----------| -| Tanzkreis 0 | Freitag | 19:00 - 20:30 | VH Vorn | -| Tanzkreis 1 | Montag | 20:00 - 21:30 | VH Mitte | -| Tanzkreis 2 | Dienstag | 20:00 - 22:00 | VH Mitte | -| Tanzkreis 3 | Mittwoch | 19:00 - 20:30 | VH Mitte | -| Tanzkreis 4 | Mittwoch | 20:30 - 22:00 | VH Mitte | -| Tanzkreis 5 | Donnerstag | 19:30 - 21:00 | VH Mitte | -| Tanzkreis 6 Einsteiger | Freitag | 18:30 - 19:00 | VH Mitte | -| Tanzkreis 6 | Freitag | 19:00 - 20:30 | VH Mitte | -| Tanzkreis 9 | Freitag | 20:30 - 22:00 | VH Mitte | +{{< tsc/calendar/table category="Tanzkreise" >}} {{% tsc/link-offers %}} diff --git a/content/page/angebote/turniersport/_index.md b/content/page/angebote/turniersport/_index.md index 875fa23..037dd02 100644 --- a/content/page/angebote/turniersport/_index.md +++ b/content/page/angebote/turniersport/_index.md @@ -22,16 +22,9 @@ Darüber hinaus können unsere Tänzer zu fast jeder Zeit noch frei trainieren - [unsere Turnierpaare]({{< relref "paare" >}}) ## Trainingszeiten Turniertanz -| Turniertanz | Tag | Zeit | Ort | -|---------------------------------------------|---------|-----------------|---------| -| Turnieraufbau Latein | Di | 19:00 - 20:00 | VH Neu | -| Turnier Standard | Di | 20:00 - 22:00 | VH Neu | -| Turnieraufbau Standard | Mi | 20:30 - 22:00 | VH Vorn | -| Turnieraufbau Kinder Jugend inkl. Basic | Mi | 16:30 - 18:00 | VH Neu | -| Turnieraufbau Kinder Jugend inkl. Basic | Do | 18:00 - 19:30 | VH Neu | -| Turnieraufbau Kinder Jugend inkl. Basic | Fr | 16:00 - 17:00 | VH Neu | -  +{{< tsc/calendar/table category="Turnier" >}} + {{% tsc/link-offers %}} ## Sportverwaltung und weitere Links diff --git a/content/page/info/raumbelegung/index.md b/content/page/info/raumbelegung/_index.md similarity index 96% rename from content/page/info/raumbelegung/index.md rename to content/page/info/raumbelegung/_index.md index 9073d85..ca916be 100644 --- a/content/page/info/raumbelegung/index.md +++ b/content/page/info/raumbelegung/_index.md @@ -11,6 +11,7 @@ aliases: --- Hier finden Sie die Übersicht zur aktuellen Belegung unserer Tanzsäle. + ## Raumbelegung - außerplanmäßig Außerhalb der Trainings- und Kurszeiten können die Räume für spezielle (Gruppen-) Trainings gebucht werden. @@ -26,8 +27,9 @@ Weitere Infos zur Reservierung findest du unterhalb der Kalender. ## Raumbelegung - planmäßig -{{< tsc/show-calendar 10 22 "Mo" "Di" "Mi" >}} -{{< tsc/show-calendar 10 22 "Do" "Fr" >}} +{{< tsc/calendar/schedule 10 22 "Mon" "Tue" "Wed" >}} + +{{< tsc/calendar/schedule 14 22 "Thu" "Fri" >}} ## Regeln für die Belegung der Tanzsäle diff --git a/content/page/info/raumbelegung/liste.md b/content/page/info/raumbelegung/liste.md new file mode 100644 index 0000000..6de9491 --- /dev/null +++ b/content/page/info/raumbelegung/liste.md @@ -0,0 +1,13 @@ +--- +title: Raumbelegungs Liste +date: 2025-01-20T09:02:00 +draft: false +build: + list: local + # render: never +--- + +Auf dieser Seite werden die Angebote des Vereins noch einmal zusammen gefasst. +Dies dient der internen Kontrolle im Verein. + +{{< tsc/calendar/list >}} diff --git a/data/calendar.yaml b/data/calendar.yaml deleted file mode 100644 index 946656d..0000000 --- a/data/calendar.yaml +++ /dev/null @@ -1,145 +0,0 @@ -calendar: - vorne: - Mo: - "17:00": - title: DS-Kids 6 - slots: 4 - "18:15": - title: DS Adults 5 - slots: 4 - "19:15": - title: DS Contest Gruppe - slots: 6 - Di: - "16:30": - title: DS Kids 4 - slots: 4 - "17:45": - title: DS Kids 0 - slots: 4 - "18:45": - title: DS Adults 1 - slots: 6 - Mi: - "17:00": - title: DS Kids 2 - slots: 4 - "18:00": - title: DS Teens 0 - slots: 4 - "20:30": - title: Turnieraufbau Std - slots: 6 - Do: - "17:00": - title: DS Kids 5 - slots: 4 - "18:30": - title: DS Kids 1 - slots: 4 - "19:30": - title: DS Adults 3 - slots: 4 - Fr: - "16:30": - title: DS Minis 0 - slots: 4 - "19:00": - title: Tanzkreis 0 - slots: 6 - "20:30": - title: Discofox - slots: 4 - # Sa: {} - # So: {} - mitte: - Mo: - "17:30": - title: DS Teens 2 - slots: 4 - "18:30": - title: Ballet - slots: 6 - "20:00": - title: Tanzkreis 1 - slots: 6 - Di: - "20:00": - title: Tanzkreis 2 - slots: 8 - Mi: - "18:00": - title: DS Kids 7 - slots: 4 - "19:00": - title: Tanzkreis 3 - slots: 6 - "20:30": - title: Tanzkreis 4 - slots: 6 - Do: - "16:45": - title: DS Minis 1 - slots: 4 - "19:30": - title: Tanzkreis 5 - slots: 6 - Fr: - "18:30": - title: Tanzkreis 6 - Einsteiger - slots: 2 - "19:00": - title: Tanzkreis 6 - slots: 6 - "20:30": - title: Tanzkreis 9 - slots: 6 - Sa: {} - So: {} - hinten: - Mo: - "16:00": - title: Ballet - slots: 6 - # "18:15": - # title: DS Adults 5 - # slots: 4 - Di: - "19:00": - title: Turnier Latein - slots: 4 - "20:00": - title: Turnier Standard - slots: 8 - Mi: - "10:30": - title: Ballet - slots: 6 - "16:45": - title: DS Kids 3 - slots: 4 - "18:00": - title: DS Teens 1 - slots: 4 - "19:00": - title: DS Adults 0 - slots: 4 - "20:00": - title: Breitensport - slots: 6 - Do: - "18:00": - title: Kindertanz 8 (Turnieraufbau) - slots: 6 - Fr: - "14:30": - title: Kindertanz 0 - slots: 4 - "15:30": - title: Kindertanz 1 - slots: 4 - "16:30": - title: Kindertanz 2 (Turnieraufbau) - slots: 10 - Sa: {} - So: {} diff --git a/data/days.yml b/data/days.yml new file mode 100644 index 0000000..ffb9f0b --- /dev/null +++ b/data/days.yml @@ -0,0 +1,7 @@ +Mon: Montag +Tue: Dienstag +Wed: Mittwoch +Thu: Donnerstag +Fri: Freitag +Sat: Samstag +Sun: Sonntag diff --git a/data/holidays.yaml b/data/holidays.yaml new file mode 100644 index 0000000..e0acd04 --- /dev/null +++ b/data/holidays.yaml @@ -0,0 +1,27 @@ +holidays: +- from: 2025-01-01 + to: 2025-01-05 +- from: 2025-04-14 + to: 2025-04-27 +- from: 2025-06-09 + to: 2025-06-22 +- from: 2025-07-31 + to: 2025-09-14 +- from: 2025-10-27 + to: 2025-11-02 +- from: 2025-12-22 + to: 2026-01-05 +feasts: +- 2025-01-01 +- 2025-01-06 +- 2025-04-18 +- 2025-04-21 +- 2025-05-01 +- 2025-05-29 +- 2025-06-09 +- 2025-06-19 +- 2025-10-03 +- 2025-11-01 +- 2025-12-25 +- 2025-12-26 + diff --git a/data/schedule.yaml b/data/schedule.yaml new file mode 100644 index 0000000..fc41a07 --- /dev/null +++ b/data/schedule.yaml @@ -0,0 +1,261 @@ +calendars: + vorne: + id: "vh-vorne-regeltermine_shared_by_tsc" + ignore: false + name: VH Vorn + schedule: + - title: DS Kids 6 + day: Mon + start: '17:00' + age: '2017 - 2019' + duration: 60 + class: DSKids + - title: DS Adults 5 + day: Mon + start: '18:15' + age: 'ab 18' + duration: 60 + class: DSAdults + - title: DS Contest Gruppe + day: Mon + start: '19:15' + age: 'auf Anfrage' + duration: 90 + class: DSAdults + + - title: DS Kids 4 + day: Tue + start: '16:30' + age: '2015 - 2017' + duration: 60 + class: DSKids + - title: DS Kids 0 + day: Tue + start: '17:45' + age: '2011 - 2013' + duration: 60 + class: DSKids + - title: DS Adults 1 + day: Tue + start: '18:45' + age: 'ab 18' + duration: 90 + class: DSAdults + + - title: DS Kids 2 + day: Wed + start: '17:00' + age: '2010 - 2012' + duration: 60 + class: DSKids + - title: DS Teens 0 + day: Wed + start: '18:00' + age: '2008 - 2010' + duration: 60 + class: DSTeens + - title: Turnieraufbau Std + weight: -1 + day: Wed + start: '20:30' + duration: 90 + class: Turnier + + - title: DS Kids 5 + day: Thu + start: '17:00' + age: '2010 - 2013' + duration: 60 + class: DSKids + - title: DS Kids 1 + day: Thu + start: '18:30' + age: '2012 - 2014' + duration: 60 + class: DSKids + - title: DS Adults 3 + day: Thu + start: '19:30' + age: 'ab 18' + duration: 60 + class: DSAdults + + - title: DS Minis 0 + weight: -1 + day: Fri + start: '16:30' + age: '2019 - 2020' + duration: 60 + class: DSKids + - title: Tanzkreis 0 + day: Fri + start: '19:00' + duration: 90 + class: Tanzkreise + - title: Discofox + day: Fri + start: '20:30' + duration: 60 + class: Discofox + + mitte: + id: "vh-mitte-regeltermine_shared_by_tsc" + name: VH Mitte + ignore: false + schedule: + - title: DS Teens 2 + day: Mon + start: '17:30' + age: '2006 - 2009' + duration: 60 + class: DSTeens + - title: Ballet + day: Mon + start: '18:30' + age: '' + extern: true + duration: 90 + # class: DSKids + - title: Tanzkreis 1 + day: Mon + start: '20:00' + duration: 90 + class: Tanzkreise + + - title: Tanzkreis 2 + day: Tue + start: '20:00' + duration: 120 + class: Tanzkreise + + - title: DS Kids 7 + day: Wed + start: '18:00' + age: '2012 - 2015' + duration: 60 + class: DSKids + - title: Tanzkreis 3 + day: Wed + start: '19:00' + duration: 90 + class: Tanzkreise + - title: Tanzkreis 4 + day: Wed + start: '20:30' + duration: 90 + class: Tanzkreise + + - title: DS Minis 1 + weight: -1 + day: Thu + start: '16:45' + age: '2020 - 2021' + duration: 60 + class: DSKids + - title: Tanzkreis 5 + day: Thu + start: '19:30' + duration: 90 + class: Tanzkreise + + - title: Tanzkreis 6 + subtitle: Einsteiger + day: Fri + start: '18:30' + duration: 30 + class: Tanzkreise + - title: Tanzkreis 6 + day: Fri + start: '19:00' + duration: 90 + class: Tanzkreise + - title: Tanzkreis 9 + day: Fri + start: '20:30' + duration: 90 + class: Tanzkreise + + hinten: + id: "vh-hinten-regeltermine_shared_by_tsc" + name: VH Hinten + ignore: false + schedule: + - title: Ballet + day: Mon + start: '16:00' + age: '' + extern: true + duration: 90 + # class: DSKids + + - title: Turnier Latein + weight: -1 + day: Tue + start: '19:00' + duration: 60 + class: Turnier + - title: Turnier Standard + weight: -1 + day: Tue + start: '20:00' + duration: 120 + class: Turnier + + - title: Ballet + day: Wed + start: '10:30' + age: '' + extern: true + duration: 90 + # class: DSKids + - title: DS Kids 3 + day: Wed + start: '16:45' + age: '2014 - 2016' + duration: 60 + class: DSKids + - title: DS Teens 1 + day: Wed + start: '18:00' + age: '2004 - 2007' + duration: 60 + class: DSTeens + - title: DS Adults 0 + day: Wed + start: '19:00' + age: 'ab 18' + duration: 60 + class: DSAdults + - title: Breitensport + subtitle: Standard + day: Wed + start: '20:00' + duration: 90 + class: Breitensport + + - title: Kindertanz 8 + subtitle: Turnieraufbau + day: Thu + start: '18:00' + duration: 90 + class: [Kinder, Turnier] + + - title: Kindertanz 0 + day: Fri + start: '14:30' + age: '2018 - 2020' + duration: 60 + class: Kinder + - title: Kindertanz 1 + day: Fri + start: '15:30' + age: '2016 - 2018' + duration: 60 + class: Kinder + - title: Kindertanz 2 + subtitle: Turnieraufbau + day: Fri + start: '16:30' + age: '' + duration: 150 + class: [Kinder, Turnier] diff --git a/hugo.yaml b/hugo.yaml index 8a0bc62..0344833 100644 --- a/hugo.yaml +++ b/hugo.yaml @@ -1,5 +1,6 @@ baseURL: https://tanzsportclub.vfl-sindelfingen.de/ languageCode: de +defaultContentLanguage: de title: TSC im VfL Sindelfingen e.V. theme: tsc_vfl relativeUrls: true diff --git a/scripts/nc-cal-sync/.gitignore b/scripts/nc-cal-sync/.gitignore new file mode 100644 index 0000000..0572a39 --- /dev/null +++ b/scripts/nc-cal-sync/.gitignore @@ -0,0 +1,4 @@ +/venv/ +__pycache__/ + +/login.json diff --git a/scripts/nc-cal-sync/Pipfile b/scripts/nc-cal-sync/Pipfile new file mode 100644 index 0000000..9d9558a --- /dev/null +++ b/scripts/nc-cal-sync/Pipfile @@ -0,0 +1,14 @@ +[[source]] +url = "https://pypi.org/simple" +verify_ssl = true +name = "pypi" + +[packages] +caldav = "*" +requests = "*" +pyyaml = "*" + +[dev-packages] + +[requires] +python_version = "3.12" diff --git a/scripts/nc-cal-sync/Pipfile.lock b/scripts/nc-cal-sync/Pipfile.lock new file mode 100644 index 0000000..502cc54 --- /dev/null +++ b/scripts/nc-cal-sync/Pipfile.lock @@ -0,0 +1,433 @@ +{ + "_meta": { + "hash": { + "sha256": "0976c7c1fc6571d991ea2c8a387c220b8bb9ecb2c86fd86a69214de581ee3c76" + }, + "pipfile-spec": 6, + "requires": { + "python_version": "3.12" + }, + "sources": [ + { + "name": "pypi", + "url": "https://pypi.org/simple", + "verify_ssl": true + } + ] + }, + "default": { + "caldav": { + "hashes": [ + "sha256:4317131127d8793f740cff2fd256f369321fa49ad750f83d6f31780f7c16c67b", + "sha256:e75e84824092e33a9e03ac693de3d01133a3e044fd50a1c542c7f78d1aff0cb2" + ], + "index": "pypi", + "version": "==1.4.0" + }, + "certifi": { + "hashes": [ + "sha256:1275f7a45be9464efc1173084eaa30f866fe2e47d389406136d332ed4967ec56", + "sha256:b650d30f370c2b724812bee08008be0c4163b163ddaec3f2546c1caf65f191db" + ], + "markers": "python_version >= '3.6'", + "version": "==2024.12.14" + }, + "charset-normalizer": { + "hashes": [ + "sha256:0167ddc8ab6508fe81860a57dd472b2ef4060e8d378f0cc555707126830f2537", + "sha256:01732659ba9b5b873fc117534143e4feefecf3b2078b0a6a2e925271bb6f4cfa", + "sha256:01ad647cdd609225c5350561d084b42ddf732f4eeefe6e678765636791e78b9a", + "sha256:04432ad9479fa40ec0f387795ddad4437a2b50417c69fa275e212933519ff294", + "sha256:0907f11d019260cdc3f94fbdb23ff9125f6b5d1039b76003b5b0ac9d6a6c9d5b", + "sha256:0924e81d3d5e70f8126529951dac65c1010cdf117bb75eb02dd12339b57749dd", + "sha256:09b26ae6b1abf0d27570633b2b078a2a20419c99d66fb2823173d73f188ce601", + "sha256:09b5e6733cbd160dcc09589227187e242a30a49ca5cefa5a7edd3f9d19ed53fd", + "sha256:0af291f4fe114be0280cdd29d533696a77b5b49cfde5467176ecab32353395c4", + "sha256:0f55e69f030f7163dffe9fd0752b32f070566451afe180f99dbeeb81f511ad8d", + "sha256:1a2bc9f351a75ef49d664206d51f8e5ede9da246602dc2d2726837620ea034b2", + "sha256:22e14b5d70560b8dd51ec22863f370d1e595ac3d024cb8ad7d308b4cd95f8313", + "sha256:234ac59ea147c59ee4da87a0c0f098e9c8d169f4dc2a159ef720f1a61bbe27cd", + "sha256:2369eea1ee4a7610a860d88f268eb39b95cb588acd7235e02fd5a5601773d4fa", + "sha256:237bdbe6159cff53b4f24f397d43c6336c6b0b42affbe857970cefbb620911c8", + "sha256:28bf57629c75e810b6ae989f03c0828d64d6b26a5e205535585f96093e405ed1", + "sha256:2967f74ad52c3b98de4c3b32e1a44e32975e008a9cd2a8cc8966d6a5218c5cb2", + "sha256:2a75d49014d118e4198bcee5ee0a6f25856b29b12dbf7cd012791f8a6cc5c496", + "sha256:2bdfe3ac2e1bbe5b59a1a63721eb3b95fc9b6817ae4a46debbb4e11f6232428d", + "sha256:2d074908e1aecee37a7635990b2c6d504cd4766c7bc9fc86d63f9c09af3fa11b", + "sha256:2fb9bd477fdea8684f78791a6de97a953c51831ee2981f8e4f583ff3b9d9687e", + "sha256:311f30128d7d333eebd7896965bfcfbd0065f1716ec92bd5638d7748eb6f936a", + "sha256:329ce159e82018d646c7ac45b01a430369d526569ec08516081727a20e9e4af4", + "sha256:345b0426edd4e18138d6528aed636de7a9ed169b4aaf9d61a8c19e39d26838ca", + "sha256:363e2f92b0f0174b2f8238240a1a30142e3db7b957a5dd5689b0e75fb717cc78", + "sha256:3a3bd0dcd373514dcec91c411ddb9632c0d7d92aed7093b8c3bbb6d69ca74408", + "sha256:3bed14e9c89dcb10e8f3a29f9ccac4955aebe93c71ae803af79265c9ca5644c5", + "sha256:44251f18cd68a75b56585dd00dae26183e102cd5e0f9f1466e6df5da2ed64ea3", + "sha256:44ecbf16649486d4aebafeaa7ec4c9fed8b88101f4dd612dcaf65d5e815f837f", + "sha256:4532bff1b8421fd0a320463030c7520f56a79c9024a4e88f01c537316019005a", + "sha256:49402233c892a461407c512a19435d1ce275543138294f7ef013f0b63d5d3765", + "sha256:4c0907b1928a36d5a998d72d64d8eaa7244989f7aaaf947500d3a800c83a3fd6", + "sha256:4d86f7aff21ee58f26dcf5ae81a9addbd914115cdebcbb2217e4f0ed8982e146", + "sha256:5777ee0881f9499ed0f71cc82cf873d9a0ca8af166dfa0af8ec4e675b7df48e6", + "sha256:5df196eb874dae23dcfb968c83d4f8fdccb333330fe1fc278ac5ceeb101003a9", + "sha256:619a609aa74ae43d90ed2e89bdd784765de0a25ca761b93e196d938b8fd1dbbd", + "sha256:6e27f48bcd0957c6d4cb9d6fa6b61d192d0b13d5ef563e5f2ae35feafc0d179c", + "sha256:6ff8a4a60c227ad87030d76e99cd1698345d4491638dfa6673027c48b3cd395f", + "sha256:73d94b58ec7fecbc7366247d3b0b10a21681004153238750bb67bd9012414545", + "sha256:7461baadb4dc00fd9e0acbe254e3d7d2112e7f92ced2adc96e54ef6501c5f176", + "sha256:75832c08354f595c760a804588b9357d34ec00ba1c940c15e31e96d902093770", + "sha256:7709f51f5f7c853f0fb938bcd3bc59cdfdc5203635ffd18bf354f6967ea0f824", + "sha256:78baa6d91634dfb69ec52a463534bc0df05dbd546209b79a3880a34487f4b84f", + "sha256:7974a0b5ecd505609e3b19742b60cee7aa2aa2fb3151bc917e6e2646d7667dcf", + "sha256:7a4f97a081603d2050bfaffdefa5b02a9ec823f8348a572e39032caa8404a487", + "sha256:7b1bef6280950ee6c177b326508f86cad7ad4dff12454483b51d8b7d673a2c5d", + "sha256:7d053096f67cd1241601111b698f5cad775f97ab25d81567d3f59219b5f1adbd", + "sha256:804a4d582ba6e5b747c625bf1255e6b1507465494a40a2130978bda7b932c90b", + "sha256:807f52c1f798eef6cf26beb819eeb8819b1622ddfeef9d0977a8502d4db6d534", + "sha256:80ed5e856eb7f30115aaf94e4a08114ccc8813e6ed1b5efa74f9f82e8509858f", + "sha256:8417cb1f36cc0bc7eaba8ccb0e04d55f0ee52df06df3ad55259b9a323555fc8b", + "sha256:8436c508b408b82d87dc5f62496973a1805cd46727c34440b0d29d8a2f50a6c9", + "sha256:89149166622f4db9b4b6a449256291dc87a99ee53151c74cbd82a53c8c2f6ccd", + "sha256:8bfa33f4f2672964266e940dd22a195989ba31669bd84629f05fab3ef4e2d125", + "sha256:8c60ca7339acd497a55b0ea5d506b2a2612afb2826560416f6894e8b5770d4a9", + "sha256:91b36a978b5ae0ee86c394f5a54d6ef44db1de0815eb43de826d41d21e4af3de", + "sha256:955f8851919303c92343d2f66165294848d57e9bba6cf6e3625485a70a038d11", + "sha256:97f68b8d6831127e4787ad15e6757232e14e12060bec17091b85eb1486b91d8d", + "sha256:9b23ca7ef998bc739bf6ffc077c2116917eabcc901f88da1b9856b210ef63f35", + "sha256:9f0b8b1c6d84c8034a44893aba5e767bf9c7a211e313a9605d9c617d7083829f", + "sha256:aabfa34badd18f1da5ec1bc2715cadc8dca465868a4e73a0173466b688f29dda", + "sha256:ab36c8eb7e454e34e60eb55ca5d241a5d18b2c6244f6827a30e451c42410b5f7", + "sha256:b010a7a4fd316c3c484d482922d13044979e78d1861f0e0650423144c616a46a", + "sha256:b1ac5992a838106edb89654e0aebfc24f5848ae2547d22c2c3f66454daa11971", + "sha256:b7b2d86dd06bfc2ade3312a83a5c364c7ec2e3498f8734282c6c3d4b07b346b8", + "sha256:b97e690a2118911e39b4042088092771b4ae3fc3aa86518f84b8cf6888dbdb41", + "sha256:bc2722592d8998c870fa4e290c2eec2c1569b87fe58618e67d38b4665dfa680d", + "sha256:c0429126cf75e16c4f0ad00ee0eae4242dc652290f940152ca8c75c3a4b6ee8f", + "sha256:c30197aa96e8eed02200a83fba2657b4c3acd0f0aa4bdc9f6c1af8e8962e0757", + "sha256:c4c3e6da02df6fa1410a7680bd3f63d4f710232d3139089536310d027950696a", + "sha256:c75cb2a3e389853835e84a2d8fb2b81a10645b503eca9bcb98df6b5a43eb8886", + "sha256:c96836c97b1238e9c9e3fe90844c947d5afbf4f4c92762679acfe19927d81d77", + "sha256:d7f50a1f8c450f3925cb367d011448c39239bb3eb4117c36a6d354794de4ce76", + "sha256:d973f03c0cb71c5ed99037b870f2be986c3c05e63622c017ea9816881d2dd247", + "sha256:d98b1668f06378c6dbefec3b92299716b931cd4e6061f3c875a71ced1780ab85", + "sha256:d9c3cdf5390dcd29aa8056d13e8e99526cda0305acc038b96b30352aff5ff2bb", + "sha256:dad3e487649f498dd991eeb901125411559b22e8d7ab25d3aeb1af367df5efd7", + "sha256:dccbe65bd2f7f7ec22c4ff99ed56faa1e9f785482b9bbd7c717e26fd723a1d1e", + "sha256:dd78cfcda14a1ef52584dbb008f7ac81c1328c0f58184bf9a84c49c605002da6", + "sha256:e218488cd232553829be0664c2292d3af2eeeb94b32bea483cf79ac6a694e037", + "sha256:e358e64305fe12299a08e08978f51fc21fac060dcfcddd95453eabe5b93ed0e1", + "sha256:ea0d8d539afa5eb2728aa1932a988a9a7af94f18582ffae4bc10b3fbdad0626e", + "sha256:eab677309cdb30d047996b36d34caeda1dc91149e4fdca0b1a039b3f79d9a807", + "sha256:eb8178fe3dba6450a3e024e95ac49ed3400e506fd4e9e5c32d30adda88cbd407", + "sha256:ecddf25bee22fe4fe3737a399d0d177d72bc22be6913acfab364b40bce1ba83c", + "sha256:eea6ee1db730b3483adf394ea72f808b6e18cf3cb6454b4d86e04fa8c4327a12", + "sha256:f08ff5e948271dc7e18a35641d2f11a4cd8dfd5634f55228b691e62b37125eb3", + "sha256:f30bf9fd9be89ecb2360c7d94a711f00c09b976258846efe40db3d05828e8089", + "sha256:fa88b843d6e211393a37219e6a1c1df99d35e8fd90446f1118f4216e307e48cd", + "sha256:fc54db6c8593ef7d4b2a331b58653356cf04f67c960f584edb7c3d8c97e8f39e", + "sha256:fd4ec41f914fa74ad1b8304bbc634b3de73d2a0889bd32076342a573e0779e00", + "sha256:ffc9202a29ab3920fa812879e95a9e78b2465fd10be7fcbd042899695d75e616" + ], + "markers": "python_version >= '3.7'", + "version": "==3.4.1" + }, + "click": { + "hashes": [ + "sha256:63c132bbbed01578a06712a2d1f497bb62d9c1c0d329b7903a866228027263b2", + "sha256:ed53c9d8990d83c2a27deae68e4ee337473f6330c040a31d4225c9574d16096a" + ], + "markers": "python_version >= '3.7'", + "version": "==8.1.8" + }, + "icalendar": { + "hashes": [ + "sha256:43c2db8632959d634f4e48f6e6131e706bf2cdddad488cf0b72fda079b796bad", + "sha256:46c09b774a6e6948495dafcb166dc15135c8259d0ae25491f154cbc822714b69" + ], + "markers": "python_version >= '3.8'", + "version": "==6.1.0" + }, + "idna": { + "hashes": [ + "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9", + "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3" + ], + "markers": "python_version >= '3.6'", + "version": "==3.10" + }, + "lxml": { + "hashes": [ + "sha256:01220dca0d066d1349bd6a1726856a78f7929f3878f7e2ee83c296c69495309e", + "sha256:02ced472497b8362c8e902ade23e3300479f4f43e45f4105c85ef43b8db85229", + "sha256:052d99051e77a4f3e8482c65014cf6372e61b0a6f4fe9edb98503bb5364cfee3", + "sha256:07da23d7ee08577760f0a71d67a861019103e4812c87e2fab26b039054594cc5", + "sha256:094cb601ba9f55296774c2d57ad68730daa0b13dc260e1f941b4d13678239e70", + "sha256:0a7056921edbdd7560746f4221dca89bb7a3fe457d3d74267995253f46343f15", + "sha256:0c120f43553ec759f8de1fee2f4794452b0946773299d44c36bfe18e83caf002", + "sha256:0d7b36afa46c97875303a94e8f3ad932bf78bace9e18e603f2085b652422edcd", + "sha256:0fdf3a3059611f7585a78ee10399a15566356116a4288380921a4b598d807a22", + "sha256:109fa6fede314cc50eed29e6e56c540075e63d922455346f11e4d7a036d2b8cf", + "sha256:146173654d79eb1fc97498b4280c1d3e1e5d58c398fa530905c9ea50ea849b22", + "sha256:1473427aff3d66a3fa2199004c3e601e6c4500ab86696edffdbc84954c72d832", + "sha256:1483fd3358963cc5c1c9b122c80606a3a79ee0875bcac0204149fa09d6ff2727", + "sha256:168f2dfcfdedf611eb285efac1516c8454c8c99caf271dccda8943576b67552e", + "sha256:17e8d968d04a37c50ad9c456a286b525d78c4a1c15dd53aa46c1d8e06bf6fa30", + "sha256:18feb4b93302091b1541221196a2155aa296c363fd233814fa11e181adebc52f", + "sha256:1afe0a8c353746e610bd9031a630a95bcfb1a720684c3f2b36c4710a0a96528f", + "sha256:1d04f064bebdfef9240478f7a779e8c5dc32b8b7b0b2fc6a62e39b928d428e51", + "sha256:1fdc9fae8dd4c763e8a31e7630afef517eab9f5d5d31a278df087f307bf601f4", + "sha256:1ffc23010330c2ab67fac02781df60998ca8fe759e8efde6f8b756a20599c5de", + "sha256:20094fc3f21ea0a8669dc4c61ed7fa8263bd37d97d93b90f28fc613371e7a875", + "sha256:213261f168c5e1d9b7535a67e68b1f59f92398dd17a56d934550837143f79c42", + "sha256:218c1b2e17a710e363855594230f44060e2025b05c80d1f0661258142b2add2e", + "sha256:23e0553b8055600b3bf4a00b255ec5c92e1e4aebf8c2c09334f8368e8bd174d6", + "sha256:25f1b69d41656b05885aa185f5fdf822cb01a586d1b32739633679699f220391", + "sha256:2b3778cb38212f52fac9fe913017deea2fdf4eb1a4f8e4cfc6b009a13a6d3fcc", + "sha256:2bc9fd5ca4729af796f9f59cd8ff160fe06a474da40aca03fcc79655ddee1a8b", + "sha256:2c226a06ecb8cdef28845ae976da407917542c5e6e75dcac7cc33eb04aaeb237", + "sha256:2c3406b63232fc7e9b8783ab0b765d7c59e7c59ff96759d8ef9632fca27c7ee4", + "sha256:2c86bf781b12ba417f64f3422cfc302523ac9cd1d8ae8c0f92a1c66e56ef2e86", + "sha256:2d9b8d9177afaef80c53c0a9e30fa252ff3036fb1c6494d427c066a4ce6a282f", + "sha256:2dec2d1130a9cda5b904696cec33b2cfb451304ba9081eeda7f90f724097300a", + "sha256:2dfab5fa6a28a0b60a20638dc48e6343c02ea9933e3279ccb132f555a62323d8", + "sha256:2ecdd78ab768f844c7a1d4a03595038c166b609f6395e25af9b0f3f26ae1230f", + "sha256:315f9542011b2c4e1d280e4a20ddcca1761993dda3afc7a73b01235f8641e903", + "sha256:36aef61a1678cb778097b4a6eeae96a69875d51d1e8f4d4b491ab3cfb54b5a03", + "sha256:384aacddf2e5813a36495233b64cb96b1949da72bef933918ba5c84e06af8f0e", + "sha256:3879cc6ce938ff4eb4900d901ed63555c778731a96365e53fadb36437a131a99", + "sha256:3c174dc350d3ec52deb77f2faf05c439331d6ed5e702fc247ccb4e6b62d884b7", + "sha256:3eb44520c4724c2e1a57c0af33a379eee41792595023f367ba3952a2d96c2aab", + "sha256:406246b96d552e0503e17a1006fd27edac678b3fcc9f1be71a2f94b4ff61528d", + "sha256:41ce1f1e2c7755abfc7e759dc34d7d05fd221723ff822947132dc934d122fe22", + "sha256:423b121f7e6fa514ba0c7918e56955a1d4470ed35faa03e3d9f0e3baa4c7e492", + "sha256:44264ecae91b30e5633013fb66f6ddd05c006d3e0e884f75ce0b4755b3e3847b", + "sha256:482c2f67761868f0108b1743098640fbb2a28a8e15bf3f47ada9fa59d9fe08c3", + "sha256:4b0c7a688944891086ba192e21c5229dea54382f4836a209ff8d0a660fac06be", + "sha256:4c1fefd7e3d00921c44dc9ca80a775af49698bbfd92ea84498e56acffd4c5469", + "sha256:4e109ca30d1edec1ac60cdbe341905dc3b8f55b16855e03a54aaf59e51ec8c6f", + "sha256:501d0d7e26b4d261fca8132854d845e4988097611ba2531408ec91cf3fd9d20a", + "sha256:516f491c834eb320d6c843156440fe7fc0d50b33e44387fcec5b02f0bc118a4c", + "sha256:51806cfe0279e06ed8500ce19479d757db42a30fd509940b1701be9c86a5ff9a", + "sha256:562e7494778a69086f0312ec9689f6b6ac1c6b65670ed7d0267e49f57ffa08c4", + "sha256:56b9861a71575f5795bde89256e7467ece3d339c9b43141dbdd54544566b3b94", + "sha256:5b8f5db71b28b8c404956ddf79575ea77aa8b1538e8b2ef9ec877945b3f46442", + "sha256:5c2fb570d7823c2bbaf8b419ba6e5662137f8166e364a8b2b91051a1fb40ab8b", + "sha256:5c54afdcbb0182d06836cc3d1be921e540be3ebdf8b8a51ee3ef987537455f84", + "sha256:5d6a6972b93c426ace71e0be9a6f4b2cfae9b1baed2eed2006076a746692288c", + "sha256:609251a0ca4770e5a8768ff902aa02bf636339c5a93f9349b48eb1f606f7f3e9", + "sha256:62d172f358f33a26d6b41b28c170c63886742f5b6772a42b59b4f0fa10526cb1", + "sha256:62f7fdb0d1ed2065451f086519865b4c90aa19aed51081979ecd05a21eb4d1be", + "sha256:658f2aa69d31e09699705949b5fc4719cbecbd4a97f9656a232e7d6c7be1a367", + "sha256:65ab5685d56914b9a2a34d67dd5488b83213d680b0c5d10b47f81da5a16b0b0e", + "sha256:68934b242c51eb02907c5b81d138cb977b2129a0a75a8f8b60b01cb8586c7b21", + "sha256:68b87753c784d6acb8a25b05cb526c3406913c9d988d51f80adecc2b0775d6aa", + "sha256:69959bd3167b993e6e710b99051265654133a98f20cec1d9b493b931942e9c16", + "sha256:6a7095eeec6f89111d03dabfe5883a1fd54da319c94e0fb104ee8f23616b572d", + "sha256:6b038cc86b285e4f9fea2ba5ee76e89f21ed1ea898e287dc277a25884f3a7dfe", + "sha256:6ba0d3dcac281aad8a0e5b14c7ed6f9fa89c8612b47939fc94f80b16e2e9bc83", + "sha256:6e91cf736959057f7aac7adfc83481e03615a8e8dd5758aa1d95ea69e8931dba", + "sha256:6ee8c39582d2652dcd516d1b879451500f8db3fe3607ce45d7c5957ab2596040", + "sha256:6f651ebd0b21ec65dfca93aa629610a0dbc13dbc13554f19b0113da2e61a4763", + "sha256:71a8dd38fbd2f2319136d4ae855a7078c69c9a38ae06e0c17c73fd70fc6caad8", + "sha256:74068c601baff6ff021c70f0935b0c7bc528baa8ea210c202e03757c68c5a4ff", + "sha256:7437237c6a66b7ca341e868cda48be24b8701862757426852c9b3186de1da8a2", + "sha256:747a3d3e98e24597981ca0be0fd922aebd471fa99d0043a3842d00cdcad7ad6a", + "sha256:74bcb423462233bc5d6066e4e98b0264e7c1bed7541fff2f4e34fe6b21563c8b", + "sha256:78d9b952e07aed35fe2e1a7ad26e929595412db48535921c5013edc8aa4a35ce", + "sha256:7b1cd427cb0d5f7393c31b7496419da594fe600e6fdc4b105a54f82405e6626c", + "sha256:7d3d1ca42870cdb6d0d29939630dbe48fa511c203724820fc0fd507b2fb46577", + "sha256:7e2f58095acc211eb9d8b5771bf04df9ff37d6b87618d1cbf85f92399c98dae8", + "sha256:7f41026c1d64043a36fda21d64c5026762d53a77043e73e94b71f0521939cc71", + "sha256:81b4e48da4c69313192d8c8d4311e5d818b8be1afe68ee20f6385d0e96fc9512", + "sha256:86a6b24b19eaebc448dc56b87c4865527855145d851f9fc3891673ff97950540", + "sha256:874a216bf6afaf97c263b56371434e47e2c652d215788396f60477540298218f", + "sha256:89e043f1d9d341c52bf2af6d02e6adde62e0a46e6755d5eb60dc6e4f0b8aeca2", + "sha256:8c72e9563347c7395910de6a3100a4840a75a6f60e05af5e58566868d5eb2d6a", + "sha256:8dc2c0395bea8254d8daebc76dcf8eb3a95ec2a46fa6fae5eaccee366bfe02ce", + "sha256:8f0de2d390af441fe8b2c12626d103540b5d850d585b18fcada58d972b74a74e", + "sha256:92e67a0be1639c251d21e35fe74df6bcc40cba445c2cda7c4a967656733249e2", + "sha256:94d6c3782907b5e40e21cadf94b13b0842ac421192f26b84c45f13f3c9d5dc27", + "sha256:97acf1e1fd66ab53dacd2c35b319d7e548380c2e9e8c54525c6e76d21b1ae3b1", + "sha256:9ada35dd21dc6c039259596b358caab6b13f4db4d4a7f8665764d616daf9cc1d", + "sha256:9c52100e2c2dbb0649b90467935c4b0de5528833c76a35ea1a2691ec9f1ee7a1", + "sha256:9e41506fec7a7f9405b14aa2d5c8abbb4dbbd09d88f9496958b6d00cb4d45330", + "sha256:9e4b47ac0f5e749cfc618efdf4726269441014ae1d5583e047b452a32e221920", + "sha256:9fb81d2824dff4f2e297a276297e9031f46d2682cafc484f49de182aa5e5df99", + "sha256:a0eabd0a81625049c5df745209dc7fcef6e2aea7793e5f003ba363610aa0a3ff", + "sha256:a3d819eb6f9b8677f57f9664265d0a10dd6551d227afb4af2b9cd7bdc2ccbf18", + "sha256:a87de7dd873bf9a792bf1e58b1c3887b9264036629a5bf2d2e6579fe8e73edff", + "sha256:aa617107a410245b8660028a7483b68e7914304a6d4882b5ff3d2d3eb5948d8c", + "sha256:aac0bbd3e8dd2d9c45ceb82249e8bdd3ac99131a32b4d35c8af3cc9db1657179", + "sha256:ab6dd83b970dc97c2d10bc71aa925b84788c7c05de30241b9e96f9b6d9ea3080", + "sha256:ace2c2326a319a0bb8a8b0e5b570c764962e95818de9f259ce814ee666603f19", + "sha256:ae5fe5c4b525aa82b8076c1a59d642c17b6e8739ecf852522c6321852178119d", + "sha256:b11a5d918a6216e521c715b02749240fb07ae5a1fefd4b7bf12f833bc8b4fe70", + "sha256:b1c8c20847b9f34e98080da785bb2336ea982e7f913eed5809e5a3c872900f32", + "sha256:b369d3db3c22ed14c75ccd5af429086f166a19627e84a8fdade3f8f31426e52a", + "sha256:b710bc2b8292966b23a6a0121f7a6c51d45d2347edcc75f016ac123b8054d3f2", + "sha256:bd96517ef76c8654446fc3db9242d019a1bb5fe8b751ba414765d59f99210b79", + "sha256:c00f323cc00576df6165cc9d21a4c21285fa6b9989c5c39830c3903dc4303ef3", + "sha256:c162b216070f280fa7da844531169be0baf9ccb17263cf5a8bf876fcd3117fa5", + "sha256:c1a69e58a6bb2de65902051d57fde951febad631a20a64572677a1052690482f", + "sha256:c1f794c02903c2824fccce5b20c339a1a14b114e83b306ff11b597c5f71a1c8d", + "sha256:c24037349665434f375645fa9d1f5304800cec574d0310f618490c871fd902b3", + "sha256:c300306673aa0f3ed5ed9372b21867690a17dba38c68c44b287437c362ce486b", + "sha256:c56a1d43b2f9ee4786e4658c7903f05da35b923fb53c11025712562d5cc02753", + "sha256:c6379f35350b655fd817cd0d6cbeef7f265f3ae5fedb1caae2eb442bbeae9ab9", + "sha256:c802e1c2ed9f0c06a65bc4ed0189d000ada8049312cfeab6ca635e39c9608957", + "sha256:cb83f8a875b3d9b458cada4f880fa498646874ba4011dc974e071a0a84a1b033", + "sha256:cf120cce539453ae086eacc0130a324e7026113510efa83ab42ef3fcfccac7fb", + "sha256:dd36439be765e2dde7660212b5275641edbc813e7b24668831a5c8ac91180656", + "sha256:dd5350b55f9fecddc51385463a4f67a5da829bc741e38cf689f38ec9023f54ab", + "sha256:df5c7333167b9674aa8ae1d4008fa4bc17a313cc490b2cca27838bbdcc6bb15b", + "sha256:e63601ad5cd8f860aa99d109889b5ac34de571c7ee902d6812d5d9ddcc77fa7d", + "sha256:e92ce66cd919d18d14b3856906a61d3f6b6a8500e0794142338da644260595cd", + "sha256:e99f5507401436fdcc85036a2e7dc2e28d962550afe1cbfc07c40e454256a859", + "sha256:ea2e2f6f801696ad7de8aec061044d6c8c0dd4037608c7cab38a9a4d316bfb11", + "sha256:eafa2c8658f4e560b098fe9fc54539f86528651f61849b22111a9b107d18910c", + "sha256:ecd4ad8453ac17bc7ba3868371bffb46f628161ad0eefbd0a855d2c8c32dd81a", + "sha256:ee70d08fd60c9565ba8190f41a46a54096afa0eeb8f76bd66f2c25d3b1b83005", + "sha256:eec1bb8cdbba2925bedc887bc0609a80e599c75b12d87ae42ac23fd199445654", + "sha256:ef0c1fe22171dd7c7c27147f2e9c3e86f8bdf473fed75f16b0c2e84a5030ce80", + "sha256:f2901429da1e645ce548bf9171784c0f74f0718c3f6150ce166be39e4dd66c3e", + "sha256:f422a209d2455c56849442ae42f25dbaaba1c6c3f501d58761c619c7836642ec", + "sha256:f65e5120863c2b266dbcc927b306c5b78e502c71edf3295dfcb9501ec96e5fc7", + "sha256:f7d4a670107d75dfe5ad080bed6c341d18c4442f9378c9f58e5851e86eb79965", + "sha256:f914c03e6a31deb632e2daa881fe198461f4d06e57ac3d0e05bbcab8eae01945", + "sha256:fb66442c2546446944437df74379e9cf9e9db353e61301d1a0e26482f43f0dd8" + ], + "markers": "python_version >= '3.6'", + "version": "==5.3.0" + }, + "python-dateutil": { + "hashes": [ + "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", + "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427" + ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2'", + "version": "==2.9.0.post0" + }, + "pytz": { + "hashes": [ + "sha256:2aa355083c50a0f93fa581709deac0c9ad65cca8a9e9beac660adcbd493c798a", + "sha256:31c7c1817eb7fae7ca4b8c7ee50c72f93aa2dd863de768e1ef4245d426aa0725" + ], + "version": "==2024.2" + }, + "pyyaml": { + "hashes": [ + "sha256:01179a4a8559ab5de078078f37e5c1a30d76bb88519906844fd7bdea1b7729ff", + "sha256:0833f8694549e586547b576dcfaba4a6b55b9e96098b36cdc7ebefe667dfed48", + "sha256:0a9a2848a5b7feac301353437eb7d5957887edbf81d56e903999a75a3d743086", + "sha256:0b69e4ce7a131fe56b7e4d770c67429700908fc0752af059838b1cfb41960e4e", + "sha256:0ffe8360bab4910ef1b9e87fb812d8bc0a308b0d0eef8c8f44e0254ab3b07133", + "sha256:11d8f3dd2b9c1207dcaf2ee0bbbfd5991f571186ec9cc78427ba5bd32afae4b5", + "sha256:17e311b6c678207928d649faa7cb0d7b4c26a0ba73d41e99c4fff6b6c3276484", + "sha256:1e2120ef853f59c7419231f3bf4e7021f1b936f6ebd222406c3b60212205d2ee", + "sha256:1f71ea527786de97d1a0cc0eacd1defc0985dcf6b3f17bb77dcfc8c34bec4dc5", + "sha256:23502f431948090f597378482b4812b0caae32c22213aecf3b55325e049a6c68", + "sha256:24471b829b3bf607e04e88d79542a9d48bb037c2267d7927a874e6c205ca7e9a", + "sha256:29717114e51c84ddfba879543fb232a6ed60086602313ca38cce623c1d62cfbf", + "sha256:2e99c6826ffa974fe6e27cdb5ed0021786b03fc98e5ee3c5bfe1fd5015f42b99", + "sha256:39693e1f8320ae4f43943590b49779ffb98acb81f788220ea932a6b6c51004d8", + "sha256:3ad2a3decf9aaba3d29c8f537ac4b243e36bef957511b4766cb0057d32b0be85", + "sha256:3b1fdb9dc17f5a7677423d508ab4f243a726dea51fa5e70992e59a7411c89d19", + "sha256:41e4e3953a79407c794916fa277a82531dd93aad34e29c2a514c2c0c5fe971cc", + "sha256:43fa96a3ca0d6b1812e01ced1044a003533c47f6ee8aca31724f78e93ccc089a", + "sha256:50187695423ffe49e2deacb8cd10510bc361faac997de9efef88badc3bb9e2d1", + "sha256:5ac9328ec4831237bec75defaf839f7d4564be1e6b25ac710bd1a96321cc8317", + "sha256:5d225db5a45f21e78dd9358e58a98702a0302f2659a3c6cd320564b75b86f47c", + "sha256:6395c297d42274772abc367baaa79683958044e5d3835486c16da75d2a694631", + "sha256:688ba32a1cffef67fd2e9398a2efebaea461578b0923624778664cc1c914db5d", + "sha256:68ccc6023a3400877818152ad9a1033e3db8625d899c72eacb5a668902e4d652", + "sha256:70b189594dbe54f75ab3a1acec5f1e3faa7e8cf2f1e08d9b561cb41b845f69d5", + "sha256:797b4f722ffa07cc8d62053e4cff1486fa6dc094105d13fea7b1de7d8bf71c9e", + "sha256:7c36280e6fb8385e520936c3cb3b8042851904eba0e58d277dca80a5cfed590b", + "sha256:7e7401d0de89a9a855c839bc697c079a4af81cf878373abd7dc625847d25cbd8", + "sha256:80bab7bfc629882493af4aa31a4cfa43a4c57c83813253626916b8c7ada83476", + "sha256:82d09873e40955485746739bcb8b4586983670466c23382c19cffecbf1fd8706", + "sha256:8388ee1976c416731879ac16da0aff3f63b286ffdd57cdeb95f3f2e085687563", + "sha256:8824b5a04a04a047e72eea5cec3bc266db09e35de6bdfe34c9436ac5ee27d237", + "sha256:8b9c7197f7cb2738065c481a0461e50ad02f18c78cd75775628afb4d7137fb3b", + "sha256:9056c1ecd25795207ad294bcf39f2db3d845767be0ea6e6a34d856f006006083", + "sha256:936d68689298c36b53b29f23c6dbb74de12b4ac12ca6cfe0e047bedceea56180", + "sha256:9b22676e8097e9e22e36d6b7bda33190d0d400f345f23d4065d48f4ca7ae0425", + "sha256:a4d3091415f010369ae4ed1fc6b79def9416358877534caf6a0fdd2146c87a3e", + "sha256:a8786accb172bd8afb8be14490a16625cbc387036876ab6ba70912730faf8e1f", + "sha256:a9f8c2e67970f13b16084e04f134610fd1d374bf477b17ec1599185cf611d725", + "sha256:bc2fa7c6b47d6bc618dd7fb02ef6fdedb1090ec036abab80d4681424b84c1183", + "sha256:c70c95198c015b85feafc136515252a261a84561b7b1d51e3384e0655ddf25ab", + "sha256:cc1c1159b3d456576af7a3e4d1ba7e6924cb39de8f67111c735f6fc832082774", + "sha256:ce826d6ef20b1bc864f0a68340c8b3287705cae2f8b4b1d932177dcc76721725", + "sha256:d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e", + "sha256:d7fded462629cfa4b685c5416b949ebad6cec74af5e2d42905d41e257e0869f5", + "sha256:d84a1718ee396f54f3a086ea0a66d8e552b2ab2017ef8b420e92edbc841c352d", + "sha256:d8e03406cac8513435335dbab54c0d385e4a49e4945d2909a581c83647ca0290", + "sha256:e10ce637b18caea04431ce14fabcf5c64a1c61ec9c56b071a4b7ca131ca52d44", + "sha256:ec031d5d2feb36d1d1a24380e4db6d43695f3748343d99434e6f5f9156aaa2ed", + "sha256:ef6107725bd54b262d6dedcc2af448a266975032bc85ef0172c5f059da6325b4", + "sha256:efdca5630322a10774e8e98e1af481aad470dd62c3170801852d752aa7a783ba", + "sha256:f753120cb8181e736c57ef7636e83f31b9c0d1722c516f7e86cf15b7aa57ff12", + "sha256:ff3824dc5261f50c9b0dfb3be22b4567a6f938ccce4587b38952d85fd9e9afe4" + ], + "index": "pypi", + "markers": "python_version >= '3.8'", + "version": "==6.0.2" + }, + "recurring-ical-events": { + "hashes": [ + "sha256:7ea75a560bf0526f7a8294dd93e324cc5379d09d520c48d0ccb4958e591dc2ff", + "sha256:a91c2de4350dc5da99ba3bad27232ea63f3ad2e2f22e9466718d770cb9a59317" + ], + "version": "==3.4.0" + }, + "requests": { + "hashes": [ + "sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760", + "sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6" + ], + "index": "pypi", + "markers": "python_version >= '3.8'", + "version": "==2.32.3" + }, + "six": { + "hashes": [ + "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", + "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81" + ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2'", + "version": "==1.17.0" + }, + "tzdata": { + "hashes": [ + "sha256:7d85cc416e9382e69095b7bdf4afd9e3880418a2413feec7069d533d6b4e31cc", + "sha256:a48093786cdcde33cad18c2555e8532f34422074448fbc874186f0abd79565cd" + ], + "markers": "python_version >= '2'", + "version": "==2024.2" + }, + "urllib3": { + "hashes": [ + "sha256:1cee9ad369867bfdbbb48b7dd50374c0967a0bb7710050facf0dd6911440e3df", + "sha256:f8c5449b3cf0861679ce7e0503c7b44b5ec981bec0d1d3795a07f1ba96f0204d" + ], + "markers": "python_version >= '3.9'", + "version": "==2.3.0" + }, + "vobject": { + "hashes": [ + "sha256:0fbdb982065cf4d1843a5d5950c88510041c6de026bda49c3502721de1c6ac3d", + "sha256:ac44e5d7e2079d84c1d52c50a615b9bec4b1ba958608c4c7fe40cbf33247b38e" + ], + "version": "==0.9.9" + }, + "x-wr-timezone": { + "hashes": [ + "sha256:1e475d2dedcd2128550cd88b4b5773a224e936e1c9f22ff50104622180455265", + "sha256:37f3927ddc32970c330af97773dc12bf8e0d2deb9ede5c3c676dc13ed62feb11" + ], + "markers": "python_version >= '3.9'", + "version": "==2.0.0" + } + }, + "develop": {} +} diff --git a/scripts/nc-cal-sync/calendar_synchronizer/__init__.py b/scripts/nc-cal-sync/calendar_synchronizer/__init__.py new file mode 100644 index 0000000..27c3191 --- /dev/null +++ b/scripts/nc-cal-sync/calendar_synchronizer/__init__.py @@ -0,0 +1,33 @@ +from . import cli + +from . import login, sync + +import logging + +_logMap = { + 0: logging.WARNING, + 1: logging.INFO, + 2: logging.DEBUG, + 3: 5 +} + +_runMap = { + 'login': login.run, + 'sync': sync.run, +} + +def main(): + args = cli.getArgs( + loginSpCb=login.buildSubparser, + syncSpCb=sync.buildSubparser + ) + logging.basicConfig() + _l = logging.getLogger(__name__) + _l.setLevel(_logMap.get(args.verbose, logging.DEBUG)) + + _l.debug('Parameters %s', args) + if args.mode in _runMap: + _runMap[args.mode](args) + else: + _l.error('Unknown mode %s', args.mode) + diff --git a/scripts/nc-cal-sync/calendar_synchronizer/__main__.py b/scripts/nc-cal-sync/calendar_synchronizer/__main__.py new file mode 100644 index 0000000..4b443c9 --- /dev/null +++ b/scripts/nc-cal-sync/calendar_synchronizer/__main__.py @@ -0,0 +1,3 @@ +import calendar_synchronizer + +calendar_synchronizer.main() diff --git a/scripts/nc-cal-sync/calendar_synchronizer/cal_helper.py b/scripts/nc-cal-sync/calendar_synchronizer/cal_helper.py new file mode 100644 index 0000000..7dfb865 --- /dev/null +++ b/scripts/nc-cal-sync/calendar_synchronizer/cal_helper.py @@ -0,0 +1,118 @@ +import datetime as dt +import re, logging +import zoneinfo + +from .sync import Event + +class IcalHelper: + + _WEEKDAY_MAP = { + 'Mon': 0, + 'Tue': 1, + 'Wed': 2, + 'Thu': 3, + 'Fri': 4, + 'Sat': 5, + 'Sun': 6 + } + + tzDE = zoneinfo.ZoneInfo('Europe/Berlin') + deltaWeek = dt.timedelta(weeks=1) + deltaDay = dt.timedelta(days=1) + firstDay = dt.date(year=dt.datetime.now().year, month=1, day=1) + + def __init__(self): + self._l = logging.getLogger(__name__) + self.reTime = re.compile(r'(\d{1,2}):(\d{2})') + + def getStart(self, event: Event, holidays): + firstDay = self._getFirstOccurence(event, holidays) + + if firstDay is None: + return None, None + return firstDay, dt.datetime.combine(firstDay, self._getTime(event.start), tzinfo=self.tzDE) + + def _getFirstWeekdayInYear(self, weekday): + candidate = self.firstDay + while candidate.weekday() != self._WEEKDAY_MAP[weekday]: + candidate += self.deltaDay + + self._l.log(5, 'First %s in year is %s', weekday, candidate) + + return candidate + + def _getSortedHolidays(self, holidays): + return sorted(holidays['holidays'], key=lambda h: h['from']) + + def _getSortedFeasts(self, holidays): + return sorted(holidays['feasts']) + + def _isFeast(self, candidate, feasts): + return candidate in feasts + + def _isHoliday(self, candidate, holidays): + for h in holidays: + if h['from'] <= candidate <= h['to']: + return True + + return False + + def _getLastHolidayException(self, holidays): + holidayList = list(self._getSortedHolidays(holidays)) + holidayList.reverse() + lastHoliday = holidayList[0] + lastHolidayDay = lastHoliday['to'] + + feastList = list(self._getSortedFeasts(holidays)) + feastList.reverse() + lastFeast = feastList[0] + lastFeastDay = lastFeast + + return max(lastHolidayDay, lastFeastDay) + + def _getFirstOccurence(self, event: Event, holidays): + firstWeekday = self._getFirstWeekdayInYear(event.day) + + + candidate = firstWeekday + while candidate.year == firstWeekday.year: + if self._isFeast(candidate, holidays['feasts']): + self._l.log(5, 'Found feast %s', candidate) + candidate += self.deltaWeek + continue + + if self._isHoliday(candidate, holidays['holidays']): + self._l.log(5, 'Found holiday %s', candidate) + candidate += self.deltaWeek + continue + + self._l.log(5, 'Found first occurence %s', candidate) + return candidate + + return None + + # firstWeekday.tzinfo = dt.timezone.tzname('Europe/Berlin') + + def _getTime(self, startStr): + match = self.reTime.match(startStr) + return dt.time(int(match.group(1)), int(match.group(2))) + + def getHolidayExceptions(self, event: Event, startDay, holidays): + lastHolidayDay = self._getLastHolidayException(holidays) + + pointer = startDay + exeptions = [] + while pointer <= lastHolidayDay: + ex = dt.datetime.combine(pointer, self._getTime(event.start), tzinfo=self.tzDE) + + if self._isFeast(pointer, holidays['feasts']): + exeptions.append(ex) + + if self._isHoliday(pointer, holidays['holidays']) and not event.external: + exeptions.append(ex) + + pointer += self.deltaWeek + + if len(exeptions) > 0: + return exeptions + return None diff --git a/scripts/nc-cal-sync/calendar_synchronizer/cli.py b/scripts/nc-cal-sync/calendar_synchronizer/cli.py new file mode 100644 index 0000000..27e1cba --- /dev/null +++ b/scripts/nc-cal-sync/calendar_synchronizer/cli.py @@ -0,0 +1,16 @@ +import argparse + +def getArgs(loginSpCb, syncSpCb): + parser = argparse.ArgumentParser() + parser.add_argument('-v', '--verbose', action='count', default=0, help='Increase the verbosity') + + subparsers = parser.add_subparsers(dest='mode') + + loginSubparser = subparsers.add_parser('login') + loginSpCb(loginSubparser) + + syncSubparser = subparsers.add_parser('sync') + syncSpCb(syncSubparser) + + # parser.add_argument("url", help="The URL to try and cache") + return parser.parse_args() diff --git a/scripts/nc-cal-sync/calendar_synchronizer/debug.py b/scripts/nc-cal-sync/calendar_synchronizer/debug.py new file mode 100644 index 0000000..2e34ccd --- /dev/null +++ b/scripts/nc-cal-sync/calendar_synchronizer/debug.py @@ -0,0 +1,10 @@ + +_listening = False + +def debugger(): + global _listening + if not _listening: + import debugpy + debugpy.listen(5678) + debugpy.wait_for_client() + _listening = True diff --git a/scripts/nc-cal-sync/calendar_synchronizer/login.py b/scripts/nc-cal-sync/calendar_synchronizer/login.py new file mode 100644 index 0000000..dfa07b4 --- /dev/null +++ b/scripts/nc-cal-sync/calendar_synchronizer/login.py @@ -0,0 +1,62 @@ +import os +import requests +import logging +import json + +def buildSubparser(subparser): + subparser.add_argument('--url', default='https://cloud.tsc-vfl.de') + +def run(args): + l = logging.getLogger(__name__) + l.debug('Login to %s', args.url) + + url = f'{args.url}/index.php/login/v2' + l.debug('Using login url %s', url) + + startRequest = requests.post(url) + startRequest.raise_for_status() + + data = startRequest.json() + + print('You need to login in the browser now to validate the login token.') + print('Please visit the following url:') + print(data['login']) + print() + print('After you have logged in, press enter to continue.') + + input() + + l.debug('Logging in') + intermediateToken = data['poll']['token'] + pollUrl = data['poll']['endpoint'] + + loginRequest = requests.post(pollUrl, data={'token': intermediateToken}) + loginRequest.raise_for_status() + + data = loginRequest.json() + + loginName = data['loginName'] + appPassword = data['appPassword'] + data = { + 'loginName': loginName, + 'appPassword': appPassword, + 'base': data['server'] + } + + with open('login.json', 'w') as f: + json.dump(data, f) + + l.info('Login successful') + +class LoginData: + def __init__(self, loginName, appPassword, base): + self.loginName = loginName + self.appPassword = appPassword + self.base = base + +def loadLoginData(): + with open('login.json', 'r') as f: + data = json.load(f) + + return LoginData(data['loginName'], data['appPassword'], data['base']) + \ No newline at end of file diff --git a/scripts/nc-cal-sync/calendar_synchronizer/sync.py b/scripts/nc-cal-sync/calendar_synchronizer/sync.py new file mode 100644 index 0000000..4e15b4a --- /dev/null +++ b/scripts/nc-cal-sync/calendar_synchronizer/sync.py @@ -0,0 +1,324 @@ +import logging, yaml, pprint, json, re +import hashlib, uuid +import caldav +import datetime as dt + +from . import login, debug + +_l = logging.getLogger(__name__) + +def buildSubparser(subparser): + subparser.add_argument('--schedule', default='../../data/schedule.yaml') + subparser.add_argument('--holidays', default='../../data/holidays.yaml') + subparser.add_argument('-n', '--dry', action='store_true') + subparser.add_argument('--recreate-all', action='store_true') + +def run(args): + loginData, schedule, holidays = _loadRawData(args) + packedSchedule = _unpackSchedules(schedule) + fixedSchedule = _addHolidayExceptions(packedSchedule, holidays) + _synchonizeCalendars(args, fixedSchedule, loginData, holidays) + +def _loadRawData(args): + _l.info('Loading data from hard disk') + loginData = login.loadLoginData() + + with open(args.schedule, 'r') as f: + schedule = yaml.safe_load(f.read()) + + _l.log(5, 'Schedule data:\n%s', pprint.pformat(schedule, indent=4, width=100)) + + with open(args.holidays, 'r') as f: + holidays = yaml.safe_load(f.read()) + + _l.log(5, 'Holidays data:\n%s', pprint.pformat(holidays, indent=4, width=100)) + + _l.info('Data was read from hard disk') + + return loginData, schedule, holidays + +class Event: + def __init__(self, day, start, duration, title): + self.day = day + self.start = start + self.duration = duration + self.title = title + self.age = None + self.external = False + self.description = '' + self.exceptions = [] + + def __repr__(self): + wAge = f' ({self.age})' if self.age is not None else '' + ext = 'Ext' if self.external else '' + return f'{ext}Ev({self.title}{wAge}) [{self.day}, {self.start}, {self.duration}]' + + def getHash(self, holidays): + def fixHolidays(holidays): + def fixDate(d): + return d.strftime('%Y-%m-%d') + + def fixFeast(f): + return fixDate(f) + + def fixHoliday(h): + return { + 'from': fixDate(h['from']), + 'to': fixDate(h['to']), + } + + return { + **holidays, + 'feasts': [fixFeast(f) for f in holidays['feasts']], + 'holidays': [fixHoliday(h) for h in holidays['holidays']], + } + + data = { + 'day': self.day, + 'start': self.start, + 'duration': self.duration, + 'title': self.title, + 'age': self.age, + 'external': self.external, + 'description': self.description, + 'exceptions': self.exceptions, + "holidays": fixHolidays(holidays), + } + # pprint.pprint(data, indent=4, width=100) + hasher = hashlib.sha1() + hasher.update(json.dumps(data, sort_keys=True).encode('utf-8')) + first = hasher.hexdigest() + second = uuid.uuid4().hex + return f'{first}___{second}', first + + dDay = dt.timedelta(days=1) + + _MAP_WEEKDAY_ICAL = { + 'Mon': 'MO', + 'Tue': 'TU', + 'Wed': 'WE', + 'Thu': 'TH', + 'Fri': 'FR', + 'Sat': 'SA', + 'Sun': 'SU', + } + + def addToCalendar(self, calendar: caldav.Calendar, holidays): + from . import cal_helper + + def getFirstTimeInYear(): + now = dt.datetime.now() + d = dt.datetime(year=now.year, month=1, day=1) + pass + + helper = cal_helper.IcalHelper() + startDay, start = helper.getStart(self, holidays) + + if start is None: + _l.warning('Could not get start time for event %s in the current year', self) + return + + uid, uidFirst = self.getHash(holidays) + icalData = { + 'uid': uid, + 'DTSTART': start, + 'DTEND': start + dt.timedelta(minutes=self.duration), + 'SUMMARY': self.title, + 'RRULE': { + 'FREQ': 'DAILY', + 'BYDAY': self._MAP_WEEKDAY_ICAL[self.day] + } + } + # debug.debugger() + + holidayExceptions = helper.getHolidayExceptions(self, startDay, holidays) + if holidayExceptions is not None: + icalData['EXDATE'] = holidayExceptions + + desc = '' + if self.age is not None: + desc += f'Jahrgänge: {self.age}' + if len(self.description) > 0: + desc += '\n\n' + desc += self.description + if len(desc) > 0: + icalData['DESCRIPTION'] = desc + + _l.log(5, 'Adding event\n%s', pprint.pformat(icalData, indent=4, width=100)) + + ical = caldav.lib.vcal.create_ical(**icalData) + _l.log(5, 'Created event\n%s', pprint.pformat(ical, indent=4, width=100)) + _l.log(5, 'Created event\n%s', ical) + calendar.add_event(**icalData) + +def _unpackSchedules(schedule): + def packSingleCalendar(cal): + def parseSingleEvent(ev): + e = Event( + day=ev['day'], + start=ev['start'], + duration=ev['duration'], + title=ev['title'] + ) + + if 'age' in ev: + e.age = ev['age'] + if ev.get('extern', False) or ev.get('external', False): + e.external = True + if 'desc' in ev: + e.description = ev['desc'] + if 'exceptions' in ev: + raise Exception('Not yet implemented to have exceptions') + + return e + + _l.log(5, 'Unpacking calendar %s', cal) + ret = { **cal } + + if 'schedule' in ret: + ret['schedule'] = [ + parseSingleEvent(e) for e in ret['schedule'] + ] + + _l.log(5, 'Unpacked calendar %s', ret) + return ret + + ret = {} + + for calName in schedule['calendars']: + if schedule['calendars'][calName].get('ignore', False): + _l.info('Ignoring calendar %s', calName) + continue + + ret[calName] = packSingleCalendar(schedule['calendars'][calName]) + + _l.log(5, 'Unpacked schedule:\n%s', pprint.pformat(ret, indent=4, width=100)) + return ret + +def _addHolidayExceptions(schedule, holidays): + return schedule + +class CalendarSynchonizer: + def __init__(self, args, calId, calName, loginData): + self.args = args + self.calId = calId + self.calName = calName + self.loginData = loginData + self._calDav = None + + def _getUrl(self): + return f'{self.loginData.base}/remote.php/dav/calendars' + + def _getCalDav(self): + + if self._calDav is None: + self._calDav = caldav.DAVClient( + url=self._getUrl(), + username=self.loginData.loginName, + password=self.loginData.appPassword + ) + return self._calDav + + def _getCalendar(self): + cd = self._getCalDav() + return cd.principal().calendar(cal_id=self.calId) + + def synchonize(self, calendars, holidays): + downstreamEvents = self._getDownstreamEvents(calendars[self.calName]['schedule'], holidays) + _l.debug('Downstream events:\n%s', pprint.pformat(downstreamEvents, indent=4, width=100)) + + upstreamEvents = self._getUpstreamEvents() + _l.debug('Upstream events:\n%s', pprint.pformat(upstreamEvents, indent=4, width=100)) + + newEvents = self._getNewEvents(upstreamEvents, downstreamEvents) + _l.log(5, 'New events:\n%s', pprint.pformat(newEvents, indent=4, width=100)) + deletedEvents = self._getDeletedEvents(upstreamEvents, downstreamEvents) + _l.log(5, 'Events marked for deletion:\n%s', pprint.pformat(deletedEvents, indent=4, width=100)) + + self._deleteEvents(deletedEvents) + self._addEvents(newEvents, holidays) + + def _getUpstreamEvents(self): + _l.debug('Fetching upstream calendar entries') + cd = self._getCalDav() + + # principal = cd.principal() + # _l.log(5, 'Principal %s', principal) + + # calendar = principal.calendar(cal_id=self.calId) + # _l.log(5, 'Calendar %s', calendar) + + calendar = self._getCalendar() + + # children = calendar.children() + # _l.debug('Getting children for calendar %s:\n%s', self.calId, children) + + events = calendar.events() + + ret = {} + + for e in events: + uidRaw = str(e.icalendar_component['uid']) + uid = uidRaw.split('___', 1)[0] + _l.log(5, 'Event with uid %s was found.', uid) + ret[uid] = e + + return ret + + def _getDownstreamEvents(self, schedule, holidays): + _l.debug('Preparing local calendar events') + + ret = {} + + for e in schedule: + uid, uidFirst = e.getHash(holidays) + _l.log(5, 'Event with uid part %s was found.', uidFirst) + ret[uidFirst] = e + + return ret + + def _getNewEvents(self, upstreamEvents, downstreamEvents): + if self.args.recreate_all: + return downstreamEvents.values() + + upstreamUids = set(upstreamEvents.keys()) + downstreamUids = set(downstreamEvents.keys()) + newUids = downstreamUids - upstreamUids + + return [downstreamEvents[uid] for uid in newUids] + + def _getDeletedEvents(self, upstreamEvents, downstreamEvents): + if self.args.recreate_all: + return upstreamEvents.values() + + upstreamUids = set(upstreamEvents.keys()) + downstreamUids = set(downstreamEvents.keys()) + toDeleteUids = upstreamUids - downstreamUids + + return [upstreamEvents[uid] for uid in toDeleteUids] + + def _addEvents(self, newEvents, holidays): + cal = self._getCalendar() + + for e in newEvents: + _l.debug('Adding event %s', e) + if self.args.dry: + _l.info('Skipping add event %s (dry mode)', e) + else: + # e.save() + e.addToCalendar(cal, holidays) + + def _deleteEvents(self, oldEvents): + for e in oldEvents: + _l.debug('Deleting event %s', e) + if self.args.dry: + _l.info('Skipping delete event %s (dry mode)', e) + else: + e.delete() + +def _synchonizeCalendars(args, calendars, loginData, holidays): + for calName in calendars: + calendar = calendars[calName] + _l.info('Synching calendar %s', calName) + calendarSynchonizer = CalendarSynchonizer(args, calendar['id'], calName, loginData) + calendarSynchonizer.synchonize(calendars, holidays) diff --git a/scripts/nc-cal-sync/helper/convert.sh b/scripts/nc-cal-sync/helper/convert.sh new file mode 100755 index 0000000..b139fa5 --- /dev/null +++ b/scripts/nc-cal-sync/helper/convert.sh @@ -0,0 +1,23 @@ +#!/bin/bash + +echo "calendars:" +for r in vorne mitte hinten +do + echo " $r:" + echo " id: \"$r\"" + echo " ignore: true" + echo " schedule:" + echo -n " " + ( + echo '[' + prefix_comma='' + for d in Mo Di Mi Do Fr + do + echo -n "$prefix_comma" + prefix_comma=', ' + cat ../../data/calendar.yaml | yq -cj '.calendar.'"$r.$d"' | . as $dict | [keys[] | . as $key | $dict[$key] | to_entries | [ .[], {key: "day", value: "'"$d"'"}, {key: "start", value: $key}, {key: "age", value: ""}, {key: "extern", value: false}, {key: "duration", value: (($dict[$key].slots) * 15)} ] | from_entries | del(.slots)]' + done + echo ']' + ) | jq -cj '[ .[][] ]' + echo +done diff --git a/scripts/nc-cal-sync/helper/test.yq b/scripts/nc-cal-sync/helper/test.yq new file mode 100644 index 0000000..b50ac87 --- /dev/null +++ b/scripts/nc-cal-sync/helper/test.yq @@ -0,0 +1,12 @@ +.calendar.vorne.Mo | . as $dict | + keys[] | . as $key | + $dict[$key] | to_entries | + [ + .[], + {key: "day", value: "Mo"}, + {key: "start", value: $key}, + {key: "age", value: ""}, + {key: "extern", value: false}, + {key: "duration", value: (($dict[$key].slots) * 15)} + ] | + from_entries | del(.slots) diff --git a/themes/tsc_vfl/assets/css/_colors.scss b/themes/tsc_vfl/assets/css/_colors.scss new file mode 100644 index 0000000..7666b62 --- /dev/null +++ b/themes/tsc_vfl/assets/css/_colors.scss @@ -0,0 +1,9 @@ +$color-red: #cd1013; + +$color-background-mobile-menu: #f5f5f5; +$color-background-mobile-menu-header: #e0e0e0; +$color-hor-line: #a5a5a5; + +$color-vh-vorne: #ddcb55; +$color-vh-mitte: #c98879; +$color-vh-hinten: #0082c9; diff --git a/themes/tsc_vfl/assets/css/_responsive.scss b/themes/tsc_vfl/assets/css/_responsive.scss new file mode 100644 index 0000000..d086a8f --- /dev/null +++ b/themes/tsc_vfl/assets/css/_responsive.scss @@ -0,0 +1,14 @@ + +@mixin media-large { + @media screen and (min-width: 700px) { + @content; + } +} + +@mixin mouse-available { + @media screen and (pointer: fine) { + @content; + } +} + + diff --git a/themes/tsc_vfl/assets/css/_schedule.scss b/themes/tsc_vfl/assets/css/_schedule.scss new file mode 100644 index 0000000..592b0f7 --- /dev/null +++ b/themes/tsc_vfl/assets/css/_schedule.scss @@ -0,0 +1,112 @@ +@use './responsive.scss' as r; +@use './colors.scss' as *; + + +.calendar-schedule { + $calendar-height-row: 60px; + + $border-style: solid lightgray 1px; + + display: grid; + + font-size: xx-small; + + @include r.media-large { + & { + font-size: small; + } + } + + .header { + display: contents; + font-weight: bold; + div { + width: 100%; + text-align: center; + box-sizing: border-box; + } + .rooms { + display: flex; + width: 100%; + + div { + flex: 1 0 0; + text-align: center; + } + } + .main-column { + border-right: $border-style; + } + } + + .table-row { + height: $calendar-height-row; + box-sizing: border-box; + + border-top: $border-style; + } + + .times-left { + border-right: $border-style; + } + .times-left, .times-right{ + width: 100%; + padding: 0 5px; + } + + .main-entry { + position: relative; + + border-right: $border-style; + + .event { + position: absolute; + z-index: 1; + + top: calc($calendar-height-row * var(--minutes) / 60.0); + height: calc($calendar-height-row * var(--duration) / 60.0); + + width: 30%; + padding: 1px 0; + box-sizing: border-box; + overflow: hidden; + + --fg-color: black; + + &.room-vorne { + --bg-color: var(--color-vhvorne); + left: 2.5%; + } + &.room-mitte { + --bg-color: var(--color-vhmitte); + left: 35% + } + &.room-hinten { + --bg-color: var(--color-vhhinten); + --fg-color: white; + left: 67.5%; + } + + div { + width: 100%; + height: 100%; + box-sizing: border-box; + padding: 3px; + border-radius: 4px; + + color: var(--fg-color); + background-color: var(--bg-color); + } + + } + } +} + +.calendar-grid-2-days { + grid-template-columns: auto repeat(2, 1fr) auto; +} + +.calendar-grid-3-days { + grid-template-columns: auto repeat(3, 1fr) auto; +} + diff --git a/themes/tsc_vfl/assets/css/main.scss b/themes/tsc_vfl/assets/css/main.scss index 2a765bd..7240868 100644 --- a/themes/tsc_vfl/assets/css/main.scss +++ b/themes/tsc_vfl/assets/css/main.scss @@ -1,14 +1,11 @@ +@use 'responsive.scss' as r; +@use './schedule.scss'; +@use './colors.scss' as *; + /* Variables */ $total-width: 95%; -$color-red: #cd1013; -$color-background-mobile-menu: #f5f5f5; -$color-background-mobile-menu-header: #e0e0e0; -$color-hor-line: #a5a5a5; $gap-columns-persons: 25px; $left-menu-width: 180px; -$color-vh-vorne: #ddcb55; -$color-vh-mitte: #c98879; -$color-vh-hinten: #0082c9; /* Mixins */ @@ -23,17 +20,6 @@ $color-vh-hinten: #0082c9; } } -@mixin media-large { - @media screen and (min-width: 700px) { - @content; - } -} - -@mixin mouse-available { - @media screen and (pointer: fine) { - @content; - } -} /* main styling */ @@ -78,7 +64,7 @@ h1 { width: $left-menu-width; - @include media-large { + @include r.media-large { display: flex; } @@ -138,138 +124,10 @@ h1 { max-width: 100%; hyphens: auto; - table { - width: 100%; - border-collapse: collapse; - - td, th { - padding: 5px; - border: none; - text-align: left; - } - - tr { - background-color: #dedede; - - &:nth-of-type(2n) { - background-color: #f7f7f7; - } - } - - thead > tr { - background-color: $color-red; - color: white; - text-align: left; - } - } - - .calendar-manual { - font-size: xx-small; - - @include media-large { - font-size: small; - } - - tr { - height: 20px; - - &.first-min { - border-top: solid lightgray 1px; - } - - &:nth-of-type(n) { - background-color: unset; - } - - .time { - vertical-align: top; - } - - .time:last-of-type, .first-col-of-room { - border-left: solid lightgray 1px; - } - - .day-title { - text-align: center; - } - } - - .calendar-block { - position: relative; - - .calendar-block-entity { - position: absolute; - top: 0; - left: 0; - width: 100%; - padding: 1.5px; - box-sizing: border-box; - - - &.height-1 { - height: 20px; - } - - &.height-2 { - height: 40px; - } - - &.height-3 { - height: 60px; - } - - &.height-4 { - height: 80px; - } - - &.height-5 { - height: 100px; - } - - &.height-6 { - height: 120px; - } - - &.height-7 { - height: 140px; - } - - &.height-8 { - height: 160px; - } - - &.height-9 { - height: 180px; - } - - &.height-10 { - height: 200px; - } - - .room-block { - width: 100%; - height: 100%; - box-sizing: border-box; - padding: 3px; - overflow: hidden; - } - - .room-vorne { - background-color: var(--color-vhvorne); - } - - .room-mitte { - background-color: var(--color-vhmitte); - } - - .room-hinten { - background-color: var(--color-vhhinten); - color: white; - } - } - } - } + + // @include schedule.legacy; + .float-right { float: right; margin: 7px 0 7px 15px; @@ -308,6 +166,33 @@ h1 { } +table { + width: 100%; + border-collapse: collapse; + + td, th { + padding: 5px; + border: none; + text-align: left; + } + + tr { + background-color: #dedede; + + &:nth-of-type(2n) { + background-color: #f7f7f7; + } + } + + thead > tr { + background-color: $color-red; + color: white; + text-align: left; + } +} + +// @include schedule.legacy; + #header { border-bottom: 2px solid rgba(173, 173, 173, 50%); margin: 0 auto 30px; @@ -342,15 +227,16 @@ h1 { // height: 250px; display: none; - @include media-large { - display: flex; - } - padding: 0 10px; border-right: 20px solid $color-red; border-left: 20px solid $color-red; position: relative; + @include r.media-large { + display: flex; + } + + > img { width: calc(100% - 20px); @@ -406,7 +292,7 @@ h1 { font: 1.5em 'Open Sans Condensed', sans-serif; display: none; - @include media-large { + @include r.media-large { display: block; } @@ -525,15 +411,15 @@ h1 { .person { width: 100%; - @include media-large { - width: calc(50% - #{$gap-columns-persons} / 2); - } - height: 80px; // margin: 10px 25px 10px 0; display: flex; + @include r.media-large { + width: calc(50% - #{$gap-columns-persons} / 2); + } + > .image { flex: 60px 0 0; @@ -587,7 +473,7 @@ h1 { margin-right: 2px; flex-direction: column; - @include media-large{ + @include r.media-large{ flex-direction: row; } @@ -596,7 +482,7 @@ h1 { display: block; height: 190px; - @include media-large{ + @include r.media-large{ flex: 33% 1 0; } @@ -664,12 +550,11 @@ h1 { padding: 30px 5% 0; box-sizing: border-box; - @include media-large { - display: none; - } - border-top: 1px solid $color-hor-line; + @include r.media-large { + display: none; + } .level-1 { width: 100%; @@ -717,7 +602,7 @@ h1 { @include menu-style; - @include media-large { + @include r.media-large { display: none; } } @@ -749,7 +634,7 @@ h1 { grid-template-columns: 1fr; gap: 15px; - @include media-large { + @include r.media-large { &.cols-2 { grid-template-columns: 1fr 1fr; } @@ -788,7 +673,7 @@ h1 { max-width: 70%; } - @include media-large { + @include r.media-large { display: flex; align-items: start; @@ -852,7 +737,7 @@ h1 { margin: 5px 0; align-items: baseline; - @include mouse-available { + @include r.mouse-available { margin: 0; } @@ -870,7 +755,7 @@ h1 { display: block; padding: 6.5px 0; - @include mouse-available { + @include r.mouse-available { padding: 3px 0; } } @@ -921,7 +806,7 @@ table.time { } } - @include media-large { + @include r.media-large { display: table; tr { diff --git a/themes/tsc_vfl/layouts/partials/page/head.html b/themes/tsc_vfl/layouts/partials/page/head.html index 3546ad2..262eb32 100644 --- a/themes/tsc_vfl/layouts/partials/page/head.html +++ b/themes/tsc_vfl/layouts/partials/page/head.html @@ -11,7 +11,7 @@ {{ end }} {{ end }} {{ with .Keywords }}{{ end }} - {{ $options := (dict "targetPath" "main.css" "outputStyle" "compressed" "enableSourceMap" (not hugo.IsProduction)) }} + {{ $options := (dict "targetPath" "main.css" "outputStyle" "compressed" "enableSourceMap" (not hugo.IsProduction) "transpiler" "dartsass") }} {{ $scss := resources.Get "css/main.scss" | css.Sass $options }} {{ $title := print .Site.Title " | " .Title }} diff --git a/themes/tsc_vfl/layouts/partials/tsc/calendar/list.html b/themes/tsc_vfl/layouts/partials/tsc/calendar/list.html new file mode 100644 index 0000000..86cbb9b --- /dev/null +++ b/themes/tsc_vfl/layouts/partials/tsc/calendar/list.html @@ -0,0 +1,18 @@ +{{ $calendars := site.Data.schedule.calendars -}} +{{- $list := slice -}} +{{- range $room, $roomData := $calendars -}} + {{- if ($roomData.ignore | default false) -}}{{ continue }}{{- end -}} + {{- $roomName := $roomData.name | default $room -}} + {{/* warnf "%s " $room */}} + {{- range $roomData.schedule -}} + {{- $addData := dict "room" $roomName "roomId" $room "weight" 0 -}} + {{- $addData = merge $addData . -}} + {{/* warnf "%#v" $addData */}} + {{- $list = $list | append $addData -}} + {{- end -}} +{{- end -}} +{{- $list = sort $list "start" -}} +{{- $list = sort $list "title" -}} +{{- $list = sort $list "weight" -}} +{{/* warnf "%#v" $list */}} +{{- return $list -}} diff --git a/themes/tsc_vfl/layouts/shortcodes/tsc/calendar/list.html b/themes/tsc_vfl/layouts/shortcodes/tsc/calendar/list.html new file mode 100644 index 0000000..cf8e880 --- /dev/null +++ b/themes/tsc_vfl/layouts/shortcodes/tsc/calendar/list.html @@ -0,0 +1,83 @@ +{{- $list := partialCached "tsc/calendar/list" . -}} +{{- $list = sort $list "weight" -}} +{{- $list = sort $list "start" -}} +{{- $list = sort $list "class" -}} +{{- $list = sort $list "title" -}} +{{- $cals := apply $list "index" "." "class" -}} +{{- $cals = uniq $cals -}} +{{/* warnf "%s" $cals */}} + + + + + + + + + + + + + {{ range $list -}} + + + + + + + + + {{- end}} + +
GruppeKategorieJahrgangTagZeitOrt
+ {{ .title }}{{ with .subtitle }} - {{ . }}{{ end }} + {{ if (.extern | default false) }}(ext.){{ end }} + + {{ with .class }}{{ . }}{{ end }} + {{ with .age }}{{ . }}{{ end }}{{ index site.Data.days .day }} + {{- $startTimeStr := printf "2025-01-02T%s:00" .start -}} + {{- $startTime := time.AsTime $startTimeStr -}} + {{- $duration := time.Duration "minute" .duration -}} + {{- $endTime := $startTime.Add $duration}} + {{/* warnf "Start %s, duration %s, %s" $startTime $duration $endTime */}} + {{- $startTime.Format "15:04"}} - {{ $endTime.Format "15:04" }} + {{ .room }}
+ +{{ range $cals }} +{{- $class := . -}} +

Class {{ with . }}{{ . }}{{else}}nil{{end}}

+ + + + + + + + + + + + + {{ range $list -}} + {{- if ne .class $class }}{{ continue }}{{ end }} + + + + + + + + {{- end}} + +
GruppeJahrgangTagZeitOrt
+ {{ .title }}{{ with .subtitle }} - {{ . }}{{ end }} + {{ if (.extern | default false) }}(ext.){{ end }} + {{ with .age }}{{ . }}{{ end }}{{ index site.Data.days .day }} + {{- $startTimeStr := printf "2025-01-02T%s:00" .start -}} + {{- $startTime := time.AsTime $startTimeStr -}} + {{- $duration := time.Duration "minute" .duration -}} + {{- $endTime := $startTime.Add $duration}} + {{/* warnf "Start %s, duration %s, %s" $startTime $duration $endTime */}} + {{- $startTime.Format "15:04"}} - {{ $endTime.Format "15:04" }} + {{ .room }}
+{{ end }} diff --git a/themes/tsc_vfl/layouts/shortcodes/tsc/calendar/schedule.html b/themes/tsc_vfl/layouts/shortcodes/tsc/calendar/schedule.html new file mode 100644 index 0000000..c7e41c6 --- /dev/null +++ b/themes/tsc_vfl/layouts/shortcodes/tsc/calendar/schedule.html @@ -0,0 +1,51 @@ +{{- $start := .Get 0 -}} +{{- $end := .Get 1 -}} +{{- $days := after 2 .Params -}} +{{- $numDays := len $days -}} +{{- $calendar := $.Site.Data.calendar.calendar -}} +{{- $listSchedule := partialCached "tsc/calendar/list" . }} +
+
+
+ {{ range $days }} +
{{ . }}
+ {{ end }} +
+
+ {{ range $days }} +
+
Vorne
+
Mitte
+
Hinten
+
+ {{ end }} +
+
+ {{ range (seq $start $end) }} + {{- $currentHour := string . -}} + {{- $addClass := "" -}} + {{- $firstRow := eq . $start -}} + {{- if eq . $start }}{{ $addClass = "first-main-row"}}{{ end -}} +
{{ printf "%2d:00" . }}
+ {{- range $id, $day := $days }} +
+ {{- range $listSchedule }} + {{- if ne .day $day }}{{ continue }}{{ end -}} + {{- $sTime := time.AsTime (printf "2025-01-02T%s:00" .start) -}} + {{- $evHour := $sTime.Format "15" -}} + {{- if ne $currentHour $evHour }}{{ continue }}{{ end -}} + {{- $hours := int ($sTime.Format "15") -}} + {{- $minutes := int ($sTime.Format "4") -}} + {{- $style := printf "--duration: %d; --minutes: %d;" .duration $minutes -}} + {{/* warnf "%s" $style */}} +
+
+ {{ .title }} +
+
+ {{ end -}} +
+ {{ end -}} +
{{ printf "%2d:00" . }}
+ {{ end }} +
diff --git a/themes/tsc_vfl/layouts/shortcodes/tsc/calendar/table.html b/themes/tsc_vfl/layouts/shortcodes/tsc/calendar/table.html new file mode 100644 index 0000000..ecf0edc --- /dev/null +++ b/themes/tsc_vfl/layouts/shortcodes/tsc/calendar/table.html @@ -0,0 +1,39 @@ +{{- $list := partialCached "tsc/calendar/list" . -}} +{{- $cat := .Get "category" -}} +{{/* warnf "%#v" $cat */}} +{{- $showAge := .Get "showAge" | default false -}} + + + + + {{ if $showAge }}{{ end }} + + + + + + + {{ range $list -}} + {{- $cats := slice .class -}} + {{- if eq (printf "%T" .class) "[]interface {}" }}{{ $cats = .class }}{{ end -}} + {{- if not (in $cats $cat) }}{{ continue }}{{ end -}} + {{/* warnf "%T %#v" .class . */}} + + + {{ if $showAge }}{{ end }} + + + + + {{- end}} + +
GruppeJahrgangTagZeitOrt
+ {{ .title }}{{ with .subtitle }} - {{ . }}{{ end }} + {{ .age }}{{ index site.Data.days .day }} + {{- $startTimeStr := printf "2025-01-02T%s:00" .start -}} + {{- $startTime := time.AsTime $startTimeStr -}} + {{- $duration := time.Duration "minute" .duration -}} + {{- $endTime := $startTime.Add $duration}} + {{/* warnf "Start %s, duration %s, %s" $startTime $duration $endTime */}} + {{- $startTime.Format "15:04"}} - {{ $endTime.Format "15:04" }} + {{ .room }}
diff --git a/themes/tsc_vfl/layouts/shortcodes/tsc/show-calendar.html b/themes/tsc_vfl/layouts/shortcodes/tsc/show-calendar.html deleted file mode 100644 index 5b307ea..0000000 --- a/themes/tsc_vfl/layouts/shortcodes/tsc/show-calendar.html +++ /dev/null @@ -1,64 +0,0 @@ -{{ $start := .Get 0 }} -{{ $end := .Get 1}} -{{ $days := after 2 .Params }} -{{ $calendar := $.Site.Data.calendar.calendar }} - - - - {{ range $days }} - - {{ end }} - - - - - {{ range $days }} - - - - {{ end }} - - - {{ range seq $start $end }} - {{ $hour := . }} - {{ range seq 0 15 45 }} - {{ $firstMin := "" }} - {{ if eq . 0 }} - {{ $firstMin = "first-min" }} - {{ end }} - - {{ $time := printf "%2d:%02d" $hour . }} - {{ if or (eq . 0) (eq . 30) }} - - {{ end }} - {{ range $days }} - {{ $day := . }} - {{ $firstRoom := true }} - {{ range slice "vorne" "mitte" "hinten" }} - {{ $room := . }} - {{ $addClass := "" }} - {{ if $firstRoom }} - {{ $addClass = "first-col-of-room" }} - {{ $firstRoom = false }} - {{ end }} - {{ with index (index (index $calendar .) $day) $time }} - - {{ else }} - - {{ end }} - {{ end }} - {{ end }} - {{ if or (eq . 0) (eq . 30) }} - {{ $time := printf "%2d.%02d" $hour . }} - - {{ end }} - - {{ end }} - {{ end }} -
{{ . }}
vornemittehinten
{{ $time }} -
-
- {{ .title }} -
-
-
{{ $time }}