Merge pull request #3 from Moritz921/responsiveDesign

Responsive design + popup alternative
This commit is contained in:
Moritz
2025-06-08 00:20:53 +02:00
committed by GitHub
8 changed files with 780 additions and 345 deletions

View File

@@ -1,27 +1,45 @@
"""
This module defines database models and utility functions for managing users and drinks in a beverage tracking application using SQLAlchemy and FastAPI.
Tables:
- users_postpaid: Stores postpaid user accounts.
- users_prepaid: Stores prepaid user accounts, linked to postpaid users.
- drinks: Records drink transactions for both postpaid and prepaid users.
Database models and operations for a drink management system using SQLAlchemy and FastAPI.
This module provides functions to manage postpaid and prepaid users, handle drink transactions,
log money transfers, and generate statistics. It includes table creation, user CRUD operations,
transaction logging, drink recording, and utility functions for drink statistics.
Constants:
DRINK_COST (int): Cost of a drink in cents.
AVAILABLE_DRINKS (list): List of available drink types.
Functions:
- create_postpaid_user(username: str) -> int:
Creates a new postpaid user with the specified username. Raises HTTPException if the user already exists or if a database error occurs. Returns the ID of the newly created user.
- get_postpaid_user(user_id: int) -> dict:
Retrieves a postpaid user's information by their user ID. Returns a dictionary with user details. Raises HTTPException if the user is not found.
- get_postpaid_user_by_username(username: str) -> dict:
Retrieves a postpaid user's information by their username. Returns a dictionary with user details. Raises HTTPException if the user is not found.
- set_postpaid_user_money(user_id: int, money: float) -> int:
Updates the 'money' balance for a postpaid user. Raises HTTPException if the user is not found. Returns the number of rows affected.
- drink_postpaid_user(user_id: int) -> int:
Deducts 100 units from the specified postpaid user's balance and records a drink entry. Raises HTTPException if the user is not found or if the drink entry could not be created. Returns the number of rows affected by the drink entry insertion.
_log_transaction(user_id, user_is_postpaid, ...): Log a user's money transaction.
create_postpaid_user(username): Create a new postpaid user.
get_postpaid_user(user_id): Retrieve a postpaid user's info by ID.
get_postpaid_user_by_username(username): Retrieve a postpaid user by username.
set_postpaid_user_money(user_id, money): Set a postpaid user's balance.
drink_postpaid_user(user_id, drink_type): Deduct drink cost and record a drink for postpaid user
toggle_activate_postpaid_user(user_id): Toggle activation status of a postpaid user.
payup_postpaid_user(current_user_id, payup_user_id, money_cent): Transfer money btw. post users.
get_prepaid_user(user_id): Retrieve a prepaid user's info by ID.
get_prepaid_user_by_username(username): Retrieve a prepaid user by username.
create_prepaid_user(prepaid_username, postpaid_user_id, start_money): Create a new prepaid user.
drink_prepaid_user(user_db_id): Process a prepaid drink transaction.
toggle_activate_prepaid_user(user_id): Toggle activation status of a prepaid user.
set_prepaid_user_money(user_id, money, postpaid_user_id): Set prepaid user's balance and link.
del_user_prepaid(user_id): Delete a prepaid user.
get_last_drink(user_id, user_is_postpaid, max_since_seconds): Get last drink within time window.
revert_last_drink(user_id, user_is_postpaid, drink_id, drink_cost): Revert a drink and refund.
update_drink_type(user_id, user_is_postpaid, drink_id, drink_type): Update drink type for drink.
get_most_used_drinks(user_id, user_is_postpaid, limit): Get most used drinks for a user.
get_stats_drink_types(): Get statistics of drink types.
HTTPException: For database errors, not found, or forbidden actions.
"""
import os
import secrets
import datetime
from sqlalchemy import create_engine, text
import random
from sqlalchemy import create_engine, text, select
from fastapi import HTTPException
import os
from dotenv import load_dotenv
load_dotenv()
@@ -31,6 +49,7 @@ DATABASE_URL = "sqlite:///" + str(DATABASE_FILE)
engine = create_engine(DATABASE_URL, connect_args={"check_same_thread": False})
DRINK_COST = 100 # cent
AVAILABLE_DRINKS = ["Paulaner Spezi", "Mio Mate", "Club Mate", "Eistee Pfirsisch"]
with engine.connect() as conn:
# Create a table for postpaid users
@@ -100,22 +119,30 @@ def _log_transaction(
description = None
):
"""
Logs a transaction for a user, recording changes in their account balance.
Depending on whether the user is postpaid or prepaid, retrieves the previous balance if not provided,
calculates the new balance and delta if necessary, and inserts a transaction record into the database.
Args:
user_id (int): The ID of the user for whom the transaction is being logged.
user_is_postpaid (bool): True if the user is postpaid, False if prepaid.
previous_money_cent (Optional[int], default=None): The user's previous balance in cents. If None, it is fetched from the database.
new_money_cent (Optional[int], default=None): The user's new balance in cents. If None, it is calculated using delta_money_cent.
delta_money_cent (Optional[int], default=None): The change in balance in cents. If None, it is calculated using new_money_cent.
description (Optional[str], default=None): A description of the transaction.
Raises:
HTTPException: If the user is not found, if both new_money_cent and delta_money_cent are missing,
or if the transaction could not be logged.
Returns:
int: The ID of the newly created transaction record.
"""
Logs a transaction for a user, recording changes in their account balance.
Depending on whether the user is postpaid or prepaid, retrieves the previous balance if not
provided, calculates the new balance and delta if necessary, and inserts a transaction record
into the database.
Args:
user_id (int): The ID of the user for whom the transaction is being logged.
user_is_postpaid (bool): True if the user is postpaid, False if prepaid.
previous_money_cent (Optional[int], default=None): The user's previous balance in cents.
If None, it is fetched from the database.
new_money_cent (Optional[int], default=None): The user's new balance in cents. If None,
it is calculated using delta_money_cent.
delta_money_cent (Optional[int], default=None): The change in balance in cents. If None,
it is calculated using new_money_cent.
description (Optional[str], default=None): A description of the transaction.
Raises:
HTTPException: If the user is not found, if both new_money_cent and delta_money_cent are
missing, or if the transaction could not be logged.
Returns:
int: The ID of the newly created transaction record.
"""
if previous_money_cent is None:
if user_is_postpaid:
t_get_prev_money = text("SELECT money FROM users_postpaid WHERE id = :id")
@@ -128,7 +155,13 @@ def _log_transaction(
else:
raise HTTPException(status_code=404, detail="User not found")
if new_money_cent is None and delta_money_cent is None:
raise HTTPException(status_code=400, detail="Either new_money or delta_money must be provided, not both")
raise HTTPException(
status_code=400,
detail=(
"Either new_money or delta_money "
"must be provided, not both"
)
)
if new_money_cent is None and delta_money_cent is not None:
new_money_cent = previous_money_cent + delta_money_cent
elif delta_money_cent is None and new_money_cent is not None:
@@ -246,8 +279,12 @@ def set_postpaid_user_money(user_id: int, money: float):
int: The number of rows affected by the update operation.
"""
print(f"set_postpaid_user_money: {user_id}, {money}")
_log_transaction(user_id, user_is_postpaid=True, new_money_cent=money, description="Set money manually via Admin UI")
_log_transaction(
user_id,
user_is_postpaid=True,
new_money_cent=money,
description="Set money manually via Admin UI"
)
t = text("UPDATE users_postpaid SET money = :money WHERE id = :id")
with engine.connect() as connection:
result = connection.execute(t, {"id": user_id, "money": money})
@@ -300,6 +337,15 @@ def drink_postpaid_user(user_id: int, drink_type: str = ""):
return result.rowcount
def toggle_activate_postpaid_user(user_id: int):
"""
Toggles the 'activated' status of a postpaid user in the database.
Args:
user_id (int): The ID of the user whose activation status should be toggled.
Returns:
int: The number of rows affected by the update operation.
Raises:
HTTPException: If no user with the given ID is found (404 error).
"""
prev_activated = get_postpaid_user(user_id)["activated"]
t = text("UPDATE users_postpaid SET activated = :activated WHERE id = :id")
with engine.connect() as connection:
@@ -309,8 +355,71 @@ def toggle_activate_postpaid_user(user_id: int):
connection.commit()
return result.rowcount
def payup_postpaid_user(current_user_id: int, payup_user_id: int, money_cent: int):
"""
Transfers a specified amount of money (in cents) from one postpaid user to another.
Args:
current_user_id (int): ID of the user paying the money.
payup_user_id (int): ID of the user receiving the money.
money_cent (int): Amount of money to transfer, in cents.
Raises:
HTTPException: If either user is not activated or not found in the database.
"""
current_user = get_postpaid_user(current_user_id)
if not current_user["activated"]:
raise HTTPException(status_code=403, detail="Current user not activated")
payup_user = get_postpaid_user(payup_user_id)
if not payup_user["activated"]:
raise HTTPException(status_code=403, detail="Payup user not activated")
# subtract money from current user
t = text("UPDATE users_postpaid SET money = money - :money WHERE id = :id")
with engine.connect() as connection:
result = connection.execute(t, {"id": current_user_id, "money": money_cent})
if result.rowcount == 0:
raise HTTPException(status_code=404, detail="Current user not found")
connection.commit()
_log_transaction(
user_id=current_user_id,
user_is_postpaid=True,
previous_money_cent=current_user["money"],
delta_money_cent=-money_cent,
description=f"Payup to user {payup_user_id}"
)
# add money to payup user
t = text("UPDATE users_postpaid SET money = money + :money WHERE id = :id")
with engine.connect() as connection:
result = connection.execute(t, {"id": payup_user_id, "money": money_cent})
if result.rowcount == 0:
raise HTTPException(status_code=404, detail="Payup user not found")
connection.commit()
_log_transaction(
user_id=payup_user_id,
user_is_postpaid=True,
previous_money_cent=payup_user["money"],
new_money_cent=payup_user["money"] + money_cent,
delta_money_cent=money_cent,
description=f"Received payup from user {current_user_id}"
)
def get_prepaid_user(user_id: int):
"""
Retrieve a prepaid user from the database by user ID.
Args:
user_id (int): The ID of the prepaid user to retrieve.
Returns:
dict: A dictionary containing the user's information with keys:
- "id": User's ID
- "username": User's username
- "user_key": User's unique key
- "postpaid_user_id": Linked postpaid user ID (if any)
- "money": User's prepaid balance
- "activated": Activation status
- "last_drink": Timestamp of the user's last drink
Raises:
HTTPException: If no user with the given ID is found (status code 404).
"""
t = text("SELECT id, username, user_key, postpaid_user_id, money, activated, last_drink FROM users_prepaid WHERE id = :id")
user_db = {}
with engine.connect() as connection:
@@ -329,15 +438,14 @@ def get_prepaid_user(user_id: int):
def get_prepaid_user_by_username(username: str):
"""
Retrieve a prepaid user from the database by their username.
Retrieve a prepaid user by username.
Args:
username (str): The username of the user to retrieve.
username (str): The username to look up.
Returns:
dict: A dictionary containing the user's id, username, money, activated status, and last_drink timestamp.
dict: User info (id, username, user_key, postpaid_user_id, money, activated, last_drink).
Raises:
HTTPException: If no user with the given username is found, raises a 404 HTTPException.
HTTPException: If user not found (404).
"""
t = text("SELECT id, username, user_key, postpaid_user_id, money, activated, last_drink FROM users_prepaid WHERE username = :username")
user_db = {}
with engine.connect() as connection:
@@ -355,6 +463,17 @@ def get_prepaid_user_by_username(username: str):
return user_db
def create_prepaid_user(prepaid_username: str, postpaid_user_id: int, start_money: int = 0):
"""
Create a new prepaid user in users_prepaid.
Args:
prepaid_username (str): Username for the new prepaid user.
postpaid_user_id (int): Associated postpaid user ID.
start_money (int, optional): Initial money for the prepaid user. Defaults to 0.
Raises:
HTTPException: If username exists (400) or user creation fails (500).
Returns:
int: ID of the new prepaid user.
"""
prepaid_key = secrets.token_urlsafe(6)
t = text("INSERT INTO users_prepaid (username, user_key, postpaid_user_id, money) VALUES (:username, :user_key, :postpaid_user_id, :start_money)")
with engine.connect() as connection:
@@ -379,6 +498,24 @@ def create_prepaid_user(prepaid_username: str, postpaid_user_id: int, start_mone
return result.lastrowid
def drink_prepaid_user(user_db_id: int):
"""
Processes a prepaid drink transaction for a user.
This function performs the following steps:
1. Retrieves the prepaid user's information from the database.
2. Checks if the user is activated; raises HTTP 403 if not.
3. Checks if the user has enough money for a drink; raises HTTP 403 if not.
4. Logs the transaction.
5. Deducts the drink cost from the user's balance and updates the last drink timestamp.
6. Inserts a new entry into the drinks table for the user.
7. Commits all changes to the database.
Args:
user_db_id (int): The database ID of the prepaid user.
Returns:
int: The number of rows affected by the drink entry insertion.
Raises:
HTTPException: If the user is not activated (403), does not have enough money (403),
is not found (404), or if the drink entry could not be created (500).
"""
user_dict = get_prepaid_user(user_db_id)
if not user_dict["activated"]:
raise HTTPException(status_code=403, detail="User not activated")
@@ -386,6 +523,7 @@ def drink_prepaid_user(user_db_id: int):
prev_money = user_dict["money"]
if prev_money < DRINK_COST:
raise HTTPException(status_code=403, detail="Not enough money")
_log_transaction(
user_id=user_db_id,
user_is_postpaid=False,
@@ -409,6 +547,15 @@ def drink_prepaid_user(user_db_id: int):
return result.rowcount
def toggle_activate_prepaid_user(user_id: int):
"""
Toggles the 'activated' status of a prepaid user in the database.
Args:
user_id (int): The ID of the user whose activation status is to be toggled.
Returns:
int: The number of rows affected by the update operation.
Raises:
HTTPException: If no user with the given ID is found (404 error).
"""
prev_activated = get_prepaid_user(user_id)["activated"]
t = text("UPDATE users_prepaid SET activated = :activated WHERE id = :id")
with engine.connect() as connection:
@@ -419,6 +566,17 @@ def toggle_activate_prepaid_user(user_id: int):
return result.rowcount
def set_prepaid_user_money(user_id: int, money: int, postpaid_user_id: int):
"""
Updates the prepaid user's money and associated postpaid user ID in the database.
Args:
user_id (int): The ID of the prepaid user whose information is to be updated.
money (int): The new amount of money (in cents) to set for the user.
postpaid_user_id (int): The ID of the associated postpaid user.
Raises:
HTTPException: If the user with the given user_id is not found in the database.
Returns:
int: The number of rows affected by the last update operation.
"""
t1 = text("UPDATE users_prepaid SET money = :money WHERE id = :id")
t2 = text("UPDATE users_prepaid SET postpaid_user_id = :postpaid_user_id WHERE id = :id")
_log_transaction(
@@ -438,6 +596,15 @@ def set_prepaid_user_money(user_id: int, money: int, postpaid_user_id: int):
return result.rowcount
def del_user_prepaid(user_id: int):
"""
Deletes a user's prepaid entry from the 'users_prepaid' table by user ID.
Args:
user_id (int): The ID of the user whose prepaid entry should be deleted.
Raises:
HTTPException: If no entry with the given user_id is found (404 Not Found).
Returns:
int: The number of rows deleted (should be 1 if successful).
"""
t = text("DELETE FROM users_prepaid WHERE id = :id")
with engine.connect() as connection:
result = connection.execute(t, {"id": user_id})
@@ -447,6 +614,18 @@ def del_user_prepaid(user_id: int):
return result.rowcount
def get_last_drink(user_id: int, user_is_postpaid: bool, max_since_seconds: int = 60):
"""
Retrieve the most recent drink entry for a user within a specified time window.
Args:
user_id (int): The ID of the user whose last drink is to be retrieved.
user_is_postpaid (bool): True if the user is postpaid, False if prepaid.
max_since_seconds (int, optional): Max seconds since last drink. Defaults to 60.
Returns:
dict or None: Dict with 'id', 'timestamp', 'drink_type' if found within time window,
else None.
"""
if user_is_postpaid:
t = text("SELECT id, timestamp, drink_type FROM drinks WHERE postpaid_user_id = :user_id ORDER BY timestamp DESC LIMIT 1")
else:
@@ -466,10 +645,24 @@ def get_last_drink(user_id: int, user_is_postpaid: bool, max_since_seconds: int
last_drink_time = last_drink_time.replace(tzinfo=datetime.timezone.utc)
if (now - last_drink_time).total_seconds() > max_since_seconds:
return None
print(f"get_last_drink: user_id={user_id}, user_is_postpaid={user_is_postpaid}, drink_id={drink_id}, timestamp={timestamp}, drink_type={drink_type}")
return {"id": drink_id, "timestamp": timestamp, "drink_type": drink_type}
drink_obj = {"id": drink_id, "timestamp": timestamp, "drink_type": drink_type}
return drink_obj
def revert_last_drink(user_id: int, user_is_postpaid: bool, drink_id: int, drink_cost: int = DRINK_COST):
"""
Reverts the last drink purchase for a user and refunds the drink cost.
Args:
user_id (int): The ID of the user whose drink is to be reverted.
user_is_postpaid (bool): True if the user is postpaid, False if prepaid.
drink_id (int): The ID of the drink to revert.
drink_cost (int, optional): The cost of the drink in cents. Defaults to DRINK_COST.
Raises:
HTTPException: If the drink or user is not found in the database.
Side Effects:
- Deletes the drink record from the database.
- Refunds the drink cost to the user's balance.
- Logs the transaction.
"""
if user_is_postpaid:
del_t = text("DELETE FROM drinks WHERE postpaid_user_id = :user_id AND id = :drink_id")
update_t = text("UPDATE users_postpaid SET money = money + :drink_cost WHERE id = :user_id")
@@ -481,7 +674,6 @@ def revert_last_drink(user_id: int, user_is_postpaid: bool, drink_id: int, drink
with engine.connect() as connection:
# Check if the drink exists
print(f"revert_last_drink: user_id={user_id}, user_is_postpaid={user_is_postpaid}, drink_id={drink_id}, drink_cost={drink_cost}")
drink_exists = connection.execute(del_t, {"user_id": user_id, "drink_id": drink_id}).rowcount > 0
if not drink_exists:
raise HTTPException(status_code=404, detail="Drink not found")
@@ -490,11 +682,11 @@ def revert_last_drink(user_id: int, user_is_postpaid: bool, drink_id: int, drink
prev_money = connection.execute(money_t, {"user_id": user_id}).fetchone()
if not prev_money:
raise HTTPException(status_code=404, detail="User not found")
new_money = prev_money[0] + drink_cost
connection.execute(update_t, {"user_id": user_id, "drink_cost": drink_cost})
connection.commit()
_log_transaction(
user_id=user_id,
user_is_postpaid=user_is_postpaid,
@@ -505,6 +697,20 @@ def revert_last_drink(user_id: int, user_is_postpaid: bool, drink_id: int, drink
)
def update_drink_type(user_id: int, user_is_postpaid: bool, drink_id, drink_type: str):
"""
Updates the drink type for a specific drink associated with a user.
Depending on whether the user is postpaid or prepaid, the function updates the `drink_type`
field in the `drinks` table for the drink with the given `drink_id` and user association.
Args:
user_id (int): The ID of the user whose drink is being updated.
user_is_postpaid (bool): Indicates if the user is postpaid (True) or prepaid (False).
drink_id: The ID of the drink to update.
drink_type (str): The new type to set for the drink.
Raises:
HTTPException: If no drink is found with the given criteria (404 Not Found).
Returns:
int: The number of rows affected by the update.
"""
if user_is_postpaid:
t = text("UPDATE drinks SET drink_type = :drink_type WHERE postpaid_user_id = :user_id AND id = :drink_id")
else:
@@ -516,3 +722,51 @@ def update_drink_type(user_id: int, user_is_postpaid: bool, drink_id, drink_type
raise HTTPException(status_code=404, detail="Drink not found")
connection.commit()
return result.rowcount
def get_most_used_drinks(user_id: int, user_is_postpaid: bool, limit: int = 4):
"""
Return up to `limit` most used drinks for a user, filling with random drinks if needed.
Args:
user_id (int): User's ID.
user_is_postpaid (bool): True if postpaid, else prepaid.
limit (int): Max drinks to return.
Returns:
list[dict]: Each dict has 'drink_type' and 'count'.
"""
if user_is_postpaid:
t = text("SELECT drink_type, count(drink_type) as count FROM drinks WHERE postpaid_user_id = :user_id AND drink_type IS NOT NULL AND drink_type != 'Sonstiges' GROUP BY drink_type ORDER BY count DESC LIMIT :limit")
else:
t = text("SELECT drink_type, count(drink_type) as count FROM drinks WHERE prepaid_user_id = :user_id AND drink_type IS NOT NULL AND drink_type != 'Sonstiges' GROUP BY drink_type ORDER BY count DESC LIMIT :limit")
with engine.connect() as connection:
result = connection.execute(t, {"user_id": user_id, "limit": limit}).fetchall()
if not result:
return []
drinks = [{"drink_type": row[0], "count": row[1]} for row in result]
while len(drinks) < limit:
random_drink = random.choice(AVAILABLE_DRINKS)
if any(drink["drink_type"] == random_drink for drink in drinks):
continue
drinks.append({"drink_type": random_drink, "count": 0})
return drinks
def get_stats_drink_types():
"""
Retrieves statistics on drink types from the database.
Executes a SQL query to count the occurrences of each non-null drink type in the 'drinks' table,
grouping and ordering the results by the count in descending order.
Returns:
list[dict]: A list of dictionaries, each containing 'drink_type' (str) and 'count' (int).
Returns an empty list if no results are found.
"""
t = text("SELECT drink_type, count(drink_type) as count FROM drinks WHERE drink_type IS NOT NULL GROUP BY drink_type ORDER BY count DESC")
with engine.connect() as connection:
result = connection.execute(t).fetchall()
if not result:
return []
drinks = [{"drink_type": row[0], "count": row[1]} for row in result]
return drinks

262
main.py
View File

@@ -1,35 +1,19 @@
import random
import os
from dotenv import load_dotenv
from fastapi import FastAPI, Request, Form, HTTPException
from fastapi.responses import RedirectResponse, HTMLResponse
from fastapi.staticfiles import StaticFiles
from fastapi.templating import Jinja2Templates
from fastapi.responses import JSONResponse
from starlette.middleware.sessions import SessionMiddleware
import uvicorn
from sqlalchemy import text
from db.models import engine
from db.models import get_postpaid_user
from db.models import get_postpaid_user_by_username
from db.models import set_postpaid_user_money
from db.models import drink_postpaid_user
from db.models import toggle_activate_postpaid_user
from db.models import get_prepaid_user
from db.models import get_prepaid_user_by_username
from db.models import create_prepaid_user
from db.models import drink_prepaid_user
from db.models import toggle_activate_prepaid_user
from db.models import set_prepaid_user_money
from db.models import del_user_prepaid
from db.models import get_last_drink
from db.models import revert_last_drink
from db.models import update_drink_type
import db.models
from auth import oidc
import os
from dotenv import load_dotenv
@@ -49,7 +33,6 @@ templates = Jinja2Templates(directory="templates")
@app.get("/", response_class=HTMLResponse)
def home(request: Request):
# Check if user is logged in and has a valid session
user_db_id = request.session.get("user_db_id")
user_authentik = request.session.get("user_authentik")
@@ -71,7 +54,7 @@ def home(request: Request):
if result:
users = []
for row in result:
user_db = get_postpaid_user(row[0])
user_db = db.models.get_postpaid_user(row[0])
if user_db:
users.append(user_db)
@@ -84,7 +67,7 @@ def home(request: Request):
if result:
db_users_prepaid = []
for row in result:
prepaid_user = get_prepaid_user(row[0])
prepaid_user = db.models.get_prepaid_user(row[0])
if prepaid_user:
db_users_prepaid.append(prepaid_user)
# additionally load all prepaid users from the current user
@@ -93,19 +76,22 @@ def home(request: Request):
if result:
prepaid_users_from_curr_user = []
for row in result:
prepaid_user = get_prepaid_user(row[0])
prepaid_user = db.models.get_prepaid_user(row[0])
if prepaid_user:
prepaid_users_from_curr_user.append(prepaid_user)
# load current user from database
user_is_postpaid = get_is_postpaid(user_authentik)
if user_is_postpaid:
db_user = get_postpaid_user(user_db_id)
db_user = db.models.get_postpaid_user(user_db_id)
else:
db_user = get_prepaid_user(user_db_id)
db_user = db.models.get_prepaid_user(user_db_id)
# get last drink for current user, if not less than 60 seconds ago
last_drink = get_last_drink(user_db_id, user_is_postpaid, 60)
last_drink = db.models.get_last_drink(user_db_id, user_is_postpaid, 60)
most_used_drinks = db.models.get_most_used_drinks(user_db_id, user_is_postpaid, 3)
most_used_drinks.append({"drink_type": "Sonstiges", "count": 0}) # ensure "Sonstiges" is in
return templates.TemplateResponse("index.html", {
"request": request,
@@ -116,7 +102,7 @@ def home(request: Request):
"db_users_prepaid": db_users_prepaid,
"prepaid_users_from_curr_user": prepaid_users_from_curr_user,
"last_drink": last_drink,
"avail_drink_types": ["Paulaner Spezi", "Mio Mate", "Club Mate", "Sonstiges"],
"avail_drink_types": most_used_drinks,
})
@app.get("/login", response_class=HTMLResponse)
@@ -128,7 +114,6 @@ def login_form(request: Request):
Returns:
TemplateResponse: The rendered login.html template with the request context.
"""
return templates.TemplateResponse("login.html", {"request": request})
@app.post("/set_money_postpaid")
@@ -151,10 +136,10 @@ def set_money_postpaid(request: Request, username = Form(...), money: float = Fo
if not user_authentik or ADMIN_GROUP not in user_authentik["groups"]:
raise HTTPException(status_code=403, detail="Nicht erlaubt")
user = get_postpaid_user_by_username(username)
user = db.models.get_postpaid_user_by_username(username)
requested_user_id = user["id"]
set_postpaid_user_money(requested_user_id, money*100)
db.models.set_postpaid_user_money(requested_user_id, money*100)
return RedirectResponse(url="/", status_code=303)
@app.post("/drink")
@@ -182,52 +167,93 @@ async def drink(request: Request):
if not user_db_id:
raise HTTPException(status_code=404, detail="User not found")
form = await request.form()
getraenk = str(form.get("getraenk"))
print(f"User {user_authentik['preferred_username']} requested drink: {getraenk}")
drink_postpaid_user(user_db_id, getraenk)
db.models.drink_postpaid_user(user_db_id)
return RedirectResponse(url="/", status_code=303)
@app.post("/payup")
def payup(request: Request, username: str = Form(...), money: float = Form(...)):
"""
Handles the payment process for a postpaid user.
This endpoint allows an authenticated admin user to record a payment for a specified user.
It validates the admin's permissions, checks the validity of the username and payment amount,
and updates the user's postpaid balance accordingly.
Args:
request (Request): The incoming HTTP request, containing session data.
username (str): The username of the user whose balance is to be updated (form data).
money (float): The amount of money to be paid (form data, must be between 0 and 1000).
Raises:
HTTPException: If the user is not authenticated as an admin (403).
HTTPException: If the specified user is not found (404).
HTTPException: If the payment amount is invalid (400).
HTTPException: If the current user is not found in the session (404).
Returns:
RedirectResponse: Redirects to the home page ("/") with a 303 status code upon success.
"""
user_auth = request.session.get("user_authentik")
if not user_auth or ADMIN_GROUP not in user_auth["groups"]:
raise HTTPException(status_code=403, detail="Not allowed")
user_db_id = get_postpaid_user_by_username(username)["id"]
user_db_id = db.models.get_postpaid_user_by_username(username)["id"]
if not user_db_id:
raise HTTPException(status_code=404, detail="User not found")
if money < 0 or money > 1000:
raise HTTPException(status_code=400, detail="Money must be between 0 and 1000")
curr_user_money = get_postpaid_user(user_db_id)["money"]
set_postpaid_user_money(user_db_id, curr_user_money + money*100)
current_user_db_id = request.session.get("user_db_id")
if not current_user_db_id:
raise HTTPException(status_code=404, detail="Current user not found")
current_user_money = get_postpaid_user(current_user_db_id)["money"]
set_postpaid_user_money(current_user_db_id, current_user_money - money*100)
db.models.payup_postpaid_user(current_user_db_id, user_db_id, int(money*100))
return RedirectResponse(url="/", status_code=303)
@app.post("/toggle_activated_user_postpaid")
def toggle_activated_user_postpaid(request: Request, username: str = Form(...)):
"""
Toggles the activation status of a postpaid user account.
Args:
request (Request): The incoming HTTP request, containing session information.
username (str): The username of the postpaid user to toggle, provided via form data.
Raises:
HTTPException: If the user is not authenticated as an admin (status code 403).
HTTPException: If the specified user is not found (status code 404).
Returns:
RedirectResponse: Redirects to the homepage ("/") with a 303 status code after toggling.
"""
user_auth = request.session.get("user_authentik")
if not user_auth or ADMIN_GROUP not in user_auth["groups"]:
raise HTTPException(status_code=403, detail="Not allowed")
user_db_id = get_postpaid_user_by_username(username)["id"]
user_db_id = db.models.get_postpaid_user_by_username(username)["id"]
if not user_db_id:
raise HTTPException(status_code=404, detail="User not found")
toggle_activate_postpaid_user(user_db_id)
db.models.toggle_activate_postpaid_user(user_db_id)
return RedirectResponse(url="/", status_code=303)
@app.post("/add_prepaid_user")
def add_prepaid_user(request: Request, username: str = Form(...), start_money: float = Form(...)):
"""
Handles the creation of a new prepaid user account.
This endpoint validates the current user's authentication and group membership,
checks for the existence of the username among prepaid and postpaid users,
validates the input parameters, creates the prepaid user, and deducts the starting
money from the current user's postpaid balance.
Args:
request (Request): The incoming HTTP request, containing session data.
username (str): The username for the new prepaid user (form field, required).
start_money (float): The initial balance for the new prepaid user (form field, required).
Raises:
HTTPException:
- 403 if the current user is not authorized.
- 404 if the current user is not found in the session.
- 400 if the username is empty, already exists, or does not meet length requirements.
- 400 if the start_money is not between 0 and 100.
Returns:
RedirectResponse: Redirects to the homepage ("/") with status code 303 upon successful creation.
"""
active_user_auth = request.session.get("user_authentik")
active_user_db_id = request.session.get("user_db_id")
if not active_user_auth or FS_GROUP not in active_user_auth["groups"]:
@@ -239,9 +265,9 @@ def add_prepaid_user(request: Request, username: str = Form(...), start_money: f
user_exists = False
try:
get_postpaid_user_by_username(username)
db.models.get_postpaid_user_by_username(username)
user_exists = True
get_prepaid_user_by_username(username)
db.models.get_prepaid_user_by_username(username)
user_exists = True
except HTTPException:
pass
@@ -254,15 +280,28 @@ def add_prepaid_user(request: Request, username: str = Form(...), start_money: f
if len(username) < 3 or len(username) > 20:
raise HTTPException(status_code=400, detail="Username must be between 3 and 20 characters")
create_prepaid_user(username, active_user_db_id, int(start_money*100))
db.models.create_prepaid_user(username, active_user_db_id, int(start_money*100))
prev_money = get_postpaid_user(active_user_db_id)["money"]
set_postpaid_user_money(active_user_db_id, prev_money - int(start_money*100))
prev_money = db.models.get_postpaid_user(active_user_db_id)["money"]
db.models.set_postpaid_user_money(active_user_db_id, prev_money - int(start_money*100))
return RedirectResponse(url="/", status_code=303)
@app.post("/drink_prepaid")
def drink_prepaid(request: Request):
"""
Handles a prepaid drink request for a user.
This function checks if the user is authenticated and has prepaid privileges.
If the user is not found in the session or does not have prepaid access, it raises an HTTPException.
If the user is valid and has prepaid access, it records the prepaid drink for the user in the database
and redirects to the home page.
Args:
request (Request): The incoming HTTP request containing session data.
Raises:
HTTPException: If the user is not found in the session or does not have prepaid privileges.
Returns:
RedirectResponse: Redirects the user to the home page with a 303 status code.
"""
user_db_id = request.session.get("user_db_id")
if not user_db_id:
raise HTTPException(status_code=404, detail="User not found")
@@ -272,25 +311,52 @@ def drink_prepaid(request: Request):
if not user_authentik["prepaid"]:
raise HTTPException(status_code=403, detail="Not allowed")
drink_prepaid_user(user_db_id)
db.models.drink_prepaid_user(user_db_id)
return RedirectResponse(url="/", status_code=303)
@app.post("/toggle_activated_user_prepaid")
def toggle_activated_user_prepaid(request: Request, username: str = Form(...)):
"""
Toggle the activation status of a prepaid user account.
This endpoint is restricted to users who are members of the ADMIN_GROUP.
It retrieves the user by username, toggles their activation status, and redirects to the homepage.
Args:
request (Request): The incoming HTTP request, containing session data.
username (str): The username of the prepaid user to toggle, provided via form data.
Raises:
HTTPException: If the user is not authenticated as an admin (403).
HTTPException: If the specified user is not found (404).
Returns:
RedirectResponse: Redirects to the homepage with a 303 status code after toggling.
"""
user_auth = request.session.get("user_authentik")
if not user_auth or ADMIN_GROUP not in user_auth["groups"]:
raise HTTPException(status_code=403, detail="Not allowed")
user_db_id = get_prepaid_user_by_username(username)["id"]
user_db_id = db.models.get_prepaid_user_by_username(username)["id"]
if not user_db_id:
raise HTTPException(status_code=404, detail="User not found")
toggle_activate_prepaid_user(user_db_id)
db.models.toggle_activate_prepaid_user(user_db_id)
return RedirectResponse(url="/", status_code=303)
@app.post("/add_money_prepaid_user")
def add_money_prepaid_user(request: Request, username: str = Form(...), money: float = Form(...)):
"""
Handles the transfer of a specified amount of money from the currently logged-in postpaid user to a prepaid user.
Args:
request (Request): The incoming HTTP request containing session data.
username (str): The username of the prepaid user to whom money will be added (provided via form data).
money (float): The amount of money to transfer (provided via form data, must be between 0 and 100).
Raises:
HTTPException:
- 403 if the current user is not authorized (not in the required group).
- 404 if the logged-in user or the prepaid user is not found.
- 400 if the money amount is not within the allowed range.
Returns:
RedirectResponse: Redirects the user to the homepage ("/") with a 303 status code after a successful transfer.
"""
curr_user_auth = request.session.get("user_authentik")
if not curr_user_auth or FS_GROUP not in curr_user_auth["groups"]:
raise HTTPException(status_code=403, detail="Not allowed")
@@ -298,7 +364,7 @@ def add_money_prepaid_user(request: Request, username: str = Form(...), money: f
if not curr_user_db_id:
raise HTTPException(status_code=404, detail="Logged In User not found")
prepaid_user_dict = get_prepaid_user_by_username(username)
prepaid_user_dict = db.models.get_prepaid_user_by_username(username)
prepaid_user_db_id = prepaid_user_dict["id"]
if not prepaid_user_db_id:
raise HTTPException(status_code=404, detail="Prepaid User not found")
@@ -306,36 +372,55 @@ def add_money_prepaid_user(request: Request, username: str = Form(...), money: f
if money < 0 or money > 100:
raise HTTPException(status_code=400, detail="Money must be between 0 and 100")
curr_user_money = get_postpaid_user(curr_user_db_id)["money"]
curr_user_money = db.models.get_postpaid_user(curr_user_db_id)["money"]
prepaid_user_money = prepaid_user_dict["money"]
set_postpaid_user_money(curr_user_db_id, curr_user_money - money*100)
set_prepaid_user_money(prepaid_user_db_id, prepaid_user_money + money*100, curr_user_db_id)
db.models.set_postpaid_user_money(curr_user_db_id, curr_user_money - money*100)
db.models.set_prepaid_user_money(prepaid_user_db_id, prepaid_user_money + money*100, curr_user_db_id)
return RedirectResponse(url="/", status_code=303)
@app.post("/del_prepaid_user")
def delete_prepaid_user(request: Request, username: str = Form(...)):
"""
Deletes a prepaid user from the system.
This endpoint allows an admin user to delete a prepaid user by their username.
It checks if the requester is part of the ADMIN_GROUP, retrieves the user to be deleted,
and removes them from the database. If the user is not found or the requester is not authorized,
an appropriate HTTPException is raised.
Args:
request (Request): The incoming HTTP request, containing session information.
username (str): The username of the prepaid user to delete, provided via form data.
Raises:
HTTPException: If the requester is not authorized (status code 403).
HTTPException: If the user to delete is not found (status code 404).
Returns:
RedirectResponse: Redirects to the homepage ("/") with status code 303 upon successful deletion.
"""
# check if user is in ADMIN_GROUP
user_auth = request.session.get("user_authentik")
if not user_auth or ADMIN_GROUP not in user_auth["groups"]:
raise HTTPException(status_code=403, detail="Nicht erlaubt")
user_to_del = get_prepaid_user_by_username(username)
user_to_del = db.models.get_prepaid_user_by_username(username)
if not user_to_del["id"]:
raise HTTPException(status_code=404, detail="User not found")
del_user_prepaid(user_to_del["id"])
db.models.del_user_prepaid(user_to_del["id"])
return RedirectResponse(url="/", status_code=303)
@app.get("/popup_getraenke")
async def popup_getraenke():
alle_getraenke = ["Wasser", "Cola", "Bier", "Mate", "Saft", "Tee", "Kaffee", "Limo"]
return JSONResponse(content={"getraenke": random.sample(alle_getraenke, 4)})
@app.post("/del_last_drink")
def del_last_drink(request: Request):
"""
Handles the deletion (reversion) of the last drink entry for the currently authenticated user.
Args:
request (Request): The incoming HTTP request containing session data.
Raises:
HTTPException: If the user is not found in the session.
Returns:
RedirectResponse: Redirects to the homepage ("/") after attempting to revert the last drink.
"""
user_db_id = request.session.get("user_db_id")
if not user_db_id:
raise HTTPException(status_code=404, detail="User not found")
@@ -343,18 +428,28 @@ def del_last_drink(request: Request):
if not user_authentik:
raise HTTPException(status_code=404, detail="User not found")
last_drink = get_last_drink(user_db_id, True, 60)
last_drink = db.models.get_last_drink(user_db_id, True, 60)
if not last_drink:
return RedirectResponse(url="/", status_code=303)
user_is_postpaid = get_is_postpaid(user_authentik)
revert_last_drink(user_db_id, user_is_postpaid, last_drink["id"])
db.models.revert_last_drink(user_db_id, user_is_postpaid, last_drink["id"])
return RedirectResponse(url="/", status_code=303)
@app.post("/update_drink_post")
def update_drink_post(request: Request, drink_type: str = Form(...)):
"""
Handles a POST request to update the type of the user's last drink.
Args:
request (Request): The incoming HTTP request containing session data.
drink_type (str, optional): The new type of drink to set, provided via form data.
Raises:
HTTPException: If the user is not found in the session or if the drink type is empty.
Returns:
RedirectResponse: Redirects to the home page after updating the drink type, or if no last drink is found.
"""
user_db_id = request.session.get("user_db_id")
if not user_db_id:
raise HTTPException(status_code=404, detail="User not found")
@@ -363,20 +458,53 @@ def update_drink_post(request: Request, drink_type: str = Form(...)):
if not user_authentik:
raise HTTPException(status_code=404, detail="User not found")
last_drink = get_last_drink(user_db_id, True, 60)
last_drink = db.models.get_last_drink(user_db_id, True, 60)
if not last_drink:
return RedirectResponse(url="/", status_code=303)
if not drink_type:
raise HTTPException(status_code=400, detail="Drink type is empty")
update_drink_type(user_db_id, get_is_postpaid(user_authentik), last_drink["id"], drink_type)
db.models.update_drink_type(user_db_id, get_is_postpaid(user_authentik), last_drink["id"], drink_type)
return RedirectResponse(url="/", status_code=303)
@app.get("/stats", response_class=HTMLResponse)
def stats(request: Request):
"""
Handles the statistics page request for authenticated admin users.
Args:
request (Request): The incoming HTTP request object containing session data.
Raises:
HTTPException: If the user is not authenticated as an admin (403).
HTTPException: If the user's database ID is not found in the session (404).
Returns:
TemplateResponse: Renders the "stats.html" template with user and drink type statistics.
"""
user_authentik = request.session.get("user_authentik")
if not user_authentik or ADMIN_GROUP not in user_authentik["groups"]:
raise HTTPException(status_code=403, detail="Not allowed")
user_db_id = request.session.get("user_db_id")
if not user_db_id:
raise HTTPException(status_code=404, detail="User not found")
drink_types = db.models.get_stats_drink_types()
def get_is_postpaid(user_authentik) -> bool:
return templates.TemplateResponse("stats.html", {
"request": request,
"user": user_authentik,
"user_db_id": user_db_id,
"stats_drink_types": drink_types,
})
def get_is_postpaid(user_authentik: dict) -> bool:
"""
Determine if a user is postpaid based on their authentication information.
Args:
user_authentik (dict): A dictionary containing user authentication data, expected to have a 'prepaid' key.
Returns:
bool: True if the user is postpaid, False if the user is prepaid. If the 'prepaid' key is missing, defaults to True (postpaid).
"""
try:
if user_authentik["prepaid"]:
user_is_postpaid = False

Binary file not shown.

After

Width:  |  Height:  |  Size: 394 KiB

BIN
static/drinks/sonstiges.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

View File

@@ -38,9 +38,71 @@ header {
background: #23272a;
color: #e0e0e0;
}
input, select, button {
input,
select,
button {
background: #23272a;
color: #e0e0e0;
border-color: #444;
}
}
/* Responsive Design */
@media (max-width: 600px) {
body {
max-width: 100vw;
padding: 0.5em;
}
header,
main {
padding: 0.5em;
margin-bottom: 0.5em;
}
h1 {
font-size: 1.3em;
text-align: center;
}
table {
font-size: 0.95em;
width: 100%;
display: block;
overflow-x: auto;
}
th,
td {
padding: 0.3em 0.5em !important;
word-break: break-word;
}
form {
flex-direction: column !important;
gap: 0.5em !important;
max-width: 100% !important;
}
input,
select,
button,
label {
width: 100% !important;
font-size: 1em !important;
box-sizing: border-box;
}
.github-icon-link {
top: 8px;
right: 8px;
width: 24px;
height: 24px;
}
img[alt="Logo"] {
height: 36px !important;
}
#popup {
max-width: 98vw !important;
left: 50% !important;
transform: translate(-50%, -25%) !important;
padding: 0.5em !important;
}
#popup-getraenke button {
font-size: 1em !important;
padding: 0.5em 0.7em !important;
}
}

View File

@@ -48,179 +48,6 @@
</header>
<main>
{% block content %}{% endblock %}
{% if user %}
{% if 'Getraenkeliste Postpaid' in user.groups %}
<p>Du bist Teil der Fachschaft Informatik.</p>
{% if prepaid_users_from_curr_user %}
<p>Liste deiner Prepaid-User:</p>
<table>
<thead>
<tr>
<th style="padding: 0.5em 1em">ID</th>
<th style="padding: 0.5em 1em">Username</th>
<th style="padding: 0.5em 1em">Key</th>
<th style="padding: 0.5em 1em">Postpaid_User ID</th>
<th style="padding: 0.5em 1em">Money (€)</th>
<th style="padding: 0.5em 1em">Activated</th>
<th style="padding: 0.5em 1em">last drink</th>
</tr>
</thead>
<tbody>
{% for prepaid_user_i in prepaid_users_from_curr_user %}
<tr{% if prepaid_user_i.money <= 0 %} style="background-color: rgba(179, 6, 44, 0.5)"{% endif %}>
<td style="padding: 0.5em 1em">{{ prepaid_user_i.id }}</td>
<td style="padding: 0.5em 1em">{{ prepaid_user_i.username }}</td>
<td style="padding: 0.5em 1em">{{ prepaid_user_i.user_key }}</td>
<td style="padding: 0.5em 1em">{{ prepaid_user_i.postpaid_user_id }}</td>
<td style="padding: 0.5em 1em">{{ prepaid_user_i.money / 100 }}</td>
<td style="padding: 0.5em 1em">{{ prepaid_user_i.activated }}</td>
<td style="padding: 0.5em 1em">{{ prepaid_user_i.last_drink }}</td>
</tr>
{% endfor %}
</tbody>
</table>
{% endif %}
<p>Füge Nutzer zur Prepaid Liste hinzu:</p>
<form method="post" action="/add_prepaid_user" style="display: flex; gap: 1em; align-items: center; margin-bottom: 1em; background: var(--hellgrau); padding: 1em; border-radius: 8px; box-shadow: 0 2px 6px rgba(0, 0, 0, 0.05); max-width: 600px;">
<label for="username" style="margin: 0 0.5em 0 0; font-weight: bold">Username:</label>
<input id="username" type="text" name="username" placeholder="Username" required style="padding: 0.5em; border: 1px solid #ccc; border-radius: 4px; width: 100%;" />
<label for="start_money" style="margin: 0 0.5em 0 0; font-weight: bold">Start Money (€):</label>
<input id="start_money" type="number" name="start_money" placeholder="Start Money" step="0.01" required style="padding: 0.5em; border: 1px solid #ccc; border-radius: 4px; width: 100px;" />
<button type="submit" style="padding: 0.5em 1em; background: rgb(0, 97, 143); color: #fff; border: none; border-radius: 4px; cursor: pointer;">Add User</button>
</form>
<p>Füge bestehendem Prepaid-User Geld hinzu:</p>
{% if db_users_prepaid %}
<form method="post" action="/add_money_prepaid_user" style="display: flex; gap: 1em; align-items: center; margin-bottom: 1em; background: var(--hellgrau); padding: 1em; border-radius: 8px; box-shadow: 0 2px 6px rgba(0, 0, 0, 0.05); max-width: 600px;">
<label for="addmoney-username" style="margin: 0 0.5em 0 0; font-weight: bold">Username:</label>
<select id="addmoney-username" name="username" required style="padding: 0.5em; border: 1px solid #ccc; border-radius: 4px;">
{% for db_user in db_users_prepaid %}
<option value="{{ db_user.username }}">{{ db_user.username }}</option>
{% endfor %}
</select>
<label for="addmoney-money" style="margin: 0 0.5em 0 0; font-weight: bold">Amount (€):</label>
<input id="addmoney-money" type="number" name="money" placeholder="Money" step="0.01" required style="padding: 0.5em; border: 1px solid #ccc; border-radius: 4px; width: 100px;" />
<button type="submit" style="padding: 0.5em 1em; background: rgb(0, 97, 143); color: #fff; border: none; border-radius: 4px; cursor: pointer;">Add Money</button>
</form>
{% else %}
<p>Es sind keine Prepaid-User vorhanden.</p>
{% endif %}
{% endif %}
{% if 'Getraenkeliste Verantwortliche' in user.groups %}
<h2>Admin Interface</h2>
<p>Ausgleichszahlung:</p>
<p>Der eingegebene Betrag wird vom aktuell eingeloggten Nutzer abgezogen und dem eingetragenem Nutzer gutgeschrieben.</p>
<form method="post" action="/payup" style="display: flex; gap: 1em; align-items: center; margin-bottom: 1em; background: var(--hellgrau); padding: 1em; border-radius: 8px; box-shadow: 0 2px 6px rgba(0, 0, 0, 0.05); max-width: 600px;">
<label for="payup-username" style="margin: 0 0.5em 0 0; font-weight: bold">Username:</label>
<select id="payup-username" name="username" required style="padding: 0.5em; border: 1px solid #ccc; border-radius: 4px;">
{% for db_user in users %}
<option value="{{ db_user.username }}">{{ db_user.username }}</option>
{% endfor %}
</select>
<label for="payup-money" style="margin: 0 0.5em 0 0; font-weight: bold">Amount (€):</label>
<input id="payup-money" type="number" name="money" placeholder="Money" step="0.01" required style="padding: 0.5em; border: 1px solid #ccc; border-radius: 4px; width: 100px;" />
<button type="submit" style="padding: 0.5em 1em; background: rgb(0, 97, 143); color: #fff; border: none; border-radius: 4px; cursor: pointer;">Pay Up</button>
</form>
<h3>Postpaid Liste</h3>
<p>Users in postpaid database:</p>
<table>
<thead>
<tr>
<th style="padding: 0.5em 1em">ID</th>
<th style="padding: 0.5em 1em">Username</th>
<th style="padding: 0.5em 1em">Money (€)</th>
<th style="padding: 0.5em 1em">Activated</th>
<th style="padding: 0.5em 1em">last drink</th>
</tr>
</thead>
<tbody>
{% for db_user_i in users %}
<tr{% if db_user_i.money <= -5000 %} style="background-color: rgba(179, 6, 44, 0.5)"{% endif %}>
<td style="padding: 0.5em 1em">{{ db_user_i.id }}</td>
<td style="padding: 0.5em 1em">{{ db_user_i.username }}</td>
<td style="padding: 0.5em 1em">{{ db_user_i.money / 100 }}</td>
<td style="padding: 0.5em 1em">{{ db_user_i.activated }}</td>
<td style="padding: 0.5em 1em">{{ db_user_i.last_drink }}</td>
</tr>
{% endfor %}
</tbody>
</table>
<p>(De-)Activate User</p>
<form method="post" action="/toggle_activated_user_postpaid" style="display: flex; gap: 1em; align-items: center; margin-bottom: 1em; background: var(--hellgrau); padding: 1em; border-radius: 8px; box-shadow: 0 2px 6px rgba(0, 0, 0, 0.05); max-width: 600px;">
<label for="activate-username" style="margin: 0 0.5em 0 0; font-weight: bold">Username:</label>
<select id="activate-username" name="username" required style="padding: 0.5em; border: 1px solid #ccc; border-radius: 4px;">
{% for db_user in users %}
<option value="{{ db_user.username }}">{{ db_user.username }}</option>
{% endfor %}
</select>
<button type="submit" style="padding: 0.5em 1em; background: rgb(0, 97, 143); color: #fff; border: none; border-radius: 4px; cursor: pointer;">Toggle Activation</button>
</form>
<p>Set user money:</p>
<form method="post" action="/set_money_postpaid" style="display: flex; gap: 1em; align-items: center; margin-bottom: 1em; background: var(--hellgrau); padding: 1em; border-radius: 8px; box-shadow: 0 2px 6px rgba(0, 0, 0, 0.05); max-width: 600px;">
<label for="setmoney-username" style="margin: 0 0.5em 0 0; font-weight: bold">Username:</label>
<select id="setmoney-username" name="username" required style="padding: 0.5em; border: 1px solid #ccc; border-radius: 4px;">
{% for db_user in users %}
<option value="{{ db_user.username }}">{{ db_user.username }}</option>
{% endfor %}
</select>
<label for="setmoney-money" style="margin: 0 0.5em 0 0; font-weight: bold">Amount (€):</label>
<input id="setmoney-money" type="number" name="money" placeholder="Money" step="0.01" required style="padding: 0.5em; border: 1px solid #ccc; border-radius: 4px; width: 100px;" />
<button type="submit" style="padding: 0.5em 1em; background: rgb(0, 97, 143); color: #fff; border: none; border-radius: 4px; cursor: pointer;">Set Money</button>
</form>
<h3>Prepaid Liste</h3>
<p>Users in prepaid database:</p>
{% if db_users_prepaid %}
<table>
<thead>
<tr>
<th style="padding: 0.5em 1em">ID</th>
<th style="padding: 0.5em 1em">Username</th>
<th style="padding: 0.5em 1em">Key</th>
<th style="padding: 0.5em 1em">Postpaid_User ID</th>
<th style="padding: 0.5em 1em">Money (€)</th>
<th style="padding: 0.5em 1em">Activated</th>
<th style="padding: 0.5em 1em">last drink</th>
</tr>
</thead>
<tbody>
{% for prepaid_user_i in db_users_prepaid %}
<tr{% if prepaid_user_i.money <= 0 %} style="background-color: rgba(179, 6, 44, 0.5)"{% endif %}>
<td style="padding: 0.5em 1em">{{ prepaid_user_i.id }}</td>
<td style="padding: 0.5em 1em">{{ prepaid_user_i.username }}</td>
<td style="padding: 0.5em 1em">{{ prepaid_user_i.user_key }}</td>
<td style="padding: 0.5em 1em">{{ prepaid_user_i.postpaid_user_id }}</td>
<td style="padding: 0.5em 1em">{{ prepaid_user_i.money / 100 }}</td>
<td style="padding: 0.5em 1em">{{ prepaid_user_i.activated }}</td>
<td style="padding: 0.5em 1em">{{ prepaid_user_i.last_drink }}</td>
</tr>
{% endfor %}
</tbody>
</table>
<p>(De-)Activate User</p>
<form method="post" action="/toggle_activated_user_prepaid" style="display: flex; gap: 1em; align-items: center; margin-bottom: 1em; background: var(--hellgrau); padding: 1em; border-radius: 8px; box-shadow: 0 2px 6px rgba(0, 0, 0, 0.05); max-width: 600px;">
<label for="activate-username" style="margin: 0 0.5em 0 0; font-weight: bold">Username:</label>
<select id="activate-username" name="username" required style="padding: 0.5em; border: 1px solid #ccc; border-radius: 4px;">
{% for db_user in db_users_prepaid %}
<option value="{{ db_user.username }}">{{ db_user.username }}</option>
{% endfor %}
</select>
<button type="submit" style="padding: 0.5em 1em; background: rgb(0, 97, 143); color: #fff; border: none; border-radius: 4px; cursor: pointer;">Toggle Activation</button>
</form>
<p>Delete User</p>
<form method="post" action="/del_prepaid_user" style="display: flex; gap: 1em; align-items: center; margin-bottom: 1em; background: var(--hellgrau); padding: 1em; border-radius: 8px; box-shadow: 0 2px 6px rgba(0, 0, 0, 0.05); max-width: 600px;">
<label for="del-username" style="margin: 0 0.5em 0 0; font-weight: bold">Username:</label>
<select id="del-username" name="username" required style="padding: 0.5em; border: 1px solid #ccc; border-radius: 4px;">
{% for db_user in db_users_prepaid %}
<option value="{{ db_user.username }}">{{ db_user.username }}</option>
{% endfor %}
</select>
<button type="submit" style="padding: 0.5em 1em; background: rgb(179, 6, 44); color: #fff; border: none; border-radius: 4px; cursor: pointer;">Delete User</button>
{% else %}
<tr>
<td colspan="7" style="text-align: center">No users in prepaid database</td>
</tr>
{% endif %}
{% endif %}
{% endif %}
</main>
</body>
</html>

View File

@@ -1,7 +1,6 @@
{% extends "base.html" %} {% block title %}Startseite{% endblock %} {% block
content %}
<h2>Willkommen, {{ user.name }}!</h2>
<p>Dies ist eine einfache geschützte Seite.</p>
<p><strong>Aktueller Kontostand:</strong></p>
{% if db_user.money > -5000 %}
<div
@@ -79,11 +78,14 @@ content %}
<div style="display: flex; flex-wrap: wrap; justify-content: center; gap: 1em; margin-top: 1em;">
{% for drink in avail_drink_types %}
<form method="post" action="/update_drink_post" style="display: inline-block;">
<input type="hidden" name="drink_type" value="{{ drink }}">
<input type="hidden" name="drink_type" value="{{ drink.drink_type }}">
<button type="submit"
style="display: flex; flex-direction: column; align-items: center; background-color: #00618F; color: #fff; border: none; border-radius: 8px; padding: 0.7em 1.2em; cursor: pointer; min-width: 120px;">
<img src="static/drinks/{{ drink|lower|replace(' ', '_') }}.png" alt="{{ drink }}" style="width:48px; height:48px; object-fit:contain; margin-bottom:0.5em;">
<span>{{ drink }}</span>
style="display: flex; flex-direction: column; align-items: center; background-color: var(--goetheblau); color: #fff; border: none; border-radius: 8px; padding: 0.7em 1.2em; cursor: pointer; min-width: 120px;">
<img src="/static/drinks/{{ drink.drink_type|lower|replace(' ', '_') }}.png" alt="{{ drink.drink_type }}" style="width:48px; height:48px; object-fit:contain; margin-bottom:0.5em;">
<span>{{ drink.drink_type }}</span>
{% if drink.count > 0 %}
<span style="font-size:0.9em; color:#eee;">x{{ drink.count }}</span>
{% endif %}
</button>
</form>
{% endfor %}
@@ -94,13 +96,24 @@ content %}
<strong>Letztes Getränk:</strong>
<div style="margin: 0.5em 0;">
Typ: {{ last_drink.drink_type }}<br>
Zeit: {{ last_drink.timestamp }}<br>
Zeit: <span class="local-timestamp" data-utc="{{ last_drink.timestamp }}">{{ last_drink.timestamp }}</span><br>
<script>
document.addEventListener("DOMContentLoaded", function() {
document.querySelectorAll('.local-timestamp').forEach(function(el) {
const utc = el.getAttribute('data-utc');
if (utc) {
const date = new Date(utc);
el.textContent = date.toLocaleString();
}
});
});
</script>
ID: {{ last_drink.id }}
</div>
<form method="post" action="/del_last_drink" style="display: inline;">
<input type="hidden" name="drink_id" value="{{ last_drink.drink_id }}">
<button type="submit"
style="background-color: #c0392b; color: #fff; border: none; border-radius: 6px; padding: 0.5em 1em; cursor: pointer;">
style="background-color: var(--emorot); color: #fff; border: none; border-radius: 6px; padding: 0.5em 1em; cursor: pointer;">
Getränk löschen
</button>
</form>
@@ -151,55 +164,170 @@ content %}
</div>
</div>
<script>
async function showDrinkPopup() {
const response = await fetch("/popup_getraenke");
const data = await response.json();
const container = document.getElementById("popup-getraenke");
container.innerHTML = "";
data.getraenke.forEach(name => {
const btn = document.createElement("button");
btn.textContent = name;
btn.style.margin = "0.5em";
btn.style.padding = "0.5em 1em";
btn.style.borderRadius = "6px";
btn.style.border = "1px solid #00618F";
btn.style.background = "#e0f4ff";
btn.style.cursor = "pointer";
btn.onclick = () => {
document.getElementById("getraenk-auswahl").value = name;
document.getElementById("popup").style.display = "none";
document.getElementById("drink-form").submit();
};
container.appendChild(btn);
});
document.getElementById("popup").style.display = "block";
let dauer = 3; // Sekunden
let verbleibend = dauer;
const bar = document.getElementById("progress-bar");
bar.style.width = "100%";
const interval = setInterval(() => {
verbleibend -= 0.1;
bar.style.width = (verbleibend / dauer * 100) + "%";
if (verbleibend <= 0) {
clearInterval(interval);
const auswahlInput = document.getElementById("getraenk-auswahl");
if (auswahlInput) {
auswahlInput.value = "";
}
document.getElementById("popup").style.display = "none";
const drinkForm = document.getElementById("drink-form");
if (drinkForm) {
drinkForm.submit();
}
}
}, 100);
console.log("Popup angezeigt und Fortschrittsbalken gestartet.");
}
</script>
{% if user %}
{% if 'Getraenkeliste Postpaid' in user.groups %}
<p>Du bist Teil der Fachschaft Informatik.</p>
{% if prepaid_users_from_curr_user %}
<p>Liste deiner Prepaid-User:</p>
<table>
<thead>
<tr>
<th style="padding: 0.5em 1em">Username</th>
<th style="padding: 0.5em 1em">Key</th>
<th style="padding: 0.5em 1em">Money (€)</th>
</tr>
</thead>
<tbody>
{% for prepaid_user_i in prepaid_users_from_curr_user %}
<tr{% if prepaid_user_i.money <= 0 %} style="background-color: rgba(179, 6, 44, 0.5)"{% endif %}>
<td style="padding: 0.5em 1em">{{ prepaid_user_i.username }}</td>
<td style="padding: 0.5em 1em">{{ prepaid_user_i.user_key }}</td>
<td style="padding: 0.5em 1em">{{ prepaid_user_i.money / 100 }}</td>
</tr>
{% endfor %}
</tbody>
</table>
{% endif %}
<p>Füge Nutzer zur Prepaid Liste hinzu:</p>
<form method="post" action="/add_prepaid_user" style="display: flex; gap: 1em; align-items: center; margin-bottom: 1em; background: var(--hellgrau); padding: 1em; border-radius: 8px; box-shadow: 0 2px 6px rgba(0, 0, 0, 0.05); max-width: 600px;">
<label for="username" style="margin: 0 0.5em 0 0; font-weight: bold">Username:</label>
<input id="username" type="text" name="username" placeholder="Username" required style="padding: 0.5em; border: 1px solid #ccc; border-radius: 4px; width: 100%;" />
<label for="start_money" style="margin: 0 0.5em 0 0; font-weight: bold">Start Money (€):</label>
<input id="start_money" type="number" name="start_money" placeholder="Start Money" step="0.01" required style="padding: 0.5em; border: 1px solid #ccc; border-radius: 4px; width: 100px;" />
<button type="submit" style="padding: 0.5em 1em; background: rgb(0, 97, 143); color: #fff; border: none; border-radius: 4px; cursor: pointer;">Add User</button>
</form>
<p>Füge bestehendem Prepaid-User Geld hinzu:</p>
{% if db_users_prepaid %}
<form method="post" action="/add_money_prepaid_user" style="display: flex; gap: 1em; align-items: center; margin-bottom: 1em; background: var(--hellgrau); padding: 1em; border-radius: 8px; box-shadow: 0 2px 6px rgba(0, 0, 0, 0.05); max-width: 600px;">
<label for="addmoney-username" style="margin: 0 0.5em 0 0; font-weight: bold">Username:</label>
<select id="addmoney-username" name="username" required style="padding: 0.5em; border: 1px solid #ccc; border-radius: 4px;">
{% for db_user in db_users_prepaid %}
<option value="{{ db_user.username }}">{{ db_user.username }}</option>
{% endfor %}
</select>
<label for="addmoney-money" style="margin: 0 0.5em 0 0; font-weight: bold">Amount (€):</label>
<input id="addmoney-money" type="number" name="money" placeholder="Money" step="0.01" required style="padding: 0.5em; border: 1px solid #ccc; border-radius: 4px; width: 100px;" />
<button type="submit" style="padding: 0.5em 1em; background: rgb(0, 97, 143); color: #fff; border: none; border-radius: 4px; cursor: pointer;">Add Money</button>
</form>
{% else %}
<p>Es sind keine Prepaid-User vorhanden.</p>
{% endif %}
{% endif %}
{% if 'Getraenkeliste Verantwortliche' in user.groups %}
<h2>Admin Interface</h2>
<p>Ausgleichszahlung:</p>
<p>Der eingegebene Betrag wird vom aktuell eingeloggten Nutzer abgezogen und dem eingetragenem Nutzer gutgeschrieben.</p>
<form method="post" action="/payup" style="display: flex; gap: 1em; align-items: center; margin-bottom: 1em; background: var(--hellgrau); padding: 1em; border-radius: 8px; box-shadow: 0 2px 6px rgba(0, 0, 0, 0.05); max-width: 600px;">
<label for="payup-username" style="margin: 0 0.5em 0 0; font-weight: bold">Username:</label>
<select id="payup-username" name="username" required style="padding: 0.5em; border: 1px solid #ccc; border-radius: 4px;">
{% for db_user in users %}
<option value="{{ db_user.username }}">{{ db_user.username }}</option>
{% endfor %}
</select>
<label for="payup-money" style="margin: 0 0.5em 0 0; font-weight: bold">Amount (€):</label>
<input id="payup-money" type="number" name="money" placeholder="Money" step="0.01" required style="padding: 0.5em; border: 1px solid #ccc; border-radius: 4px; width: 100px;" />
<button type="submit" style="padding: 0.5em 1em; background: rgb(0, 97, 143); color: #fff; border: none; border-radius: 4px; cursor: pointer;">Pay Up</button>
</form>
<h3>Postpaid Liste</h3>
<p>Users in postpaid database:</p>
<table>
<thead>
<tr>
<th style="padding: 0.5em 1em">ID</th>
<th style="padding: 0.5em 1em">Username</th>
<th style="padding: 0.5em 1em">Money (€)</th>
<th style="padding: 0.5em 1em">Activated</th>
<th style="padding: 0.5em 1em">last drink</th>
</tr>
</thead>
<tbody>
{% for db_user_i in users %}
<tr{% if db_user_i.money <= -5000 %} style="background-color: rgba(179, 6, 44, 0.5)"{% endif %}>
<td style="padding: 0.5em 1em">{{ db_user_i.id }}</td>
<td style="padding: 0.5em 1em">{{ db_user_i.username }}</td>
<td style="padding: 0.5em 1em">{{ db_user_i.money / 100 }}</td>
<td style="padding: 0.5em 1em">{{ db_user_i.activated }}</td>
<td style="padding: 0.5em 1em">{{ db_user_i.last_drink }}</td>
</tr>
{% endfor %}
</tbody>
</table>
<p>(De-)Activate User</p>
<form method="post" action="/toggle_activated_user_postpaid" style="display: flex; gap: 1em; align-items: center; margin-bottom: 1em; background: var(--hellgrau); padding: 1em; border-radius: 8px; box-shadow: 0 2px 6px rgba(0, 0, 0, 0.05); max-width: 600px;">
<label for="activate-username" style="margin: 0 0.5em 0 0; font-weight: bold">Username:</label>
<select id="activate-username" name="username" required style="padding: 0.5em; border: 1px solid #ccc; border-radius: 4px;">
{% for db_user in users %}
<option value="{{ db_user.username }}">{{ db_user.username }}</option>
{% endfor %}
</select>
<button type="submit" style="padding: 0.5em 1em; background: rgb(0, 97, 143); color: #fff; border: none; border-radius: 4px; cursor: pointer;">Toggle Activation</button>
</form>
<p>Set user money:</p>
<form method="post" action="/set_money_postpaid" style="display: flex; gap: 1em; align-items: center; margin-bottom: 1em; background: var(--hellgrau); padding: 1em; border-radius: 8px; box-shadow: 0 2px 6px rgba(0, 0, 0, 0.05); max-width: 600px;">
<label for="setmoney-username" style="margin: 0 0.5em 0 0; font-weight: bold">Username:</label>
<select id="setmoney-username" name="username" required style="padding: 0.5em; border: 1px solid #ccc; border-radius: 4px;">
{% for db_user in users %}
<option value="{{ db_user.username }}">{{ db_user.username }}</option>
{% endfor %}
</select>
<label for="setmoney-money" style="margin: 0 0.5em 0 0; font-weight: bold">Amount (€):</label>
<input id="setmoney-money" type="number" name="money" placeholder="Money" step="0.01" required style="padding: 0.5em; border: 1px solid #ccc; border-radius: 4px; width: 100px;" />
<button type="submit" style="padding: 0.5em 1em; background: rgb(0, 97, 143); color: #fff; border: none; border-radius: 4px; cursor: pointer;">Set Money</button>
</form>
<h3>Prepaid Liste</h3>
<p>Users in prepaid database:</p>
{% if db_users_prepaid %}
<table>
<thead>
<tr>
<th style="padding: 0.5em 1em">ID</th>
<th style="padding: 0.5em 1em">Username</th>
<th style="padding: 0.5em 1em">Key</th>
<th style="padding: 0.5em 1em">Postpaid_User ID</th>
<th style="padding: 0.5em 1em">Money (€)</th>
<th style="padding: 0.5em 1em">Activated</th>
<th style="padding: 0.5em 1em">last drink</th>
</tr>
</thead>
<tbody>
{% for prepaid_user_i in db_users_prepaid %}
<tr{% if prepaid_user_i.money <= 0 %} style="background-color: rgba(179, 6, 44, 0.5)"{% endif %}>
<td style="padding: 0.5em 1em">{{ prepaid_user_i.id }}</td>
<td style="padding: 0.5em 1em">{{ prepaid_user_i.username }}</td>
<td style="padding: 0.5em 1em">{{ prepaid_user_i.user_key }}</td>
<td style="padding: 0.5em 1em">{{ prepaid_user_i.postpaid_user_id }}</td>
<td style="padding: 0.5em 1em">{{ prepaid_user_i.money / 100 }}</td>
<td style="padding: 0.5em 1em">{{ prepaid_user_i.activated }}</td>
<td style="padding: 0.5em 1em">{{ prepaid_user_i.last_drink }}</td>
</tr>
{% endfor %}
</tbody>
</table>
<p>(De-)Activate User</p>
<form method="post" action="/toggle_activated_user_prepaid" style="display: flex; gap: 1em; align-items: center; margin-bottom: 1em; background: var(--hellgrau); padding: 1em; border-radius: 8px; box-shadow: 0 2px 6px rgba(0, 0, 0, 0.05); max-width: 600px;">
<label for="activate-username" style="margin: 0 0.5em 0 0; font-weight: bold">Username:</label>
<select id="activate-username" name="username" required style="padding: 0.5em; border: 1px solid #ccc; border-radius: 4px;">
{% for db_user in db_users_prepaid %}
<option value="{{ db_user.username }}">{{ db_user.username }}</option>
{% endfor %}
</select>
<button type="submit" style="padding: 0.5em 1em; background: rgb(0, 97, 143); color: #fff; border: none; border-radius: 4px; cursor: pointer;">Toggle Activation</button>
</form>
<p>Delete User</p>
<form method="post" action="/del_prepaid_user" style="display: flex; gap: 1em; align-items: center; margin-bottom: 1em; background: var(--hellgrau); padding: 1em; border-radius: 8px; box-shadow: 0 2px 6px rgba(0, 0, 0, 0.05); max-width: 600px;">
<label for="del-username" style="margin: 0 0.5em 0 0; font-weight: bold">Username:</label>
<select id="del-username" name="username" required style="padding: 0.5em; border: 1px solid #ccc; border-radius: 4px;">
{% for db_user in db_users_prepaid %}
<option value="{{ db_user.username }}">{{ db_user.username }}</option>
{% endfor %}
</select>
<button type="submit" style="padding: 0.5em 1em; background: rgb(179, 6, 44); color: #fff; border: none; border-radius: 4px; cursor: pointer;">Delete User</button>
{% else %}
<tr>
<td colspan="7" style="text-align: center">No users in prepaid database</td>
</tr>
{% endif %}
{% endif %}
{% endif %}
{% endblock %}

36
templates/stats.html Normal file
View File

@@ -0,0 +1,36 @@
{% extends "base.html" %}
{% block title %}Statistik{% endblock %}
{% block content %}
<!-- Chart.js einbinden -->
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<h3>Verteilung der Getränkesorten</h3>
<canvas id="pieChart" width="400" height="200"></canvas>
<script>
// Pie Chart für Getränketypen
const pieChartLabels = {{ stats_drink_types | map(attribute='drink_type') | list | tojson }};
const pieChartData = {{ stats_drink_types | map(attribute='count') | list | tojson }};
const pieCtx = document.getElementById('pieChart').getContext('2d');
new Chart(pieCtx, {
type: 'doughnut',
data: {
labels: pieChartLabels,
datasets: [{
data: pieChartData,
}]
},
options: {
responsive: true,
plugins: {
legend: { position: 'top' },
title: {
display: true,
text: 'Verteilung der Getränkesorten'
}
}
}
});
</script>
{% endblock %}