diff --git a/src/ez_lan_manager/components/UserEditForm.py b/src/ez_lan_manager/components/UserEditForm.py new file mode 100644 index 0000000..cb53b00 --- /dev/null +++ b/src/ez_lan_manager/components/UserEditForm.py @@ -0,0 +1,250 @@ +from datetime import date +from hashlib import sha256 +from typing import Optional + +from email_validator import validate_email, EmailNotValidError +from from_root import from_root +from rio import Component, Column, Button, Color, TextStyle, Text, TextInput, Row, Image, event, Spacer, DateInput, \ + TextInputChangeEvent, NoFileSelectedError + +from src.ez_lan_manager.services.UserService import UserService, NameNotAllowedError +from src.ez_lan_manager.services.ConfigurationService import ConfigurationService +from src.ez_lan_manager.types.SessionStorage import SessionStorage +from src.ez_lan_manager.types.User import User + + +class UserEditForm(Component): + is_own_profile: bool = True + profile_picture: Optional[bytes] = None + user: Optional[User] = None + + input_user_name: str = "" + input_user_mail: str = "" + input_user_first_name: str = "" + input_user_last_name: str = "" + input_password_1: str = "" + input_password_2: str = "" + input_birthday: date = date.today() + + is_email_valid: bool = True + + result_text: str = "" + result_success: bool = True + + @event.on_populate + async def on_populate(self) -> None: + await self.session.set_title(f"{self.session[ConfigurationService].get_lan_info().name} - Profil bearbeiten") + if self.is_own_profile: + self.user = await self.session[UserService].get_user(self.session[SessionStorage].user_id) + self.profile_picture = await self.session[UserService].get_profile_picture(self.user.user_id) + else: + self.profile_picture = await self.session[UserService].get_profile_picture(self.user.user_id) + + self.input_user_name = self.user.user_name + self.input_user_mail = self.user.user_mail + self.input_user_first_name = self.optional_str_to_str(self.user.user_first_name) + self.input_user_last_name = self.optional_str_to_str(self.user.user_last_name) + self.input_birthday = self.user.user_birth_day if self.user.user_birth_day else date.today() + + + @staticmethod + def optional_str_to_str(s: Optional[str]) -> str: + if s: + return s + return "" + + def on_email_changed(self, change_event: TextInputChangeEvent) -> None: + try: + validate_email(change_event.text, check_deliverability=False) + self.is_email_valid = True + except EmailNotValidError: + self.is_email_valid = False + + async def upload_new_pfp(self) -> None: + try: + new_pfp = await self.session.pick_file(file_types=("png", "jpg", "jpeg"), multiple=False) + except NoFileSelectedError: + self.result_text = "Keine Datei ausgewählt!" + self.result_success = False + return + + if new_pfp.size_in_bytes > 2 * 1_000_000: + self.result_text = "Bild zu groß! (> 2MB)" + self.result_success = False + return + + image_data = await new_pfp.read_bytes() + await self.session[UserService].set_profile_picture(self.user.user_id, image_data) + self.profile_picture = image_data + self.result_text = "Gespeichert!" + self.result_success = True + + async def remove_profile_picture(self) -> None: + await self.session[UserService].remove_profile_picture(self.user.user_id) + self.profile_picture = None + self.result_text = "Profilbild entfernt!" + self.result_success = True + + async def on_save_pressed(self) -> None: + if not all((self.is_email_valid, self.input_user_name, self.input_user_mail)): + self.result_text = "Ungültige Werte!" + self.result_success = False + return + + if len(self.input_password_1.strip()) > 0: + if self.input_password_1.strip() != self.input_password_2.strip(): + self.result_text = "Passwörter nicht gleich!" + self.result_success = False + return + + self.user.user_mail = self.input_user_mail + + if self.input_birthday == date.today(): + self.user.user_birth_day = None + else: + self.user.user_birth_day = self.input_birthday + + self.user.user_first_name = self.input_user_first_name + self.user.user_last_name = self.input_user_last_name + self.user.user_name = self.input_user_name + if len(self.input_password_1.strip()) > 0: + self.user.user_password = sha256(self.input_password_1.strip().encode(encoding="utf-8")).hexdigest() + + try: + await self.session[UserService].update_user(self.user) + except NameNotAllowedError: + self.result_text = "Ungültige Zeichen in Nutzername" + self.result_success = False + return + + self.result_text = "Gespeichert!" + self.result_success = True + + def build(self) -> Component: + pfp_image_container = Image( + from_root("src/ez_lan_manager/assets/img/anon_pfp.png") if self.profile_picture is None else self.profile_picture, + align_x=0.5, + min_width=10, + min_height=10, + margin_top=1, + margin_bottom=1 + ) + + return Column( + pfp_image_container, + Button( + content=Text( + "Neues Bild hochladen", + style=TextStyle(fill=Color.from_hex("02dac5"), font_size=0.9) + ), + align_x=0.5, + margin_bottom=1, + shape="rectangle", + style="major", + color="primary", + on_press=self.upload_new_pfp + ) if self.is_own_profile else Button( + content=Text( + "Bild löschen", + style=TextStyle(fill=Color.from_hex("02dac5"), font_size=0.9) + ), + align_x=0.5, + margin_bottom=1, + shape="rectangle", + style="major", + color="primary", + on_press=self.remove_profile_picture + ), + Row( + TextInput( + label=f"{'Deine ' if self.is_own_profile else ''}User-ID", + text=str(self.user.user_id), + is_sensitive=False, + margin_left=1, + grow_x=False + ), + TextInput( + label=f"{'Dein ' if self.is_own_profile else ''}Nickname", + text=self.bind().input_user_name, + is_sensitive=not self.is_own_profile, + margin_left=1, + margin_right=1, + grow_x=True + ), + margin_bottom=1 + ), + TextInput( + label="E-Mail Adresse", + text=self.bind().input_user_mail, + margin_left=1, + margin_right=1, + margin_bottom=1, + grow_x=True, + is_valid=self.is_email_valid, + on_change=self.on_email_changed + ), + Row( + TextInput( + label="Vorname", + text=self.bind().input_user_first_name, + margin_left=1, + margin_right=1, + grow_x=True + ), + TextInput( + label="Nachname", + text=self.bind().input_user_last_name, + margin_right=1, + grow_x=True + ), + margin_bottom=1 + ), + DateInput( + value=self.bind().input_birthday, + label="Geburtstag", + margin_left=1, + margin_right=1, + margin_bottom=1, + grow_x=True + ), + TextInput( + label="Neues Passwort setzen", + text=self.bind().input_password_1, + margin_left=1, + margin_right=1, + margin_bottom=1, + grow_x=True, + is_secret=True + ), + TextInput( + label="Neues Passwort wiederholen", + text=self.bind().input_password_2, + margin_left=1, + margin_right=1, + margin_bottom=1, + grow_x=True, + is_secret=True + ), + + Row( + Text( + text=self.bind().result_text, + style=TextStyle(fill=self.session.theme.success_color if self.result_success else self.session.theme.danger_color), + margin_left=1 + ), + Button( + content=Text( + "Speichern", + style=TextStyle(fill=self.session.theme.success_color, font_size=0.9), + align_x=0.2 + ), + align_x=0.9, + margin_top=2, + margin_bottom=1, + shape="rectangle", + style="major", + color="primary", + on_press=self.on_save_pressed + ), + ) + ) if self.user else Spacer() \ No newline at end of file diff --git a/src/ez_lan_manager/pages/EditProfile.py b/src/ez_lan_manager/pages/EditProfile.py index 7bb8aab..7dfa603 100644 --- a/src/ez_lan_manager/pages/EditProfile.py +++ b/src/ez_lan_manager/pages/EditProfile.py @@ -1,15 +1,10 @@ -from datetime import date, datetime -from hashlib import sha256 from typing import Optional -from from_root import from_root -from rio import Column, Component, event, Text, TextStyle, Button, Color, Row, TextInput, Image, TextInputChangeEvent, NoFileSelectedError, \ - ProgressCircle -from email_validator import validate_email, EmailNotValidError +from rio import Column, Component, event, Spacer from src.ez_lan_manager import ConfigurationService, UserService -from src.ez_lan_manager.components.AnimatedText import AnimatedText from src.ez_lan_manager.components.MainViewContentBox import MainViewContentBox +from src.ez_lan_manager.components.UserEditForm import UserEditForm from src.ez_lan_manager.types.SessionStorage import SessionStorage from src.ez_lan_manager.types.User import User @@ -18,210 +13,14 @@ class EditProfilePage(Component): user: Optional[User] = None pfp: Optional[bytes] = None - @staticmethod - def optional_date_to_str(d: Optional[date]) -> str: - if not d: - return "" - return d.strftime("%d.%m.%Y") - @event.on_populate async def on_populate(self) -> None: await self.session.set_title(f"{self.session[ConfigurationService].get_lan_info().name} - Profil bearbeiten") self.user = await self.session[UserService].get_user(self.session[SessionStorage].user_id) self.pfp = await self.session[UserService].get_profile_picture(self.user.user_id) - def on_email_changed(self, change_event: TextInputChangeEvent) -> None: - try: - validate_email(change_event.text, check_deliverability=False) - self.email_input.is_valid = True - except EmailNotValidError: - self.email_input.is_valid = False - - def on_birthday_changed(self, change_event: TextInputChangeEvent) -> None: - if len(change_event.text) == 0: - self.birthday_input.is_valid = True - return - try: - day, month, year = change_event.text.split(".") - year = int(year) - if year < 1900 or year > datetime.now().year - 12: - raise ValueError - date(day=int(day), month=int(month), year=year) - self.birthday_input.is_valid = True - except (ValueError, TypeError, IndexError): - self.birthday_input.is_valid = False - - async def upload_new_pfp(self) -> None: - try: - new_pfp = await self.session.file_chooser(file_extensions=("png", "jpg", "jpeg"), multiple=False) - except NoFileSelectedError: - await self.animated_text.display_text(False, "Keine Datei ausgewählt!") - return - - if new_pfp.size_in_bytes > 2 * 1_000_000: - await self.animated_text.display_text(False, "Bild zu groß! (> 2MB)") - return - - image_data = await new_pfp.read_bytes() - await self.session[UserService].set_profile_picture(self.session[SessionStorage].user_id, image_data) - self.pfp_image_container.image = image_data - await self.animated_text.display_text(True, "Gespeichert!") - - async def on_save_pressed(self) -> None: - if not all((self.email_input.is_valid, self.birthday_input.is_valid)): - await self.animated_text.display_text(False, "Ungültige Werte!") - return - - if len(self.new_pw_1_input.text.strip()) > 0: - if self.new_pw_1_input.text.strip() != self.new_pw_2_input.text.strip(): - await self.animated_text.display_text(False, "Passwörter nicht gleich!") - return - - user: User = await self.session[UserService].get_user(self.session[SessionStorage].user_id) - user.user_mail = self.email_input.text - - if len(self.birthday_input.text) == 0: - user.user_birth_day = None - else: - day, month, year = self.birthday_input.text.split(".") - user.user_birth_day = date(day=int(day), month=int(month), year=int(year)) - - user.user_first_name = self.first_name_input.text - user.user_last_name = self.last_name_input.text - if len(self.new_pw_1_input.text.strip()) > 0: - user.user_password = sha256(self.new_pw_1_input.text.encode(encoding="utf-8")).hexdigest() - - await self.session[UserService].update_user(user) - await self.animated_text.display_text(True, "Gespeichert!") - def build(self) -> Component: - if not self.user: - return Column( - MainViewContentBox( - ProgressCircle( - color="secondary", - align_x=0.5, - margin_top=2, - margin_bottom=2 - ) - ), - align_y=0 - ) - - self.animated_text = AnimatedText( - margin_top=2, - margin_bottom=1, - align_x=0.1 - ) - - self.email_input = TextInput( - label="E-Mail Adresse", - text=self.user.user_mail, - margin_left=1, - margin_right=1, - margin_bottom=1, - grow_x=True, - on_change=self.on_email_changed - ) - self.first_name_input = TextInput( - label="Vorname", - text=self.user.user_first_name, - margin_left=1, - margin_right=1, - grow_x=True - ) - self.last_name_input = TextInput( - label="Nachname", - text=self.user.user_last_name, - margin_right=1, - grow_x=True - ) - self.birthday_input = TextInput( - label="Geburtstag (TT.MM.JJJJ)", - text=self.optional_date_to_str(self.user.user_birth_day), - margin_left=1, - margin_right=1, - margin_bottom=1, - grow_x=True, - on_change=self.on_birthday_changed - ) - self.new_pw_1_input = TextInput( - label="Neues Passwort setzen", - text="", - margin_left=1, - margin_right=1, - margin_bottom=1, - grow_x=True, - is_secret=True - ) - self.new_pw_2_input = TextInput( - label="Neues Passwort wiederholen", - text="", - margin_left=1, - margin_right=1, - margin_bottom=1, - grow_x=True, - is_secret=True - ) - - self.pfp_image_container = Image( - from_root("src/ez_lan_manager/assets/img/anon_pfp.png") if self.pfp is None else self.pfp, - align_x=0.5, - min_width=10, - min_height=10, - margin_top=1, - margin_bottom=1 - ) - return Column( - MainViewContentBox( - content=Column( - self.pfp_image_container, - Button( - content=Text( - "Neues Bild hochladen", - style=TextStyle(fill=Color.from_hex("02dac5"), font_size=0.9) - ), - align_x=0.5, - margin_bottom=1, - shape="rectangle", - style="major", - color="primary", - on_press=self.upload_new_pfp - ), - Row( - TextInput(label="Deine User-ID", text=self.user.user_id, is_sensitive=False, margin_left=1, grow_x=False), - TextInput(label="Dein Nickname", text=self.user.user_name, is_sensitive=False, margin_left=1, margin_right=1, grow_x=True), - margin_bottom=1 - ), - self.email_input, - Row( - self.first_name_input, - self.last_name_input, - margin_bottom=1 - ), - self.birthday_input, - self.new_pw_1_input, - self.new_pw_2_input, - - Row( - self.animated_text, - Button( - content=Text( - "Speichern", - style=TextStyle(fill=self.session.theme.success_color, font_size=0.9), - align_x=0.2 - ), - align_x=0.9, - margin_top=2, - margin_bottom=1, - shape="rectangle", - style="major", - color="primary", - on_press=self.on_save_pressed - ), - ) - ) - ), - align_y=0, + MainViewContentBox(UserEditForm(is_own_profile=True)), + Spacer(grow_y=True) ) diff --git a/src/ez_lan_manager/pages/ManageUsersPage.py b/src/ez_lan_manager/pages/ManageUsersPage.py index 0a92073..2e143a0 100644 --- a/src/ez_lan_manager/pages/ManageUsersPage.py +++ b/src/ez_lan_manager/pages/ManageUsersPage.py @@ -4,10 +4,11 @@ from typing import Optional, Coroutine import rio from rio import Column, Component, event, TextStyle, Text, TextInput, ThemeContextSwitcher, Grid, \ - PointerEventListener, PointerEvent, Rectangle, CursorStyle, Color, TextInputChangeEvent + PointerEventListener, PointerEvent, Rectangle, CursorStyle, Color, TextInputChangeEvent, Spacer from src.ez_lan_manager import ConfigurationService, UserService from src.ez_lan_manager.components.MainViewContentBox import MainViewContentBox +from src.ez_lan_manager.components.UserEditForm import UserEditForm from src.ez_lan_manager.types.User import User logger = logging.getLogger(__name__.split(".")[-1]) @@ -104,24 +105,29 @@ class ManageUsersPage(Component): ) ), MainViewContentBox( - Text( - text=f"Nutzer {self.selected_user.user_name} gewählt.", - style=TextStyle( - fill=self.session.theme.background_color, - font_size=1.2 - ), - margin_top=2, - margin_bottom=2, - align_x=0.5 - ) if self.selected_user else Text( - text="Bitte Nutzer auswählen...", - style=TextStyle( - fill=self.session.theme.background_color, - font_size=1.2 - ), - margin_top=2, - margin_bottom=2, - align_x=0.5 - )), + Column( + Text( + text="Allgemeines", + style=TextStyle( + fill=self.session.theme.background_color, + font_size=1.2 + ), + margin_top=2, + margin_bottom=2, + align_x=0.5 + ) if self.selected_user else Spacer(), + UserEditForm( + is_own_profile=False, + user=self.selected_user + ) if self.selected_user else Text( + text="Bitte Nutzer auswählen...", + style=TextStyle( + fill=self.session.theme.background_color, + font_size=1.2 + ), + margin_top=2, + margin_bottom=2, + align_x=0.5 + ))), align_y=0 ) diff --git a/src/ez_lan_manager/services/DatabaseService.py b/src/ez_lan_manager/services/DatabaseService.py index a917edd..dd8619f 100644 --- a/src/ez_lan_manager/services/DatabaseService.py +++ b/src/ez_lan_manager/services/DatabaseService.py @@ -754,3 +754,20 @@ class DatabaseService: results.append(self._map_db_result_to_user(user_raw)) return results + + async def remove_profile_picture(self, user_id: int): + async with self._connection_pool.acquire() as conn: + async with conn.cursor(aiomysql.Cursor) as cursor: + try: + await cursor.execute( + "DELETE FROM user_profile_picture WHERE user_id = %s", + user_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_profile_picture(user_id) + except Exception as e: + logger.warning(f"Error deleting user profile picture: {e}") diff --git a/src/ez_lan_manager/services/UserService.py b/src/ez_lan_manager/services/UserService.py index 2120ae4..aa53b7c 100644 --- a/src/ez_lan_manager/services/UserService.py +++ b/src/ez_lan_manager/services/UserService.py @@ -33,6 +33,9 @@ class UserService: async def set_profile_picture(self, user_id: int, picture: bytes) -> None: await self._db_service.set_user_profile_picture(user_id, picture) + async def remove_profile_picture(self, user_id: int) -> None: + await self._db_service.remove_profile_picture(user_id) + async def get_profile_picture(self, user_id: int) -> bytes: return await self._db_service.get_user_profile_picture(user_id)