Skip to content

Commit

Permalink
add ics export
Browse files Browse the repository at this point in the history
- fixes #27
- provides ical output at /calendar.ics
- refactor: use ics and dhtmlx strategy in files
  • Loading branch information
niccokunzmann committed Jan 16, 2020
1 parent 573073b commit 449819b
Show file tree
Hide file tree
Showing 3 changed files with 147 additions and 87 deletions.
96 changes: 9 additions & 87 deletions app.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@
import traceback
import io
import sys
from convert_to_dhtmlx import ConvertToDhtmlx
from convert_to_ics import ConvertToICS

# configuration
DEBUG = os.environ.get("APP_DEBUG", "true").lower() == "true"
Expand Down Expand Up @@ -124,91 +126,6 @@ def get_specification(query=None):
specification[parameter] = value
return specification


class ConvertToDhtmlx:
"""Convert events to dhtmlx. This conforms to a stratey pattern."""

def __init__(self, timeshift_minutes):
"""Create a DHTMLX conversion strategy.
- timeshift_minutes is the timeshift specified by the calendar
for dates.
"""
self.timeshift = timeshift_minutes

def date_to_string(self, date):
"""Convert a date to a string."""
# use ISO format
# see https://docs.dhtmlx.com/scheduler/howtostart_nodejs.html#step4implementingcrud
# see https://docs.python.org/3/library/datetime.html#datetime.datetime.isoformat
# see https://docs.python.org/3/library/datetime.html#strftime-and-strptime-behavior
timezone = datetime.timezone(datetime.timedelta(minutes=-self.timeshift))
if isinstance(date, datetime.date) and not isinstance(date, datetime.datetime):
date = datetime.datetime(date.year, date.month, date.day, tzinfo=timezone)
elif date.tzinfo is None:
date = date.replace(tzinfo=timezone)
date = date.astimezone(datetime.timezone.utc)
return date.strftime("%Y-%m-%d %H:%M")


def convert_ical_event(self, calendar_event):
start = calendar_event["DTSTART"].dt
end = calendar_event.get("DTEND", calendar_event["DTSTART"]).dt
geo = calendar_event.get("GEO", None)
if geo:
geo = {"lon": geo.longitude, "lat": geo.latitude}
name = calendar_event.get("SUMMARY", "")
sequence = str(calendar_event.get("SEQUENCE", 0))
uid = calendar_event["UID"]
start_date = self.date_to_string(start)
return {
"start_date": start_date,
"end_date": self.date_to_string(end),
"start_date_iso": start.isoformat(),
"end_date_iso": end.isoformat(),
"start_date_iso_0": start.isoformat(),
"end_date_iso_0": end.isoformat(),
"text": name,
"description": calendar_event.get("DESCRIPTION", ""),
"location": calendar_event.get("LOCATION", None),
"geo": geo,
"uid": uid,
"ical": calendar_event.to_ical().decode("UTF-8"),
"sequence": sequence,
"recurrence": None,
"url": calendar_event.get("URL"),
"id": (uid, start_date),
"type": "event"
}

def error(self, ty, error, tb, url=None):
"""Create an error which can be used by the dhtmlx scheduler."""
now = datetime.datetime.now();
now_iso = now.isoformat()
now_s = self.date_to_string(now)
tb_s = io.StringIO()
traceback.print_exception(ty, error, tb, file=tb_s)
return {
"start_date": now_s,
"end_date": now_s,
"start_date_iso": now_iso,
"end_date_iso": now_iso,
"start_date_iso_0": now_iso,
"end_date_iso_0": now_iso,
"text": type(error).__name__,
"description": str(error),
"traceback": tb_s.getvalue(),
"location": None,
"geo": None,
"uid": "error",
"ical": "",
"sequence": 0,
"recurrence": None,
"url": url,
"id": id(error),
"type": "error"
}

def retrieve_calendar(url, specification, conversion_strategy):
"""Get the calendar entry from a url.
Expand Down Expand Up @@ -265,8 +182,13 @@ def get_calendar(type):
if type == "spec":
return jsonify(specification)
if type == "events.json":
entries = get_events(specification, ConvertToDhtmlx(int(specification["timeshift"])))
return jsonify(entries)
strategy = ConvertToDhtmlx(specification)
entries = get_events(specification, strategy)
return strategy.merge(entries)
if type == "ics":
strategy = ConvertToICS(specification)
entries = get_events(specification, strategy)
return strategy.merge(entries)
if type == "html":
template_name = specification["template"]
all_template_names = os.listdir(CALENDAR_TEMPLATE_FOLDER)
Expand Down
92 changes: 92 additions & 0 deletions convert_to_dhtmlx.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
import datetime
import io
import traceback
from flask import jsonify

class ConvertToDhtmlx:
"""Convert events to dhtmlx. This conforms to a stratey pattern."""

def __init__(self, specification):
"""Create a DHTMLX conversion strategy.
- timeshift_minutes is the timeshift specified by the calendar
for dates.
"""
self.timeshift = int(specification["timeshift"])

def date_to_string(self, date):
"""Convert a date to a string."""
# use ISO format
# see https://docs.dhtmlx.com/scheduler/howtostart_nodejs.html#step4implementingcrud
# see https://docs.python.org/3/library/datetime.html#datetime.datetime.isoformat
# see https://docs.python.org/3/library/datetime.html#strftime-and-strptime-behavior
timezone = datetime.timezone(datetime.timedelta(minutes=-self.timeshift))
if isinstance(date, datetime.date) and not isinstance(date, datetime.datetime):
date = datetime.datetime(date.year, date.month, date.day, tzinfo=timezone)
elif date.tzinfo is None:
date = date.replace(tzinfo=timezone)
date = date.astimezone(datetime.timezone.utc)
return date.strftime("%Y-%m-%d %H:%M")


def convert_ical_event(self, calendar_event):
start = calendar_event["DTSTART"].dt
end = calendar_event.get("DTEND", calendar_event["DTSTART"]).dt
geo = calendar_event.get("GEO", None)
if geo:
geo = {"lon": geo.longitude, "lat": geo.latitude}
name = calendar_event.get("SUMMARY", "")
sequence = str(calendar_event.get("SEQUENCE", 0))
uid = calendar_event["UID"]
start_date = self.date_to_string(start)
return {
"start_date": start_date,
"end_date": self.date_to_string(end),
"start_date_iso": start.isoformat(),
"end_date_iso": end.isoformat(),
"start_date_iso_0": start.isoformat(),
"end_date_iso_0": end.isoformat(),
"text": name,
"description": calendar_event.get("DESCRIPTION", ""),
"location": calendar_event.get("LOCATION", None),
"geo": geo,
"uid": uid,
"ical": calendar_event.to_ical().decode("UTF-8"),
"sequence": sequence,
"recurrence": None,
"url": calendar_event.get("URL"),
"id": (uid, start_date),
"type": "event"
}

def error(self, ty, error, tb, url=None):
"""Create an error which can be used by the dhtmlx scheduler."""
now = datetime.datetime.now();
now_iso = now.isoformat()
now_s = self.date_to_string(now)
tb_s = io.StringIO()
traceback.print_exception(ty, error, tb, file=tb_s)
return {
"start_date": now_s,
"end_date": now_s,
"start_date_iso": now_iso,
"end_date_iso": now_iso,
"start_date_iso_0": now_iso,
"end_date_iso_0": now_iso,
"text": type(error).__name__,
"description": str(error),
"traceback": tb_s.getvalue(),
"location": None,
"geo": None,
"uid": "error",
"ical": "",
"sequence": 0,
"recurrence": None,
"url": url,
"id": id(error),
"type": "error"
}

def merge(self, events):
return jsonify(events)

46 changes: 46 additions & 0 deletions convert_to_ics.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import datetime
from icalendar import Event, Calendar
from icalendar.prop import vDDDTypes
from flask import Response
import io
import traceback

class ConvertToICS:
"""Convert events to dhtmlx. This conforms to a stratey pattern."""

def __init__(self, specification):
"""Create an ICS conversion strategy."""
self.title = specification["title"]

def convert_ical_event(self, calendar_event):
return calendar_event

def error(self, ty, error, tb, url=None):
"""Create an error which can be used by the dhtmlx scheduler."""
tb_s = io.StringIO()
traceback.print_exception(ty, error, tb, file=tb_s)
event = Event()
event["DTSTART"] = event["DTEND"] = vDDDTypes(datetime.datetime.now())
event["SUMMARY"] = type(error).__name__
event["DESCRIPTION"] = str(error) + "\n\n" + tb_s.getvalue()
event["UID"] = "error" + str(id(error))
if url:
event["URL"] = url
return event

def create_calendar(self):
calendar = Calendar()
calendar["VERSION"] = "2.0"
calendar["PRODID"] = "open-web-calendar"
calendar["CALSCALE"] = "GREGORIAN"
calendar["METHOD"] = "PUBLISH"
calendar["X-WR-CALNAME"] = self.title
calendar["X-PROD-SOURCE"] = "https://github.com/niccokunzmann/open-web-calendar/"
return calendar

def merge(self, events):
calendar = self.create_calendar()
for event in events:
calendar.add_component(event)
return Response(calendar.to_ical(), mimetype="text/calendar")

0 comments on commit 449819b

Please sign in to comment.