diff --git a/CHANGELOG.md b/CHANGELOG.md index 6d0fc62..b72c937 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,20 @@ # Matemat Changelog + +## Version 0.3.14 + +Improvement of quick-purchase via EAN codes + +### Changes + + +- fix: show the purchase warning banner also on the touchkey login +- feat: replace overlay system with a generic notification banner system +- feat: add a config option to automatically close tabs after ean purchase + + + + ## Version 0.3.13 diff --git a/matemat/__init__.py b/matemat/__init__.py index 56b0657..d9195dc 100644 --- a/matemat/__init__.py +++ b/matemat/__init__.py @@ -1,2 +1,2 @@ -__version__ = '0.3.13' +__version__ = '0.3.14' diff --git a/matemat/webserver/pagelets/buy.py b/matemat/webserver/pagelets/buy.py index 84b7855..96296b4 100644 --- a/matemat/webserver/pagelets/buy.py +++ b/matemat/webserver/pagelets/buy.py @@ -26,6 +26,7 @@ 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: @@ -37,7 +38,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}') + redirect(f'/logout?lastaction=buy&lastproduct={pid}&lastprice={price}&closetab={closetab}') # Redirect to the main page (where this request should have come from) - redirect(f'/?lastaction=buy&lastproduct={pid}&lastprice={price}') + redirect(f'/?lastaction=buy&lastproduct={pid}&lastprice={price}&closetab={closetab}') redirect('/') diff --git a/matemat/webserver/pagelets/main.py b/matemat/webserver/pagelets/main.py index 37c898a..6cf7c65 100644 --- a/matemat/webserver/pagelets/main.py +++ b/matemat/webserver/pagelets/main.py @@ -4,7 +4,9 @@ from bottle import route, redirect, request from matemat.db import MatematDatabase from matemat.webserver import template, session +from matemat.webserver.template import Notification from matemat.webserver.config import get_app_config, get_stock_provider +from matemat.util.currency_format import format_chf @route('/') @@ -18,18 +20,21 @@ def main_page(): with MatematDatabase(config['DatabaseFile']) as db: # Fetch the list of products to display products = db.list_products() - if request.params.lastproduct: - lastproduct = db.get_product(request.params.lastproduct) - else: - lastproduct = None + 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) + elif request.params.lastaction == 'buy' and lastprice and request.params.lastproduct: + 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) + Notification.success( + f'Login will purchase {buyproduct.name}. Click here to abort.') except ValueError: - buyproduct = None - else: - buyproduct = None + 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 @@ -37,14 +42,16 @@ def main_page(): authlevel: int = session.get(session_id, 'authentication_level') # If an EAN code was scanned, directly trigger the purchase if buyproduct: - redirect(f'/buy?pid={buyproduct.id}') + url = f'/buy?pid={buyproduct.id}' + if config.get('CloseTabAfterEANPurchase', '0') == '1': + url += '&closetab=1' + redirect(url) # Fetch the user object from the database (for name display, price calculation and admin check) users = db.list_users() user = db.get_user(uid) # Prepare a response with a jinja2 template return template.render('productlist.html', authuser=user, users=users, products=products, authlevel=authlevel, - lastaction=request.params.lastaction, lastprice=lastprice, lastproduct=lastproduct, stock=get_stock_provider(), setupname=config['InstanceName'], now=now) else: # If there are no admin users registered, jump to the admin creation procedure @@ -55,5 +62,4 @@ def main_page(): return template.render('userlist.html', users=users, setupname=config['InstanceName'], now=now, signup=(config.get('SignupEnabled', '0') == '1'), - lastaction=request.params.lastaction, lastprice=lastprice, lastproduct=lastproduct, - buyproduct=buyproduct) + buyproduct=buyproduct, closetab=closetab) diff --git a/matemat/webserver/pagelets/touchkey.py b/matemat/webserver/pagelets/touchkey.py index dc94876..3494940 100644 --- a/matemat/webserver/pagelets/touchkey.py +++ b/matemat/webserver/pagelets/touchkey.py @@ -4,6 +4,7 @@ from matemat.db import MatematDatabase from matemat.db.primitives import User from matemat.exceptions import AuthenticationError from matemat.webserver import template, session +from matemat.webserver.template import Notification from matemat.webserver.config import get_app_config @@ -16,36 +17,45 @@ def touchkey_page(): """ config = get_app_config() session_id: str = session.start() - # If a user is already logged in, simply redirect to the main page, showing the product list - if session.has(session_id, 'authenticated_user'): - redirect('/') - # If requested via HTTP GET, render the login page showing the touchkey UI - if request.method == 'GET': - if request.params.buypid: - buypid = str(request.params.buypid) - else: + with MatematDatabase(config['DatabaseFile']) as db: + # If a user is already logged in, simply redirect to the main page, showing the product list + if session.has(session_id, 'authenticated_user'): + redirect('/') + # If requested via HTTP GET, render the login page showing the touchkey UI + if request.method == 'GET': buypid = None - return template.render('touchkey.html', - username=str(request.params.username), uid=int(str(request.params.uid)), - setupname=config['InstanceName'], buypid=buypid) - # If requested via HTTP POST, read the request arguments and attempt to log in with the provided credentials - elif request.method == 'POST': - # Connect to the database - with MatematDatabase(config['DatabaseFile']) as db: - try: - # Read the request arguments and attempt to log in with them - user: User = db.login(str(request.params.username), touchkey=str(request.params.touchkey)) - except AuthenticationError: - # Reload the touchkey login page on failure - redirect(f'/touchkey?uid={str(request.params.uid)}&username={str(request.params.username)}') - # Set the user ID session variable - session.put(session_id, 'authenticated_user', user.id) - # Set the authlevel session variable (0 = none, 1 = touchkey, 2 = password login) - session.put(session_id, 'authentication_level', 1) - if request.params.buypid: - buypid = str(request.params.buypid) - 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 - abort(405) + if request.params.buypid: + buypid = str(request.params.buypid) + try: + buyproduct = db.get_product(int(buypid)) + Notification.success( + f'Login will purchase {buyproduct.name}. Click here to abort.') + except ValueError: + Notification.error(f'No product with id {buypid}', decay=True) + return template.render('touchkey.html', + username=str(request.params.username), uid=int(str(request.params.uid)), + setupname=config['InstanceName'], buypid=buypid) + # If requested via HTTP POST, read the request arguments and attempt to log in with the provided credentials + elif request.method == 'POST': + # Connect to the database + with MatematDatabase(config['DatabaseFile']) as db: + try: + # Read the request arguments and attempt to log in with them + user: User = db.login(str(request.params.username), touchkey=str(request.params.touchkey)) + except AuthenticationError: + # Reload the touchkey login page on failure + redirect(f'/touchkey?uid={str(request.params.uid)}&username={str(request.params.username)}') + # Set the user ID session variable + session.put(session_id, 'authenticated_user', user.id) + # Set the authlevel session variable (0 = none, 1 = touchkey, 2 = password login) + 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 to the main page, showing the product list + redirect('/') + # If neither GET nor POST was used, show a 405 Method Not Allowed error page + abort(405) diff --git a/matemat/webserver/template/__init__.py b/matemat/webserver/template/__init__.py index 919a08f..353495a 100644 --- a/matemat/webserver/template/__init__.py +++ b/matemat/webserver/template/__init__.py @@ -1,2 +1,3 @@ +from .notification import Notification from .template import init, render diff --git a/matemat/webserver/template/notification.py b/matemat/webserver/template/notification.py new file mode 100644 index 0000000..a82690d --- /dev/null +++ b/matemat/webserver/template/notification.py @@ -0,0 +1,25 @@ + + +class Notification: + notifications = [] + + def __init__(self, msg: str, classes=None, decay: bool = False): + self.msg = msg + self.classes = [] + self.classes.extend(classes) + if decay: + self.classes.append('decay') + + @classmethod + def render(cls): + n = list(cls.notifications) + cls.notifications.clear() + return n + + @classmethod + def success(cls, msg: str, decay: bool = False): + cls.notifications.append(cls(msg, classes=['success'], decay=decay)) + + @classmethod + def error(cls, msg: str, decay: bool = False): + cls.notifications.append(cls(msg, classes=['error'], decay=decay)) diff --git a/matemat/webserver/template/template.py b/matemat/webserver/template/template.py index 0238776..e3b04ad 100644 --- a/matemat/webserver/template/template.py +++ b/matemat/webserver/template/template.py @@ -5,6 +5,7 @@ import jinja2 from matemat import __version__ from matemat.util.currency_format import format_chf +from matemat.webserver.template import Notification __jinja_env: jinja2.Environment = None @@ -22,4 +23,8 @@ def init(config: Dict[str, Any]) -> None: def render(name: str, **kwargs): global __jinja_env template: jinja2.Template = __jinja_env.get_template(name) - return template.render(__version__=__version__, **kwargs).encode('utf-8') + return template.render( + __version__=__version__, + notifications=Notification.render(), + **kwargs + ).encode('utf-8') diff --git a/package/debian/matemat/etc/matemat.conf b/package/debian/matemat/etc/matemat.conf index c28fcd1..b0ba760 100644 --- a/package/debian/matemat/etc/matemat.conf +++ b/package/debian/matemat/etc/matemat.conf @@ -37,6 +37,11 @@ InstanceName=Matemat #SignupEnabled=1 #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 +# +#CloseTabAfterEANPurchase=1 # Add static HTTP headers in this section # [HttpHeaders] diff --git a/static/css/matemat.css b/static/css/matemat.css index 87dbae2..cfe432c 100644 --- a/static/css/matemat.css +++ b/static/css/matemat.css @@ -57,8 +57,19 @@ nav div { width: calc(100% - 36px); margin: 10px; padding: 10px; +} +.notification.success { background-color: #c0ffc0; } +.notification.error { + background-color: #ffc0c0; +} +.notification.decay { + animation: notificationdecay 0s 7s forwards; +} +@keyframes notificationdecay { + to { display: none; } +} @media print { footer { @@ -306,37 +317,3 @@ div.osk-button.osk-button-space { flex: 5 0 1px; color: #606060; } - -aside#overlay { - position: absolute; - top: 0; - right: 0; - bottom: 0; - left: 0; - background: #88ff88; - text-align: center; - z-index: 1000; - padding: 5%; - font-family: sans-serif; - display: none; - transition: opacity 700ms; - opacity: 0; -} - -aside#overlay.fade { - opacity: 100%; -} - -aside#overlay > h2 { - font-size: 3em; -} - -aside#overlay > img { - width: 30%; - height: auto; -} - -aside#overlay > div.price { - padding-top: 30px; - font-size: 2em; -} diff --git a/static/js/overlay.js b/static/js/overlay.js deleted file mode 100644 index d970d7c..0000000 --- a/static/js/overlay.js +++ /dev/null @@ -1,18 +0,0 @@ - -setTimeout(() => { - let overlay = document.getElementById('overlay'); - if (overlay !== null) { - overlay.style.display = 'block'; - setTimeout(() => { - overlay.classList.add('fade'); - setTimeout(() => { - setTimeout(() => { - overlay.classList.remove('fade'); - setTimeout(() => { - overlay.style.display = 'none'; - }, 700); - }, 700); - }, 700); - }, 10); - } -}, 0); \ No newline at end of file diff --git a/templates/base.html b/templates/base.html index 25f702e..9134b7e 100644 --- a/templates/base.html +++ b/templates/base.html @@ -12,27 +12,6 @@
-{% block overlay %} -{% if lastaction is defined and lastaction is not none %} -{% if lastaction == 'buy' %} - -{% elif lastaction == 'deposit' %} - -{% endif %} -{% endif %} -{% endblock %} -