Merge pull request 'Migrate to central appointment database' (#45) from sync-calendar into develop

Reviewed-on: #45
This commit is contained in:
Christian Wolf 2025-01-20 13:11:19 +00:00
commit ad93f9fba7
35 changed files with 1757 additions and 439 deletions

View File

@ -14,9 +14,7 @@ Mit unserer Breitensportgruppe sprechen wie diejenigen an, die zwar (noch) keine
   
## Trainingszeit Breitensport ## Trainingszeit Breitensport
| Breitensport Standard | Tag | Zeit | Ort | {{<tsc/calendar/table category="Breitensport">}}
|-----------------------|-------------|----------------|-----------|
| Breitensport Standard | Mittwoch | 20:00 - 21:30 | VH Hinten |
{{% tsc/link-offers %}} {{% tsc/link-offers %}}

View File

@ -22,36 +22,15 @@ Und wann bist Du dabei?
## Trainingszeiten Dance-Styles Adults ## Trainingszeiten Dance-Styles Adults
|Dance Styles | Alter | Tag | Zeit | Ort | {{< tsc/calendar/table category="DSAdults" showAge="true" >}}
|--------------------|---------|------------|---------------|-----------|
| 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 |
## Trainingszeiten Dance-Styles Teens ## Trainingszeiten Dance-Styles Teens
|Dance-Styles Teens | Jahrgang | Tag | Zeit | Ort | {{< tsc/calendar/table category="DSTeens" showAge="true" >}}
|-------------------|-----------|----------|---------------|------------|
| 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 |
## Trainingszeiten Dance-Styles Kids ## Trainingszeiten Dance-Styles Kids
|Dance-Styles Kids | Jahrgang | Tag | Zeit | Ort | {{< tsc/calendar/table category="DSKids" showAge="true" >}}
|---------------------|-----------|------------|---------------|-----------|
| 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 |
Interesse an Standard- und Lateintanzkursen für Kinder und Jugendliche? Im Bereich Kinder/Jugend findest du weitere Angebote. Interesse an Standard- und Lateintanzkursen für Kinder und Jugendliche? Im Bereich Kinder/Jugend findest du weitere Angebote.

View File

@ -18,8 +18,6 @@ Wer schon Discofox tanzen kann, lernt bestimmt die eine oder andere tolle neue F
## Trainingszeit Discofox ## Trainingszeit Discofox
|Discofox | Tag | Zeit | Ort | {{< tsc/calendar/table category="Discofox" >}}
|---|---|---|---|
|Discofox | Freitag | 20:30 - 21:30 | VH Vorn |
{{% tsc/link-offers %}} {{% tsc/link-offers %}}

View File

@ -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 ## Trainingszeiten Kinder- / Jugendgruppen, Standard und Latein
| Kinder | Jahrgang | Tag | Zeit | Ort | {{< tsc/calendar/table category="Kinder" showAge="true" >}}
|----------------------------------------|-----------|------------|---------------|------------|
| 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 |
Weitere spannende Angebote für Kinder und Teens findet ihr bei unseren Dance-Stylern. Weitere spannende Angebote für Kinder und Teens findet ihr bei unseren Dance-Stylern.

View File

@ -17,16 +17,6 @@ Interessiert? Na, dann schauen Sie doch einfach einmal vorbei!
## Trainingszeiten Tanzkreise ## Trainingszeiten Tanzkreise
| Tanzkreise | Tag | Zeit | Ort | {{< tsc/calendar/table category="Tanzkreise" >}}
|-------------|-------------|---------------|----------|
| 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/link-offers %}} {{% tsc/link-offers %}}

View File

@ -22,16 +22,9 @@ Darüber hinaus können unsere Tänzer zu fast jeder Zeit noch frei trainieren -
[unsere Turnierpaare]({{< relref "paare" >}}) [unsere Turnierpaare]({{< relref "paare" >}})
## Trainingszeiten Turniertanz ## 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 %}} {{% tsc/link-offers %}}
## Sportverwaltung und weitere Links ## Sportverwaltung und weitere Links

View File

@ -11,6 +11,7 @@ aliases:
--- ---
Hier finden Sie die Übersicht zur aktuellen Belegung unserer Tanzsäle. Hier finden Sie die Übersicht zur aktuellen Belegung unserer Tanzsäle.
## Raumbelegung - außerplanmäßig ## Raumbelegung - außerplanmäßig
Außerhalb der Trainings- und Kurszeiten können die Räume für spezielle (Gruppen-) Trainings gebucht werden. 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 ## Raumbelegung - planmäßig
{{< tsc/show-calendar 10 22 "Mo" "Di" "Mi" >}} {{< tsc/calendar/schedule 10 22 "Mon" "Tue" "Wed" >}}
{{< tsc/show-calendar 10 22 "Do" "Fr" >}}
{{< tsc/calendar/schedule 14 22 "Thu" "Fri" >}}
## Regeln für die Belegung der Tanzsäle ## Regeln für die Belegung der Tanzsäle

View File

@ -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 >}}

View File

@ -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: {}

7
data/days.yml Normal file
View File

@ -0,0 +1,7 @@
Mon: Montag
Tue: Dienstag
Wed: Mittwoch
Thu: Donnerstag
Fri: Freitag
Sat: Samstag
Sun: Sonntag

27
data/holidays.yaml Normal file
View File

@ -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

261
data/schedule.yaml Normal file
View File

@ -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]

View File

@ -1,5 +1,6 @@
baseURL: https://tanzsportclub.vfl-sindelfingen.de/ baseURL: https://tanzsportclub.vfl-sindelfingen.de/
languageCode: de languageCode: de
defaultContentLanguage: de
title: TSC im VfL Sindelfingen e.V. title: TSC im VfL Sindelfingen e.V.
theme: tsc_vfl theme: tsc_vfl
relativeUrls: true relativeUrls: true

4
scripts/nc-cal-sync/.gitignore vendored Normal file
View File

@ -0,0 +1,4 @@
/venv/
__pycache__/
/login.json

View File

@ -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"

433
scripts/nc-cal-sync/Pipfile.lock generated Normal file
View File

@ -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": {}
}

View File

@ -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)

View File

@ -0,0 +1,3 @@
import calendar_synchronizer
calendar_synchronizer.main()

View File

@ -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

View File

@ -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()

View File

@ -0,0 +1,10 @@
_listening = False
def debugger():
global _listening
if not _listening:
import debugpy
debugpy.listen(5678)
debugpy.wait_for_client()
_listening = True

View File

@ -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'])

View File

@ -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)

View File

@ -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

View File

@ -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)

View File

@ -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;

View File

@ -0,0 +1,14 @@
@mixin media-large {
@media screen and (min-width: 700px) {
@content;
}
}
@mixin mouse-available {
@media screen and (pointer: fine) {
@content;
}
}

View File

@ -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;
}

View File

@ -1,14 +1,11 @@
@use 'responsive.scss' as r;
@use './schedule.scss';
@use './colors.scss' as *;
/* Variables */ /* Variables */
$total-width: 95%; $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; $gap-columns-persons: 25px;
$left-menu-width: 180px; $left-menu-width: 180px;
$color-vh-vorne: #ddcb55;
$color-vh-mitte: #c98879;
$color-vh-hinten: #0082c9;
/* Mixins */ /* 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 */ /* main styling */
@ -78,7 +64,7 @@ h1 {
width: $left-menu-width; width: $left-menu-width;
@include media-large { @include r.media-large {
display: flex; display: flex;
} }
@ -138,137 +124,9 @@ h1 {
max-width: 100%; max-width: 100%;
hyphens: auto; 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 { // @include schedule.legacy;
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;
}
}
}
}
.float-right { .float-right {
float: right; float: right;
@ -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 { #header {
border-bottom: 2px solid rgba(173, 173, 173, 50%); border-bottom: 2px solid rgba(173, 173, 173, 50%);
margin: 0 auto 30px; margin: 0 auto 30px;
@ -342,15 +227,16 @@ h1 {
// height: 250px; // height: 250px;
display: none; display: none;
@include media-large {
display: flex;
}
padding: 0 10px; padding: 0 10px;
border-right: 20px solid $color-red; border-right: 20px solid $color-red;
border-left: 20px solid $color-red; border-left: 20px solid $color-red;
position: relative; position: relative;
@include r.media-large {
display: flex;
}
> img { > img {
width: calc(100% - 20px); width: calc(100% - 20px);
@ -406,7 +292,7 @@ h1 {
font: 1.5em 'Open Sans Condensed', sans-serif; font: 1.5em 'Open Sans Condensed', sans-serif;
display: none; display: none;
@include media-large { @include r.media-large {
display: block; display: block;
} }
@ -525,15 +411,15 @@ h1 {
.person { .person {
width: 100%; width: 100%;
@include media-large {
width: calc(50% - #{$gap-columns-persons} / 2);
}
height: 80px; height: 80px;
// margin: 10px 25px 10px 0; // margin: 10px 25px 10px 0;
display: flex; display: flex;
@include r.media-large {
width: calc(50% - #{$gap-columns-persons} / 2);
}
> .image { > .image {
flex: 60px 0 0; flex: 60px 0 0;
@ -587,7 +473,7 @@ h1 {
margin-right: 2px; margin-right: 2px;
flex-direction: column; flex-direction: column;
@include media-large{ @include r.media-large{
flex-direction: row; flex-direction: row;
} }
@ -596,7 +482,7 @@ h1 {
display: block; display: block;
height: 190px; height: 190px;
@include media-large{ @include r.media-large{
flex: 33% 1 0; flex: 33% 1 0;
} }
@ -664,12 +550,11 @@ h1 {
padding: 30px 5% 0; padding: 30px 5% 0;
box-sizing: border-box; box-sizing: border-box;
@include media-large {
display: none;
}
border-top: 1px solid $color-hor-line; border-top: 1px solid $color-hor-line;
@include r.media-large {
display: none;
}
.level-1 { .level-1 {
width: 100%; width: 100%;
@ -717,7 +602,7 @@ h1 {
@include menu-style; @include menu-style;
@include media-large { @include r.media-large {
display: none; display: none;
} }
} }
@ -749,7 +634,7 @@ h1 {
grid-template-columns: 1fr; grid-template-columns: 1fr;
gap: 15px; gap: 15px;
@include media-large { @include r.media-large {
&.cols-2 { &.cols-2 {
grid-template-columns: 1fr 1fr; grid-template-columns: 1fr 1fr;
} }
@ -788,7 +673,7 @@ h1 {
max-width: 70%; max-width: 70%;
} }
@include media-large { @include r.media-large {
display: flex; display: flex;
align-items: start; align-items: start;
@ -852,7 +737,7 @@ h1 {
margin: 5px 0; margin: 5px 0;
align-items: baseline; align-items: baseline;
@include mouse-available { @include r.mouse-available {
margin: 0; margin: 0;
} }
@ -870,7 +755,7 @@ h1 {
display: block; display: block;
padding: 6.5px 0; padding: 6.5px 0;
@include mouse-available { @include r.mouse-available {
padding: 3px 0; padding: 3px 0;
} }
} }
@ -921,7 +806,7 @@ table.time {
} }
} }
@include media-large { @include r.media-large {
display: table; display: table;
tr { tr {

View File

@ -11,7 +11,7 @@
{{ end }} {{ end }}
{{ end }} {{ end }}
{{ with .Keywords }}<meta name="keywords" content="{{ delimit . "," "," }}">{{ end }} {{ with .Keywords }}<meta name="keywords" content="{{ delimit . "," "," }}">{{ 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 }} {{ $scss := resources.Get "css/main.scss" | css.Sass $options }}
<link rel="stylesheet" type="text/css" href="{{ $scss.RelPermalink }}" /> <link rel="stylesheet" type="text/css" href="{{ $scss.RelPermalink }}" />
{{ $title := print .Site.Title " | " .Title }} {{ $title := print .Site.Title " | " .Title }}

View File

@ -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 -}}

View File

@ -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 */}}
<table>
<thead>
<tr>
<th>Gruppe</th>
<th>Kategorie</th>
<th>Jahrgang</th>
<th>Tag</th>
<th>Zeit</th>
<th>Ort</th>
</tr>
</thead>
<tbody>
{{ range $list -}}
<tr>
<td>
{{ .title }}{{ with .subtitle }} - {{ . }}{{ end }}
{{ if (.extern | default false) }}<i>(ext.)</i>{{ end }}
</td>
<td>
{{ with .class }}{{ . }}{{ end }}
</td>
<td>{{ with .age }}{{ . }}{{ end }}</td>
<td>{{ index site.Data.days .day }}</td>
<td>
{{- $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" }}
</td>
<td>{{ .room }}</td>
</tr>
{{- end}}
</tbody>
</table>
{{ range $cals }}
{{- $class := . -}}
<h2>Class {{ with . }}{{ . }}{{else}}<i>nil</i>{{end}}</h2>
<table>
<thead>
<tr>
<th>Gruppe</th>
<th>Jahrgang</th>
<th>Tag</th>
<th>Zeit</th>
<th>Ort</th>
</tr>
</thead>
<tbody>
{{ range $list -}}
{{- if ne .class $class }}{{ continue }}{{ end }}
<tr>
<td>
{{ .title }}{{ with .subtitle }} - {{ . }}{{ end }}
{{ if (.extern | default false) }}<i>(ext.)</i>{{ end }}
</td>
<td>{{ with .age }}{{ . }}{{ end }}</td>
<td>{{ index site.Data.days .day }}</td>
<td>
{{- $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" }}
</td>
<td>{{ .room }}</td>
</tr>
{{- end}}
</tbody>
</table>
{{ end }}

View File

@ -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" . }}
<div class="calendar-schedule calendar-grid-{{ $numDays }}-days">
<div class="header">
<div class="times-left"></div>
{{ range $days }}
<div class="main-column">{{ . }}</div>
{{ end }}
<div class="times-right"></div>
<div class="times-left"></div>
{{ range $days }}
<div class="rooms main-column">
<div>Vorne</div>
<div>Mitte</div>
<div>Hinten</div>
</div>
{{ end }}
<div class="times-right"></div>
</div>
{{ range (seq $start $end) }}
{{- $currentHour := string . -}}
{{- $addClass := "" -}}
{{- $firstRow := eq . $start -}}
{{- if eq . $start }}{{ $addClass = "first-main-row"}}{{ end -}}
<div class="table-row times-left">{{ printf "%2d:00" . }}</div>
{{- range $id, $day := $days }}
<div class="table-row main-entry">
{{- 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 */}}
<div class="event room-{{ .roomId }}" style="{{ $style | safeCSS }}">
<div>
{{ .title }}
</div>
</div>
{{ end -}}
</div>
{{ end -}}
<div class="table-row times-right">{{ printf "%2d:00" . }}</div>
{{ end }}
</div>

View File

@ -0,0 +1,39 @@
{{- $list := partialCached "tsc/calendar/list" . -}}
{{- $cat := .Get "category" -}}
{{/* warnf "%#v" $cat */}}
{{- $showAge := .Get "showAge" | default false -}}
<table>
<thead>
<tr>
<th>Gruppe</th>
{{ if $showAge }}<th>Jahrgang</th>{{ end }}
<th>Tag</th>
<th>Zeit</th>
<th>Ort</th>
</tr>
</thead>
<tbody>
{{ 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 . */}}
<tr>
<td>
{{ .title }}{{ with .subtitle }} - {{ . }}{{ end }}
</td>
{{ if $showAge }}<td>{{ .age }}</td>{{ end }}
<td>{{ index site.Data.days .day }}</td>
<td>
{{- $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" }}
</td>
<td>{{ .room }}</td>
</tr>
{{- end}}
</tbody>
</table>

View File

@ -1,64 +0,0 @@
{{ $start := .Get 0 }}
{{ $end := .Get 1}}
{{ $days := after 2 .Params }}
{{ $calendar := $.Site.Data.calendar.calendar }}
<table class="calendar-manual">
<tr>
<th></th>
{{ range $days }}
<th colspan="3" class="day-title">{{ . }}</th>
{{ end }}
<th></th>
</tr>
<tr>
<th></th>
{{ range $days }}
<th class="first-col-of-room">vorne</th>
<th>mitte</th>
<th>hinten</th>
{{ end }}
<th></th>
</tr>
{{ range seq $start $end }}
{{ $hour := . }}
{{ range seq 0 15 45 }}
{{ $firstMin := "" }}
{{ if eq . 0 }}
{{ $firstMin = "first-min" }}
{{ end }}
<tr class="{{ $firstMin }}">
{{ $time := printf "%2d:%02d" $hour . }}
{{ if or (eq . 0) (eq . 30) }}
<td rowspan="2" class="time">{{ $time }}</td>
{{ 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 }}
<td class="calendar-block {{ $addClass }}">
<div class="calendar-block-entity height-{{ .slots }}">
<div class="room-block room-{{ $room }}">
{{ .title }}
</div>
</div>
</td>
{{ else }}
<td class="{{ $addClass }}"></td>
{{ end }}
{{ end }}
{{ end }}
{{ if or (eq . 0) (eq . 30) }}
{{ $time := printf "%2d.%02d" $hour . }}
<td rowspan="2" class="time">{{ $time }}</td>
{{ end }}
</tr>
{{ end }}
{{ end }}
</table>