From f614fe1afcf75fb8a44241e2c6674521dfc5953b Mon Sep 17 00:00:00 2001 From: s3lph Date: Mon, 25 Nov 2024 23:29:30 +0100 Subject: [PATCH] breaking: remove the config option to automatically close tabs after ean purchase fix: improve error handling on database consistency errors (e.g. non-unique ean codes) in the settings feat: handle ean codes in the already open tab via a websocket connection feat: populate ean code input field when a barcode is scanned while in the product settings --- CHANGELOG.md | 16 ++++++++++++++++ matemat/__init__.py | 2 +- matemat/exceptions/DatabaseConsistencyError.py | 2 +- matemat/webserver/pagelets/admin.py | 14 +++++++++----- matemat/webserver/pagelets/buy.py | 5 ++--- matemat/webserver/pagelets/main.py | 10 ++++------ matemat/webserver/pagelets/touchkey.py | 5 +---- matemat/webserver/template/template.py | 12 ++++++++++++ package/debian/matemat/etc/matemat.conf | 8 +++++--- templates/admin.html | 9 +++++++++ templates/admin_restricted.html | 4 ++-- templates/base.html | 10 ++++++++++ templates/modproduct.html | 9 +++++++++ templates/productlist.html | 18 ++++++++++++------ templates/userlist.html | 11 ++++++----- 15 files changed, 99 insertions(+), 36 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b72c937..4407fe1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,21 @@ # Matemat Changelog + +## Version 0.3.15 + +Websocket-based EAN code handling + +### Changes + + +- breaking: remove the config option to automatically close tabs after ean purchase +- fix: improve error handling on database consistency errors (e.g. non-unique ean codes) in the settings +- feat: handle ean codes in the already open tab via a websocket connection +- feat: populate ean code input field when a barcode is scanned while in the product settings + + + + ## Version 0.3.14 diff --git a/matemat/__init__.py b/matemat/__init__.py index d9195dc..67cbcd2 100644 --- a/matemat/__init__.py +++ b/matemat/__init__.py @@ -1,2 +1,2 @@ -__version__ = '0.3.14' +__version__ = '0.3.15' diff --git a/matemat/exceptions/DatabaseConsistencyError.py b/matemat/exceptions/DatabaseConsistencyError.py index 5f91b58..89818f1 100644 --- a/matemat/exceptions/DatabaseConsistencyError.py +++ b/matemat/exceptions/DatabaseConsistencyError.py @@ -2,7 +2,7 @@ from typing import Optional -class DatabaseConsistencyError(BaseException): +class DatabaseConsistencyError(Exception): def __init__(self, msg: Optional[str] = None) -> None: self._msg: Optional[str] = msg diff --git a/matemat/webserver/pagelets/admin.py b/matemat/webserver/pagelets/admin.py index 9018f25..99f3771 100644 --- a/matemat/webserver/pagelets/admin.py +++ b/matemat/webserver/pagelets/admin.py @@ -13,6 +13,7 @@ from matemat.exceptions import AuthenticationError, DatabaseConsistencyError from matemat.util.currency_format import parse_chf from matemat.webserver import session, template from matemat.webserver.config import get_app_config, get_stock_provider +from matemat.webserver.template import Notification @get('/admin') @@ -207,7 +208,7 @@ def handle_admin_change(args: FormsDict, files: FormsDict, db: MatematDatabase): custom_price = 'custom_price' in args stockable = 'stockable' in args ean = str(args.ean) or None - # Create the user in the database + # Create the product in the database newproduct = db.create_product(name, price_member, price_non_member, custom_price, stockable, ean) # If a new product image was uploaded, process it image = files.image.file.read() if 'image' in files else None @@ -226,7 +227,8 @@ def handle_admin_change(args: FormsDict, files: FormsDict, db: MatematDatabase): image.thumbnail((150, 150), Image.LANCZOS) # Write the image to the file image.save(os.path.join(abspath, f'{newproduct.id}.png'), 'PNG') - except OSError: + except OSError as e: + Notification.error(str(e), decay=True) return else: # If no image was uploaded and a default avatar is set, copy it to the product's avatar path @@ -282,8 +284,10 @@ def handle_admin_change(args: FormsDict, files: FormsDict, db: MatematDatabase): image.thumbnail((150, 150), Image.LANCZOS) # Write the image to the file image.save(os.path.join(abspath, f'default.png'), 'PNG') - except OSError: + except OSError as e: + Notification.error(str(e), decay=True) return - except UnicodeDecodeError: - raise ValueError('an argument not a string') + except Exception as e: + Notification.error(str(e), decay=True) + return diff --git a/matemat/webserver/pagelets/buy.py b/matemat/webserver/pagelets/buy.py index 96296b4..84b7855 100644 --- a/matemat/webserver/pagelets/buy.py +++ b/matemat/webserver/pagelets/buy.py @@ -26,7 +26,6 @@ def buy(): if 'pid' in request.params: pid = int(str(request.params.pid)) product = db.get_product(pid) - closetab = int(str(request.params.closetab) or 0) if c.get_dispenser().dispense(product, 1): price = product.price_member if user.is_member else product.price_non_member if 'price' in request.params: @@ -38,7 +37,7 @@ def buy(): stock_provider.update_stock(product, -1) # Logout user if configured, logged in via touchkey and no price entry input was shown if user.logout_after_purchase and authlevel < 2 and not product.custom_price: - redirect(f'/logout?lastaction=buy&lastproduct={pid}&lastprice={price}&closetab={closetab}') + redirect(f'/logout?lastaction=buy&lastproduct={pid}&lastprice={price}') # Redirect to the main page (where this request should have come from) - redirect(f'/?lastaction=buy&lastproduct={pid}&lastprice={price}&closetab={closetab}') + redirect(f'/?lastaction=buy&lastproduct={pid}&lastprice={price}') redirect('/') diff --git a/matemat/webserver/pagelets/main.py b/matemat/webserver/pagelets/main.py index 6cf7c65..7f9a255 100644 --- a/matemat/webserver/pagelets/main.py +++ b/matemat/webserver/pagelets/main.py @@ -20,7 +20,6 @@ def main_page(): with MatematDatabase(config['DatabaseFile']) as db: # Fetch the list of products to display products = db.list_products() - closetab = int(str(request.params.closetab) or 0) lastprice = int(request.params.lastprice) if request.params.lastprice else None if request.params.lastaction == 'deposit' and lastprice: Notification.success(f'Deposited {format_chf(lastprice)}', decay=True) @@ -28,6 +27,7 @@ def main_page(): lastproduct = db.get_product(request.params.lastproduct) Notification.success(f'Purchased {lastproduct.name} for {format_chf(lastprice)}', decay=True) buyproduct = None + if request.params.ean: try: buyproduct = db.get_product_by_ean(request.params.ean) @@ -35,6 +35,7 @@ def main_page(): f'Login will purchase {buyproduct.name}. Click here to abort.') except ValueError: Notification.error(f'EAN code {request.params.ean} is not associated with any product.', decay=True) + # Check whether a user is logged in if session.has(session_id, 'authenticated_user'): # Fetch the user id and authentication level (touchkey vs password) from the session storage @@ -42,10 +43,7 @@ def main_page(): authlevel: int = session.get(session_id, 'authentication_level') # If an EAN code was scanned, directly trigger the purchase if buyproduct: - url = f'/buy?pid={buyproduct.id}' - if config.get('CloseTabAfterEANPurchase', '0') == '1': - url += '&closetab=1' - redirect(url) + redirect(f'/buy?pid={buyproduct.id}') # Fetch the user object from the database (for name display, price calculation and admin check) users = db.list_users() user = db.get_user(uid) @@ -62,4 +60,4 @@ def main_page(): return template.render('userlist.html', users=users, setupname=config['InstanceName'], now=now, signup=(config.get('SignupEnabled', '0') == '1'), - buyproduct=buyproduct, closetab=closetab) + buyproduct=buyproduct) diff --git a/matemat/webserver/pagelets/touchkey.py b/matemat/webserver/pagelets/touchkey.py index 3494940..9395431 100644 --- a/matemat/webserver/pagelets/touchkey.py +++ b/matemat/webserver/pagelets/touchkey.py @@ -51,10 +51,7 @@ def touchkey_page(): session.put(session_id, 'authentication_level', 1) if request.params.buypid: buypid = str(request.params.buypid) - url = f'/buy?pid={buypid}' - if config.get('CloseTabAfterEANPurchase', '0') == '1': - url += '&closetab=1' - redirect(url) + redirect(f'/buy?pid={buypid}') # Redirect to the main page, showing the product list redirect('/') # If neither GET nor POST was used, show a 405 Method Not Allowed error page diff --git a/matemat/webserver/template/template.py b/matemat/webserver/template/template.py index e3b04ad..f048af6 100644 --- a/matemat/webserver/template/template.py +++ b/matemat/webserver/template/template.py @@ -2,10 +2,15 @@ from typing import Any, Dict import os.path import jinja2 +import netaddr + +from bottle import request from matemat import __version__ from matemat.util.currency_format import format_chf from matemat.webserver.template import Notification +from matemat.webserver.config import get_app_config + __jinja_env: jinja2.Environment = None @@ -22,9 +27,16 @@ def init(config: Dict[str, Any]) -> None: def render(name: str, **kwargs): global __jinja_env + config = get_app_config() template: jinja2.Template = __jinja_env.get_template(name) + wsacl = netaddr.IPSet([addr.strip() for addr in config.get('EanWebsocketAcl', '').split(',')]) + if config.get('EanWebsocketUrl', '') and request.remote_addr in wsacl: + eanwebsocket = config.get('EanWebsocketUrl') + else: + eanwebsocket = None return template.render( __version__=__version__, notifications=Notification.render(), + eanwebsocket=eanwebsocket, **kwargs ).encode('utf-8') diff --git a/package/debian/matemat/etc/matemat.conf b/package/debian/matemat/etc/matemat.conf index b0ba760..09ceab5 100644 --- a/package/debian/matemat/etc/matemat.conf +++ b/package/debian/matemat/etc/matemat.conf @@ -38,10 +38,12 @@ InstanceName=Matemat #SignupKioskMode= ::1, ::ffff:127.0.0.0/8, 127.0.0.0/8 # -# Close tabs after completing an EAN code scan based purchase. -# This only works in Firefox with dom.allow_scripts_to_close_windows=true +# Open a websocket connection on which to listen for scanned barcodes. +# Can be restricted so that e.g. the connection is only attempted when the client is localhost. # -#CloseTabAfterEANPurchase=1 +#EanWebsocketUrl=ws://localhost:47808/ws +#EanWebsocketAcl=::1,::ffff:127.0.0.0/104,127.0.0.0/8 + # Add static HTTP headers in this section # [HttpHeaders] diff --git a/templates/admin.html b/templates/admin.html index 7343c6d..f397c46 100644 --- a/templates/admin.html +++ b/templates/admin.html @@ -23,3 +23,12 @@ {{ super() }} {% endblock %} + +{% block eanwebsocket %} + function (e) { + let eaninput = document.getElementById("admin-newproduct-ean"); + eaninput.value = e.data; + eaninput.select(); + eaninput.scrollIntoView(); + } +{% endblock %} diff --git a/templates/admin_restricted.html b/templates/admin_restricted.html index 4581cf0..dc3056e 100644 --- a/templates/admin_restricted.html +++ b/templates/admin_restricted.html @@ -17,8 +17,8 @@
- -
+ +
diff --git a/templates/base.html b/templates/base.html index 9134b7e..7609d79 100644 --- a/templates/base.html +++ b/templates/base.html @@ -56,5 +56,15 @@ {% endblock %} +{% if eanwebsocket %} + +{% endif %} diff --git a/templates/modproduct.html b/templates/modproduct.html index dc1931e..fcaaba0 100644 --- a/templates/modproduct.html +++ b/templates/modproduct.html @@ -52,3 +52,12 @@ {{ super() }} {% endblock %} + +{% block eanwebsocket %} + function (e) { + let eaninput = document.getElementById("modproduct-ean"); + eaninput.value = e.data; + eaninput.select(); + eaninput.scrollIntoView(); + } +{% endblock %} diff --git a/templates/productlist.html b/templates/productlist.html index 8c3b7ad..5f355f4 100644 --- a/templates/productlist.html +++ b/templates/productlist.html @@ -53,7 +53,7 @@ {% if product.custom_price %} {% else %} - + {% endif %} {{ product.name }} {% if product.custom_price %} @@ -81,9 +81,15 @@ {{ super() }} - {% if closetab | default(0) %} - {# This only works in Firefox with dom.allow_scripts_to_close_windows=true #} - - {% endif %} - +{% endblock %} + +{% block eanwebsocket %} + function (e) { + let eaninput = document.getElementById("a-buy-ean" + e.data); + if (eaninput === null) { + document.location = "?ean=" + e.data; + } else { + eaninput.click(); + } + } {% endblock %} diff --git a/templates/userlist.html b/templates/userlist.html index fedb8be..840ac06 100644 --- a/templates/userlist.html +++ b/templates/userlist.html @@ -33,9 +33,10 @@ {{ super() }} - {% if closetab | default(0) %} - {# This only works in Firefox with dom.allow_scripts_to_close_windows=true #} - - {% endif %} - +{% endblock %} + +{% block eanwebsocket %} + function (e) { + document.location = "?ean=" + e.data; + } {% endblock %}