From b00a819325d917e3bc182f77d8f5ad2020b634c4 Mon Sep 17 00:00:00 2001 From: David Rodenkirchen Date: Wed, 28 Aug 2024 11:59:25 +0200 Subject: [PATCH] Refactor logged-in and out messaging, Prepare Catering Module with shopping cart --- src/EzLanManager.py | 8 +- .../components/CateringCartItem.py | 30 ++++ .../components/DesktopNavigation.py | 10 +- src/ez_lan_manager/components/LoginBox.py | 4 +- src/ez_lan_manager/components/UserInfoBox.py | 18 ++- src/ez_lan_manager/pages/CateringPage.py | 131 ++++++++++++++++++ src/ez_lan_manager/pages/Logout.py | 30 ---- src/ez_lan_manager/pages/__init__.py | 2 +- .../services/CateringService.py | 24 ++++ src/ez_lan_manager/types/SessionStorage.py | 22 ++- 10 files changed, 227 insertions(+), 52 deletions(-) create mode 100644 src/ez_lan_manager/components/CateringCartItem.py create mode 100644 src/ez_lan_manager/pages/CateringPage.py delete mode 100644 src/ez_lan_manager/pages/Logout.py diff --git a/src/EzLanManager.py b/src/EzLanManager.py index 64b25c3..d8e31d8 100644 --- a/src/EzLanManager.py +++ b/src/EzLanManager.py @@ -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, diff --git a/src/ez_lan_manager/components/CateringCartItem.py b/src/ez_lan_manager/components/CateringCartItem.py new file mode 100644 index 0000000..6298995 --- /dev/null +++ b/src/ez_lan_manager/components/CateringCartItem.py @@ -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) + ) diff --git a/src/ez_lan_manager/components/DesktopNavigation.py b/src/ez_lan_manager/components/DesktopNavigation.py index 777839b..0d03a77 100644 --- a/src/ez_lan_manager/components/DesktopNavigation.py +++ b/src/ez_lan_manager/components/DesktopNavigation.py @@ -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"), diff --git a/src/ez_lan_manager/components/LoginBox.py b/src/ez_lan_manager/components/LoginBox.py index 2060932..56bde83 100644 --- a/src/ez_lan_manager/components/LoginBox.py +++ b/src/ez_lan_manager/components/LoginBox.py @@ -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 diff --git a/src/ez_lan_manager/components/UserInfoBox.py b/src/ez_lan_manager/components/UserInfoBox.py index a34448b..40b0e7f 100644 --- a/src/ez_lan_manager/components/UserInfoBox.py +++ b/src/ez_lan_manager/components/UserInfoBox.py @@ -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, diff --git a/src/ez_lan_manager/pages/CateringPage.py b/src/ez_lan_manager/pages/CateringPage.py new file mode 100644 index 0000000..2400889 --- /dev/null +++ b/src/ez_lan_manager/pages/CateringPage.py @@ -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 + ) + ) diff --git a/src/ez_lan_manager/pages/Logout.py b/src/ez_lan_manager/pages/Logout.py deleted file mode 100644 index 2d5dde3..0000000 --- a/src/ez_lan_manager/pages/Logout.py +++ /dev/null @@ -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, - ) - ) diff --git a/src/ez_lan_manager/pages/__init__.py b/src/ez_lan_manager/pages/__init__.py index e4d2c66..fddb2d2 100644 --- a/src/ez_lan_manager/pages/__init__.py +++ b/src/ez_lan_manager/pages/__init__.py @@ -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 diff --git a/src/ez_lan_manager/services/CateringService.py b/src/ez_lan_manager/services/CateringService.py index df8f771..6287e83 100644 --- a/src/ez_lan_manager/services/CateringService.py +++ b/src/ez_lan_manager/services/CateringService.py @@ -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 [] diff --git a/src/ez_lan_manager/types/SessionStorage.py b/src/ez_lan_manager/types/SessionStorage.py index b8199c6..ede4b78 100644 --- a/src/ez_lan_manager/types/SessionStorage.py +++ b/src/ez_lan_manager/types/SessionStorage.py @@ -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()