breaking: remove the config option to automatically close tabs after ean purchase
All checks were successful
/ test (push) Successful in 1m22s
/ codestyle (push) Successful in 1m4s
/ build_wheel (push) Successful in 1m59s
/ build_debian (push) Successful in 2m33s

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
This commit is contained in:
s3lph 2024-11-25 23:29:30 +01:00
parent 4eb71415fd
commit f614fe1afc
Signed by: s3lph
GPG key ID: 0AA29A52FB33CFB5
15 changed files with 99 additions and 36 deletions

View file

@ -1,5 +1,21 @@
# Matemat Changelog
<!-- BEGIN RELEASE v0.3.15 -->
## Version 0.3.15
Websocket-based EAN code handling
### Changes
<!-- BEGIN CHANGES 0.3.15 -->
- 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
<!-- END CHANGES 0.3.15 -->
<!-- END RELEASE v0.3.15 -->
<!-- BEGIN RELEASE v0.3.14 -->
## Version 0.3.14

View file

@ -1,2 +1,2 @@
__version__ = '0.3.14'
__version__ = '0.3.15'

View file

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

View file

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

View file

@ -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('/')

View file

@ -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 <strong>{buyproduct.name}</strong>. Click <a href="/">here</a> 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)

View file

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

View file

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

View file

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

View file

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

View file

@ -17,8 +17,8 @@
<label for="admin-newuser-isadmin">Admin: </label>
<input id="admin-newuser-isadmin" type="checkbox" name="isadmin" /><br/>
<label for="admin-myaccount-logout-after-purchase">Logout after purchase: </label>
<input id="admin-myaccount-logout-after-purchase" type="checkbox" name="logout_after_purchase" /><br/>
<label for="admin-newuser-logout-after-purchase">Logout after purchase: </label>
<input id="admin-newuser-logout-after-purchase" type="checkbox" name="logout_after_purchase" /><br/>
<input type="submit" value="Create User" />
</form>

View file

@ -56,5 +56,15 @@
{% endblock %}
</footer>
{% if eanwebsocket %}
<script>
function connect() {
let socket = new WebSocket("{{ eanwebsocket }}");
socket.onclose = () => { setTimeout(connect, 1000); };
socket.onmessage = {% block eanwebsocket %}() => {}{% endblock %};
}
window.addEventListener("load", () => { connect(); });
</script>
{% endif %}
</body>
</html>

View file

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

View file

@ -53,7 +53,7 @@
{% if product.custom_price %}
<a onclick="setup_custom_price({{ product.id }}, '{{ product.name}}');">
{% else %}
<a href="/buy?pid={{ product.id }}">
<a {% if product.ean %}id="a-buy-ean{{ product.ean }}"{% endif %} href="/buy?pid={{ product.id }}">
{% endif %}
<span class="thumblist-title">{{ product.name }}</span>
{% 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 #}
<script>setTimeout(window.close, 3000);</script>
{% 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 %}

View file

@ -33,9 +33,10 @@
{{ super() }}
{% if closetab | default(0) %}
{# This only works in Firefox with dom.allow_scripts_to_close_windows=true #}
<script>setTimeout(window.close, 3000);</script>
{% endif %}
{% endblock %}
{% block eanwebsocket %}
function (e) {
document.location = "?ean=" + e.data;
}
{% endblock %}