add TicketBuying Feature

This commit is contained in:
David Rodenkirchen 2024-09-04 12:44:11 +02:00
parent 5ed1230fde
commit e20ce6b78b
9 changed files with 250 additions and 5 deletions

View File

@ -25,14 +25,16 @@
[tickets] [tickets]
[tickets."NORMAL"] [tickets."NORMAL"]
total_tickets=30 total_tickets=30
price=2500 # Eurocent price=2500
description="Normales Ticket" description="Normales Ticket"
additional_info="Berechtigt zur Nutzung eines regulären Platzes für die gesamte Dauer der LAN"
is_default=true is_default=true
[tickets."LUXUS"] [tickets."LUXUS"]
total_tickets=10 total_tickets=10
price=4000 # Eurocent price=3500
description="Luxus Ticket" 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 is_default=false
[misc] [misc]

View File

@ -64,12 +64,12 @@ if __name__ == "__main__":
Page( Page(
name="BuyTicket", name="BuyTicket",
page_url="buy_ticket", page_url="buy_ticket",
build=lambda: pages.PlaceholderPage(placeholder_name="Tickets kaufen"), build=pages.BuyTicketPage,
), ),
Page( Page(
name="SeatingPlan", name="SeatingPlan",
page_url="seating", page_url="seating",
build=lambda: pages.PlaceholderPage(placeholder_name="Sitzplan"), build=pages.SeatingPlanPage,
), ),
Page( Page(
name="Catering", name="Catering",

View File

@ -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
)

View File

@ -10,7 +10,7 @@ from src.ez_lan_manager.components.DesktopNavigation import DesktopNavigation
class BasePage(Component): class BasePage(Component):
content: Component content: Component
@event.periodic(5) @event.periodic(60)
async def check_db_conn(self) -> None: async def check_db_conn(self) -> None:
is_healthy = await self.session[DatabaseService].is_healthy() is_healthy = await self.session[DatabaseService].is_healthy()
if not is_healthy: if not is_healthy:

View File

@ -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
)

View File

@ -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
)

View File

@ -13,3 +13,5 @@ from .TournamentsPage import TournamentsPage
from .GuestsPage import GuestsPage from .GuestsPage import GuestsPage
from .CateringPage import CateringPage from .CateringPage import CateringPage
from .DbErrorPage import DbErrorPage from .DbErrorPage import DbErrorPage
from .SeatingPlanPage import SeatingPlanPage
from .BuyTicketPage import BuyTicketPage

View File

@ -90,6 +90,7 @@ class ConfigurationService:
total_tickets=self._config["tickets"][value]["total_tickets"], total_tickets=self._config["tickets"][value]["total_tickets"],
price=self._config["tickets"][value]["price"], price=self._config["tickets"][value]["price"],
description=self._config["tickets"][value]["description"], description=self._config["tickets"][value]["description"],
additional_info=self._config["tickets"][value]["additional_info"],
is_default=self._config["tickets"][value]["is_default"] is_default=self._config["tickets"][value]["is_default"]
) for value in self._config["tickets"]]) ) for value in self._config["tickets"]])
except KeyError as e: except KeyError as e:

View File

@ -20,6 +20,7 @@ class TicketInfo:
total_tickets: int total_tickets: int
price: int price: int
description: str description: str
additional_info: str
is_default: bool is_default: bool
@dataclass(frozen=True) @dataclass(frozen=True)