diff --git a/content/images/termine/bls2526.jpg b/content/images/termine/bls2526.jpg new file mode 100644 index 0000000..d0f9d3d Binary files /dev/null and b/content/images/termine/bls2526.jpg differ diff --git a/theme/static/css/custom.css b/theme/static/css/custom.css index fc3bb67..c668b31 100644 --- a/theme/static/css/custom.css +++ b/theme/static/css/custom.css @@ -6,6 +6,9 @@ --tr-smallest-width: 350px; --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, Oxygen, Ubuntu, Cantarell, Helvetica, Arial, "Helvetica Neue", sans-serif, var(--pico-font-family-emoji); @@ -388,16 +391,42 @@ article form { color: var(--pico-muted-color); } +#events-legend { + width: 100%; + display: flex; + justify-content: center; +} + +span.babelsberger-lesesalon, .babelsberger-lesesalon .event-info { + background-color: var(--tr-events-color-bls); +} + +span.andere-welten, .andere-welten .event-info { + background-color: var(--tr-events-color-paw); +} + /* event list */ .event-info { vertical-align: top; max-width: 240px; } -.event-detail { +#content-body .event-detail { vertical-align: top; + display: flex; + flex-wrap: nowrap; + justify-content: space-between; + align-items: flex-start; + 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; + } } + /* flex settings for layout of cards */ .cards { display: flex; diff --git a/theme/templates/termine.html b/theme/templates/termine.html index 9cca6b3..170810e 100644 --- a/theme/templates/termine.html +++ b/theme/templates/termine.html @@ -25,23 +25,34 @@ Wann & Wo - Was & Wieso + Was & Wieso {# loop over all events that have not started yet #} {% for t in page.termine | selectattr("startdate", ">=", DATETIME_NOW) %} - - {{ t.startdate | strftime(date_format) }}{% if t.enddate %}–{{t.enddate | strftime(date_format) }}{% elif t.starttime %} {{ t.starttime | strftime(time_format) }}{% endif %}
{{ t.location }} + + + {{ t.startdate | strftime(date_format) }}{% if t.enddate %}–{{t.enddate | strftime(date_format) }}{% elif t.starttime %} {{ t.starttime | strftime(time_format) }}{% endif %}
{{ t.location }} + -

{{ t.summary }} {% if "Moderation" in t.categories %}(Moderation){%endif%}

- {% if t.description %}

{{ t.description | replace("\n\n", "

") | replace("\n", "
") | replace("


", "

")}}

{% endif %} - {% if t.attach %}

Mehr Infos

{% endif %} +
+

{{ t.summary }} {% if "Moderation" in t.categories %}(Moderation){%endif%}

+ {% if t.description %}

{{ t.description | replace("\n\n", "

") | replace("\n", "
") | replace("


", "

")}}

{% endif %} + {% if t.link %}

Mehr Infos

{% endif %} +
+ {% if t.image %}siehe auch … {% endif %} {% endfor %} +
+

Regelmäßige Veranstaltungsreihen: +  Babelsberger Lesesalon   +  Andere Welten  +

+
{% endblock content_body %} diff --git a/utils/refresh_events.py b/utils/refresh_events.py index b5925d6..afb30c3 100755 --- a/utils/refresh_events.py +++ b/utils/refresh_events.py @@ -7,6 +7,7 @@ import vobject from datetime import datetime, date, time, timedelta import yaml import os, os.path +from urllib.parse import urlparse # find out where events.ini lives; that's the project root dir @@ -58,19 +59,22 @@ cal_prop_datetimes = ["dtend", "due", "dtstart", "duration", "dtstamp", "last-mo # Current time as an aware datetime.datetime object. now = datetime.now().astimezone() +image_suffixes = ["jpg", "jpeg", "png", "webp", "gif"] +internal_hosts = ["", "localhost", "127.0.0.1", "127.0.1.1", "tobias-radloff.de", "www.tobias-radloff.de"] + """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: tz = now.tzinfo - + # datetime object is made aware if necessary if type(d) == datetime: if d.tzinfo: return d - else: + else: return datetime(d.date(), d.time(), tz) - + # raise error if d is neither datetime nor date elif type(d) != date: raise TypeError("parameter must be a datetime.date or datetime.datetime object") @@ -84,7 +88,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.""" -def extractEventData(events): +def extractEventData(events: list) -> list: e_tmp = [] # iterate over events @@ -96,17 +100,59 @@ def extractEventData(events): # we're only interested in a subset of properties if not k in cal_properties: 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: d[k] = v[0].value - # but sometimes the list has more than one item; this only ever happens for the property "categories" - # 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: + continue + + # 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))] + # 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) 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: + # make sure path starts with a slash + if parsed_url.path[0] != "/": + parsed_url.path = "/" + parsed_url.path + # prefix path with ".." and add it to the event as "image" property + event["image"] = ".." + parsed_url.path + # use external link as is + else: + event["image"] = attachment + # handle non-image link + else: + event["link"] = attachment + + print(event) + return events + # create vobject calendar vcal = vobject.newFromBehavior("vcalendar") @@ -139,6 +185,9 @@ for e in events: # keep only future events 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 events.sort(key=lambda e: e["dtstart"])