From 0b517f2b308df311bd2337b28042aacc7eb6094a Mon Sep 17 00:00:00 2001 From: Alexander Malzkuhn Date: Mon, 27 Oct 2025 13:47:57 +0100 Subject: [PATCH] =?UTF-8?q?"No=20Time=20Users"=20hinzugef=C3=BCgt.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- create_docker.py | 22 +++---- lib/admin.py | 59 +++++++++++++---- lib/definitions.py | 1 + lib/touchscreen.py | 141 +++++++++++++++++++++-------------------- lib/users.py | 5 ++ lib/web_ui.py | 29 +++++---- requirements.txt | 131 -------------------------------------- settings.json | 5 +- settings/settings.json | 17 +++++ 9 files changed, 170 insertions(+), 240 deletions(-) delete mode 100644 requirements.txt create mode 100644 settings/settings.json diff --git a/create_docker.py b/create_docker.py index e7e6170..d3266a9 100644 --- a/create_docker.py +++ b/create_docker.py @@ -5,13 +5,13 @@ import os server = 'gitea.am-td.de' server_user = 'alexander' -if os.getuid() == 0: - subprocess.run(["docker", "build", "--force-rm", "-t", f"{server}/{server_user}/{app_title.lower()}:{app_version}", "."]) - if input("docker-compose erstellen j=JA ") == "j": - userfolder = input("Pfad für Benutzerdaten /users:") - backupfolder = input("Pfad für Backupdaten /backup:") - settingsfolder = input("Pfad für Einstellungen /settings:") - docker_compose_content = f''' +#if os.getuid() == 0: +subprocess.run(["docker", "build", "--force-rm", "-t", f"{server}/{server_user}/{app_title.lower()}:{app_version}", "."]) +if input("docker-compose erstellen j=JA ") == "j": + userfolder = input("Pfad für Benutzerdaten /users:") + backupfolder = input("Pfad für Backupdaten /backup:") + settingsfolder = input("Pfad für Einstellungen /settings:") + docker_compose_content = f''' services: zeiterfassung: image: {server}/{server_user}/{app_title.lower()}:{app_version.lower()} @@ -25,7 +25,7 @@ services: - {backupfolder}:/backup - {settingsfolder}:/settings''' - with open('docker-compose.yml', 'w') as docker_compose: - docker_compose.write(docker_compose_content) -else: - print("Es werden Root-Rechte benötigt.") + with open('docker-compose.yml', 'w') as docker_compose: + docker_compose.write(docker_compose_content) +#else: +# print("Es werden Root-Rechte benötigt.") diff --git a/lib/admin.py b/lib/admin.py index 17c3df9..4804882 100644 --- a/lib/admin.py +++ b/lib/admin.py @@ -4,6 +4,7 @@ import dateutil.easter from dateutil.easter import * from nicegui import ui, app, events +from nicegui.functions.navigate import navigate from nicegui.html import button from nicegui.events import KeyEventArguments @@ -69,6 +70,13 @@ def page_admin(): def update_userlist(): nonlocal userlist userlist = list_users() + # Benutzerliste um No Time Users bereinigen + users_to_remove = [ ] + for entry in userlist: + if entry in get_no_time_users_list(): + users_to_remove.append(entry) + for i in users_to_remove: + userlist.remove(i) update_userlist() @@ -763,19 +771,27 @@ def page_admin(): with ui.tab_panel(settings): with ui.grid(columns='auto auto'): with ui.card(): - ui.label("Administrationsbenutzer:").classes('text-bold') - with ui.grid(columns=2).classes('items-baseline'): + ui.label("Benutzereinstellungen:").classes('text-bold') + with ui.grid(columns=2).classes('items-center'): ui.label("Benutzer mit Administrationsrechten:") user_switch_list = [] - with ui.element(): + with ui.grid(columns=2).classes('gap-y-0'): for i in list_users(): user_switch_list.append(ui.switch(i)) for i in user_switch_list: if i.text in get_admin_list(): i.value = True secret = data["secret"] - + ui.separator().classes('col-span-2') + ui.label("Benutzer ohne Zeiterfassung:") + no_time_user_switch_list = [ ] + with ui.grid(columns=2).classes('gap-y-0'): + for i in list_users(): + no_time_user_switch_list.append(ui.switch(i)) + for item in no_time_user_switch_list: + if item.text in get_no_time_users_list(): + item.value = True with ui.card(): ui.label("Systemeinstellungen:").classes('text-bold') with ui.grid(columns=2).classes('items-baseline'): @@ -1064,7 +1080,17 @@ def page_admin(): dialog.open() else: + no_time_users = { } + no_time_users_counter = -1 + for i in no_time_user_switch_list: + if i.value == True: + no_time_users_counter += 1 + no_time_users[str(no_time_users_counter)] = i.text + + old_no_time_users_list = get_no_time_users_list() + write_adminsetting("admin_user", admin_users) + write_adminsetting("no_time_users", no_time_users) write_adminsetting("port", port.value) write_adminsetting("secret", secret) write_adminsetting("touchscreen", touchscreen_switch.value) @@ -1086,6 +1112,12 @@ def page_admin(): reset_visibility.value = False timetable.refresh() + if not set(old_no_time_users_list) == set(get_no_time_users_list()): + with ui.dialog() as infobox, ui.card(): + ui.label("Benutzer ohne Zeiterfassung wurden geändert. Die Seite wird neu geladen.") + ui.button("OK", on_click=ui.navigate.reload) + infobox.open() + with ui.button("Speichern", on_click=save_admin_settings): with ui.tooltip(): ui.label("Hiermit werden sämtliche oben gemachten Einstellungen gespeichert.\nGgf. müssen Sie die Seite neu laden um die Auswirkungen sichtbar zu machen.").style('white-space: pre-wrap') @@ -1109,8 +1141,6 @@ def page_admin(): else: delete_info.value = False delete_binder.value = True - print(delete_info.value) - print(delete_binder.value) api_key_input.value = current_user.api_key api_link_column.clear() @@ -1183,7 +1213,7 @@ def page_admin(): ui.label("Benutzername wurde geändert.").classes('text-bold') ui.label(f"Benutzerdaten werden in den neuen Ordner {username_input.value} verschoben.") ui.label("Sollen die Einstellungen gespeichert werden?") - with ui.row(): + with ui.row().classes('w-full justify-center'): ui.button("Speichern", on_click=save_settings) ui.button("Abbrechen", on_click=dialog.close) dialog.open() @@ -1236,9 +1266,9 @@ def page_admin(): with ui.dialog() as dialog, ui.card(): ui.label("Sollen die Änderungen an den Arbeitsstunden und/oder Urlaubstagen gespeichert werden?") - with ui.row(): + with ui.row().classes('justify-center w-full'): ui.button("Speichern", on_click=save_settings) - ui.button("Abrrechen", on_click=dialog.close) + ui.button("Abbrechen", on_click=dialog.close) dialog.open() def delete_workhour_entry(): @@ -1342,11 +1372,12 @@ def page_admin(): ui.button("Neu", on_click=new_api_key).tooltip("Neuen API-Schlüssel erzeugen. Wird erst beim Klick auf Speichern übernommen und entsprechende Links und QR-Codes aktualisiert") ui.label('Aufruf zum Stempeln:') global api_link_column - with ui.column().classes('gap-0') as api_link_column: - global stamp_link - stamp_link = [ ] - for i in app.urls: - stamp_link.append(ui.link(f'{i}/api/stamp/"API-Schüssel"')) + with ui.expansion("").classes('w-full'): + with ui.column().classes('gap-0') as api_link_column: + global stamp_link + stamp_link = [ ] + for i in app.urls: + stamp_link.append(ui.link(f'{i}/api/stamp/"API-Schüssel"')) ui.label("Administratoren können nicht gelöscht werden. Um das Konto zu löschen, müssen Sie ihm zuerst die Administrationsrechte entziehen.").bind_visibility_from(delete_info, 'value').classes('font-bold text-red') with ui.grid(columns=2): diff --git a/lib/definitions.py b/lib/definitions.py index 4b86e79..f2438fb 100644 --- a/lib/definitions.py +++ b/lib/definitions.py @@ -38,6 +38,7 @@ status_out = "ausgestempelt" standard_adminsettings = { "admin_user": { 0: "admin"}, + "no_time_users": { }, "port": "8090", "secret": "ftgzuhjikg,mt5jn46uzer8sfi9okrmtzjhndfierko5zltjhdgise", "times_on_touchscreen": True, diff --git a/lib/touchscreen.py b/lib/touchscreen.py index 9557552..a06beca 100644 --- a/lib/touchscreen.py +++ b/lib/touchscreen.py @@ -41,15 +41,15 @@ def page_touchscreen(): def set_columns(width): nonlocal number_of_columns if width > 1400: - number_of_columns = 6 - elif width > 1200: number_of_columns = 5 - elif width > 900: + elif width > 1200: number_of_columns = 4 - elif width > 750: + elif width > 900: number_of_columns = 3 - else: + elif width > 750: number_of_columns = 2 + else: + number_of_columns = 1 user_buttons.refresh() ui.on('resize', lambda e: set_columns(e.args['width'])) @@ -71,76 +71,77 @@ def page_touchscreen(): ''') - with ui.grid(columns=number_of_columns).classes('w-full center'): + with ui.grid(columns=number_of_columns).classes('w-full'): for name in userlist: current_user = user(name) - current_button = ui.button(on_click=lambda name=name: button_click(name)).classes(f'w-md h-full min-h-[{admin_settings["button_height"]}px]') + if not current_user.username in get_no_time_users_list(): + current_button = ui.button(on_click=lambda name=name: button_click(name)).classes(f'h-full min-h-[{admin_settings["button_height"]}px]') - with current_button: - with ui.grid(columns='1fr 1fr').classes('w-full h-full py-5 items-start'): + with current_button: + with ui.grid(columns='1fr 1fr').classes('w-full h-full py-5 items-start'): - if admin_settings["photos_on_touchscreen"]: - image_size = int(admin_settings["picture_height"]) - try: - with open(current_user.photofile, 'r') as file: - pass - ui.image(current_user.photofile).classes(f'max-h-[{image_size}px]').props('fit=scale-down') - except: - no_photo_svg = f''' - - - - - - - - - - - - - - - - - - ''' - ui.html(no_photo_svg) - with ui.column().classes('' if admin_settings["photos_on_touchscreen"] else 'col-span-2'): - ui.label(current_user.fullname).classes('text-left text-xl text.bold') - if admin_settings["times_on_touchscreen"]: - todays_timestamps = current_user.get_day_timestamps() - # Wenn wir Einträge haben - if len(todays_timestamps) > 0 and admin_settings["times_on_touchscreen"]: - table_string = "" - for i in range(0, len(todays_timestamps), 2): - try: - table_string += f"{datetime.datetime.fromtimestamp(todays_timestamps[i]).strftime('%H:%M')} - {datetime.datetime.fromtimestamp(todays_timestamps[i+1]).strftime('%H:%M')}" - except IndexError: - table_string += f"{datetime.datetime.fromtimestamp(todays_timestamps[i]).strftime('%H:%M')} -" - if i < len(todays_timestamps) - 2: - table_string += "\n" - ui.label(table_string).style('white-space: pre-wrap').classes('text-left') - if current_user.stamp_status() == status_in: - current_button.props('color=green') - else: - current_button.props('color=red') - buttons[name] = current_button + if admin_settings["photos_on_touchscreen"]: + image_size = int(admin_settings["picture_height"]) + try: + with open(current_user.photofile, 'r') as file: + pass + ui.image(current_user.photofile).classes(f'max-h-[{image_size}px]').props('fit=scale-down') + except: + no_photo_svg = f''' + + + + + + + + + + + + + + + + + + ''' + ui.html(no_photo_svg, sanitize=False) + with ui.column().classes('' if admin_settings["photos_on_touchscreen"] else 'col-span-2'): + ui.label(current_user.fullname).classes('text-left text-xl text.bold') + if admin_settings["times_on_touchscreen"]: + todays_timestamps = current_user.get_day_timestamps() + # Wenn wir Einträge haben + if len(todays_timestamps) > 0 and admin_settings["times_on_touchscreen"]: + table_string = "" + for i in range(0, len(todays_timestamps), 2): + try: + table_string += f"{datetime.datetime.fromtimestamp(todays_timestamps[i]).strftime('%H:%M')} - {datetime.datetime.fromtimestamp(todays_timestamps[i+1]).strftime('%H:%M')}" + except IndexError: + table_string += f"{datetime.datetime.fromtimestamp(todays_timestamps[i]).strftime('%H:%M')} -" + if i < len(todays_timestamps) - 2: + table_string += "\n" + ui.label(table_string).style('white-space: pre-wrap').classes('text-left') + if current_user.stamp_status() == status_in: + current_button.props('color=green') + else: + current_button.props('color=red') + buttons[name] = current_button user_buttons() else: diff --git a/lib/users.py b/lib/users.py index ae8b92a..345b440 100644 --- a/lib/users.py +++ b/lib/users.py @@ -583,3 +583,8 @@ def get_admin_list(): admin_list = load_adminsettings()["admin_user"] return admin_list.values() +def get_no_time_users_list(): + adnin_settings = load_adminsettings() + admin_list = load_adminsettings()["no_time_users"] + return admin_list.values() + diff --git a/lib/web_ui.py b/lib/web_ui.py index 816ac23..d14864a 100644 --- a/lib/web_ui.py +++ b/lib/web_ui.py @@ -41,20 +41,23 @@ class login_mask: if username.value in get_admin_list(): current_user = user(username.value) if hash_password(password.value) == current_user.password: - with ui.dialog() as forward_dialog, ui.card(): - ui.label("Wollen Sie den Administrationsbereich oder den Datenbereich aufrufen?") - def admin_area(): - app.storage.user['admin_authenticated'] = True - ui.navigate.to('/admin') - def time_area(): - app.storage.user['active_user'] = current_user.username - ui.navigate.to(self.target) - with ui.grid(columns=2): - ui.button("Administrationsbereich", on_click=admin_area) - ui.button("Datenbereich", on_click=time_area) + if not username.value in get_no_time_users_list(): + with ui.dialog() as forward_dialog, ui.card(): + ui.label("Wollen Sie den Administrationsbereich oder den Datenbereich aufrufen?") + def admin_area(): + app.storage.user['admin_authenticated'] = True + ui.navigate.to('/admin') + def time_area(): + app.storage.user['active_user'] = current_user.username + ui.navigate.to(self.target) + with ui.grid(columns=2).classes('justify-center w-full'): + ui.button("Administrationsbereich", on_click=admin_area) + ui.button("Datenbereich", on_click=time_area) - forward_dialog.open() - #ui.navigate.to("/admin") + forward_dialog.open() + else: + app.storage.user['admin_authenticated'] = True + ui.navigate.to("/admin") else: ui.notify("Login fehlgeschlagen") else: diff --git a/requirements.txt b/requirements.txt deleted file mode 100644 index 0f30e3d..0000000 --- a/requirements.txt +++ /dev/null @@ -1,131 +0,0 @@ -aiofiles==24.1.0 -aiohappyeyeballs==2.6.1 -aiohttp==3.11.16 -aiosignal==1.3.2 -altgraph==0.17.4 -annotated-types==0.7.0 -anyio==4.9.0 -attrs==25.3.0 -Babel==2.10.3 -beautifulsoup4==4.11.2 -bidict==0.23.1 -blinker==1.5 -Brlapi==0.8.4 -certifi==2025.1.31 -chardet==5.1.0 -charset-normalizer==3.0.1 -click==8.1.8 -crit==3.17.1 -cryptography==38.0.4 -cupshelpers==1.0 -dbus-python==1.3.2 -distro==1.8.0 -distro-info==1.5+deb12u1 -docker==5.0.3 -docker-compose==1.29.2 -dockerpty==0.4.1 -docopt==0.6.2 -docutils==0.21.2 -easygui==0.98.1 -fastapi==0.115.12 -feedgenerator==2.0.0 -frozenlist==1.5.0 -gitdb==4.0.9 -GitPython==3.1.30 -gpg==1.18.0 -h11==0.14.0 -html5lib==1.1 -httpcore==1.0.8 -httplib2==0.20.4 -httptools==0.6.4 -httpx==0.28.1 -idna==3.3 -ifaddr==0.2.0 -imaplib2==3.5 -invoke==2.0.0 -itsdangerous==2.2.0 -Jinja2==3.1.6 -jsonpointer==2.3 -jsonschema==4.10.3 -lazr.restfulclient==0.14.5 -lazr.uri==1.0.6 -louis==3.24.0 -lxml==4.9.2 -Mako==1.2.4.dev0 -Markdown==3.4.1 -markdown-it-py==2.1.0 -markdown2==2.5.3 -MarkupSafe==2.1.2 -mdurl==0.1.2 -menulibre==2.2.2 -multidict==6.4.3 -nicegui==2.15.0 -ntpsec==1.2.2 -numpy==2.2.5 -oauthlib==3.2.2 -olefile==0.46 -opencv-python==4.11.0.86 -orjson==3.10.16 -packaging==25.0 -pelican==4.8.0 -photocollage==1.4.5 -Pillow==9.4.0 -playsound3==3.2.3 -propcache==0.3.1 -protobuf==4.21.12 -pscript==0.7.7 -psutil==5.9.4 -pycairo==1.20.1 -pycups==2.0.1 -pydantic==2.11.3 -pydantic_core==2.33.1 -pyenchant==3.2.2 -Pygments==2.19.1 -PyGObject==3.42.2 -pygtkspellcheck==4.0.5 -pyinstaller==6.13.0 -pyinstaller-hooks-contrib==2025.4 -PyJWT==2.6.0 -pyparsing==3.0.9 -pyrsistent==0.18.1 -pysmbc==1.0.23 -python-apt==2.6.0 -python-dateutil==2.8.2 -python-dotenv==1.1.0 -python-engineio==4.12.0 -python-multipart==0.0.20 -python-socketio==5.13.0 -pytz==2022.7.1 -pyxdg==0.28 -PyYAML==6.0.2 -requests==2.32.3 -rfc3987==1.3.8 -rich==13.3.1 -roman==3.3 -segno==1.6.6 -simple-websocket==1.1.0 -six==1.16.0 -smmap==5.0.0 -sniffio==1.3.1 -soupsieve==2.3.2 -starlette==0.46.2 -texttable==1.6.7 -typing-inspection==0.4.0 -typing_extensions==4.13.2 -Unidecode==1.3.6 -uritemplate==4.1.1 -urllib3==2.4.0 -uvicorn==0.34.1 -uvloop==0.21.0 -vboxapi==1.0 -vbuild==0.8.2 -wadllib==1.3.6 -watchfiles==1.0.5 -webcolors==1.11.1 -webencodings==0.5.1 -websocket-client==1.2.3 -websockets==15.0.1 -wsproto==1.2.0 -wxPython==4.2.0 -xdg==5 -yarl==1.19.0 diff --git a/settings.json b/settings.json index 68532ee..7e32a9c 100644 --- a/settings.json +++ b/settings.json @@ -3,13 +3,16 @@ "0": "admin", "1": "admin2" }, + "no_time_users": { + "0": "admin" + }, "port": "8090", "secret": "ftgzuhjikg,mt5jn46uzer8sfi9okrmtzjhndfierko5zltjhdgise", "times_on_touchscreen": true, "photos_on_touchscreen": true, "touchscreen": true, "picture_height": 200, - "button_height": 300, + "button_height": "300", "user_notes": true, "vacation_application": true, "backup_folder": "/home/alexander/Dokumente/Python/Zeiterfassung/backup", diff --git a/settings/settings.json b/settings/settings.json new file mode 100644 index 0000000..f60cc13 --- /dev/null +++ b/settings/settings.json @@ -0,0 +1,17 @@ +{ + "admin_user": { + "0": "admin" + }, + "port": "8090", + "secret": "ftgzuhjikg,mt5jn46uzer8sfi9okrmtzjhndfierko5zltjhdgise", + "times_on_touchscreen": true, + "photos_on_touchscreen": true, + "touchscreen": true, + "picture_height": 200, + "button_height": 300, + "user_notes": true, + "vacation_application": true, + "backup_folder": "/backup", + "backup_api_key": "0b3b1d883b915bd2f05ee4d256d97a404df67a93", + "holidays": {} +} \ No newline at end of file