From e20ce6b78b6b908b78e89548e54cc002937627d3 Mon Sep 17 00:00:00 2001 From: David Rodenkirchen Date: Wed, 4 Sep 2024 12:44:11 +0200 Subject: [PATCH] add TicketBuying Feature --- config/config.example.toml | 6 +- src/EzLanManager.py | 4 +- .../components/TicketBuyCard.py | 88 ++++++++++++ src/ez_lan_manager/pages/BasePage.py | 2 +- src/ez_lan_manager/pages/BuyTicketPage.py | 127 ++++++++++++++++++ src/ez_lan_manager/pages/SeatingPlanPage.py | 24 ++++ src/ez_lan_manager/pages/__init__.py | 2 + .../services/ConfigurationService.py | 1 + .../types/ConfigurationTypes.py | 1 + 9 files changed, 250 insertions(+), 5 deletions(-) create mode 100644 src/ez_lan_manager/components/TicketBuyCard.py create mode 100644 src/ez_lan_manager/pages/BuyTicketPage.py create mode 100644 src/ez_lan_manager/pages/SeatingPlanPage.py diff --git a/config/config.example.toml b/config/config.example.toml index b4be508..656526c 100644 --- a/config/config.example.toml +++ b/config/config.example.toml @@ -25,14 +25,16 @@ [tickets] [tickets."NORMAL"] total_tickets=30 - price=2500 # Eurocent + price=2500 description="Normales Ticket" + additional_info="Berechtigt zur Nutzung eines regulären Platzes für die gesamte Dauer der LAN" is_default=true [tickets."LUXUS"] total_tickets=10 - price=4000 # Eurocent + price=3500 description="Luxus Ticket" + additional_info="Berechtigt zur Nutzung eines verbesserten Platzes. Dieser ist mit einer höheren Internet-Bandbreite und einem Sitzkissen ausgestattet." is_default=false [misc] diff --git a/src/EzLanManager.py b/src/EzLanManager.py index dbad871..50d7842 100644 --- a/src/EzLanManager.py +++ b/src/EzLanManager.py @@ -64,12 +64,12 @@ if __name__ == "__main__": Page( name="BuyTicket", page_url="buy_ticket", - build=lambda: pages.PlaceholderPage(placeholder_name="Tickets kaufen"), + build=pages.BuyTicketPage, ), Page( name="SeatingPlan", page_url="seating", - build=lambda: pages.PlaceholderPage(placeholder_name="Sitzplan"), + build=pages.SeatingPlanPage, ), Page( name="Catering", diff --git a/src/ez_lan_manager/components/TicketBuyCard.py b/src/ez_lan_manager/components/TicketBuyCard.py new file mode 100644 index 0000000..ca5d276 --- /dev/null +++ b/src/ez_lan_manager/components/TicketBuyCard.py @@ -0,0 +1,88 @@ +from functools import partial +from typing import Callable, Optional + +import rio +from rio import Component, Card, Column, Text, Row, Button, TextStyle, ProgressBar, event, Spacer + +from src.ez_lan_manager import TicketingService +from src.ez_lan_manager.services.AccountingService import AccountingService +from src.ez_lan_manager.types.Ticket import Ticket + + +class TicketBuyCard(Component): + description: str + additional_info: str + price: int + category: str + pressed_cb: Callable + is_enabled: bool + total_tickets: int + user_ticket: Optional[Ticket] + available_tickets: int = 0 + + @event.on_populate + async def async_init(self) -> None: + self.available_tickets = await self.session[TicketingService].get_available_tickets_for_category(self.category) + + def build(self) -> rio.Component: + ticket_description_style = TextStyle( + fill=self.session.theme.neutral_color, + font_size=1.2, + ) + ticket_additional_info_style = TextStyle( + fill=self.session.theme.neutral_color, + font_size=0.8 + ) + ticket_owned_style = TextStyle( + fill=self.session.theme.success_color, + font_size=0.8 + ) + + try: + progress = self.available_tickets / self.total_tickets + except ZeroDivisionError: + progress = 0 + progress_bar = ProgressBar( + progress=progress, + color=self.session.theme.success_color if progress > 0.25 else self.session.theme.danger_color, + margin_right=1, + grow_x=True + ) + + tickets_side_text = Text( + f"{self.available_tickets}/{self.total_tickets}", + align_x=1 + ) + + return Card( + Column( + Text(self.description, margin_left=1, margin_top=1, style=ticket_description_style), + Text("Du besitzt dieses Ticket!", margin_left=1, margin_top=1, style=ticket_owned_style) if self.user_ticket is not None and self.user_ticket.category == self.category else Spacer(), + Text(self.additional_info, margin_left=1, margin_top=1, style=ticket_additional_info_style, wrap=True), + Row( + progress_bar, + tickets_side_text, + margin_top=1, + margin_left=1, + margin_right=1 + ), + Row( + Text(f"{AccountingService.make_euro_string_from_int(self.price)}", margin_left=1, margin_top=1, grow_x=True), + Button( + Text("Kaufen", align_x=0.5, margin=0.4), + margin_right=1, + margin_top=1, + style="major", + shape="rounded", + on_press=partial(self.pressed_cb, self.category), + is_sensitive=self.is_enabled + ), + margin_bottom=1 + ) + ), + margin_left=3, + margin_right=3, + margin_bottom=1, + color=self.session.theme.hud_color, + corner_radius=0.2 + ) \ No newline at end of file diff --git a/src/ez_lan_manager/pages/BasePage.py b/src/ez_lan_manager/pages/BasePage.py index 1004b20..ea4e1fd 100644 --- a/src/ez_lan_manager/pages/BasePage.py +++ b/src/ez_lan_manager/pages/BasePage.py @@ -10,7 +10,7 @@ from src.ez_lan_manager.components.DesktopNavigation import DesktopNavigation class BasePage(Component): content: Component - @event.periodic(5) + @event.periodic(60) async def check_db_conn(self) -> None: is_healthy = await self.session[DatabaseService].is_healthy() if not is_healthy: diff --git a/src/ez_lan_manager/pages/BuyTicketPage.py b/src/ez_lan_manager/pages/BuyTicketPage.py new file mode 100644 index 0000000..285f332 --- /dev/null +++ b/src/ez_lan_manager/pages/BuyTicketPage.py @@ -0,0 +1,127 @@ +from asyncio import sleep +from functools import partial +from typing import Optional + +from rio import Text, Column, TextStyle, Component, event, TextInput, MultiLineTextInput, Row, Button, Card, Popup + +from src.ez_lan_manager import ConfigurationService, UserService, MailingService, AccountingService, TicketingService +from src.ez_lan_manager.components.AnimatedText import AnimatedText +from src.ez_lan_manager.components.MainViewContentBox import MainViewContentBox +from src.ez_lan_manager.components.TicketBuyCard import TicketBuyCard +from src.ez_lan_manager.pages import BasePage +from src.ez_lan_manager.services.AccountingService import InsufficientFundsError +from src.ez_lan_manager.services.TicketingService import TicketNotAvailableError, UserAlreadyHasTicketError +from src.ez_lan_manager.types.SessionStorage import SessionStorage +from src.ez_lan_manager.types.Ticket import Ticket +from src.ez_lan_manager.types.User import User + + +class BuyTicketPage(Component): + user: Optional[User] = None + user_ticket: Optional[Ticket] = None + is_popup_open: bool = False + popup_message: str = "" + is_popup_success: bool = False + is_buying_enabled: bool = False + + @event.on_populate + async def on_populate(self) -> None: + await self.session.set_title(f"{self.session[ConfigurationService].get_lan_info().name} - Ticket kaufen") + self.user = await self.session[UserService].get_user(self.session[SessionStorage].user_id) + if self.user is None: # No user logged in + self.is_buying_enabled = False + else: # User is logged in + possible_ticket = await self.session[TicketingService].get_user_ticket(self.user.user_id) + self.user_ticket = possible_ticket + if possible_ticket is not None: # User already has a ticket + self.is_buying_enabled = False + else: + self.is_buying_enabled = True + + async def on_buy_pressed(self, category: str) -> None: + if not self.user: + return + self.is_buying_enabled = False + await self.force_refresh() + + try: + t_s = self.session[TicketingService] + ticket = await t_s.purchase_ticket(self.user.user_id, category) + self.popup_message = f"Ticket erfolgreich gekauft. Deine Ticket-ID lautet: {ticket.ticket_id}." + self.is_popup_success = True + except TicketNotAvailableError: + self.popup_message = "Das ausgewählte Ticket ist nicht verfügbar." + self.is_popup_success = False + except InsufficientFundsError: + self.popup_message = "Dein Guthaben reicht nicht aus um dieses Ticket zu kaufen." + self.is_popup_success = False + except UserAlreadyHasTicketError: + self.popup_message = (f"Du besitzt bereits ein Ticket. Um dein aktuelles Ticket zu stornieren, kontaktiere bitte den Support unter " + f"{self.session[ConfigurationService].get_lan_info().organizer_mail}.") + self.is_popup_success = False + except RuntimeError: + self.popup_message = "Ein unbekannter Fehler ist aufgetreten." + self.is_popup_success = False + self.is_popup_open = True + await self.on_populate() + + + async def on_popup_close_pressed(self) -> None: + self.is_popup_open = False + self.popup_message = "" + + + def build(self) -> Component: + ticket_infos = self.session[ConfigurationService].get_ticket_info() + header = Text( + "Tickets & Preise", + style=TextStyle( + fill=self.session.theme.background_color, + font_size=1.2 + ), + margin_top=2, + margin_bottom=2, + align_x=0.5 + ) + + return BasePage( + content=Column( + MainViewContentBox( + Column( + header, + Popup( + anchor=header, + content=Column( + Text( + self.popup_message, + style=TextStyle(font_size=1.1, fill=self.session.theme.success_color if self.is_popup_success else self.session.theme.danger_color), + wrap=True, + grow_y=True, + margin=1 + ), + Button("Bestätigen", shape="rounded", grow_y=False, on_press=self.on_popup_close_pressed), + min_width=34, + min_height=10 + ), + is_open=self.is_popup_open, + position="bottom", + margin=1, + corner_radius=0.2, + color=self.session.theme.primary_color + ), + *[TicketBuyCard( + description=t.description, + additional_info=t.additional_info, + price=t.price, + category=t.category, + pressed_cb=self.on_buy_pressed, + is_enabled=self.is_buying_enabled, + total_tickets=t.total_tickets, + user_ticket=self.user_ticket + ) for t in ticket_infos] + ), + ), + align_y=0 + ), + grow_x=True + ) diff --git a/src/ez_lan_manager/pages/SeatingPlanPage.py b/src/ez_lan_manager/pages/SeatingPlanPage.py new file mode 100644 index 0000000..c95a4e1 --- /dev/null +++ b/src/ez_lan_manager/pages/SeatingPlanPage.py @@ -0,0 +1,24 @@ +from rio import Text, Column, TextStyle, Component, event, TextInput, MultiLineTextInput, Row, Button + +from src.ez_lan_manager import ConfigurationService, UserService, MailingService +from src.ez_lan_manager.components.AnimatedText import AnimatedText +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 +from src.ez_lan_manager.types.User import User + + +class SeatingPlanPage(Component): + @event.on_populate + async def on_populate(self) -> None: + await self.session.set_title(f"{self.session[ConfigurationService].get_lan_info().name} - Sitzplan") + + def build(self) -> Component: + return BasePage( + content=Column( + MainViewContentBox(), + MainViewContentBox(), + align_y=0 + ), + grow_x=True + ) diff --git a/src/ez_lan_manager/pages/__init__.py b/src/ez_lan_manager/pages/__init__.py index 1950e63..92d6210 100644 --- a/src/ez_lan_manager/pages/__init__.py +++ b/src/ez_lan_manager/pages/__init__.py @@ -13,3 +13,5 @@ from .TournamentsPage import TournamentsPage from .GuestsPage import GuestsPage from .CateringPage import CateringPage from .DbErrorPage import DbErrorPage +from .SeatingPlanPage import SeatingPlanPage +from .BuyTicketPage import BuyTicketPage diff --git a/src/ez_lan_manager/services/ConfigurationService.py b/src/ez_lan_manager/services/ConfigurationService.py index f14f52a..c648d5b 100644 --- a/src/ez_lan_manager/services/ConfigurationService.py +++ b/src/ez_lan_manager/services/ConfigurationService.py @@ -90,6 +90,7 @@ class ConfigurationService: total_tickets=self._config["tickets"][value]["total_tickets"], price=self._config["tickets"][value]["price"], description=self._config["tickets"][value]["description"], + additional_info=self._config["tickets"][value]["additional_info"], is_default=self._config["tickets"][value]["is_default"] ) for value in self._config["tickets"]]) except KeyError as e: diff --git a/src/ez_lan_manager/types/ConfigurationTypes.py b/src/ez_lan_manager/types/ConfigurationTypes.py index 9fb073a..d54e07d 100644 --- a/src/ez_lan_manager/types/ConfigurationTypes.py +++ b/src/ez_lan_manager/types/ConfigurationTypes.py @@ -20,6 +20,7 @@ class TicketInfo: total_tickets: int price: int description: str + additional_info: str is_default: bool @dataclass(frozen=True)