matemat/matemat/webserver/pagelets/admin.py

145 lines
6.7 KiB
Python

import os
from datetime import datetime, UTC
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.util.thumbnails import upload_thumbnail
from matemat.webserver import session, template
from matemat.webserver.config import get_app_config, get_stock_provider
from matemat.webserver.template import Notification
@get('/admin')
@post('/admin')
def admin():
"""
The admin panel, shows settings to modify other users and products.
"""
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 (2) or token (1)
if authlevel < 3:
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 an "adminchange" parameter, delegate the change handling to the function below
if 'adminchange' in request.params and user.is_admin:
handle_change(request.params, request.files, db)
# Fetch all existing users and products from the database
users = db.list_users()
products = db.list_products()
barcodes = db.list_barcodes()
# Render the "Admin" page
now = str(int(datetime.now(UTC).timestamp()))
return template.render('admin.html',
authuser=user, authlevel=authlevel, users=users, products=products, barcodes=barcodes,
receipt_preference_class=ReceiptPreference, now=now,
setupname=config['InstanceName'], config_smtp_enabled=config['SmtpSendReceipts'])
def handle_change(args: FormsDict, files: FormsDict, db: MatematDatabase):
"""
Write the changes requested by an admin for users of products.
:param args: The RequestArguments object passed to the pagelet.
:param db: The database facade where changes are written to.
"""
config = get_app_config()
try:
# Read the type of change requested by the admin, then switch over it
change = str(args.adminchange)
# The user requested to create a new user
if change == 'newuser':
# Only create a new user if all required properties of the user are present in the request arguments
if 'username' not in args or 'password' not in args:
return
# Read the properties from the request arguments
username = str(args.username)
email = None
if 'email' in args and len(args.email) > 0:
# An empty e-mail field should be interpreted as NULL
email = str(args.email)
password = str(args.password)
is_member = 'ismember' in args
is_admin = 'isadmin' in args
logout_after_purchase = 'logout_after_purchase' in args
balance = parse_chf(str(args.balance))
# Create the user in the database
newuser: User = db.create_user(username, password, email, member=is_member, admin=is_admin,
logout_after_purchase=logout_after_purchase, balance=balance)
# The user requested to create a new product
elif change == 'newproduct':
# Only create a new product if all required properties of the product are present in the request arguments
for key in ['name', 'pricemember', 'pricenonmember']:
if key not in args:
return
# Read the properties from the request arguments
name = str(args.name)
price_member = parse_chf(str(args.pricemember))
price_non_member = parse_chf(str(args.pricenonmember))
custom_price = 'custom_price' in args
stockable = 'stockable' in args
barcode = str(args.barcode) or None
# Create the product in the database
newproduct = db.create_product(name, price_member, price_non_member, custom_price, stockable, barcode)
# If a new product image was uploaded, process it
image = files.image.file.read() if 'image' in files else None
filename = os.path.join(os.path.abspath(config['UploadDir']), f'thumbnails/products/{newproduct.id}.png')
if image is not None and len(image) > 0:
try:
upload_thumbnail(image, filename)
except Exception as e:
Notification.error(str(e), decay=True)
return
# The user requested to restock a product
elif change == 'restock':
stock_provider = get_stock_provider()
if not stock_provider.needs_update():
return
# Only restock a product if all required properties are present in the request arguments
if 'productid' not in args or 'amount' not in args:
return
# Read the properties from the request arguments
productid = int(str(args.productid))
amount = int(str(args.amount))
# Fetch the product to restock from the database
product = db.get_product(productid)
if not product.stockable:
return
stock_provider.update_stock(product, amount)
# The user requested to set default images
elif change == 'defaultimg':
# Iterate the possible images to set
for category in 'users', 'products':
if category not in files:
continue
# Read the raw image data from the request
default: bytes = files[category].file.read()
filename: str = os.path.join(os.path.abspath(config['UploadDir']), f'thumbnails/{category}/default.png')
try:
if upload_thumbnail(default, filename):
Notification.success(f'{category} default image updated successfully.', decay=True)
except Exception as e:
Notification.error(str(e), decay=True)
return
except Exception as e:
Notification.error(str(e), decay=True)
return