From b33d3aecbb39067f08dbd796ee8cb1efe5886860 Mon Sep 17 00:00:00 2001 From: eclipse Date: Mon, 17 Feb 2025 10:04:01 +0100 Subject: [PATCH] termine.md now gets default data; also lots of bugfixes around dates and times --- content/pages/termine.md.metadata | 9 +++ pelicanconf.py | 6 +- utils/refresh-events.py | 108 +++++++++++++++++++++++------- 3 files changed, 99 insertions(+), 24 deletions(-) create mode 100644 content/pages/termine.md.metadata diff --git a/content/pages/termine.md.metadata b/content/pages/termine.md.metadata new file mode 100644 index 0000000..1ad4c95 --- /dev/null +++ b/content/pages/termine.md.metadata @@ -0,0 +1,9 @@ +title: Termine +author: Tobias Radloff +summary: Lesungen und Veranstaltungen +template: termine.noformat +lang: de +category: Termine +slug: termine +url: termine/ +save_as: termine/index.html diff --git a/pelicanconf.py b/pelicanconf.py index c06d405..ad7ad12 100644 --- a/pelicanconf.py +++ b/pelicanconf.py @@ -8,6 +8,10 @@ SITESUBTITLE = "Schriftsteller" SITEURL = "" TIMEZONE = 'Europe/Berlin' +DATE_FORMATS = { + 'de': '%a, %d. %b %y', + 'en': '%Y-%m-%d(%a)', +} DEFAULT_LANG = 'de' THEME = "theme/" @@ -22,7 +26,7 @@ PAGE_PATHS = ["pages"] STATIC_PATHS = ["images", "favicon"] DIRECT_TEMPLATES = ['index', 'tags'] -IGNORE_FILES = ['**/.*', '__pycache__', 'favicon-from-svg.sh'] +IGNORE_FILES = ['**/.*', '__pycache__', 'favicon-from-svg.sh', '*.metadata'] EXTRA_PATH_METADATA = { 'favicon/favicon.ico': {'path': 'favicon.ico'}, diff --git a/utils/refresh-events.py b/utils/refresh-events.py index 0a0feca..d4ee99e 100755 --- a/utils/refresh-events.py +++ b/utils/refresh-events.py @@ -1,15 +1,25 @@ import caldav import vobject -from datetime import datetime, date, time +from datetime import datetime, date, time, timedelta import yaml +import os +import locale +import threading +from contextlib import contextmanager + + +"""CalDAV server url""" server_url = "https://***REMOVED*** -cal_user = "tobias" +"""CalDAV calendar url""" cal_url = "https://***REMOVED*** +"""CalDAV username""" +cal_user = "tobias" +"""Category to filter events for. Only events that belong to this category will be processed.""" cal_category = "Veranstaltung" +"""Name of the file the YAML data will be writte to.""" result_file = "../content/pages/termine.md" -now = datetime.now().astimezone() - +"""List of icalendar properties that will be passed on to Pelican. At this time, all properties are used except those regarding alarms.""" cal_properties = [ # taken from ical specification # descriptive "attach", "categories", "class", "comment", "description", "geo", "location", "percent-complete", "priority", "resources", "status", "summary", @@ -26,10 +36,13 @@ cal_properties = [ # taken from ical specification # change management "created", "dtstamp", "last-modified", "sequence" ] +"""List of icalendar properties that the vobject passes as either datetime.datetime or datetime.date objects. To be able to compare any two objects, the script makes each one aware and/or converts into datetime.datetime if necessary. Default timezone is the system timezone; default time is 23:59:59.""" cal_prop_datetimes = ["dtend", "due", "dtstart", "duration", "dtstamp", "last-modified"] +"""Current time as an aware datetime.datetime object.""" +now = datetime.now().astimezone() -"""Converts a datetime.datetime or datetime.date object into an aware 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): if not tz: tz = now.tzinfo @@ -53,6 +66,30 @@ def fixDatetime(d, t=None, tz=None): return datetime.combine(d, t) +"""Takes a list of events in vobject representation and converts each event into a dict of Python datatypes.""" +def extractEventData(events): + e_tmp = [] + + # iterate over events + for e in events: + d = {} + + # iterate over event properties + for k, v in e.contents.items(): + # we're only interested in a subset of properties + if not k in cal_properties: + continue + + # 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: + d[k] = [v[i].value[0] for i in range(len(v))] + e_tmp.append(d) + return e_tmp + # get password with open(".caldav_pass", "r") as f: cal_pass = f.read().strip() @@ -73,29 +110,17 @@ with caldav.DAVClient(url=server_url, username=cal_user, password=cal_pass) as d # we only want events belonging to a specific category events = [e for e in vcal.getChildren() if "categories" in e.contents.keys() and cal_category in map(lambda x: x.value[0], e.contents["categories"])] -# convert events into a structure suitable for Pelican -e_tmp = [] -for e in events: - d = {} - for k, v in e.contents.items(): - # only keep specific calendar properties - if not k in cal_properties: - continue - - # v is usually a list with a single item - if len(v) == 1: - d[k] = v[0].value - # but sometimes there's more than one item (this only ever happens when k is "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: - d[k] = [v[i].value[0] for i in range(len(v))] - e_tmp.append(d) -events = e_tmp +# extract event data +events = extractEventData(events) # fix dates and datetimes for e in events: for k in e.keys(): if k in cal_prop_datetimes: + # fix a CalDAV/vobject/icalendar (?) bug where a dtend value which is a date only (no time) automatically moves one day into the future + if k == "dtend" and type(e[k]) == date: + e[k] = e[k] - timedelta(days=1) + # fix all datetimes so they can be compared and sorted e[k] = fixDatetime(e[k]) # keep only future events @@ -104,9 +129,46 @@ events = [e for e in events if e["dtstart"] >= now] # sort by start date events.sort(key=lambda e: e["dtstart"]) +# read default metadata if present +default_metadata = None +if os.path.isfile(result_file + ".metadata"): + with open(result_file + ".metadata", "r") as f: + default_metadata = yaml.safe_load(f) + +# set up a context manager in order to threadsafely format dates and times with the German locale +# source: https://stackoverflow.com/a/24070673 +LOCALE_LOCK = threading.Lock() + +@contextmanager +def setlocale(name): + with LOCALE_LOCK: + saved = locale.setlocale(locale.LC_TIME) + try: + yield locale.setlocale(locale.LC_TIME, name) + finally: + locale.setlocale(locale.LC_TIME, saved) + +# temporary set locale to de_DE (category "time" only) and format start date as well as start time or end date +with setlocale('de_DE.UTF-8'): + for e in events: + # start date + e["startdate"] = e["dtstart"].strftime("%a, %d.%m.") + # start time (if not the default time) + if e["dtstart"].timetz() != time(23, 59, 59, tzinfo=now.tzinfo): + e["starttime"] = e["dtstart"].strftime("%H:%M Uhr") + # end date (if different from start date) + if e["dtstart"].date() < e["dtend"].date(): + e["enddate"] = e["dtend"].date().strftime("%a, %d.%m.") + + # add current date to default metadata while we're at it + if default_metadata: + default_metadata["date"] = now.strftime("%Y-%m-%d %H:%M") + # write data as YAML with open(result_file, 'w') as f: f.write("---\n") + if default_metadata: + yaml.dump(default_metadata, f) yaml.dump({"termine": events}, f) f.write("---\n") f.write("written at " + datetime.today().isoformat(" "))