Aufholen Master Branch

This commit is contained in:
Alexander Malzkuhn 2025-10-30 14:12:34 +01:00
commit 0358ef44e4
12 changed files with 280 additions and 196 deletions

4
.gitignore vendored
View File

@ -5,6 +5,8 @@ Testplan.md
.venv .venv
users/ users/
backup/ backup/
settings/
Archiv/ Archiv/
Docker/ Docker/
docker-work/ docker-work/
Wiki/

View File

@ -1,14 +1,13 @@
FROM debian:latest FROM python:latest
RUN apt update && apt upgrade -y RUN apt update && apt upgrade -y
RUN apt install python3 python3-pip python3.11-venv locales -y RUN apt install locales -y
RUN mkdir /app RUN mkdir /app
RUN mkdir /.venv RUN mkdir /.venv
RUN mkdir /backup RUN mkdir /backup
RUN mkdir /settings RUN mkdir /settings
RUN python3 -m venv /.venv RUN pip install nicegui
RUN /.venv/bin/pip install nicegui RUN pip install segno
RUN /.venv/bin/pip install segno RUN pip install python-dateutil
RUN /.venv/bin/pip install python-dateutil
RUN sed -i '/de_DE.UTF-8/s/^# //g' /etc/locale.gen && \ RUN sed -i '/de_DE.UTF-8/s/^# //g' /etc/locale.gen && \
locale-gen locale-gen
@ -19,4 +18,4 @@ ENV LC_ALL de_DE.UTF-8
COPY main.py /app/main.py COPY main.py /app/main.py
COPY lib /app/lib/ COPY lib /app/lib/
EXPOSE 8090 EXPOSE 8090
ENTRYPOINT ["/.venv/bin/python", "/app/main.py"] ENTRYPOINT ["python", "/app/main.py"]

View File

@ -5,13 +5,13 @@ import os
server = 'gitea.am-td.de' server = 'gitea.am-td.de'
server_user = 'alexander' server_user = 'alexander'
if os.getuid() == 0: #if os.getuid() == 0:
subprocess.run(["docker", "build", "--force-rm", "-t", f"{server}/{server_user}/{app_title.lower()}:{app_version}", "."]) subprocess.run(["docker", "build", "--force-rm", "-t", f"{server}/{server_user}/{app_title.lower()}:{app_version}", "."])
if input("docker-compose erstellen j=JA ") == "j": if input("docker-compose erstellen j=JA ") == "j":
userfolder = input("Pfad für Benutzerdaten /users:") userfolder = input("Pfad für Benutzerdaten /users:")
backupfolder = input("Pfad für Backupdaten /backup:") backupfolder = input("Pfad für Backupdaten /backup:")
settingsfolder = input("Pfad für Einstellungen /settings:") settingsfolder = input("Pfad für Einstellungen /settings:")
docker_compose_content = f''' docker_compose_content = f'''
services: services:
zeiterfassung: zeiterfassung:
image: {server}/{server_user}/{app_title.lower()}:{app_version.lower()} image: {server}/{server_user}/{app_title.lower()}:{app_version.lower()}
@ -25,7 +25,7 @@ services:
- {backupfolder}:/backup - {backupfolder}:/backup
- {settingsfolder}:/settings''' - {settingsfolder}:/settings'''
with open('docker-compose.yml', 'w') as docker_compose: with open('docker-compose.yml', 'w') as docker_compose:
docker_compose.write(docker_compose_content) docker_compose.write(docker_compose_content)
else: #else:
print("Es werden Root-Rechte benötigt.") # print("Es werden Root-Rechte benötigt.")

View File

@ -1,13 +1,14 @@
services: services:
zeiterfassung: zeiterfassung:
image: gitea.am-td.de/alexander/zeiterfassung:beta-2025.0.1 image: gitea.am-td.de/alexander/zeiterfassung:beta-2025.0.2
restart: always restart: always
ports: ports:
- 8090:8090 - 8090:8090
environment: environment:
- PYTHONUNBUFFERED=1 - PYTHONUNBUFFERED=1
volumes: volumes:
- ./docker-work/users:/users - ./users:/users
- ./docker-work/backup:/backup - ./backup:/backup
- ./docker-work/settings:/settings - ./settings:/settings
- /etc/localtime:/etc/localtime:ro

View File

@ -4,6 +4,7 @@ import dateutil.easter
from dateutil.easter import * from dateutil.easter import *
from nicegui import ui, app, events from nicegui import ui, app, events
from nicegui.functions.navigate import navigate
from nicegui.html import button from nicegui.html import button
from nicegui.events import KeyEventArguments from nicegui.events import KeyEventArguments
@ -44,6 +45,10 @@ def page_admin():
updates_available = ValueBinder() updates_available = ValueBinder()
updates_available.value = False updates_available.value = False
delete_binder = ValueBinder()
delete_binder.value = True
delete_info = ValueBinder()
delete_info.value = False
enabled_because_not_docker = ValueBinder enabled_because_not_docker = ValueBinder
if is_docker(): if is_docker():
@ -65,6 +70,13 @@ def page_admin():
def update_userlist(): def update_userlist():
nonlocal userlist nonlocal userlist
userlist = list_users() 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() update_userlist()
@ -759,17 +771,27 @@ def page_admin():
with ui.tab_panel(settings): with ui.tab_panel(settings):
with ui.grid(columns='auto auto'): with ui.grid(columns='auto auto'):
with ui.card(): with ui.card():
ui.label("Administrationsbenutzer:").classes('text-bold') ui.label("Benutzereinstellungen:").classes('text-bold')
with ui.grid(columns=2).classes('items-baseline'): with ui.grid(columns=2).classes('items-center'):
ui.label("Benutzername des Adminstrators:")
admin_user = ui.input().tooltip("Geben Sie hier den Benutzernamen für den Adminstationsnutzer ein")
admin_user.value = data["admin_user"]
ui.label("Passwort des Administrators:")
admin_password = ui.input(password=True).tooltip("Geben Sie hier das Passwort für den Administationsnutzer ein. Merken Sie sich dieses Passwort gut. Es kann nicht über das Webinterface zurückgesetzt werden.")
ui.label("Benutzer mit Administrationsrechten:")
user_switch_list = []
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"] 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(): with ui.card():
ui.label("Systemeinstellungen:").classes('text-bold') ui.label("Systemeinstellungen:").classes('text-bold')
with ui.grid(columns=2).classes('items-baseline'): with ui.grid(columns=2).classes('items-baseline'):
@ -1045,31 +1067,56 @@ def page_admin():
holiday_section() holiday_section()
def save_admin_settings(): def save_admin_settings():
write_adminsetting("admin_user", admin_user.value) admin_users = { }
if admin_password.value != "": admin_counter = -1
write_adminsetting("admin_password", hash_password(admin_password.value)) for i in user_switch_list:
else: if i.value == True:
write_adminsetting("admin_password", data["admin_password"]) admin_counter += 1
write_adminsetting("port", port.value) admin_users[str(admin_counter)] = i.text
write_adminsetting("secret", secret) if len(admin_users) == 0:
write_adminsetting("touchscreen", touchscreen_switch.value)
write_adminsetting("times_on_touchscreen", timestamp_switch.value)
write_adminsetting("photos_on_touchscreen", photo_switch.value)
write_adminsetting("picture_height", picture_height_input.value)
write_adminsetting("button_height", button_height_input.value)
write_adminsetting("user_notes", notes_switch.value)
write_adminsetting("holidays", data["holidays"])
write_adminsetting("vacation_application", va_switch.value)
if int(old_port) != int(port.value):
with ui.dialog() as dialog, ui.card(): with ui.dialog() as dialog, ui.card():
ui.label( ui.label("Es wurde kein Administrationsbenutzer ausgewählt. Mindestens ein Benutzer muss Administrationsrechte haben.")
"Damit die Porteinstellungen wirksam werden, muss der Server neu gestartet werden.") ui.button("OK", on_click=dialog.close)
ui.button("OK", on_click=lambda: dialog.close())
dialog.open() dialog.open()
ui.notify("Einstellungen gespeichert") else:
reset_visibility.value = False
timetable.refresh() 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)
write_adminsetting("times_on_touchscreen", timestamp_switch.value)
write_adminsetting("photos_on_touchscreen", photo_switch.value)
write_adminsetting("picture_height", picture_height_input.value)
write_adminsetting("button_height", button_height_input.value)
write_adminsetting("user_notes", notes_switch.value)
write_adminsetting("holidays", data["holidays"])
write_adminsetting("vacation_application", va_switch.value)
if int(old_port) != int(port.value):
with ui.dialog() as dialog, ui.card():
ui.label(
"Damit die Porteinstellungen wirksam werden, muss der Server neu gestartet werden.")
ui.button("OK", on_click=lambda: dialog.close())
dialog.open()
ui.notify("Einstellungen gespeichert")
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.button("Speichern", on_click=save_admin_settings):
with ui.tooltip(): with ui.tooltip():
@ -1080,7 +1127,6 @@ def page_admin():
workhours = [ ] workhours = [ ]
with ui.row(): with ui.row():
def user_selection_changed(): def user_selection_changed():
try: try:
if user_selection.value != None: if user_selection.value != None:
@ -1089,7 +1135,12 @@ def page_admin():
fullname_input.value = current_user.fullname fullname_input.value = current_user.fullname
#password_input.value = current_user.password #password_input.value = current_user.password
usersettingscard.visible = True usersettingscard.visible = True
if current_user.username in get_admin_list():
delete_info.value = True
delete_binder.value = False
else:
delete_info.value = False
delete_binder.value = True
api_key_input.value = current_user.api_key api_key_input.value = current_user.api_key
api_link_column.clear() api_link_column.clear()
@ -1162,7 +1213,7 @@ def page_admin():
ui.label("Benutzername wurde geändert.").classes('text-bold') ui.label("Benutzername wurde geändert.").classes('text-bold')
ui.label(f"Benutzerdaten werden in den neuen Ordner {username_input.value} verschoben.") ui.label(f"Benutzerdaten werden in den neuen Ordner {username_input.value} verschoben.")
ui.label("Sollen die Einstellungen gespeichert werden?") 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("Speichern", on_click=save_settings)
ui.button("Abbrechen", on_click=dialog.close) ui.button("Abbrechen", on_click=dialog.close)
dialog.open() dialog.open()
@ -1215,9 +1266,9 @@ def page_admin():
with ui.dialog() as dialog, ui.card(): with ui.dialog() as dialog, ui.card():
ui.label("Sollen die Änderungen an den Arbeitsstunden und/oder Urlaubstagen gespeichert werden?") 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("Speichern", on_click=save_settings)
ui.button("Abrrechen", on_click=dialog.close) ui.button("Abbrechen", on_click=dialog.close)
dialog.open() dialog.open()
def delete_workhour_entry(): def delete_workhour_entry():
@ -1321,16 +1372,18 @@ 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.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:') ui.label('Aufruf zum Stempeln:')
global api_link_column global api_link_column
with ui.column().classes('gap-0') as api_link_column: with ui.expansion("").classes('w-full'):
global stamp_link with ui.column().classes('gap-0') as api_link_column:
stamp_link = [ ] global stamp_link
for i in app.urls: stamp_link = [ ]
stamp_link.append(ui.link(f'{i}/api/stamp/"API-Schüssel"')) 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): with ui.grid(columns=2):
ui.button("Speichern", on_click=save_user_settings).tooltip("Klicken Sie hier um die Änderungen zu speichern.") ui.button("Speichern", on_click=save_user_settings).tooltip("Klicken Sie hier um die Änderungen zu speichern.")
ui.button("Löschen", on_click=del_user) ui.button("Löschen", on_click=del_user).bind_enabled_from(delete_binder, 'value')
usersettings_card() usersettings_card()

View File

@ -6,7 +6,7 @@ from pathlib import Path
import hashlib import hashlib
app_title = "Zeiterfassung" app_title = "Zeiterfassung"
app_version = "beta-2025.0.1" app_version = "beta-2025.0.2"
# Standardpfade # Standardpfade
@ -36,8 +36,9 @@ status_out = "ausgestempelt"
# Standardadmin Settings: # Standardadmin Settings:
standard_adminsettings = { "admin_user": "admin", standard_adminsettings = { "admin_user": {
"admin_password": "8c6976e5b5410415bde908bd4dee15dfb167a9c873fc4bb8a81f6f2ab448a918", 0: "admin"},
"no_time_users": { },
"port": "8090", "port": "8090",
"secret": "ftgzuhjikg,mt5jn46uzer8sfi9okrmtzjhndfierko5zltjhdgise", "secret": "ftgzuhjikg,mt5jn46uzer8sfi9okrmtzjhndfierko5zltjhdgise",
"times_on_touchscreen": True, "times_on_touchscreen": True,
@ -55,9 +56,9 @@ standard_adminsettings = { "admin_user": "admin",
# Standard User Settings: # Standard User Settings:
standard_usersettings = { standard_usersettings = {
"username": "default", "username": "admin",
"fullname": "Standardbenutzer", "fullname": "Administrator",
"password": "37a8eec1ce19687d132fe29051dca629d164e2c4958ba141d5f4133a33f0688f", "password": "8c6976e5b5410415bde908bd4dee15dfb167a9c873fc4bb8a81f6f2ab448a918",
"api_key": "1234567890", "api_key": "1234567890",
"workhours": { } "workhours": { }
} }

View File

@ -1,41 +0,0 @@
from datetime import datetime
from nicegui import ui, app
from lib.web_ui import *
from lib.users import *
from lib.definitions import *
from calendar import monthrange
import hashlib
import calendar
import locale
@ui.page('/login')
def page_login():
# Settingsdatei einlesen
data = load_adminsettings()
def login():
nonlocal data
if username.value == data["admin_user"]:
print(f"Input Hash: {hash_password(password.value)} gespeichert: {data['admin_password']}")
if hash_password(password.value) == data["admin_password"]:
app.storage.user['authenticated'] = True
ui.navigate.to("/admin")
else:
ui.notify("Login fehlgeschlagen")
#ui.markdown(f"## {app_title} {app_version}")
#ui.markdown("Bitte einloggen")
pageheader("Bitte einloggen:")
with ui.grid(columns=2):
ui.markdown("Benutzer:")
username = ui.input('Benutzername')
ui.markdown("Passwort:")
password = ui.input('Passwort', password=True)
ui.button(text="Login", on_click=lambda: login())

View File

@ -41,15 +41,15 @@ def page_touchscreen():
def set_columns(width): def set_columns(width):
nonlocal number_of_columns nonlocal number_of_columns
if width > 1400: if width > 1400:
number_of_columns = 6
elif width > 1200:
number_of_columns = 5 number_of_columns = 5
elif width > 900: elif width > 1200:
number_of_columns = 4 number_of_columns = 4
elif width > 750: elif width > 900:
number_of_columns = 3 number_of_columns = 3
else: elif width > 750:
number_of_columns = 2 number_of_columns = 2
else:
number_of_columns = 1
user_buttons.refresh() user_buttons.refresh()
ui.on('resize', lambda e: set_columns(e.args['width'])) ui.on('resize', lambda e: set_columns(e.args['width']))
@ -71,76 +71,77 @@ def page_touchscreen():
</script> </script>
''') ''')
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: for name in userlist:
current_user = user(name) 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 current_button:
with ui.grid(columns='1fr 1fr').classes('w-full h-full py-5 items-start'): with ui.grid(columns='1fr 1fr').classes('w-full h-full py-5 items-start'):
if admin_settings["photos_on_touchscreen"]: if admin_settings["photos_on_touchscreen"]:
image_size = int(admin_settings["picture_height"]) image_size = int(admin_settings["picture_height"])
try: try:
with open(current_user.photofile, 'r') as file: with open(current_user.photofile, 'r') as file:
pass pass
ui.image(current_user.photofile).classes(f'max-h-[{image_size}px]').props('fit=scale-down') ui.image(current_user.photofile).classes(f'max-h-[{image_size}px]').props('fit=scale-down')
except: except:
no_photo_svg = f'''<?xml version="1.0" encoding="iso-8859-1"?> no_photo_svg = f'''<?xml version="1.0" encoding="iso-8859-1"?>
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools --> <!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
<svg height="{image_size/2}px" width="{image_size/2}px" version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" <svg height="{image_size/2}px" width="{image_size/2}px" version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"
viewBox="0 0 496.158 496.158" xml:space="preserve"> viewBox="0 0 496.158 496.158" xml:space="preserve">
<path style="fill:#D61E1E;" d="M248.082,0.003C111.07,0.003,0,111.063,0,248.085c0,137.001,111.07,248.07,248.082,248.07 <path style="fill:#D61E1E;" d="M248.082,0.003C111.07,0.003,0,111.063,0,248.085c0,137.001,111.07,248.07,248.082,248.07
c137.006,0,248.076-111.069,248.076-248.07C496.158,111.062,385.088,0.003,248.082,0.003z"/> c137.006,0,248.076-111.069,248.076-248.07C496.158,111.062,385.088,0.003,248.082,0.003z"/>
<path style="fill:#F4EDED;" d="M248.082,39.002C132.609,39.002,39,132.602,39,248.084c0,115.463,93.609,209.072,209.082,209.072 <path style="fill:#F4EDED;" d="M248.082,39.002C132.609,39.002,39,132.602,39,248.084c0,115.463,93.609,209.072,209.082,209.072
c115.467,0,209.076-93.609,209.076-209.072C457.158,132.602,363.549,39.002,248.082,39.002z"/> c115.467,0,209.076-93.609,209.076-209.072C457.158,132.602,363.549,39.002,248.082,39.002z"/>
<g> <g>
<path style="fill:#5B5147;" d="M145.23,144.237h-24.44c-3.21,0-5.819,4.741-5.819,10.605s2.609,10.611,5.819,10.611h24.44 <path style="fill:#5B5147;" d="M145.23,144.237h-24.44c-3.21,0-5.819,4.741-5.819,10.605s2.609,10.611,5.819,10.611h24.44
c3.217,0,5.826-4.747,5.826-10.611C151.057,148.978,148.447,144.237,145.23,144.237z"/> c3.217,0,5.826-4.747,5.826-10.611C151.057,148.978,148.447,144.237,145.23,144.237z"/>
<path style="fill:#5B5147;" d="M380.289,172.06H226.545c-2.025-9.851-9.416-17.176-18.244-17.176h-92.199 <path style="fill:#5B5147;" d="M380.289,172.06H226.545c-2.025-9.851-9.416-17.176-18.244-17.176h-92.199
c-10.403,0-18.818,10.125-18.818,22.592V328.9c0,10.254,8.314,18.581,18.58,18.581h264.425c10.262,0,18.586-8.327,18.586-18.581 c-10.403,0-18.818,10.125-18.818,22.592V328.9c0,10.254,8.314,18.581,18.58,18.581h264.425c10.262,0,18.586-8.327,18.586-18.581
V190.655C398.875,180.38,390.551,172.06,380.289,172.06z"/> V190.655C398.875,180.38,390.551,172.06,380.289,172.06z"/>
</g> </g>
<path style="fill:#F4EDED;" d="M248.076,166.711c-51.133,0-92.604,41.462-92.604,92.602c0,51.146,41.471,92.608,92.604,92.608 <path style="fill:#F4EDED;" d="M248.076,166.711c-51.133,0-92.604,41.462-92.604,92.602c0,51.146,41.471,92.608,92.604,92.608
c51.139,0,92.6-41.462,92.6-92.608C340.676,208.174,299.215,166.711,248.076,166.711z"/> c51.139,0,92.6-41.462,92.6-92.608C340.676,208.174,299.215,166.711,248.076,166.711z"/>
<path style="fill:#5B5147;" d="M248.086,171.416c-48.547,0-87.909,39.355-87.909,87.909c0,48.537,39.362,87.898,87.909,87.898 <path style="fill:#5B5147;" d="M248.086,171.416c-48.547,0-87.909,39.355-87.909,87.909c0,48.537,39.362,87.898,87.909,87.898
c48.543,0,87.896-39.361,87.896-87.898C335.981,210.771,296.629,171.416,248.086,171.416z"/> c48.543,0,87.896-39.361,87.896-87.898C335.981,210.771,296.629,171.416,248.086,171.416z"/>
<path style="fill:#F4EDED;" d="M248.611,205.005c-29.992,0-54.312,24.31-54.312,54.308c0,29.991,24.319,54.321,54.312,54.321 <path style="fill:#F4EDED;" d="M248.611,205.005c-29.992,0-54.312,24.31-54.312,54.308c0,29.991,24.319,54.321,54.312,54.321
s54.318-24.33,54.318-54.321C302.93,229.315,278.603,205.005,248.611,205.005z"/> s54.318-24.33,54.318-54.321C302.93,229.315,278.603,205.005,248.611,205.005z"/>
<path style="fill:#5B5147;" d="M248.611,209.528c-27.494,0-49.789,22.286-49.789,49.786c0,27.494,22.295,49.798,49.789,49.798 <path style="fill:#5B5147;" d="M248.611,209.528c-27.494,0-49.789,22.286-49.789,49.786c0,27.494,22.295,49.798,49.789,49.798
c27.496,0,49.795-22.304,49.795-49.798C298.406,231.814,276.107,209.528,248.611,209.528z"/> c27.496,0,49.795-22.304,49.795-49.798C298.406,231.814,276.107,209.528,248.611,209.528z"/>
<g> <g>
<path style="fill:#F4EDED;" d="M230.224,215.002c-14.401,0-26.065,11.674-26.065,26.067c0,14.399,11.664,26.073,26.065,26.073 <path style="fill:#F4EDED;" d="M230.224,215.002c-14.401,0-26.065,11.674-26.065,26.067c0,14.399,11.664,26.073,26.065,26.073
c14.391,0,26.065-11.674,26.065-26.073C256.289,226.676,244.614,215.002,230.224,215.002z"/> c14.391,0,26.065-11.674,26.065-26.073C256.289,226.676,244.614,215.002,230.224,215.002z"/>
<path style="fill:#F4EDED;" d="M159.698,165.453h-45.712c-3.756,0-6.805,3.045-6.805,6.792v25.594c0,3.04,2.004,5.575,4.756,6.448 <path style="fill:#F4EDED;" d="M159.698,165.453h-45.712c-3.756,0-6.805,3.045-6.805,6.792v25.594c0,3.04,2.004,5.575,4.756,6.448
c0.65,0.209,1.328,0.35,2.049,0.35h45.712c3.76,0,6.793-3.04,6.793-6.798v-25.594C166.491,168.498,163.458,165.453,159.698,165.453 c0.65,0.209,1.328,0.35,2.049,0.35h45.712c3.76,0,6.793-3.04,6.793-6.798v-25.594C166.491,168.498,163.458,165.453,159.698,165.453
z"/> z"/>
</g> </g>
<path style="fill:#D61E1E;" d="M85.85,60.394c-9.086,7.86-17.596,16.37-25.456,25.456l349.914,349.914 <path style="fill:#D61E1E;" d="M85.85,60.394c-9.086,7.86-17.596,16.37-25.456,25.456l349.914,349.914
c9.086-7.861,17.596-16.37,25.456-25.456L85.85,60.394z"/> c9.086-7.861,17.596-16.37,25.456-25.456L85.85,60.394z"/>
</svg>''' </svg>'''
ui.html(no_photo_svg) ui.html(no_photo_svg, sanitize=False)
with ui.column().classes('' if admin_settings["photos_on_touchscreen"] else 'col-span-2'): 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') ui.label(current_user.fullname).classes('text-left text-xl text.bold')
if admin_settings["times_on_touchscreen"]: if admin_settings["times_on_touchscreen"]:
todays_timestamps = current_user.get_day_timestamps() todays_timestamps = current_user.get_day_timestamps()
# Wenn wir Einträge haben # Wenn wir Einträge haben
if len(todays_timestamps) > 0 and admin_settings["times_on_touchscreen"]: if len(todays_timestamps) > 0 and admin_settings["times_on_touchscreen"]:
table_string = "" table_string = ""
for i in range(0, len(todays_timestamps), 2): for i in range(0, len(todays_timestamps), 2):
try: try:
table_string += f"{datetime.datetime.fromtimestamp(todays_timestamps[i]).strftime('%H:%M')} - {datetime.datetime.fromtimestamp(todays_timestamps[i+1]).strftime('%H:%M')}" table_string += f"{datetime.datetime.fromtimestamp(todays_timestamps[i]).strftime('%H:%M')} - {datetime.datetime.fromtimestamp(todays_timestamps[i+1]).strftime('%H:%M')}"
except IndexError: except IndexError:
table_string += f"{datetime.datetime.fromtimestamp(todays_timestamps[i]).strftime('%H:%M')} -" table_string += f"{datetime.datetime.fromtimestamp(todays_timestamps[i]).strftime('%H:%M')} -"
if i < len(todays_timestamps) - 2: if i < len(todays_timestamps) - 2:
table_string += "\n" table_string += "\n"
ui.label(table_string).style('white-space: pre-wrap').classes('text-left') ui.label(table_string).style('white-space: pre-wrap').classes('text-left')
if current_user.stamp_status() == status_in: if current_user.stamp_status() == status_in:
current_button.props('color=green') current_button.props('color=green')
else: else:
current_button.props('color=red') current_button.props('color=red')
buttons[name] = current_button buttons[name] = current_button
user_buttons() user_buttons()
else: else:

View File

@ -578,3 +578,13 @@ def write_adminsetting(key: str, value):
except KeyError: except KeyError:
print(f"Kein Einstellungsschlüssel {key} vorhanden.") print(f"Kein Einstellungsschlüssel {key} vorhanden.")
def get_admin_list():
adnin_settings = load_adminsettings()
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()

View File

@ -38,10 +38,26 @@ class login_mask:
def login(): def login():
nonlocal data nonlocal data
if username.value == data["admin_user"]: if username.value in get_admin_list():
if hash_password(password.value) == data["admin_password"]: current_user = user(username.value)
app.storage.user['admin_authenticated'] = True if hash_password(password.value) == current_user.password:
ui.navigate.to("/admin") 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()
else:
app.storage.user['admin_authenticated'] = True
ui.navigate.to("/admin")
else: else:
ui.notify("Login fehlgeschlagen") ui.notify("Login fehlgeschlagen")
else: else:

38
main.py
View File

@ -4,7 +4,6 @@ import os.path
from lib.web_ui import * from lib.web_ui import *
from lib.admin import * from lib.admin import *
from lib.login import *
from lib.users import * from lib.users import *
from lib.touchscreen import * from lib.touchscreen import *
from lib.definitions import * from lib.definitions import *
@ -33,7 +32,7 @@ def main():
list_users() list_users()
homepage() #homepage()
def startup_message(): def startup_message():
@ -54,7 +53,7 @@ def main():
ui.toggle.default_props('rounded') ui.toggle.default_props('rounded')
ui.row.default_classes('items-baseline') ui.row.default_classes('items-baseline')
ui.run(favicon='', port=port, storage_secret=secret, language='de-DE', show_welcome_message=False) ui.run(root=homepage, favicon='', port=port, storage_secret=secret, language='de-DE', show_welcome_message=False)
if __name__ in ("__main__", "__mp_main__"): if __name__ in ("__main__", "__mp_main__"):
parser = argparse.ArgumentParser(description=f'{app_title} {app_version}') parser = argparse.ArgumentParser(description=f'{app_title} {app_version}')
@ -85,11 +84,34 @@ if __name__ in ("__main__", "__mp_main__"):
print("Sollen diese Einstellungen übernommen werden? j=Ja") print("Sollen diese Einstellungen übernommen werden? j=Ja")
question = input() question = input()
if question == "j": if question == "j":
admin_settings["admin_user"] = admin_user if not os.path.exists(userfolder):
admin_settings["admin_password"] = hash_password(admin_password) os.makedirs(userfolder)
json_dict = json.dumps(admin_settings, indent=4) print("Kein Ordner mit Benutzerdaten gefunden. Lege ihn an.")
with open(os.path.join(scriptpath, usersettingsfilename), "w") as outputfile: if not os.path.exists(os.path.join(userfolder, admin_user)):
outputfile.write(json_dict) print("Benutzer existiert noch nicht. Lege ihn an.")
os.makedirs(os.path.join(userfolder, admin_user))
start_date_dt = datetime.datetime.now()
start_date = start_date_dt.strftime("%Y-%m-%d")
settings_to_write = standard_usersettings
settings_to_write["workhours"][start_date] = {}
settings_to_write["fullname"] = "Administrator"
settings_to_write["username"] = admin_user
# API-Key erzeugen
string_to_hash = f'{admin_user}_{datetime.datetime.now().timestamp()}'
hash_string = hashlib.shake_256(bytes(string_to_hash, 'utf-8')).hexdigest(20)
settings_to_write["api_key"] = hash_string
for i in range(1, 8):
settings_to_write["workhours"][start_date][str(i)] = 0
settings_to_write["workhours"][start_date]["vacation"] = 0
with open(f"{userfolder}/{admin_user}/{usersettingsfilename}", 'w') as json_file:
json_dict = json.dumps(standard_usersettings, indent=4)
json_file.write(json_dict)
current_user = user(admin_user)
current_user.password = hash_password(admin_password)
current_user.write_settings()
admin_users_list = load_adminsettings()["admin_user"]
admin_users_list[str(len(admin_users_list))] = admin_user
write_adminsetting("admin_user", admin_users_list)
print("Daten geschrieben") print("Daten geschrieben")
quit() quit()
else: else:

20
settings.json Normal file
View File

@ -0,0 +1,20 @@
{
"admin_user": {
"0": "admin"
},
"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",
"user_notes": true,
"vacation_application": true,
"backup_folder": "/home/alexander/Dokumente/Python/Zeiterfassung/backup",
"backup_api_key": "6fed93dc4a35308b2c073a8a6f3284afe1fb9946",
"holidays": {}
}