add is_active to login, add account and seating management to user management, redirect to base page on logout

This commit is contained in:
David Rodenkirchen 2024-11-30 12:32:31 +01:00
parent 18ff806d3b
commit 82b16b868f
4 changed files with 259 additions and 35 deletions

View File

@ -1,6 +1,5 @@
from typing import Callable
from rio import Component, TextStyle, Color, TextInput, Button, Text, Rectangle, Column, Row, Spacer, TextInputChangeEvent
from rio import Component, TextStyle, Color, TextInput, Button, Text, Rectangle, Column, Row, Spacer, \
EventHandler
from src.ez_lan_manager.services.UserService import UserService
from src.ez_lan_manager.types.SessionStorage import SessionStorage
@ -8,51 +7,49 @@ from src.ez_lan_manager.types.User import User
class LoginBox(Component):
status_change_cb: Callable
status_change_cb: EventHandler = None
TEXT_STYLE = TextStyle(fill=Color.from_hex("02dac5"), font_size=0.9)
user_name_input_text: list[str] = [""]
password_input_text: list[str] = [""]
user_name_input_text: str = ""
password_input_text: str = ""
user_name_input_is_valid = True
password_input_is_valid = True
login_button_is_loading = False
is_account_locked: bool = False
async def _on_login_pressed(self) -> None:
if await self.session[UserService].is_login_valid(self.user_name_input_text[0], self.password_input_text[0]):
user: User = await self.session[UserService].get_user(self.user_name_input_text[0])
if await self.session[UserService].is_login_valid(self.user_name_input_text, self.password_input_text):
user: User = await self.session[UserService].get_user(self.user_name_input_text)
if not user.is_active:
self.is_account_locked = True
return
self.user_name_input_is_valid = True
self.password_input_is_valid = True
self.login_button_is_loading = False
self.is_account_locked = False
await self.session[SessionStorage].set_user_id_and_team_member_flag(user.user_id, user.is_team_member)
await self.status_change_cb()
else:
self.user_name_input_is_valid = False
self.password_input_is_valid = False
self.login_button_is_loading = False
self.is_account_locked = False
await self.force_refresh()
def build(self) -> Component:
def set_user_name_input_text(e: TextInputChangeEvent) -> None:
self.user_name_input_text[0] = e.text
def set_password_input_text(e: TextInputChangeEvent) -> None:
self.password_input_text[0] = e.text
user_name_input = TextInput(
text="",
text=self.bind().user_name_input_text,
label="Benutzername",
accessibility_label="Benutzername",
min_height=0.5,
on_confirm=lambda _: self._on_login_pressed(),
on_change=set_user_name_input_text,
is_valid=self.user_name_input_is_valid
)
password_input = TextInput(
text="",
text=self.bind().password_input_text,
label="Passwort",
accessibility_label="Passwort",
is_secret=True,
on_confirm=lambda _: self._on_login_pressed(),
on_change=set_password_input_text,
is_valid=self.password_input_is_valid
)
login_button = Button(
@ -91,8 +88,10 @@ class LoginBox(Component):
Spacer(),
forgot_password_button,
proportions=(49, 2, 49)
)
),
margin_bottom=0.5
),
Text(text="Dieses Konto\nist gesperrt", style=TextStyle(fill=self.session.theme.danger_color, font_size=0.9 if self.is_account_locked else 0), align_x=0.5),
spacing=0.4
),
fill=Color.TRANSPARENT,

View File

@ -0,0 +1,76 @@
from datetime import datetime
from typing import Optional
from rio import Component, Column, NumberInput, ThemeContextSwitcher, TextInput, Row, Button, EventHandler
from src.ez_lan_manager.types.Transaction import Transaction
from src.ez_lan_manager.types.User import User
class NewTransactionForm(Component):
user: Optional[User] = None
input_value: float = 0
input_reason: str = ""
new_transaction_cb: EventHandler[Transaction] = None
async def send_debit_transaction(self) -> None:
await self.call_event_handler(
self.new_transaction_cb,
Transaction(
user_id=self.user.user_id,
value=round(self.input_value * 100),
is_debit=True,
reference=self.input_reason,
transaction_date=datetime.now()
)
)
async def send_credit_transaction(self) -> None:
await self.call_event_handler(
self.new_transaction_cb,
Transaction(
user_id=self.user.user_id,
value=round(self.input_value * 100),
is_debit=False,
reference=self.input_reason,
transaction_date=datetime.now()
)
)
def build(self) -> Component:
return ThemeContextSwitcher(
content=Column(
NumberInput(
value=self.bind().input_value,
label="Betrag",
suffix_text="",
decimals=2,
thousands_separator=".",
margin=1,
margin_bottom=0
),
TextInput(
text=self.bind().input_reason,
label="Beschreibung",
margin=1,
margin_bottom=0
),
Row(
Button(
content="Entfernen",
shape="rectangle",
color="danger",
margin=1,
on_press=self.send_debit_transaction
),
Button(
content="Hinzufügen",
shape="rectangle",
color="success",
margin=1,
on_press=self.send_credit_transaction
)
)
),
color="primary"
)

View File

@ -1,7 +1,7 @@
from random import choice
from typing import Optional, Callable
from typing import Optional
from rio import Component, TextStyle, Color, Button, Text, Rectangle, Column, Row, Spacer, Link, event
from rio import Component, TextStyle, Color, Button, Text, Rectangle, Column, Row, Spacer, Link, event, EventHandler
from src.ez_lan_manager.components.UserInfoBoxButton import UserInfoBoxButton
from src.ez_lan_manager.services.UserService import UserService
@ -38,7 +38,7 @@ class StatusButton(Component):
)
class UserInfoBox(Component):
status_change_cb: Callable
status_change_cb: EventHandler = None
TEXT_STYLE = TextStyle(fill=Color.from_hex("02dac5"), font_size=0.9)
user: Optional[User] = None
user_balance: Optional[int] = 0
@ -53,6 +53,7 @@ class UserInfoBox(Component):
await self.session[SessionStorage].clear()
self.user = None
await self.status_change_cb()
self.session.navigate_to("/")
@event.on_populate
async def async_init(self) -> None:

View File

@ -1,26 +1,26 @@
import logging
from dataclasses import field
from typing import Optional, Coroutine
from typing import Optional
import rio
from rio import Column, Component, event, TextStyle, Text, TextInput, ThemeContextSwitcher, Grid, \
PointerEventListener, PointerEvent, Rectangle, CursorStyle, Color, TextInputChangeEvent, Spacer
PointerEventListener, PointerEvent, Rectangle, CursorStyle, Color, TextInputChangeEvent, Spacer, Row, Switch, \
SwitchChangeEvent, EventHandler
from src.ez_lan_manager import ConfigurationService, UserService
from src.ez_lan_manager import ConfigurationService, UserService, AccountingService, SeatingService
from src.ez_lan_manager.components.MainViewContentBox import MainViewContentBox
from src.ez_lan_manager.components.NewTransactionForm import NewTransactionForm
from src.ez_lan_manager.components.UserEditForm import UserEditForm
from src.ez_lan_manager.services.AccountingService import InsufficientFundsError
from src.ez_lan_manager.types.SessionStorage import SessionStorage
from src.ez_lan_manager.types.Transaction import Transaction
from src.ez_lan_manager.types.User import User
logger = logging.getLogger(__name__.split(".")[-1])
# Helps type checker grasp the concept of "lambda _: None" as a Coroutine
async def noop(_) -> None:
pass
class ClickableGridContent(Component):
text: str = ""
is_hovered: bool = False
clicked_cb: Coroutine = noop
clicked_cb: EventHandler[str] = None
async def on_mouse_enter(self, _: PointerEvent) -> None:
self.is_hovered = True
@ -29,9 +29,9 @@ class ClickableGridContent(Component):
self.is_hovered = False
async def on_mouse_click(self, _: PointerEvent) -> None:
await self.clicked_cb(self.text)
await self.call_event_handler(self.clicked_cb, self.text)
def build(self) -> rio.Component:
def build(self) -> Component:
return PointerEventListener(
content=Rectangle(
content=Text(
@ -51,19 +51,63 @@ class ManageUsersPage(Component):
selected_user: Optional[User] = None
all_users: Optional[list] = None
search_results: list[User] = field(default_factory=list)
accounting_section_result_text: str = ""
accounting_section_result_success: bool = True
user_account_balance: str = "0.00 €"
user_seat: str = "-"
is_user_account_locked: bool = False
@event.on_populate
async def on_populate(self) -> None:
await self.session.set_title(f"{self.session[ConfigurationService].get_lan_info().name} - News Verwaltung")
await self.session.set_title(f"{self.session[ConfigurationService].get_lan_info().name} - Nutzer Verwaltung")
self.all_users = await self.session[UserService].get_all_users()
self.search_results = self.all_users
async def on_user_clicked(self, user_name: str) -> None:
self.selected_user = next(filter(lambda user: user.user_name == user_name, self.all_users))
user_account_balance_raw = await self.session[AccountingService].get_balance(self.selected_user.user_id)
self.user_account_balance = AccountingService.make_euro_string_from_int(user_account_balance_raw)
seat = await self.session[SeatingService].get_user_seat(self.selected_user.user_id)
self.user_seat = seat.seat_id if seat else "-"
self.is_user_account_locked = not self.selected_user.is_active
async def on_search_parameters_changed(self, e: TextInputChangeEvent) -> None:
self.search_results = list(filter(lambda user: (e.text.lower() in user.user_name.lower()) or e.text.lower() in str(user.user_id), self.all_users))
async def change_account_active(self, _: SwitchChangeEvent) -> None:
self.selected_user.is_active = not self.is_user_account_locked
await self.session[UserService].update_user(self.selected_user)
async def on_new_transaction(self, transaction: Transaction) -> None:
if not self.session[SessionStorage].is_team_member: # Better safe than sorry
return
logger.info(f"Got new transaction for user with ID '{transaction.user_id}' over "
f"{'-' if transaction.is_debit else '+'}"
f"{AccountingService.make_euro_string_from_int(transaction.value)} "
f"with reference '{transaction.reference}'")
if transaction.is_debit:
try:
await self.session[AccountingService].remove_balance(
transaction.user_id,
transaction.value,
transaction.reference
)
except InsufficientFundsError:
self.accounting_section_result_text = "Guthaben nicht ausreichend!"
self.accounting_section_result_success = False
return
else:
await self.session[AccountingService].add_balance(
transaction.user_id,
transaction.value,
transaction.reference
)
self.accounting_section_result_text = f"Guthaben {'entfernt' if transaction.is_debit else 'hinzugefügt'}!"
self.accounting_section_result_success = True
def build(self) -> Component:
return Column(
@ -128,6 +172,110 @@ class ManageUsersPage(Component):
margin_top=2,
margin_bottom=2,
align_x=0.5
))),
)
)
),
MainViewContentBox(
Column(
Text(
text="Konto & Sitzplatz",
style=TextStyle(
fill=self.session.theme.background_color,
font_size=1.2
),
margin_top=2,
margin_bottom=2,
align_x=0.5
),
Row(
Text(
text="Kontostand:",
style=TextStyle(
fill=self.session.theme.background_color,
font_size=1
),
margin_top=0.5,
margin_bottom=1,
margin_left=2
),
Text(
text=self.bind().user_account_balance,
style=TextStyle(
fill=self.session.theme.neutral_color,
font_size=1
),
margin_top=0.5,
margin_bottom=1,
margin_right=2,
justify="right"
),
),
Row(
Text(
text="Kontosperrung:",
style=TextStyle(
fill=self.session.theme.background_color,
font_size=1
),
margin_top=0.5,
margin_bottom=1,
margin_left=2,
grow_x=True
),
ThemeContextSwitcher(
content=Switch(
is_on=self.bind().is_user_account_locked,
margin_top=0.5,
margin_bottom=1,
margin_right=2,
on_change=self.change_account_active
),
color="primary"
),
),
Row(
Text(
text="Sitzplatz:",
style=TextStyle(
fill=self.session.theme.background_color,
font_size=1
),
margin_top=0.5,
margin_bottom=1,
margin_left=2
),
Text(
text=self.bind().user_seat,
style=TextStyle(
fill=self.session.theme.neutral_color,
font_size=1
),
margin_top=0.5,
margin_bottom=1,
margin_right=2,
justify="right"
),
),
Text(
text="Geld hinzufügen/entfernen",
style=TextStyle(
fill=self.session.theme.background_color,
font_size=1
),
margin_top=0.5,
margin_bottom=1,
align_x=0.5
),
NewTransactionForm(user=self.selected_user, new_transaction_cb=self.on_new_transaction),
Text(
text=self.bind().accounting_section_result_text,
style=TextStyle(
fill=self.session.theme.success_color if self.accounting_section_result_success else self.session.theme.danger_color
),
margin_left=1,
margin_bottom=1
)
)
) if self.selected_user else Spacer(),
align_y=0
)