Compare commits
9 Commits
e30bc6cd06
...
d5e66bd052
| Author | SHA1 | Date | |
|---|---|---|---|
| d5e66bd052 | |||
| b0d0effed1 | |||
| 8290a50c2e | |||
| 337b90e521 | |||
| e7efa26817 | |||
| 00a25fdad9 | |||
| 1fe6cc24f4 | |||
| b232ccddf8 | |||
| 93cff0c680 |
2
.gitignore
vendored
2
.gitignore
vendored
@ -9,4 +9,4 @@ config.dev.ini
|
|||||||
config.prod.ini
|
config.prod.ini
|
||||||
events.ini
|
events.ini
|
||||||
deploy.ini
|
deploy.ini
|
||||||
venv/
|
.venv/
|
||||||
11
.vscode/settings.json
vendored
Normal file
11
.vscode/settings.json
vendored
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
{
|
||||||
|
"spellright.language": [
|
||||||
|
"de_DE",
|
||||||
|
"en_US"
|
||||||
|
],
|
||||||
|
"spellright.documentTypes": [
|
||||||
|
"markdown",
|
||||||
|
"latex",
|
||||||
|
"plaintext"
|
||||||
|
]
|
||||||
|
}
|
||||||
46
README.md
46
README.md
@ -133,6 +133,52 @@ Alle Werke und Texte hingegen verwenden das Template `article.html`.
|
|||||||
|
|
||||||
### Termine neu einlesen
|
### Termine neu einlesen
|
||||||
|
|
||||||
|
Um die Termine automatisch einzulesen und dem Pelican-Template `theme/templates/termine.html` zur Verfügung zu stellen, gibt es das Python-Script `utils/refresh_events.py`. Dieses Script
|
||||||
|
|
||||||
|
1. öffnet die Config-Datei `events.ini` – diese muss im Hauptverzeichnis des Projekts liegen –,
|
||||||
|
2. öffnet den in der Config-Datei definierten CalDAV-Kalender und liest daraus alle Events ein, die der Kategorie "Veranstaltung" angehören,
|
||||||
|
3. bereitet die Event-Daten für die Weiterverarbeitung auf
|
||||||
|
4. und speichert sie im YAML-Format in der Datei `content/pages/termine.md`.
|
||||||
|
|
||||||
|
Im Development-Modus muss `refresh_events.py` per Hand aufgerufen werden; im Publication-Modus geschieht dies automatisch.
|
||||||
|
|
||||||
|
Damit eine Lesung aus dem Kalender auf die Termine-Seite übernommen wird, muss sie folgende Voraussetzungen erfüllen:
|
||||||
|
|
||||||
|
1. im richtigen Kalender eingetragen sein (duh)
|
||||||
|
2. der Kategorie "Veranstaltung" angehören
|
||||||
|
3. in der Zukunft liegen
|
||||||
|
|
||||||
|
Standardmäßig werden alle Properties des "icalender"-Standards eingelesen und als Metadaten abgespeichert; ausgenommen davon sind nur die Properties "action", "repeat" und "trigger". Konkret werden vom Template die folgenden Properties ausgewertet:
|
||||||
|
|
||||||
|
* "dtstart": wird vom Script in "startdate" und ggf. "starttime" aufgespalten
|
||||||
|
* "dtend": wird vom Script zu "enddate" umgewandelt, wenn es vom Startdatum abweicht
|
||||||
|
* "summary": der Titel der Veranstaltung
|
||||||
|
* "description": zusäzliche Informationen zur Veranstaltung
|
||||||
|
* bestimmte Einträge in "categories":
|
||||||
|
* "Moderation": hängt "(Moderation)" an die Summary an
|
||||||
|
* "Babelsberger Lesesalon", "Andere Welten": gibt dem Datumsfeld des Termins die der entsprechenden Veranstaltungsreihe zugeordnete Hintergrundfarbe
|
||||||
|
* "attach": das Script geht die dem Event angehängten Links durch und stellt dem Template zur Verfügung:
|
||||||
|
* max. einen Link zu einer Webseite (als "link")
|
||||||
|
* max. einen Link zu einer Bilddatei (als "image")
|
||||||
|
|
||||||
|
Zu den Attachments ist Folgendes zu beachten:
|
||||||
|
|
||||||
|
* Alle Links, die zu einer Bilddatei verweisen (bei denen der "Pfad"-Teil der URL auf eine Datei mit einer Bilddatei-Suffix, z.B. "jpg", verweist), gelten als Bild-Links. Alle akzeptierten Suffixe sind in der Script-Variable "image_suffixes" definiert.
|
||||||
|
* Alle Links, die nicht als Bild-Links eingestugt werden, gelten als reguläre Links.
|
||||||
|
* Wenn mehrere Bild- oder reguläre Links existieren, wird der jeweils letzte verwendet.
|
||||||
|
* Soll auf eine lokale Bilddatei verwiesen werden, muss der Hostname des "attach"-Eintrags `localhost`, und eine gleićhnamige Datei muss im Verzeichnis `content/images/termine/` vorhanden sein. URL-Schema, -Pfad und Query-Parameter werden nicht ausgewertet. Attachments, die auf Webseiten oder auf Bilddateien auf externen Servern verweisen, bleiben unverändert.
|
||||||
|
|
||||||
|
Beispiele:
|
||||||
|
|
||||||
|
| Attachment URL | URL-Typ | aufbereitete URL |
|
||||||
|
| -------------- | ---------------- | ------- |
|
||||||
|
| `http://localhost/spam/eggs/image.jpg` | Bild | `../images/termine/image.jpg` |
|
||||||
|
| `ftp://localhost/image2.png` | Bild | `../images/termine/image2.jpg` |
|
||||||
|
| `https://example.com/path/to/image3.png` | Bild | (unverändert) |
|
||||||
|
| `http://veranstaltungsort.de/kalender/coole-lesung-mit-tobi.html` | Link | (unverändert) |
|
||||||
|
| `https://127.0.0.1/foo/bar/image4.webp?size=400&height=260` | Bild | `../images/termine/image4.webp` |
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
## TODOs
|
## TODOs
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 316 KiB After Width: | Height: | Size: 316 KiB |
BIN
content/images/termine/bls2526.jpg
Normal file
BIN
content/images/termine/bls2526.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 282 KiB |
@ -11,7 +11,7 @@ card_images:
|
|||||||
link: kurzprosa/person-of-the-year.html
|
link: kurzprosa/person-of-the-year.html
|
||||||
alt: Person of the Year
|
alt: Person of the Year
|
||||||
y_offset: 50%
|
y_offset: 50%
|
||||||
- pic: images/kurzprosa/cover-blinde-flecken-ct.jpg
|
- pic: images/kurzprosa/motif-blinde-flecken-ct.jpg
|
||||||
link: kurzprosa/blinde-flecken.html
|
link: kurzprosa/blinde-flecken.html
|
||||||
alt: Blinde Flecken
|
alt: Blinde Flecken
|
||||||
y_offset: 65%
|
y_offset: 65%
|
||||||
|
|||||||
@ -24,7 +24,7 @@ summary: Warum wir uns in der Zukunft über Kundendiensthotlines noch mehr ärge
|
|||||||
lang: de
|
lang: de
|
||||||
order: 45
|
order: 45
|
||||||
featured_image:
|
featured_image:
|
||||||
- pic: ../images/kurzprosa/cover-blinde-flecken-ct.jpg
|
- pic: ../images/kurzprosa/motif-blinde-flecken-ct.jpg
|
||||||
credit: Illustration aus c't Ausgabe 24/23
|
credit: Illustration aus c't Ausgabe 24/23
|
||||||
credit_link: https://www.heise.de/select/ct/2023/24/2230012174824069537
|
credit_link: https://www.heise.de/select/ct/2023/24/2230012174824069537
|
||||||
---
|
---
|
||||||
|
|||||||
7
requirements.txt
Normal file
7
requirements.txt
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
caldav
|
||||||
|
ConfigParser
|
||||||
|
pelican[markdown]
|
||||||
|
pelican-yaml-metadata
|
||||||
|
pelican-image-process
|
||||||
|
Pillow
|
||||||
|
vobject
|
||||||
@ -6,6 +6,9 @@
|
|||||||
--tr-smallest-width: 350px;
|
--tr-smallest-width: 350px;
|
||||||
--tr-accent-color: #cf0000;
|
--tr-accent-color: #cf0000;
|
||||||
|
|
||||||
|
--tr-events-color-bls: #e66720;
|
||||||
|
--tr-events-color-paw: #a830b3;
|
||||||
|
|
||||||
--pico-font-family-sans-serif: "Libertinus Sans", system-ui, "Segoe UI", Roboto,
|
--pico-font-family-sans-serif: "Libertinus Sans", system-ui, "Segoe UI", Roboto,
|
||||||
Oxygen, Ubuntu, Cantarell, Helvetica, Arial, "Helvetica Neue", sans-serif,
|
Oxygen, Ubuntu, Cantarell, Helvetica, Arial, "Helvetica Neue", sans-serif,
|
||||||
var(--pico-font-family-emoji);
|
var(--pico-font-family-emoji);
|
||||||
@ -384,20 +387,69 @@ article form {
|
|||||||
50% { clip-path: polygon(0 0, 100% 0, 100% 100%, 0% 100%); }
|
50% { clip-path: polygon(0 0, 100% 0, 100% 100%, 0% 100%); }
|
||||||
}
|
}
|
||||||
|
|
||||||
.events .tr-muted {
|
|
||||||
color: var(--pico-muted-color);
|
#events-legend {
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* event list */
|
/* event list */
|
||||||
.event-info {
|
.events {
|
||||||
vertical-align: top;
|
.tr-muted {
|
||||||
max-width: 240px;
|
color: var(--pico-muted-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
tr {
|
||||||
|
background: linear-gradient(100deg, color-mix(in srgb, var(--pico-background-color) 95%, var(--pico-contrast)), 25%, var(--pico-background-color));
|
||||||
|
|
||||||
|
td {
|
||||||
|
background: unset;
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
tr.babelsberger-lesesalon {
|
||||||
|
background: linear-gradient(100deg, var(--tr-events-color-bls), 25%, var(--pico-background-color));
|
||||||
|
}
|
||||||
|
|
||||||
|
tr.andere-welten {
|
||||||
|
background: linear-gradient(100deg, var(--tr-events-color-paw), 25%, var(--pico-background-color));
|
||||||
|
}
|
||||||
|
|
||||||
|
.event-info {
|
||||||
|
vertical-align: top;
|
||||||
|
max-width: 240px;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.event-detail {
|
#content-body .event-detail {
|
||||||
|
height: 100%;
|
||||||
vertical-align: top;
|
vertical-align: top;
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: nowrap;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: stretch;
|
||||||
|
gap: var(--pico-block-spacing-horizontal);
|
||||||
|
|
||||||
|
|
||||||
|
img.event-flyer {
|
||||||
|
max-height: calc(var(--tr-card-height) * .666);
|
||||||
|
max-width: calc(var(--tr-smallest-width) * .666);
|
||||||
|
min-width: unset;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
span.babelsberger-lesesalon {
|
||||||
|
background: linear-gradient(100deg, color-mix(in srgb, var(--tr-events-color-bls) 75%, transparent), 75%, var(--pico-background-color));
|
||||||
|
}
|
||||||
|
|
||||||
|
span.andere-welten {
|
||||||
|
background: linear-gradient(100deg, color-mix(in srgb, var(--tr-events-color-paw) 75%, transparent), 75%, var(--pico-background-color));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/* flex settings for layout of cards */
|
/* flex settings for layout of cards */
|
||||||
.cards {
|
.cards {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|||||||
@ -25,23 +25,34 @@
|
|||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th class="event-info">Wann & Wo</th>
|
<th class="event-info">Wann & Wo</th>
|
||||||
<th class="event-detail">Was & Wieso</th>
|
<th col-span="2" class="event-detail">Was & Wieso</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{# loop over all events that have not started yet #}
|
{# loop over all events that have not started yet #}
|
||||||
{% for t in page.termine | selectattr("startdate", ">=", DATETIME_NOW) %}
|
{% for t in page.termine | selectattr("startdate", ">=", DATETIME_NOW) %}
|
||||||
<tr>
|
<tr{% if "Babelsberger Lesesalon" in t.categories %} class="babelsberger-lesesalon"{% elif "Andere Welten" in t.categories %} class="andere-welten"{% endif %}>
|
||||||
<td class="event-info"><strong>{{ t.startdate | strftime(date_format) }}{% if t.enddate %}–{{t.enddate | strftime(date_format) }}{% elif t.starttime %} {{ t.starttime | strftime(time_format) }}{% endif %}</strong><br>{{ t.location }}</td>
|
<td class="event-info">
|
||||||
|
<strong>{{ t.startdate | strftime(date_format) }}{% if t.enddate %}–{{t.enddate | strftime(date_format) }}{% elif t.starttime %} {{ t.starttime | strftime(time_format) }}{% endif %}</strong><br>{{ t.location }}
|
||||||
|
</td>
|
||||||
<td class="event-detail">
|
<td class="event-detail">
|
||||||
<p>{{ t.summary }} {% if "Moderation" in t.categories %}(Moderation){%endif%}</p>
|
<div>
|
||||||
{% if t.description %}<p>{{ t.description | replace("\n\n", "</p><p>") | replace("\n", "<br>") | replace("</p><br><p>", "</p><p>")}}</p>{% endif %}
|
<p>{{ t.summary }} {% if "Moderation" in t.categories %}(Moderation){%endif%}</p>
|
||||||
{% if t.attach %}<p><a href="{{ t.attach }}" target="_blank" title="siehe auch …">Mehr Infos</a></p>{% endif %}
|
{% if t.description %}<p>{{ t.description | replace("\n\n", "</p><p>") | replace("\n", "<br>") | replace("</p><br><p>", "</p><p>")}}</p>{% endif %}
|
||||||
|
{% if t.link %}<p><a href="{{ t.link }}" target="_blank" title="siehe auch …">Mehr Infos</a></p>{% endif %}
|
||||||
|
</div>
|
||||||
|
{% if t.image %}<a href="{{ t.image }}" target="_blank"><img class="event-flyer" src="{{ t.image }}" alt="siehe auch …"></a> {% endif %}
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
<div id="events-legend">
|
||||||
|
<p>Regelmäßige Veranstaltungsreihen:
|
||||||
|
<span class="babelsberger-lesesalon"> Babelsberger Lesesalon </span>
|
||||||
|
<span class="andere-welten"> Andere Welten </span>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endblock content_body %}
|
{% endblock content_body %}
|
||||||
|
|
||||||
|
|||||||
@ -7,6 +7,7 @@ import vobject
|
|||||||
from datetime import datetime, date, time, timedelta
|
from datetime import datetime, date, time, timedelta
|
||||||
import yaml
|
import yaml
|
||||||
import os, os.path
|
import os, os.path
|
||||||
|
from urllib.parse import urlparse
|
||||||
|
|
||||||
|
|
||||||
# find out where events.ini lives; that's the project root dir
|
# find out where events.ini lives; that's the project root dir
|
||||||
@ -58,19 +59,23 @@ cal_prop_datetimes = ["dtend", "due", "dtstart", "duration", "dtstamp", "last-mo
|
|||||||
# Current time as an aware datetime.datetime object.
|
# Current time as an aware datetime.datetime object.
|
||||||
now = datetime.now().astimezone()
|
now = datetime.now().astimezone()
|
||||||
|
|
||||||
|
image_suffixes = ["jpg", "jpeg", "png", "webp", "gif"]
|
||||||
|
internal_hosts = ["", "localhost", "127.0.0.1", "127.0.1.1"]
|
||||||
|
image_link_prefix = "../images/termine/"
|
||||||
|
|
||||||
|
|
||||||
"""Returns an aware datetime.datetime object based on the given datetime or date. Conversion happens only if necessary, i.e. if d is of type datetime.date or a naive datetime.datetime object."""
|
"""Returns an aware datetime.datetime object based on the given datetime or date. Conversion happens only if necessary, i.e. if d is of type datetime.date or a naive datetime.datetime object."""
|
||||||
def fixDatetime(d, t=None, tz=None):
|
def fixDatetime(d: datetime | date, t=None, tz=None):
|
||||||
if not tz:
|
if not tz:
|
||||||
tz = now.tzinfo
|
tz = now.tzinfo
|
||||||
|
|
||||||
# datetime object is made aware if necessary
|
# datetime object is made aware if necessary
|
||||||
if type(d) == datetime:
|
if type(d) == datetime:
|
||||||
if d.tzinfo:
|
if d.tzinfo:
|
||||||
return d
|
return d
|
||||||
else:
|
else:
|
||||||
return datetime(d.date(), d.time(), tz)
|
return datetime(d.date(), d.time(), tz)
|
||||||
|
|
||||||
# raise error if d is neither datetime nor date
|
# raise error if d is neither datetime nor date
|
||||||
elif type(d) != date:
|
elif type(d) != date:
|
||||||
raise TypeError("parameter must be a datetime.date or datetime.datetime object")
|
raise TypeError("parameter must be a datetime.date or datetime.datetime object")
|
||||||
@ -84,7 +89,7 @@ def fixDatetime(d, t=None, tz=None):
|
|||||||
|
|
||||||
|
|
||||||
"""Takes a list of events in vobject representation and converts each event into a dict of Python datatypes."""
|
"""Takes a list of events in vobject representation and converts each event into a dict of Python datatypes."""
|
||||||
def extractEventData(events):
|
def extractEventData(events: list) -> list:
|
||||||
e_tmp = []
|
e_tmp = []
|
||||||
|
|
||||||
# iterate over events
|
# iterate over events
|
||||||
@ -96,17 +101,58 @@ def extractEventData(events):
|
|||||||
# we're only interested in a subset of properties
|
# we're only interested in a subset of properties
|
||||||
if not k in cal_properties:
|
if not k in cal_properties:
|
||||||
continue
|
continue
|
||||||
|
# print(f"k is {k} and v is {v}") #DEBUG
|
||||||
|
|
||||||
# Usually, v is a list with a single item. We only need the item's value attribute
|
# Usually, v is a list with a single item. We only need the item's value attribute.
|
||||||
if len(v) == 1:
|
if len(v) == 1:
|
||||||
d[k] = v[0].value
|
d[k] = v[0].value
|
||||||
# but sometimes the list has more than one item; this only ever happens for the property "categories"
|
continue
|
||||||
# and in this case, each list item's value is itself a one item list (which is stupid but that's how vobject handles categories …)
|
|
||||||
else:
|
# but sometimes v has more than one item
|
||||||
|
# special case: if k is "categories", each list item's value is itself a one item list (which is stupid but that's how vobject handles categories)
|
||||||
|
if k == "categories":
|
||||||
d[k] = [v[i].value[0] for i in range(len(v))]
|
d[k] = [v[i].value[0] for i in range(len(v))]
|
||||||
|
# all other cases: extract each list item's value as is
|
||||||
|
else:
|
||||||
|
d[k] = [v[i].value for i in range(len(v))]
|
||||||
e_tmp.append(d)
|
e_tmp.append(d)
|
||||||
return e_tmp
|
return e_tmp
|
||||||
|
|
||||||
|
|
||||||
|
def handleAttachments(events: list) -> list:
|
||||||
|
for event in events:
|
||||||
|
# skip events without attachment(s)
|
||||||
|
if "attach" not in event.keys():
|
||||||
|
continue
|
||||||
|
|
||||||
|
# turn single attachment into a single element list
|
||||||
|
if type(event["attach"]) == str:
|
||||||
|
event["attach"] = [event["attach"]]
|
||||||
|
|
||||||
|
# loop over attachments
|
||||||
|
for attachment in event["attach"]:
|
||||||
|
parsed_url = urlparse(attachment)
|
||||||
|
isImage = True if parsed_url.path.split(".")[-1] in image_suffixes else False
|
||||||
|
# print(f"attachment is {attachment}, parsed_url is {parsed_url} and isImage is {isImage}") #DEBUG
|
||||||
|
|
||||||
|
# handle image link
|
||||||
|
if isImage:
|
||||||
|
# make internal link relative to "/termine"
|
||||||
|
if parsed_url.hostname in internal_hosts:
|
||||||
|
# isolate filename
|
||||||
|
filename = parsed_url.path.split("/")[-1]
|
||||||
|
# prefix filename with path and add it to the event as "image" property
|
||||||
|
event["image"] = image_link_prefix + filename
|
||||||
|
# use external link as is
|
||||||
|
else:
|
||||||
|
event["image"] = attachment
|
||||||
|
# handle non-image link
|
||||||
|
else:
|
||||||
|
event["link"] = attachment
|
||||||
|
|
||||||
|
# print(event) #DEBUG
|
||||||
|
return events
|
||||||
|
|
||||||
# create vobject calendar
|
# create vobject calendar
|
||||||
vcal = vobject.newFromBehavior("vcalendar")
|
vcal = vobject.newFromBehavior("vcalendar")
|
||||||
|
|
||||||
@ -139,6 +185,9 @@ for e in events:
|
|||||||
# keep only future events
|
# keep only future events
|
||||||
events = [e for e in events if e["dtstart"] >= now]
|
events = [e for e in events if e["dtstart"] >= now]
|
||||||
|
|
||||||
|
# sort attached links into page links and image links
|
||||||
|
events = handleAttachments(events)
|
||||||
|
|
||||||
# sort by start date
|
# sort by start date
|
||||||
events.sort(key=lambda e: e["dtstart"])
|
events.sort(key=lambda e: e["dtstart"])
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user