11 Commits

Author SHA1 Message Date
David Rodenkirchen d5cd05c0e5 release 0.3.7 2026-04-16 08:51:21 +02:00
David Rodenkirchen b8c1df5ff8 Disable commercial PayPal charging 2026-04-16 08:50:11 +02:00
dusker 8877de2cef Add EPC QR code to make bank transactions easier (#61)
See https://de.wikipedia.org/wiki/EPC-QR-Code#EPC-QR-Code_f%C3%BCr_%C3%9Cberweisung_erstellen for more information about the EPC coding

Co-authored-by: dusker <dusker@gmx.de>
Reviewed-on: #61
Co-authored-by: dusker <jens.graef+ezgg@posteo.de>
Co-committed-by: dusker <jens.graef+ezgg@posteo.de>
2026-04-16 06:48:46 +00:00
David Rodenkirchen bd5c142bcf Fix logout not redirecting properly 2026-04-16 07:32:35 +02:00
David Rodenkirchen e0ed3c7059 update sanitized backup 2026-04-16 07:07:00 +02:00
dusker a53e7100da Fix mariadb health check by adding the root password 2026-04-03 22:09:39 +02:00
David Rodenkirchen 2902c6a58c add sanitized production export 2026-02-24 00:54:24 +01:00
David Rodenkirchen 4541d3763f Hotfix: Remove Session override 2026-02-24 00:33:40 +01:00
David Rodenkirchen ce45c389ef fix login not working after registration 2026-02-23 23:50:13 +01:00
David Rodenkirchen edf1d70b54 Bump to Version 0.3.5 (Sessioning Rework / Catering UI Improvement) 2026-02-23 21:46:47 +01:00
David Rodenkirchen 8b02390bee Make disabled catering items clearer 2026-02-23 21:46:06 +01:00
13 changed files with 1033 additions and 122 deletions
+1 -1
View File
@@ -1 +1 @@
0.3.4
0.3.7
+2 -1
View File
@@ -21,7 +21,7 @@ services:
MARIADB_USER: ezgg_lan_manager
MARIADB_PASSWORD: Alkohol1
healthcheck:
test: ["CMD", "mariadb-admin", "ping", "-h", "localhost"]
test: ["CMD", "mariadb-admin", "ping", "-h", "localhost", "-pAlkohol1"]
interval: 5s
timeout: 3s
retries: 5
@@ -30,6 +30,7 @@ services:
volumes:
- database:/var/lib/mysql
- ./sql/create_database.sql:/docker-entrypoint-initdb.d/init.sql
- ./sql:/sql
volumes:
BIN
View File
Binary file not shown.
File diff suppressed because one or more lines are too long
+4
View File
@@ -3,6 +3,7 @@ import logging
import sys
from pathlib import Path
from uuid import uuid4
from rio import App, Theme, Color, Font, ComponentPage, Session
from from_root import from_root
@@ -10,6 +11,7 @@ from from_root import from_root
from src.ezgg_lan_manager import pages, init_services, LocalDataService
from src.ezgg_lan_manager.helpers.LoggedInGuard import logged_in_guard, not_logged_in_guard, team_guard
from src.ezgg_lan_manager.services.LocalDataService import LocalData
from src.ezgg_lan_manager.types.UserSession import UserSession
logger = logging.getLogger("EzggLanManager")
@@ -33,6 +35,8 @@ if __name__ == "__main__":
lan_info = default_attachments[3].get_lan_info()
async def on_session_start(session: Session) -> None:
# Use this line to fake being any user without having to log in
# session.attach(UserSession(id=uuid4(), user_id=30, is_team_member=True))
await session.set_title(lan_info.name)
if session[LocalData].stored_session_token:
user_session = session[LocalDataService].verify_token(session[LocalData].stored_session_token)
@@ -1,38 +0,0 @@
from asyncio import sleep
from rio import Text, Component, TextStyle
class AnimatedText(Component):
def __post_init__(self) -> None:
self._display_printing: list[bool] = [False]
self.text_comp = Text("")
async def display_text(self, success: bool, text: str, speed: float = 0.06, font_size: float = 0.9) -> None:
if self._display_printing[0]:
return
else:
self._display_printing[0] = True
self.text_comp.text = ""
if success:
self.text_comp.style = TextStyle(
fill=self.session.theme.success_color,
font_size=font_size
)
for c in text:
self.text_comp.text = self.text_comp.text + c
self.text_comp.force_refresh()
await sleep(speed)
else:
self.text_comp.style = TextStyle(
fill=self.session.theme.danger_color,
font_size=font_size
)
for c in text:
self.text_comp.text = self.text_comp.text + c
self.text_comp.force_refresh()
await sleep(speed)
self._display_printing[0] = False
def build(self) -> Component:
return self.text_comp
@@ -46,10 +46,10 @@ class CateringSelectionItem(Component):
Text(AccountingService.make_euro_string_from_decimal(self.article_price),
style=TextStyle(fill=self.session.theme.background_color, font_size=0.9)),
IconButton(
icon="material/add",
icon="material/add" if self.is_sensitive else "material/do_not_disturb_on_total_silence",
min_size=2,
color=self.session.theme.success_color,
style="plain-text",
color=self.session.theme.success_color if self.is_sensitive else self.session.theme.danger_color,
style="colored-text",
on_press=lambda: self.on_add_callback(self.article_id),
is_sensitive=self.is_sensitive
),
@@ -60,8 +60,10 @@ class UserInfoBox(Component):
self.session[LocalDataService].del_session(self.session[LocalData].stored_session_token)
self.session[LocalData].stored_session_token = None
self.session.attach(self.session[LocalData])
await self.status_change_cb()
if self.status_change_cb is not None:
await self.status_change_cb()
await self.session[RefreshService].trigger_refresh()
self.session.navigate_to("")
@event.on_populate
async def async_init(self) -> None:
+26 -15
View File
@@ -1,7 +1,7 @@
from decimal import Decimal
from typing import Optional
from rio import Column, Component, event, Text, TextStyle, Button, Color, Revealer, Row, ProgressCircle, Link
from rio import Column, Component, event, Text, TextStyle, Button, Color, Revealer, Row, ProgressCircle, Link, Image
from src.ezgg_lan_manager import ConfigurationService, UserService, AccountingService
from src.ezgg_lan_manager.components.MainViewContentBox import MainViewContentBox
@@ -14,6 +14,7 @@ class AccountPage(Component):
user: Optional[User] = None
balance: Optional[Decimal] = None
transaction_history: list[Transaction] = list()
payment_qr_image: bytes = None
banking_info_revealer_open: bool = False
paypal_info_revealer_open: bool = False
@@ -28,6 +29,11 @@ class AccountPage(Component):
self.user = await self.session[UserService].get_user(user_id)
self.balance = await self.session[AccountingService].get_balance(user_id)
self.transaction_history = await self.session[AccountingService].get_transaction_history(user_id)
self.payment_qr_image = self.session[AccountingService].make_payment_qr_image(
"Einfach Zocken Gaming Gesellschaft",
"GENODE51BIK",
"DE47517624340019856607",
f"AUFLADUNG - {self.user.user_id} - {self.user.user_name}")
async def _on_banking_info_press(self) -> None:
self.banking_info_revealer_open = not self.banking_info_revealer_open
@@ -36,7 +42,7 @@ class AccountPage(Component):
self.paypal_info_revealer_open = not self.paypal_info_revealer_open
def build(self) -> Component:
if not self.user and not self.balance:
if not self.user or not self.payment_qr_image:
return Column(
MainViewContentBox(
ProgressCircle(
@@ -85,6 +91,10 @@ class AccountPage(Component):
margin=0,
margin_bottom=1,
align_x=0.5
),
Image(self.payment_qr_image,
min_width=20,
min_height=20
)
),
margin=2,
@@ -223,19 +233,20 @@ class AccountPage(Component):
on_press=self._on_paypal_info_press
),
paypal_info_revealer,
Link(
content=Button(
content=Text("PAYPAL (3% Gebühr - Gewerblich)", style=TextStyle(fill=Color.from_hex("121212"), font_size=0.8), justify="center"),
shape="rectangle",
style="major",
color="secondary",
grow_x=True,
margin=2,
margin_top=0
),
target_url="https://www.paypal.com/ncp/payment/89YMGVZ4S33RS",
open_in_new_tab=True
)
# Disabled because people did not understand the fee's and kept charging 24.03 € to their accounts
# Link(
# content=Button(
# content=Text("PAYPAL (3% Gebühr - Gewerblich)", style=TextStyle(fill=Color.from_hex("121212"), font_size=0.8), justify="center"),
# shape="rectangle",
# style="major",
# color="secondary",
# grow_x=True,
# margin=2,
# margin_top=0
# ),
# target_url="https://www.paypal.com/ncp/payment/89YMGVZ4S33RS",
# open_in_new_tab=True
# )
)
),
MainViewContentBox(
+84 -60
View File
@@ -1,10 +1,10 @@
import logging
from asyncio import sleep, create_task
from email_validator import validate_email, EmailNotValidError
from rio import Column, Component, event, Text, TextStyle, TextInput, TextInputChangeEvent, Button
from src.ezgg_lan_manager import ConfigurationService, UserService, MailingService
from src.ezgg_lan_manager.components.AnimatedText import AnimatedText
from src.ezgg_lan_manager.components.MainViewContentBox import MainViewContentBox
MINIMUM_PASSWORD_LENGTH = 6
@@ -13,125 +13,154 @@ logger = logging.getLogger(__name__.split(".")[-1])
class RegisterPage(Component):
pw_1: str = ""
pw_2: str = ""
email: str = ""
user_name: str = ""
pw_1_valid: bool = True
pw_2_valid: bool = True
email_valid: bool = True
submit_button_loading: bool = False
display_text: str = ""
display_text_style: TextStyle = TextStyle()
def on_pw_focus_loss(self, _: TextInputChangeEvent) -> None:
if not (self.pw_1.text == self.pw_2.text) or len(self.pw_1.text) < MINIMUM_PASSWORD_LENGTH:
self.pw_1.is_valid = False
self.pw_2.is_valid = False
if not (self.pw_1 == self.pw_2) or len(self.pw_1) < MINIMUM_PASSWORD_LENGTH:
self.pw_1_valid = False
self.pw_2_valid = False
return
self.pw_1.is_valid = True
self.pw_2.is_valid = True
self.pw_1_valid = True
self.pw_2_valid = True
def on_email_focus_loss(self, change_event: TextInputChangeEvent) -> None:
try:
validate_email(change_event.text, check_deliverability=False)
self.email_input.is_valid = True
self.email_valid = True
except EmailNotValidError:
self.email_input.is_valid = False
self.email_valid = False
def on_user_name_focus_loss(self, _: TextInputChangeEvent) -> None:
current_text = self.user_name_input.text
current_text = self.user_name
if len(current_text) > UserService.MAX_USERNAME_LENGTH:
self.user_name_input.text = current_text[:UserService.MAX_USERNAME_LENGTH]
self.user_name = current_text[:UserService.MAX_USERNAME_LENGTH]
async def on_submit_button_pressed(self) -> None:
self.submit_button.is_loading = True
self.submit_button.force_refresh()
self.submit_button_loading = True
if len(self.user_name_input.text) < 1:
await self.animated_text.display_text(False, "Nutzername darf nicht leer sein!")
self.submit_button.is_loading = False
if len(self.user_name) < 1:
await self.display_animated_text(False, "Nutzername darf nicht leer sein!")
self.submit_button_loading = False
return
if not (self.pw_1.text == self.pw_2.text):
await self.animated_text.display_text(False, "Passwörter stimmen nicht überein!")
self.submit_button.is_loading = False
if not (self.pw_1 == self.pw_2):
await self.display_animated_text(False, "Passwörter stimmen nicht überein!")
self.submit_button_loading = False
return
if len(self.pw_1.text) < MINIMUM_PASSWORD_LENGTH:
await self.animated_text.display_text(False, f"Passwort muss mindestens {MINIMUM_PASSWORD_LENGTH} Zeichen lang sein!")
self.submit_button.is_loading = False
if len(self.pw_1) < MINIMUM_PASSWORD_LENGTH:
await self.display_animated_text(False, f"Passwort muss mindestens {MINIMUM_PASSWORD_LENGTH} Zeichen lang sein!")
self.submit_button_loading = False
return
if not self.email_input.is_valid or len(self.email_input.text) < 3:
await self.animated_text.display_text(False, "E-Mail Adresse ungültig!")
self.submit_button.is_loading = False
if not self.email_valid or len(self.email) < 3:
await self.display_animated_text(False, "E-Mail Adresse ungültig!")
self.submit_button_loading = False
return
user_service = self.session[UserService]
mailing_service = self.session[MailingService]
lan_info = self.session[ConfigurationService].get_lan_info()
if await user_service.get_user(self.email_input.text) is not None or await user_service.get_user(self.user_name_input.text) is not None:
await self.animated_text.display_text(False, "Benutzername oder E-Mail bereits regestriert!")
self.submit_button.is_loading = False
if await user_service.get_user(self.email) is not None or await user_service.get_user(self.user_name) is not None:
await self.display_animated_text(False, "Benutzername oder E-Mail bereits registriert!")
self.submit_button_loading = False
return
try:
new_user = await user_service.create_user(self.user_name_input.text, self.email_input.text, self.pw_1.text)
new_user = await user_service.create_user(self.user_name, self.email, self.pw_1)
if not new_user:
logger.warning(f"UserService.create_user returned: {new_user}") # ToDo: Seems like the user is created fine, even if not returned #FixMe
logger.error(f"create_user returned: {new_user}")
raise Exception(f"create_user returned: {new_user}")
except Exception as e:
logger.error(f"Unknown error during new user registration: {e}")
await self.animated_text.display_text(False, "Es ist ein unbekannter Fehler aufgetreten :(")
self.submit_button.is_loading = False
await self.display_animated_text(False, "Es ist ein unbekannter Fehler aufgetreten :(")
self.submit_button_loading = False
return
await mailing_service.send_email(
subject="Erfolgreiche Registrierung",
body=f"Hallo {self.user_name_input.text},\n\n"
body=f"Hallo {self.user_name},\n\n"
f"Du hast dich erfolgreich beim EZGG-LAN Manager für {lan_info.name} {lan_info.iteration} registriert.\n\n"
f"Wenn du dich nicht registriert hast, kontaktiere bitte unser Team über unsere Homepage.\n\n"
f"Liebe Grüße\nDein {lan_info.name} - Team",
receiver=self.email_input.text
receiver=self.email
)
self.submit_button.is_loading = False
await self.animated_text.display_text(True, "Erfolgreich registriert!")
self.submit_button_loading = False
await self.display_animated_text(True, "Erfolgreich registriert!")
@event.on_populate
async def on_populate(self) -> None:
await self.session.set_title(f"{self.session[ConfigurationService].get_lan_info().name} - Registrieren")
async def display_animated_text(self, success: bool, text: str) -> None:
self.display_text = ""
style = TextStyle(
fill=self.session.theme.success_color if success else self.session.theme.danger_color,
font_size=0.9
)
self.display_text_style = style
_ = create_task(self._animate_text(text))
async def _animate_text(self, text: str) -> None:
for c in text:
self.display_text += c
await sleep(0.06)
def build(self) -> Component:
self.user_name_input = TextInput(
user_name_input = TextInput(
label="Benutzername",
text="",
text=self.bind().user_name,
margin_left=1,
margin_right=1,
margin_bottom=1,
grow_x=True,
on_lose_focus=self.on_user_name_focus_loss
)
self.email_input = TextInput(
email_input = TextInput(
label="E-Mail Adresse",
text="",
text=self.bind().email,
margin_left=1,
margin_right=1,
margin_bottom=1,
grow_x=True,
on_lose_focus=self.on_email_focus_loss
on_lose_focus=self.on_email_focus_loss,
is_valid=self.email_valid
)
self.pw_1 = TextInput(
pw_1_input = TextInput(
label="Passwort",
text="",
text=self.bind().pw_1,
margin_left=1,
margin_right=1,
margin_bottom=1,
grow_x=True,
is_secret=True,
on_lose_focus=self.on_pw_focus_loss
on_lose_focus=self.on_pw_focus_loss,
is_valid=self.pw_1_valid
)
self.pw_2 = TextInput(
pw_2_input = TextInput(
label="Passwort wiederholen",
text="",
text=self.bind().pw_2,
margin_left=1,
margin_right=1,
margin_bottom=1,
grow_x=True,
is_secret=True,
on_lose_focus=self.on_pw_focus_loss
on_lose_focus=self.on_pw_focus_loss,
is_valid=self.pw_2_valid
)
self.submit_button = Button(
submit_button = Button(
content=Text(
"Registrieren",
style=TextStyle(fill=self.session.theme.background_color, font_size=0.9),
@@ -145,13 +174,8 @@ class RegisterPage(Component):
shape="rectangle",
style="minor",
color=self.session.theme.secondary_color,
on_press=self.on_submit_button_pressed
)
self.animated_text = AnimatedText(
margin_top=2,
margin_left=1,
margin_right=1,
margin_bottom=2
on_press=self.on_submit_button_pressed,
is_loading=self.submit_button_loading
)
return Column(
MainViewContentBox(
@@ -166,12 +190,12 @@ class RegisterPage(Component):
margin_bottom=2,
align_x=0.5
),
self.user_name_input,
self.email_input,
self.pw_1,
self.pw_2,
self.submit_button,
self.animated_text
user_name_input,
email_input,
pw_1_input,
pw_2_input,
submit_button,
Text(self.display_text, margin_top=2, margin_left=1, margin_right=1, margin_bottom=2, style=self.display_text_style)
)
),
align_y=0,
@@ -1,4 +1,6 @@
import io
import logging
import qrcode
from collections.abc import Callable
from datetime import datetime
from decimal import Decimal, ROUND_DOWN
@@ -74,3 +76,29 @@ class AccountingService:
return "0.00 €"
rounded_decimal = str(euros.quantize(Decimal(".01"), rounding=ROUND_DOWN))
return f"{rounded_decimal}"
@staticmethod
def make_payment_qr_image(beneficiary_name, beneficiary_bic, beneficiary_iban, text, amount_euros=None) -> bytes:
text = text.replace("\n",";")
amount_formatted = "EUR{:.2f}".format(amount_euros) if amount_euros else ""
epc_text = f"""BCD
002
1
SCT
{beneficiary_bic}
{beneficiary_name}
{beneficiary_iban}
{amount_formatted}
{text}
"""
qr = qrcode.QRCode(
version=6,
error_correction=qrcode.constants.ERROR_CORRECT_M,
)
qr.add_data(epc_text)
img = qr.make_image()
img_bytes = io.BytesIO()
img.save(img_bytes)
return img_bytes.getvalue()
@@ -62,7 +62,8 @@ class DatabaseService:
password=self._database_config.db_password,
db=self._database_config.db_name,
minsize=1,
maxsize=40
maxsize=40,
autocommit=True
)
except aiomysql.OperationalError:
return False
@@ -21,5 +21,6 @@ class LocalDataService:
self._session[key] = session
return key
def del_session(self, token: str) -> None:
self._session.pop(token, None)
def del_session(self, token: Optional[str]) -> None:
if token is not None:
self._session.pop(token, None)