diff --git a/.gitignore b/.gitignore index b3660fe..bf30007 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,4 @@ outputCalendar.svg +*.conf +.idea +templates diff --git a/README.md b/README.md index 8338f52..da09349 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,47 @@ -CRCR -==== - -FSI CE's version of the CalendrCreatr for fast creation of beautiful calendars -in svg format. +# CRCR +CalendrCreatr is a python script for fast creation of simple-styled calendars in svg format. This is a fork from t-animal/CRCR. -This software is released under CC-BY-SA-NC 3 +## Features +- Automatic Import of german holidays if `holidays` package is installed. +- Creates a calendar for any period of 12 months +- Write periodic dates in the calendar + +## Quick Execution Guide + +On Linux: +``` +python -m venv ./venv +source venv/bin/activate +# following line optional +pip install holidays +python calendrcreatr.py ./calendar_2024_2025_temp.conf +``` + +On Windows: +``` +python -m venv .\venv +.\venv\bin\activate +# following line optional +pip install holidays +python calendrcreatr.py .\calendar_2024_2025_temp.conf +``` + +## Config Structure + +See `calendar_2024_2025_temp.conf` for an example of a config file. + +Empty lines and lines starting with `#` are ignored. +Single dates can be given in den format `DD.MM.YYYY`, a range can be also given by `DD.MM.YYYY-DD.MM.YYYY`. +Weekly dates can be written by `DD.MM.YYYY~1w~DD.MM.YYYY`. The given number can be adjusted to `2` for biweekly schedules. + +## License + +This software is released under CC-BY-SA-NC 3. (yes I know that this is technically not a software license. So use it for personal use, ask if you want to make money from that and retain a notice of this license. Use your common sense.) + +### Contributors +- t-animal +- FS CE +- Timm638 \ No newline at end of file diff --git a/calendar.conf b/calendar.conf deleted file mode 100644 index d8a313f..0000000 --- a/calendar.conf +++ /dev/null @@ -1,24 +0,0 @@ -#beispielconfig, zeilen mit # am anfang sind kommentare -#leitet nur am anfang der zeile einen kommentar ein -# -#die erste datenzeile muss das jahr des kalender beinhalten -2013 -#dann durch kommata getrennt die wochen der stammtisch -#fuer keinen stammtisch hier eine null eintragen -2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41, 43, 47, 53 -#die termine der ferien als komma separierte liste -#im format [d]d.[m]m-[d]d.[m]m,... -#wenn keine ferien gewuenscht sind hier 0.0-0.0 eintragen -21.7-14.10,10.2-31.3,24.12-31.12 -#weitere freie tage als komma separierte liste -#wenn keine weiteren freien tage gewuenscht hier 0.0 eintragen -20.5, 21.5, 1.5, 9.5, 20.5, 30.5, 1.11 -#besonders zu markierende tage wie geburtstage -#im format [d]d.[m]m,name,css-klasse -#ab hier zeilenweise -11.7,CalCreatr,birthday -14.3,einstein,birthday -30.11,ben stiller,birthday -30.11,billy idol,birthday -30.11,ridley scott,birthday -15.4,lincoln,todestag diff --git a/calendar_2024_2025_temp.conf b/calendar_2024_2025_temp.conf new file mode 100644 index 0000000..4fa1e90 --- /dev/null +++ b/calendar_2024_2025_temp.conf @@ -0,0 +1,78 @@ +# Config-Datei für Kalenderskript + +# Title +FSI-Kalender WS 2024 / SS 2025 + +# Startmonat (inklusiv) +# MM / YYYY +10/2024 +# Endmonat (inklusiv) +# MM / YYYY +09/2025 + +# Vorlesungezeiten: +# DD.MM.YYYY-DD.MM.YYYY, ... +13.10.2024-07.02.2025, 23.04.2025-25.07.2025 + +# Vorlesungsfreie Zeiten +# DD.MM.YYYY-DD.MM.YYYY, ... +26.12.2024-06.01.2025 + +# Prüfungszeiträume: +# DD.MM.YYYY-DD.MM.YYYY, ... +10.02.2025-22.02.2025, 28.03.2025-17.04.2025, 28.07.2025-09.08.2025, 19.09.2025-12.10.2025 + +# Sondertage, Keine Kommata im Namen: +# DD.MM, ,birthday +01.01,Hans,birthday +# TODO: Hier weitere Geburtage eintragen + +# Feiertage +# Werden automatisch eingepflegt durchs Skript +# 18.04.2025,Karfreitag,feiertag +# 21.04.2025,Ostermontag,feiertag +# 29.05.2025,Christi Himmelfahrt,feiertag +# 20.05.2025,Pfingstmontag,feiertag +# 19.06.2025,Fronleichnam,feiertag +# same day every year +# 01.01,Neujahr,feiertag +# 06.01,Heilige Drei Könige,feiertag +# 01.05,Tag der Arbeit,feiertag +# 15.08,Mariä Himmelfahrt,feiertag +# 03.10,Deutsche Einheit,feiertag +# 01.11,Allerheiligen,feiertag +# 25.12,1. Weihnachtstag,feiertag +# 26.12,2. Weihnachtstag,feiertag + +# Nerd-Feiertage +# same day every year +# DD.MM, ,loscher +12.03,GNU Terry Pratchett,loscher +14.03,Pi Day,loscher +25.05,Towel Day,loscher +28.06,Tau Day,loscher + +# Uni-Events +# TODO: TdI 2025 +# DD.MM, ,sonstiges +04.11.2024,Dies Academicus,sonstiges +# TODO: Schlossgartenfest 2025 +14.10.2024,TFGDT,sonstiges +05.05.2025-16.05.2025,Berch,sonstiges + +# Loscher +# DD.MM.YYYY~2w~DD.MM.YYYY bedeutet alle zwei Wochen beginnend vom Startdatum bis zum Enddatum +12.01.2024~2w~31.12.2024,Loscher,loscher +10.01.2025~2w~31.12.2025,Loscher,loscher + +# Semesterbeitrag +01.07.2024-07.07.2024,72€,sosonstiges +# DD.MM, ,sonstigesnstiges +05.02.2025-09.02.2025,72€,sonstiges + +# KIF +17.10.2024-21.10.2024,KIF 52.5,sonstiges +18.06.2025-22.06.2025,KIF 53.0,sonstiges + +# CCC +27.12.2024-30.12.2024,CCC,sonstiges diff --git a/calendar_ce.conf b/calendar_ce.conf deleted file mode 100644 index c181c17..0000000 --- a/calendar_ce.conf +++ /dev/null @@ -1,183 +0,0 @@ -#beispielconfig, zeilen mit # am anfang sind kommentare -#leitet nur am anfang der zeile einen kommentar ein -# -#die erste datenzeile muss das jahr des kalender beinhalten -2016 -#dann durch kommata getrennt die wochen der stammtisch -#fuer keinen stammtisch hier eine null eintragen -0 -#dann durch kommata getrennt die wochen der loscher lieferungen -#fuer keine lieferung hier eine null eintragen -1,3,5,7,9,11,13,15,17,19,21,23,25,27,29,31,33,35,37,39,41,43,45,47,49,51 -#die termine der ferien als komma separierte liste -#im format [d]d.[m]m-[d]d.[m]m,... -#wenn keine ferien gewuenscht sind hier 0.0-0.0 eintragen -6.2-10.4,17.7-16.10,1.1-6.1,24.12-31.12 -#weitere freie tage als komma separierte liste -#wenn keine weiteren freien tage gewuenscht hier 0.0 eintragen -0.0 -#besonders zu markierende tage wie geburtstage -#im format [d]d.[m]m,name,css-klasse -#ab hier zeilenweise - -12.5,Berg,berg -13.5,Berg,berg -14.5,Berg,berg -15.5,Berg,berg -16.5,Berg,berg -17.5,Berg,berg -18.5,Berg,berg -19.5,Berg,berg -20.5,Berg,berg -21.5,Berg,berg -22.5,Berg,berg -23.5,Berg,berg - -#16.4.stammtisch,stammtisch -#13.5.stammtisch,stammtisch -#9.6.stammtisch,stammtisch -#9.7.stammtisch,stammtisch - -1.1,Neujahr,feiertag -6.1,HL 3 Könige,feiertag -10.2,Aschermittwoch, -14.2,Valentinstag,sonstiges -24.3,Gründonnerstag, -25.3,Karfreitag,feiertag -27.3,Ostern, -28.3,Ostern,feiertag -1.5,Tag der Arbeit,feiertag -5.5,Chr Himmelfahrt,feiertag -15.5,Pfingsten,feiertag -16.5,Pfingsten,feiertag -17.5,Berg frei,feiertag -26.5,Fronleichnam,feiertag -2.10,Erntedankfest, -3.10,Tag der dt Einheit,feiertag -31.10,Reformationstag, -1.11,Allerheiligen,feiertag -13.11,Volkstrauertag, -16.11,Buß- und Bettag, -20.11,Totensonntag, -27.11,1ter Advent, -4.12,2ter Advent, -6.12,Nikolaus, -11.12,3ter Advent, -18.12,4ter Advent, -24.12,HL Abend, -25.12,Weihnachten,feiertag -26.12,Weihnachten,feiertag -31.12,Silvester, - -1.2,Rückmeldung,sonstiges -2.2,Rückmeldung,sonstiges -3.2,Rückmeldung,sonstiges -4.2,Rückmeldung,sonstiges -5.2,Rückmeldung,sonstiges - -1.7,Rückmeldung,sonstiges -2.7,Rückmeldung,sonstiges -3.7,Rückmeldung,sonstiges -4.7,Rückmeldung,sonstiges -5.7,Rückmeldung,sonstiges -6.7,Rückmeldung,sonstiges -7.7,Rückmeldung,sonstiges -8.7,Rückmeldung,sonstiges - -8.2,Prüfungszeitraum,pruefung -9.2,Prüfungszeitraum,pruefung -10.2,Prüfungszeitraum,pruefung -11.2,Prüfungszeitraum,pruefung -12.2,Prüfungszeitraum,pruefung -13.2,Prüfungszeitraum,pruefung -14.2,Prüfungszeitraum,pruefung -15.2,Prüfungszeitraum,pruefung -16.2,Prüfungszeitraum,pruefung -17.2,Prüfungszeitraum,pruefung -18.2,Prüfungszeitraum,pruefung -19.2,Prüfungszeitraum,pruefung -20.2,Prüfungszeitraum,pruefung - -17.3,Prüfungszeitraum,pruefung -18.3,Prüfungszeitraum,pruefung -19.3,Prüfungszeitraum,pruefung -20.3,Prüfungszeitraum,pruefung -21.3,Prüfungszeitraum,pruefung -22.3,Prüfungszeitraum,pruefung -23.3,Prüfungszeitraum,pruefung -24.3,Prüfungszeitraum,pruefung -#25.3,Prüfungszeitraum,pruefung -#26.3,Prüfungszeitraum,pruefung -#27.3,Prüfungszeitraum,pruefung -#28.3,Prüfungszeitraum,pruefung -29.3,Prüfungszeitraum,pruefung -30.3,Prüfungszeitraum,pruefung -31.3,Prüfungszeitraum,pruefung -1.4,Prüfungszeitraum,pruefung -2.4,Prüfungszeitraum,pruefung -3.4,Prüfungszeitraum,pruefung -4.4,Prüfungszeitraum,pruefung -5.4,Prüfungszeitraum,pruefung -6.4,Prüfungszeitraum,pruefung -7.4,Prüfungszeitraum,pruefung -8.4,Prüfungszeitraum,pruefung -9.4,Prüfungszeitraum,pruefung - -18.7,Prüfungszeitraum,pruefung -19.7,Prüfungszeitraum,pruefung -20.7,Prüfungszeitraum,pruefung -21.7,Prüfungszeitraum,pruefung -22.7,Prüfungszeitraum,pruefung -23.7,Prüfungszeitraum,pruefung -24.7,Prüfungszeitraum,pruefung -25.7,Prüfungszeitraum,pruefung -26.7,Prüfungszeitraum,pruefung -27.7,Prüfungszeitraum,pruefung -28.7,Prüfungszeitraum,pruefung -29.7,Prüfungszeitraum,pruefung -30.7,Prüfungszeitraum,pruefung - -30.9,Prüfungszeitraum,pruefung -1.10,Prüfungszeitraum,pruefung -2.10,Prüfungszeitraum,pruefung -#3.10,Prüfungszeitraum,pruefungs -4.10,Prüfungszeitraum,pruefung -5.10,Prüfungszeitraum,pruefung -6.10,Prüfungszeitraum,pruefung -7.10,Prüfungszeitraum,pruefung -8.10,Prüfungszeitraum,pruefung -9.10,Prüfungszeitraum,pruefung -10.10,Prüfungszeitraum,pruefung -11.10,Prüfungszeitraum,pruefung -12.10,Prüfungszeitraum,pruefung -13.10,Prüfungszeitraum,pruefung -14.10,Prüfungszeitraum,pruefung -15.10,Prüfungszeitraum,pruefung - -#18.5,beginn,sonstiges -#18.5,prüfungsanmeldung,sonstiges -#19.5, ,sonstiges -#20.5, ,sonstiges -#2.6, ,sonstiges -#3.6, ,sonstiges -#4.6, ,sonstiges -#5.6,ende,sonstiges -#5.6,prüfungsanmeldung,sonstiges - -17.3,Schülerinfotag,fsi -5.11,50 Jahre TechFak,sonstiges -30.6,TechFak Sommer,sonstiges -30.6,Fest,sonstiges -15.4.Tag der Informatik,sonstiges -17.10,ESE,fsi -17.10,TechFak goes DT,sonstiges -11.4,TechFak goes DT,sonstiges -#30.6,hochschulwahl,sonstiges -#18.7.journey,sonstiges -#ICMP - -27.3,Sommerzeit,sonstiges -30.10,Winterzeit,sonstiges - - -# TODO einrückung diff --git a/calendrcreatr.py b/calendrcreatr.py index e9c506f..7594386 100755 --- a/calendrcreatr.py +++ b/calendrcreatr.py @@ -3,259 +3,376 @@ import datetime import itertools import sys +import calendar +import re +import argparse +from pathlib import Path -xml.dom.minidom.Element.addClass = lambda x,y: x.setAttribute("class", x.getAttribute("class")+" "+str(y)) - -#default werte setzen -jahr=datetime.date.today().year -stammtischwochen = [2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41, 43, 47, 53] -loscherwochen = [1,3,5,7,9,11,13,15,17,19,21,23,25,27,29,31,33,35,37,39,41,43,45,47,49,51] -ferienzeitraeume = [] -freietage = [] -sondertage = [] -filename = "templates/kalender_fsce.svg" -stylefile="templates/style1.css" - -#config lesen -try: - conf = open("calendar_ce.conf", "r") - configIterator = itertools.filterfalse(lambda l:l[0]=="#" or l[0] == '\n' or len(l)==0, conf) - conf.readline = lambda:next(configIterator) - - #zeile fuer zeile einlesen, adaequat splitten und zu int konvertieren - jahr = int(conf.readline()) - stammtischwochen = list(map(int, conf.readline().split(","))) - loscherwochen = list(map(int, conf.readline().split(","))) - ferienzeitraeume = [(tuple(map(int,von.split("."))),tuple(map(int,bis.split(".")))) for von, bis in [zeitraum.split("-") for zeitraum in conf.readline().split(",")]] - freietage = [tuple(map(int,b.split("."))) for b in conf.readline().split(",")] - - #ab hier nur noch sondertage, bis StopIteration - while True: - sondertage += [((int(tag),int(monat)), titel, klasse) for (tag,monat,titel,klasse) in [conf.readline().replace("\n", "").replace(".",",").split(","),]] -except StopIteration: - pass -except Exception as e: - print("Fehler beim Einlesen der config") - print(e) - sys.exit() - - -#einige hilfsstrukturen initialisieren -monatslaenge = [31,28,31,30,31,30,31,31,30,31,30,31] -wochentage = ("montag", "dienstag", "mittwoch", "donnerstag", "freitag", "samstag", "sonntag") -wochentagoffset = datetime.date(jahr, 1, 1).weekday() - -#im schaltjahr februar +1 Tag -if ((jahr%4 == 0 and not jahr%100 == 0) or jahr%400 == 0): - monatslaenge[1] += 1 - - -#dict mit 1 Eintrag pro Tag, mit offset im jahr als key und tuple(titel, klasse) als value -sondertage2 = dict((i, []) for i in range(1,367))#367: 365 Tage+Schalttag -for (d,m),titel,klasse in sondertage: - sondertage2[sum(monatslaenge[:m-1])+d] += [(titel,klasse)] -sondertage = sondertage2 - -#einzelne freien tage eintragen -freietage = [sum(monatslaenge[:m-1])+d for (d,m) in freietage] if not freietage[0]==(0,0) else [] -#pro ferienzeitraum, jeden freien tag (range(anfang, ende+1)) eintragn -freietage += [x for ((d1,m1),(d2,m2)) in ferienzeitraeume for x in range(sum(monatslaenge[:m1-1])+d1,sum(monatslaenge[:m2-1])+d2+1)] if not ferienzeitraeume[0] == ((0,0),(0,0)) else [] - -#vorlage oeffnen -try: - image = xml.dom.minidom.parse(filename) -except IOError: - print("Konnte Vorlage nicht öffnen. Dateiname?") - sys.exit(1) - -#css eintragen try: - image.getElementsByTagName("style")[0].firstChild.replaceWholeText(open(stylefile).read()) -except IOError: - print("Konnte CSS nicht oeffnen. Huh?!") - sys.exit() - -#jahr eintragen -textElement = [t for t in image.getElementsByTagName("text") if t.getAttribute("id") == "title"] -textElement[0].firstChild.replaceWholeText("FSI-Kalender {}".format(jahr)) - -#jedes rect anschauen -for rect in image.getElementsByTagName("rect"): - if "title bar" in rect.getAttribute("class"): - rect.addClass("titlebar") - - if "day" in rect.getAttribute("class"): - id = rect.getAttribute("id") - - #tag und monat ausblenden + import holidays + + print('holidays is installed, enabling automatic import of german holidays.') + add_auto_holidays = True +except ImportError as e: + print('holidays is not installed, disabling automatic import of german holidays.') + add_auto_holidays = False + +# Constants +xml.dom.minidom.Element.addClass = lambda x, y: x.setAttribute('class', x.getAttribute('class') + ' ' + str(y)) +german_weekdays = ('montag', 'dienstag', 'mittwoch', 'donnerstag', 'freitag', 'samstag', 'sonntag') +german_months = ('Januar', 'Februar', 'März', 'April', 'Mai', 'Juni', + 'Juli', 'August', 'September', 'Oktober', 'November', 'Dezember') +filename = './templates/kalender_fsce.svg' +stylefile = './templates/style1.css' + + +class Calendar: + def __init__(self): + self.title = 'FSI-Kalender' + self.lecture_times = [] + self.exam_times = [] + self.holidays_times = [] + self.date_to_special_day = {} + # The first month shown in the calendar + self.start_time = datetime.date(datetime.date.today().year, 1, 1) + # The last month shown in the calendar + self.end_time = datetime.date(datetime.date.today().year, 12, 31) + # How many months is this calendar offset? + self.global_offset = 0 + + def read_config(self, config: str): try: - month = int(id[0:id.find("-")]) - day = int(id[id.find("-")+1:]) - - if not 0 < month < 13 or not 0 < day < 32: - raise ValueError() - except ValueError: - print("Warning: Invalid id {}!".format(id)) - continue - - #tage, die es im monat nicht gibt, ausblenden - if day > monatslaenge[month-1]: - rect.addClass("invisible") - rect.nextSibling.nextSibling.addClass("invisible") - continue - - tagImJahr = sum(monatslaenge[:month-1])+day - wochentag = (tagImJahr + wochentagoffset)%7 - rect.setAttribute("weekday", wochentage[wochentag-1]) - - #wochennummer einfuegen - offset = 0 - if wochentag == 1: - offset = 15 #15=1/2 fontsize - rect.parentNode.insertBefore(xml.dom.minidom.parseString("""{}""" - .format(month, - day, - float(rect.getAttribute("x"))+16, - float(rect.getAttribute("y"))+float(rect.getAttribute("height"))/2+10-offset, - int(tagImJahr/7)+1)).firstChild, rect.nextSibling) - - #stammtischwochen - if wochentag == 2 and int(tagImJahr/7+1) in stammtischwochen: - rect.addClass("trunktable") - - #loscherwochen - if wochentag == 5 and int(tagImJahr/7+1) in loscherwochen: - rect.nextSibling.nextSibling.addClass("loscher") - - #ferien und feiertage faerben - if tagImJahr in freietage: - rect.addClass("holiday") - - if wochentag == 0 or wochentag == 6: - rect.addClass("weekend") - - #besondere tage (geburtstage etc.) kennzeichnen - if tagImJahr in sondertage: - for i, sondertag in enumerate(sondertage[tagImJahr]): - rect.addClass(sondertag[1]) - - - if tagImJahr in sondertage: - #pro eintrag an diesem tag klasse setzen, bezeichnung einfuegen - for i, sondertag in enumerate(sondertage[tagImJahr]): - if "birthday" in sondertag[1]: - #rect.addClass(sondertag[1]) - rect.parentNode.insertBefore(xml.dom.minidom.parseString("""{}""" - .format(month, - day, - #textgroesse haengt von anzahl der eintraege ab - float(rect.getAttribute("height"))/(1.7+0.5*len(sondertage[tagImJahr])), - float(rect.getAttribute("x"))+60, - #y mit magischer formel bestimmen, die auch die anzahl der eintraege mit einbezieht - float(rect.getAttribute("y"))+float(rect.getAttribute("height"))/len(sondertage[tagImJahr])*(i+1)-0.3*(float(rect.getAttribute("height"))/(0.7+0.7*len(sondertage[tagImJahr]))), - sondertag[0])).firstChild, rect.nextSibling) - else: - if "feiertag" in rect.getAttribute("class"): - rect.addClass("weekend") - #rect.addClass(sondertag[1]) - rect.parentNode.insertBefore(xml.dom.minidom.parseString("""{}""" - .format(month, - day, - #textgroesse haengt von anzahl der eintraege ab - float(rect.getAttribute("height"))/(2.8+0.5*len(sondertage[tagImJahr])), - float(rect.getAttribute("x"))+58, - #y mit magischer formel bestimmen, die auch die anzahl der eintraege mit einbezieht - float(rect.getAttribute("y"))+float(rect.getAttribute("height"))/len(sondertage[tagImJahr])*(i+1)-0.3*(float(rect.getAttribute("height"))/(0.7+0.7*len(sondertage[tagImJahr]))), - sondertag[0])).firstChild, rect.nextSibling) - - - #wochentag einfuegen - rect.parentNode.insertBefore(xml.dom.minidom.parseString("""{}""" - .format(month, - day, - float(rect.getAttribute("x"))+16, - float(rect.getAttribute("y"))+float(rect.getAttribute("height"))/2+10+offset, - wochentage[wochentag-1][0:2])).firstChild, rect.nextSibling) - - - - if "mark" in rect.getAttribute("class"): - id = rect.getAttribute("id") - - #tag und monat ausblenden + # Ignore empty lines and comments + conf = open(config, 'r') + config_iterator = itertools.filterfalse(lambda l: l[0] == '#' or l[0] == '\n' or len(l) == 0, conf) + conf.readline = lambda: next(config_iterator) + + self.title = conf.readline().strip() + self.start_time = self.to_datetime(conf.readline().strip()) + self.end_time = self.to_datetime(conf.readline().strip()) + self.end_time.replace(day=calendar.monthrange(self.end_time.year, self.end_time.month)[1]) + self.global_offset = self.start_time.month - 1 + + self.lecture_times = self.line_to_datetimes(conf.readline().strip()) + lecture_free_times = self.line_to_datetimes(conf.readline().strip()) + self.lecture_times = [item for item in self.lecture_times if item not in lecture_free_times] + + self.exam_times = self.line_to_datetimes(conf.readline().strip()) + + # ab hier nur noch sondertage, bis StopIteration + while True: + cur_special_days = [(self.to_datetime(date, return_as_list=True), title, category) for + (date, title, category) in + [conf.readline().strip().split(',')]] + + for csd in cur_special_days: + for csd_date in csd[0]: + if self.date_to_special_day.get(csd_date) is None: + self.date_to_special_day[csd_date] = [] + self.date_to_special_day[csd_date].append((csd[1], csd[2])) + except StopIteration: + pass + except Exception as e: + print('Error while reading in the config') + print(e) + sys.exit() + + def add_holidays(self): + if add_auto_holidays: + de_holidays = holidays.country_holidays('DE', subdiv='BY', language='de') + tmp_loop_date = self.start_time + while tmp_loop_date <= self.end_time: + tmp_loop_date += datetime.timedelta(days=1) + if tmp_loop_date in de_holidays: + if self.date_to_special_day.get(tmp_loop_date) is None: + self.date_to_special_day[tmp_loop_date] = [] + self.date_to_special_day[tmp_loop_date].append((de_holidays.get(tmp_loop_date), 'feiertag')) + + def handle_sched(self, match: re.Match, return_as_list: bool = False): + output = [] + start_date = self.to_datetime(match.group('beg')) + end_date = self.to_datetime(match.group('end')) try: - month = int(id[0:id.find("-")]) - day = int(id[id.find("-")+1:]) - - if not 0 < month < 13 or not 0 < day < 32: - raise ValueError() - except ValueError: - print("Warning: Invalid id {}!".format(id)) - continue - - if day > monatslaenge[month-1]: - rect.addClass("invisible") - continue - - tagImJahr = sum(monatslaenge[:month-1])+day - - #besondere tage (geburtstage, ccc) kennzeichnen - if tagImJahr in sondertage: - for i, sondertag in enumerate(sondertage[tagImJahr]): - rect.addClass(sondertag[1]) - + sched_units = { + 'w': datetime.timedelta(days=7), + 'd': datetime.timedelta(days=1), + } + if match.group('sch_u') not in sched_units.keys(): + raise RuntimeError('Non-implemented schedule unit') + sched_unit = sched_units[match.group('sch_u')] + sched_amount = int(match.group('sch_a')) + delta = sched_unit * sched_amount + except IndexError as _: + delta = datetime.timedelta(days=1) + while start_date <= end_date: + output.append(start_date) + start_date += delta + return output + + def handle_single_date(self, match: re.Match, return_as_list: bool = False): + try: + d = int(match.group('d')) + except IndexError as _: + d = 1 + m = int(match.group('m')) - if tagImJahr in sondertage: - if "birthday" in rect.getAttribute("class"): - rect.addClass("birthdaymark") + output = [] + try: + y = int(match.group('y')) + output.append(datetime.date(y, m, d)) + except IndexError as _: + for year in range(self.start_time.year, self.end_time.year + 1): + output.append(datetime.date(year, m, d)) + + if not return_as_list and len(output) == 1: + return output[0] + else: + return output + + # Converts DD.MM, MM/YYYY, DD.MM.YYYY, DD.MM.YYYY-DD.MM.YYYY and DD.MM.YYYY~\d\w~DD.MM.YYYY to datetime objects + # If return_list is True, then return singular element as lists. + def to_datetime(self, string: str, return_as_list: bool = False): + # In order of execution, only for first match handler is called + patterns = [ + (r'(?P\d?\d\.\d?\d\.\d\d\d\d)-(?P\d\d\.\d?\d\.\d\d\d\d)', + self.handle_sched), + (r'(?P\d?\d\.\d?\d\.\d\d\d\d)~(?P\d+)(?P\w)~(?P\d?\d\.\d?\d\.\d\d\d\d)', + self.handle_sched), + (r'(?P\d?\d)\.(?P\d\d)\.(?P\d\d\d\d)', + self.handle_single_date), + (r'(?P\d?\d)\.(?P\d?\d)', + self.handle_single_date), + (r'(?P\d?\d)/(?P\d\d\d\d)', + self.handle_single_date), + ] + + for pattern in patterns: + match = re.match(pattern[0], string) + if match is not None: + return pattern[1](match, return_as_list=return_as_list) + + # Parse line of format DT-DT, DT-DT to a list of individual date times + def line_to_datetimes(self, string: str): + output = [] + ranges = string.split(',') + for entry in ranges: + res = self.to_datetime(entry.strip()) + if isinstance(res, list): + output.extend(res) else: - if "berg" in rect.getAttribute("class"): - rect.addClass("bergmark") - if "stammtisch" in rect.getAttribute("class"): - rect.addClass("stammtischmark") - if "sonstiges" in rect.getAttribute("class"): - rect.addClass("sonstigesmark") - if "fsi" in rect.getAttribute("class"): - rect.addClass("fsimark") - if "pruefung" in rect.getAttribute("class"): - rect.addClass("pruefungmark") - - if tagImJahr in freietage: - if "birthday" not in rect.getAttribute("class") and "berg" not in rect.getAttribute("class") and "stammtisch" not in rect.getAttribute("class") and "sonstiges" not in rect.getAttribute("class"): - rect.addClass("holidaymark") - - - - - - if "holidaymark" not in rect.getAttribute("class") and "birthday" not in rect.getAttribute("class") and "berg" not in rect.getAttribute("class") and "stammtisch" not in rect.getAttribute("class") and "sonstiges" not in rect.getAttribute("class"): - rect.addClass("invisible") + output.append(res) + return output + # Returns None if id-string is of a non-existing date + def date_from_id_string(self, id_string: str, offset: int = 0): + month = (int(id_string[0:id_string.find('-')]) - 1 + offset) % 12 + 1 + day = int(id_string[id_string.find('-') + 1:]) - if "frame" in rect.getAttribute("class"): - id = rect.getAttribute("id") - - #tag und monat ausblenden try: - month = int(id[0:id.find("-")]) - day = int(id[id.find("-")+1:]) - - if not 0 < month < 13 or not 0 < day < 32: - raise ValueError() + l_cur_date = datetime.date(self.start_time.year, month, day) + if l_cur_date < self.start_time: + year = self.start_time.year + 1 + else: + year = self.start_time.year + # That has to be done here because of friggn February 29th. + l_cur_date = datetime.date(year, month, day) except ValueError: - print("Warning: Invalid id {}!".format(id)) - continue - - if day > monatslaenge[month-1]: - rect.addClass("invisible") - continue + return None + return l_cur_date - rect.addClass("nofill"); - -f = open('outputCalendar.svg','w') - -image.writexml(f) -f.close() + def generate_calendar(self, template_file: str, style_file: str, outpupt_file: str): + # vorlage oeffnen + try: + image = xml.dom.minidom.parse(filename) + except IOError: + print('No .svg data found under ' + filename) + sys.exit(1) -print("Warning: When printing, convert to cmyk first (inkscape->png; convert mycal.png -colorspace cmyk mycal.jpg)") + # css eintragen + try: + image.getElementsByTagName('style')[0].firstChild.replaceWholeText(open(stylefile).read()) + except IOError: + print('Konnte CSS nicht oeffnen. Huh?!') + sys.exit() + + # Add Title + textElement = [t for t in image.getElementsByTagName('text') if t.getAttribute('id') == 'title'] + textElement[0].firstChild.replaceWholeText(self.title) + + # jedes rect anschauen + for rect in image.getElementsByTagName('rect'): + if 'title bar' in rect.getAttribute('class'): + rect.addClass('titlebar') + + if 'day' in rect.getAttribute('class'): + # tag und monat ausblenden + cur_date = self.date_from_id_string(rect.getAttribute('id'), self.global_offset) + + # tage, die es im monat nicht gibt, ausblenden + if cur_date is None: + rect.addClass('invisible') + rect.nextSibling.nextSibling.addClass('invisible') + continue + + wochentag = cur_date.weekday() + rect.setAttribute('weekday', german_weekdays[wochentag]) + + # wochennummer einfuegen + offset = 0 + if wochentag == 0: + offset = 15 # 15=1/2 fontsize + rect.parentNode.insertBefore(xml.dom.minidom.parseString( + '''{}''' + .format(cur_date.month, + cur_date.day, + float(rect.getAttribute('x')) + 16, + float(rect.getAttribute('y')) + float(rect.getAttribute('height')) / 2 + 10 - offset, + cur_date.isocalendar().week)).firstChild, rect.nextSibling) + + # ferien und feiertage faerben + if cur_date not in self.lecture_times or cur_date in self.holidays_times: + rect.addClass('holiday') + + if wochentag == 5 or wochentag == 6: + rect.addClass('weekend') + + # besondere tage (geburtstage etc.) kennzeichnen + if cur_date in self.date_to_special_day: + for i, sondertag in enumerate(self.date_to_special_day[cur_date]): + rect.addClass(sondertag[1]) + + if cur_date in self.date_to_special_day: + # pro eintrag an diesem tag klasse setzen, bezeichnung einfuegen + for i, sondertag in enumerate(self.date_to_special_day[cur_date]): + if 'birthday' in sondertag[1]: + # rect.addClass(sondertag[1]) + rect.parentNode.insertBefore(xml.dom.minidom.parseString( + '''{}''' + .format(cur_date.month, + cur_date.day, + # textgroesse haengt von anzahl der eintraege ab + float(rect.getAttribute('height')) / 2.2, + float(rect.getAttribute('x')) + 60, + # y mit magischer formel bestimmen, die auch die anzahl der eintraege mit einbezieht + float(rect.getAttribute('y')) + float(rect.getAttribute('height')) / len( + self.date_to_special_day[cur_date]) * (i + 1) - 0.3 * ( + float(rect.getAttribute('height')) / ( + 0.7 + 0.7 * len(self.date_to_special_day[cur_date]))), + sondertag[0])).firstChild, rect.nextSibling) + else: + if 'feiertag' in rect.getAttribute('class'): + rect.addClass('weekend') + # rect.addClass(sondertag[1]) + rect.parentNode.insertBefore(xml.dom.minidom.parseString( + '''{}''' + .format(cur_date.month, + cur_date.day, + # textgroesse haengt von anzahl der eintraege ab + float(rect.getAttribute('height')) / 3.3, + float(rect.getAttribute('x')) + 58, + # y mit magischer formel bestimmen, die auch die anzahl der eintraege mit einbezieht + float(rect.getAttribute('y')) + float(rect.getAttribute('height')) / len( + self.date_to_special_day[cur_date]) * (i + 1) - 0.3 * ( + float(rect.getAttribute('height')) / ( + 0.7 + 0.7 * len(self.date_to_special_day[cur_date]))), + sondertag[0])).firstChild, rect.nextSibling) + + # wochentag einfuegen + rect.parentNode.insertBefore( + xml.dom.minidom.parseString( + '''{}''' + .format(cur_date.month, + cur_date.day, + float(rect.getAttribute('x')) + 16, + float(rect.getAttribute('y')) + float( + rect.getAttribute('height')) / 2 + 10 + offset, + german_weekdays[wochentag][0:2])).firstChild, rect.nextSibling) + + if 'mark' in rect.getAttribute('class'): + # tag und monat ausblenden + cur_date = self.date_from_id_string(rect.getAttribute('id'), self.global_offset) + + if cur_date is None: + rect.addClass('invisible') + continue + + # besondere tage (geburtstage, ccc) kennzeichnen + if cur_date in self.date_to_special_day: + for i, sondertag in enumerate(self.date_to_special_day[cur_date]): + rect.addClass(sondertag[1]) + + if cur_date in self.exam_times: + rect.addClass('pruefungmark') + + if cur_date in self.date_to_special_day: + for i, sondertag in enumerate(self.date_to_special_day[cur_date]): + if 'berg' in rect.getAttribute('class'): + rect.addClass('bergmark') + if 'sonstiges' in rect.getAttribute('class'): + rect.addClass('sonstigesmark') + if 'fsi' in rect.getAttribute('class'): + rect.addClass('fsimark') + if 'birthday' in rect.getAttribute('class'): + rect.addClass('birthdaymark') + + if cur_date in self.holidays_times: + if 'birthday' not in rect.getAttribute('class') and 'berg' not in rect.getAttribute( + 'class') and 'sonstiges' not in rect.getAttribute('class'): + rect.addClass('holidaymark') + + if 'berg' not in rect.getAttribute('class') and 'sonstiges' not in rect.getAttribute( + 'class') and 'pruefung' not in rect.getAttribute('class'): + rect.addClass('invisible') + + if 'frame' in rect.getAttribute('class'): + # tag und monat ausblenden + cur_date = self.date_from_id_string(rect.getAttribute('id'), self.global_offset) + + if cur_date is None: + rect.addClass('invisible') + continue + + rect.addClass('nofill') + + # Update month names + month_count = 0 + txt_node_0 = None + for txt in image.getElementsByTagName('text'): + if 'monthname' in txt.getAttribute('class'): + cur_mon = (month_count + self.global_offset) % 12 + if cur_mon == 0: + txt_node_0 = txt + txt.childNodes[0].data = german_months[cur_mon] + month_count += 1 + txt_node_0.parentNode.insertBefore(xml.dom.minidom.parseString( + '''{}''' + .format(float(txt_node_0.getAttribute('x')), + float(txt_node_0.getAttribute('y')) - 60.0, + str(self.end_time.year))).firstChild, txt_node_0.nextSibling) + txt_node_0.parentNode.insertBefore(xml.dom.minidom.parseString( + '''{}''' + .format(204.02109, + float(txt_node_0.getAttribute('y')) - 60.0, + str(self.start_time.year))).firstChild, txt_node_0.nextSibling) + + f = open(outpupt_file, 'w') + + image.writexml(f) + f.close() + + +if __name__ == '__main__': + + parser = argparse.ArgumentParser(description='Generate a calendar') + parser.add_argument('config', metavar='config', type=Path, help='Path to config file') + parser.add_argument('-o', '--output', type=Path, help='Path to write output file', + default='./outputCalendar.svg', required=False, nargs='?') + args = parser.parse_args() + + cal = Calendar() + cal.read_config(args.config) + cal.add_holidays() + cal.generate_calendar(template_file=filename, style_file=stylefile, outpupt_file=args.output) + + print('Warning: When printing, convert to cmyk first (inkscape->png; convert mycal.png -colorspace cmyk mycal.jpg)') diff --git a/templates/kalender_fsce.svg b/templates/kalender_fsce.svg index 7b69af4..ac15670 100644 --- a/templates/kalender_fsce.svg +++ b/templates/kalender_fsce.svg @@ -20,7 +20,7 @@ FSI-Kalender 2015 - Lizenziert unter Creative-Commons BY-NC-SA 3; created with CalendRCreatR http://github.com/FSI-CE/CRCR auf Basis des CalendRCreatR von t_animal + Generated with CalendRCreatR http://github.com/Timm638/CRCR diff --git a/templates/style1.css b/templates/style1.css index 90d40e5..fd81c69 100644 --- a/templates/style1.css +++ b/templates/style1.css @@ -12,9 +12,7 @@ text { .birthdaytext { font-family:Droid Sans; fill-opacity:1; - /*stroke-width:.3px;*/ - stroke:#CC0000; - fill:#CC0000; + fill:#101088; } .feiertagstext { @@ -23,10 +21,7 @@ text { fill:#000000; } -.loscher { - font-family:Droid Sans; - fill:#FF9900; -} + .title{ fill-opacity:1; @@ -38,9 +33,7 @@ text { .titlebar{ fill:#009900; - fill-opacity:1; - stroke:#009900; - stroke-opacity:1; + fill-opacity:0; } .copyleft{ @@ -53,20 +46,16 @@ text { font-size:55px; text-align:center; text-anchor:middle; - font-family:Maya; + font-family:Droid Sans; } .month{ fill:none; - stroke:#000000; - stroke-width:0; enable-background:accumulate } .day{ fill:none; - stroke:#000000; - stroke-width:1; enable-background:accumulate } @@ -75,9 +64,7 @@ text { } .nofill { - stroke:#000000; fill-opacity:0; - stroke-width:1; } .daynumber{ @@ -88,87 +75,37 @@ text { .weekday{ fill-opacity:1; - stroke:#339900; - fill:#339900; + fill:#101088; font-size:30px; } -/*stammtisch*/ -.trunktable{ - stroke-width:5; -} - .holiday{ - fill-opacity:.08; - fill:#00CC00; -} - -.holidaymark{ - fill:#008000; + fill-opacity:.025; + fill:#0000FF; } - .weekend{ - fill-opacity:.2; - fill:#00CC00; + fill:#F0F0F0; } .holiday.weekend{ - /*fill:#D0F0D0;*/ -} - -.test{ - fill:#CC0000; -} - -.birthday{ - /*fill:#CC0000;*/ -} - -.birthdaymark{ - stroke-width:0; - fill:#CC0000; -} - -.birthday.weekend{ - /*fill:#F0F000;*/ -} - -.birthday.holiday{ - /*fill:#DFFFA0;*/ + fill-opacity:1; + fill:#DFE8FF; } -.birthday.holiday.weekend{ - /*fill:#AFDF00;*/ +.pruefungmark{ + fill:#660066; + fill-opacity:.7; } .bergmark{ fill:#003399; } -.stammtischmark{ - fill:#FF9900; -} - - .sonstigesmark{ - fill:#660066; + fill:#000080; } .fsimark{ - fill:#FFFF00; -} - -.pruefungmark{ - fill:#FF9900; - fill-opacity:.7; - -} - -.termine.weekend{ - /*fill:#FFB0B0;*/ -} - -.termine.birthday{ - /*fill:#FFE090;*/ + fill:#FFFF00; }