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 %}