add news mananger
This commit is contained in:
parent
48ad800853
commit
947a05ad14
@ -9,7 +9,7 @@ from rio import App, Theme, Color, Font, ComponentPage, Session
|
||||
from from_root import from_root
|
||||
|
||||
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.types.SessionStorage import SessionStorage
|
||||
|
||||
@ -131,6 +131,12 @@ if __name__ == "__main__":
|
||||
build=pages.AccountPage,
|
||||
guard=logged_in_guard
|
||||
),
|
||||
ComponentPage(
|
||||
name="ManageNewsPage",
|
||||
url_segment="manage-news",
|
||||
build=pages.ManageNewsPage,
|
||||
guard=team_guard
|
||||
),
|
||||
ComponentPage(
|
||||
name="DbErrorPage",
|
||||
url_segment="db-error",
|
||||
|
||||
@ -44,10 +44,10 @@ class DesktopNavigation(Component):
|
||||
team_navigation = [
|
||||
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)),
|
||||
DesktopNavigationButton("News", "./manage_news", is_team_navigation=True),
|
||||
DesktopNavigationButton("Benutzer", "./manage_users", is_team_navigation=True),
|
||||
DesktopNavigationButton("Catering", "./manage_catering", is_team_navigation=True),
|
||||
DesktopNavigationButton("Turniere", "./manage_tournaments", is_team_navigation=True),
|
||||
DesktopNavigationButton("News", "./manage-news", is_team_navigation=True),
|
||||
DesktopNavigationButton("Benutzer", "./manage-users", is_team_navigation=True),
|
||||
DesktopNavigationButton("Catering", "./manage-catering", is_team_navigation=True),
|
||||
DesktopNavigationButton("Turniere", "./manage-tournaments", is_team_navigation=True),
|
||||
Spacer(min_height=1),
|
||||
Revealer(
|
||||
header="Normale Navigation",
|
||||
|
||||
@ -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.types.SessionStorage import SessionStorage
|
||||
from src.ez_lan_manager.types.User import User
|
||||
|
||||
|
||||
class LoginBox(Component):
|
||||
@ -17,10 +18,11 @@ class LoginBox(Component):
|
||||
|
||||
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]):
|
||||
user: User = await self.session[UserService].get_user(self.user_name_input_text[0])
|
||||
self.user_name_input_is_valid = True
|
||||
self.password_input_is_valid = True
|
||||
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()
|
||||
else:
|
||||
self.user_name_input_is_valid = False
|
||||
|
||||
@ -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):
|
||||
@ -79,3 +83,67 @@ class NewsPost(Component):
|
||||
shadow_offset_y=0,
|
||||
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
|
||||
)
|
||||
|
||||
@ -1,7 +1,8 @@
|
||||
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
|
||||
|
||||
|
||||
@ -14,3 +15,10 @@ def 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:
|
||||
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("./")
|
||||
|
||||
131
src/ez_lan_manager/pages/ManageNewsPage.py
Normal file
131
src/ez_lan_manager/pages/ManageNewsPage.py
Normal 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
|
||||
)
|
||||
@ -15,3 +15,4 @@ from .CateringPage import CateringPage
|
||||
from .DbErrorPage import DbErrorPage
|
||||
from .SeatingPlanPage import SeatingPlanPage
|
||||
from .BuyTicketPage import BuyTicketPage
|
||||
from .ManageNewsPage import ManageNewsPage
|
||||
|
||||
@ -246,7 +246,47 @@ class DatabaseService:
|
||||
|
||||
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 with self._connection_pool.acquire() as conn:
|
||||
|
||||
@ -24,6 +24,12 @@ class NewsService:
|
||||
dt_start = date(1900, 1, 1)
|
||||
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]:
|
||||
try:
|
||||
all_news = await self.get_news(None, date.today())
|
||||
|
||||
@ -11,10 +11,11 @@ logger = logging.getLogger(__name__.split(".")[-1])
|
||||
@dataclass(frozen=False)
|
||||
class SessionStorage:
|
||||
_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)
|
||||
|
||||
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:
|
||||
self._notification_callbacks[component_id] = callback
|
||||
@ -23,8 +24,13 @@ class SessionStorage:
|
||||
def user_id(self) -> Optional[int]:
|
||||
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._is_team_member = is_team_member
|
||||
for component_id, callback in self._notification_callbacks.items():
|
||||
logger.debug(f"Calling logged in callback from {component_id}")
|
||||
await callback()
|
||||
|
||||
Loading…
Reference in New Issue
Block a user