prerelease/0.6.0 (#1)
Co-authored-by: David Rodenkirchen <drodenkirchen@linetco.com> Reviewed-on: #1
This commit was merged in pull request #1.
This commit is contained in:
@@ -0,0 +1,224 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from asyncio import sleep
|
||||
from functools import partial
|
||||
from typing import Optional
|
||||
from decimal import Decimal
|
||||
|
||||
from beanie import PydanticObjectId
|
||||
from rio import Component, Column, Row, Text, Spacer, page, Rectangle, GuardEvent, FlowContainer, List, PointerEventListener, Overlay, Link, Switch, SwitchChangeEvent
|
||||
from rio.event import on_populate
|
||||
|
||||
from elm.types import UserSession, User, Seat
|
||||
from elm.services import AccountingService, ReceiptPrintingService
|
||||
from elm.components import ElmButton
|
||||
from elm.types.CateringTypes import CateringOrder, CateringOrderStatus, CateringMenuItem, CateringMenuItemCategory
|
||||
|
||||
logger = logging.getLogger(__name__.split(".")[-1])
|
||||
|
||||
def catering_admin_page_guard(event: GuardEvent) -> Optional[str]:
|
||||
try:
|
||||
if event.session[UserSession].is_team_member:
|
||||
return None
|
||||
return "/"
|
||||
except KeyError:
|
||||
return "/"
|
||||
|
||||
@page(name="Cateringverwaltung", url_segment="catering-admin", guard=catering_admin_page_guard)
|
||||
class CateringAdminPage(Component):
|
||||
open_orders: List[CateringOrder] = List()
|
||||
all_users: list[User] = []
|
||||
all_seats: list[Seat] = []
|
||||
all_menu_items: list[CateringMenuItem] = []
|
||||
edited_order: Optional[CateringOrder] = None
|
||||
|
||||
@on_populate
|
||||
async def on_populate(self) -> None:
|
||||
self.all_users = await User.find_all().to_list()
|
||||
self.all_seats = await Seat.find_all(fetch_links=True).to_list()
|
||||
self.all_menu_items = await CateringMenuItem.find_all(fetch_links=True).to_list()
|
||||
self.open_orders = List(await CateringOrder.find_many(
|
||||
{
|
||||
"status": {
|
||||
"$nin": [
|
||||
CateringOrderStatus.COMPLETED,
|
||||
CateringOrderStatus.CANCELED,
|
||||
]
|
||||
}
|
||||
}
|
||||
).to_list())
|
||||
await sleep(5)
|
||||
self.session.create_task(self.on_populate())
|
||||
|
||||
def get_name_for_user_id(self, id_: PydanticObjectId) -> str:
|
||||
return next(filter(lambda user: user.id == id_ ,self.all_users)).user_name
|
||||
|
||||
def get_seat_for_user_id(self, id_: PydanticObjectId) -> str:
|
||||
try:
|
||||
found_seat: Optional[Seat] = next(filter(lambda seat: seat.user is not None and seat.user.id == id_, self.all_seats), None)
|
||||
if found_seat:
|
||||
return found_seat.seat_id
|
||||
return "-"
|
||||
except Exception:
|
||||
return "-"
|
||||
|
||||
async def on_order_pressed(self, order: CateringOrder) -> None:
|
||||
self.edited_order = order
|
||||
|
||||
async def change_order_status(self, new_status: CateringOrderStatus) -> None:
|
||||
if not self.edited_order:
|
||||
return
|
||||
|
||||
if new_status == CateringOrderStatus.CANCELED:
|
||||
pass
|
||||
|
||||
if self.edited_order.status == new_status:
|
||||
self.edited_order = None
|
||||
return
|
||||
|
||||
if new_status == CateringOrderStatus.CANCELED:
|
||||
user = await User.find_one(User.id == self.edited_order.customer_id)
|
||||
if not user:
|
||||
self.edited_order = None
|
||||
return
|
||||
|
||||
price = Decimal(0)
|
||||
for item in self.edited_order.items:
|
||||
price += item.final_unit_price
|
||||
await self.session[AccountingService].add_balance(user.user_name, price, f"CATERING REFUND - {str(self.edited_order.id)[-5:]}")
|
||||
|
||||
self.edited_order.status = new_status
|
||||
await self.edited_order.save()
|
||||
self.open_orders = List(await CateringOrder.find_many(
|
||||
{
|
||||
"status": {
|
||||
"$nin": [
|
||||
CateringOrderStatus.COMPLETED,
|
||||
CateringOrderStatus.CANCELED,
|
||||
]
|
||||
}
|
||||
}
|
||||
).to_list())
|
||||
self.edited_order = None
|
||||
|
||||
async def print_receipt(self) -> None:
|
||||
if not self.edited_order:
|
||||
return
|
||||
user = await User.find_one(User.id == self.edited_order.customer_id)
|
||||
if not user:
|
||||
self.edited_order = None
|
||||
return
|
||||
self.session.create_task(self.session[ReceiptPrintingService].print_order(user, self.edited_order))
|
||||
self.edited_order = None
|
||||
|
||||
@staticmethod
|
||||
async def change_item_active(event: SwitchChangeEvent, item: CateringMenuItem) -> None:
|
||||
item.active = event.is_on
|
||||
await item.save()
|
||||
|
||||
def build(self) -> Component:
|
||||
if self.edited_order:
|
||||
overlay = [
|
||||
Overlay(
|
||||
content=Rectangle(
|
||||
content=Column(
|
||||
Text(f"Status ändern - Bestellung {str(self.edited_order.id)[-5:]}", margin_bottom=1),
|
||||
*[ElmButton(text=CateringOrder.translate_order_status(status), on_press=partial(self.change_order_status, status)) for status in CateringOrderStatus],
|
||||
Row(ElmButton(text="Bon drucken", on_press=self.print_receipt), ElmButton(text="Abbrechen", on_press=lambda: self.__setattr__("edited_order", None)), spacing=1, margin_top=2),
|
||||
spacing=0.5,
|
||||
margin=1
|
||||
),
|
||||
fill=self.session.theme.box_color,
|
||||
stroke_width=0.2,
|
||||
stroke_color=self.session.theme.box_border_color,
|
||||
align_x=0.5,
|
||||
align_y=0.5
|
||||
)
|
||||
)
|
||||
]
|
||||
else:
|
||||
overlay = []
|
||||
return Row(
|
||||
*overlay,
|
||||
Rectangle(
|
||||
content=Column(
|
||||
Rectangle(
|
||||
content=Rectangle(
|
||||
content=Row(
|
||||
Text("Offene Bestellungen", margin=0.5, selectable=False, overflow="wrap"),
|
||||
Link(content="Neue Bestellung", target_url="./new-pos-order")
|
||||
),
|
||||
fill=self.session.theme.header_box_background_color,
|
||||
margin=0.4
|
||||
),
|
||||
stroke_width=0.1,
|
||||
stroke_color=self.session.theme.box_border_color,
|
||||
),
|
||||
FlowContainer(
|
||||
*[PointerEventListener(
|
||||
content=Rectangle(
|
||||
content=Column(
|
||||
Row(Text(f"ID:", font_size=1.2), Text(str(order.id)[-5:], justify="right", font_size=1.2)),
|
||||
Row(Text("Nutzer:", font_size=1.2), Text(self.get_name_for_user_id(order.customer_id), font_size=1.2, justify="right")),
|
||||
Row(Text(f"Sitzplatz:", font_size=1.2), Text(self.get_seat_for_user_id(order.customer_id), font_size=1.2, justify="right")),
|
||||
Row(Text(f"Status:", font_size=1.2), Text(CateringOrder.translate_order_status(order.status), font_size=1.2, justify="right"), margin_bottom=2),
|
||||
*[Text(item.name, overflow="ellipsize") for item in order.items],
|
||||
margin=0.5,
|
||||
spacing=0.2
|
||||
),
|
||||
stroke_color=self.session.theme.primary_color,
|
||||
stroke_width=0.1,
|
||||
cursor="pointer",
|
||||
hover_stroke_color=self.session.theme.warning_color,
|
||||
hover_stroke_width=0.1,
|
||||
min_width=30
|
||||
),
|
||||
on_press=lambda event, order=order: self.on_order_pressed(order),
|
||||
) for order in self.open_orders],
|
||||
Spacer(),
|
||||
spacing=1,
|
||||
margin=1
|
||||
),
|
||||
Spacer()
|
||||
),
|
||||
fill=self.session.theme.box_color,
|
||||
stroke_width=0.1,
|
||||
stroke_color=self.session.theme.box_border_color,
|
||||
min_width=25,
|
||||
grow_x=True,
|
||||
margin_right=1
|
||||
),
|
||||
Rectangle(
|
||||
content=Column(
|
||||
Rectangle(
|
||||
content=Rectangle(
|
||||
content=Text("Speisekarte", margin=0.5, selectable=False, overflow="wrap"),
|
||||
fill=self.session.theme.header_box_background_color,
|
||||
margin=0.4
|
||||
),
|
||||
stroke_width=0.1,
|
||||
stroke_color=self.session.theme.box_border_color,
|
||||
),
|
||||
Column(
|
||||
*[Column(
|
||||
Text(text=category.value, margin_bottom=1, margin_top=0.5, fill=self.session.theme.primary_color),
|
||||
*[Rectangle(
|
||||
content=Row(Text(text=item.name, overflow="ellipsize", grow_x=True), Switch(is_on=item.active, on_change=lambda event, item=item: self.change_item_active(event, item)), margin=0.1),
|
||||
stroke_width=0.1,
|
||||
stroke_color=self.session.theme.box_border_color
|
||||
) for item in filter(lambda i: i.category == category, self.all_menu_items)],
|
||||
spacing=0.5
|
||||
) for category in CateringMenuItemCategory],
|
||||
spacing=0.5,
|
||||
margin=1
|
||||
),
|
||||
Spacer()
|
||||
),
|
||||
fill=self.session.theme.box_color,
|
||||
stroke_width=0.1,
|
||||
stroke_color=self.session.theme.box_border_color,
|
||||
min_width=25
|
||||
),
|
||||
margin=1
|
||||
)
|
||||
@@ -0,0 +1,65 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Literal
|
||||
|
||||
from rio import Component, Column, Row, Text, page, Rectangle, SwitcherBar, SwitcherBarChangeEvent, List
|
||||
|
||||
from elm.components import CateringCategoryDisplay, CateringCart
|
||||
from elm.types.CateringTypes import CateringMenuItem, CateringOrderedItem
|
||||
|
||||
|
||||
@page(name="Catering", url_segment="catering")
|
||||
class CateringPage(Component):
|
||||
active_category: Literal["Frühstück", "Hauptspeisen", "Snacks & Dessert", "Softdrinks", "Alkohol"] = "Hauptspeisen"
|
||||
cart: List[CateringOrderedItem] = List()
|
||||
|
||||
async def on_switcher_bar_change(self, event: SwitcherBarChangeEvent) -> None:
|
||||
self.active_category = event.value
|
||||
|
||||
async def add_to_cart_pressed(self, item: CateringMenuItem, changed_options: dict[str, bool]) -> None:
|
||||
order_item = CateringOrderedItem.from_menu_item(item, changed_options)
|
||||
self.cart.append(order_item)
|
||||
|
||||
def build(self) -> Component:
|
||||
return Row(
|
||||
Column(
|
||||
Rectangle(
|
||||
content=Column(
|
||||
Rectangle(
|
||||
content=SwitcherBar("Frühstück", "Hauptspeisen", "Snacks & Dessert", "Softdrinks", "Alkohol", margin=0.5, selected_value=self.bind().active_category, on_change=self.on_switcher_bar_change),
|
||||
stroke_width=0.1,
|
||||
stroke_color=self.session.theme.box_border_color,
|
||||
)
|
||||
),
|
||||
fill=self.session.theme.box_color,
|
||||
stroke_width=0.1,
|
||||
stroke_color=self.session.theme.box_border_color
|
||||
),
|
||||
CateringCategoryDisplay(active_category=self.active_category, add_to_cart_pressed_callback=self.add_to_cart_pressed, grow_y=True),
|
||||
grow_x=True,
|
||||
spacing=1
|
||||
),
|
||||
Column(
|
||||
Rectangle(
|
||||
content=Column(
|
||||
Rectangle(
|
||||
content=Rectangle(
|
||||
content=Text("Warenkorb", margin=0.5, selectable=False, overflow="wrap"),
|
||||
fill=self.session.theme.header_box_background_color,
|
||||
margin=0.4
|
||||
),
|
||||
stroke_width=0.1,
|
||||
stroke_color=self.session.theme.box_border_color,
|
||||
)
|
||||
),
|
||||
fill=self.session.theme.box_color,
|
||||
stroke_width=0.1,
|
||||
stroke_color=self.session.theme.box_border_color
|
||||
),
|
||||
CateringCart(cart=self.cart, grow_y=True),
|
||||
spacing=1,
|
||||
min_width=20
|
||||
),
|
||||
spacing=1,
|
||||
margin=1
|
||||
)
|
||||
@@ -0,0 +1,38 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from rio import Component, Column, Row, Text, Spacer, page, Rectangle
|
||||
|
||||
from elm.services import PreloadService
|
||||
|
||||
@page(name="FAQ", url_segment="faq")
|
||||
class FaqPage(Component):
|
||||
def build(self) -> Component:
|
||||
return Row(
|
||||
Rectangle(
|
||||
content=Column(
|
||||
Rectangle(
|
||||
content=Rectangle(
|
||||
content=Text("F.A.Q.", margin=0.5, selectable=False, overflow="wrap"),
|
||||
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(q_a[0], font_weight="bold", overflow="wrap", fill=self.session.theme.primary_color),
|
||||
Text(q_a[1], italic=True, overflow="wrap"),
|
||||
spacing=0.5,
|
||||
margin_bottom=1.5,
|
||||
margin=1
|
||||
) for q_a in self.session[PreloadService].faq],
|
||||
Spacer()
|
||||
),
|
||||
fill=self.session.theme.box_color,
|
||||
stroke_width=0.1,
|
||||
stroke_color=self.session.theme.box_border_color
|
||||
),
|
||||
grow_x=True,
|
||||
grow_y=True,
|
||||
margin=0 if self.session.is_mobile() else 1
|
||||
)
|
||||
@@ -0,0 +1,84 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from _sha2 import sha256
|
||||
from random import choices
|
||||
from typing import Any, Optional
|
||||
|
||||
from rio import Component, Column, Row, Text, Spacer, page, Color, Rectangle, TextInput, GuardEvent, ProgressCircle
|
||||
|
||||
from elm.types import UserSession, User
|
||||
from elm.services import UserService, ConfigurationService, MailingService
|
||||
from elm.components import ElmButton
|
||||
|
||||
def forgot_password_page_guard(event: GuardEvent) -> Optional[str]:
|
||||
try:
|
||||
_ = event.session[UserSession].user_name
|
||||
return "/"
|
||||
except KeyError:
|
||||
return None
|
||||
|
||||
@page(name="Forgot password", url_segment="lost-pw", guard=forgot_password_page_guard)
|
||||
class ForgotPasswordPage(Component):
|
||||
mail: str = ""
|
||||
success: bool = False
|
||||
is_loading: bool = False
|
||||
|
||||
async def on_confirm(self, _: Any = None) -> None:
|
||||
self.is_loading = True
|
||||
|
||||
lan_info = self.session[ConfigurationService].get_lan_info()
|
||||
user_service = self.session[UserService]
|
||||
mailing_service = self.session[MailingService]
|
||||
user = await user_service.get_user_by_mail(self.mail.strip())
|
||||
if user is not None:
|
||||
new_password = "".join(choices(user_service.ALLOWED_USER_NAME_SYMBOLS, k=16))
|
||||
user.user_fallback_password = sha256(new_password.encode(encoding="utf-8")).hexdigest()
|
||||
await User.save(user)
|
||||
await mailing_service.send_email(
|
||||
subject=f"Dein neues Passwort für {lan_info.name}",
|
||||
body=f"Du hast für den EZGG LAN Manager der {lan_info.name} ein neues Passwort angefragt. "
|
||||
f"Und hier ist es schon:\n\n{new_password}\n\nSolltest du kein neues Passwort angefordert haben, "
|
||||
f"ignoriere diese E-Mail.\n\nLiebe Grüße\nDein {lan_info.name} - Team",
|
||||
receiver=self.mail.strip()
|
||||
)
|
||||
|
||||
self.success = True
|
||||
self.is_loading = False
|
||||
|
||||
def build(self) -> Component:
|
||||
return Row(
|
||||
Rectangle(
|
||||
content=Column(
|
||||
Rectangle(
|
||||
content=Rectangle(
|
||||
content=Row(
|
||||
Text("Passwort vergessen", margin=0.5, selectable=False, overflow="wrap", grow_x=True),
|
||||
ProgressCircle(min_size=1, margin=0.5, color="primary", progress=None if self.is_loading else 0)
|
||||
),
|
||||
fill=self.session.theme.header_box_background_color,
|
||||
margin=0.4
|
||||
),
|
||||
stroke_width=0.1,
|
||||
stroke_color=self.session.theme.box_border_color,
|
||||
),
|
||||
Column(
|
||||
TextInput(
|
||||
text=self.bind().mail,
|
||||
label="Mail Adresse",
|
||||
on_confirm=self.on_confirm
|
||||
),
|
||||
Text("Prüfe deine Mails!", fill=self.session.theme.success_color, overflow="wrap", justify="center") if self.success else Spacer(grow_x=False, grow_y=False),
|
||||
ElmButton(text="Neues Passwort anfordern", style="small" if self.session.is_mobile() else "normal", on_press=self.on_confirm, wrap=self.session.is_mobile()),
|
||||
margin=1,
|
||||
spacing=1
|
||||
),
|
||||
Spacer()
|
||||
),
|
||||
fill=self.session.theme.box_color,
|
||||
stroke_width=0.1,
|
||||
stroke_color=self.session.theme.box_border_color,
|
||||
min_height=15
|
||||
),
|
||||
align_x=0.5,
|
||||
align_y=0.5
|
||||
)
|
||||
@@ -0,0 +1,44 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from rio import Component, Column, Row, Text, page, Rectangle, CodeBlock, Link
|
||||
|
||||
from elm.services import PreloadService
|
||||
|
||||
@page(name="Imprint", url_segment="imprint")
|
||||
class ImprintPage(Component):
|
||||
def build(self) -> Component:
|
||||
return Row(
|
||||
Rectangle(
|
||||
content=Column(
|
||||
Rectangle(
|
||||
content=Rectangle(
|
||||
content=Text("Impressum & DSGVO", margin=0.5, selectable=False, overflow="wrap"),
|
||||
fill=self.session.theme.header_box_background_color,
|
||||
margin=0.4
|
||||
),
|
||||
stroke_width=0.1,
|
||||
stroke_color=self.session.theme.box_border_color,
|
||||
),
|
||||
CodeBlock(
|
||||
code=self.session[PreloadService].imprint,
|
||||
show_controls=False,
|
||||
language="text",
|
||||
scroll_code_x="auto",
|
||||
margin=1,
|
||||
grow_y=True
|
||||
),
|
||||
Link(
|
||||
content="Datenschutzerklärung",
|
||||
target_url="https://ezgg-ev.de/privacy", # ToDo: Move into this app and make preloaded content
|
||||
open_in_new_tab=True,
|
||||
margin=1
|
||||
)
|
||||
),
|
||||
fill=self.session.theme.box_color,
|
||||
stroke_width=0.1,
|
||||
stroke_color=self.session.theme.box_border_color
|
||||
),
|
||||
grow_x=True,
|
||||
grow_y=True,
|
||||
margin=0 if self.session.is_mobile() else 1
|
||||
)
|
||||
@@ -0,0 +1,81 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from rio import Component, Column, Row, Text, Spacer, page, Color, TextStyle
|
||||
|
||||
from elm.components import LanCountdownBox, LanInfoBox, LandingPageBoxFull, LandingPageBoxHalf
|
||||
|
||||
|
||||
@page(name="Landing", url_segment="")
|
||||
class LandingPage(Component):
|
||||
def build(self) -> Component:
|
||||
full_box = LandingPageBoxFull(
|
||||
image_name="news_image.jpg",
|
||||
heading_text="EZGG LAN geht in die 2. Runde",
|
||||
article_text="Am 04.06.2027 ist es soweit. Dann findet die EZGG LAN in der zweiten Edition statt. Es erwarten euch viele Verbesserungen zur letzten Edition und wir hoffen euch auch dieses mal begrüßen zu dürfen.",
|
||||
date="15.05.26"
|
||||
)
|
||||
half_box_1 = LandingPageBoxHalf(
|
||||
heading_text="Sponsored by Crackz",
|
||||
image_name="crackz.png",
|
||||
article_text="CRACKZ ist dein Co-Op-Partner für verboten guten Geschmack und der Snack, der mit dir durchzockt.\n\n\n\nMehr auf crackz.gg",
|
||||
link="https://www.crackz.gg"
|
||||
)
|
||||
half_box_2 = LandingPageBoxHalf(
|
||||
heading_text="Made with rio",
|
||||
image_name="rio.png",
|
||||
article_text="Unsere Webseite ist mit rio umgesetzt.\n\nEinem einfach zu bedienenden Framework um ganze Webapps in reinem Python zu entwickeln.\n\n\nMehr auf rio.dev",
|
||||
link="https://rio.dev"
|
||||
)
|
||||
|
||||
if self.session.is_mobile():
|
||||
return Column(
|
||||
Column(
|
||||
Row(
|
||||
Text("//", style=TextStyle(font_size=1.7, fill=self.session.theme.primary_color), margin_right=2),
|
||||
Text("Neuigkeiten", style=TextStyle(font_size=1.2, fill=Color.WHITE, font_weight="bold")),
|
||||
Spacer(),
|
||||
margin_bottom=0.5
|
||||
),
|
||||
Row(full_box, margin_bottom=2),
|
||||
Column(half_box_1, half_box_2, spacing=1, margin_bottom=1),
|
||||
Spacer()
|
||||
),
|
||||
Column(
|
||||
LanInfoBox(),
|
||||
LanCountdownBox(),
|
||||
Spacer(),
|
||||
spacing=2,
|
||||
),
|
||||
margin=0.5
|
||||
)
|
||||
|
||||
else: # Tablet & Desktop
|
||||
return Row(
|
||||
Column(
|
||||
Row(
|
||||
Text("//", style=TextStyle(font_size=1.7, fill=self.session.theme.primary_color), margin_right=2),
|
||||
Text("Neuigkeiten", style=TextStyle(font_size=1.2, fill=Color.WHITE, font_weight="bold")),
|
||||
Spacer(),
|
||||
Text("■", margin_right=0.5, style=TextStyle(font_size=1.2, fill=self.session.theme.primary_color)),
|
||||
Text("■", margin_right=0.5, style=TextStyle(font_size=1.2, fill=self.session.theme.primary_color_darker)),
|
||||
Text("■", margin_right=0.5, style=TextStyle(font_size=1.2, fill=self.session.theme.primary_color_dark)),
|
||||
margin_bottom=0.5
|
||||
),
|
||||
Row(full_box, margin_bottom=2),
|
||||
Row(
|
||||
Row(half_box_1, half_box_2, spacing=1, margin_bottom=1),
|
||||
spacing=1,
|
||||
margin_bottom=2
|
||||
),
|
||||
Spacer(),
|
||||
grow_x=True
|
||||
),
|
||||
Column(
|
||||
LanInfoBox(),
|
||||
LanCountdownBox(),
|
||||
Spacer(),
|
||||
spacing=2,
|
||||
),
|
||||
spacing=2,
|
||||
margin=2
|
||||
)
|
||||
@@ -0,0 +1,106 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from copy import copy
|
||||
from typing import Any, Optional
|
||||
from uuid import uuid4
|
||||
|
||||
from rio import Component, Column, Row, Text, Spacer, page, Color, Rectangle, TextInput, GuardEvent
|
||||
|
||||
from elm.types import UserSession, User
|
||||
from elm.services import UserService, LocalData, LocalDataService, ConfigurationService
|
||||
from elm.components import ElmButton
|
||||
|
||||
def login_page_guard(event: GuardEvent) -> Optional[str]:
|
||||
try:
|
||||
_ = event.session[UserSession].user_name
|
||||
return "/"
|
||||
except KeyError:
|
||||
return None
|
||||
|
||||
@page(name="Login", url_segment="login", guard=login_page_guard)
|
||||
class LoginPage(Component):
|
||||
user_name: str = ""
|
||||
password: str = ""
|
||||
error_on_last_attempt: bool = False
|
||||
login_in_progress: bool = False
|
||||
|
||||
async def on_login_confirmed(self, _: Any) -> None:
|
||||
""" Handler for pressing ENTER inside the text inputs """
|
||||
await self.on_login_pressed()
|
||||
|
||||
async def on_login_pressed(self) -> None:
|
||||
self.login_in_progress = True
|
||||
user_name = copy(self.user_name) # Prevents race condition name swap
|
||||
is_valid = await self.session[UserService].is_login_valid(user_name, self.password)
|
||||
if is_valid:
|
||||
user: User = await self.session[UserService].get_user(user_name)
|
||||
self.error_on_last_attempt = False
|
||||
user_session = UserSession(id=uuid4(), user_name=user.user_name, is_team_member=user.is_team_member)
|
||||
self.session.attach(user_session)
|
||||
token = self.session[LocalDataService].set_session(user_session)
|
||||
self.session[LocalData].stored_session_token = token
|
||||
self.session[UserSession].profile_picture = await self.load_user_picture()
|
||||
self.session.attach(self.session[LocalData])
|
||||
self.login_in_progress = False
|
||||
self.session.navigate_to("./")
|
||||
else:
|
||||
self.login_in_progress = False
|
||||
self.error_on_last_attempt = True
|
||||
|
||||
async def load_user_picture(self) -> bytes:
|
||||
try:
|
||||
user_picture = await self.session[UserService].get_user_picture(self.session[UserSession].user_name)
|
||||
if user_picture is not None and len(user_picture) > 0:
|
||||
return user_picture
|
||||
except KeyError:
|
||||
return self.session[ConfigurationService].DEFAULT_PROFILE_PICTURE
|
||||
return self.session[ConfigurationService].DEFAULT_PROFILE_PICTURE
|
||||
|
||||
def on_register_pressed(self) -> None:
|
||||
self.session.navigate_to("./register")
|
||||
|
||||
def on_lost_password_pressed(self) -> None:
|
||||
self.session.navigate_to("./lost-pw")
|
||||
|
||||
def build(self) -> Component:
|
||||
return Row(
|
||||
Rectangle(
|
||||
content=Column(
|
||||
Rectangle(
|
||||
content=Rectangle(
|
||||
content=Text("Login", margin=0.5, selectable=False, overflow="wrap"),
|
||||
fill=self.session.theme.header_box_background_color,
|
||||
margin=0.4
|
||||
),
|
||||
stroke_width=0.1,
|
||||
stroke_color=self.session.theme.box_border_color,
|
||||
),
|
||||
Column(
|
||||
TextInput(
|
||||
text=self.bind().user_name,
|
||||
label="Nutzername",
|
||||
on_confirm=self.on_login_confirmed
|
||||
),
|
||||
TextInput(
|
||||
text=self.bind().password,
|
||||
label="Passwort",
|
||||
is_secret=True,
|
||||
on_confirm=self.on_login_confirmed
|
||||
),
|
||||
Text("Falscher Nutzername oder Passwort", fill=self.session.theme.danger_color, overflow="wrap", justify="center") if self.error_on_last_attempt else Spacer(grow_x=False, grow_y=False),
|
||||
ElmButton(text="Login", style="small" if self.session.is_mobile() else "normal", on_press=self.on_login_pressed, is_loading=self.login_in_progress),
|
||||
ElmButton(text="Passwort\nvergessen" if self.session.is_mobile() else "Passwort vergessen", style="small" if self.session.is_mobile() else "normal", on_press=self.on_lost_password_pressed),
|
||||
ElmButton(text="Account anlegen", style="small" if self.session.is_mobile() else "normal", on_press=self.on_register_pressed),
|
||||
margin=1,
|
||||
spacing=1
|
||||
),
|
||||
Spacer()
|
||||
),
|
||||
fill=self.session.theme.box_color,
|
||||
stroke_width=0.1,
|
||||
stroke_color=self.session.theme.box_border_color,
|
||||
min_height=15
|
||||
),
|
||||
align_x=0.5,
|
||||
align_y=0.5
|
||||
)
|
||||
@@ -0,0 +1,25 @@
|
||||
from __future__ import annotations
|
||||
|
||||
|
||||
from rio import Component, Row, page
|
||||
from rio.event import on_populate
|
||||
|
||||
from elm.types import UserSession
|
||||
from elm.services import LocalData, LocalDataService
|
||||
|
||||
@page(name="Logout", url_segment="logout")
|
||||
class LandingPage(Component):
|
||||
|
||||
@on_populate
|
||||
def on_populate(self) -> None:
|
||||
try:
|
||||
self.session.detach(UserSession)
|
||||
except KeyError:
|
||||
pass
|
||||
self.session[LocalDataService].del_session(self.session[LocalData].stored_session_token)
|
||||
self.session[LocalData].stored_session_token = None
|
||||
self.session.attach(self.session[LocalData])
|
||||
self.session.navigate_to("/")
|
||||
|
||||
def build(self) -> Component:
|
||||
return Row()
|
||||
@@ -0,0 +1,242 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from asyncio import sleep
|
||||
from datetime import datetime
|
||||
from decimal import Decimal, ROUND_DOWN
|
||||
from typing import Optional
|
||||
|
||||
from rio import Component, Column, Row, Text, Spacer, page, Rectangle, GuardEvent, Revealer, Image, NumberInput
|
||||
from rio.event import on_populate
|
||||
|
||||
from elm.types import UserSession, Transaction
|
||||
from elm.services import AccountingService
|
||||
from elm.components import ElmButton
|
||||
|
||||
logger = logging.getLogger(__name__.split(".")[-1])
|
||||
|
||||
class TransactionRow(Component):
|
||||
transaction_time: datetime
|
||||
transaction_title: str
|
||||
transaction_amount: Decimal
|
||||
is_debit: bool
|
||||
|
||||
def build(self) -> Component:
|
||||
color = self.session.theme.danger_color if self.is_debit else self.session.theme.success_color
|
||||
return Rectangle(
|
||||
content=Row(
|
||||
Text(
|
||||
f"{self.transaction_time.strftime("%d.%m.%y")} /",
|
||||
justify="left",
|
||||
font_size=0.8,
|
||||
margin_left=0.5,
|
||||
fill=color
|
||||
),
|
||||
Text(
|
||||
self.transaction_title,
|
||||
justify="left",
|
||||
font_size=0.8,
|
||||
margin_left=0.5,
|
||||
fill=color,
|
||||
overflow="ellipsize",
|
||||
grow_x=True
|
||||
),
|
||||
Text(
|
||||
f"{'-' if self.is_debit else '+'}{str(self.transaction_amount.quantize(Decimal('.01'), rounding=ROUND_DOWN))} €",
|
||||
justify="right",
|
||||
font_size=0.8,
|
||||
margin_right=0.5,
|
||||
fill=color
|
||||
),
|
||||
margin_bottom=0.5,
|
||||
margin_top=0.5
|
||||
),
|
||||
hover_fill=self.session.theme.background_color,
|
||||
transition_time=0.2
|
||||
)
|
||||
|
||||
def my_balance_page_guard(event: GuardEvent) -> Optional[str]:
|
||||
try:
|
||||
_ = event.session[UserSession].user_name
|
||||
return None
|
||||
except KeyError:
|
||||
return "/"
|
||||
|
||||
@page(name="My Balance", url_segment="balance", guard=my_balance_page_guard)
|
||||
class MyBalancePage(Component):
|
||||
current_balance: str = "-"
|
||||
last_20_transactions: list[Transaction] = []
|
||||
bank_revealer_open: bool = False
|
||||
paypal_revealer_open: bool = False
|
||||
payment_qr_image: bytes = bytes()
|
||||
paypal_charge_amount: float = 0.00
|
||||
paypal_charge_in_progress: bool = False
|
||||
|
||||
@on_populate
|
||||
async def async_init(self) -> None:
|
||||
self.current_balance = self.session[AccountingService].make_euro_string_from_decimal(
|
||||
await self.session[AccountingService].get_balance(self.session[UserSession].user_name)
|
||||
)
|
||||
|
||||
self.last_20_transactions = (await self.session[AccountingService].get_transaction_history(self.session[UserSession].user_name))[:20]
|
||||
self.payment_qr_image = self.session[AccountingService].make_payment_qr_image(
|
||||
"Einfach Zocken Gaming Gesellschaft",
|
||||
"GENODE51BIK",
|
||||
"DE47517624340019856607",
|
||||
f"AUFLADUNG - {self.session[UserSession].user_name}")
|
||||
|
||||
async def check_if_paypal_process_done(self) -> None:
|
||||
await sleep(2)
|
||||
if await self.session[AccountingService].has_user_open_orders(self.session[UserSession].user_name):
|
||||
self.session.create_task(self.check_if_paypal_process_done())
|
||||
else:
|
||||
self.paypal_charge_in_progress = False
|
||||
self.paypal_charge_amount = 0.00
|
||||
self.current_balance = self.session[AccountingService].make_euro_string_from_decimal(
|
||||
await self.session[AccountingService].get_balance(self.session[UserSession].user_name)
|
||||
)
|
||||
self.last_20_transactions = (await self.session[AccountingService].get_transaction_history(self.session[UserSession].user_name))[:20]
|
||||
self.paypal_revealer_open = False
|
||||
|
||||
async def pay_with_paypal(self) -> None:
|
||||
self.paypal_charge_in_progress = True
|
||||
logger.info("Starting PayPal transaction over %s for user %s", f"{self.paypal_charge_amount} €", self.session[UserSession].user_name)
|
||||
amount = Decimal(self.paypal_charge_amount)
|
||||
try:
|
||||
approval_url = await self.session[AccountingService].start_paypal_process(self.session[UserSession].user_name, amount)
|
||||
except Exception as e:
|
||||
logger.error(e)
|
||||
return
|
||||
self.session.open_url_in_browser(approval_url)
|
||||
self.session.create_task(self.check_if_paypal_process_done())
|
||||
|
||||
async def toggle_bank_revealer(self) -> None:
|
||||
self.bank_revealer_open = not self.bank_revealer_open
|
||||
|
||||
async def toggle_paypal_revealer(self) -> None:
|
||||
self.paypal_revealer_open = not self.paypal_revealer_open
|
||||
|
||||
def build(self) -> Component:
|
||||
col_row = Column if self.session.is_mobile() else Row
|
||||
|
||||
transaction_rows = []
|
||||
for transaction in sorted(self.last_20_transactions, key=lambda t: t.transaction_date, reverse=True):
|
||||
transaction_rows.append(
|
||||
TransactionRow(
|
||||
transaction_time=transaction.transaction_date,
|
||||
transaction_title=transaction.title,
|
||||
transaction_amount=transaction.value,
|
||||
is_debit=transaction.is_debit
|
||||
)
|
||||
)
|
||||
|
||||
return col_row(
|
||||
Column(
|
||||
Rectangle(
|
||||
content=Column(
|
||||
Rectangle(
|
||||
content=Rectangle(
|
||||
content=Text("Guthaben", margin=0.5, selectable=False, overflow="wrap"),
|
||||
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(
|
||||
text=self.current_balance,
|
||||
justify="center",
|
||||
font_size=2,
|
||||
grow_x=True,
|
||||
grow_y=True,
|
||||
margin_top=2,
|
||||
margin_bottom=1
|
||||
),
|
||||
Spacer(),
|
||||
margin=1,
|
||||
spacing=1
|
||||
)
|
||||
),
|
||||
fill=self.session.theme.box_color,
|
||||
stroke_width=0.1,
|
||||
stroke_color=self.session.theme.box_border_color
|
||||
),
|
||||
Rectangle(
|
||||
content=Column(
|
||||
Rectangle(
|
||||
content=Rectangle(
|
||||
content=Text("Guthaben aufladen", margin=0.5, selectable=False, overflow="wrap"),
|
||||
fill=self.session.theme.header_box_background_color,
|
||||
margin=0.4
|
||||
),
|
||||
stroke_width=0.1,
|
||||
stroke_color=self.session.theme.box_border_color,
|
||||
),
|
||||
Column(
|
||||
ElmButton(text="Banküberweißung", style="small" if self.session.is_mobile() else "normal", on_press=self.toggle_bank_revealer),
|
||||
Revealer(header=None, is_open=self.bank_revealer_open, content=Column(
|
||||
Text("QR Code", justify="center"),
|
||||
Image(self.payment_qr_image, min_width=14, min_height=14, margin_bottom=1),
|
||||
Text("Bankverbindung", justify="center"),
|
||||
Text("Empfänger: Einfach Zocken Gaming Gesellschaft", justify="left", overflow="wrap", font_size=0.7),
|
||||
Text("IBAN: DE47517624340019856607", justify="left", overflow="wrap", font_size=0.7),
|
||||
Text("BIC: GENODE51BIK", justify="left", overflow="wrap", font_size=0.7),
|
||||
Text(f"Verwendungszweck: AUFLADUNG - {self.session[UserSession].user_name}", justify="left", overflow="wrap", font_size=0.7),
|
||||
spacing=1
|
||||
)),
|
||||
ElmButton(text="Paypal", style="small" if self.session.is_mobile() else "normal", on_press=self.toggle_paypal_revealer),
|
||||
Revealer(header=None, is_open=self.paypal_revealer_open, content=Column(
|
||||
NumberInput(label="Summe", decimals=2, value=self.bind().paypal_charge_amount, suffix_text="€"),
|
||||
ElmButton(text="Jetzt aufladen", style="small" if self.session.is_mobile() else "normal", on_press=self.pay_with_paypal, is_loading=self.paypal_charge_in_progress),
|
||||
spacing=1
|
||||
)),
|
||||
margin=1,
|
||||
spacing=1
|
||||
)
|
||||
),
|
||||
fill=self.session.theme.box_color,
|
||||
stroke_width=0.1,
|
||||
stroke_color=self.session.theme.box_border_color
|
||||
),
|
||||
Spacer(),
|
||||
spacing=1
|
||||
),
|
||||
Column(
|
||||
Rectangle(
|
||||
content=Column(
|
||||
Rectangle(
|
||||
content=Rectangle(
|
||||
content=Text("Letzte Transaktionen", margin=0.5, selectable=False, overflow="wrap"),
|
||||
fill=self.session.theme.header_box_background_color,
|
||||
margin=0.4
|
||||
),
|
||||
stroke_width=0.1,
|
||||
stroke_color=self.session.theme.box_border_color,
|
||||
),
|
||||
Column(
|
||||
Rectangle(
|
||||
content=Row(
|
||||
Text("Datum / Titel", justify="left", font_size=0.8, margin_left=0.5),
|
||||
Text("Betrag", justify="right", font_size=0.8, margin_right=0.5),
|
||||
margin_bottom=0.5,
|
||||
margin_top=0.5
|
||||
)
|
||||
),
|
||||
*transaction_rows,
|
||||
spacing=0.5,
|
||||
margin=1
|
||||
)
|
||||
),
|
||||
fill=self.session.theme.box_color,
|
||||
stroke_width=0.1,
|
||||
stroke_color=self.session.theme.box_border_color
|
||||
),
|
||||
Spacer(),
|
||||
spacing=1,
|
||||
grow_x=True
|
||||
),
|
||||
spacing=1,
|
||||
margin=1,
|
||||
margin_right=2
|
||||
)
|
||||
@@ -0,0 +1,75 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Optional
|
||||
|
||||
from rio import Component, Column, Spacer, page, GuardEvent, Rectangle, Text, FlowContainer, Color, ProgressCircle
|
||||
from rio.event import on_populate
|
||||
|
||||
from elm.services import UserService
|
||||
from elm.types import UserSession
|
||||
from elm.types.CateringTypes import CateringOrder, CateringOrderStatus
|
||||
|
||||
|
||||
def my_orders_page_guard(event: GuardEvent) -> Optional[str]:
|
||||
try:
|
||||
_ = event.session[UserSession].user_name
|
||||
return None
|
||||
except KeyError:
|
||||
return "/"
|
||||
|
||||
@page(name="My Orders", url_segment="my-orders", guard=my_orders_page_guard)
|
||||
class MyOrdersPage(Component):
|
||||
orders: list[CateringOrder] = []
|
||||
is_loading: bool = True
|
||||
|
||||
@on_populate
|
||||
async def on_populate(self) -> None:
|
||||
user = await self.session[UserService].get_user(self.session[UserSession].user_name)
|
||||
if not user:
|
||||
self.is_loading = False
|
||||
return
|
||||
|
||||
self.orders = await CateringOrder.find_many(CateringOrder.customer_id == user.id).to_list()
|
||||
self.is_loading = False
|
||||
|
||||
def get_status_color(self, status: CateringOrderStatus) -> Color:
|
||||
color = self.session.theme.warning_color
|
||||
if status == CateringOrderStatus.DELAYED or status == CateringOrderStatus.CANCELED:
|
||||
color = self.session.theme.danger_color
|
||||
elif status == CateringOrderStatus.COMPLETED:
|
||||
color = self.session.theme.success_color
|
||||
return color
|
||||
|
||||
def build(self) -> Component:
|
||||
if self.is_loading:
|
||||
return ProgressCircle(margin=self.session.screen_width // 5)
|
||||
return Column(
|
||||
FlowContainer(
|
||||
*[Rectangle(
|
||||
content=Column(
|
||||
Rectangle(
|
||||
content=Rectangle(
|
||||
content=Text(f"Bestellung\n\n{str(order.id)[-5:]}", margin=0.5, selectable=False, overflow="wrap", justify="center"),
|
||||
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(item.name, overflow="wrap") for item in order.items],
|
||||
spacing=1,
|
||||
margin=1
|
||||
),
|
||||
Spacer(),
|
||||
Text(CateringOrder.translate_order_status(order.status), fill=self.get_status_color(order.status), margin=1, font_weight="bold", font_size=1, justify="center"),
|
||||
),
|
||||
fill=self.session.theme.box_color,
|
||||
min_width=18
|
||||
) for order in self.orders],
|
||||
spacing=1
|
||||
),
|
||||
Spacer(),
|
||||
spacing=1,
|
||||
margin=1
|
||||
)
|
||||
@@ -0,0 +1,54 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Optional
|
||||
|
||||
from rio import Component, Column, Row, Spacer, page, GuardEvent
|
||||
|
||||
from elm.types import UserSession
|
||||
from elm.components import AvatarEditBox, AccountInfoBox, PersonalInfoBox
|
||||
|
||||
def my_profile_page_guard(event: GuardEvent) -> Optional[str]:
|
||||
try:
|
||||
_ = event.session[UserSession].user_name
|
||||
return None
|
||||
except KeyError:
|
||||
return "/"
|
||||
|
||||
@page(name="My Profile", url_segment="my-profile", guard=my_profile_page_guard)
|
||||
class MyProfilePage(Component):
|
||||
def build(self) -> Component:
|
||||
if self.session.is_mobile():
|
||||
return Column(
|
||||
Column(
|
||||
AvatarEditBox(),
|
||||
Spacer(),
|
||||
spacing=1
|
||||
),
|
||||
Column(
|
||||
AccountInfoBox(),
|
||||
PersonalInfoBox(),
|
||||
Spacer(),
|
||||
spacing=1,
|
||||
grow_x=True
|
||||
),
|
||||
spacing=1,
|
||||
margin=0.5
|
||||
)
|
||||
else:
|
||||
return Row(
|
||||
Column(
|
||||
AvatarEditBox(),
|
||||
Spacer(),
|
||||
spacing=1
|
||||
),
|
||||
Column(
|
||||
AccountInfoBox(),
|
||||
PersonalInfoBox(),
|
||||
Spacer(),
|
||||
spacing=1,
|
||||
grow_x=True
|
||||
),
|
||||
spacing=1,
|
||||
margin=1,
|
||||
margin_right=2
|
||||
)
|
||||
@@ -0,0 +1,68 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from asyncio import sleep
|
||||
|
||||
from rio import Component, Column, Row, Text, Spacer, page, Rectangle, QueryParameter, ProgressCircle
|
||||
from rio.event import on_populate
|
||||
|
||||
from elm.services import AccountingService
|
||||
|
||||
logger = logging.getLogger(__name__.split(".")[-1])
|
||||
|
||||
@page(name="PayPal Return", url_segment="return-paypal")
|
||||
class PayPalReturnPage(Component):
|
||||
token: QueryParameter[str] = "No Value"
|
||||
in_progress: bool = True
|
||||
error_message: str = ""
|
||||
success_message: str = ""
|
||||
|
||||
@on_populate
|
||||
async def on_populate(self) -> None:
|
||||
result = await self.session[AccountingService].finalize_paypal_process(self.token)
|
||||
await sleep(1)
|
||||
if result:
|
||||
self.in_progress = False
|
||||
self.success_message = "Aufladung erfolgreich. Du kannst dieses Fenster schließen."
|
||||
else:
|
||||
self.in_progress = False
|
||||
self.error_message = "Es ist ein Fehler aufgetreten, bitte kontaktiere uns"
|
||||
|
||||
def build(self) -> Component:
|
||||
col_contents = []
|
||||
if self.in_progress:
|
||||
col_contents.append(ProgressCircle(min_size=5, color=self.session.theme.primary_color))
|
||||
col_contents.append(Text("Wir prüfen deine Aufladung", overflow="wrap", justify="center"))
|
||||
else:
|
||||
if self.error_message:
|
||||
col_contents.append(Text(self.error_message, overflow="wrap", justify="center", fill=self.session.theme.danger_color))
|
||||
elif self.success_message:
|
||||
col_contents.append(Text(self.success_message, overflow="wrap", justify="center", fill=self.session.theme.success_color))
|
||||
|
||||
return Row(
|
||||
Rectangle(
|
||||
content=Column(
|
||||
Rectangle(
|
||||
content=Rectangle(
|
||||
content=Text("Paypal Aufladung", margin=0.5, selectable=False, overflow="wrap"),
|
||||
fill=self.session.theme.header_box_background_color,
|
||||
margin=0.4
|
||||
),
|
||||
stroke_width=0.1,
|
||||
stroke_color=self.session.theme.box_border_color,
|
||||
),
|
||||
Column(
|
||||
*col_contents,
|
||||
margin=1,
|
||||
spacing=1
|
||||
),
|
||||
Spacer()
|
||||
),
|
||||
fill=self.session.theme.box_color,
|
||||
stroke_width=0.1,
|
||||
stroke_color=self.session.theme.box_border_color,
|
||||
min_width=1 if self.session.is_mobile() else 25
|
||||
),
|
||||
align_x=0.5,
|
||||
align_y=0.5
|
||||
)
|
||||
@@ -0,0 +1,147 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from copy import copy
|
||||
from typing import Any, Optional
|
||||
|
||||
from rio import Component, Column, Row, Text, Spacer, page, Color, Rectangle, TextInput, GuardEvent, ProgressCircle
|
||||
|
||||
from email_validator import validate_email, EmailNotValidError
|
||||
|
||||
from elm.types import UserSession
|
||||
from elm.services import UserService, NameNotAllowedError, MailAlreadyInUseError
|
||||
from elm.components import ElmButton
|
||||
|
||||
|
||||
def register_page_guard(event: GuardEvent) -> Optional[str]:
|
||||
try:
|
||||
_ = event.session[UserSession].user_name
|
||||
return "/"
|
||||
except KeyError:
|
||||
return None
|
||||
|
||||
@page(name="Register", url_segment="register", guard=register_page_guard)
|
||||
class RegisterPage(Component):
|
||||
user_name: str = ""
|
||||
password_1: str = ""
|
||||
password_2: str = ""
|
||||
mail: str = ""
|
||||
error_message: str = ""
|
||||
success_message: str = ""
|
||||
input_blocked: bool = False
|
||||
|
||||
async def on_register_confirmed(self, _: Any) -> None:
|
||||
""" Handler for pressing ENTER inside the text inputs """
|
||||
await self.on_register_pressed()
|
||||
|
||||
async def on_register_pressed(self) -> None:
|
||||
self.input_blocked = True
|
||||
user_name = copy(self.user_name) # Prevents race condition name swap
|
||||
|
||||
if len(user_name) < 3:
|
||||
self.error_message = f"Nutzername muss mindestens 3 Zeichen haben"
|
||||
self.input_blocked = False
|
||||
return
|
||||
|
||||
if len(self.password_1) == 0 or len(self.password_2) == 0:
|
||||
self.error_message = "Kein Passwort gesetzt"
|
||||
self.input_blocked = False
|
||||
return
|
||||
|
||||
if self.password_1 != self.password_2:
|
||||
self.error_message = "Passwörter stimmen nicht überein"
|
||||
self.input_blocked = False
|
||||
return
|
||||
|
||||
try:
|
||||
validate_email(self.mail, check_deliverability=False)
|
||||
except EmailNotValidError:
|
||||
self.error_message = "Ungültige Mail Adresse"
|
||||
self.input_blocked = False
|
||||
return
|
||||
|
||||
existing_user = await self.session[UserService].get_user(user_name)
|
||||
if existing_user:
|
||||
self.error_message = "Nutzer exisitiert bereits"
|
||||
self.input_blocked = False
|
||||
return
|
||||
|
||||
try:
|
||||
await self.session[UserService].create_user(user_name, self.mail, self.password_1)
|
||||
except NameNotAllowedError as e:
|
||||
self.error_message = f"Nutzername enthält unerlaubte Zeichen: {e.disallowed_char}"
|
||||
self.input_blocked = False
|
||||
return
|
||||
except MailAlreadyInUseError:
|
||||
self.error_message = "Mail Adresse bereits in Nutzung"
|
||||
self.input_blocked = False
|
||||
return
|
||||
|
||||
self.error_message = ""
|
||||
self.user_name, self.password_1, self.password_2, self.mail = "", "", "", ""
|
||||
self.success_message = "Registrierung erfolgreich"
|
||||
self.input_blocked = False
|
||||
|
||||
def build(self) -> Component:
|
||||
return Row(
|
||||
Rectangle(
|
||||
content=Column(
|
||||
Rectangle(
|
||||
content=Rectangle(
|
||||
content=Row(
|
||||
Text("Registrierung", margin=0.5, selectable=False, overflow="wrap", grow_x=True),
|
||||
ProgressCircle(min_size=1, margin=0.5, color="primary", progress=None if self.input_blocked else 0)
|
||||
),
|
||||
fill=self.session.theme.header_box_background_color,
|
||||
margin=0.4
|
||||
),
|
||||
stroke_width=0.1,
|
||||
stroke_color=self.session.theme.box_border_color,
|
||||
),
|
||||
Column(
|
||||
TextInput(
|
||||
text=self.bind().user_name,
|
||||
label="Nutzername",
|
||||
on_confirm=self.on_register_confirmed,
|
||||
is_sensitive=not self.input_blocked
|
||||
),
|
||||
TextInput(
|
||||
text=self.bind().mail,
|
||||
label="E-Mail",
|
||||
on_confirm=self.on_register_confirmed,
|
||||
is_sensitive=not self.input_blocked
|
||||
),
|
||||
TextInput(
|
||||
text=self.bind().password_1,
|
||||
label="Passwort",
|
||||
is_secret=True,
|
||||
on_confirm=self.on_register_confirmed,
|
||||
is_sensitive=not self.input_blocked
|
||||
),
|
||||
TextInput(
|
||||
text=self.bind().password_2,
|
||||
label="wiederholen" if self.session.is_mobile() else "Passwort wiederholen",
|
||||
is_secret=True,
|
||||
on_confirm=self.on_register_confirmed,
|
||||
is_sensitive=not self.input_blocked
|
||||
),
|
||||
Text(self.error_message, fill=self.session.theme.danger_color, overflow="wrap", justify="center") if self.error_message else Spacer(grow_x=False, grow_y=False),
|
||||
Text(self.success_message, fill=self.session.theme.success_color, overflow="wrap", justify="center") if self.success_message else Spacer(grow_x=False, grow_y=False),
|
||||
ElmButton(
|
||||
text="Registrieren",
|
||||
style="small" if self.session.is_mobile() else "normal",
|
||||
on_press=self.on_register_pressed
|
||||
) if not self.success_message else Spacer(grow_x=False, grow_y=False),
|
||||
ElmButton(text="Jetzt einloggen", style="small" if self.session.is_mobile() else "normal", on_press=lambda: self.session.navigate_to("./login")) if self.success_message else Spacer(grow_x=False, grow_y=False),
|
||||
margin=1,
|
||||
spacing=1
|
||||
),
|
||||
Spacer()
|
||||
),
|
||||
fill=self.session.theme.box_color,
|
||||
stroke_width=0.1,
|
||||
stroke_color=self.session.theme.box_border_color,
|
||||
min_height=15
|
||||
),
|
||||
align_x=0.5,
|
||||
align_y=0.5
|
||||
)
|
||||
@@ -0,0 +1,80 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from rio import Component, Column, Text, page, Rectangle
|
||||
|
||||
from elm.services import PreloadService
|
||||
|
||||
|
||||
@page(name="Rules", url_segment="rules")
|
||||
class RulesPage(Component):
|
||||
def build(self) -> Component:
|
||||
return Column(
|
||||
Rectangle(
|
||||
content=Column(
|
||||
Rectangle(
|
||||
content=Rectangle(
|
||||
content=Text("Regeln", margin=0.5, selectable=False, overflow="wrap"),
|
||||
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(
|
||||
rule.split(":")[0],
|
||||
font_size=0.8 if self.session.is_mobile() else 1.2,
|
||||
fill=self.session.theme.primary_color,
|
||||
overflow="wrap"
|
||||
),
|
||||
Text(
|
||||
rule.split(":")[1].strip(),
|
||||
font_size=0.7 if self.session.is_mobile() else 1,
|
||||
overflow="wrap",
|
||||
margin_left=1
|
||||
),
|
||||
margin=1,
|
||||
spacing=1
|
||||
) for rule in self.session[PreloadService].rules]
|
||||
),
|
||||
fill=self.session.theme.box_color,
|
||||
stroke_width=0.1,
|
||||
stroke_color=self.session.theme.box_border_color
|
||||
),
|
||||
Rectangle(
|
||||
content=Column(
|
||||
Rectangle(
|
||||
content=Rectangle(
|
||||
content=Text("AGB", margin=0.5, selectable=False, overflow="wrap"),
|
||||
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(
|
||||
text=paragraph,
|
||||
font_size=0.8 if self.session.is_mobile() else 1.2,
|
||||
fill=self.session.theme.primary_color,
|
||||
overflow="wrap"
|
||||
),
|
||||
*[Text(
|
||||
section,
|
||||
font_size=0.7 if self.session.is_mobile() else 1,
|
||||
overflow="wrap",
|
||||
margin_left=1
|
||||
) for section in self.session[PreloadService].gtc[paragraph]],
|
||||
margin=1,
|
||||
spacing=1
|
||||
) for paragraph in self.session[PreloadService].gtc]
|
||||
),
|
||||
fill=self.session.theme.box_color,
|
||||
stroke_width=0.1,
|
||||
stroke_color=self.session.theme.box_border_color
|
||||
),
|
||||
grow_x=True,
|
||||
grow_y=True,
|
||||
spacing=1,
|
||||
margin=0 if self.session.is_mobile() else 1
|
||||
)
|
||||
@@ -0,0 +1,129 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Optional
|
||||
|
||||
from bson import ObjectId
|
||||
from rio import Component, page, Rectangle, ProgressCircle, Row, QueryParameter, Column, Text, Spacer
|
||||
from rio.event import on_populate
|
||||
|
||||
from elm import UserSession
|
||||
from elm.components import ElmButton
|
||||
from elm.types import Seat, User, Ticket
|
||||
|
||||
|
||||
@page(name="Seat Info", url_segment="seat-info")
|
||||
class SeatInfoPage(Component):
|
||||
seat_id: QueryParameter[str] = ""
|
||||
seat: Optional[Seat] = None
|
||||
seat_user: Optional[User] = None
|
||||
initial_load_done: bool = False
|
||||
choosing_button_loading: bool = False
|
||||
message: str = ""
|
||||
message_is_error: bool = False
|
||||
|
||||
@on_populate
|
||||
async def on_populate(self) -> None:
|
||||
self.seat = await Seat.find_one(Seat.seat_id == self.seat_id)
|
||||
if self.seat and self.seat.user is not None:
|
||||
self.seat_user = await self.seat.user.fetch()
|
||||
self.initial_load_done = True
|
||||
|
||||
async def choose_seat(self) -> None:
|
||||
self.choosing_button_loading = True
|
||||
try:
|
||||
user_name = self.session[UserSession].user_name
|
||||
except KeyError:
|
||||
self.session.navigate_to("./login")
|
||||
return
|
||||
|
||||
user = await User.find_one(User.user_name == user_name)
|
||||
if not user:
|
||||
return
|
||||
|
||||
user_ticket = await Ticket.find_one(
|
||||
{"owner.$id": ObjectId(user.id)}
|
||||
)
|
||||
|
||||
if not user_ticket or user_ticket.category != self.seat.category:
|
||||
self.message = "Du hast nicht das passende Ticket"
|
||||
self.message_is_error = True
|
||||
self.choosing_button_loading = False
|
||||
return
|
||||
|
||||
user_seat = await Seat.find_one(
|
||||
{"user.$id": ObjectId(user.id)}
|
||||
)
|
||||
|
||||
if user_seat is not None:
|
||||
self.message = "Du hast bereits einen Sitzplatz"
|
||||
self.message_is_error = True
|
||||
self.choosing_button_loading = False
|
||||
return
|
||||
|
||||
s = await Seat.find_one(Seat.seat_id == self.seat_id)
|
||||
if not s:
|
||||
return
|
||||
s.user = user
|
||||
await s.save()
|
||||
self.message = "Sitzplatz gewählt!"
|
||||
self.message_is_error = False
|
||||
self.choosing_button_loading = False
|
||||
|
||||
def build(self) -> Component:
|
||||
if not self.initial_load_done:
|
||||
box_contents = [ProgressCircle(margin=1)]
|
||||
else:
|
||||
if self.seat is None:
|
||||
box_contents = [Text(text="Der angeforderte Sitzplatz konnte nicht gefunden werden", margin=1, overflow="wrap", justify="center", fill=self.session.theme.danger_color)]
|
||||
else:
|
||||
box_contents = [
|
||||
Row(
|
||||
Text(text="Kategorie:", justify="left"),
|
||||
Text(text=self.seat.category, justify="right"),
|
||||
spacing=1
|
||||
),
|
||||
Row(
|
||||
Text(text="Belegt:", justify="left"),
|
||||
Text(text="Ja" if self.seat.user is not None or self.seat.is_blocked else "Nein", justify="right", fill=self.session.theme.danger_color if self.seat.user is not None or self.seat.is_blocked else self.session.theme.success_color),
|
||||
spacing=1
|
||||
),
|
||||
Row(
|
||||
Text(text="Nutzer:", justify="left"),
|
||||
Text(text=self.seat_user.user_name if self.seat_user else "-", justify="right"),
|
||||
spacing=1
|
||||
),
|
||||
]
|
||||
if not self.seat.is_blocked and self.seat.user is None:
|
||||
box_contents.append(
|
||||
ElmButton(text="Platz wählen", on_press=self.choose_seat, is_loading=self.choosing_button_loading)
|
||||
)
|
||||
box_contents.append(
|
||||
Text(text=self.message, fill=self.session.theme.danger_color if self.message_is_error else self.session.theme.success_color, overflow="wrap", justify="center")
|
||||
)
|
||||
|
||||
return Row(
|
||||
Rectangle(
|
||||
content=Column(
|
||||
Rectangle(
|
||||
content=Rectangle(
|
||||
content=Text(f"Sitzplatz: {self.seat_id}", margin=0.5, selectable=False),
|
||||
fill=self.session.theme.header_box_background_color,
|
||||
margin=0.4
|
||||
),
|
||||
stroke_width=0.1,
|
||||
stroke_color=self.session.theme.box_border_color,
|
||||
),
|
||||
Column(
|
||||
*box_contents,
|
||||
margin=1,
|
||||
spacing=1
|
||||
),
|
||||
Spacer()
|
||||
),
|
||||
fill=self.session.theme.box_color,
|
||||
stroke_width=0.1,
|
||||
stroke_color=self.session.theme.box_border_color
|
||||
),
|
||||
align_x=0.5,
|
||||
align_y=0.5
|
||||
)
|
||||
@@ -0,0 +1,29 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Optional
|
||||
|
||||
from rio import Component, page, Rectangle, PointerEvent, ProgressCircle, Row
|
||||
from rio.event import on_populate
|
||||
|
||||
from elm.components import SeatingPlan
|
||||
from elm.types import Seat
|
||||
|
||||
|
||||
@page(name="Seating Plan", url_segment="seating")
|
||||
class SeatingPlanPage(Component):
|
||||
preloaded_seats: Optional[list[Seat]] = None
|
||||
|
||||
@on_populate
|
||||
async def on_populate(self) -> None:
|
||||
self.preloaded_seats = await Seat.find_all().to_list()
|
||||
|
||||
|
||||
def build(self) -> Component:
|
||||
return Rectangle(
|
||||
content=Row(ProgressCircle(), margin=self.session.screen_width // 6) if self.preloaded_seats is None else SeatingPlan(margin=0, preloaded_seats=self.preloaded_seats),
|
||||
fill=self.session.theme.box_color,
|
||||
stroke_width = 0.1,
|
||||
stroke_color = self.session.theme.box_border_color,
|
||||
margin_left=1,
|
||||
margin_top=1
|
||||
)
|
||||
@@ -0,0 +1,20 @@
|
||||
from __future__ import annotations
|
||||
|
||||
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):
|
||||
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(BuyTicketBox(ticket_info=ticket_info))
|
||||
|
||||
return row_col(
|
||||
*ticket_boxes,
|
||||
spacing=1,
|
||||
margin=1
|
||||
)
|
||||
@@ -0,0 +1,42 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from copy import copy
|
||||
from typing import Any, Optional
|
||||
from uuid import uuid4
|
||||
|
||||
from rio import Component, Column, Row, Text, Spacer, page, Color, Rectangle, TextInput, GuardEvent
|
||||
|
||||
from elm.types import UserSession, User
|
||||
from elm.services import UserService, LocalData, LocalDataService, ConfigurationService
|
||||
from elm.components import ElmButton
|
||||
|
||||
|
||||
@page(name="Tournaments", url_segment="tournaments")
|
||||
class TournamentsPage(Component):
|
||||
def build(self) -> Component:
|
||||
return Row(
|
||||
Rectangle(
|
||||
content=Column(
|
||||
Rectangle(
|
||||
content=Rectangle(
|
||||
content=Text("Turniere", margin=0.5, selectable=False, overflow="wrap"),
|
||||
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("Es wurde noch kein Turnierbaum hinterlegt", fill=self.session.theme.text_color, overflow="wrap", justify="center"),
|
||||
margin=1,
|
||||
spacing=1
|
||||
),
|
||||
Spacer()
|
||||
),
|
||||
fill=self.session.theme.box_color,
|
||||
stroke_width=0.1,
|
||||
stroke_color=self.session.theme.box_border_color
|
||||
),
|
||||
align_x=0.5,
|
||||
align_y=0.5
|
||||
)
|
||||
@@ -0,0 +1,212 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from functools import partial
|
||||
from typing import Optional
|
||||
from decimal import Decimal
|
||||
|
||||
from rio import Component, Column, Row, Text, Spacer, page, Rectangle, TextInput, GuardEvent, Button, TextInputChangeEvent, NumberInput, IconButton
|
||||
from rio.event import on_populate
|
||||
|
||||
from elm.types import UserSession, User, Transaction
|
||||
from elm.services import AccountingService, MailingService
|
||||
from elm.components import AccountInfoBox
|
||||
|
||||
logger = logging.getLogger(__name__.split(".")[-1])
|
||||
|
||||
def user_admin_page_guard(event: GuardEvent) -> Optional[str]:
|
||||
try:
|
||||
if event.session[UserSession].is_team_member:
|
||||
return None
|
||||
return "/"
|
||||
except KeyError:
|
||||
return "/"
|
||||
|
||||
@page(name="Benutzerverwaltung", url_segment="user-admin", guard=user_admin_page_guard)
|
||||
class UserAdminPage(Component):
|
||||
all_users: list[User] = list()
|
||||
user_list: list[User] = list()
|
||||
search_bar_text: str = ""
|
||||
active_user: Optional[User] = None
|
||||
transaction_value: float = 0.0
|
||||
transaction_reason: str = ""
|
||||
active_user_balance: str = "0.00 €"
|
||||
|
||||
@on_populate
|
||||
async def on_populate(self) -> None:
|
||||
user_list = await User.find_all().to_list()
|
||||
self.all_users = sorted(user_list, key=lambda u: u.user_name)
|
||||
self.user_list = sorted(user_list, key=lambda u: u.user_name)
|
||||
|
||||
async def on_search_bar_text_changed(self, e: TextInputChangeEvent) -> None:
|
||||
self.user_list = list(filter(lambda user: (e.text.lower() in user.user_name.lower()), self.all_users))
|
||||
|
||||
async def on_user_clicked(self, user: User) -> None:
|
||||
self.active_user = user
|
||||
self.active_user_balance = self.session[AccountingService].make_euro_string_from_decimal(
|
||||
await self.session[AccountingService].get_balance(user.user_name)
|
||||
)
|
||||
|
||||
async def create_debit_transaction(self) -> None:
|
||||
if not self.active_user:
|
||||
return
|
||||
logger.info(f"Trying to remove {self.transaction_value} to {self.active_user.user_name} ({self.transaction_reason})")
|
||||
new_transaction = Transaction(
|
||||
user_name=self.active_user.user_name,
|
||||
value=Decimal(str(self.transaction_value)),
|
||||
is_debit=True,
|
||||
title=self.transaction_reason
|
||||
)
|
||||
try:
|
||||
await new_transaction.save()
|
||||
except Exception as e:
|
||||
logger.error(e)
|
||||
|
||||
self.transaction_value = 0.0
|
||||
self.transaction_reason = ""
|
||||
self.active_user_balance = self.session[AccountingService].make_euro_string_from_decimal(
|
||||
await self.session[AccountingService].get_balance(self.active_user.user_name)
|
||||
)
|
||||
|
||||
async def create_credit_transaction(self) -> None:
|
||||
if not self.active_user:
|
||||
return
|
||||
logger.info(f"Trying to add {self.transaction_value} to {self.active_user.user_name} ({self.transaction_reason})")
|
||||
value = Decimal(str(self.transaction_value))
|
||||
new_transaction = Transaction(
|
||||
user_name=self.active_user.user_name,
|
||||
value=value,
|
||||
is_debit=False,
|
||||
title=self.transaction_reason
|
||||
)
|
||||
try:
|
||||
await new_transaction.save()
|
||||
except Exception as e:
|
||||
logger.error(e)
|
||||
|
||||
self.transaction_value = 0.0
|
||||
self.transaction_reason = ""
|
||||
total_balance = await self.session[AccountingService].get_balance(self.active_user.user_name)
|
||||
self.active_user_balance = self.session[AccountingService].make_euro_string_from_decimal(total_balance)
|
||||
|
||||
self.session.create_task(self.session[MailingService].send_email(
|
||||
subject="Dein Guthaben wurde aufgeladen!",
|
||||
body=self.session[MailingService].generate_account_balance_added_mail_body(user=self.active_user, added_balance=value, total_balance=total_balance),
|
||||
receiver=self.active_user.user_mail
|
||||
))
|
||||
|
||||
def build(self) -> Component:
|
||||
right_panel_contents = []
|
||||
if not self.active_user:
|
||||
right_panel_contents.append(Spacer())
|
||||
else:
|
||||
right_panel_contents.extend([
|
||||
AccountInfoBox(fixed_user=self.active_user),
|
||||
Rectangle(
|
||||
content=Column(
|
||||
Rectangle(
|
||||
content=Rectangle(
|
||||
content=Text(f"LAN Konto - Kontostand: {self.active_user_balance}", margin=0.5, selectable=False, overflow="wrap"),
|
||||
fill=self.session.theme.header_box_background_color,
|
||||
margin=0.4
|
||||
),
|
||||
stroke_width=0.1,
|
||||
stroke_color=self.session.theme.box_border_color,
|
||||
),
|
||||
Column(
|
||||
NumberInput(
|
||||
value=self.bind().transaction_value,
|
||||
label="Betrag",
|
||||
suffix_text="€",
|
||||
decimals=2,
|
||||
margin=1,
|
||||
margin_bottom=0
|
||||
),
|
||||
TextInput(
|
||||
text=self.bind().transaction_reason,
|
||||
label="Beschreibung",
|
||||
margin=1,
|
||||
margin_bottom=0
|
||||
),
|
||||
Row(
|
||||
Button(
|
||||
content="Entfernen",
|
||||
shape="rectangle",
|
||||
color="danger",
|
||||
margin=1,
|
||||
on_press=self.create_debit_transaction
|
||||
),
|
||||
Button(
|
||||
content="Hinzufügen",
|
||||
shape="rectangle",
|
||||
color="success",
|
||||
margin=1,
|
||||
on_press=self.create_credit_transaction
|
||||
)
|
||||
),
|
||||
margin=1,
|
||||
spacing=1
|
||||
),
|
||||
Spacer()
|
||||
),
|
||||
fill=self.session.theme.box_color,
|
||||
stroke_width=0.1,
|
||||
stroke_color=self.session.theme.box_border_color
|
||||
),
|
||||
])
|
||||
|
||||
|
||||
|
||||
return Row(
|
||||
Rectangle(
|
||||
content=Column(
|
||||
Rectangle(
|
||||
content=Rectangle(
|
||||
content=Text("Nutzerliste", margin=0.5, selectable=False, overflow="wrap"),
|
||||
fill=self.session.theme.header_box_background_color,
|
||||
margin=0.4
|
||||
),
|
||||
stroke_width=0.1,
|
||||
stroke_color=self.session.theme.box_border_color,
|
||||
),
|
||||
Column(
|
||||
TextInput(label="Nutzername", text=self.bind().search_bar_text, on_change=self.on_search_bar_text_changed, margin_bottom=1),
|
||||
*[Button(content=user.user_name, shape="rectangle", style="plain-text", on_press=partial(self.on_user_clicked, user)) for user in self.user_list],
|
||||
margin=1
|
||||
),
|
||||
Spacer()
|
||||
),
|
||||
fill=self.session.theme.box_color,
|
||||
stroke_width=0.1,
|
||||
stroke_color=self.session.theme.box_border_color,
|
||||
min_width=25
|
||||
),
|
||||
Rectangle(
|
||||
content=Column(
|
||||
Rectangle(
|
||||
content=Rectangle(
|
||||
content=Row(
|
||||
Text(f"Nutzer bearbeiten{': ' if self.active_user else ''}{self.active_user.user_name if self.active_user else ''}", margin=0.5, selectable=False, overflow="wrap", grow_x=True),
|
||||
IconButton("material/cancel", min_size=1.5, color="primary", margin_right=1, on_press=lambda: self.__setattr__("active_user", None)),
|
||||
),
|
||||
fill=self.session.theme.header_box_background_color,
|
||||
margin=0.4
|
||||
),
|
||||
stroke_width=0.1,
|
||||
stroke_color=self.session.theme.box_border_color,
|
||||
),
|
||||
Column(
|
||||
*right_panel_contents,
|
||||
margin=1,
|
||||
spacing=1
|
||||
),
|
||||
Spacer()
|
||||
),
|
||||
fill=self.session.theme.box_color,
|
||||
stroke_width=0.1,
|
||||
stroke_color=self.session.theme.box_border_color,
|
||||
grow_x=True
|
||||
),
|
||||
spacing=1,
|
||||
margin=1
|
||||
)
|
||||
Reference in New Issue
Block a user