Add user submnission
All checks were successful
docker-image / docker (push) Successful in 10m24s

This commit is contained in:
Stefan Bethke 2025-06-16 20:39:29 +02:00
commit 2321d7d552
9 changed files with 194 additions and 18 deletions

View file

@ -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/<filepath>")
@ -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)

View file

@ -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)

View file

@ -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):

View file

@ -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

View file

@ -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)

View file

@ -30,3 +30,15 @@ svg.geavision__row {
.gvson {
fill: #4f0;
}
.info-form__row {
display: flex;
}
.info-form__field {
display: flex;
flex-direction: column;
}
.info-form__field * {
display: block;
}

View file

@ -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();
})
}

View file

@ -8,7 +8,6 @@
<script src="static/main.js" type="module"></script>
</head>
<body>
<h1>{{ self.page_title() }}</h1>
{% block page_body %}{% endblock %}
</body>
</html>

View file

@ -2,4 +2,32 @@
{% block page_title %}CCCHH Buba{% endblock %}
{% block page_body %}
<div id="geavision-display">...</div>
<h3>Submit Your Own Info</h3>
<div class="info-form">
<form id="info-form__form">
<div class="info-form__row">
<div class="info-form__field">
<label for="info-form__title">Title</label>
<input id="info-form__title" type="text" name="title" size="32">
</div>
</div>
{% for r in user_forms %}
<div class="info-form__row info-form__row-data">
<div class="info-form__field">
<label for="info-form__row-{{ r }}-text">Description</label><br>
<input id="info-form__row-{{ r }}-text" class="info-row__field-text" type="text" name="row-{{ r }}-text" size="27"><br>
</div>
<div class="info-form__field">
<label for="info-form__row-{{ r }}-value">Value</label><br>
<input id="info-form__row-{{ r }}-value" class="info-row__field-value" type="text" name="row-{{ r }}-value" size="5">
</div>
</div>
{% endfor %}
<div class="info-form__row">
<div class="info-form__field">
<button id="info-send">Send Info</button>
</div>
</div>
</form>
</div>
{% endblock %}