diff --git a/buba/__main__.py b/buba/__main__.py index a6982ea..57b8432 100644 --- a/buba/__main__.py +++ b/buba/__main__.py @@ -1,11 +1,12 @@ import logging -from bottle import Bottle, static_file, TEMPLATE_PATH, jinja2_view +from bottle import Bottle, static_file, TEMPLATE_PATH, jinja2_view, request, HTTPResponse from bottle_log import LoggingPlugin from bottle_websocket import websocket, GeventWebSocketServer from geventwebsocket.websocket import WebSocket from buba.animationconfig import setup_animations +from buba.animations.webqueue import WebQueue from buba.appconfig import AppConfig from buba.bubaanimator import BubaAnimator from buba.bubacmd import BubaCmd @@ -25,7 +26,8 @@ TEMPLATE_PATH.insert(0, config.templatepath) websocket_clients = WebSocketClients() buba = BubaCmd(config.serial, websocket_clients.send) animator = BubaAnimator(buba) -setup_animations(config, animator) +webqueue = WebQueue(buba) +setup_animations(config, animator, webqueue) @app.route("/static/") @@ -36,7 +38,9 @@ def server_static(filepath): @app.get("/") @jinja2_view("home.html.j2") def root(): - return {} + return { + "user_forms": range(webqueue.maxrows), + } @app.get('/ws', apply=[websocket]) @@ -53,6 +57,49 @@ def websocket_endpoint(ws: WebSocket): websocket_clients.remove(ws) +def json_error(status: str, body: dict) -> HTTPResponse: + return HTTPResponse( + body=body, + status=status, + headers={ + "Content-Type": "application/json"} + ) + + +def json_error_400(error: str) -> HTTPResponse: + return json_error("400 Bad Request", { + "status": "error", + "error": error, + }) + + +@app.post("/user-entry") +def user_entry(): + log.debug(f"json: {request.json}") + title = request.json.get("title") + if not isinstance(title, str): + raise json_error_400("title must be a string") + title = title.strip() + if title == "": + raise json_error_400("title must not be empty") + if not isinstance(request.json.get("rows"), list): + raise json_error_400("rows must be a list") + rows = [] + for row in request.json.get("rows"): + if not isinstance(row[0], str) or not isinstance(row[0], str): + raise json_error_400("rows elements must be strings") + row = [v.strip() for v in row] + if row[0] != "" or row[1] != "": + rows.append(row) + rows = rows[:webqueue.maxrows] + if webqueue.add(title, rows): + return { + "status": "success", + } + else: + raise json_error_400("queue is full") + + if __name__ == '__main__': app.run(host=config.listen_host, port=config.listen_port, server=GeventWebSocketServer, debug=config.debug, quiet=not config.debug) diff --git a/buba/animationconfig.py b/buba/animationconfig.py index a964515..109e6bc 100644 --- a/buba/animationconfig.py +++ b/buba/animationconfig.py @@ -8,25 +8,21 @@ from buba.animations.icalevents import IcalEvents from buba.animations.snake import SnakeAnimation from buba.animations.spaceapi import Spaceapi from buba.animations.time import BubaTime +from buba.animations.webqueue import WebQueue from buba.bubaanimator import BubaAnimator LOG = logging.getLogger(__name__) -def setup_animations(config, animator: BubaAnimator): +def setup_animations(config, animator: BubaAnimator, webqueue: WebQueue): cs = BubaCharset(animator.buba) - # animator.add(cs) bt = BubaTime(animator.buba) - animator.add(bt) snake = SnakeAnimation(animator.buba) - animator.add(snake) dbf_ahst = DBF(animator.buba, ds100="AHST", station="Holstenstraße") dbf_ahs = DBF(animator.buba, ds100="AHS", station="Altona", count=9) - animator.add(dbf_ahst) - animator.add(dbf_ahs) ccchh_events = IcalEvents(animator.buba, url="https://cloud.hamburg.ccc.de/remote.php/dav/public-calendars/QJAdExziSnNJEz5g?export", @@ -34,15 +30,24 @@ def setup_animations(config, animator: BubaAnimator): ccc_events = IcalEvents(animator.buba, url="https://events.ccc.de/calendar/events.ics", title="CCC Events", range=datetime.timedelta(weeks=8)) - animator.add(ccchh_events) - animator.add(ccc_events) ccchh_spaceapi = Spaceapi(animator.buba, "https://spaceapi.hamburg.ccc.de", "CCCHH") - animator.add(ccchh_spaceapi) + ca = None if config.clubassistant_token is not None: ca = HomeAssistant(animator.buba, "https://club-assistant.ccchh.net", "Club Assistant", config.clubassistant_token) - animator.add(ca) else: LOG.warning("Club Assistant token not set, not activating animation") + + # animator.add(cs) + animator.add(bt) + animator.add(snake) + animator.add(dbf_ahst) + animator.add(dbf_ahs) + animator.add(ccchh_events) + animator.add(ccc_events) + animator.add(ccchh_spaceapi) + if ca is not None: + animator.add(ca) + animator.add(webqueue) diff --git a/buba/animations/icalevents.py b/buba/animations/icalevents.py index 3464bd9..148951c 100644 --- a/buba/animations/icalevents.py +++ b/buba/animations/icalevents.py @@ -1,13 +1,10 @@ import os from datetime import timedelta, datetime -from threading import Thread -from time import sleep import icalevents.icalevents from pytz import timezone from buba.bubaanimation import BubaAnimation -from buba.bubacmd import BubaCmd class IcalEvents(BubaAnimation): diff --git a/buba/animations/webqueue.py b/buba/animations/webqueue.py new file mode 100644 index 0000000..66bbde5 --- /dev/null +++ b/buba/animations/webqueue.py @@ -0,0 +1,48 @@ +from datetime import timedelta +from queue import Queue, Empty +from time import sleep + +from buba.bubaanimation import BubaAnimation +from buba.bubaanimator import LineLayoutColumn +from buba.bubacmd import BubaCmd + + +class WebQueue(BubaAnimation): + """ + + """ + def __init__(self, buba, maxlen:int=3, maxrows:int=6): + """ + A queue of pages to show. If + :param buba: + :param maxlen: + """ + super().__init__(buba) + self.title = "(waiting for input)" + self.queue = Queue() + self.maxlen = maxlen + self.maxrows = maxrows + + def show(self) -> None: + try: + (self.title, self.rows) = self.queue.get(block=False) + self.layout = self.default_layout + self.show_pages() + except Empty: + self.buba.text(page=0, row=0, col_start=0, col_end=119, text="\u0010\u0010 Your Data Here \u0011\u0011", align=BubaCmd.ALIGN_CENTER) + self.buba.text(page=0, row=1, col_start=0, col_end=119, text="Submit your own data to", align=BubaCmd.ALIGN_CENTER) + self.buba.text(page=0, row=2, col_start=0, col_end=119, text="this display! Go to", align=BubaCmd.ALIGN_CENTER) + self.buba.text(page=0, row=3, col_start=0, col_end=119, text="https://buba.ccchh.net", align=BubaCmd.ALIGN_CENTER) + sleep(7) + + def add(self, title, rows) -> bool: + """ + Add an entry to the queue. Returns False if the maximum number of entries has been reached. + :param title: + :param rows: + :return: + """ + if self.queue.qsize() >= self.maxlen: + return False + self.queue.put((title, rows)) + return True \ No newline at end of file diff --git a/buba/bubacmd.py b/buba/bubacmd.py index 030e22e..67c35d5 100644 --- a/buba/bubacmd.py +++ b/buba/bubacmd.py @@ -64,6 +64,7 @@ class BubaCmd: :param align: alignment options, see MIS1TextDisplay.ALIGN_* :return: """ + text = text.replace("\u00ad", "") # remove soft hyphen text = text.encode("CP437", "replace").decode("CP437") # force text to be CP437 compliant if self.display is not None: self.display.text(page, row, col_start, col_end, text, align) diff --git a/buba/static/main.css b/buba/static/main.css index 19924ae..05bdb37 100644 --- a/buba/static/main.css +++ b/buba/static/main.css @@ -29,4 +29,16 @@ svg.geavision__row { .gvson { fill: #4f0; +} + +.info-form__row { + display: flex; +} + +.info-form__field { + display: flex; + flex-direction: column; +} +.info-form__field * { + display: block; } \ No newline at end of file diff --git a/buba/static/main.js b/buba/static/main.js index 7edc355..b9966ab 100644 --- a/buba/static/main.js +++ b/buba/static/main.js @@ -11,7 +11,7 @@ if (container) { if (m.cmd === undefined) { console.log("undefined command", m) } - switch(m.cmd) { + switch (m.cmd) { case "set_pages": display.set_pages(m.pages); break; @@ -32,5 +32,44 @@ if (container) { ws.close(); }); } + connect(); } + +let info_send = document.getElementById("info-send"); +if (info_send) { + document.getElementById("info-form__form").addEventListener("submit", (e) => { + e.preventDefault(); + e.stopPropagation(); + }) + info_send.addEventListener("click", (e) => { + let info = { + "title": document.getElementById("info-form__title").value, + "rows": [] + } + for (let e of document.querySelectorAll(".info-form__row-data")) { + let t = e.querySelector(".info-row__field-text").value; + let v = e.querySelector(".info-row__field-value").value; + info.rows.push([t, v]); + } + fetch("/user-entry", { + method: "POST", + body: JSON.stringify(info), + headers: { + "Content-Type": "application/json" + } + }).then(res => { + if (res.status === 200) { + alert("Your entry has been sent!") + } else { + return res.json() + } + }).then(json => { + alert("Your entry could not be accepted: " + json.error) + }).catch((err) => { + console.log(err); + }) + e.preventDefault(); + e.stopPropagation(); + }) +} \ No newline at end of file diff --git a/buba/templates/base.html.j2 b/buba/templates/base.html.j2 index 28bb51c..5d0523d 100644 --- a/buba/templates/base.html.j2 +++ b/buba/templates/base.html.j2 @@ -8,7 +8,6 @@ -

{{ self.page_title() }}

{% block page_body %}{% endblock %} \ No newline at end of file diff --git a/buba/templates/home.html.j2 b/buba/templates/home.html.j2 index 7f878a6..1623adb 100644 --- a/buba/templates/home.html.j2 +++ b/buba/templates/home.html.j2 @@ -2,4 +2,32 @@ {% block page_title %}CCCHH Buba{% endblock %} {% block page_body %}
...
+

Submit Your Own Info

+
+
+
+
+ + +
+
+ {% for r in user_forms %} +
+
+
+
+
+
+
+ +
+
+ {% endfor %} +
+
+ +
+
+
+
{% endblock %}