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
This commit is contained in:
parent
8287dc1947
commit
4eb71415fd
14 changed files with 141 additions and 128 deletions
15
CHANGELOG.md
15
CHANGELOG.md
|
@ -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
|
||||
|
||||
|
|
|
@ -1,2 +1,2 @@
|
|||
|
||||
__version__ = '0.3.13'
|
||||
__version__ = '0.3.14'
|
||||
|
|
|
@ -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('/')
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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,15 +17,21 @@ def touchkey_page():
|
|||
"""
|
||||
config = get_app_config()
|
||||
session_id: str = session.start()
|
||||
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
|
||||
if request.params.buypid:
|
||||
buypid = str(request.params.buypid)
|
||||
else:
|
||||
buypid = None
|
||||
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)
|
||||
|
@ -44,7 +51,10 @@ def touchkey_page():
|
|||
session.put(session_id, 'authentication_level', 1)
|
||||
if request.params.buypid:
|
||||
buypid = str(request.params.buypid)
|
||||
redirect(f'/buy?pid={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
|
||||
|
|
|
@ -1,2 +1,3 @@
|
|||
|
||||
from .notification import Notification
|
||||
from .template import init, render
|
||||
|
|
25
matemat/webserver/template/notification.py
Normal file
25
matemat/webserver/template/notification.py
Normal 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))
|
|
@ -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')
|
||||
|
|
|
@ -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]
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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);
|
|
@ -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>
|
||||
|
|
|
@ -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 %}
|
||||
|
|
|
@ -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 %}
|
||||
|
|
Loading…
Reference in a new issue