add ticketing
This commit is contained in:
@@ -1,7 +1,9 @@
|
||||
from typing import Optional
|
||||
|
||||
from rio import Component, Rectangle, Column, Text, Row, PointerEventListener, TextInput
|
||||
from rio.event import on_populate
|
||||
|
||||
from elm.types import UserSession
|
||||
from elm.types import UserSession, Ticket
|
||||
from elm.components import ElmButton
|
||||
from elm.services import UserService
|
||||
|
||||
@@ -13,13 +15,18 @@ class AccountInfoBox(Component):
|
||||
account_info_is_error: bool = False
|
||||
password_input_blocked: bool = False
|
||||
password_change_in_progress: bool = False
|
||||
ticket: Optional[Ticket] = None
|
||||
|
||||
@on_populate
|
||||
async def on_populate(self) -> None:
|
||||
user = await self.session[UserService].get_user(self.session[UserSession].user_name)
|
||||
if user:
|
||||
self.mail = user.user_mail
|
||||
else:
|
||||
try:
|
||||
user = await self.session[UserService].get_user(self.session[UserSession].user_name)
|
||||
if user:
|
||||
self.mail = user.user_mail
|
||||
self.ticket = await Ticket.find_one({"owner.$id": user.id})
|
||||
else:
|
||||
self.session.navigate_to("./login")
|
||||
except KeyError:
|
||||
self.session.navigate_to("./login")
|
||||
|
||||
async def set_new_password(self) -> None:
|
||||
@@ -55,6 +62,9 @@ class AccountInfoBox(Component):
|
||||
|
||||
def build(self) -> Component:
|
||||
row_col = Row
|
||||
ticket_text = "-"
|
||||
if self.ticket:
|
||||
ticket_text = self.ticket.category
|
||||
if self.session.is_mobile():
|
||||
row_col = Column
|
||||
|
||||
@@ -75,13 +85,13 @@ class AccountInfoBox(Component):
|
||||
row_col(
|
||||
PointerEventListener(
|
||||
Rectangle(
|
||||
content=Row(Text("Ticket:", margin=1, overflow="wrap", justify="left"), Text("-", margin=1, overflow="wrap", justify="right")),
|
||||
fill=self.session.theme.danger_color_dark,
|
||||
content=Row(Text("Ticket:", margin=1, overflow="wrap", justify="left"), Text(ticket_text, margin=1, overflow="wrap", justify="right")),
|
||||
fill=self.session.theme.success_color if self.ticket else self.session.theme.danger_color_dark,
|
||||
stroke_width=0.1,
|
||||
stroke_color=self.session.theme.danger_color,
|
||||
hover_fill=self.session.theme.danger_color,
|
||||
stroke_color=self.session.theme.success_color if self.ticket else self.session.theme.danger_color,
|
||||
hover_fill=self.session.theme.success_color if self.ticket else self.session.theme.danger_color,
|
||||
hover_stroke_width=0.1,
|
||||
hover_stroke_color=self.session.theme.danger_color_dark,
|
||||
hover_stroke_color=self.session.theme.success_color if self.ticket else self.session.theme.danger_color_dark,
|
||||
transition_time=0.2,
|
||||
cursor="pointer"
|
||||
),
|
||||
|
||||
@@ -0,0 +1,137 @@
|
||||
from asyncio import sleep
|
||||
from datetime import datetime
|
||||
from typing import Optional
|
||||
|
||||
from rio import Component, Column, Row, Text, Spacer, Rectangle, ProgressBar, Tooltip
|
||||
from rio.event import on_populate
|
||||
|
||||
from elm.services import AccountingService
|
||||
from elm.components import ElmButton
|
||||
from elm.services.AccountingService import InsufficientFundsError
|
||||
from elm.types import Ticket, UserSession, User, TicketInfo, TicketState
|
||||
|
||||
|
||||
class BuyTicketBox(Component):
|
||||
ticket_info: TicketInfo
|
||||
user_ticket: Optional[Ticket] = None
|
||||
ticket_state: TicketState = TicketState.UNAVAILABLE
|
||||
sold_tickets: int = 0
|
||||
purchase_in_process: bool = False
|
||||
purchase_status: str = ""
|
||||
purchase_error_message: str = ""
|
||||
|
||||
@on_populate
|
||||
async def on_populate(self) -> None:
|
||||
self.sold_tickets = len(await Ticket.find_many(Ticket.category == self.ticket_info.category).to_list())
|
||||
if self.sold_tickets >= self.ticket_info.total_tickets:
|
||||
self.ticket_state = TicketState.SOLD_OUT
|
||||
elif self.ticket_info.can_be_sold:
|
||||
self.ticket_state = TicketState.AVAILABLE
|
||||
else:
|
||||
self.ticket_state = TicketState.UNAVAILABLE
|
||||
self.user_ticket = await self.get_user_ticket()
|
||||
|
||||
async def get_user_ticket(self) -> Optional[Ticket]:
|
||||
try:
|
||||
user = await User.find_one(User.user_name == self.session[UserSession].user_name)
|
||||
if not user:
|
||||
return None
|
||||
return await Ticket.find_one({"owner.$id": user.id})
|
||||
except KeyError:
|
||||
return None
|
||||
|
||||
def is_logged_in(self) -> bool:
|
||||
try:
|
||||
return bool(self.session[UserSession].user_name)
|
||||
except KeyError:
|
||||
return False
|
||||
|
||||
def get_available_tickets(self) -> int:
|
||||
return self.ticket_info.total_tickets - self.sold_tickets
|
||||
|
||||
async def buy_ticket(self) -> None:
|
||||
self.purchase_in_process = True
|
||||
self.purchase_status = "Ticket wird gekauft..."
|
||||
await sleep(1)
|
||||
|
||||
try:
|
||||
user = await User.find_one(User.user_name == self.session[UserSession].user_name)
|
||||
if not user:
|
||||
raise KeyError
|
||||
except KeyError:
|
||||
self.session.navigate_to("./login")
|
||||
return
|
||||
|
||||
try:
|
||||
await self.session[AccountingService].remove_balance(user.user_name, self.ticket_info.price, f"Ticketkauf - {self.ticket_info.category}")
|
||||
except InsufficientFundsError:
|
||||
self.purchase_in_process = False
|
||||
self.purchase_status = ""
|
||||
self.purchase_error_message = "Ungenügendes Guthaben!"
|
||||
return
|
||||
|
||||
new_ticket = Ticket(category=self.ticket_info.category, purchase_date=datetime.now(), owner=user)
|
||||
await new_ticket.save()
|
||||
self.user_ticket = new_ticket
|
||||
self.purchase_in_process = False
|
||||
self.purchase_status = ""
|
||||
self.sold_tickets = len(await Ticket.find_many(Ticket.category == self.ticket_info.category).to_list())
|
||||
|
||||
|
||||
def build(self) -> Component:
|
||||
ticket_owned_text = ""
|
||||
if self.purchase_error_message:
|
||||
button_row_content = Row(
|
||||
Text(self.purchase_error_message, justify="center", margin=0.5, fill=self.session.theme.danger_color, overflow="wrap"),
|
||||
)
|
||||
elif self.purchase_in_process:
|
||||
button_row_content = Row(
|
||||
Text(self.purchase_status, justify="center", margin=0.5, overflow="wrap")
|
||||
)
|
||||
else:
|
||||
if self.user_ticket:
|
||||
button_row_content = Tooltip(anchor=ElmButton(text="Kaufen", is_disabled=True), tip="Du hast bereits ein Ticket")
|
||||
ticket_owned_text = "Du besitzt dieses Ticket!" if self.user_ticket.category == self.ticket_info.category else ""
|
||||
elif self.is_logged_in():
|
||||
if self.ticket_state == TicketState.UNAVAILABLE:
|
||||
button_row_content = Tooltip(anchor=ElmButton(text="Kaufen", is_disabled=True), tip="Aktuell nicht verfügbar")
|
||||
elif self.ticket_state == TicketState.SOLD_OUT:
|
||||
button_row_content = Tooltip(anchor=ElmButton(text="Kaufen", is_disabled=True), tip="Ausverkauft")
|
||||
elif self.ticket_state == TicketState.AVAILABLE:
|
||||
button_row_content = ElmButton(text="Kaufen", on_press=self.buy_ticket)
|
||||
else:
|
||||
button_row_content = Tooltip(anchor=ElmButton(text="Kaufen", is_disabled=True), tip="Entwickler hauen!")
|
||||
else:
|
||||
button_row_content = ElmButton(text="Kaufen", on_press=lambda: self.session.navigate_to("./login"))
|
||||
return Column(
|
||||
Rectangle(
|
||||
content=Column(
|
||||
Rectangle(
|
||||
content=Rectangle(
|
||||
content=Row(
|
||||
Text(self.ticket_info.description, margin=0.5, selectable=False, overflow="wrap", grow_x=True),
|
||||
Text(self.session[AccountingService].make_euro_string_from_decimal(self.ticket_info.price), justify="right", margin_right=0.5, fill=self.session.theme.warning_color)
|
||||
),
|
||||
fill=self.session.theme.header_box_background_color,
|
||||
margin=0.4
|
||||
),
|
||||
stroke_width=0.1,
|
||||
stroke_color=self.session.theme.box_border_color,
|
||||
),
|
||||
Column(
|
||||
Text(self.ticket_info.additional_info, overflow="wrap", margin_bottom=1),
|
||||
Text(ticket_owned_text, margin_bottom=3, overflow="wrap", fill=self.session.theme.success_color),
|
||||
Row(Text("Verfügbar:", font_size=0.8 if self.session.is_mobile() else 1), Text(f"{self.get_available_tickets()} / {self.ticket_info.total_tickets}", justify="right", font_size=0.8 if self.session.is_mobile() else 1)),
|
||||
ProgressBar(progress=self.get_available_tickets() / self.ticket_info.total_tickets, min_height=1),
|
||||
button_row_content,
|
||||
margin=1,
|
||||
spacing=1
|
||||
),
|
||||
Spacer()
|
||||
),
|
||||
fill=self.session.theme.box_color,
|
||||
stroke_width=0.1,
|
||||
stroke_color=self.session.theme.box_border_color
|
||||
),
|
||||
Spacer()
|
||||
)
|
||||
@@ -17,8 +17,11 @@ class ElmButton(Component):
|
||||
style: Literal["small", "normal"] = "normal"
|
||||
wrap: bool = False
|
||||
is_loading: bool = False
|
||||
is_disabled: bool = False
|
||||
|
||||
async def _on_press(self, event: PointerEvent) -> None:
|
||||
if self.is_disabled:
|
||||
return
|
||||
if iscoroutinefunction(self.on_press):
|
||||
await self.on_press()
|
||||
else:
|
||||
@@ -54,10 +57,10 @@ class ElmButton(Component):
|
||||
stroke_width=0.1,
|
||||
stroke_color=self.session.theme.secondary_color,
|
||||
hover_stroke_width=0.1,
|
||||
hover_stroke_color=self.session.theme.hud_color,
|
||||
hover_fill=self.session.theme.hud_color,
|
||||
hover_stroke_color=self.session.theme.secondary_color if self.is_disabled else self.session.theme.hud_color,
|
||||
hover_fill=Color.TRANSPARENT if self.is_disabled else self.session.theme.hud_color,
|
||||
transition_time=0,
|
||||
cursor="pointer"
|
||||
cursor="not-allowed" if self.is_disabled else "pointer"
|
||||
),
|
||||
on_press=self._on_press
|
||||
)
|
||||
)
|
||||
|
||||
@@ -6,3 +6,4 @@ from .ElmButton import ElmButton
|
||||
from .AvatarEditBox import AvatarEditBox
|
||||
from .AccountInfoBox import AccountInfoBox
|
||||
from .PersonalInfoBox import PersonalInfoBox
|
||||
from .BuyTicketBox import BuyTicketBox
|
||||
|
||||
@@ -1,70 +1,20 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from rio import Component, Column, Row, Text, Spacer, page, Color, TextStyle, Rectangle, TextInput, ProgressBar, Dict
|
||||
from rio.event import on_populate
|
||||
|
||||
from elm.services import ConfigurationService, AccountingService
|
||||
from elm.components import LanCountdownBox, LanInfoBox, LandingPageBoxFull, LandingPageBoxHalf, ElmButton
|
||||
from elm.types import Ticket
|
||||
from rio import Component, Column, Row, page
|
||||
|
||||
from elm.services import ConfigurationService
|
||||
from elm.components import BuyTicketBox
|
||||
|
||||
@page(name="Tickets", url_segment="tickets")
|
||||
class TicketsPage(Component):
|
||||
sold_tickets_by_category: Dict[str, int] = Dict()
|
||||
|
||||
"""
|
||||
ToDo: Implement conditional ticket buying (check login!)
|
||||
"""
|
||||
|
||||
@on_populate
|
||||
async def on_populate(self) -> None:
|
||||
for ticket_info in self.session[ConfigurationService].get_ticket_info():
|
||||
self.sold_tickets_by_category[ticket_info.category] = len(await Ticket.find_many(Ticket.category == ticket_info.category).to_list())
|
||||
|
||||
def get_available_tickets_by_category(self, category: str, total_tickets: int) -> int:
|
||||
return total_tickets - self.sold_tickets_by_category.get(category, 0)
|
||||
|
||||
def build(self) -> Component:
|
||||
row_col = Column if self.session.is_mobile() else Row
|
||||
ticket_boxes = []
|
||||
for ticket_info in self.session[ConfigurationService].get_ticket_info():
|
||||
ticket_boxes.append(
|
||||
Column(
|
||||
Rectangle(
|
||||
content=Column(
|
||||
Rectangle(
|
||||
content=Rectangle(
|
||||
content=Row(
|
||||
Text(ticket_info.description, margin=0.5, selectable=False, overflow="wrap", grow_x=True),
|
||||
Text(self.session[AccountingService].make_euro_string_from_decimal(ticket_info.price), justify="right", margin_right=0.5, fill=self.session.theme.warning_color)
|
||||
),
|
||||
fill=self.session.theme.header_box_background_color,
|
||||
margin=0.4
|
||||
),
|
||||
stroke_width=0.1,
|
||||
stroke_color=self.session.theme.box_border_color,
|
||||
),
|
||||
Column(
|
||||
Text(ticket_info.additional_info, overflow="wrap", margin_bottom=1),
|
||||
Text("Du besitzt dieses Ticket!", margin_bottom=3, overflow="wrap", fill=self.session.theme.success_color),
|
||||
Row(Text("Verfügbar:", font_size=0.8 if self.session.is_mobile() else 1), Text(f"{self.get_available_tickets_by_category(ticket_info.category, ticket_info.total_tickets)} / {ticket_info.total_tickets}", justify="right", font_size=0.8 if self.session.is_mobile() else 1)),
|
||||
ProgressBar(progress=self.get_available_tickets_by_category(ticket_info.category, ticket_info.total_tickets) / ticket_info.total_tickets, min_height=1),
|
||||
ElmButton(text="Kaufen"),
|
||||
margin=1,
|
||||
spacing=1
|
||||
),
|
||||
Spacer()
|
||||
),
|
||||
fill=self.session.theme.box_color,
|
||||
stroke_width=0.1,
|
||||
stroke_color=self.session.theme.box_border_color
|
||||
),
|
||||
Spacer()
|
||||
)
|
||||
)
|
||||
ticket_boxes.append(BuyTicketBox(ticket_info=ticket_info))
|
||||
|
||||
return row_col(
|
||||
*ticket_boxes,
|
||||
spacing=1,
|
||||
margin=1
|
||||
)
|
||||
)
|
||||
|
||||
@@ -54,7 +54,8 @@ class ConfigurationService:
|
||||
total_tickets=self._config["tickets"][value]["total_tickets"],
|
||||
price=Decimal(self._config["tickets"][value]["price"]),
|
||||
description=self._config["tickets"][value]["description"],
|
||||
additional_info=self._config["tickets"][value]["additional_info"]
|
||||
additional_info=self._config["tickets"][value]["additional_info"],
|
||||
can_be_sold=self._config["tickets"][value]["can_be_sold"]
|
||||
) for value in self._config["tickets"]])
|
||||
except KeyError as e:
|
||||
logger.debug(e)
|
||||
|
||||
@@ -54,3 +54,4 @@ class TicketInfo:
|
||||
price: Decimal
|
||||
description: str
|
||||
additional_info: str
|
||||
can_be_sold: bool
|
||||
|
||||
@@ -1,10 +1,16 @@
|
||||
from datetime import datetime
|
||||
from enum import Enum
|
||||
from typing import Optional
|
||||
|
||||
from beanie import Document, Link
|
||||
|
||||
from elm.types import User
|
||||
|
||||
class TicketState(Enum):
|
||||
AVAILABLE = 1
|
||||
SOLD_OUT = 2
|
||||
UNAVAILABLE = 3
|
||||
|
||||
|
||||
class Ticket(Document):
|
||||
category: str
|
||||
|
||||
@@ -2,4 +2,4 @@ from .User import User
|
||||
from .UserSession import UserSession
|
||||
from .ConfigurationTypes import *
|
||||
from .Transaction import Transaction
|
||||
from .Ticket import Ticket
|
||||
from .Ticket import Ticket, TicketState
|
||||
|
||||
Reference in New Issue
Block a user