fix: show the purchase warning banner also on the touchkey login
All checks were successful
/ test (push) Successful in 1m22s
/ codestyle (push) Successful in 1m5s
/ build_wheel (push) Successful in 1m59s
/ build_debian (push) Successful in 2m32s

feat: replace overlay system with a generic notification banner system
feat: add a config option to automatically close tabs after ean purchase
This commit is contained in:
s3lph 2024-11-23 09:48:53 +01:00
parent 8287dc1947
commit 4eb71415fd
Signed by: s3lph
GPG key ID: 0AA29A52FB33CFB5
14 changed files with 141 additions and 128 deletions

View file

@ -1,5 +1,20 @@
# Matemat Changelog
<!-- BEGIN RELEASE v0.3.14 -->
## Version 0.3.14
Improvement of quick-purchase via EAN codes
### Changes
<!-- BEGIN CHANGES 0.3.14 -->
- 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
<!-- END CHANGES 0.3.14 -->
<!-- END RELEASE v0.3.14 -->
<!-- BEGIN RELEASE v0.3.13 -->
## Version 0.3.13

View file

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

View file

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

View file

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

View file

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

View file

@ -1,2 +1,3 @@
from .notification import Notification
from .template import init, render

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -12,27 +12,6 @@
<body>
{% block overlay %}
{% if lastaction is defined and lastaction is not none %}
{% if lastaction == 'buy' %}
<aside id="overlay">
<h2>{{ lastproduct.name }}</h2>
<img src="/static/upload/thumbnails/products/{{ lastproduct.id }}.png?cacheBuster={{ now }}" alt="Picture of {{ lastproduct.name }}" draggable="false"/>
{% if lastprice is not none %}
<div class="price">{{ lastprice|chf }}</div>
{% endif %}
</aside>
{% elif lastaction == 'deposit' %}
<aside id="overlay">
<h2>Deposit</h2>
{% if lastprice is not none %}
<div class="price">{{ lastprice|chf }}</div>
{% endif %}
</aside>
{% endif %}
{% endif %}
{% endblock %}
<header>
{% block header %}
@ -55,6 +34,11 @@
</header>
<main>
{% block notifications %}
{% for n in notifications | default([]) %}
<div class="notification {{ n.classes | join(' ') }}">{{ n.msg|safe }}</div>
{% endfor %}
{% endblock %}
{% block main %}
{# Here be content. #}
{% endblock %}
@ -72,6 +56,5 @@
{% endblock %}
</footer>
<script src="/static/js/overlay.js"></script>
</body>
</html>

View file

@ -81,4 +81,9 @@
{{ 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 %}

View file

@ -8,13 +8,6 @@
{% block main %}
{% if buyproduct %}
<div class="notification">
Login will purchase <strong>{{ buyproduct.name }}</strong>.
Click <a href="/">here</a> to abort.
</div>
{% endif %}
{% for user in users %}
{# Show an item per user, consisting of the username, and the avatar, linking to the touchkey login #}
<div class="thumblist-item">
@ -40,4 +33,9 @@
{{ 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 %}