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
This commit is contained in:
parent
4eb71415fd
commit
f614fe1afc
15 changed files with 99 additions and 36 deletions
16
CHANGELOG.md
16
CHANGELOG.md
|
@ -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
|
||||
|
||||
|
|
|
@ -1,2 +1,2 @@
|
|||
|
||||
__version__ = '0.3.14'
|
||||
__version__ = '0.3.15'
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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('/')
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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')
|
||||
|
|
|
@ -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]
|
||||
|
|
|
@ -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 %}
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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 %}
|
||||
|
|
|
@ -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 %}
|
||||
|
|
|
@ -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 %}
|
||||
|
|
Loading…
Reference in a new issue