Refactor logged-in and out messaging, Prepare Catering Module with shopping cart

This commit is contained in:
David Rodenkirchen 2024-08-28 11:59:25 +02:00
parent bde331a32c
commit b00a819325
10 changed files with 227 additions and 52 deletions

View File

@ -64,7 +64,7 @@ if __name__ == "__main__":
Page( Page(
name="Catering", name="Catering",
page_url="catering", page_url="catering",
build=lambda: pages.PlaceholderPage(placeholder_name="Catering"), build=pages.CateringPage,
), ),
Page( Page(
name="Guests", name="Guests",
@ -119,12 +119,6 @@ if __name__ == "__main__":
page_url="account", page_url="account",
build=pages.AccountPage, build=pages.AccountPage,
guard=logged_in_guard guard=logged_in_guard
),
Page(
name="Logout",
page_url="logout",
build=pages.LogoutPage,
guard=logged_in_guard
) )
], ],
theme=theme, theme=theme,

View File

@ -0,0 +1,30 @@
from typing import Callable
import rio
from rio import Component, Row, Text, IconButton, TextStyle
from src.ez_lan_manager import AccountingService
MAX_LEN = 24
class CateringCartItem(Component):
article_name: str
article_price: int
article_id: int
list_id: int
remove_item_cb: Callable
@staticmethod
def ellipsize_string(string: str) -> str:
if len(string) <= MAX_LEN:
return string
return string[:MAX_LEN - 3] + "..."
def build(self) -> rio.Component:
return Row(
Text(self.ellipsize_string(self.article_name), align_x=0, wrap=True, min_width=19, style=TextStyle(fill=self.session.theme.background_color, font_size=0.9)),
Text(AccountingService.make_euro_string_from_int(self.article_price), style=TextStyle(fill=self.session.theme.background_color, font_size=0.9)),
IconButton(icon="material/close", size=2, color=self.session.theme.danger_color, style="plain", on_press=lambda: self.remove_item_cb(self.list_id)),
proportions=(19, 5, 2)
)

View File

@ -7,20 +7,20 @@ from src.ez_lan_manager.components.UserInfoBox import UserInfoBox
from src.ez_lan_manager.types.SessionStorage import SessionStorage from src.ez_lan_manager.types.SessionStorage import SessionStorage
class DesktopNavigation(Component): class DesktopNavigation(Component):
def __post_init__(self) -> None:
self.session[SessionStorage].subscribe_to_logged_in_or_out_event(self.__class__.__name__, self.refresh_cb)
async def refresh_cb(self) -> None: async def refresh_cb(self) -> None:
self.box = self.login_box if self.session[SessionStorage].user_id is None else self.user_info_box
await self.force_refresh() await self.force_refresh()
def build(self) -> Component: def build(self) -> Component:
self.user_info_box = UserInfoBox() box = LoginBox() if self.session[SessionStorage].user_id is None else UserInfoBox()
self.login_box = LoginBox(self.refresh_cb)
self.box = self.login_box if self.session[SessionStorage].user_id is None else self.user_info_box
lan_info = self.session[ConfigurationService].get_lan_info() lan_info = self.session[ConfigurationService].get_lan_info()
return Card( return Card(
Column( Column(
Text(lan_info.name, align_x=0.5, margin_top=0.3, style=TextStyle(fill=self.session.theme.hud_color, font_size=2.5)), Text(lan_info.name, align_x=0.5, margin_top=0.3, style=TextStyle(fill=self.session.theme.hud_color, font_size=2.5)),
Text(f"Edition {lan_info.iteration}", align_x=0.5, style=TextStyle(fill=self.session.theme.hud_color, font_size=1.2), margin_top=0.3, margin_bottom=2), Text(f"Edition {lan_info.iteration}", align_x=0.5, style=TextStyle(fill=self.session.theme.hud_color, font_size=1.2), margin_top=0.3, margin_bottom=2),
self.box, box,
DesktopNavigationButton("News", "./news"), DesktopNavigationButton("News", "./news"),
Spacer(min_height=1), Spacer(min_height=1),
DesktopNavigationButton(f"Über {lan_info.name} {lan_info.iteration}", "./overview"), DesktopNavigationButton(f"Über {lan_info.name} {lan_info.iteration}", "./overview"),

View File

@ -8,17 +8,15 @@ from src.ez_lan_manager.types.SessionStorage import SessionStorage
class LoginBox(Component): class LoginBox(Component):
TEXT_STYLE = TextStyle(fill=Color.from_hex("02dac5"), font_size=0.9) TEXT_STYLE = TextStyle(fill=Color.from_hex("02dac5"), font_size=0.9)
refresh_cb: Callable
async def _on_login_pressed(self) -> None: async def _on_login_pressed(self) -> None:
self.login_button.is_loading = True self.login_button.is_loading = True
user_name = self.user_name_input.text.lower() user_name = self.user_name_input.text.lower()
if self.session[UserService].is_login_valid(user_name, self.password_input.text): if self.session[UserService].is_login_valid(user_name, self.password_input.text):
self.session[SessionStorage].user_id = self.session[UserService].get_user(user_name).user_id
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
await self.refresh_cb() await self.session[SessionStorage].set_user_id(self.session[UserService].get_user(user_name).user_id)
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

View File

@ -1,6 +1,6 @@
from random import choice from random import choice
from rio import Component, Card, Column, Text, Row, Rectangle, Button, TextStyle, Color, Spacer, TextInput, Link from rio import Component, Column, Text, Row, Rectangle, Button, TextStyle, Color, Link
from src.ez_lan_manager import UserService, AccountingService, TicketingService, SeatingService from src.ez_lan_manager import UserService, AccountingService, TicketingService, SeatingService
from src.ez_lan_manager.components.UserInfoBoxButton import UserInfoBoxButton from src.ez_lan_manager.components.UserInfoBoxButton import UserInfoBoxButton
@ -35,6 +35,10 @@ class UserInfoBox(Component):
def get_greeting() -> str: def get_greeting() -> str:
return choice(["Grüße", "Hallo", "Willkommen", "Moin", "Ahoi"]) return choice(["Grüße", "Hallo", "Willkommen", "Moin", "Ahoi"])
async def logout(self) -> None:
await self.session[SessionStorage].clear()
await self.force_refresh()
def build(self) -> Component: def build(self) -> Component:
user = self.session[UserService].get_user(self.session[SessionStorage].user_id) user = self.session[UserService].get_user(self.session[SessionStorage].user_id)
if user is None: # Noone logged in if user is None: # Noone logged in
@ -52,7 +56,17 @@ class UserInfoBox(Component):
), ),
UserInfoBoxButton("Profil bearbeiten", "./edit-profile"), UserInfoBoxButton("Profil bearbeiten", "./edit-profile"),
UserInfoBoxButton(f"Guthaben: {a_s.make_euro_string_from_int(a_s.get_balance(user.user_id))}", "./account"), UserInfoBoxButton(f"Guthaben: {a_s.make_euro_string_from_int(a_s.get_balance(user.user_id))}", "./account"),
UserInfoBoxButton("Ausloggen", "./logout") Button(
content=Text("Ausloggen", style=TextStyle(fill=Color.from_hex("02dac5"), font_size=0.6)),
shape="rectangle",
style="minor",
color="secondary",
grow_x=True,
margin_left=0.6,
margin_right=0.6,
margin_top=0.6,
on_press=self.logout
)
), ),
fill=Color.TRANSPARENT, fill=Color.TRANSPARENT,
min_height=8, min_height=8,

View File

@ -0,0 +1,131 @@
from typing import Optional
from rio import Column, Component, event, TextStyle, Text, ScrollContainer, Row, Button, Spacer, IconButton
from src.ez_lan_manager import ConfigurationService, CateringService, AccountingService
from src.ez_lan_manager.components.CateringCartItem import CateringCartItem
from src.ez_lan_manager.components.MainViewContentBox import MainViewContentBox
from src.ez_lan_manager.pages import BasePage
from src.ez_lan_manager.types.CateringMenuItem import CateringMenuItem
from src.ez_lan_manager.types.SessionStorage import SessionStorage
class CateringPage(Component):
def __post_init__(self) -> None:
self.session[SessionStorage].subscribe_to_logged_in_or_out_event(self.__class__.__name__, self.on_user_logged_in_status_changed)
@event.on_populate
async def on_populate(self) -> None:
await self.session.set_title(f"{self.session[ConfigurationService].get_lan_info().name} - Catering")
async def on_user_logged_in_status_changed(self) -> None:
await self.force_refresh()
async def on_remove_item(self, list_id: int) -> None:
catering_service = self.session[CateringService]
user_id = self.session[SessionStorage].user_id
cart = catering_service.get_cart(user_id)
try:
cart.pop(list_id)
except IndexError:
return
catering_service.save_cart(user_id, cart)
await self.force_refresh()
async def on_empty_cart_pressed(self) -> None:
self.session[CateringService].save_cart(self.session[SessionStorage].user_id, [])
await self.force_refresh()
def build(self) -> Component:
user_id = self.session[SessionStorage].user_id
catering_service = self.session[CateringService]
cart = catering_service.get_cart(user_id)
cart_container = ScrollContainer(
content=Column(
*[CateringCartItem(
article_name=cart_item.name,
article_price=cart_item.price,
article_id=cart_item.item_id,
remove_item_cb=self.on_remove_item,
list_id=idx
) for idx, cart_item in enumerate(cart)],
Spacer(grow_y=True)
),
min_height=8,
min_width=33,
margin=1
)
shopping_cart = MainViewContentBox(
Column(
Text(
text="Catering",
style=TextStyle(
fill=self.session.theme.background_color,
font_size=1.2
),
margin_top=2,
margin_bottom=0,
align_x=0.5
),
Text(
text="Warenkorb",
style=TextStyle(
fill=self.session.theme.background_color,
font_size=0.8
),
margin_top=0.2,
margin_bottom=0,
align_x=0.5
),
cart_container,
Row(
Text(
text=f"Preis: {AccountingService.make_euro_string_from_int(sum(cart_item.price for cart_item in cart))}",
style=TextStyle(
fill=self.session.theme.background_color,
font_size=0.8
),
margin=1
),
Button(
content=Text(
"Warenkorb leeren",
style=TextStyle(fill=self.session.theme.danger_color, font_size=0.9),
align_x=0.2
),
margin=1,
margin_left=0,
shape="rectangle",
style="major",
color="primary",
on_press=self.on_empty_cart_pressed
),
Button(
content=Text(
"Bestellen",
style=TextStyle(fill=self.session.theme.success_color, font_size=0.9),
align_x=0.2
),
margin=1,
margin_left=0,
shape="rectangle",
style="major",
color="primary"
),
)
)
) if user_id else Spacer()
return BasePage(
content=Column(
# SHOPPING CART
shopping_cart,
# ITEM SELECTION
MainViewContentBox(
),
align_y=0
)
)

View File

@ -1,30 +0,0 @@
from rio import Column, Component, event, Text, TextStyle
from src.ez_lan_manager import ConfigurationService
from src.ez_lan_manager.components.MainViewContentBox import MainViewContentBox
from src.ez_lan_manager.pages import BasePage
from src.ez_lan_manager.types.SessionStorage import SessionStorage
class LogoutPage(Component):
@event.on_populate
async def on_populate(self) -> None:
await self.session.set_title(f"{self.session[ConfigurationService].get_lan_info().name} - Logout")
def build(self) -> Component:
self.session[SessionStorage].clear()
return BasePage(
content=Column(
MainViewContentBox(
content=Text(
"Auf wiedersehen o/",
style=TextStyle(
fill=self.session.theme.background_color,
font_size=1.4
),
margin=2
)
),
align_y=0,
)
)

View File

@ -1,7 +1,6 @@
from .BasePage import BasePage from .BasePage import BasePage
from .NewsPage import NewsPage from .NewsPage import NewsPage
from .PlaceholderPage import PlaceholderPage from .PlaceholderPage import PlaceholderPage
from .Logout import LogoutPage
from .Account import AccountPage from .Account import AccountPage
from .EditProfile import EditProfilePage from .EditProfile import EditProfilePage
from .ForgotPassword import ForgotPasswordPage from .ForgotPassword import ForgotPasswordPage
@ -12,3 +11,4 @@ from .RulesPage import RulesPage
from .FaqPage import FaqPage from .FaqPage import FaqPage
from .TournamentsPage import TournamentsPage from .TournamentsPage import TournamentsPage
from .GuestsPage import GuestsPage from .GuestsPage import GuestsPage
from .CateringPage import CateringPage

View File

@ -19,6 +19,16 @@ class CateringService:
self._db_service = db_service self._db_service = db_service
self._accounting_service = accounting_service self._accounting_service = accounting_service
self._user_service = user_service self._user_service = user_service
self.cached_cart: dict[int, list[CateringMenuItem]] = { # REMOVE
27: [
CateringMenuItem(1, "Bockwurst", 150, CateringMenuItemCategory.SNACK),
CateringMenuItem(2, "Pils", 120, CateringMenuItemCategory.SNACK),
CateringMenuItem(3, "Pfezzi", 200, CateringMenuItemCategory.SNACK),
CateringMenuItem(3, "Pfezzi", 200, CateringMenuItemCategory.SNACK),
CateringMenuItem(4, "Pizza", 1150, CateringMenuItemCategory.MAIN_COURSE),
CateringMenuItem(5, "Zigaretten", 800, CateringMenuItemCategory.NON_FOOD),
]
}
# ORDERS # ORDERS
@ -110,3 +120,17 @@ class CateringService:
def enable_menu_items_by_category(self, category: CateringMenuItemCategory) -> bool: def enable_menu_items_by_category(self, category: CateringMenuItemCategory) -> bool:
items = self.get_menu(category=category) items = self.get_menu(category=category)
return all([self.enable_menu_item(item.item_id) for item in items]) 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 []

View File

@ -1,4 +1,5 @@
from dataclasses import dataclass from collections.abc import Callable
from dataclasses import dataclass, field
from typing import Optional from typing import Optional
@ -6,7 +7,20 @@ from typing import Optional
# Note for ToDo: rio.UserSettings are saved LOCALLY, do not just read a user_id here! # Note for ToDo: rio.UserSettings are saved LOCALLY, do not just read a user_id here!
@dataclass(frozen=False) @dataclass(frozen=False)
class SessionStorage: class SessionStorage:
user_id: Optional[int] = None # DEBUG: Put user ID here to skip login _user_id: Optional[int] = None # DEBUG: Put user ID here to skip login
_notification_callbacks: dict[str, Callable] = field(default_factory=dict)
def clear(self) -> None: async def clear(self) -> None:
self.user_id = None await self.set_user_id(None)
def subscribe_to_logged_in_or_out_event(self, component_id: str, callback: Callable) -> None:
self._notification_callbacks[component_id] = callback
@property
def user_id(self) -> Optional[int]:
return self._user_id
async def set_user_id(self, user_id: Optional[int]) -> None:
self._user_id = user_id
for callback in self._notification_callbacks.values():
await callback()