forked from s3lph/matemat
feat: redesign ui using bootstrap
feat: split user settings and admin settings fix: list user tokens in admin user settings feat!: remove osk, osk should be provided by kiosk browser
This commit is contained in:
parent
b3b47b6b60
commit
67e2a813d5
32 changed files with 959 additions and 1016 deletions
16
CHANGELOG.md
16
CHANGELOG.md
|
@ -1,5 +1,21 @@
|
||||||
# Matemat Changelog
|
# Matemat Changelog
|
||||||
|
|
||||||
|
<!-- BEGIN RELEASE v0.4.0 -->
|
||||||
|
## Version 0.4.0
|
||||||
|
|
||||||
|
Bootstrap UI Release
|
||||||
|
|
||||||
|
### Changes
|
||||||
|
|
||||||
|
<!-- BEGIN CHANGES 0.4.0 -->
|
||||||
|
- feat: redesign ui using bootstrap
|
||||||
|
- feat: split user settings and admin settings
|
||||||
|
- fix: list user tokens in admin user settings
|
||||||
|
- feat!: remove osk, osk should be provided by kiosk browser
|
||||||
|
<!-- END CHANGES 0.4.0 -->
|
||||||
|
|
||||||
|
<!-- END RELEASE v0.4.0 -->
|
||||||
|
|
||||||
<!-- BEGIN RELEASE v0.3.18 -->
|
<!-- BEGIN RELEASE v0.3.18 -->
|
||||||
## Version 0.3.18
|
## Version 0.3.18
|
||||||
|
|
||||||
|
|
|
@ -1,2 +1,2 @@
|
||||||
|
|
||||||
__version__ = '0.3.18'
|
__version__ = '0.4.0'
|
||||||
|
|
|
@ -18,3 +18,4 @@ from .moduser import moduser
|
||||||
from .modproduct import modproduct
|
from .modproduct import modproduct
|
||||||
from .userbootstrap import userbootstrap
|
from .userbootstrap import userbootstrap
|
||||||
from .statistics import statistics
|
from .statistics import statistics
|
||||||
|
from .settings import settings
|
||||||
|
|
|
@ -20,8 +20,7 @@ from matemat.webserver.template import Notification
|
||||||
@post('/admin')
|
@post('/admin')
|
||||||
def admin():
|
def admin():
|
||||||
"""
|
"""
|
||||||
The admin panel, shows a user's own settings. Additionally, for administrators, settings to modify other users and
|
The admin panel, shows settings to modify other users and products.
|
||||||
products are shown.
|
|
||||||
"""
|
"""
|
||||||
config = get_app_config()
|
config = get_app_config()
|
||||||
session_id: str = session.start()
|
session_id: str = session.start()
|
||||||
|
@ -38,18 +37,15 @@ def admin():
|
||||||
with MatematDatabase(config['DatabaseFile']) as db:
|
with MatematDatabase(config['DatabaseFile']) as db:
|
||||||
# Fetch the authenticated user
|
# Fetch the authenticated user
|
||||||
user = db.get_user(uid)
|
user = db.get_user(uid)
|
||||||
# If the POST request contains a "change" parameter, delegate the change handling to the function below
|
|
||||||
if 'change' in request.params:
|
|
||||||
handle_change(request.params, request.files, user, db)
|
|
||||||
# If the POST request contains an "adminchange" parameter, delegate the change handling to the function below
|
# If the POST request contains an "adminchange" parameter, delegate the change handling to the function below
|
||||||
elif 'adminchange' in request.params and user.is_admin:
|
if 'adminchange' in request.params and user.is_admin:
|
||||||
handle_admin_change(request.params, request.files, db)
|
handle_change(request.params, request.files, db)
|
||||||
|
|
||||||
# Fetch all existing users and products from the database
|
# Fetch all existing users and products from the database
|
||||||
users = db.list_users()
|
users = db.list_users()
|
||||||
tokens = db.list_tokens(uid)
|
tokens = db.list_tokens(uid)
|
||||||
products = db.list_products()
|
products = db.list_products()
|
||||||
# Render the "Admin/Settings" page
|
# Render the "Admin" page
|
||||||
now = str(int(datetime.now(UTC).timestamp()))
|
now = str(int(datetime.now(UTC).timestamp()))
|
||||||
return template.render('admin.html',
|
return template.render('admin.html',
|
||||||
authuser=user, authlevel=authlevel, tokens=tokens, users=users, products=products,
|
authuser=user, authlevel=authlevel, tokens=tokens, users=users, products=products,
|
||||||
|
@ -57,134 +53,7 @@ def admin():
|
||||||
setupname=config['InstanceName'], config_smtp_enabled=config['SmtpSendReceipts'])
|
setupname=config['InstanceName'], config_smtp_enabled=config['SmtpSendReceipts'])
|
||||||
|
|
||||||
|
|
||||||
def handle_change(args: FormsDict, files: FormsDict, user: User, db: MatematDatabase) -> None:
|
def handle_change(args: FormsDict, files: FormsDict, db: MatematDatabase):
|
||||||
"""
|
|
||||||
Write the changes requested by a user for its own account to the database.
|
|
||||||
|
|
||||||
:param args: The FormsDict object passed to the pagelet.
|
|
||||||
:param user: The user to edit.
|
|
||||||
:param db: The database facade where changes are written to.
|
|
||||||
"""
|
|
||||||
config = get_app_config()
|
|
||||||
try:
|
|
||||||
# Read the type of change requested by the user, then switch over it
|
|
||||||
change = str(args.change)
|
|
||||||
|
|
||||||
# The user requested a modification of its general account information (username, email)
|
|
||||||
if change == 'account':
|
|
||||||
# Username and email must be set in the request arguments
|
|
||||||
if 'username' not in args or 'email' not in args:
|
|
||||||
return
|
|
||||||
username = str(args.username)
|
|
||||||
email = str(args.email)
|
|
||||||
logout_after_purchase = 'logout_after_purchase' in args
|
|
||||||
# An empty e-mail field should be interpreted as NULL
|
|
||||||
if len(email) == 0:
|
|
||||||
email = None
|
|
||||||
try:
|
|
||||||
receipt_pref = ReceiptPreference(int(str(args.receipt_pref)))
|
|
||||||
except ValueError:
|
|
||||||
return
|
|
||||||
# Attempt to update username, e-mail and receipt preference
|
|
||||||
try:
|
|
||||||
db.change_user(user, agent=None, name=username, email=email, receipt_pref=receipt_pref,
|
|
||||||
logout_after_purchase=logout_after_purchase)
|
|
||||||
except DatabaseConsistencyError:
|
|
||||||
return
|
|
||||||
|
|
||||||
# The user requested a password change
|
|
||||||
elif change == 'password':
|
|
||||||
# The old password and 2x the new password must be present
|
|
||||||
if 'oldpass' not in args or 'newpass' not in args or 'newpass2' not in args:
|
|
||||||
return
|
|
||||||
# Read the passwords from the request arguments
|
|
||||||
oldpass = str(args.oldpass)
|
|
||||||
newpass = str(args.newpass)
|
|
||||||
newpass2 = str(args.newpass2)
|
|
||||||
# The two instances of the new password must match
|
|
||||||
if newpass != newpass2:
|
|
||||||
raise ValueError('New passwords don\'t match')
|
|
||||||
# Write the new password to the database
|
|
||||||
try:
|
|
||||||
db.change_password(user, oldpass, newpass)
|
|
||||||
except AuthenticationError:
|
|
||||||
raise ValueError('Old password doesn\'t match')
|
|
||||||
|
|
||||||
# The user requested a touchkey change
|
|
||||||
elif change == 'touchkey':
|
|
||||||
# The touchkey must be present
|
|
||||||
if 'touchkey' not in args:
|
|
||||||
return
|
|
||||||
# Read the touchkey from the request arguments
|
|
||||||
touchkey = str(args.touchkey)
|
|
||||||
# An empty touchkey field should set the touchkey to NULL (disable touchkey login)
|
|
||||||
if len(touchkey) == 0:
|
|
||||||
touchkey = None
|
|
||||||
# Write the new touchkey to the database
|
|
||||||
db.change_touchkey(user, '', touchkey, verify_password=False)
|
|
||||||
|
|
||||||
# The user added a new token
|
|
||||||
elif change == 'addtoken':
|
|
||||||
if 'token' not in args:
|
|
||||||
return
|
|
||||||
token = str(args.token)
|
|
||||||
if len(token) < 6:
|
|
||||||
return
|
|
||||||
name = None if 'name' not in args or len(args.name) == 0 else str(args.name)
|
|
||||||
try:
|
|
||||||
tokobj = db.add_token(user, token, name)
|
|
||||||
Notification.success(f'Token {tokobj.name} created successfully')
|
|
||||||
except DatabaseConsistencyError:
|
|
||||||
Notification.error('Token already exists', decay=True)
|
|
||||||
|
|
||||||
elif change == 'deltoken':
|
|
||||||
try:
|
|
||||||
tokid = int(str(request.params.token))
|
|
||||||
token = db.get_token(tokid)
|
|
||||||
except Exception as e:
|
|
||||||
Notification.error('Token not found', decay=True)
|
|
||||||
return
|
|
||||||
if token.user_id != user.id:
|
|
||||||
Notification.error('Token not found', decay=True)
|
|
||||||
return
|
|
||||||
try:
|
|
||||||
db.delete_token(token)
|
|
||||||
except DatabaseConsistencyError:
|
|
||||||
Notification.error(f'Failed to delete token {token.name}', decay=True)
|
|
||||||
Notification.success(f'Token {token.name} removed', decay=True)
|
|
||||||
|
|
||||||
# The user requested an avatar change
|
|
||||||
elif change == 'avatar':
|
|
||||||
# The new avatar field must be present
|
|
||||||
if 'avatar' not in files:
|
|
||||||
return
|
|
||||||
# Read the raw image data from the request
|
|
||||||
avatar = files.avatar.file.read()
|
|
||||||
# Only process the image, if its size is more than zero. Zero size means no new image was uploaded
|
|
||||||
if len(avatar) == 0:
|
|
||||||
return
|
|
||||||
# Detect the MIME type
|
|
||||||
filemagic: magic.FileMagic = magic.detect_from_content(avatar)
|
|
||||||
if not filemagic.mime_type.startswith('image/'):
|
|
||||||
return
|
|
||||||
# Create the absolute path of the upload directory
|
|
||||||
abspath: str = os.path.join(os.path.abspath(config['UploadDir']), 'thumbnails/users/')
|
|
||||||
os.makedirs(abspath, exist_ok=True)
|
|
||||||
try:
|
|
||||||
# Parse the image data
|
|
||||||
image: Image = Image.open(BytesIO(avatar))
|
|
||||||
# Resize the image to 150x150
|
|
||||||
image.thumbnail((150, 150), Image.LANCZOS)
|
|
||||||
# Write the image to the file
|
|
||||||
image.save(os.path.join(abspath, f'{user.id}.png'), 'PNG')
|
|
||||||
except OSError:
|
|
||||||
return
|
|
||||||
|
|
||||||
except UnicodeDecodeError:
|
|
||||||
raise ValueError('an argument not a string')
|
|
||||||
|
|
||||||
|
|
||||||
def handle_admin_change(args: FormsDict, files: FormsDict, db: MatematDatabase):
|
|
||||||
"""
|
"""
|
||||||
Write the changes requested by an admin for users of products.
|
Write the changes requested by an admin for users of products.
|
||||||
|
|
||||||
|
|
|
@ -21,7 +21,7 @@ def login_page():
|
||||||
redirect('/')
|
redirect('/')
|
||||||
# If requested via HTTP GET, render the login page showing the login UI
|
# If requested via HTTP GET, render the login page showing the login UI
|
||||||
if request.method == 'GET':
|
if request.method == 'GET':
|
||||||
return template.render('login.html',
|
return template.render('login.html', signup=(config.get('SignupEnabled', '0') == '1'),
|
||||||
setupname=config['InstanceName'])
|
setupname=config['InstanceName'])
|
||||||
# If requested via HTTP POST, read the request arguments and attempt to log in with the provided credentials
|
# If requested via HTTP POST, read the request arguments and attempt to log in with the provided credentials
|
||||||
elif request.method == 'POST':
|
elif request.method == 'POST':
|
||||||
|
|
|
@ -27,7 +27,8 @@ def main_page():
|
||||||
try:
|
try:
|
||||||
buyproduct = db.get_product_by_ean(request.params.ean)
|
buyproduct = db.get_product_by_ean(request.params.ean)
|
||||||
Notification.success(
|
Notification.success(
|
||||||
f'Login will purchase <strong>{buyproduct.name}</strong>. Click <a href="/">here</a> to abort.')
|
f'Login will purchase <strong>{buyproduct.name}</strong>. ' +
|
||||||
|
'Click <a class="alert-link" href="/">here</a> to abort.')
|
||||||
except ValueError:
|
except ValueError:
|
||||||
if not session.has(session_id, 'authenticated_user'):
|
if not session.has(session_id, 'authenticated_user'):
|
||||||
try:
|
try:
|
||||||
|
|
|
@ -56,10 +56,11 @@ def moduser():
|
||||||
redirect('/admin')
|
redirect('/admin')
|
||||||
|
|
||||||
# Render the "Modify User" page
|
# Render the "Modify User" page
|
||||||
|
tokens = db.list_tokens(moduser_id)
|
||||||
now = str(int(datetime.now(UTC).timestamp()))
|
now = str(int(datetime.now(UTC).timestamp()))
|
||||||
return template.render('moduser.html',
|
return template.render('moduser.html',
|
||||||
authuser=authuser, user=user, authlevel=authlevel, now=now,
|
authuser=authuser, user=user, authlevel=authlevel, now=now,
|
||||||
receipt_preference_class=ReceiptPreference,
|
receipt_preference_class=ReceiptPreference, tokens=tokens,
|
||||||
setupname=config['InstanceName'], config_smtp_enabled=config['SmtpSendReceipts'])
|
setupname=config['InstanceName'], config_smtp_enabled=config['SmtpSendReceipts'])
|
||||||
|
|
||||||
|
|
||||||
|
|
178
matemat/webserver/pagelets/settings.py
Normal file
178
matemat/webserver/pagelets/settings.py
Normal file
|
@ -0,0 +1,178 @@
|
||||||
|
import os
|
||||||
|
from datetime import datetime, UTC
|
||||||
|
from io import BytesIO
|
||||||
|
from shutil import copyfile
|
||||||
|
|
||||||
|
import magic
|
||||||
|
from PIL import Image
|
||||||
|
from bottle import get, post, abort, redirect, request, FormsDict
|
||||||
|
|
||||||
|
from matemat.db import MatematDatabase
|
||||||
|
from matemat.db.primitives import User, ReceiptPreference
|
||||||
|
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('/settings')
|
||||||
|
@post('/settings')
|
||||||
|
def settings():
|
||||||
|
"""
|
||||||
|
The settings panel, shows a user's own settings.
|
||||||
|
"""
|
||||||
|
config = get_app_config()
|
||||||
|
session_id: str = session.start()
|
||||||
|
# If no user is logged in, redirect to the login page
|
||||||
|
if not session.has(session_id, 'authentication_level') or not session.has(session_id, 'authenticated_user'):
|
||||||
|
redirect('/login')
|
||||||
|
authlevel: int = session.get(session_id, 'authentication_level')
|
||||||
|
uid: int = session.get(session_id, 'authenticated_user')
|
||||||
|
# Show a 403 Forbidden error page if no user is logged in (0) or a user logged in via touchkey or token (1)
|
||||||
|
if authlevel < 2:
|
||||||
|
abort(403)
|
||||||
|
|
||||||
|
# Connect to the database
|
||||||
|
with MatematDatabase(config['DatabaseFile']) as db:
|
||||||
|
# Fetch the authenticated user
|
||||||
|
user = db.get_user(uid)
|
||||||
|
# If the POST request contains a "change" parameter, delegate the change handling to the function below
|
||||||
|
if 'change' in request.params:
|
||||||
|
handle_change(request.params, request.files, user, db)
|
||||||
|
|
||||||
|
user = db.get_user(uid)
|
||||||
|
tokens = db.list_tokens(uid)
|
||||||
|
# Render the "Settings" page
|
||||||
|
now = str(int(datetime.now(UTC).timestamp()))
|
||||||
|
return template.render('settings.html',
|
||||||
|
authuser=user, authlevel=authlevel, tokens=tokens,
|
||||||
|
receipt_preference_class=ReceiptPreference, now=now,
|
||||||
|
setupname=config['InstanceName'], config_smtp_enabled=config['SmtpSendReceipts'])
|
||||||
|
|
||||||
|
|
||||||
|
def handle_change(args: FormsDict, files: FormsDict, user: User, db: MatematDatabase) -> None:
|
||||||
|
"""
|
||||||
|
Write the changes requested by a user for its own account to the database.
|
||||||
|
|
||||||
|
:param args: The FormsDict object passed to the pagelet.
|
||||||
|
:param user: The user to edit.
|
||||||
|
:param db: The database facade where changes are written to.
|
||||||
|
"""
|
||||||
|
config = get_app_config()
|
||||||
|
try:
|
||||||
|
# Read the type of change requested by the user, then switch over it
|
||||||
|
change = str(args.change)
|
||||||
|
|
||||||
|
# The user requested a modification of its general account information (username, email)
|
||||||
|
if change == 'account':
|
||||||
|
# Username and email must be set in the request arguments
|
||||||
|
if 'username' not in args or 'email' not in args:
|
||||||
|
return
|
||||||
|
username = str(args.username)
|
||||||
|
email = str(args.email)
|
||||||
|
logout_after_purchase = 'logout_after_purchase' in args
|
||||||
|
# An empty e-mail field should be interpreted as NULL
|
||||||
|
if len(email) == 0:
|
||||||
|
email = None
|
||||||
|
try:
|
||||||
|
receipt_pref = ReceiptPreference(int(str(args.receipt_pref)))
|
||||||
|
except ValueError:
|
||||||
|
return
|
||||||
|
# Attempt to update username, e-mail and receipt preference
|
||||||
|
try:
|
||||||
|
db.change_user(user, agent=None, name=username, email=email, receipt_pref=receipt_pref,
|
||||||
|
logout_after_purchase=logout_after_purchase)
|
||||||
|
except DatabaseConsistencyError:
|
||||||
|
return
|
||||||
|
|
||||||
|
# The user requested a password change
|
||||||
|
elif change == 'password':
|
||||||
|
# The old password and 2x the new password must be present
|
||||||
|
if 'oldpass' not in args or 'newpass' not in args or 'newpass2' not in args:
|
||||||
|
return
|
||||||
|
# Read the passwords from the request arguments
|
||||||
|
oldpass = str(args.oldpass)
|
||||||
|
newpass = str(args.newpass)
|
||||||
|
newpass2 = str(args.newpass2)
|
||||||
|
# The two instances of the new password must match
|
||||||
|
if newpass != newpass2:
|
||||||
|
raise ValueError('New passwords don\'t match')
|
||||||
|
# Write the new password to the database
|
||||||
|
try:
|
||||||
|
db.change_password(user, oldpass, newpass)
|
||||||
|
except AuthenticationError:
|
||||||
|
raise ValueError('Old password doesn\'t match')
|
||||||
|
|
||||||
|
# The user requested a touchkey change
|
||||||
|
elif change == 'touchkey':
|
||||||
|
# The touchkey must be present
|
||||||
|
if 'touchkey' not in args:
|
||||||
|
return
|
||||||
|
# Read the touchkey from the request arguments
|
||||||
|
touchkey = str(args.touchkey)
|
||||||
|
# An empty touchkey field should set the touchkey to NULL (disable touchkey login)
|
||||||
|
if len(touchkey) == 0:
|
||||||
|
touchkey = None
|
||||||
|
# Write the new touchkey to the database
|
||||||
|
db.change_touchkey(user, '', touchkey, verify_password=False)
|
||||||
|
|
||||||
|
# The user added a new token
|
||||||
|
elif change == 'addtoken':
|
||||||
|
if 'token' not in args:
|
||||||
|
return
|
||||||
|
token = str(args.token)
|
||||||
|
if len(token) < 6:
|
||||||
|
return
|
||||||
|
name = None if 'name' not in args or len(args.name) == 0 else str(args.name)
|
||||||
|
try:
|
||||||
|
tokobj = db.add_token(user, token, name)
|
||||||
|
Notification.success(f'Token {tokobj.name} created successfully')
|
||||||
|
except DatabaseConsistencyError:
|
||||||
|
Notification.error('Token already exists', decay=True)
|
||||||
|
|
||||||
|
elif change == 'deltoken':
|
||||||
|
try:
|
||||||
|
tokid = int(str(request.params.token))
|
||||||
|
token = db.get_token(tokid)
|
||||||
|
except Exception as e:
|
||||||
|
Notification.error('Token not found', decay=True)
|
||||||
|
return
|
||||||
|
if token.user_id != user.id:
|
||||||
|
Notification.error('Token not found', decay=True)
|
||||||
|
return
|
||||||
|
try:
|
||||||
|
db.delete_token(token)
|
||||||
|
except DatabaseConsistencyError:
|
||||||
|
Notification.error(f'Failed to delete token {token.name}', decay=True)
|
||||||
|
Notification.success(f'Token {token.name} removed', decay=True)
|
||||||
|
|
||||||
|
# The user requested an avatar change
|
||||||
|
elif change == 'avatar':
|
||||||
|
# The new avatar field must be present
|
||||||
|
if 'avatar' not in files:
|
||||||
|
return
|
||||||
|
# Read the raw image data from the request
|
||||||
|
avatar = files.avatar.file.read()
|
||||||
|
# Only process the image, if its size is more than zero. Zero size means no new image was uploaded
|
||||||
|
if len(avatar) == 0:
|
||||||
|
return
|
||||||
|
# Detect the MIME type
|
||||||
|
filemagic: magic.FileMagic = magic.detect_from_content(avatar)
|
||||||
|
if not filemagic.mime_type.startswith('image/'):
|
||||||
|
return
|
||||||
|
# Create the absolute path of the upload directory
|
||||||
|
abspath: str = os.path.join(os.path.abspath(config['UploadDir']), 'thumbnails/users/')
|
||||||
|
os.makedirs(abspath, exist_ok=True)
|
||||||
|
try:
|
||||||
|
# Parse the image data
|
||||||
|
image: Image = Image.open(BytesIO(avatar))
|
||||||
|
# Resize the image to 150x150
|
||||||
|
image.thumbnail((150, 150), Image.LANCZOS)
|
||||||
|
# Write the image to the file
|
||||||
|
image.save(os.path.join(abspath, f'{user.id}.png'), 'PNG')
|
||||||
|
except OSError:
|
||||||
|
return
|
||||||
|
|
||||||
|
except UnicodeDecodeError:
|
||||||
|
raise ValueError('an argument not a string')
|
|
@ -94,7 +94,9 @@ def signup():
|
||||||
acl = netaddr.IPSet([addr.strip() for addr in config.get('SignupKioskMode', '').split(',')])
|
acl = netaddr.IPSet([addr.strip() for addr in config.get('SignupKioskMode', '').split(',')])
|
||||||
if request.remote_addr in acl:
|
if request.remote_addr in acl:
|
||||||
return template.render('signup_kiosk.html',
|
return template.render('signup_kiosk.html',
|
||||||
|
signup=(config.get('SignupEnabled', '0') == '1'),
|
||||||
zip=zip,
|
zip=zip,
|
||||||
setupname=config['InstanceName'])
|
setupname=config['InstanceName'])
|
||||||
return template.render('signup.html',
|
return template.render('signup.html',
|
||||||
|
signup=(config.get('SignupEnabled', '0') == '1'),
|
||||||
setupname=config['InstanceName'])
|
setupname=config['InstanceName'])
|
||||||
|
|
|
@ -29,10 +29,11 @@ def touchkey_page():
|
||||||
try:
|
try:
|
||||||
buyproduct = db.get_product(int(buypid))
|
buyproduct = db.get_product(int(buypid))
|
||||||
Notification.success(
|
Notification.success(
|
||||||
f'Login will purchase <strong>{buyproduct.name}</strong>. Click <a href="/">here</a> to abort.')
|
f'Login will purchase <strong>{buyproduct.name}</strong>. ' +
|
||||||
|
'Click <a class="alert-link" href="/">here</a> to abort.')
|
||||||
except ValueError:
|
except ValueError:
|
||||||
Notification.error(f'No product with id {buypid}', decay=True)
|
Notification.error(f'No product with id {buypid}', decay=True)
|
||||||
return template.render('touchkey.html',
|
return template.render('touchkey.html', signup=(config.get('SignupEnabled', '0') == '1'),
|
||||||
username=str(request.params.username), uid=int(str(request.params.uid)),
|
username=str(request.params.username), uid=int(str(request.params.uid)),
|
||||||
setupname=config['InstanceName'], buypid=buypid)
|
setupname=config['InstanceName'], buypid=buypid)
|
||||||
# If requested via HTTP POST, read the request arguments and attempt to log in with the provided credentials
|
# If requested via HTTP POST, read the request arguments and attempt to log in with the provided credentials
|
||||||
|
|
|
@ -22,9 +22,9 @@ class Notification:
|
||||||
@classmethod
|
@classmethod
|
||||||
def success(cls, msg: str, decay: bool = False):
|
def success(cls, msg: str, decay: bool = False):
|
||||||
session_id: str = session.start()
|
session_id: str = session.start()
|
||||||
session.setdefault(session_id, 'notifications', []).append(cls(msg, classes=['success'], decay=decay))
|
session.setdefault(session_id, 'notifications', []).append(cls(msg, classes=['alert-success'], decay=decay))
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def error(cls, msg: str, decay: bool = False):
|
def error(cls, msg: str, decay: bool = False):
|
||||||
session_id: str = session.start()
|
session_id: str = session.start()
|
||||||
session.setdefault(session_id, 'notifications', []).append(cls(msg, classes=['error'], decay=decay))
|
session.setdefault(session_id, 'notifications', []).append(cls(msg, classes=['alert-danger'], decay=decay))
|
||||||
|
|
7
static/css/bootstrap.min.css
vendored
Normal file
7
static/css/bootstrap.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
1
static/css/bootstrap.min.css.map
Normal file
1
static/css/bootstrap.min.css.map
Normal file
File diff suppressed because one or more lines are too long
|
@ -1,70 +1,5 @@
|
||||||
* {
|
|
||||||
-webkit-touch-callout: none;
|
|
||||||
-webkit-user-select: none;
|
|
||||||
-khtml-user-select: none;
|
|
||||||
-moz-user-select: none;
|
|
||||||
-ms-user-select: none;
|
|
||||||
user-select: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
nav div {
|
.alert.decay {
|
||||||
display: inline-block;
|
|
||||||
}
|
|
||||||
|
|
||||||
.thumblist-item {
|
|
||||||
display: inline-block;
|
|
||||||
margin: 5px 5px 10px 10px;
|
|
||||||
padding: 15px;
|
|
||||||
background: #f0f0f0;
|
|
||||||
text-decoration: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.thumblist-item a {
|
|
||||||
text-decoration: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.thumblist-item .imgcontainer {
|
|
||||||
width: 100px;
|
|
||||||
height: 100px;
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
|
|
||||||
.thumblist-item .imgcontainer img {
|
|
||||||
max-width: 100px;
|
|
||||||
max-height: 100px;
|
|
||||||
position: absolute;
|
|
||||||
top: 50%;
|
|
||||||
left: 50%;
|
|
||||||
transform: translate(-50%, -50%);
|
|
||||||
}
|
|
||||||
|
|
||||||
.thumblist-title {
|
|
||||||
display: block;
|
|
||||||
font-weight: bolder;
|
|
||||||
}
|
|
||||||
|
|
||||||
.thumblist-stock {
|
|
||||||
position: absolute;
|
|
||||||
z-index: 10;
|
|
||||||
bottom: 0;
|
|
||||||
right: 0;
|
|
||||||
background: #f0f0f0;
|
|
||||||
padding: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.notification {
|
|
||||||
display: block;
|
|
||||||
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;
|
animation: notificationdecay 0s 7s forwards;
|
||||||
}
|
}
|
||||||
@keyframes notificationdecay {
|
@keyframes notificationdecay {
|
||||||
|
@ -83,10 +18,6 @@ nav div {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#depositlist {
|
|
||||||
display: inline;
|
|
||||||
}
|
|
||||||
|
|
||||||
#deposit-wrapper {
|
#deposit-wrapper {
|
||||||
display: none;
|
display: none;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
|
@ -149,6 +80,11 @@ nav div {
|
||||||
font-family: monospace;
|
font-family: monospace;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#touchkey-svg {
|
||||||
|
width: 400px;
|
||||||
|
height: auto;
|
||||||
|
}
|
||||||
|
|
||||||
.numpad {
|
.numpad {
|
||||||
background: #f0f0f0;
|
background: #f0f0f0;
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
|
@ -274,46 +210,26 @@ nav div {
|
||||||
background: #60f060;
|
background: #60f060;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.itemlist a {
|
||||||
div.osk-kbd {
|
text-decoration: none;
|
||||||
display: none;
|
}
|
||||||
font-family: sans-serif;
|
.itemlist a:visited {
|
||||||
position: fixed;
|
text-decoration: none;
|
||||||
left: 0;
|
|
||||||
bottom: 0;
|
|
||||||
right: 0;
|
|
||||||
flex-direction: column;
|
|
||||||
z-index: 100;
|
|
||||||
background: white;
|
|
||||||
font-size: 5vh;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
div.osk-kbd.visible {
|
.itemlist img {
|
||||||
display: flex;
|
max-width: 100%;
|
||||||
|
max-height: 128px;
|
||||||
|
height: auto;
|
||||||
|
object-fit: cover;
|
||||||
}
|
}
|
||||||
|
|
||||||
div.osk-kbd-row {
|
.card-img-overlay {
|
||||||
width: 100%;
|
padding: 0;
|
||||||
height: 10vh;
|
left: auto;
|
||||||
flex-grow: 1;
|
opacity: .7;
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
div.osk-button {
|
.card-img-overlay span {
|
||||||
flex: 1 0 1px;
|
padding: .5em;
|
||||||
background: #f0f0f0;
|
|
||||||
padding: 5px;
|
|
||||||
margin: 2px;
|
|
||||||
text-align: center;
|
|
||||||
line-height: calc(10vh - 10px);
|
|
||||||
}
|
|
||||||
|
|
||||||
div.osk-button:active, div.osk-button.osk-locked {
|
|
||||||
background: #606060;
|
|
||||||
}
|
|
||||||
|
|
||||||
div.osk-button.osk-button-space {
|
|
||||||
flex: 5 0 1px;
|
|
||||||
color: #606060;
|
|
||||||
}
|
}
|
||||||
|
|
7
static/js/bootstrap.bundle.min.js
vendored
Normal file
7
static/js/bootstrap.bundle.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
1
static/js/bootstrap.bundle.min.js.map
Normal file
1
static/js/bootstrap.bundle.min.js.map
Normal file
File diff suppressed because one or more lines are too long
|
@ -15,16 +15,17 @@ let product_id = null;
|
||||||
let target_user = null;
|
let target_user = null;
|
||||||
let target_user_li = null;
|
let target_user_li = null;
|
||||||
let deposit = '0';
|
let deposit = '0';
|
||||||
let button = document.createElement('div');
|
let button = document.createElement('a');
|
||||||
let button_transfer = document.createElement('div');
|
let button_transfer = document.createElement('a');
|
||||||
let input = document.getElementById('deposit-wrapper');
|
let input = document.getElementById('deposit-wrapper');
|
||||||
let amount = document.getElementById('deposit-amount');
|
let amount = document.getElementById('deposit-amount');
|
||||||
let title = document.getElementById('deposit-title');
|
let title = document.getElementById('deposit-title');
|
||||||
let userlist = document.getElementById('transfer-userlist');
|
let userlist = document.getElementById('transfer-userlist');
|
||||||
let userlist_list = document.getElementById('transfer-userlist-list');
|
let userlist_list = document.getElementById('transfer-userlist-list');
|
||||||
let ok_button = document.getElementById('numpad-ok');
|
let ok_button = document.getElementById('numpad-ok');
|
||||||
button.classList.add('thumblist-item');
|
button.classList.add('btn');
|
||||||
button.classList.add('fakelink');
|
button.classList.add('btn-primary');
|
||||||
|
button.classList.add('me-2');
|
||||||
button.innerText = 'Deposit';
|
button.innerText = 'Deposit';
|
||||||
button.onclick = (ev) => {
|
button.onclick = (ev) => {
|
||||||
mode = Mode.Deposit;
|
mode = Mode.Deposit;
|
||||||
|
@ -37,8 +38,9 @@ button.onclick = (ev) => {
|
||||||
userlist.classList.remove('show');
|
userlist.classList.remove('show');
|
||||||
ok_button.classList.remove('disabled');
|
ok_button.classList.remove('disabled');
|
||||||
};
|
};
|
||||||
button_transfer.classList.add('thumblist-item');
|
button_transfer.classList.add('btn');
|
||||||
button_transfer.classList.add('fakelink');
|
button_transfer.classList.add('btn-primary');
|
||||||
|
button_transfer.classList.add('me-2');
|
||||||
button_transfer.innerText = 'Transfer';
|
button_transfer.innerText = 'Transfer';
|
||||||
button_transfer.onclick = (ev) => {
|
button_transfer.onclick = (ev) => {
|
||||||
mode = Mode.Transfer;
|
mode = Mode.Transfer;
|
||||||
|
|
|
@ -1,38 +1,164 @@
|
||||||
{% extends "base.html" %}
|
{% extends "base.html" %}
|
||||||
|
|
||||||
{% block header %}
|
|
||||||
{# If the logged in user is an administrator, call the title "Administration", otherwise "Settings" #}
|
|
||||||
{% if authuser.is_admin %}
|
|
||||||
<h1>Administration</h1>
|
|
||||||
{% else %}
|
|
||||||
<h1>Settings</h1>
|
|
||||||
{% endif %}
|
|
||||||
{{ super() }}
|
|
||||||
{% endblock %}
|
|
||||||
|
|
||||||
{% block main %}
|
{% block main %}
|
||||||
|
|
||||||
{# Always show the settings a user can edit for itself #}
|
<h1>Administration</h1>
|
||||||
{% include "admin_all.html" %}
|
|
||||||
|
|
||||||
{# Only show the "restricted" section if the user is an admin #}
|
<ul class="nav nav-tabs" id="adminTab" role="tablist">
|
||||||
{% if authuser.is_admin %}
|
<li class="nav-item" role="presentation">
|
||||||
{% include "admin_restricted.html" %}
|
<button class="nav-link active" id="admin-users-tab" data-bs-toggle="tab" data-bs-target="#admin-users-tab-pane" type="button" role="tab" aria-controls="admin-users-tab-pane" aria-selected="true">Users</button>
|
||||||
{% endif %}
|
</li>
|
||||||
|
<li class="nav-item" role="presentation">
|
||||||
|
<button class="nav-link" id="admin-products-tab" data-bs-toggle="tab" data-bs-target="#admin-products-tab-pane" type="button" role="tab" aria-controls="admin-products-tab-pane" aria-selected="false">Products</button>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item" role="presentation">
|
||||||
|
<button class="nav-link" id="admin-default-images-tab" data-bs-toggle="tab" data-bs-target="#admin-default-images-tab-pane" type="button" role="tab" aria-controls="admin-default-images-tab-pane" aria-selected="false">Default Images</button>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<div class="tab-content" id="adminTabContent">
|
||||||
|
|
||||||
|
<section class="tab-pane fade pt-3 show active" id="admin-users-tab-pane" role="tabpanel">
|
||||||
|
<h2>Users</h2>
|
||||||
|
|
||||||
|
<form id="admin-newuser-form" method="post" action="/admin?adminchange=newuser" accept-charset="UTF-8">
|
||||||
|
<table class="table table-striped">
|
||||||
|
<tr>
|
||||||
|
<th>Username</th>
|
||||||
|
<th>E-Mail (optional)</th>
|
||||||
|
<th>Password</th>
|
||||||
|
<th>Member</th>
|
||||||
|
<th>Admin</th>
|
||||||
|
<th>Logout after purchase</th>
|
||||||
|
<th>Actions</th>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><input class="form-control" id="admin-newuser-username" type="text" name="username" placeholder="New username"></td>
|
||||||
|
<td><input class="form-control" id="admin-newuser-email" type="text" name="email" placeholder="New e-mail"></td>
|
||||||
|
<td><input class="form-control" id="admin-newuser-password" type="password" name="password" placeholder="New password"></td>
|
||||||
|
<td><input class="form-check-input" id="admin-newuser-ismember" type="checkbox" name="ismember"></td>
|
||||||
|
<td><input class="form-check-input" id="admin-newuser-isadmin" type="checkbox" name="isadmin"></td>
|
||||||
|
<td><input class="form-check-input" id="admin-newuser-logout-after-purchase" type="checkbox" name="logout_after_purchase"></td>
|
||||||
|
<td><input class="btn btn-success" type="submit" value="Create User"></td>
|
||||||
|
</tr>
|
||||||
|
{% for user in users %}
|
||||||
|
<tr>
|
||||||
|
<td>{{ user.name }}</td>
|
||||||
|
<td>{{ '✓' if user.email else '✗' }}</td>
|
||||||
|
<td>••••••••</td>
|
||||||
|
<td>{{ '✓' if user.is_member else '✗' }}</td>
|
||||||
|
<td>{{ '✓' if user.is_admin else '✗' }}</td>
|
||||||
|
<td>{{ '✓' if user.logout_after_purchase else '✗' }}</td>
|
||||||
|
<td>
|
||||||
|
<a class="btn btn-primary" href="/moduser?userid={{ user.id }}">Edit</a>
|
||||||
|
<a class="btn btn-danger" href="/moduser?userid={{ user.id }}&change=del">Delete</a>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</table>
|
||||||
|
</form>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="tab-pane fade pt-3" id="admin-products-tab-pane" role="tabpanel">
|
||||||
|
<h2>Products</h2>
|
||||||
|
|
||||||
|
<form id="admin-newproduct-form" method="post" action="/admin?adminchange=newproduct" enctype="multipart/form-data" accept-charset="UTF-8">
|
||||||
|
<table class="table table-striped">
|
||||||
|
<tr>
|
||||||
|
<th>Name</th>
|
||||||
|
<th>EAN code</th>
|
||||||
|
<th>Member price</th>
|
||||||
|
<th>Non-member price</th>
|
||||||
|
<th>Custom price</th>
|
||||||
|
<th>Stockable</th>
|
||||||
|
<th>Image</th>
|
||||||
|
<th>Actions</th>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><input class="form-control" id="admin-newproduct-name" type="text" name="name" placeholder="New product name"></td>
|
||||||
|
<td><input class="form-control" id="admin-newproduct-ean" type="text" name="ean" placeholder="Scan to insert EAN"></td>
|
||||||
|
<td>
|
||||||
|
<div class="input-group mb-3">
|
||||||
|
<span class="input-group-text">CHF</span>
|
||||||
|
<input class="form-control" id="admin-newproduct-price-member" type="number" step="0.01" name="pricemember" value="0">
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<div class="input-group mb-3">
|
||||||
|
<span class="input-group-text">CHF</span>
|
||||||
|
<input class="form-control" id="admin-newproduct-price-non-member" type="number" step="0.01" name="pricenonmember" value="0">
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td><input class="form-check-input" id="admin-custom-price" type="checkbox" name="custom_price"></td>
|
||||||
|
<td><input class="form-check-input" id="admin-newproduct-stockable" type="checkbox" name="stockable" checked="checked"></td>
|
||||||
|
<td><input class="form-control" id="admin-newproduct-image" name="image" type="file" accept="image/*"></td>
|
||||||
|
<td><input class="btn btn-success" type="submit" value="Create Product"></td>
|
||||||
|
</tr>
|
||||||
|
{% for product in products %}
|
||||||
|
<tr>
|
||||||
|
<td>{{ product.name }}</td>
|
||||||
|
<td>{{ product.ean or '' }}</td>
|
||||||
|
<td>{{ product.price_member | chf }}</td>
|
||||||
|
<td>{{ product.price_non_member | chf }}</td>
|
||||||
|
<td>{{ '✓' if product.custom_price else '✗' }}</td>
|
||||||
|
<td>{{ '✓' if product.stockable else '✗' }}</td>
|
||||||
|
<td><img style="height: 2em;" src="/static/upload/thumbnails/products/{{ product.id }}.png?cacheBuster={{ now }}" alt="Picture of {{ product.name }}" draggable="false"></td>
|
||||||
|
<td>
|
||||||
|
<a class="btn btn-primary" href="/modproduct?productid={{ product.id }}">Edit</a>
|
||||||
|
<a class="btn btn-danger" href="/modproduct?productid={{ product.id }}&change=del">Delete</a>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</table>
|
||||||
|
</form>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="tab-pane fade pt-3" id="admin-default-images-tab-pane" role="tabpanel">
|
||||||
|
<h2>Default Images</h2>
|
||||||
|
|
||||||
|
<form id="admin-default-images-form" method="post" action="/admin?adminchange=defaultimg" enctype="multipart/form-data" accept-charset="UTF-8">
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
|
||||||
|
<div class="col-sm-2 g-4">
|
||||||
|
<div class="card h-100">
|
||||||
|
<img class="card-img-top" src="/static/upload/thumbnails/users/default.png" alt="Default user avatar" />
|
||||||
|
<div class="card-body">
|
||||||
|
<label class="card-title" for="admin-default-images-user">Default user avatar</label>
|
||||||
|
<div class="card-text">
|
||||||
|
<input class="form-control" id="admin-default-images-user" type="file" name="users" accept="image/*" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-sm-2 g-4">
|
||||||
|
<div class="card h-100">
|
||||||
|
<img class="card-img-top" src="/static/upload/thumbnails/product/default.png" alt="Default product image" />
|
||||||
|
<div class="card-body">
|
||||||
|
<label class="card-title" for="admin-default-images-product">Default product image</label>
|
||||||
|
<div class="card-text">
|
||||||
|
<input class="form-control" id="admin-default-images-product" type="file" name="products" accept="image/*" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
<input class="btn btn-primary" type="submit" value="Save changes">
|
||||||
|
</p>
|
||||||
|
</form>
|
||||||
|
</section>
|
||||||
|
|
||||||
{{ super() }}
|
{{ super() }}
|
||||||
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block eanwebsocket %}
|
{% block eanwebsocket %}
|
||||||
let tokeninput = document.getElementById("admin-newtoken-token");
|
|
||||||
tokeninput.value = e.data;
|
|
||||||
tokeninput.select();
|
|
||||||
{% if authuser.is_admin %}
|
|
||||||
let eaninput = document.getElementById("admin-newproduct-ean");
|
let eaninput = document.getElementById("admin-newproduct-ean");
|
||||||
eaninput.value = e.data;
|
eaninput.value = e.data;
|
||||||
eaninput.select();
|
eaninput.select();
|
||||||
{% else %}
|
eaninput.scrollIntoView();
|
||||||
tokeninput.scrollIntoView();
|
|
||||||
{% endif %}
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
|
@ -1,113 +0,0 @@
|
||||||
<section id="admin-myaccount">
|
|
||||||
<h2>My Account</h2>
|
|
||||||
|
|
||||||
<form id="admin-myaccount-form" method="post" action="/admin?change=account" accept-charset="UTF-8">
|
|
||||||
<label for="admin-myaccount-username">Username: </label>
|
|
||||||
<input id="admin-myaccount-username" type="text" name="username" value="{{ authuser.name }}" /><br/>
|
|
||||||
|
|
||||||
<label for="admin-myaccount-email">E-Mail: </label>
|
|
||||||
<input id="admin-myaccount-email" type="text" name="email" value="{% if authuser.email is not none %}{{ authuser.email }}{% endif %}" /><br/>
|
|
||||||
|
|
||||||
<label for="admin-myaccount-receipt-pref">Receipts: </label>
|
|
||||||
<select id="admin-myaccount-receipt-pref" name="receipt_pref">
|
|
||||||
{% for pref in receipt_preference_class %}
|
|
||||||
<option value="{{ pref.value }}" {% if authuser.receipt_pref == pref %} selected="selected" {% endif %}>{{ pref.human_readable }}</option>
|
|
||||||
{% endfor %}
|
|
||||||
</select>
|
|
||||||
{% if config_smtp_enabled != '1' %}Sending receipts is disabled by your administrator.{% endif %}
|
|
||||||
<br/>
|
|
||||||
|
|
||||||
<label for="admin-myaccount-ismember">Member: </label>
|
|
||||||
<input id="admin-myaccount-ismember" type="checkbox" disabled="disabled" {% if authuser.is_member %} checked="checked" {% endif %}/><br/>
|
|
||||||
|
|
||||||
<label for="admin-myaccount-isadmin">Admin: </label>
|
|
||||||
<input id="admin-myaccount-isadmin" type="checkbox" disabled="disabled" {% if authuser.is_admin %} checked="checked" {% endif %}/><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" {% if authuser.logout_after_purchase %} checked="checked" {% endif %}/><br/>
|
|
||||||
|
|
||||||
<input type="submit" value="Save changes" />
|
|
||||||
</form>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section id="admin-avatar">
|
|
||||||
<h2>Avatar</h2>
|
|
||||||
|
|
||||||
<form id="admin-avatar-form" method="post" action="/admin?change=avatar" enctype="multipart/form-data" accept-charset="UTF-8">
|
|
||||||
<img src="/static/upload/thumbnails/users/{{ authuser.id }}.png?cacheBuster={{ now }}" alt="Avatar of {{ authuser.name }}" /><br/>
|
|
||||||
|
|
||||||
<label for="admin-avatar-avatar">Upload new file: </label>
|
|
||||||
<input id="admin-avatar-avatar" type="file" name="avatar" accept="image/*" /><br/>
|
|
||||||
|
|
||||||
<input type="submit" value="Save changes" />
|
|
||||||
</form>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section id="admin-password">
|
|
||||||
<h2>Password</h2>
|
|
||||||
|
|
||||||
<form id="admin-password-form" method="post" action="/admin?change=password" accept-charset="UTF-8">
|
|
||||||
<label for="admin-password-oldpass">Current password: </label>
|
|
||||||
<input id="admin-password-oldpass" type="password" name="oldpass" /><br/>
|
|
||||||
|
|
||||||
<label for="admin-password-newpass">New password: </label>
|
|
||||||
<input id="admin-password-newpass" type="password" name="newpass" /><br/>
|
|
||||||
|
|
||||||
<label for="admin-password-newpass2">Repeat password: </label>
|
|
||||||
<input id="admin-password-newpass2" type="password" name="newpass2" /><br/>
|
|
||||||
|
|
||||||
<input type="submit" value="Save changes" />
|
|
||||||
</form>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section id="admin-touchkey">
|
|
||||||
<h2>Touchkey</h2>
|
|
||||||
|
|
||||||
<form id="admin-touchkey-form" method="post" action="/admin?change=touchkey" accept-charset="UTF-8">
|
|
||||||
Draw a new touchkey (leave empty to disable):
|
|
||||||
<br/>
|
|
||||||
<svg id="touchkey-svg" width="400" height="400"></svg>
|
|
||||||
<br/>
|
|
||||||
<input id="admin-touchkey-touchkey" type="hidden" name="touchkey" value="" />
|
|
||||||
|
|
||||||
<input type="submit" value="Save changes" />
|
|
||||||
</form>
|
|
||||||
|
|
||||||
<script src="/static/js/touchkey.js" ></script>
|
|
||||||
<script>
|
|
||||||
initTouchkey(true, 'touchkey-svg', null, 'admin-touchkey-touchkey');
|
|
||||||
</script>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section id="admin-tokens">
|
|
||||||
<h2>Tokens</h2>
|
|
||||||
|
|
||||||
<strong>Warning:</strong>
|
|
||||||
Login tokens are a convenience feature that if used may weaken security.
|
|
||||||
Make sure you only use tokens not easily accessible to other people.
|
|
||||||
|
|
||||||
<form id="admin-newtoken-form" method="post" action="/admin?change=addtoken" accept-charset="UTF-8">
|
|
||||||
<table border="1">
|
|
||||||
<tr>
|
|
||||||
<th>Token</th>
|
|
||||||
<th>Name</th>
|
|
||||||
<th>Created</th>
|
|
||||||
<th>Actions</th>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td><input id="admin-newtoken-token" type="password" name="token" value=""></td>
|
|
||||||
<td><input id="admin-newtoken-name" type="text" name="name" value=""></td>
|
|
||||||
<td></td>
|
|
||||||
<td><input type="submit" value="Create Token"></td>
|
|
||||||
</tr>
|
|
||||||
{% for token in tokens %}
|
|
||||||
<tr>
|
|
||||||
<td>••••••••</td>
|
|
||||||
<td>{{ token.name }}</td>
|
|
||||||
<td>{{ token.date }}</td>
|
|
||||||
<td><a style="text-decoration: none; color: #ff0000;" href="/admin?change=deltoken&token={{ token.id }}">🗑</a></td>
|
|
||||||
</tr>
|
|
||||||
{% endfor %}
|
|
||||||
</table>
|
|
||||||
</form>
|
|
||||||
</section>
|
|
|
@ -1,118 +0,0 @@
|
||||||
<section id="admin-restricted-newuser">
|
|
||||||
<h2>Users</h2>
|
|
||||||
|
|
||||||
<form id="admin-newuser-form" method="post" action="/admin?adminchange=newuser" accept-charset="UTF-8">
|
|
||||||
<table border="1">
|
|
||||||
<tr>
|
|
||||||
<th>Username</th>
|
|
||||||
<th>E-Mail (optional)</th>
|
|
||||||
<th>Password</th>
|
|
||||||
<th>Member</th>
|
|
||||||
<th>Admin</th>
|
|
||||||
<th>Logout after purchase</th>
|
|
||||||
<th>Actions</th>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td><input id="admin-newuser-username" type="text" name="username" placeholder="New username"></td>
|
|
||||||
<td><input id="admin-newuser-email" type="text" name="email" placeholder="New e-mail"></td>
|
|
||||||
<td><input id="admin-newuser-password" type="password" name="password" placeholder="New password"></td>
|
|
||||||
<td><input id="admin-newuser-ismember" type="checkbox" name="ismember"></td>
|
|
||||||
<td><input id="admin-newuser-isadmin" type="checkbox" name="isadmin"></td>
|
|
||||||
<td><input id="admin-newuser-logout-after-purchase" type="checkbox" name="logout_after_purchase"></td>
|
|
||||||
<td><input type="submit" value="Create User"></td>
|
|
||||||
</tr>
|
|
||||||
{% for user in users %}
|
|
||||||
<tr>
|
|
||||||
<td>{{ user.name }}</td>
|
|
||||||
<td>{{ '✓' if user.email else '✗' }}</td>
|
|
||||||
<td>••••••••</td>
|
|
||||||
<td>{{ '✓' if user.is_member else '✗' }}</td>
|
|
||||||
<td>{{ '✓' if user.is_admin else '✗' }}</td>
|
|
||||||
<td>{{ '✓' if user.logout_after_purchase else '✗' }}</td>
|
|
||||||
<td>
|
|
||||||
<a style="text-decoration: none; color: #0000ff;" href="/moduser?userid={{ user.id }}">🖊</a>
|
|
||||||
<a style="text-decoration: none; color: #ff0000;" href="/moduser?userid={{ user.id }}&change=del">🗑</a>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
{% endfor %}
|
|
||||||
</table>
|
|
||||||
</form>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section id="admin-restricted-newproduct">
|
|
||||||
<h2>Products</h2>
|
|
||||||
|
|
||||||
<form id="admin-newproduct-form" method="post" action="/admin?adminchange=newproduct" enctype="multipart/form-data" accept-charset="UTF-8">
|
|
||||||
<table border="1">
|
|
||||||
<tr>
|
|
||||||
<th>Name</th>
|
|
||||||
<th>EAN code</th>
|
|
||||||
<th>Member price</th>
|
|
||||||
<th>Non-member price</th>
|
|
||||||
<th>Custom price</th>
|
|
||||||
<th>Stockable</th>
|
|
||||||
<th>Image</th>
|
|
||||||
<th>Actions</th>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td><input id="admin-newproduct-name" type="text" name="name" placeholder="New product name"></td>
|
|
||||||
<td><input id="admin-newproduct-ean" type="text" name="ean" placeholder="Scan to insert EAN"></td>
|
|
||||||
<td>CHF <input id="admin-newproduct-price-member" type="number" step="0.01" name="pricemember" value="0"></td>
|
|
||||||
<td>CHF <input id="admin-newproduct-price-non-member" type="number" step="0.01" name="pricenonmember" value="0"></td>
|
|
||||||
<td><input id="admin-custom-price" type="checkbox" name="custom_price"></td>
|
|
||||||
<td><input id="admin-newproduct-stockable" type="checkbox" name="stockable" checked="checked"></td>
|
|
||||||
<td><input id="admin-newproduct-image" name="image" type="file" accept="image/*"></td>
|
|
||||||
<td><input type="submit" value="Create Product"></td>
|
|
||||||
</tr>
|
|
||||||
{% for product in products %}
|
|
||||||
<tr>
|
|
||||||
<td>{{ product.name }}</td>
|
|
||||||
<td>{{ product.ean or '' }}</td>
|
|
||||||
<td>{{ product.price_member | chf }}</td>
|
|
||||||
<td>{{ product.price_non_member | chf }}</td>
|
|
||||||
<td>{{ '✓' if product.custom_price else '✗' }}</td>
|
|
||||||
<td>{{ '✓' if product.stockable else '✗' }}</td>
|
|
||||||
<td><img style="height: 2em;" src="/static/upload/thumbnails/products/{{ product.id }}.png?cacheBuster={{ now }}" alt="Picture of {{ product.name }}" draggable="false"></td>
|
|
||||||
<td>
|
|
||||||
<a style="text-decoration: none; color: #0000ff;" href="/modproduct?productid={{ product.id }}">🖊</a>
|
|
||||||
<a style="text-decoration: none; color: #ff0000;" href="/modproduct?productid={{ product.id }}&change=del">🗑</a>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
{% endfor %}
|
|
||||||
</table>
|
|
||||||
</form>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section id="admin-restricted-default-images">
|
|
||||||
<h2>Set default images</h2>
|
|
||||||
|
|
||||||
<form id="admin-default-images-form" method="post" action="/admin?adminchange=defaultimg" enctype="multipart/form-data" accept-charset="UTF-8">
|
|
||||||
<table>
|
|
||||||
<tr>
|
|
||||||
<th>Default user avatar</th>
|
|
||||||
<th>Default product image</th>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td>
|
|
||||||
<label for="admin-default-images-user">
|
|
||||||
<img src="/static/upload/thumbnails/users/default.png" alt="Default user avatar" />
|
|
||||||
</label>
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
<label for="admin-default-images-product">
|
|
||||||
<img src="/static/upload/thumbnails/products/default.png" alt="Default product avatar" />
|
|
||||||
</label>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td>
|
|
||||||
<input id="admin-default-images-user" type="file" name="users" accept="image/*" />
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
<input id="admin-default-images-product" type="file" name="products" accept="image/*" />
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</table>
|
|
||||||
<input type="submit" value="Save changes">
|
|
||||||
</form>
|
|
||||||
</section>
|
|
|
@ -1,63 +1,74 @@
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
{% block head %}
|
<meta http-equiv="encoding" charset="utf-8" />
|
||||||
{# Show the setup name, as set in the config file, as tab title. Don't escape HTML entities. #}
|
{# Show the setup name, as set in the config file, as tab title. Don't escape HTML entities. #}
|
||||||
<title>{{ setupname|safe }}</title>
|
<title>{{ setupname|safe }}</title>
|
||||||
|
<link rel="stylesheet" href="/static/css/bootstrap.min.css" />
|
||||||
<link rel="stylesheet" href="/static/css/matemat.css"/>
|
<link rel="stylesheet" href="/static/css/matemat.css"/>
|
||||||
<link rel="stylesheet" href="/static/css/theme.css"/>
|
<link rel="stylesheet" href="/static/css/theme.css"/>
|
||||||
{% endblock %}
|
<script src="/static/js/bootstrap.bundle.min.js"></script>
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
|
|
||||||
<header>
|
<header class="navbar navbar-expand-lg navbar-dark bg-dark">
|
||||||
{% block header %}
|
{% block header %}
|
||||||
|
|
||||||
<nav class="navbarbutton">
|
<nav class="container-fluid">
|
||||||
|
<a class="navbar-brand" href="/">{{ setupname|safe }}</a>
|
||||||
|
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbar-collapse" aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="Toggle navigation">
|
||||||
|
<span class="navbar-toggler-icon"></span>
|
||||||
|
</button>
|
||||||
|
<div class="collapse navbar-collapse" id="navbar-collapse">
|
||||||
|
<ul class="navbar-nav me-auto mb-2 mb-lg-0">
|
||||||
{# Show a link to the settings, if a user logged in via password (authlevel 2). #}
|
{# Show a link to the settings, if a user logged in via password (authlevel 2). #}
|
||||||
{% if authlevel|default(0) > 1 %}
|
{% if authuser is defined and authlevel|default(0) > 1 %}
|
||||||
{% if authuser is defined %}
|
<li class="nav-item"><a href="/settings" class="nav-link">Settings</a></li>
|
||||||
<a href="/">Home</a>
|
|
||||||
{# If a user is an admin, call the link "Administration". Otherwise, call it "Settings". #}
|
|
||||||
{% if authuser.is_admin %}
|
{% if authuser.is_admin %}
|
||||||
<a href="/admin">Administration</a>
|
<li class="nav-item"><a href="/admin" class="nav-link">Administration</a></li>
|
||||||
<a href="/statistics">Sales Statistics</a>
|
<li class="nav-item"><a href="/statistics" class="nav-link">Sales Statistics</a></li>
|
||||||
|
{% endif %}
|
||||||
|
{% endif %}
|
||||||
|
{# Login/Logout buttons #}
|
||||||
|
{% if authuser is defined %}
|
||||||
|
<li class="nav-item justify-content-end"><a href="/logout" class="nav-link">Logout</a></li>
|
||||||
{% else %}
|
{% else %}
|
||||||
<a href="/admin">Settings</a>
|
<li class="nav-item justify-content-end"><a href="/login" class="nav-link">Login</a></li>
|
||||||
{% endif %}
|
{% if signup|default(false) %}
|
||||||
|
<li class="nav-item justify-content-end"><a href="/signup" class="nav-link">Signup</a></li>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
</nav>
|
</nav>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<main>
|
<main class="container-fluid pb-5 pt-3">
|
||||||
{% block notifications %}
|
{% block notifications %}
|
||||||
{% for n in notifications | default([]) %}
|
{% for n in notifications | default([]) %}
|
||||||
<div class="notification {{ n.classes | join(' ') }}">{{ n.msg|safe }}</div>
|
<div class="alert {{ n.classes | join(' ') }}" role="alert">
|
||||||
|
{{ n.msg|safe }}
|
||||||
|
</div>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
{% block main %}
|
{% block main %}
|
||||||
{# Here be content. #}
|
{# Here be content. #}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
<footer>
|
<footer class="fixed-bottom p-3 bg-light">
|
||||||
{% block footer %}
|
{% block footer %}
|
||||||
{# Show some information in the footer, e.g. the instance name, the version, and copyright info. #}
|
<div class="container text-muted">
|
||||||
<ul>
|
{{ setupname|safe }} | Matemat {{ __version__ }}
|
||||||
<li> {{ setupname|safe }}
|
</div>
|
||||||
<li> Matemat {{ __version__ }}
|
|
||||||
<li> MIT License
|
|
||||||
<li> git.kabelsalat.ch/s3lph/matemat
|
|
||||||
</ul>
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
</footer>
|
</footer>
|
||||||
|
|
||||||
{% if eanwebsocket %}
|
{% if eanwebsocket %}
|
||||||
<script>
|
<script>
|
||||||
function connect() {
|
function connect() {
|
||||||
let socket = new WebSocket("{{ eanwebsocket }}");
|
let socket = new WebSocket("{{ eanwebsocket }}");
|
||||||
socket.onclose = () => { setTimeout(connect, 1000); };
|
socket.onclose = () => { setTimeout(connect, 1000); };
|
||||||
|
@ -70,7 +81,7 @@
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
window.addEventListener("load", () => { connect(); });
|
window.addEventListener("load", () => { connect(); });
|
||||||
</script>
|
</script>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
|
@ -1,27 +1,21 @@
|
||||||
{% extends "base.html" %}
|
{% extends "base.html" %}
|
||||||
|
|
||||||
{% block header %}
|
|
||||||
<h1>Welcome</h1>
|
|
||||||
{{ super() }}
|
|
||||||
{% endblock %}
|
|
||||||
|
|
||||||
{% block main %}
|
{% block main %}
|
||||||
|
|
||||||
|
<h1>Welcome</h1>
|
||||||
|
|
||||||
{# Show a username/password login form #}
|
{# Show a username/password login form #}
|
||||||
<form method="post" action="/login" id="loginform" accept-charset="UTF-8">
|
<form method="post" action="/login" id="loginform" accept-charset="UTF-8">
|
||||||
<label for="login-username">Username: </label>
|
<label class="form-label" for="login-username">Username: </label>
|
||||||
<input id="login-username" type="text" name="username"/><br/>
|
<input class="form-control" id="login-username" type="text" name="username"/><br/>
|
||||||
|
|
||||||
<label for="login-password">Password: </label>
|
<label class="form-label" for="login-password">Password: </label>
|
||||||
<input id="login-password" type="password" name="password"/><br/>
|
<input class="form-control" id="login-password" type="password" name="password"/><br/>
|
||||||
|
|
||||||
<input type="submit" value="Login">
|
<input class="btn btn-primary" type="submit" value="Login">
|
||||||
|
<a class="btn btn-secondary" href="/">Cancel</a>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
<div class="thumblist-item">
|
|
||||||
<a href="/">Cancel</a>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{{ super() }}
|
{{ super() }}
|
||||||
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
|
@ -1,50 +1,58 @@
|
||||||
{% extends "base.html" %}
|
{% extends "base.html" %}
|
||||||
|
|
||||||
{% block header %}
|
|
||||||
<h1>Administration</h1>
|
|
||||||
{{ super() }}
|
|
||||||
{% endblock %}
|
|
||||||
|
|
||||||
{% block main %}
|
{% block main %}
|
||||||
|
|
||||||
<section id="modproduct">
|
<section id="modproduct">
|
||||||
<h2>Modify {{ product.name }}</h2>
|
<h1>Modify {{ product.name }}</h1>
|
||||||
|
|
||||||
<form id="modproduct-form" method="post" action="/modproduct?change=update" enctype="multipart/form-data" accept-charset="UTF-8">
|
<form id="modproduct-form" method="post" action="/modproduct?change=update" enctype="multipart/form-data" accept-charset="UTF-8">
|
||||||
<label for="modproduct-name">Name: </label>
|
<label class="form-label" for="modproduct-name">Name: </label>
|
||||||
<input id="modproduct-name" type="text" name="name" value="{{ product.name }}" /><br/>
|
<input class="form-control" id="modproduct-name" type="text" name="name" value="{{ product.name }}" /><br/>
|
||||||
|
|
||||||
<label for="modproduct-ean">EAN code: </label>
|
<label class="form-label" for="modproduct-ean">EAN code: </label>
|
||||||
<input id="modproduct-ean" type="text" name="ean" value="{{ product.ean or '' }}" /><br/>
|
<input class="form-control" id="modproduct-ean" type="text" name="ean" value="{{ product.ean or '' }}" /><br/>
|
||||||
|
|
||||||
<label for="modproduct-price-member">Member price: </label>
|
<label class="form-label" for="modproduct-price-member">Member price: </label>
|
||||||
CHF <input id="modproduct-price-member" type="number" step="0.01" name="pricemember" value="{{ product.price_member|chf(False) }}" /><br/>
|
<div class="input-group mb-3">
|
||||||
|
<span class="input-group-text">CHF</span>
|
||||||
|
<input class="form-control" id="modproduct-price-member" type="number" step="0.01" name="pricemember" value="{{ product.price_member|chf(False) }}" />
|
||||||
|
</div>
|
||||||
|
|
||||||
<label for="modproduct-price-non-member">Non-member price: </label>
|
<label class="form-label" for="modproduct-price-non-member">Non-member price: </label>
|
||||||
CHF <input id="modproduct-price-non-member" type="number" step="0.01" name="pricenonmember" value="{{ product.price_non_member|chf(False) }}" /><br/>
|
<div class="input-group mb-3">
|
||||||
|
<span class="input-group-text">CHF</span>
|
||||||
|
<input class="form-control" id="modproduct-price-non-member" type="number" step="0.01" name="pricenonmember" value="{{ product.price_non_member|chf(False) }}" />
|
||||||
|
</div>
|
||||||
|
|
||||||
<label for="modproduct-custom-price"><abbr title="When 'Custom Price' is enabled, users choose the price to pay, but at least the prices given above">Custom Price</abbr>: </label>
|
|
||||||
<input id="modproduct-custom-price" type="checkbox" name="custom_price" {% if product.custom_price %} checked="checked" {% endif %} /><br/>
|
|
||||||
|
|
||||||
<label for="modproduct-stockable">Stockable: </label>
|
<div class="form-check">
|
||||||
<input id="modproduct-stockable" type="checkbox" name="stockable" {% if product.stockable %} checked="checked" {% endif %} /><br/>
|
<input class="form-check-input" id="modproduct-custom-price" type="checkbox" name="custom_price" {% if product.custom_price %} checked="checked" {% endif %} />
|
||||||
|
<label class="form-check-label" for="modproduct-custom-price"><abbr title="When 'Custom Price' is enabled, users choose the price to pay, but at least the prices given above">Custom Price</abbr></label>
|
||||||
|
</div>
|
||||||
|
|
||||||
<label for="modproduct-balance">Stock: </label>
|
<div class="form-check">
|
||||||
<input id="modproduct-balance" name="stock" type="text" value="{{ product.stock }}" /><br/>
|
<input class="form-check-input" id="modproduct-stockable" type="checkbox" name="stockable" {% if product.stockable %} checked="checked" {% endif %} />
|
||||||
|
<label class="form-check-label" for="modproduct-stockable">Stockable</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
<label for="modproduct-image">
|
<label class="form-label" for="modproduct-balance">Stock: </label>
|
||||||
|
<input class="form-control" id="modproduct-balance" name="stock" type="text" value="{{ product.stock }}" /><br/>
|
||||||
|
|
||||||
|
<label class="form-label" for="modproduct-image">
|
||||||
<img height="150" src="/static/upload/thumbnails/products/{{ product.id }}.png?cacheBuster={{ now }}" alt="Image of {{ product.name }}" />
|
<img height="150" src="/static/upload/thumbnails/products/{{ product.id }}.png?cacheBuster={{ now }}" alt="Image of {{ product.name }}" />
|
||||||
</label><br/>
|
</label><br/>
|
||||||
<input id="modproduct-image" type="file" name="image" accept="image/*" /><br/>
|
<input class="form-control" id="modproduct-image" type="file" name="image" accept="image/*" /><br/>
|
||||||
|
|
||||||
<input id="modproduct-productid" type="hidden" name="productid" value="{{ product.id }}" /><br/>
|
<input id="modproduct-productid" type="hidden" name="productid" value="{{ product.id }}" /><br/>
|
||||||
|
|
||||||
<input type="submit" value="Save changes">
|
<input class="btn btn-primary" type="submit" value="Save changes">
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
|
<h2>Delete Product</h2>
|
||||||
|
|
||||||
<form id="modproduct-delproduct-form" method="post" action="/modproduct?change=del" accept-charset="UTF-8">
|
<form id="modproduct-delproduct-form" method="post" action="/modproduct?change=del" accept-charset="UTF-8">
|
||||||
<input id="modproduct-delproduct-productid" type="hidden" name="productid" value="{{ product.id }}" /><br/>
|
<input id="modproduct-delproduct-productid" type="hidden" name="productid" value="{{ product.id }}" /><br/>
|
||||||
<input type="submit" value="Delete product" />
|
<input class="btn btn-danger" type="submit" value="Delete product {{ product.name }}" />
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
</section>
|
</section>
|
||||||
|
|
|
@ -1,27 +1,22 @@
|
||||||
{% extends "base.html" %}
|
{% extends "base.html" %}
|
||||||
|
|
||||||
{% block header %}
|
|
||||||
<h1>Administration</h1>
|
|
||||||
{{ super() }}
|
|
||||||
{% endblock %}
|
|
||||||
|
|
||||||
{% block main %}
|
{% block main %}
|
||||||
|
|
||||||
<section id="moduser-account">
|
<section id="moduser-account">
|
||||||
<h2>Modify {{ user.name }}</h2>
|
<h1>Modify {{ user.name }}</h1>
|
||||||
|
|
||||||
<form id="moduser-account-form" method="post" action="/moduser?change=update" enctype="multipart/form-data" accept-charset="UTF-8">
|
<form id="moduser-account-form" method="post" action="/moduser?change=update" enctype="multipart/form-data" accept-charset="UTF-8">
|
||||||
<label for="moduser-account-username">Username: </label>
|
<label class="form-label" for="moduser-account-username">Username: </label>
|
||||||
<input id="moduser-account-username" type="text" name="username" value="{{ user.name }}" /><br/>
|
<input class="form-control" id="moduser-account-username" type="text" name="username" value="{{ user.name }}" /><br/>
|
||||||
|
|
||||||
<label for="moduser-account-email">E-Mail: </label>
|
<label class="form-label" for="moduser-account-email">E-Mail: </label>
|
||||||
<input id="moduser-account-email" type="text" name="email" value="{% if user.email is not none %}{{ user.email }}{% endif %}" /><br/>
|
<input class="form-control" id="moduser-account-email" type="text" name="email" value="{% if user.email is not none %}{{ user.email }}{% endif %}" /><br/>
|
||||||
|
|
||||||
<label for="moduser-account-password">Password: </label>
|
<label class="form-label" for="moduser-account-password">Password: </label>
|
||||||
<input id="moduser-account-password" type="password" name="password" /><br/>
|
<input class="form-control" id="moduser-account-password" type="password" name="password" /><br/>
|
||||||
|
|
||||||
<label for="moduser-account-receipt-pref">Receipts: </label>
|
<label class="form-label" for="moduser-account-receipt-pref">Receipts: </label>
|
||||||
<select id="moduser-account-receipt-pref" name="receipt_pref">
|
<select class="form-select" id="moduser-account-receipt-pref" name="receipt_pref">
|
||||||
{% for pref in receipt_preference_class %}
|
{% for pref in receipt_preference_class %}
|
||||||
<option value="{{ pref.value }}" {% if user.receipt_pref == pref %} selected="selected" {% endif %}>{{ pref.human_readable }}</option>
|
<option value="{{ pref.value }}" {% if user.receipt_pref == pref %} selected="selected" {% endif %}>{{ pref.human_readable }}</option>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
@ -29,34 +24,43 @@
|
||||||
{% if config_smtp_enabled != '1' %}Sending receipts is disabled by your administrator.{% endif %}
|
{% if config_smtp_enabled != '1' %}Sending receipts is disabled by your administrator.{% endif %}
|
||||||
<br/>
|
<br/>
|
||||||
|
|
||||||
<label for="moduser-account-ismember">Member: </label>
|
<div class="form-check">
|
||||||
<input id="moduser-account-ismember" name="ismember" type="checkbox" {% if user.is_member %} checked="checked" {% endif %}/><br/>
|
<input class="form-check-input" id="moduser-account-ismember" name="ismember" type="checkbox" {% if user.is_member %} checked="checked" {% endif %}>
|
||||||
|
<label class="form-check-label" for="moduser-account-ismember">Member</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
<label for="moduser-account-isadmin">Admin: </label>
|
<div class="form-check">
|
||||||
<input id="moduser-account-isadmin" name="isadmin" type="checkbox" {% if user.is_admin %} checked="checked" {% endif %}/><br/>
|
<input class="form-check-input" id="moduser-account-isadmin" name="isadmin" type="checkbox" {% if user.is_admin %} checked="checked" {% endif %}/>
|
||||||
|
<label class="form-check-label" for="moduser-account-isadmin">Admin</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
<label for="admin-myaccount-logout-after-purchase">Logout after purchase: </label>
|
<div class="form-check">
|
||||||
<input id="admin-myaccount-logout-after-purchase" type="checkbox" name="logout_after_purchase" {% if user.logout_after_purchase %} checked="checked" {% endif %}/><br/>
|
<input class="form-check-input" id="admin-myaccount-logout-after-purchase" type="checkbox" name="logout_after_purchase" {% if user.logout_after_purchase %} checked="checked" {% endif %}/>
|
||||||
|
<label class="form-check-label" for="admin-myaccount-logout-after-purchase">Logout after purchase</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
<label for="moduser-account-balance">Balance: </label>
|
<label class="form-label" for="moduser-account-balance">Balance: </label>
|
||||||
CHF <input id="moduser-account-balance" name="balance" type="number" step="0.01" value="{{ user.balance|chf(False) }}" /><br/>
|
<div class="input-group mb-3">
|
||||||
|
<span class="input-group-text">CHF</span>
|
||||||
|
<input class="form-control" id="moduser-account-balance" name="balance" type="number" step="0.01" value="{{ user.balance|chf(False) }}" />
|
||||||
|
</div>
|
||||||
|
|
||||||
<label for="moduser-account-balance-reason">Reason for balance modification: </label>
|
<label class="form-label" for="moduser-account-balance-reason">Reason for balance modification: </label>
|
||||||
<input id="moduser-account-balance-reason" type="text" name="reason" placeholder="Shows up on receipt" /><br/>
|
<input class="form-control" id="moduser-account-balance-reason" type="text" name="reason" placeholder="Shows up on receipt" /><br/>
|
||||||
|
|
||||||
<label for="moduser-account-avatar">
|
<label class="form-label" for="moduser-account-avatar">
|
||||||
<img src="/static/upload/thumbnails/users/{{ user.id }}.png?cacheBuster={{ now }}" alt="Avatar of {{ user.name }}" />
|
<img src="/static/upload/thumbnails/users/{{ user.id }}.png?cacheBuster={{ now }}" alt="Avatar of {{ user.name }}" />
|
||||||
</label><br/>
|
</label><br/>
|
||||||
<input id="moduser-account-avatar" type="file" name="avatar" accept="image/*" /><br/>
|
<input class="form-control" id="moduser-account-avatar" type="file" name="avatar" accept="image/*" /><br/>
|
||||||
|
|
||||||
<input id="moduser-account-userid" type="hidden" name="userid" value="{{ user.id }}" /><br/>
|
<input id="moduser-account-userid" type="hidden" name="userid" value="{{ user.id }}" /><br/>
|
||||||
|
|
||||||
<input type="submit" value="Save changes">
|
<input class="btn btn-primary" type="submit" value="Save changes">
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
<h2>Tokens</h2>
|
<h2>Tokens</h2>
|
||||||
|
|
||||||
<table border="1">
|
<table class="table table-striped">
|
||||||
<tr>
|
<tr>
|
||||||
<th>Token</th>
|
<th>Token</th>
|
||||||
<th>Name</th>
|
<th>Name</th>
|
||||||
|
@ -68,14 +72,16 @@
|
||||||
<td>••••••••</td>
|
<td>••••••••</td>
|
||||||
<td>{{ token.name }}</td>
|
<td>{{ token.name }}</td>
|
||||||
<td>{{ token.date }}</td>
|
<td>{{ token.date }}</td>
|
||||||
<td><a style="text-decoration: none; color: #ff0000;" href="/moduser?change=deltoken&token={{ token.id }}">🗑</a></td>
|
<td><a class="btn btn-danger" href="/moduser?change=deltoken&token={{ token.id }}">Delete</a></td>
|
||||||
</tr>
|
</tr>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</table>
|
</table>
|
||||||
|
|
||||||
|
<h2>Delete Account</h2>
|
||||||
|
|
||||||
<form id="moduser-deluser-form" method="post" action="/moduser?change=del" accept-charset="UTF-8">
|
<form id="moduser-deluser-form" method="post" action="/moduser?change=del" accept-charset="UTF-8">
|
||||||
<input id="moduser-deluser-userid" type="hidden" name="userid" value="{{ user.id }}" /><br/>
|
<input id="moduser-deluser-userid" type="hidden" name="userid" value="{{ user.id }}" /><br/>
|
||||||
<input type="submit" value="Delete user" />
|
<input class="btn btn-danger" type="submit" value="Delete user account {{ user.name }}" />
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
</section>
|
</section>
|
||||||
|
|
|
@ -1,30 +1,18 @@
|
||||||
{% extends "base.html" %}
|
{% extends "base.html" %}
|
||||||
|
|
||||||
{% block header %}
|
|
||||||
{# Show the username. #}
|
|
||||||
<h1>Welcome, {{ authuser.name }}</h1>
|
|
||||||
{{ super() }}
|
|
||||||
{% endblock %}
|
|
||||||
|
|
||||||
{% block main %}
|
{% block main %}
|
||||||
|
|
||||||
|
<h1>Welcome, {{ authuser.name }}</h1>
|
||||||
|
|
||||||
{# Show the users current balance #}
|
{# Show the users current balance #}
|
||||||
Your balance: {{ authuser.balance|chf }}
|
<p>
|
||||||
<br/>
|
Your balance: <strong>{{ authuser.balance|chf }}</strong>
|
||||||
{# Logout link #}
|
</p>
|
||||||
<div class="thumblist-item">
|
<p id="depositlist">
|
||||||
<a href="/logout">Logout</a>
|
<a class="btn btn-primary me-2" href="/deposit?n=100">Deposit CHF 1</a>
|
||||||
</div>
|
<a class="btn btn-primary me-2" href="/deposit?n=1000">Deposit CHF 10</a>
|
||||||
{# Links to deposit two common amounts of cash. TODO: Will be replaced by a nicer UI later (#20) #}
|
</p>
|
||||||
<div id="depositlist">
|
<div id="deposit-wrapper">
|
||||||
<div class="thumblist-item">
|
|
||||||
<a href="/deposit?n=100">Deposit CHF 1</a>
|
|
||||||
</div>
|
|
||||||
<div class="thumblist-item">
|
|
||||||
<a href="/deposit?n=1000">Deposit CHF 10</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div id="deposit-wrapper">
|
|
||||||
<div id="deposit-input">
|
<div id="deposit-input">
|
||||||
<div id="deposit-output">
|
<div id="deposit-output">
|
||||||
<span id="deposit-title"></span>
|
<span id="deposit-title"></span>
|
||||||
|
@ -43,43 +31,48 @@
|
||||||
<div id="scroll-down" onclick="scrollUserlist(+130);">▼</div>
|
<div id="scroll-down" onclick="scrollUserlist(+130);">▼</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<script src="/static/js/depositlist.js"></script>
|
<script src="/static/js/depositlist.js"></script>
|
||||||
<br/>
|
<br/>
|
||||||
|
|
||||||
|
<div class="row itemlist">
|
||||||
{% for product in products %}
|
{% for product in products %}
|
||||||
{# Show an item per product, consisting of the name, image, price and stock, triggering a purchase on click #}
|
{# Show an item per product, consisting of the name, image, price and stock, triggering a purchase on click #}
|
||||||
<div class="thumblist-item">
|
<div class="col-sm-1 g-4">
|
||||||
{% if product.custom_price %}
|
{% if product.custom_price %}
|
||||||
<a onclick="setup_custom_price({{ product.id }}, '{{ product.name}}');">
|
<a class="card h-100 text-bg-light" onclick="setup_custom_price({{ product.id }}, '{{ product.name}}');">
|
||||||
{% else %}
|
{% else %}
|
||||||
<a {% if product.ean %}id="a-buy-ean{{ product.ean }}"{% endif %} href="/buy?pid={{ product.id }}">
|
<a class="card h-100 text-bg-light" {% if product.ean %}id="a-buy-ean{{ product.ean }}"{% endif %} href="/buy?pid={{ product.id }}">
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<span class="thumblist-title">{{ product.name }}</span>
|
<div class="card-header">
|
||||||
|
{{ product.name }}
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
{% if product.custom_price %}
|
{% if product.custom_price %}
|
||||||
<span class="thumblist-detail">Custom Price</span><br/>
|
<span class="card-text">Custom Price</span>
|
||||||
{% else %}
|
{% else %}
|
||||||
<span class="thumblist-detail">Price:
|
<span class="card-text">
|
||||||
{% if authuser.is_member %}
|
{% if authuser.is_member %}
|
||||||
{{ product.price_member|chf }}
|
{{ product.price_member|chf }}
|
||||||
{% else %}
|
{% else %}
|
||||||
{{ product.price_non_member|chf }}
|
{{ product.price_non_member|chf }}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</span><br/>
|
</span>
|
||||||
{% endif %}
|
|
||||||
<div class="imgcontainer">
|
|
||||||
<img src="/static/upload/thumbnails/products/{{ product.id }}.png?cacheBuster={{ now }}" alt="Picture of {{ product.name }}" draggable="false"/>
|
|
||||||
{% set pstock = stock.get_stock(product) %}
|
|
||||||
{% if pstock is not none %}
|
|
||||||
<span class="thumblist-stock">{{ pstock }}</span>
|
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
<img class="card-img-bottom" src="/static/upload/thumbnails/products/{{ product.id }}.png?cacheBuster={{ now }}" alt="Picture of {{ product.name }}" draggable="false"/>
|
||||||
|
{% set pstock = stock.get_stock(product) %}
|
||||||
|
{% if pstock is not none %}
|
||||||
|
<div class="card-img-overlay d-flex flex-column justify-content-end">
|
||||||
|
<span class="card-text text-bg-light">{{ pstock }}</span>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
<br/>
|
</div>
|
||||||
|
|
||||||
{{ super() }}
|
{{ super() }}
|
||||||
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
|
|
152
templates/settings.html
Normal file
152
templates/settings.html
Normal file
|
@ -0,0 +1,152 @@
|
||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% block main %}
|
||||||
|
|
||||||
|
<h1>Settings</h1>
|
||||||
|
|
||||||
|
<ul class="nav nav-tabs" id="settingsTabContent" role="tablist">
|
||||||
|
<li class="nav-item" role="presentation">
|
||||||
|
<button class="nav-link active" id="settings-account-tab" data-bs-toggle="tab" data-bs-target="#settings-account-tab-pane" type="button" role="tab" aria-controls="settings-account-tab-pane" aria-selected="true">My Account</button>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item" role="presentation">
|
||||||
|
<button class="nav-link" id="settings-password-tab" data-bs-toggle="tab" data-bs-target="#settings-password-tab-pane" type="button" role="tab" aria-controls="settings-password-tab-pane" aria-selected="false">Password</button>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item" role="presentation">
|
||||||
|
<button class="nav-link" id="settings-touchkey-tab" data-bs-toggle="tab" data-bs-target="#settings-touchkey-tab-pane" type="button" role="tab" aria-controls="settings-touchkey-tab-pane" aria-selected="false">Touchkey</button>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item" role="presentation">
|
||||||
|
<button class="nav-link" id="settings-tokens-tab" data-bs-toggle="tab" data-bs-target="#settings-tokens-tab-pane" type="button" role="tab" aria-controls="settings-tokens-tab-pane" aria-selected="false">Tokens</button>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<div class="tab-content" id="settingsTabContent">
|
||||||
|
|
||||||
|
<section class="tab-pane fade pt-3 show active" id="settings-account-tab-pane" role="tabpanel">
|
||||||
|
<h2>My Account</h2>
|
||||||
|
|
||||||
|
<form id="settings-myaccount-form" method="post" action="/settings?change=account" accept-charset="UTF-8">
|
||||||
|
<label class="form-label" for="settings-myaccount-username">Username: </label>
|
||||||
|
<input class="form-control" id="settings-myaccount-username" type="text" name="username" value="{{ authuser.name }}" /><br/>
|
||||||
|
|
||||||
|
<label class="form-label" for="settings-myaccount-email">E-Mail: </label>
|
||||||
|
<input class="form-control" id="settings-myaccount-email" type="text" name="email" value="{% if authuser.email is not none %}{{ authuser.email }}{% endif %}" /><br/>
|
||||||
|
|
||||||
|
<label class="form-label" for="settings-myaccount-receipt-pref">Receipts: </label>
|
||||||
|
<select class="form-select" id="settings-myaccount-receipt-pref" name="receipt_pref">
|
||||||
|
{% for pref in receipt_preference_class %}
|
||||||
|
<option value="{{ pref.value }}" {% if authuser.receipt_pref == pref %} selected="selected" {% endif %}>{{ pref.human_readable }}</option>
|
||||||
|
{% endfor %}
|
||||||
|
</select>
|
||||||
|
{% if config_smtp_enabled != '1' %}Sending receipts is disabled by your administrator.{% endif %}
|
||||||
|
<br/>
|
||||||
|
|
||||||
|
<div class="form-check">
|
||||||
|
<input class="form-check-input" id="settings-myaccount-ismember" type="checkbox" disabled="disabled" {% if authuser.is_member %} checked="checked" {% endif %}/>
|
||||||
|
<label class="form-check-label" for="settings-myaccount-ismember">Member</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-check">
|
||||||
|
<input class="form-check-input" id="settings-myaccount-isadmin" type="checkbox" disabled="disabled" {% if authuser.is_admin %} checked="checked" {% endif %}/>
|
||||||
|
<label class="form-check-label" for="settings-myaccount-isadmin">Admin</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-check">
|
||||||
|
<input class="form-check-input" id="settings-myaccount-logout-after-purchase" type="checkbox" name="logout_after_purchase" {% if authuser.logout_after_purchase %} checked="checked" {% endif %}/>
|
||||||
|
<label class="form-check-label" for="settings-myaccount-logout-after-purchase">Logout after purchase</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<input class="btn btn-primary" type="submit" value="Save changes" />
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<h2>Avatar</h2>
|
||||||
|
|
||||||
|
<form id="settings-avatar-form" method="post" action="/settings?change=avatar" enctype="multipart/form-data" accept-charset="UTF-8">
|
||||||
|
<img src="/static/upload/thumbnails/users/{{ authuser.id }}.png?cacheBuster={{ now }}" alt="Avatar of {{ authuser.name }}" /><br/>
|
||||||
|
|
||||||
|
<label class="form-label" for="settings-avatar-avatar">Upload new file: </label>
|
||||||
|
<input class="form-control" id="settings-avatar-avatar" type="file" name="avatar" accept="image/*" /><br/>
|
||||||
|
|
||||||
|
<input class="btn btn-primary" type="submit" value="Save changes" />
|
||||||
|
</form>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="tab-pane fade pt-3" id="settings-password-tab-pane" role="tabpanel">
|
||||||
|
<h2>Password</h2>
|
||||||
|
|
||||||
|
<form id="settings-password-form" method="post" action="/settings?change=password" accept-charset="UTF-8">
|
||||||
|
<label class="form-label" for="settings-password-oldpass">Current password: </label>
|
||||||
|
<input class="form-control" id="settings-password-oldpass" type="password" name="oldpass" /><br/>
|
||||||
|
|
||||||
|
<label class="form-label" for="settings-password-newpass">New password: </label>
|
||||||
|
<input class="form-control" id="settings-password-newpass" type="password" name="newpass" /><br/>
|
||||||
|
|
||||||
|
<label class="form-label" for="settings-password-newpass2">Repeat password: </label>
|
||||||
|
<input class="form-control" id="settings-password-newpass2" type="password" name="newpass2" /><br/>
|
||||||
|
|
||||||
|
<input class="btn btn-primary" type="submit" value="Save changes" />
|
||||||
|
</form>
|
||||||
|
|
||||||
|
</section>
|
||||||
|
<section class="tab-pane fade pt-3" id="settings-touchkey-tab-pane" role="tabpanel">
|
||||||
|
<h2>Touchkey</h2>
|
||||||
|
|
||||||
|
<form id="settings-touchkey-form" method="post" action="/settings?change=touchkey" accept-charset="UTF-8">
|
||||||
|
Draw a new touchkey (leave empty to disable):
|
||||||
|
<br/>
|
||||||
|
<svg id="touchkey-svg" width="400" height="400"></svg>
|
||||||
|
<br/>
|
||||||
|
<input id="settings-touchkey-touchkey" type="hidden" name="touchkey" value="" />
|
||||||
|
|
||||||
|
<input class="btn btn-primary" type="submit" value="Save changes" />
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<script src="/static/js/touchkey.js" ></script>
|
||||||
|
<script>
|
||||||
|
initTouchkey(true, 'touchkey-svg', null, 'settings-touchkey-touchkey');
|
||||||
|
</script>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="tab-pane fade pt-3" id="settings-tokens-tab-pane" role="tabpanel">
|
||||||
|
<h2>Tokens</h2>
|
||||||
|
|
||||||
|
<strong>Warning:</strong>
|
||||||
|
Login tokens are a convenience feature that if used may weaken security.
|
||||||
|
Make sure you only use tokens not easily accessible to other people.
|
||||||
|
|
||||||
|
<form id="settings-newtoken-form" method="post" action="/settings?change=addtoken" accept-charset="UTF-8">
|
||||||
|
<table class="table table-striped">
|
||||||
|
<tr>
|
||||||
|
<th>Token</th>
|
||||||
|
<th>Name</th>
|
||||||
|
<th>Created</th>
|
||||||
|
<th>Actions</th>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><input class="form-control" id="settings-newtoken-token" type="password" name="token" value="" placeholder="Scan to insert EAN"></td>
|
||||||
|
<td><input class="form-control" id="settings-newtoken-name" type="text" name="name" value="" placeholder="New token name"></td>
|
||||||
|
<td></td>
|
||||||
|
<td><input class="btn btn-success" type="submit" value="Create Token"></td>
|
||||||
|
</tr>
|
||||||
|
{% for token in tokens %}
|
||||||
|
<tr>
|
||||||
|
<td>••••••••</td>
|
||||||
|
<td>{{ token.name }}</td>
|
||||||
|
<td>{{ token.date }}</td>
|
||||||
|
<td><a class="btn btn-danger" href="/settings?change=deltoken&token={{ token.id }}">Delete</a></td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</table>
|
||||||
|
</form>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
|
||||||
|
{{ super() }}
|
||||||
|
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block eanwebsocket %}
|
||||||
|
let tokeninput = document.getElementById("settings-newtoken-token");
|
||||||
|
tokeninput.value = e.data;
|
||||||
|
tokeninput.select();
|
||||||
|
tokeninput.scrollIntoView();
|
||||||
|
{% endblock %}
|
|
@ -1,42 +1,36 @@
|
||||||
{% extends "base.html" %}
|
{% extends "base.html" %}
|
||||||
|
|
||||||
{% block header %}
|
|
||||||
<h1>Signup</h1>
|
|
||||||
{{ super() }}
|
|
||||||
{% endblock %}
|
|
||||||
|
|
||||||
{% block main %}
|
{% block main %}
|
||||||
|
|
||||||
|
<h1>Signup</h1>
|
||||||
|
|
||||||
{# Show a username/password signup form #}
|
{# Show a username/password signup form #}
|
||||||
<form method="post" action="/signup" id="signupform" enctype="multipart/form-data" accept-charset="UTF-8">
|
<form method="post" action="/signup" id="signupform" enctype="multipart/form-data" accept-charset="UTF-8">
|
||||||
<label for="signup-username"><b>Username</b>: </label>
|
<label class="form-label" for="signup-username"><b>Username</b>: </label>
|
||||||
<input id="signup-username" type="text" name="username" required="required"/><br/>
|
<input class="form-control" id="signup-username" type="text" name="username" required="required"/><br/>
|
||||||
|
|
||||||
<label for="signup-password"><b>Choose a password</b>: </label>
|
<label class="form-label" for="signup-password"><b>Choose a password</b>: </label>
|
||||||
<input id="signup-password" type="password" name="password" required="required"/><br/>
|
<input class="form-control" id="signup-password" type="password" name="password" required="required"/><br/>
|
||||||
|
|
||||||
<label for="signup-password2"><b>Repeat password</b>: </label>
|
<label class="form-label" for="signup-password2"><b>Repeat password</b>: </label>
|
||||||
<input id="signup-password2" type="password" name="password2" required="required"/><br/>
|
<input class="form-control" id="signup-password2" type="password" name="password2" required="required"/><br/>
|
||||||
|
|
||||||
<label for="signup-email">E-Mail: </label>
|
<label class="form-label" for="signup-email">E-Mail: </label>
|
||||||
<input id="signup-email" type="text" name="email"/><br/>
|
<input class="form-control" id="signup-email" type="text" name="email"/><br/>
|
||||||
|
|
||||||
<label for="signup-avatar">Upload a profile picture: </label>
|
<label class="form-label" for="signup-avatar">Upload a profile picture: </label>
|
||||||
<input id="signup-avatar" type="file" name="avatar" accept="image/*" /><br/>
|
<input class="form-control" id="signup-avatar" type="file" name="avatar" accept="image/*" /><br/>
|
||||||
|
|
||||||
<label for="signup-touchkey">Draw a touchkey (touchscreen login pattern)</label>
|
<label class="form-label" for="signup-touchkey">Draw a touchkey (touchscreen login pattern)</label>
|
||||||
<br/>
|
<br/>
|
||||||
<svg id="touchkey-svg" width="400" height="400"></svg>
|
<svg id="touchkey-svg" width="400" height="400"></svg>
|
||||||
<br/>
|
<br/>
|
||||||
<input id="signup-touchkey" type="hidden" name="touchkey" value="" />
|
<input id="signup-touchkey" type="hidden" name="touchkey" value="" />
|
||||||
|
|
||||||
<input type="submit" value="Create account">
|
<input class="btn btn-primary" type="submit" value="Create account">
|
||||||
|
<a class="btn btn-secondary" href="/">Cancel</a>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
<div class="thumblist-item">
|
|
||||||
<a href="/">Cancel</a>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<script src="/static/js/touchkey.js" ></script>
|
<script src="/static/js/touchkey.js" ></script>
|
||||||
<script>
|
<script>
|
||||||
initTouchkey(true, 'touchkey-svg', null, 'signup-touchkey');
|
initTouchkey(true, 'touchkey-svg', null, 'signup-touchkey');
|
||||||
|
|
|
@ -1,121 +1,34 @@
|
||||||
{% extends "base.html" %}
|
{% extends "base.html" %}
|
||||||
|
|
||||||
{% block header %}
|
|
||||||
<h1>Signup</h1>
|
|
||||||
{{ super() }}
|
|
||||||
{% endblock %}
|
|
||||||
|
|
||||||
{% block main %}
|
{% block main %}
|
||||||
|
|
||||||
|
<h1>Signup</h1>
|
||||||
|
|
||||||
{# Show a username/password signup form #}
|
{# Show a username/password signup form #}
|
||||||
<form method="post" action="/signup" id="signupform" enctype="multipart/form-data" accept-charset="UTF-8">
|
<form method="post" action="/signup" id="signupform" enctype="multipart/form-data" accept-charset="UTF-8">
|
||||||
<label for="signup-username"><b>Username</b>: </label>
|
<label class="form-label" for="signup-username">Username:</label>
|
||||||
<input id="signup-username" type="text" name="username" required="required" class="osk-target"/><br/>
|
<input class="form-control" id="signup-username" type="text" name="username" required="required" class="osk-target"/><br/>
|
||||||
|
|
||||||
<label for="signup-password"><b>Choose a password</b>: </label>
|
<label class="form-label" for="signup-password">Choose a password:</label>
|
||||||
<input id="signup-password" type="password" name="password" required="required" class="osk-target"/><br/>
|
<input class="form-control" id="signup-password" type="password" name="password" required="required" class="osk-target"/><br/>
|
||||||
|
|
||||||
<label for="signup-password2"><b>Repeat password</b>: </label>
|
<label class="form-label" for="signup-password2">Repeat password:</label>
|
||||||
<input id="signup-password2" type="password" name="password2" required="required" class="osk-target"/><br/>
|
<input class="form-control" id="signup-password2" type="password" name="password2" required="required" class="osk-target"/><br/>
|
||||||
|
|
||||||
<label for="signup-touchkey">Draw a touchkey (touchscreen login pattern)</label>
|
<label class="form-label" for="signup-touchkey">Draw a touchkey (touchscreen login pattern)</label>
|
||||||
<br/>
|
<br/>
|
||||||
<svg id="touchkey-svg" width="400" height="400"></svg>
|
<svg id="touchkey-svg" width="400" height="400"></svg>
|
||||||
<br/>
|
<br/>
|
||||||
<input id="signup-touchkey" type="hidden" name="touchkey" value="" />
|
<input id="signup-touchkey" type="hidden" name="touchkey" value="" />
|
||||||
|
|
||||||
<input type="submit" value="Create account">
|
<input class="btn btn-primary" type="submit" value="Create account">
|
||||||
|
<a class="btn btn-secondary" href="/">Cancel</a>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
<div class="thumblist-item">
|
|
||||||
<a href="/">Cancel</a>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div id="osk-kbd" class="osk osk-kbd">
|
|
||||||
{% set lower = [['1','2','3','4','5','6','7','8','9','0','-','⌫'],
|
|
||||||
['q','w','e','r','t','y','u','i','o','p','[','⇥'],
|
|
||||||
['a','s','d','f','g','h','j','k','l',';','\'','⇤'],
|
|
||||||
['z','x','c','v','b','n','m',',','.','/','{','⇧'],
|
|
||||||
['SPACE']] %}
|
|
||||||
{% set upper = [['!','@','#','$','%','^','&','*','(',')','_','⌫'],
|
|
||||||
['Q','W','E','R','T','Y','U','I','O','P',']','⇥'],
|
|
||||||
['A','S','D','F','G','H','J','K','L',':','"','⇤'],
|
|
||||||
['Z','X','C','V','B','N','M','<','>','?','}','⇧'],
|
|
||||||
['SPACE']] %}
|
|
||||||
{% for lrow, urow in zip(lower, upper) %}
|
|
||||||
<div class="osk osk-kbd-row">
|
|
||||||
{% for lc, uc in zip(lrow, urow) %}
|
|
||||||
<div tabindex="1000" class="osk osk-button{% if lc == 'SPACE' %} osk-button-space{% endif %}" data-lowercase="{{ lc }}" data-uppercase="{{ uc }}">{{ lc }}</div>
|
|
||||||
{% endfor %}
|
|
||||||
</div>
|
|
||||||
{% endfor %}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<script src="/static/js/touchkey.js" ></script>
|
<script src="/static/js/touchkey.js" ></script>
|
||||||
<script>
|
<script>
|
||||||
initTouchkey(true, 'touchkey-svg', null, 'signup-touchkey');
|
initTouchkey(true, 'touchkey-svg', null, 'signup-touchkey');
|
||||||
</script>
|
</script>
|
||||||
<script>
|
|
||||||
let lastfocus = null;
|
|
||||||
let shift = 0;
|
|
||||||
let osk = document.getElementById('osk-kbd');
|
|
||||||
let inputs = [].slice.call(document.getElementsByClassName('osk-target'));
|
|
||||||
let oskButtons = document.getElementsByClassName('osk-button');
|
|
||||||
for (let i = 0; i < inputs.length; ++i) {
|
|
||||||
inputs[i].onfocus = () => {
|
|
||||||
osk.classList.add('visible');
|
|
||||||
}
|
|
||||||
inputs[i].onblur = (blur) => {
|
|
||||||
if (blur.relatedTarget !== undefined && blur.relatedTarget !== null && blur.relatedTarget.classList.contains('osk')) {
|
|
||||||
lastfocus = blur.target;
|
|
||||||
} else {
|
|
||||||
lastfocus = null;
|
|
||||||
if (blur.relatedTarget === undefined || blur.relatedTarget === null || !blur.relatedTarget.classList.contains('osk-target')) {
|
|
||||||
osk.classList.remove('visible');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for (let i = 0; i < oskButtons.length; ++i) {
|
|
||||||
oskButtons[i].onclick = (click) => {
|
|
||||||
if (lastfocus === null || lastfocus === undefined) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
let btn = click.target.innerText;
|
|
||||||
let idx = inputs.indexOf(lastfocus);
|
|
||||||
switch (btn) {
|
|
||||||
case '⇥':
|
|
||||||
lastfocus = inputs[(idx + 1) % inputs.length];
|
|
||||||
break;
|
|
||||||
case '⇤':
|
|
||||||
lastfocus = inputs[(((idx - 1) % inputs.length) + inputs.length) % inputs.length];
|
|
||||||
break;
|
|
||||||
case '⌫':
|
|
||||||
if (lastfocus.value.length > 0) {
|
|
||||||
lastfocus.value = lastfocus.value.substring(0, lastfocus.value.length-1);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case 'SPACE':
|
|
||||||
lastfocus.value += ' ';
|
|
||||||
break;
|
|
||||||
case '⇧':
|
|
||||||
shift = !shift;
|
|
||||||
click.target.classList.toggle('osk-locked');
|
|
||||||
for (let j = 0; j < oskButtons.length; ++j) {
|
|
||||||
oskButtons[j].innerText = oskButtons[j].getAttribute(shift ? 'data-uppercase' : 'data-lowercase');
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
default: {
|
|
||||||
lastfocus.value += btn;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
lastfocus.focus();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
</script>
|
|
||||||
|
|
||||||
{{ super() }}
|
{{ super() }}
|
||||||
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
|
@ -1,8 +1,6 @@
|
||||||
{% extends "base.html" %}
|
{% extends "base.html" %}
|
||||||
|
|
||||||
{% block header %}
|
{% block header %}
|
||||||
{# Show the setup name, as set in the config file, as page title. Don't escape HTML entities. #}
|
|
||||||
<h1>{{ setupname|safe }} Sales Statistics</h1>
|
|
||||||
<style>
|
<style>
|
||||||
@media all {
|
@media all {
|
||||||
svg g text {
|
svg g text {
|
||||||
|
@ -32,20 +30,22 @@
|
||||||
|
|
||||||
{% block main %}
|
{% block main %}
|
||||||
|
|
||||||
|
<h1>Sales Statistics</h1>
|
||||||
|
|
||||||
<section id="statistics-range">
|
<section id="statistics-range">
|
||||||
|
|
||||||
<h2>Time Range</h2>
|
<h2>Time Range</h2>
|
||||||
|
|
||||||
<form action="/statistics" method="get" accept-charset="utf-8">
|
<form action="/statistics" method="get" accept-charset="utf-8">
|
||||||
<label for="statistics-range-from">From:</label>
|
<label class="form-label" for="statistics-range-from">From:</label>
|
||||||
<input type="date" id="statistics-range-from" name="fromdate" value="{{fromdate}}" />
|
<input class="form-control" type="date" id="statistics-range-from" name="fromdate" value="{{fromdate}}" />
|
||||||
<span class="input-replacement">{{fromdate}}</span>
|
<span class="input-replacement">{{fromdate}}</span>
|
||||||
|
|
||||||
<label for="statistics-range-to">To:</label>
|
<label class="form-label" for="statistics-range-to">To:</label>
|
||||||
<input type="date" id="statistics-range-to" name="todate" value="{{todate}}" />
|
<input class="form-control" type="date" id="statistics-range-to" name="todate" value="{{todate}}" />
|
||||||
<span class="input-replacement">{{todate}}</span>
|
<span class="input-replacement">{{todate}}</span>
|
||||||
|
|
||||||
<input type="submit" value="Update">
|
<input class="btn btn-primary" type="submit" value="Update">
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
</section>
|
</section>
|
||||||
|
@ -67,12 +67,18 @@
|
||||||
|
|
||||||
<h2>Purchases</h2>
|
<h2>Purchases</h2>
|
||||||
|
|
||||||
<table>
|
<table class="table table-striped">
|
||||||
<tr class="head"><td>Product</td><td>Income</td><td>Units</td></tr>
|
<thead>
|
||||||
|
<tr><th>Product</th><th>Income</th><th>Units</th></tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
{% for prod, data in consumptions.items() %}
|
{% for prod, data in consumptions.items() %}
|
||||||
<tr class="{{ loop.cycle('odd', '') }}"><td><b>{{ prod }}</b></td><td>{{ -data[0]|chf }}</td><td>{{ data[1] }}</td></tr>
|
<tr><td>{{ prod }}</td><td>{{ -data[0]|chf }}</td><td>{{ data[1] }}</td></tr>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
<tr class="foot"><td>Total</td><td>{{ total_income|chf }}</td><td>{{ total_consumption }}</td></tr>
|
</tbody>
|
||||||
|
<tfoot>
|
||||||
|
<tr><td>Total</td><td>{{ total_income|chf }}</td><td>{{ total_consumption }}</td></tr>
|
||||||
|
</tfoot>
|
||||||
</table>
|
</table>
|
||||||
|
|
||||||
{# Really hacky pie chart implementation. #}
|
{# Really hacky pie chart implementation. #}
|
||||||
|
|
|
@ -1,43 +1,28 @@
|
||||||
{% extends "base.html" %}
|
{% extends "base.html" %}
|
||||||
|
|
||||||
{% block head %}
|
|
||||||
{{ super() }}
|
|
||||||
<style>
|
|
||||||
svg {
|
|
||||||
width: 400px;
|
|
||||||
height: auto;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
{% endblock %}
|
|
||||||
|
|
||||||
{% block header %}
|
|
||||||
<h1>Welcome, {{ username }}</h1>
|
|
||||||
{{ super() }}
|
|
||||||
{% endblock %}
|
|
||||||
|
|
||||||
{% block main %}
|
{% block main %}
|
||||||
|
|
||||||
<svg id="touchkey-svg" width="400" height="400"></svg>
|
<h1>Welcome, {{ username }}</h1>
|
||||||
|
|
||||||
<form method="post" action="/touchkey" id="loginform" accept-charset="UTF-8">
|
<svg id="touchkey-svg" width="400" height="400"></svg>
|
||||||
|
|
||||||
|
<form method="post" action="/touchkey" id="loginform" accept-charset="UTF-8">
|
||||||
<input type="hidden" name="uid" value="{{ uid }}" />
|
<input type="hidden" name="uid" value="{{ uid }}" />
|
||||||
<input type="hidden" name="username" value="{{ username }}" />
|
<input type="hidden" name="username" value="{{ username }}" />
|
||||||
<input type="hidden" name="touchkey" value="" id="loginform-touchkey-value" />
|
<input type="hidden" name="touchkey" value="" id="loginform-touchkey-value" />
|
||||||
{% if buypid %}
|
{% if buypid %}
|
||||||
<input type="hidden" name="buypid" value="{{ buypid }}" />
|
<input type="hidden" name="buypid" value="{{ buypid }}" />
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
<div class="thumblist-item">
|
<a class="btn btn-secondary" href="/">Cancel</a>
|
||||||
<a href="/">Cancel</a>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<script src="/static/js/touchkey.js"></script>
|
<script src="/static/js/touchkey.js"></script>
|
||||||
<script>
|
<script>
|
||||||
initTouchkey(false, 'touchkey-svg', 'loginform', 'loginform-touchkey-value');
|
initTouchkey(false, 'touchkey-svg', 'loginform', 'loginform-touchkey-value');
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{{ super() }}
|
{{ super() }}
|
||||||
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
|
|
|
@ -1,26 +1,22 @@
|
||||||
{% extends "base.html" %}
|
{% extends "base.html" %}
|
||||||
|
|
||||||
{% block header %}
|
|
||||||
{# Show the setup name, as set in the config file, as page title followed by "Setup". Don't escape HTML entities. #}
|
|
||||||
<h1>{{ setupname|safe }} Setup</h1>
|
|
||||||
{{ super() }}
|
|
||||||
{% endblock %}
|
|
||||||
|
|
||||||
{% block main %}
|
{% block main %}
|
||||||
|
|
||||||
|
<h1>Welcome</h1>
|
||||||
|
|
||||||
{# Show a user creation form #}
|
{# Show a user creation form #}
|
||||||
Please create an admin user account
|
Please create an admin user account
|
||||||
<form method="post" action="/userbootstrap" accept-charset="UTF-8">
|
<form method="post" action="/userbootstrap" accept-charset="UTF-8">
|
||||||
<label for="username">Username: </label>
|
<label class="form-label" for="username">Username: </label>
|
||||||
<input id="username" type="text" name="username"/><br/>
|
<input class="form-control" id="username" type="text" name="username"/><br/>
|
||||||
|
|
||||||
<label for="password">Password: </label>
|
<label class="form-label" for="password">Password: </label>
|
||||||
<input id="password" type="password" name="password"/><br/>
|
<input class="form-control" id="password" type="password" name="password"/><br/>
|
||||||
|
|
||||||
<label for="password2">Repeat: </label>
|
<label class="form-label" for="password2">Repeat: </label>
|
||||||
<input id="password2" type="password" name="password2"/><br/>
|
<input class="form-control" id="password2" type="password" name="password2"/><br/>
|
||||||
|
|
||||||
<input type="submit" value="Create user">
|
<input class="btn btn-success" type="submit" value="Create user">
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
{{ super() }}
|
{{ super() }}
|
||||||
|
|
|
@ -1,35 +1,22 @@
|
||||||
{% extends "base.html" %}
|
{% extends "base.html" %}
|
||||||
|
|
||||||
{% block header %}
|
|
||||||
{# Show the setup name, as set in the config file, as page title. Don't escape HTML entities. #}
|
|
||||||
<h1>{{ setupname|safe }}</h1>
|
|
||||||
{{ super() }}
|
|
||||||
{% endblock %}
|
|
||||||
|
|
||||||
{% block main %}
|
{% block main %}
|
||||||
|
|
||||||
|
<div class="row itemlist">
|
||||||
{% for user in users %}
|
{% for user in users %}
|
||||||
{# Show an item per user, consisting of the username, and the avatar, linking to the touchkey login #}
|
{# Show an item per user, consisting of the username, and the avatar, linking to the touchkey login #}
|
||||||
<div class="thumblist-item">
|
<div class="col-sm-1 g-4">
|
||||||
<a href="/touchkey?uid={{ user.id }}&username={{ user.name }}{% if buyproduct %}&buypid={{ buyproduct.id }}{% endif %}">
|
<a class="card h-100 text-bg-light" href="/touchkey?uid={{ user.id }}&username={{ user.name }}{% if buyproduct %}&buypid={{ buyproduct.id }}{% endif %}">
|
||||||
<span class="thumblist-title">{{ user.name }}</span><br/>
|
<div class="card-header">
|
||||||
<div class="imgcontainer">
|
{{ user.name }}
|
||||||
<img src="/static/upload/thumbnails/users/{{ user.id }}.png?cacheBuster={{ now }}" alt="Avatar of {{ user.name }}" draggable="false"/>
|
|
||||||
</div>
|
</div>
|
||||||
|
<img class="card-img-bottom" src="/static/upload/thumbnails/users/{{ user.id }}.png?cacheBuster={{ now }}" alt="Avatar of {{ user.name }}" draggable="false"/>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
|
||||||
<br/>
|
<br/>
|
||||||
{# Link to the password login #}
|
|
||||||
<div class="thumblist-item">
|
|
||||||
<a href="/login">Password login</a>
|
|
||||||
</div>
|
|
||||||
{% if signup %}
|
|
||||||
<div class="thumblist-item">
|
|
||||||
<a href="/signup">Create account</a>
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
{{ super() }}
|
{{ super() }}
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue