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, \
EventHandler
from rio import Component, TextStyle, Color, TextInput, Button, Text, Rectangle, Column, Row, Spacer, TextInputChangeEvent
from src.ez_lan_manager.services.UserService import UserService from src.ez_lan_manager.services.UserService import UserService
from src.ez_lan_manager.types.SessionStorage import SessionStorage 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): class LoginBox(Component):
status_change_cb: Callable status_change_cb: EventHandler = None
TEXT_STYLE = TextStyle(fill=Color.from_hex("02dac5"), font_size=0.9) TEXT_STYLE = TextStyle(fill=Color.from_hex("02dac5"), font_size=0.9)
user_name_input_text: list[str] = [""] user_name_input_text: str = ""
password_input_text: list[str] = [""] password_input_text: str = ""
user_name_input_is_valid = True user_name_input_is_valid = True
password_input_is_valid = True password_input_is_valid = True
login_button_is_loading = False login_button_is_loading = False
is_account_locked: bool = False
async def _on_login_pressed(self) -> None: 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]): 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[0]) 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.user_name_input_is_valid = True
self.password_input_is_valid = True self.password_input_is_valid = True
self.login_button_is_loading = False 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.session[SessionStorage].set_user_id_and_team_member_flag(user.user_id, user.is_team_member)
await self.status_change_cb() await self.status_change_cb()
else: else:
self.user_name_input_is_valid = False self.user_name_input_is_valid = False
self.password_input_is_valid = False self.password_input_is_valid = False
self.login_button_is_loading = False self.login_button_is_loading = False
self.is_account_locked = False
await self.force_refresh() await self.force_refresh()
def build(self) -> Component: 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( user_name_input = TextInput(
text="", text=self.bind().user_name_input_text,
label="Benutzername", label="Benutzername",
accessibility_label="Benutzername", accessibility_label="Benutzername",
min_height=0.5, min_height=0.5,
on_confirm=lambda _: self._on_login_pressed(), on_confirm=lambda _: self._on_login_pressed(),
on_change=set_user_name_input_text,
is_valid=self.user_name_input_is_valid is_valid=self.user_name_input_is_valid
) )
password_input = TextInput( password_input = TextInput(
text="", text=self.bind().password_input_text,
label="Passwort", label="Passwort",
accessibility_label="Passwort", accessibility_label="Passwort",
is_secret=True, is_secret=True,
on_confirm=lambda _: self._on_login_pressed(), on_confirm=lambda _: self._on_login_pressed(),
on_change=set_password_input_text,
is_valid=self.password_input_is_valid is_valid=self.password_input_is_valid
) )
login_button = Button( login_button = Button(
@ -91,8 +88,10 @@ class LoginBox(Component):
Spacer(), Spacer(),
forgot_password_button, forgot_password_button,
proportions=(49, 2, 49) 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 spacing=0.4
), ),
fill=Color.TRANSPARENT, 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 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.components.UserInfoBoxButton import UserInfoBoxButton
from src.ez_lan_manager.services.UserService import UserService from src.ez_lan_manager.services.UserService import UserService
@ -38,7 +38,7 @@ class StatusButton(Component):
) )
class UserInfoBox(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) TEXT_STYLE = TextStyle(fill=Color.from_hex("02dac5"), font_size=0.9)
user: Optional[User] = None user: Optional[User] = None
user_balance: Optional[int] = 0 user_balance: Optional[int] = 0
@ -53,6 +53,7 @@ class UserInfoBox(Component):
await self.session[SessionStorage].clear() await self.session[SessionStorage].clear()
self.user = None self.user = None
await self.status_change_cb() await self.status_change_cb()
self.session.navigate_to("/")
@event.on_populate @event.on_populate
async def async_init(self) -> None: async def async_init(self) -> None:

View File

@ -1,26 +1,26 @@
import logging import logging
from dataclasses import field 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, \ 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.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.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 from src.ez_lan_manager.types.User import User
logger = logging.getLogger(__name__.split(".")[-1]) 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): class ClickableGridContent(Component):
text: str = "" text: str = ""
is_hovered: bool = False is_hovered: bool = False
clicked_cb: Coroutine = noop clicked_cb: EventHandler[str] = None
async def on_mouse_enter(self, _: PointerEvent) -> None: async def on_mouse_enter(self, _: PointerEvent) -> None:
self.is_hovered = True self.is_hovered = True
@ -29,9 +29,9 @@ class ClickableGridContent(Component):
self.is_hovered = False self.is_hovered = False
async def on_mouse_click(self, _: PointerEvent) -> None: 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( return PointerEventListener(
content=Rectangle( content=Rectangle(
content=Text( content=Text(
@ -51,19 +51,63 @@ class ManageUsersPage(Component):
selected_user: Optional[User] = None selected_user: Optional[User] = None
all_users: Optional[list] = None all_users: Optional[list] = None
search_results: list[User] = field(default_factory=list) 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 @event.on_populate
async def on_populate(self) -> None: 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.all_users = await self.session[UserService].get_all_users()
self.search_results = self.all_users self.search_results = self.all_users
async def on_user_clicked(self, user_name: str) -> None: 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)) 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: 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)) 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: def build(self) -> Component:
return Column( return Column(
@ -128,6 +172,110 @@ class ManageUsersPage(Component):
margin_top=2, margin_top=2,
margin_bottom=2, margin_bottom=2,
align_x=0.5 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 align_y=0
) )