mirror of
https://github.com/netzbegruenung/schaufenster.git
synced 2024-05-04 10:13:40 +02:00
First version of an iCal calender service
This commit is contained in:
parent
99ed046b7a
commit
1fcc52925e
|
@ -33,3 +33,9 @@ OpenSource alternative zu Yodeck. Sollte ähnlichen Umfang haben wie Yodeck, abe
|
||||||
Beschreibung folgt.
|
Beschreibung folgt.
|
||||||
|
|
||||||
https://www.screenly.io/ose/
|
https://www.screenly.io/ose/
|
||||||
|
|
||||||
|
## Software
|
||||||
|
|
||||||
|
Im Unterordner `service` entsteht Software zur Erzeugung dynamischer
|
||||||
|
HTML-Inhalte, die für Digital Signage eingesetzt werden können. Dokumentation
|
||||||
|
folgt, sobald diese Software einsatzfähig ist.
|
||||||
|
|
3
service/.gitignore
vendored
Normal file
3
service/.gitignore
vendored
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
venv
|
||||||
|
.pytest_cache
|
||||||
|
*.pyc
|
6
service/Makefile
Normal file
6
service/Makefile
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
|
||||||
|
serve:
|
||||||
|
gunicorn --reload api.main:app
|
||||||
|
|
||||||
|
test:
|
||||||
|
pytest tests
|
42
service/README.md
Normal file
42
service/README.md
Normal file
|
@ -0,0 +1,42 @@
|
||||||
|
# Schaufenster Service
|
||||||
|
|
||||||
|
Dies ist ein Webservice zur Erzeugung dynamischer Inhalte für
|
||||||
|
Digital Signage Anwendungen.
|
||||||
|
|
||||||
|
_Anwendungsbeispiel:_
|
||||||
|
|
||||||
|
Auf einer digitalen Anzeigetafel soll stets aktuell der nächste Sitzungstermin
|
||||||
|
angezeigt werden. Hierfür geben wir eine iCal-Kalender-URL an
|
||||||
|
und bekommen dafür Titel und weitere Details der nächsten Termine in diesem
|
||||||
|
Kalender zurück.
|
||||||
|
|
||||||
|
## API
|
||||||
|
|
||||||
|
### `GET /events/` - Die nächsten Termine eines iCal Kalenders ausgeben
|
||||||
|
|
||||||
|
Request URL Parameter:
|
||||||
|
|
||||||
|
- `ical_url`: Adresse des iCal-Kalenders (erforderlich).
|
||||||
|
- `num`: Maximale Anzahl der Termine, die ausgegeben werden.
|
||||||
|
- `charset`: Zeichensatz der iCal-Quelle. Normalerweise wird der Zeichensatz
|
||||||
|
angenommen, den der Webserver im `Content-type` header angibt. Mit diesem
|
||||||
|
Parameter kann der Wert des Servers überschrieben werden. Beispiel: `charset=utf-8`.
|
||||||
|
|
||||||
|
Ausgabe:
|
||||||
|
|
||||||
|
```json
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"title": "Karfreitag",
|
||||||
|
"start": "2018-03-30",
|
||||||
|
"end": "2018-03-31"
|
||||||
|
},
|
||||||
|
...
|
||||||
|
]
|
||||||
|
```
|
||||||
|
|
||||||
|
Liste mit Terminen als JSON Array. Jeder Termin enthält:
|
||||||
|
|
||||||
|
- `title`: Titel des Termins
|
||||||
|
- `start`: Start-Datum (oder Datum/Uhrzeit) des Termins
|
||||||
|
- `end`: (optional) Enddatum (oder Datum/Uhrzeit) des Termins
|
0
service/api/__init__.py
Normal file
0
service/api/__init__.py
Normal file
|
@ -1,4 +1,4 @@
|
||||||
# coding: utf8
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
import requests
|
import requests
|
||||||
import icalendar
|
import icalendar
|
||||||
|
@ -7,9 +7,9 @@ from datetime import date
|
||||||
|
|
||||||
class Client(object):
|
class Client(object):
|
||||||
|
|
||||||
def __init__(self, url, timezone=None):
|
def __init__(self, url, charset=None):
|
||||||
self.url = url
|
self.url = url
|
||||||
self.timezone = timezone
|
self.charset = charset
|
||||||
self.events = []
|
self.events = []
|
||||||
self.__load()
|
self.__load()
|
||||||
|
|
||||||
|
@ -17,25 +17,33 @@ class Client(object):
|
||||||
r = requests.get(self.url)
|
r = requests.get(self.url)
|
||||||
r.raise_for_status()
|
r.raise_for_status()
|
||||||
|
|
||||||
|
# requests normally uses encoding returned by "Content-type" header.
|
||||||
|
# If charset is set, this overwrites the detected character encoding.
|
||||||
|
if self.charset is not None:
|
||||||
|
r.encoding = self.charset
|
||||||
|
|
||||||
cal = icalendar.Calendar.from_ical(r.text)
|
cal = icalendar.Calendar.from_ical(r.text)
|
||||||
self.events = []
|
self.events = []
|
||||||
|
|
||||||
for event in cal.walk('vevent'):
|
for event in cal.walk('vevent'):
|
||||||
title = None
|
title = None
|
||||||
description = None
|
|
||||||
if "SUMMARY" in event:
|
if "SUMMARY" in event:
|
||||||
title = event["SUMMARY"]
|
title = event["SUMMARY"]
|
||||||
if "DESCRIPTION" in event:
|
|
||||||
description = event["DESCRIPTION"]
|
|
||||||
dtstart = event["DTSTART"].dt
|
dtstart = event["DTSTART"].dt
|
||||||
dtend = event["DTEND"].dt
|
dtend = event["DTEND"].dt
|
||||||
self.events.append({
|
self.events.append({
|
||||||
"title": title,
|
"title": title,
|
||||||
"description": description,
|
|
||||||
"start": dtstart,
|
"start": dtstart,
|
||||||
"end": dtend,
|
"end": dtend,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
# sort events by start datetime
|
||||||
|
def getdatetime(event):
|
||||||
|
if isinstance(event["start"], date):
|
||||||
|
return datetime.combine(event["start"], datetime.min.time())
|
||||||
|
return event["start"]
|
||||||
|
self.events = sorted(self.events, key=getdatetime)
|
||||||
|
|
||||||
def next_events(self, num=10):
|
def next_events(self, num=10):
|
||||||
"""
|
"""
|
||||||
Returns the next num events from the calendar
|
Returns the next num events from the calendar
|
||||||
|
@ -48,4 +56,6 @@ class Client(object):
|
||||||
end = datetime.combine(end, datetime.min.time())
|
end = datetime.combine(end, datetime.min.time())
|
||||||
if end > now:
|
if end > now:
|
||||||
out.append(event)
|
out.append(event)
|
||||||
|
if len(out) >= num:
|
||||||
|
break
|
||||||
return out
|
return out
|
||||||
|
|
38
service/api/jsonhandler.py
Normal file
38
service/api/jsonhandler.py
Normal file
|
@ -0,0 +1,38 @@
|
||||||
|
import six
|
||||||
|
|
||||||
|
from datetime import date, datetime
|
||||||
|
|
||||||
|
from falcon import errors
|
||||||
|
from falcon.media import BaseHandler
|
||||||
|
from falcon.util import json
|
||||||
|
|
||||||
|
class ComplexEncoder(json.JSONEncoder):
|
||||||
|
|
||||||
|
"""JSONENcoder that handles date and datetime"""
|
||||||
|
|
||||||
|
def default(self, obj):
|
||||||
|
if isinstance(obj, date) or isinstance(obj, datetime):
|
||||||
|
return obj.isoformat()
|
||||||
|
# Let the base class default method raise the TypeError
|
||||||
|
return json.JSONEncoder.default(self, obj)
|
||||||
|
|
||||||
|
class JSONHandler(BaseHandler):
|
||||||
|
"""Handler built using Python's :py:mod:`json` module."""
|
||||||
|
|
||||||
|
def deserialize(self, raw):
|
||||||
|
try:
|
||||||
|
return json.loads(raw.decode('utf-8'))
|
||||||
|
except ValueError as err:
|
||||||
|
raise errors.HTTPBadRequest(
|
||||||
|
'Invalid JSON',
|
||||||
|
'Could not parse JSON body - {0}'.format(err)
|
||||||
|
)
|
||||||
|
|
||||||
|
def serialize(self, media):
|
||||||
|
result = json.dumps(media,
|
||||||
|
ensure_ascii=False,
|
||||||
|
cls=ComplexEncoder)
|
||||||
|
if six.PY3 or not isinstance(result, bytes):
|
||||||
|
return result.encode('utf-8')
|
||||||
|
|
||||||
|
return result
|
37
service/api/main.py
Normal file
37
service/api/main.py
Normal file
|
@ -0,0 +1,37 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
import falcon
|
||||||
|
from falcon import media
|
||||||
|
import logging
|
||||||
|
from . import events
|
||||||
|
from . import jsonhandler
|
||||||
|
|
||||||
|
|
||||||
|
class EventsResource(object):
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self.logger = logging.getLogger('api.' + __name__)
|
||||||
|
|
||||||
|
def on_get(self, req, resp):
|
||||||
|
"""
|
||||||
|
Loads an ical Calendar and returns the next events
|
||||||
|
"""
|
||||||
|
ical_url = req.get_param("ical_url", required=True)
|
||||||
|
charset = req.get_param("charset")
|
||||||
|
num = int(req.get_param("num", required=False, default="10"))
|
||||||
|
|
||||||
|
client = events.Client(url=ical_url, charset=charset)
|
||||||
|
next_events = client.next_events(num)
|
||||||
|
resp.media = next_events
|
||||||
|
|
||||||
|
|
||||||
|
handlers = media.Handlers({
|
||||||
|
'application/json': jsonhandler.JSONHandler(),
|
||||||
|
})
|
||||||
|
|
||||||
|
app = falcon.API()
|
||||||
|
|
||||||
|
app.req_options.media_handlers = handlers
|
||||||
|
app.resp_options.media_handlers = handlers
|
||||||
|
|
||||||
|
app.add_route('/events/', EventsResource())
|
17
service/requirements.txt
Normal file
17
service/requirements.txt
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
attrs==17.4.0
|
||||||
|
certifi==2018.1.18
|
||||||
|
chardet==3.0.4
|
||||||
|
falcon==1.4.1
|
||||||
|
funcsigs==1.0.2
|
||||||
|
gunicorn==19.7.1
|
||||||
|
icalendar==4.0.0
|
||||||
|
idna==2.6
|
||||||
|
pluggy==0.6.0
|
||||||
|
py==1.5.2
|
||||||
|
pytest==3.4.0
|
||||||
|
python-dateutil==2.6.1
|
||||||
|
python-mimeparse==1.6.0
|
||||||
|
pytz==2017.3
|
||||||
|
requests==2.18.4
|
||||||
|
six==1.11.0
|
||||||
|
urllib3==1.22
|
0
service/tests/__init__.py
Normal file
0
service/tests/__init__.py
Normal file
23
service/tests/test_main.py
Normal file
23
service/tests/test_main.py
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
import falcon
|
||||||
|
from falcon import testing
|
||||||
|
import pytest
|
||||||
|
from api.main import app
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def client():
|
||||||
|
return testing.TestClient(app)
|
||||||
|
|
||||||
|
|
||||||
|
def test_get_events_no_ical_url(client):
|
||||||
|
"""
|
||||||
|
No ical URL given bad request
|
||||||
|
"""
|
||||||
|
response = client.simulate_get('/events/')
|
||||||
|
assert response.status == falcon.HTTP_BAD_REQUEST
|
||||||
|
# TODO: assertion for response format
|
||||||
|
|
||||||
|
def test_get_events(client):
|
||||||
|
response = client.simulate_get('/events/', params={
|
||||||
|
"ical_url": "http://www.webcal.fi/cal.php?id=75&rid=ics&wrn=0&wp=12&wf=55"
|
||||||
|
})
|
||||||
|
assert response.status == falcon.HTTP_OK
|
Loading…
Reference in a new issue