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(
name="Catering",
page_url="catering",
build=lambda: pages.PlaceholderPage(placeholder_name="Catering"),
build=pages.CateringPage,
),
Page(
name="Guests",
@ -119,12 +119,6 @@ if __name__ == "__main__":
page_url="account",
build=pages.AccountPage,
guard=logged_in_guard
),
Page(
name="Logout",
page_url="logout",
build=pages.LogoutPage,
guard=logged_in_guard
)
],
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
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:
self.box = self.login_box if self.session[SessionStorage].user_id is None else self.user_info_box
await self.force_refresh()
def build(self) -> Component:
self.user_info_box = 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
box = LoginBox() if self.session[SessionStorage].user_id is None else UserInfoBox()
lan_info = self.session[ConfigurationService].get_lan_info()
return Card(
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(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"),
Spacer(min_height=1),
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):
TEXT_STYLE = TextStyle(fill=Color.from_hex("02dac5"), font_size=0.9)
refresh_cb: Callable
async def _on_login_pressed(self) -> None:
self.login_button.is_loading = True
user_name = self.user_name_input.text.lower()
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.password_input.is_valid = True
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:
self.user_name_input.is_valid = False
self.password_input.is_valid = False

View File

@ -1,6 +1,6 @@
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.components.UserInfoBoxButton import UserInfoBoxButton
@ -35,6 +35,10 @@ class UserInfoBox(Component):
def get_greeting() -> str:
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:
user = self.session[UserService].get_user(self.session[SessionStorage].user_id)
if user is None: # Noone logged in
@ -52,7 +56,17 @@ class UserInfoBox(Component):
),
UserInfoBoxButton("Profil bearbeiten", "./edit-profile"),
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,
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 .NewsPage import NewsPage
from .PlaceholderPage import PlaceholderPage
from .Logout import LogoutPage
from .Account import AccountPage
from .EditProfile import EditProfilePage
from .ForgotPassword import ForgotPasswordPage
@ -12,3 +11,4 @@ from .RulesPage import RulesPage
from .FaqPage import FaqPage
from .TournamentsPage import TournamentsPage
from .GuestsPage import GuestsPage
from .CateringPage import CateringPage

View File

@ -19,6 +19,16 @@ class CateringService:
self._db_service = db_service
self._accounting_service = accounting_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
@ -110,3 +120,17 @@ class CateringService:
def enable_menu_items_by_category(self, category: CateringMenuItemCategory) -> bool:
items = 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 []

View File

@ -1,4 +1,5 @@
from dataclasses import dataclass
from collections.abc import Callable
from dataclasses import dataclass, field
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!
@dataclass(frozen=False)
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:
self.user_id = None
async def clear(self) -> 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()