add news mananger

This commit is contained in:
David Rodenkirchen 2024-11-28 18:52:51 +01:00
parent 48ad800853
commit 947a05ad14
10 changed files with 278 additions and 10 deletions

View File

@ -9,7 +9,7 @@ from rio import App, Theme, Color, Font, ComponentPage, Session
from from_root import from_root from from_root import from_root
from src.ez_lan_manager import pages, init_services from src.ez_lan_manager import pages, init_services
from src.ez_lan_manager.helpers.LoggedInGuard import logged_in_guard, not_logged_in_guard from src.ez_lan_manager.helpers.LoggedInGuard import logged_in_guard, not_logged_in_guard, team_guard
from src.ez_lan_manager.services.DatabaseService import NoDatabaseConnectionError from src.ez_lan_manager.services.DatabaseService import NoDatabaseConnectionError
from src.ez_lan_manager.types.SessionStorage import SessionStorage from src.ez_lan_manager.types.SessionStorage import SessionStorage
@ -131,6 +131,12 @@ if __name__ == "__main__":
build=pages.AccountPage, build=pages.AccountPage,
guard=logged_in_guard guard=logged_in_guard
), ),
ComponentPage(
name="ManageNewsPage",
url_segment="manage-news",
build=pages.ManageNewsPage,
guard=team_guard
),
ComponentPage( ComponentPage(
name="DbErrorPage", name="DbErrorPage",
url_segment="db-error", url_segment="db-error",

View File

@ -44,10 +44,10 @@ class DesktopNavigation(Component):
team_navigation = [ team_navigation = [
Text("Verwaltung", align_x=0.5, margin_top=0.3, style=TextStyle(fill=Color.from_hex("F0EADE"), font_size=1.2)), Text("Verwaltung", align_x=0.5, margin_top=0.3, style=TextStyle(fill=Color.from_hex("F0EADE"), font_size=1.2)),
Text("Vorsichtig sein!", align_x=0.5, margin_top=0.3, style=TextStyle(fill=self.session.theme.danger_color, font_size=0.6)), Text("Vorsichtig sein!", align_x=0.5, margin_top=0.3, style=TextStyle(fill=self.session.theme.danger_color, font_size=0.6)),
DesktopNavigationButton("News", "./manage_news", is_team_navigation=True), DesktopNavigationButton("News", "./manage-news", is_team_navigation=True),
DesktopNavigationButton("Benutzer", "./manage_users", is_team_navigation=True), DesktopNavigationButton("Benutzer", "./manage-users", is_team_navigation=True),
DesktopNavigationButton("Catering", "./manage_catering", is_team_navigation=True), DesktopNavigationButton("Catering", "./manage-catering", is_team_navigation=True),
DesktopNavigationButton("Turniere", "./manage_tournaments", is_team_navigation=True), DesktopNavigationButton("Turniere", "./manage-tournaments", is_team_navigation=True),
Spacer(min_height=1), Spacer(min_height=1),
Revealer( Revealer(
header="Normale Navigation", header="Normale Navigation",

View File

@ -4,6 +4,7 @@ from rio import Component, TextStyle, Color, TextInput, Button, Text, Rectangle,
from src.ez_lan_manager.services.UserService import UserService from src.ez_lan_manager.services.UserService import UserService
from src.ez_lan_manager.types.SessionStorage import SessionStorage from src.ez_lan_manager.types.SessionStorage import SessionStorage
from src.ez_lan_manager.types.User import User
class LoginBox(Component): class LoginBox(Component):
@ -17,10 +18,11 @@ class LoginBox(Component):
async def _on_login_pressed(self) -> None: async def _on_login_pressed(self) -> None:
if await self.session[UserService].is_login_valid(self.user_name_input_text[0], self.password_input_text[0]): if await self.session[UserService].is_login_valid(self.user_name_input_text[0], self.password_input_text[0]):
user: User = await self.session[UserService].get_user(self.user_name_input_text[0])
self.user_name_input_is_valid = True self.user_name_input_is_valid = True
self.password_input_is_valid = True self.password_input_is_valid = True
self.login_button_is_loading = False self.login_button_is_loading = False
await self.session[SessionStorage].set_user_id((await self.session[UserService].get_user(self.user_name_input_text[0])).user_id) await self.session[SessionStorage].set_user_id_and_team_member_flag(user.user_id, user.is_team_member)
await self.status_change_cb() await self.status_change_cb()
else: else:
self.user_name_input_is_valid = False self.user_name_input_is_valid = False

View File

@ -1,4 +1,8 @@
from rio import Component, Rectangle, Text, TextStyle, Column, Row from datetime import datetime
from functools import partial
from typing import Optional, Callable
from rio import Component, Rectangle, Text, TextStyle, Column, Row, TextInput, DateInput, MultiLineTextInput, IconButton, Color, Button
class NewsPost(Component): class NewsPost(Component):
@ -79,3 +83,67 @@ class NewsPost(Component):
shadow_offset_y=0, shadow_offset_y=0,
corner_radius=0.2 corner_radius=0.2
) )
class EditableNewsPost(NewsPost):
news_id: int = -1
save_cb: Callable = lambda _: None
delete_cb: Callable = lambda _: None
def set_prop(self, prop, value) -> None:
self.__setattr__(prop, value)
def build(self) -> Component:
return Rectangle(
content=Column(
Row(
TextInput(
text=self.title,
label="Titel",
style="rounded",
min_width=15,
on_change=lambda e: self.set_prop("title", e.text)
),
DateInput(
value=datetime.strptime(self.date, "%d.%m.%Y"),
style="rounded",
on_change=lambda e: self.set_prop("date", e.value.strftime("%d.%m.%Y"))
)
),
TextInput(
text=self.subtitle,
label="Untertitel",
style="rounded",
grow_x=True,
on_change=lambda e: self.set_prop("subtitle", e.text)
),
MultiLineTextInput(
text=self.text,
label="Text",
style="rounded",
grow_x=True,
min_height=12,
on_change=lambda e: self.set_prop("text", e.text)
),
Row(
TextInput(
text=self.author,
label="Autor",
style="rounded",
grow_x=True,
on_change=lambda e: self.set_prop("author", e.text)
),
Rectangle(content=Button(icon="material/delete", style="major", color="danger", shape="rectangle", on_press=partial(self.delete_cb, self.news_id)), fill=Color.from_hex("0b7372")),
Rectangle(content=Button(icon="material/save", style="major", color="success", shape="rectangle", on_press=partial(self.save_cb, self)), fill=Color.from_hex("0b7372"))
)
),
fill=self.session.theme.primary_color,
margin_left=1,
margin_right=1,
margin_top=2,
margin_bottom=1,
shadow_radius=0.2,
shadow_color=self.session.theme.background_color,
shadow_offset_y=0,
corner_radius=0.2
)

View File

@ -1,7 +1,8 @@
from typing import Optional from typing import Optional
from rio import Session, URL, GuardEvent from rio import URL, GuardEvent
from src.ez_lan_manager.services.UserService import UserService
from src.ez_lan_manager.types.SessionStorage import SessionStorage from src.ez_lan_manager.types.SessionStorage import SessionStorage
@ -14,3 +15,10 @@ def logged_in_guard(event: GuardEvent) -> Optional[URL]:
def not_logged_in_guard(event: GuardEvent) -> Optional[URL]: def not_logged_in_guard(event: GuardEvent) -> Optional[URL]:
if event.session[SessionStorage].user_id is not None: if event.session[SessionStorage].user_id is not None:
return URL("./") return URL("./")
# Guards pages against access from users that are NOT logged in and NOT team members
def team_guard(event: GuardEvent) -> Optional[URL]:
user_id = event.session[SessionStorage].user_id
is_team_member = event.session[SessionStorage].is_team_member
if user_id is None or not is_team_member:
return URL("./")

View File

@ -0,0 +1,131 @@
import logging
from asyncio import sleep
from datetime import datetime
from time import strptime
from rio import Column, Component, event, TextStyle, Text
from src.ez_lan_manager import ConfigurationService, UserService
from src.ez_lan_manager.components.MainViewContentBox import MainViewContentBox
from src.ez_lan_manager.components.NewsPost import EditableNewsPost
from src.ez_lan_manager.services.NewsService import NewsService
from src.ez_lan_manager.types.News import News
logger = logging.getLogger(__name__.split(".")[-1])
class ManageNewsPage(Component):
news_posts: list[News] = []
show_success_message = False
@event.on_populate
async def on_populate(self) -> None:
await self.session.set_title(f"{self.session[ConfigurationService].get_lan_info().name} - News Verwaltung")
self.news_posts = (await self.session[NewsService].get_news())[:8]
async def on_new_news_post(self, post: EditableNewsPost) -> None:
# @todo: For some reason, new posts do not appear through a force_refresh, only after visiting the page again
author = await self.session[UserService].get_user(post.author)
if author is None:
logger.warning(f"Tried to set news post author to '{post.author}', which does not exist.")
return
await self.session[NewsService].add_news(News(
news_id=None,
title=post.title,
subtitle=post.subtitle,
content=post.text,
author=author,
news_date=strptime(post.date, "%d.%m.%Y"),
))
self.news_posts = (await self.session[NewsService].get_news())[:8]
self.show_success_message = True
await self.force_refresh()
await sleep(3)
self.show_success_message = False
await self.force_refresh()
async def on_news_post_changed(self, post: EditableNewsPost) -> None:
author = await self.session[UserService].get_user(post.author)
if author is None:
logger.warning(f"Tried to set news post author to '{post.author}', which does not exist.")
return
await self.session[NewsService].update_news(News(
news_id=post.news_id,
title=post.title,
subtitle=post.subtitle,
content=post.text,
author=author,
news_date=strptime(post.date, "%d.%m.%Y"),
))
self.news_posts = (await self.session[NewsService].get_news())[:8]
async def on_news_post_deleted(self, news_id: int) -> None:
await self.session[NewsService].delete_news(news_id)
self.news_posts = (await self.session[NewsService].get_news())[:8]
def build(self) -> Component:
posts = sorted([EditableNewsPost(
news_id=news.news_id,
title=news.title,
subtitle=news.subtitle,
text=news.content,
date=news.news_date.strftime("%d.%m.%Y"),
author=news.author.user_name,
save_cb=self.on_news_post_changed,
delete_cb=self.on_news_post_deleted
) for news in self.news_posts], key=lambda p: p.date)
return Column(
MainViewContentBox(
Column(
Text(
text="News Verwaltung",
style=TextStyle(
fill=self.session.theme.background_color,
font_size=1.2
),
margin_top=2,
margin_bottom=0,
align_x=0.5
),
Text(
text="Neuen News Post erstellen",
style=TextStyle(
fill=self.session.theme.background_color,
font_size=1.1
),
margin_top=2,
margin_bottom=0,
align_x=0.5
),
EditableNewsPost(
title="",
subtitle="",
text="",
date=datetime.now().strftime("%d.%m.%Y"),
author="",
save_cb=self.on_new_news_post
),
Text(
text="Post erfolgreich erstellt",
style=TextStyle(
fill=self.session.theme.success_color,
font_size=0.7 if self.show_success_message else 0
),
margin_top=0.1,
margin_bottom=0,
align_x=0.5
),
Text(
text="Bisherige Posts",
style=TextStyle(
fill=self.session.theme.background_color,
font_size=1.1
),
margin_top=2,
margin_bottom=0,
align_x=0.5
),
*posts
)
),
align_y=0
)

View File

@ -15,3 +15,4 @@ from .CateringPage import CateringPage
from .DbErrorPage import DbErrorPage from .DbErrorPage import DbErrorPage
from .SeatingPlanPage import SeatingPlanPage from .SeatingPlanPage import SeatingPlanPage
from .BuyTicketPage import BuyTicketPage from .BuyTicketPage import BuyTicketPage
from .ManageNewsPage import ManageNewsPage

View File

@ -246,7 +246,47 @@ class DatabaseService:
return results return results
async def update_news(self, news: News) -> None:
async with self._connection_pool.acquire() as conn:
async with conn.cursor(aiomysql.Cursor) as cursor:
try:
await cursor.execute(
"""
UPDATE news
SET news_content = %s,
news_title = %s,
news_subtitle = %s,
news_author = %s,
news_date = %s
WHERE news_id = %s
""",
(news.content, news.title, news.subtitle, news.author.user_id, news.news_date, news.news_id)
)
await conn.commit()
except aiomysql.InterfaceError:
pool_init_result = await self.init_db_pool()
if not pool_init_result:
raise NoDatabaseConnectionError
return await self.update_news(news)
except Exception as e:
logger.warning(f"Error updating news: {e}")
async def remove_news(self, news_id: int) -> None:
async with self._connection_pool.acquire() as conn:
async with conn.cursor(aiomysql.Cursor) as cursor:
try:
await cursor.execute(
"DELETE FROM news WHERE news_id = %s",
(news_id,)
)
await conn.commit()
except aiomysql.InterfaceError:
pool_init_result = await self.init_db_pool()
if not pool_init_result:
raise NoDatabaseConnectionError
return await self.remove_news(news_id)
except Exception as e:
logger.warning(f"Error removing news with ID {news_id}: {e}")
async def get_tickets(self) -> list[Ticket]: async def get_tickets(self) -> list[Ticket]:
async with self._connection_pool.acquire() as conn: async with self._connection_pool.acquire() as conn:

View File

@ -24,6 +24,12 @@ class NewsService:
dt_start = date(1900, 1, 1) dt_start = date(1900, 1, 1)
return await self._db_service.get_news(dt_start, dt_end) return await self._db_service.get_news(dt_start, dt_end)
async def update_news(self, news: News) -> None:
return await self._db_service.update_news(news)
async def delete_news(self, news_id: int) -> None:
return await self._db_service.remove_news(news_id)
async def get_latest_news(self) -> Optional[News]: async def get_latest_news(self) -> Optional[News]:
try: try:
all_news = await self.get_news(None, date.today()) all_news = await self.get_news(None, date.today())

View File

@ -11,10 +11,11 @@ logger = logging.getLogger(__name__.split(".")[-1])
@dataclass(frozen=False) @dataclass(frozen=False)
class SessionStorage: class SessionStorage:
_user_id: Optional[int] = None # DEBUG: Put user ID here to skip login _user_id: Optional[int] = None # DEBUG: Put user ID here to skip login
_is_team_member: bool = False
_notification_callbacks: dict[str, Callable] = field(default_factory=dict) _notification_callbacks: dict[str, Callable] = field(default_factory=dict)
async def clear(self) -> None: async def clear(self) -> None:
await self.set_user_id(None) await self.set_user_id_and_team_member_flag(None, False)
def subscribe_to_logged_in_or_out_event(self, component_id: str, callback: Callable) -> None: def subscribe_to_logged_in_or_out_event(self, component_id: str, callback: Callable) -> None:
self._notification_callbacks[component_id] = callback self._notification_callbacks[component_id] = callback
@ -23,8 +24,13 @@ class SessionStorage:
def user_id(self) -> Optional[int]: def user_id(self) -> Optional[int]:
return self._user_id return self._user_id
async def set_user_id(self, user_id: Optional[int]) -> None: @property
def is_team_member(self) -> bool:
return self._is_team_member
async def set_user_id_and_team_member_flag(self, user_id: Optional[int], is_team_member: bool) -> None:
self._user_id = user_id self._user_id = user_id
self._is_team_member = is_team_member
for component_id, callback in self._notification_callbacks.items(): for component_id, callback in self._notification_callbacks.items():
logger.debug(f"Calling logged in callback from {component_id}") logger.debug(f"Calling logged in callback from {component_id}")
await callback() await callback()