rename lan

This commit was merged in pull request #22.
This commit is contained in:
David Rodenkirchen
2025-07-26 14:16:09 +02:00
parent 6e598b577f
commit 29caadaca2
81 changed files with 234 additions and 234 deletions
@@ -0,0 +1,76 @@
import logging
from collections.abc import Callable
from datetime import datetime
from decimal import Decimal, ROUND_DOWN
from typing import Optional
from src.ezgg_lan_manager.services.DatabaseService import DatabaseService
from src.ezgg_lan_manager.types.Transaction import Transaction
logger = logging.getLogger(__name__.split(".")[-1])
class InsufficientFundsError(Exception):
pass
class AccountingService:
def __init__(self, db_service: DatabaseService) -> None:
self._db_service = db_service
self._update_hooks: set[Callable] = set()
def add_update_hook(self, update_hook: Callable) -> None:
""" Adds a function to this service, which is called whenever the account balance changes """
self._update_hooks.add(update_hook)
async def add_balance(self, user_id: int, balance_to_add: Decimal, reference: str) -> Decimal:
await self._db_service.add_transaction(Transaction(
user_id=user_id,
value=balance_to_add,
is_debit=False,
reference=reference,
transaction_date=datetime.now()
))
logger.debug(f"Added balance of {self.make_euro_string_from_decimal(balance_to_add)} to user with ID {user_id}")
for update_hook in self._update_hooks:
await update_hook()
return await self.get_balance(user_id)
async def remove_balance(self, user_id: int, balance_to_remove: Decimal, reference: str) -> Decimal:
current_balance = await self.get_balance(user_id)
if (current_balance - balance_to_remove) < 0:
raise InsufficientFundsError
await self._db_service.add_transaction(Transaction(
user_id=user_id,
value=balance_to_remove,
is_debit=True,
reference=reference,
transaction_date=datetime.now()
))
logger.debug(
f"Removed balance of {self.make_euro_string_from_decimal(balance_to_remove)} to user with ID {user_id}")
for update_hook in self._update_hooks:
await update_hook()
return await self.get_balance(user_id)
async def get_balance(self, user_id: int) -> Decimal:
balance_buffer = Decimal("0")
for transaction in await self._db_service.get_all_transactions_for_user(user_id):
if transaction.is_debit:
balance_buffer -= transaction.value
else:
balance_buffer += transaction.value
return balance_buffer
async def get_transaction_history(self, user_id: int) -> list[Transaction]:
return await self._db_service.get_all_transactions_for_user(user_id)
@staticmethod
def make_euro_string_from_decimal(euros: Optional[Decimal]) -> str:
"""
Internally, all money values are euros as decimal. Only when showing them to the user we generate a string.
"""
if euros is None:
return "0.00 €"
rounded_decimal = str(euros.quantize(Decimal(".01"), rounding=ROUND_DOWN))
return f"{rounded_decimal}"
@@ -0,0 +1,147 @@
import logging
from decimal import Decimal
from enum import Enum
from typing import Optional
from src.ezgg_lan_manager.services.AccountingService import AccountingService
from src.ezgg_lan_manager.services.DatabaseService import DatabaseService
from src.ezgg_lan_manager.services.UserService import UserService
from src.ezgg_lan_manager.services.ReceiptPrintingService import ReceiptPrintingService
from src.ezgg_lan_manager.types.CateringOrder import CateringOrder, CateringOrderStatus, CateringMenuItemsWithAmount
from src.ezgg_lan_manager.types.CateringMenuItem import CateringMenuItem, CateringMenuItemCategory
logger = logging.getLogger(__name__.split(".")[-1])
class CateringErrorType(Enum):
INCLUDES_DISABLED_ITEM = 0
INSUFFICIENT_FUNDS = 1
GENERIC = 99
class CateringError(Exception):
def __init__(self, message: str, error_type: CateringErrorType = CateringErrorType.GENERIC) -> None:
self.message = message
self.error_type = error_type
class CateringService:
def __init__(self, db_service: DatabaseService, accounting_service: AccountingService, user_service: UserService, receipt_printing_service: ReceiptPrintingService) -> None:
self._db_service = db_service
self._accounting_service = accounting_service
self._user_service = user_service
self._receipt_printing_service = receipt_printing_service
self.cached_cart: dict[int, list[CateringMenuItem]] = {}
# ORDERS
async def place_order(self, menu_items: CateringMenuItemsWithAmount, user_id: int,
is_delivery: bool = True) -> CateringOrder:
for menu_item in menu_items:
if menu_item.is_disabled:
raise CateringError("Order includes disabled items", CateringErrorType.INCLUDES_DISABLED_ITEM)
user = await self._user_service.get_user(user_id)
if not user:
raise CateringError("User does not exist")
total_price = sum([item.price * quantity for item, quantity in menu_items.items()], Decimal(0))
if await self._accounting_service.get_balance(user_id) < total_price:
raise CateringError("Insufficient funds", CateringErrorType.INSUFFICIENT_FUNDS)
order = await self._db_service.add_new_order(menu_items, user_id, is_delivery)
if order:
await self._accounting_service.remove_balance(user_id, total_price, f"CATERING - {order.order_id}")
logger.info(
f"User '{order.customer.user_name}' (ID:{order.customer.user_id}) ordered from catering for {self._accounting_service.make_euro_string_from_decimal(total_price)}")
await self._receipt_printing_service.print_order(user, order)
# await self.cancel_order(order) # ToDo: Check if commented out before commit. Un-comment to auto-cancel every placed order
return order
async def update_order_status(self, order_id: int, new_status: CateringOrderStatus) -> bool:
if new_status == CateringOrderStatus.CANCELED:
# Cancelled orders need to be refunded
raise CateringError("Orders cannot be canceled this way, use CateringService.cancel_order")
return await self._db_service.change_order_status(order_id, new_status)
async def get_orders(self) -> list[CateringOrder]:
return await self._db_service.get_orders()
async def get_orders_for_user(self, user_id: int) -> list[CateringOrder]:
return await self._db_service.get_orders(user_id=user_id)
async def get_orders_by_status(self, status: CateringOrderStatus) -> list[CateringOrder]:
return await self._db_service.get_orders(status=status)
async def cancel_order(self, order: CateringOrder) -> bool:
change_result = await self._db_service.change_order_status(order.order_id, CateringOrderStatus.CANCELED)
if change_result:
await self._accounting_service.add_balance(order.customer.user_id, order.price,
f"CATERING REFUND - {order.order_id}")
return True
return False
# MENU ITEMS
async def get_menu(self, category: Optional[CateringMenuItemCategory] = None) -> list[CateringMenuItem]:
items = await self._db_service.get_menu_items()
if not category:
return items
return list(filter(lambda item: item.category == category, items))
async def get_menu_item_by_id(self, menu_item_id: int) -> CateringMenuItem:
item = await self._db_service.get_menu_item(menu_item_id)
if not item:
raise CateringError("Menu item not found")
return item
async def add_menu_item(self, name: str, info: str, price: Decimal, category: CateringMenuItemCategory,
is_disabled: bool = False) -> CateringMenuItem:
if new_item := await self._db_service.add_menu_item(name, info, price, category, is_disabled):
return new_item
raise CateringError(f"Could not add item '{name}' to the menu.")
async def remove_menu_item(self, menu_item_id: int) -> bool:
return await self._db_service.delete_menu_item(menu_item_id)
async def change_menu_item(self, updated_item: CateringMenuItem) -> bool:
return await self._db_service.update_menu_item(updated_item)
async def disable_menu_item(self, menu_item_id: int) -> bool:
try:
item = await self.get_menu_item_by_id(menu_item_id)
except CateringError:
return False
item.is_disabled = True
return await self._db_service.update_menu_item(item)
async def enable_menu_item(self, menu_item_id: int) -> bool:
try:
item = await self.get_menu_item_by_id(menu_item_id)
except CateringError:
return False
item.is_disabled = False
return await self._db_service.update_menu_item(item)
async def disable_menu_items_by_category(self, category: CateringMenuItemCategory) -> bool:
items = await self.get_menu(category=category)
return all([self.disable_menu_item(item.item_id) for item in items])
async def enable_menu_items_by_category(self, category: CateringMenuItemCategory) -> bool:
items = await self.get_menu(category=category)
return all([self.enable_menu_item(item.item_id) for item in items])
# CART
def save_cart(self, user_id: Optional[int], cart: list[CateringMenuItem]) -> None:
if user_id:
self.cached_cart[user_id] = cart
def get_cart(self, user_id: Optional[int]) -> list[CateringMenuItem]:
if user_id is None:
return []
try:
return self.cached_cart[user_id]
except KeyError:
return []
@@ -0,0 +1,108 @@
import sys
from datetime import datetime
from decimal import Decimal
from pathlib import Path
import logging
import tomllib
from from_root import from_root
from src.ezgg_lan_manager.types.ConfigurationTypes import DatabaseConfiguration, MailingServiceConfiguration, LanInfo, \
SeatingConfiguration, TicketInfo, ReceiptPrintingConfiguration
logger = logging.getLogger(__name__.split(".")[-1])
class ConfigurationService:
def __init__(self, config_file_path: Path) -> None:
try:
with open(from_root("VERSION"), "r") as version_file:
self._version = version_file.read().strip()
except FileNotFoundError:
logger.warning("Could not find VERSION file, defaulting to '0.0.0'")
self._version = "0.0.0"
try:
with open(config_file_path, "rb") as config_file:
self._config = tomllib.load(config_file)
except FileNotFoundError:
logger.fatal(f"Could not find config file at \"{config_file_path}\", exiting...")
exit(1)
def get_database_configuration(self) -> DatabaseConfiguration:
try:
database_configuration = self._config["database"]
return DatabaseConfiguration(
db_user=database_configuration["db_user"],
db_password=database_configuration["db_password"],
db_host=database_configuration["db_host"],
db_port=database_configuration["db_port"],
db_name=database_configuration["db_name"]
)
except KeyError:
logger.fatal("Error loading DatabaseConfiguration, exiting...")
sys.exit(1)
def get_mailing_service_configuration(self) -> MailingServiceConfiguration:
try:
mailing_configuration = self._config["mailing"]
return MailingServiceConfiguration(
smtp_server=mailing_configuration["smtp_server"],
smtp_port=mailing_configuration["smtp_port"],
sender=mailing_configuration["sender"],
username=mailing_configuration["username"],
password=mailing_configuration["password"]
)
except KeyError:
logger.fatal("Error loading MailingServiceConfiguration, exiting...")
sys.exit(1)
def get_lan_info(self) -> LanInfo:
try:
lan_info = self._config["lan"]
return LanInfo(
name=lan_info["name"],
iteration=lan_info["iteration"],
date_from=datetime.strptime(lan_info["date_from"], "%Y-%m-%d %H:%M:%S"),
date_till=datetime.strptime(lan_info["date_till"], "%Y-%m-%d %H:%M:%S"),
organizer_mail=lan_info["organizer_mail"]
)
except KeyError:
logger.fatal("Error loading LAN Info, exiting...")
sys.exit(1)
def get_ticket_info(self) -> tuple[TicketInfo, ...]:
try:
return tuple([TicketInfo(
category=value,
total_tickets=self._config["tickets"][value]["total_tickets"],
price=Decimal(self._config["tickets"][value]["price"]),
description=self._config["tickets"][value]["description"],
additional_info=self._config["tickets"][value]["additional_info"],
is_default=self._config["tickets"][value]["is_default"]
) for value in self._config["tickets"]])
except KeyError as e:
logger.debug(e)
logger.fatal("Error loading seating configuration, exiting...")
sys.exit(1)
def get_receipt_printing_configuration(self) -> ReceiptPrintingConfiguration:
try:
receipt_printing_configuration = self._config["receipt_printing"]
return ReceiptPrintingConfiguration(
host=receipt_printing_configuration["host"],
port=receipt_printing_configuration["port"],
order_print_endpoint=receipt_printing_configuration["order_print_endpoint"],
password=receipt_printing_configuration["password"]
)
except KeyError:
logger.fatal("Error loading Receipt Printing Configuration, exiting...")
sys.exit(1)
@property
def APP_VERSION(self) -> str:
return self._version
@property
def DEV_MODE_ACTIVE(self) -> bool:
return self._config["misc"]["dev_mode_active"]
@@ -0,0 +1,789 @@
import logging
from datetime import date, datetime
from typing import Optional
from decimal import Decimal
import aiomysql
from src.ezgg_lan_manager.types.CateringOrder import CateringOrder
from src.ezgg_lan_manager.types.CateringMenuItem import CateringMenuItem, CateringMenuItemCategory
from src.ezgg_lan_manager.types.CateringOrder import CateringMenuItemsWithAmount, CateringOrderStatus
from src.ezgg_lan_manager.types.ConfigurationTypes import DatabaseConfiguration
from src.ezgg_lan_manager.types.News import News
from src.ezgg_lan_manager.types.Seat import Seat
from src.ezgg_lan_manager.types.Ticket import Ticket
from src.ezgg_lan_manager.types.Transaction import Transaction
from src.ezgg_lan_manager.types.User import User
logger = logging.getLogger(__name__.split(".")[-1])
class DuplicationError(Exception):
pass
class NoDatabaseConnectionError(Exception):
pass
class DatabaseService:
MAX_CONNECTION_RETRIES = 5
def __init__(self, database_config: DatabaseConfiguration) -> None:
self._database_config = database_config
self._connection_pool: Optional[aiomysql.Pool] = None
async def is_healthy(self) -> bool:
try:
async with self._connection_pool.acquire() as conn:
async with conn.cursor() as _:
return True
except aiomysql.OperationalError:
return False
except Exception as e:
logger.error(f"Failed to acquire a connection: {e}")
return False
async def init_db_pool(self) -> bool:
logger.info(
f"Connecting to database '{self._database_config.db_name}' on "
f"{self._database_config.db_user}@{self._database_config.db_host}:{self._database_config.db_port}"
)
try:
self._connection_pool = await aiomysql.create_pool(
host=self._database_config.db_host,
port=self._database_config.db_port,
user=self._database_config.db_user,
password=self._database_config.db_password,
db=self._database_config.db_name,
minsize=1,
maxsize=40
)
except aiomysql.OperationalError:
return False
return True
@staticmethod
def _map_db_result_to_user(data: tuple) -> User:
return User(
user_id=data[0],
user_name=data[1],
user_mail=data[2],
user_password=data[3],
user_first_name=data[4],
user_last_name=data[5],
user_birth_day=data[6],
is_active=bool(data[7]),
is_team_member=bool(data[8]),
is_admin=bool(data[9]),
created_at=data[10],
last_updated_at=data[11]
)
async def get_user_by_name(self, user_name: str) -> Optional[User]:
async with self._connection_pool.acquire() as conn:
async with conn.cursor(aiomysql.Cursor) as cursor:
await cursor.execute("SELECT * FROM users WHERE user_name=%s", (user_name,))
result = await cursor.fetchone()
if not result:
return
return self._map_db_result_to_user(result)
async def get_user_by_id(self, user_id: int) -> Optional[User]:
async with self._connection_pool.acquire() as conn:
async with conn.cursor(aiomysql.Cursor) as cursor:
await cursor.execute("SELECT * FROM users WHERE user_id=%s", (user_id,))
result = await cursor.fetchone()
if not result:
return
return self._map_db_result_to_user(result)
async def get_user_by_mail(self, user_mail: str) -> Optional[User]:
async with self._connection_pool.acquire() as conn:
async with conn.cursor(aiomysql.Cursor) as cursor:
await cursor.execute("SELECT * FROM users WHERE user_mail=%s", (user_mail.lower(),))
result = await cursor.fetchone()
if not result:
return
return self._map_db_result_to_user(result)
async def create_user(self, user_name: str, user_mail: str, password_hash: str) -> User:
async with self._connection_pool.acquire() as conn:
async with conn.cursor(aiomysql.Cursor) as cursor:
try:
await cursor.execute(
"INSERT INTO users (user_name, user_mail, user_password) "
"VALUES (%s, %s, %s)", (user_name, user_mail.lower(), password_hash)
)
await conn.commit()
except aiomysql.InterfaceError:
pool_init_result = await self.init_db_pool()
if not pool_init_result:
raise NoDatabaseConnectionError
return await self.create_user(user_name, user_mail, password_hash)
except aiomysql.IntegrityError as e:
logger.warning(f"Aborted duplication entry: {e}")
raise DuplicationError
return await self.get_user_by_name(user_name)
async def update_user(self, user: User) -> User:
async with self._connection_pool.acquire() as conn:
async with conn.cursor(aiomysql.Cursor) as cursor:
try:
await cursor.execute(
"UPDATE users SET user_name=%s, user_mail=%s, user_password=%s, user_first_name=%s, "
"user_last_name=%s, user_birth_date=%s, is_active=%s, is_team_member=%s, is_admin=%s "
"WHERE (user_id=%s)",
(user.user_name, user.user_mail.lower(), user.user_password,
user.user_first_name, user.user_last_name, user.user_birth_day,
user.is_active, user.is_team_member, user.is_admin,
user.user_id)
)
await conn.commit()
except aiomysql.InterfaceError:
pool_init_result = await self.init_db_pool()
if not pool_init_result:
raise NoDatabaseConnectionError
return await self.update_user(user)
except aiomysql.IntegrityError as e:
logger.warning(f"Aborted duplication entry: {e}")
raise DuplicationError
return user
async def add_transaction(self, transaction: Transaction) -> Optional[Transaction]:
async with self._connection_pool.acquire() as conn:
async with conn.cursor(aiomysql.Cursor) as cursor:
try:
await cursor.execute(
"INSERT INTO transactions (user_id, value, is_debit, transaction_date, transaction_reference) "
"VALUES (%s, %s, %s, %s, %s)",
(transaction.user_id, transaction.value, transaction.is_debit, transaction.transaction_date,
transaction.reference)
)
await conn.commit()
except aiomysql.InterfaceError:
pool_init_result = await self.init_db_pool()
if not pool_init_result:
raise NoDatabaseConnectionError
return await self.add_transaction(transaction)
except Exception as e:
logger.warning(f"Error adding Transaction: {e}")
return
return transaction
async def get_all_transactions_for_user(self, user_id: int) -> list[Transaction]:
async with self._connection_pool.acquire() as conn:
async with conn.cursor(aiomysql.Cursor) as cursor:
transactions = []
try:
await cursor.execute("SELECT * FROM transactions WHERE user_id=%s", (user_id,))
await conn.commit()
result = await cursor.fetchall()
except aiomysql.InterfaceError:
pool_init_result = await self.init_db_pool()
if not pool_init_result:
raise NoDatabaseConnectionError
return await self.get_all_transactions_for_user(user_id)
except aiomysql.Error as e:
logger.error(f"Error getting all transactions for user: {e}")
return []
for transaction_raw in result:
transactions.append(Transaction(
user_id=user_id,
value=Decimal(transaction_raw[2]),
is_debit=bool(transaction_raw[3]),
transaction_date=transaction_raw[4],
reference=transaction_raw[5]
))
return transactions
async def add_news(self, news: News) -> None:
async with self._connection_pool.acquire() as conn:
async with conn.cursor(aiomysql.Cursor) as cursor:
try:
await cursor.execute(
"INSERT INTO news (news_content, news_title, news_subtitle, news_author, news_date) "
"VALUES (%s, %s, %s, %s, %s)",
(news.content, news.title, news.subtitle, news.author.user_id, news.news_date)
)
await conn.commit()
except aiomysql.InterfaceError:
pool_init_result = await self.init_db_pool()
if not pool_init_result:
raise NoDatabaseConnectionError
return await self.add_news(news)
except Exception as e:
logger.warning(f"Error adding Transaction: {e}")
async def get_news(self, dt_start: date, dt_end: date) -> list[News]:
async with self._connection_pool.acquire() as conn:
async with conn.cursor(aiomysql.Cursor) as cursor:
results = []
try:
await cursor.execute(
"SELECT * FROM news INNER JOIN users ON news.news_author = users.user_id WHERE news_date"
" BETWEEN %s AND %s;",
(dt_start, dt_end))
await conn.commit()
except aiomysql.InterfaceError:
pool_init_result = await self.init_db_pool()
if not pool_init_result:
raise NoDatabaseConnectionError
return await self.get_news(dt_start, dt_end)
except Exception as e:
logger.warning(f"Error fetching news: {e}")
return []
for news_raw in await cursor.fetchall():
user = self._map_db_result_to_user(news_raw[6:])
results.append(News(
news_id=news_raw[0],
title=news_raw[2],
subtitle=news_raw[3],
author=user,
content=news_raw[1],
news_date=news_raw[5]
))
return results
async def update_news(self, news: News) -> None:
async with self._connection_pool.acquire() as conn:
async with conn.cursor(aiomysql.Cursor) as cursor:
try:
await cursor.execute(
"""
UPDATE news
SET news_content = %s,
news_title = %s,
news_subtitle = %s,
news_author = %s,
news_date = %s
WHERE news_id = %s
""",
(news.content, news.title, news.subtitle, news.author.user_id, news.news_date, news.news_id)
)
await conn.commit()
except aiomysql.InterfaceError:
pool_init_result = await self.init_db_pool()
if not pool_init_result:
raise NoDatabaseConnectionError
return await self.update_news(news)
except Exception as e:
logger.warning(f"Error updating news: {e}")
async def remove_news(self, news_id: int) -> None:
async with self._connection_pool.acquire() as conn:
async with conn.cursor(aiomysql.Cursor) as cursor:
try:
await cursor.execute(
"DELETE FROM news WHERE news_id = %s",
(news_id,)
)
await conn.commit()
except aiomysql.InterfaceError:
pool_init_result = await self.init_db_pool()
if not pool_init_result:
raise NoDatabaseConnectionError
return await self.remove_news(news_id)
except Exception as e:
logger.warning(f"Error removing news with ID {news_id}: {e}")
async def get_tickets(self) -> list[Ticket]:
async with self._connection_pool.acquire() as conn:
async with conn.cursor(aiomysql.Cursor) as cursor:
results = []
try:
await cursor.execute("SELECT * FROM tickets INNER JOIN users ON tickets.user = users.user_id;", ())
await conn.commit()
except aiomysql.InterfaceError:
pool_init_result = await self.init_db_pool()
if not pool_init_result:
raise NoDatabaseConnectionError
return await self.get_tickets()
except Exception as e:
logger.warning(f"Error fetching tickets: {e}")
return []
for ticket_raw in await cursor.fetchall():
user = self._map_db_result_to_user(ticket_raw[3:])
results.append(Ticket(
ticket_id=ticket_raw[0],
category=ticket_raw[1],
purchase_date=ticket_raw[3],
owner=user
))
return results
async def get_ticket_for_user(self, user_id: int) -> Optional[Ticket]:
async with self._connection_pool.acquire() as conn:
async with conn.cursor(aiomysql.Cursor) as cursor:
try:
await cursor.execute(
"SELECT * FROM tickets INNER JOIN users ON tickets.user = users.user_id WHERE user_id=%s;",
(user_id,))
await conn.commit()
except aiomysql.InterfaceError:
pool_init_result = await self.init_db_pool()
if not pool_init_result:
raise NoDatabaseConnectionError
return await self.get_ticket_for_user(user_id)
except Exception as e:
logger.warning(f"Error fetching ticket for user: {e}")
return
result = await cursor.fetchone()
if not result:
return
user = self._map_db_result_to_user(result[3:])
return Ticket(
ticket_id=result[0],
category=result[1],
purchase_date=result[3],
owner=user
)
async def generate_ticket_for_user(self, user_id: int, category: str) -> Optional[Ticket]:
async with self._connection_pool.acquire() as conn:
async with conn.cursor(aiomysql.Cursor) as cursor:
try:
await cursor.execute("INSERT INTO tickets (ticket_category, user) VALUES (%s, %s)",
(category, user_id))
await conn.commit()
except aiomysql.InterfaceError:
pool_init_result = await self.init_db_pool()
if not pool_init_result:
raise NoDatabaseConnectionError
return await self.generate_ticket_for_user(user_id, category)
except Exception as e:
logger.warning(f"Error generating ticket for user: {e}")
return
return await self.get_ticket_for_user(user_id)
async def change_ticket_owner(self, ticket_id: int, new_owner_id: int) -> bool:
async with self._connection_pool.acquire() as conn:
async with conn.cursor(aiomysql.Cursor) as cursor:
try:
await cursor.execute("UPDATE tickets SET user = %s WHERE ticket_id = %s;",
(new_owner_id, ticket_id))
affected_rows = cursor.rowcount
await conn.commit()
except aiomysql.InterfaceError:
pool_init_result = await self.init_db_pool()
if not pool_init_result:
raise NoDatabaseConnectionError
return await self.change_ticket_owner(ticket_id, new_owner_id)
except Exception as e:
logger.warning(f"Error transferring ticket to user: {e}")
return False
return affected_rows > 0
async def delete_ticket(self, ticket_id: int) -> bool:
async with self._connection_pool.acquire() as conn:
async with conn.cursor(aiomysql.Cursor) as cursor:
try:
await cursor.execute("DELETE FROM tickets WHERE ticket_id = %s;", (ticket_id,))
await conn.commit()
except aiomysql.InterfaceError:
pool_init_result = await self.init_db_pool()
if not pool_init_result:
raise NoDatabaseConnectionError
return await self.change_ticket_owner(ticket_id)
except Exception as e:
logger.warning(f"Error deleting ticket: {e}")
return False
return True
async def generate_fresh_seats_table(self, seats: list[tuple[str, str]]) -> None:
""" WARNING: THIS WILL DELETE ALL EXISTING DATA! DO NOT USE ON PRODUCTION DATABASE! """
async with self._connection_pool.acquire() as conn:
async with conn.cursor(aiomysql.Cursor) as cursor:
try:
await cursor.execute("TRUNCATE seats;")
for seat in seats:
await cursor.execute("INSERT INTO seats (seat_id, seat_category) VALUES (%s, %s);",
(seat[0], seat[1]))
await conn.commit()
except aiomysql.InterfaceError:
pool_init_result = await self.init_db_pool()
if not pool_init_result:
raise NoDatabaseConnectionError
return await self.generate_fresh_seats_table(seats)
except Exception as e:
logger.warning(f"Error generating fresh seats table: {e}")
return
async def get_seating_info(self) -> list[Seat]:
async with self._connection_pool.acquire() as conn:
async with conn.cursor(aiomysql.Cursor) as cursor:
results = []
try:
await cursor.execute(
"SELECT seats.*, users.* FROM seats LEFT JOIN users ON seats.user = users.user_id;")
await conn.commit()
except aiomysql.InterfaceError:
pool_init_result = await self.init_db_pool()
if not pool_init_result:
raise NoDatabaseConnectionError
return await self.get_seating_info()
except Exception as e:
logger.warning(f"Error getting seats table: {e}")
return results
for seat_raw in await cursor.fetchall():
if seat_raw[3] is None: # Empty seat
results.append(Seat(seat_raw[0], bool(seat_raw[1]), seat_raw[2], None))
else:
user = self._map_db_result_to_user(seat_raw[4:])
results.append(Seat(seat_raw[0], bool(seat_raw[1]), seat_raw[2], user))
return results
async def seat_user(self, seat_id: str, user_id: int) -> bool:
async with self._connection_pool.acquire() as conn:
async with conn.cursor(aiomysql.Cursor) as cursor:
try:
await cursor.execute("UPDATE seats SET user = %s WHERE seat_id = %s;", (user_id, seat_id))
affected_rows = cursor.rowcount
await conn.commit()
except aiomysql.InterfaceError:
pool_init_result = await self.init_db_pool()
if not pool_init_result:
raise NoDatabaseConnectionError
return await self.seat_user(seat_id, user_id)
except Exception as e:
logger.warning(f"Error seating user: {e}")
return False
return affected_rows > 0
async def get_menu_items(self) -> list[CateringMenuItem]:
async with self._connection_pool.acquire() as conn:
async with conn.cursor(aiomysql.Cursor) as cursor:
results = []
try:
await cursor.execute("SELECT * FROM catering_menu_items;")
await conn.commit()
except aiomysql.InterfaceError:
pool_init_result = await self.init_db_pool()
if not pool_init_result:
raise NoDatabaseConnectionError
return await self.get_menu_items()
except Exception as e:
logger.warning(f"Error fetching menu items: {e}")
return results
for menu_item_raw in await cursor.fetchall():
results.append(CateringMenuItem(
item_id=menu_item_raw[0],
name=menu_item_raw[1],
additional_info=menu_item_raw[2],
price=Decimal(menu_item_raw[3]),
category=CateringMenuItemCategory(menu_item_raw[4]),
is_disabled=bool(menu_item_raw[5])
))
return results
async def get_menu_item(self, menu_item_id) -> Optional[CateringMenuItem]:
async with self._connection_pool.acquire() as conn:
async with conn.cursor(aiomysql.Cursor) as cursor:
try:
await cursor.execute("SELECT * FROM catering_menu_items WHERE catering_menu_item_id = %s;",
(menu_item_id,))
await conn.commit()
except aiomysql.InterfaceError:
pool_init_result = await self.init_db_pool()
if not pool_init_result:
raise NoDatabaseConnectionError
return await self.get_menu_item(menu_item_id)
except Exception as e:
logger.warning(f"Error fetching menu items: {e}")
return
raw_data = await cursor.fetchone()
if raw_data is None:
return
return CateringMenuItem(
item_id=raw_data[0],
name=raw_data[1],
additional_info=raw_data[2],
price=Decimal(raw_data[3]),
category=CateringMenuItemCategory(raw_data[4]),
is_disabled=bool(raw_data[5])
)
async def add_menu_item(self, name: str, info: str, price: Decimal, category: CateringMenuItemCategory,
is_disabled: bool = False) -> Optional[CateringMenuItem]:
async with self._connection_pool.acquire() as conn:
async with conn.cursor(aiomysql.Cursor) as cursor:
try:
await cursor.execute(
"INSERT INTO catering_menu_items (name, additional_info, price, category, is_disabled) VALUES "
"(%s, %s, %s, %s, %s);",
(name, info, price, category.value, is_disabled)
)
await conn.commit()
except aiomysql.InterfaceError:
pool_init_result = await self.init_db_pool()
if not pool_init_result:
raise NoDatabaseConnectionError
return await self.add_menu_item(name, info, price, category, is_disabled)
except Exception as e:
logger.warning(f"Error adding menu item: {e}")
return
return CateringMenuItem(
item_id=cursor.lastrowid,
name=name,
additional_info=info,
price=price,
category=category,
is_disabled=is_disabled
)
async def delete_menu_item(self, menu_item_id: int) -> bool:
async with self._connection_pool.acquire() as conn:
async with conn.cursor(aiomysql.Cursor) as cursor:
try:
await cursor.execute("DELETE FROM catering_menu_items WHERE catering_menu_item_id = %s;",
(menu_item_id,))
await conn.commit()
except aiomysql.InterfaceError:
pool_init_result = await self.init_db_pool()
if not pool_init_result:
raise NoDatabaseConnectionError
return await self.delete_menu_item(menu_item_id)
except Exception as e:
logger.warning(f"Error deleting menu item: {e}")
return False
return cursor.affected_rows > 0
async def update_menu_item(self, updated_item: CateringMenuItem) -> bool:
async with self._connection_pool.acquire() as conn:
async with conn.cursor(aiomysql.Cursor) as cursor:
try:
await cursor.execute(
"UPDATE catering_menu_items SET name = %s, additional_info = %s, price = %s, category = %s, "
"is_disabled = %s WHERE catering_menu_item_id = %s;",
(updated_item.name, updated_item.additional_info, updated_item.price,
updated_item.category.value, updated_item.is_disabled, updated_item.item_id)
)
affected_rows = cursor.rowcount
await conn.commit()
except aiomysql.InterfaceError:
pool_init_result = await self.init_db_pool()
if not pool_init_result:
raise NoDatabaseConnectionError
return await self.update_menu_item(updated_item)
except Exception as e:
logger.warning(f"Error updating menu item: {e}")
return False
return affected_rows > 0
async def add_new_order(self, menu_items: CateringMenuItemsWithAmount, user_id: int, is_delivery: bool) -> Optional[CateringOrder]:
async with self._connection_pool.acquire() as conn:
async with conn.cursor(aiomysql.Cursor) as cursor:
now = datetime.now()
try:
await cursor.execute(
"INSERT INTO orders (status, user, is_delivery, order_date) VALUES (%s, %s, %s, %s);",
(CateringOrderStatus.RECEIVED.value, user_id, is_delivery, now)
)
order_id = cursor.lastrowid
for menu_item, quantity in menu_items.items():
await cursor.execute(
"INSERT INTO order_catering_menu_item (order_id, catering_menu_item_id, quantity) VALUES "
"(%s, %s, %s);",
(order_id, menu_item.item_id, quantity)
)
await conn.commit()
return CateringOrder(
order_id=order_id,
order_date=now,
status=CateringOrderStatus.RECEIVED,
items=menu_items,
customer=await self.get_user_by_id(user_id),
is_delivery=is_delivery
)
except aiomysql.InterfaceError:
pool_init_result = await self.init_db_pool()
if not pool_init_result:
raise NoDatabaseConnectionError
return await self.add_new_order(menu_items, user_id, is_delivery)
except Exception as e:
logger.warning(f"Error placing order: {e}")
return
async def change_order_status(self, order_id: int, status: CateringOrderStatus) -> bool:
async with self._connection_pool.acquire() as conn:
async with conn.cursor(aiomysql.Cursor) as cursor:
try:
await cursor.execute(
"UPDATE orders SET status = %s WHERE order_id = %s;",
(status.value, order_id)
)
affected_rows = cursor.rowcount
await conn.commit()
except aiomysql.InterfaceError:
pool_init_result = await self.init_db_pool()
if not pool_init_result:
raise NoDatabaseConnectionError
return await self.change_order_status(order_id, status)
except Exception as e:
logger.warning(f"Error updating menu item: {e}")
return False
return affected_rows > 0
async def get_orders(self, user_id: Optional[int] = None, status: Optional[CateringOrderStatus] = None) -> list[CateringOrder]:
async with self._connection_pool.acquire() as conn:
async with conn.cursor(aiomysql.Cursor) as cursor:
fetched_orders = []
query = "SELECT * FROM orders LEFT JOIN users ON orders.user = users.user_id"
if user_id is not None and status is None:
query += f" WHERE user = {user_id};"
elif status is not None and user_id is None:
query += f" WHERE status = '{status.value}';"
elif status is not None and user_id is not None:
query += f" WHERE user = {user_id} AND status = '{status.value}';"
else:
query += ";"
try:
await cursor.execute(query)
await conn.commit()
except aiomysql.InterfaceError:
pool_init_result = await self.init_db_pool()
if not pool_init_result:
raise NoDatabaseConnectionError
return await self.get_orders(user_id, status)
except Exception as e:
logger.warning(f"Error getting orders: {e}")
return fetched_orders
for raw_order in await cursor.fetchall():
fetched_orders.append(
CateringOrder(
order_id=raw_order[0],
status=CateringOrderStatus(raw_order[1]),
customer=self._map_db_result_to_user(raw_order[5:]),
items=await self.get_menu_items_for_order(raw_order[0]),
is_delivery=bool(raw_order[4]),
order_date=raw_order[3],
)
)
return fetched_orders
async def get_menu_items_for_order(self, order_id: int) -> CateringMenuItemsWithAmount:
async with self._connection_pool.acquire() as conn:
async with conn.cursor(aiomysql.Cursor) as cursor:
result = {}
try:
await cursor.execute(
"SELECT * FROM order_catering_menu_item "
"LEFT JOIN catering_menu_items ON order_catering_menu_item.catering_menu_item_id = catering_menu_items.catering_menu_item_id "
"WHERE order_id = %s;",
(order_id,)
)
await conn.commit()
except aiomysql.InterfaceError:
pool_init_result = await self.init_db_pool()
if not pool_init_result:
raise NoDatabaseConnectionError
return await self.get_menu_items_for_order(order_id)
except Exception as e:
logger.warning(f"Error getting order items: {e}")
return result
for order_catering_menu_item_raw in await cursor.fetchall():
result[CateringMenuItem(
item_id=order_catering_menu_item_raw[1],
name=order_catering_menu_item_raw[4],
additional_info=order_catering_menu_item_raw[5],
price=Decimal(order_catering_menu_item_raw[6]),
category=CateringMenuItemCategory(order_catering_menu_item_raw[7]),
is_disabled=bool(order_catering_menu_item_raw[8])
)] = order_catering_menu_item_raw[2]
return result
async def set_user_profile_picture(self, user_id: int, picture_data: bytes) -> None:
async with self._connection_pool.acquire() as conn:
async with conn.cursor(aiomysql.Cursor) as cursor:
try:
await cursor.execute(
"INSERT INTO user_profile_picture (user_id, picture) VALUES (%s, %s) ON DUPLICATE KEY UPDATE picture = VALUES(picture)",
(user_id, picture_data)
)
await conn.commit()
except aiomysql.InterfaceError:
pool_init_result = await self.init_db_pool()
if not pool_init_result:
raise NoDatabaseConnectionError
return await self.set_user_profile_picture(user_id, picture_data)
except Exception as e:
logger.warning(f"Error setting user profile picture: {e}")
async def get_user_profile_picture(self, user_id: int) -> Optional[bytes]:
async with self._connection_pool.acquire() as conn:
async with conn.cursor(aiomysql.Cursor) as cursor:
try:
await cursor.execute("SELECT (picture) FROM user_profile_picture WHERE user_id = %s", (user_id,))
await conn.commit()
r = await cursor.fetchone()
if r is None:
return
return r[0]
except aiomysql.InterfaceError:
pool_init_result = await self.init_db_pool()
if not pool_init_result:
raise NoDatabaseConnectionError
return await self.get_user_profile_picture(user_id)
except Exception as e:
logger.warning(f"Error setting user profile picture: {e}")
return None
async def get_all_users(self) -> list[User]:
async with self._connection_pool.acquire() as conn:
async with conn.cursor(aiomysql.Cursor) as cursor:
results = []
try:
await cursor.execute("SELECT * FROM users;")
await conn.commit()
except aiomysql.InterfaceError:
pool_init_result = await self.init_db_pool()
if not pool_init_result:
raise NoDatabaseConnectionError
return await self.get_all_users()
except Exception as e:
logger.warning(f"Error getting all users: {e}")
return results
for user_raw in await cursor.fetchall():
results.append(self._map_db_result_to_user(user_raw))
return results
async def remove_profile_picture(self, user_id: int):
async with self._connection_pool.acquire() as conn:
async with conn.cursor(aiomysql.Cursor) as cursor:
try:
await cursor.execute(
"DELETE FROM user_profile_picture WHERE user_id = %s",
user_id
)
await conn.commit()
except aiomysql.InterfaceError:
pool_init_result = await self.init_db_pool()
if not pool_init_result:
raise NoDatabaseConnectionError
return await self.remove_profile_picture(user_id)
except Exception as e:
logger.warning(f"Error deleting user profile picture: {e}")
@@ -0,0 +1,25 @@
import secrets
from typing import Optional
from rio import UserSettings
from src.ezgg_lan_manager.types.SessionStorage import SessionStorage
class LocalData(UserSettings):
stored_session_token: Optional[str] = None
class LocalDataService:
def __init__(self) -> None:
self._session: dict[str, SessionStorage] = {}
def verify_token(self, token: str) -> Optional[SessionStorage]:
return self._session.get(token)
def set_session(self, session: SessionStorage) -> str:
key = secrets.token_hex(32)
self._session[key] = session
return key
def del_session(self, token: str) -> None:
self._session.pop(token, None)
@@ -0,0 +1,37 @@
import logging
from email.message import EmailMessage
from asyncio import sleep
import aiosmtplib
from src.ezgg_lan_manager.services.ConfigurationService import ConfigurationService
logger = logging.getLogger(__name__.split(".")[-1])
class MailingService:
def __init__(self, configuration_service: ConfigurationService):
self._configuration_service = configuration_service
self._config = self._configuration_service.get_mailing_service_configuration()
async def send_email(self, subject: str, body: str, receiver: str) -> None:
if self._configuration_service.DEV_MODE_ACTIVE:
logger.info(f"Skipped sending mail to {receiver} because demo mode is active.")
await sleep(1)
return
try:
message = EmailMessage()
message["From"] = self._config.sender
message["To"] = receiver
message["Subject"] = subject
message.set_content(body)
await aiosmtplib.send(
message,
hostname=self._config.smtp_server,
port=self._config.smtp_port,
username=self._config.username,
password=self._config.password
)
except Exception as e:
logger.error(f"Failed to send email: {e}")
@@ -0,0 +1,39 @@
import logging
from datetime import date
from typing import Optional
from src.ezgg_lan_manager.services.DatabaseService import DatabaseService
from src.ezgg_lan_manager.types.News import News
logger = logging.getLogger(__name__.split(".")[-1])
class NewsService:
def __init__(self, db_service: DatabaseService) -> None:
self._db_service = db_service
async def add_news(self, news: News) -> None:
if news.news_id is not None:
logger.warning("Can not add news with ID, ignoring...")
return
await self._db_service.add_news(news)
async def get_news(self, dt_start: Optional[date] = None, dt_end: Optional[date] = None, newest_first: bool = True) -> list[News]:
if not dt_end:
dt_end = date.today()
if not dt_start:
dt_start = date(1900, 1, 1)
fetched_news = await self._db_service.get_news(dt_start, dt_end)
return sorted(fetched_news, key=lambda news: news.news_date, reverse=newest_first)
async def update_news(self, news: News) -> None:
return await self._db_service.update_news(news)
async def delete_news(self, news_id: int) -> None:
return await self._db_service.remove_news(news_id)
async def get_latest_news(self) -> Optional[News]:
try:
all_news = await self.get_news(None, date.today())
return all_news[0]
except IndexError:
logger.debug("There are no news to fetch")
@@ -0,0 +1,48 @@
import logging
import requests
from src.ezgg_lan_manager.services.SeatingService import SeatingService
from src.ezgg_lan_manager.types.CateringOrder import CateringOrder
from src.ezgg_lan_manager.types.ConfigurationTypes import ReceiptPrintingConfiguration
from src.ezgg_lan_manager.types.User import User
logger = logging.getLogger(__name__.split(".")[-1])
logging.getLogger("urllib3").setLevel(logging.FATAL) # Disable logging for urllib3
class ReceiptPrintingService:
def __init__(self, seating_service: SeatingService, config: ReceiptPrintingConfiguration, dev_mode_enabled: bool) -> None:
self._seating_service = seating_service
self._config = config
self._dev_mode_enabled = dev_mode_enabled
async def print_order(self, user: User, order: CateringOrder) -> None:
seat_id = await self._seating_service.get_user_seat(user.user_id)
if not seat_id:
seat_id = " - "
menu_items_payload = []
for item, amount in order.items.items():
menu_items_payload.append({
"menu_item_name": item.name,
"amount": amount
})
payload = {
"order_id": str(order.order_id),
"order_date": order.order_date.strftime('%Y-%m-%dT%H:%M:%S.%f')[:-3] + 'Z',
"customer_name": user.user_name,
"seat_id": seat_id,
"items": menu_items_payload
}
try:
requests.post(
f"http://{self._config.host}:{self._config.port}/{self._config.order_print_endpoint}",
json=payload,
headers={"x-password": self._config.password}
)
except Exception as e:
if self._dev_mode_enabled:
logger.info("An error occurred trying to print a receipt:", e)
return
logger.error("An error occurred trying to print a receipt:", e)
@@ -0,0 +1,62 @@
import logging
from typing import Optional
from src.ezgg_lan_manager.services.DatabaseService import DatabaseService
from src.ezgg_lan_manager.services.TicketingService import TicketingService
from src.ezgg_lan_manager.types.ConfigurationTypes import LanInfo, SeatingConfiguration
from src.ezgg_lan_manager.types.Seat import Seat
logger = logging.getLogger(__name__.split(".")[-1])
class NoTicketError(Exception):
pass
class SeatNotFoundError(Exception):
pass
class WrongCategoryError(Exception):
pass
class SeatAlreadyTakenError(Exception):
pass
class SeatingService:
def __init__(self, lan_info: LanInfo, db_service: DatabaseService, ticketing_service: TicketingService) -> None:
self._lan_info = lan_info
self._db_service = db_service
self._ticketing_service = ticketing_service
async def get_seating(self) -> list[Seat]:
return await self._db_service.get_seating_info()
async def get_seat(self, seat_id: str, cached_data: Optional[list[Seat]] = None) -> Optional[Seat]:
all_seats = await self.get_seating() if not cached_data else cached_data
for seat in all_seats:
if seat.seat_id == seat_id:
return seat
async def get_user_seat(self, user_id: int) -> Optional[Seat]:
all_seats = await self.get_seating()
for seat in all_seats:
if seat.user and seat.user.user_id == user_id:
return seat
async def seat_user(self, user_id: int, seat_id: str) -> None:
user_ticket = await self._ticketing_service.get_user_ticket(user_id)
if not user_ticket:
raise NoTicketError
seat = await self.get_seat(seat_id)
if not seat:
raise SeatNotFoundError
if seat.category != user_ticket.category:
raise WrongCategoryError
if seat.user is not None:
raise SeatAlreadyTakenError
await self._db_service.seat_user(seat_id, user_id)
# ToDo: Make function that creates database table `seats` from config
@@ -0,0 +1,93 @@
import logging
from typing import Optional
from src.ezgg_lan_manager.services.AccountingService import AccountingService, InsufficientFundsError
from src.ezgg_lan_manager.services.DatabaseService import DatabaseService
from src.ezgg_lan_manager.types.ConfigurationTypes import TicketInfo
from src.ezgg_lan_manager.types.Ticket import Ticket
logger = logging.getLogger(__name__.split(".")[-1])
class TicketNotAvailableError(Exception):
def __init__(self, category: str):
self.category = category
class UserAlreadyHasTicketError(Exception):
pass
class TicketingService:
def __init__(self, ticket_infos: tuple[TicketInfo, ...], db_service: DatabaseService,
accounting_service: AccountingService) -> None:
self._ticket_infos = ticket_infos
self._db_service = db_service
self._accounting_service = accounting_service
def get_ticket_info_by_category(self, category: str) -> Optional[TicketInfo]:
return next(filter(lambda t: t.category == category, self._ticket_infos), None)
def get_total_tickets(self) -> int:
return sum([t_i.total_tickets for t_i in self._ticket_infos])
async def get_available_tickets_for_category(self, category: str) -> int:
ticket_info = self.get_ticket_info_by_category(category)
if not ticket_info or ticket_info.total_tickets < 1:
return 0
result = ticket_info.total_tickets
all_tickets = await self._db_service.get_tickets()
for ticket in all_tickets:
if ticket.category == category:
result -= 1
return result
async def purchase_ticket(self, user_id: int, category: str) -> Ticket:
all_categories = [t_i.category for t_i in self._ticket_infos]
if category not in all_categories or (await self.get_available_tickets_for_category(category)) < 1:
raise TicketNotAvailableError(category)
user_balance = await self._accounting_service.get_balance(user_id)
ticket_info = self.get_ticket_info_by_category(category)
if not ticket_info:
raise TicketNotAvailableError(category)
if ticket_info.price > user_balance:
raise InsufficientFundsError
if await self.get_user_ticket(user_id):
raise UserAlreadyHasTicketError
if new_ticket := await self._db_service.generate_ticket_for_user(user_id, category):
await self._accounting_service.remove_balance(
user_id,
ticket_info.price,
f"TICKET {new_ticket.ticket_id}"
)
logger.debug(f"User {user_id} purchased ticket {new_ticket.ticket_id}")
return new_ticket
raise RuntimeError("An unknown error occurred while purchasing ticket")
async def refund_ticket(self, user_id: int) -> bool:
user_ticket = await self.get_user_ticket(user_id)
if not user_ticket:
return False
ticket_info = self.get_ticket_info_by_category(user_ticket.category)
if await self._db_service.delete_ticket(user_ticket.ticket_id):
await self._accounting_service.add_balance(user_id, ticket_info.price,
f"TICKET REFUND {user_ticket.ticket_id}")
logger.debug(f"User {user_id} refunded ticket {user_ticket.ticket_id}")
return True
return False
async def transfer_ticket(self, ticket_id: int, user_id: int) -> bool:
return await self._db_service.change_ticket_owner(ticket_id, user_id)
async def get_user_ticket(self, user_id: int) -> Optional[Ticket]:
return await self._db_service.get_ticket_for_user(user_id)
@@ -0,0 +1,70 @@
from hashlib import sha256
from typing import Union, Optional
from string import ascii_letters, digits
from src.ezgg_lan_manager.services.DatabaseService import DatabaseService
from src.ezgg_lan_manager.types.User import User
class NameNotAllowedError(Exception):
def __init__(self, disallowed_char: str) -> None:
self.disallowed_char = disallowed_char
class UserService:
ALLOWED_USER_NAME_SYMBOLS = ascii_letters + digits + "!#$%&*+,-./:;<=>?[]^_{|}~"
MAX_USERNAME_LENGTH = 14
def __init__(self, db_service: DatabaseService) -> None:
self._db_service = db_service
async def get_all_users(self) -> list[User]:
return await self._db_service.get_all_users()
async def get_user(self, accessor: Optional[Union[str, int]]) -> Optional[User]:
if accessor is None:
return
if isinstance(accessor, int):
return await self._db_service.get_user_by_id(accessor)
accessor = accessor.lower()
if "@" in accessor:
return await self._db_service.get_user_by_mail(accessor)
return await self._db_service.get_user_by_name(accessor)
async def set_profile_picture(self, user_id: int, picture: bytes) -> None:
await self._db_service.set_user_profile_picture(user_id, picture)
async def remove_profile_picture(self, user_id: int) -> None:
await self._db_service.remove_profile_picture(user_id)
async def get_profile_picture(self, user_id: int) -> bytes:
return await self._db_service.get_user_profile_picture(user_id)
async def create_user(self, user_name: str, user_mail: str, password_clear_text: str) -> User:
disallowed_char = self._check_for_disallowed_char(user_name)
if disallowed_char:
raise NameNotAllowedError(disallowed_char)
user_name = user_name.lower()
hashed_pw = sha256(password_clear_text.encode(encoding="utf-8")).hexdigest()
created_user = await self._db_service.create_user(user_name, user_mail, hashed_pw)
return created_user
async def update_user(self, user: User) -> User:
disallowed_char = self._check_for_disallowed_char(user.user_name)
if disallowed_char:
raise NameNotAllowedError(disallowed_char)
user.user_name = user.user_name.lower()
return await self._db_service.update_user(user)
async def is_login_valid(self, user_name_or_mail: str, password_clear_text: str) -> bool:
user = await self.get_user(user_name_or_mail)
if not user:
return False
return user.user_password == sha256(password_clear_text.encode(encoding="utf-8")).hexdigest()
def _check_for_disallowed_char(self, name: str) -> Optional[str]:
for c in name:
if c not in self.ALLOWED_USER_NAME_SYMBOLS:
return c