add ticketing
This commit is contained in:
+2
-2
@@ -27,14 +27,14 @@
|
|||||||
price="20.00"
|
price="20.00"
|
||||||
description="Normales Ticket"
|
description="Normales Ticket"
|
||||||
additional_info="Berechtigt zur Nutzung eines regulären Platzes für die gesamte Dauer der LAN"
|
additional_info="Berechtigt zur Nutzung eines regulären Platzes für die gesamte Dauer der LAN"
|
||||||
is_default=true
|
can_be_sold=true
|
||||||
|
|
||||||
[tickets."DELUXE"]
|
[tickets."DELUXE"]
|
||||||
total_tickets=30
|
total_tickets=30
|
||||||
price="25.00"
|
price="25.00"
|
||||||
description="Deluxe Ticket"
|
description="Deluxe Ticket"
|
||||||
additional_info="Wie das normale Ticket, aber mit doppelt so breitem Tisch (160cm)"
|
additional_info="Wie das normale Ticket, aber mit doppelt so breitem Tisch (160cm)"
|
||||||
is_default=false
|
can_be_sold=true
|
||||||
|
|
||||||
[receipt_printing]
|
[receipt_printing]
|
||||||
host="10.0.0.103"
|
host="10.0.0.103"
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
|
from typing import Optional
|
||||||
|
|
||||||
from rio import Component, Rectangle, Column, Text, Row, PointerEventListener, TextInput
|
from rio import Component, Rectangle, Column, Text, Row, PointerEventListener, TextInput
|
||||||
from rio.event import on_populate
|
from rio.event import on_populate
|
||||||
|
|
||||||
from elm.types import UserSession
|
from elm.types import UserSession, Ticket
|
||||||
from elm.components import ElmButton
|
from elm.components import ElmButton
|
||||||
from elm.services import UserService
|
from elm.services import UserService
|
||||||
|
|
||||||
@@ -13,14 +15,19 @@ class AccountInfoBox(Component):
|
|||||||
account_info_is_error: bool = False
|
account_info_is_error: bool = False
|
||||||
password_input_blocked: bool = False
|
password_input_blocked: bool = False
|
||||||
password_change_in_progress: bool = False
|
password_change_in_progress: bool = False
|
||||||
|
ticket: Optional[Ticket] = None
|
||||||
|
|
||||||
@on_populate
|
@on_populate
|
||||||
async def on_populate(self) -> None:
|
async def on_populate(self) -> None:
|
||||||
|
try:
|
||||||
user = await self.session[UserService].get_user(self.session[UserSession].user_name)
|
user = await self.session[UserService].get_user(self.session[UserSession].user_name)
|
||||||
if user:
|
if user:
|
||||||
self.mail = user.user_mail
|
self.mail = user.user_mail
|
||||||
|
self.ticket = await Ticket.find_one({"owner.$id": user.id})
|
||||||
else:
|
else:
|
||||||
self.session.navigate_to("./login")
|
self.session.navigate_to("./login")
|
||||||
|
except KeyError:
|
||||||
|
self.session.navigate_to("./login")
|
||||||
|
|
||||||
async def set_new_password(self) -> None:
|
async def set_new_password(self) -> None:
|
||||||
self.password_change_in_progress = True
|
self.password_change_in_progress = True
|
||||||
@@ -55,6 +62,9 @@ class AccountInfoBox(Component):
|
|||||||
|
|
||||||
def build(self) -> Component:
|
def build(self) -> Component:
|
||||||
row_col = Row
|
row_col = Row
|
||||||
|
ticket_text = "-"
|
||||||
|
if self.ticket:
|
||||||
|
ticket_text = self.ticket.category
|
||||||
if self.session.is_mobile():
|
if self.session.is_mobile():
|
||||||
row_col = Column
|
row_col = Column
|
||||||
|
|
||||||
@@ -75,13 +85,13 @@ class AccountInfoBox(Component):
|
|||||||
row_col(
|
row_col(
|
||||||
PointerEventListener(
|
PointerEventListener(
|
||||||
Rectangle(
|
Rectangle(
|
||||||
content=Row(Text("Ticket:", margin=1, overflow="wrap", justify="left"), Text("-", margin=1, overflow="wrap", justify="right")),
|
content=Row(Text("Ticket:", margin=1, overflow="wrap", justify="left"), Text(ticket_text, margin=1, overflow="wrap", justify="right")),
|
||||||
fill=self.session.theme.danger_color_dark,
|
fill=self.session.theme.success_color if self.ticket else self.session.theme.danger_color_dark,
|
||||||
stroke_width=0.1,
|
stroke_width=0.1,
|
||||||
stroke_color=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.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_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,
|
transition_time=0.2,
|
||||||
cursor="pointer"
|
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"
|
style: Literal["small", "normal"] = "normal"
|
||||||
wrap: bool = False
|
wrap: bool = False
|
||||||
is_loading: bool = False
|
is_loading: bool = False
|
||||||
|
is_disabled: bool = False
|
||||||
|
|
||||||
async def _on_press(self, event: PointerEvent) -> None:
|
async def _on_press(self, event: PointerEvent) -> None:
|
||||||
|
if self.is_disabled:
|
||||||
|
return
|
||||||
if iscoroutinefunction(self.on_press):
|
if iscoroutinefunction(self.on_press):
|
||||||
await self.on_press()
|
await self.on_press()
|
||||||
else:
|
else:
|
||||||
@@ -54,10 +57,10 @@ class ElmButton(Component):
|
|||||||
stroke_width=0.1,
|
stroke_width=0.1,
|
||||||
stroke_color=self.session.theme.secondary_color,
|
stroke_color=self.session.theme.secondary_color,
|
||||||
hover_stroke_width=0.1,
|
hover_stroke_width=0.1,
|
||||||
hover_stroke_color=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=self.session.theme.hud_color,
|
hover_fill=Color.TRANSPARENT if self.is_disabled else self.session.theme.hud_color,
|
||||||
transition_time=0,
|
transition_time=0,
|
||||||
cursor="pointer"
|
cursor="not-allowed" if self.is_disabled else "pointer"
|
||||||
),
|
),
|
||||||
on_press=self._on_press
|
on_press=self._on_press
|
||||||
)
|
)
|
||||||
@@ -6,3 +6,4 @@ from .ElmButton import ElmButton
|
|||||||
from .AvatarEditBox import AvatarEditBox
|
from .AvatarEditBox import AvatarEditBox
|
||||||
from .AccountInfoBox import AccountInfoBox
|
from .AccountInfoBox import AccountInfoBox
|
||||||
from .PersonalInfoBox import PersonalInfoBox
|
from .PersonalInfoBox import PersonalInfoBox
|
||||||
|
from .BuyTicketBox import BuyTicketBox
|
||||||
|
|||||||
@@ -1,67 +1,17 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from rio import Component, Column, Row, Text, Spacer, page, Color, TextStyle, Rectangle, TextInput, ProgressBar, Dict
|
from rio import Component, Column, Row, page
|
||||||
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 elm.services import ConfigurationService
|
||||||
|
from elm.components import BuyTicketBox
|
||||||
|
|
||||||
@page(name="Tickets", url_segment="tickets")
|
@page(name="Tickets", url_segment="tickets")
|
||||||
class TicketsPage(Component):
|
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:
|
def build(self) -> Component:
|
||||||
row_col = Column if self.session.is_mobile() else Row
|
row_col = Column if self.session.is_mobile() else Row
|
||||||
ticket_boxes = []
|
ticket_boxes = []
|
||||||
for ticket_info in self.session[ConfigurationService].get_ticket_info():
|
for ticket_info in self.session[ConfigurationService].get_ticket_info():
|
||||||
ticket_boxes.append(
|
ticket_boxes.append(BuyTicketBox(ticket_info=ticket_info))
|
||||||
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()
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
return row_col(
|
return row_col(
|
||||||
*ticket_boxes,
|
*ticket_boxes,
|
||||||
|
|||||||
@@ -54,7 +54,8 @@ class ConfigurationService:
|
|||||||
total_tickets=self._config["tickets"][value]["total_tickets"],
|
total_tickets=self._config["tickets"][value]["total_tickets"],
|
||||||
price=Decimal(self._config["tickets"][value]["price"]),
|
price=Decimal(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"]
|
additional_info=self._config["tickets"][value]["additional_info"],
|
||||||
|
can_be_sold=self._config["tickets"][value]["can_be_sold"]
|
||||||
) for value in self._config["tickets"]])
|
) for value in self._config["tickets"]])
|
||||||
except KeyError as e:
|
except KeyError as e:
|
||||||
logger.debug(e)
|
logger.debug(e)
|
||||||
|
|||||||
@@ -54,3 +54,4 @@ class TicketInfo:
|
|||||||
price: Decimal
|
price: Decimal
|
||||||
description: str
|
description: str
|
||||||
additional_info: str
|
additional_info: str
|
||||||
|
can_be_sold: bool
|
||||||
|
|||||||
@@ -1,10 +1,16 @@
|
|||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
from enum import Enum
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
from beanie import Document, Link
|
from beanie import Document, Link
|
||||||
|
|
||||||
from elm.types import User
|
from elm.types import User
|
||||||
|
|
||||||
|
class TicketState(Enum):
|
||||||
|
AVAILABLE = 1
|
||||||
|
SOLD_OUT = 2
|
||||||
|
UNAVAILABLE = 3
|
||||||
|
|
||||||
|
|
||||||
class Ticket(Document):
|
class Ticket(Document):
|
||||||
category: str
|
category: str
|
||||||
|
|||||||
@@ -2,4 +2,4 @@ from .User import User
|
|||||||
from .UserSession import UserSession
|
from .UserSession import UserSession
|
||||||
from .ConfigurationTypes import *
|
from .ConfigurationTypes import *
|
||||||
from .Transaction import Transaction
|
from .Transaction import Transaction
|
||||||
from .Ticket import Ticket
|
from .Ticket import Ticket, TicketState
|
||||||
|
|||||||
Reference in New Issue
Block a user